From 33ef399dee7a31a28008ebae3c35d5941188d855 Mon Sep 17 00:00:00 2001 From: oveddan Date: Thu, 13 Aug 2026 22:14:16 -0600 Subject: [PATCH 1/6] feat: publish the CLI as the mft-config npm command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing this tool meant cloning the repo, installing dependencies, building, and copying a skill directory by hand — five steps and a Node toolchain before touching a knob. Every documented example read `node dist/cli.js`. Publish the package instead: - rename `@djtechtools/mft-export` to `mft-config` and drop `private` - ship both `mft-config` and a deprecated `mft-export` alias as bins - restrict the tarball to `dist/` and `schema/` - add a `prepack` that builds and tests from a clean checkout - rename the CLI in usage output *and* the error prefix Install is now `npm install -g mft-config`, or no install at all with `npx -y mft-config`. The skill and docs reference the published command, and the skill install is a single curl that fetches only the skill directory — no clone, no build. Add CI, which did not exist. It packs the artifact and installs it into a clean directory on Linux, macOS, and Windows rather than only invoking `dist/cli.js` in the source tree. `--help` is not treated as a smoke test: it returns before the dynamic import of `./midi.js`, so it never loads the native addon and would miss a broken prebuild. The matrix loads the addon explicitly and runs an offline `plan` against a synthetic fixture, which needs no attached hardware. The fixture is synthetic on purpose — a real export carries the unit ID and the complete mapping, which the docs treat as private device data. Removing "run from the repository root" also removes what kept the `.mft-state/` journal in one place, so the skill and README now state the working-directory rule explicitly. The underlying fix, a stable state root, is tracked separately. Refs #3 Co-Authored-By: Claude Opus 5 --- .claude/skills/mft-configurator/SKILL.md | 41 +- .../mft-configurator/references/settings.md | 6 +- .github/workflows/ci.yml | 93 + README.md | 85 +- docs/agent-skill.md | 67 +- package.json | 24 +- src/cli.ts | 10 +- test/fixtures/synthetic-four-bank.json | 3609 +++++++++++++++++ 8 files changed, 3857 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 test/fixtures/synthetic-four-bank.json diff --git a/.claude/skills/mft-configurator/SKILL.md b/.claude/skills/mft-configurator/SKILL.md index 783d37c..ecc74dc 100644 --- a/.claude/skills/mft-configurator/SKILL.md +++ b/.claude/skills/mft-configurator/SKILL.md @@ -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 ` when the default 500 ms is too short. @@ -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 @@ -65,8 +78,8 @@ Plans expire after 15 minutes. If the plan expired or the device may have change 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. ```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 ``` Preserve these invariants: diff --git a/.claude/skills/mft-configurator/references/settings.md b/.claude/skills/mft-configurator/references/settings.md index 5a23e22..d4930c0 100644 --- a/.claude/skills/mft-configurator/references/settings.md +++ b/.claude/skills/mft-configurator/references/settings.md @@ -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 \ @@ -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 @@ -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 \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d0a198d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + name: Build and test + runs-on: ubuntu-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 + - 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. + # Load it explicitly and open a port count. + - name: Native MIDI addon loads + shell: bash + working-directory: ${{ runner.temp }}/smoke + run: | + node -e " + import('@julusian/midi').then((m) => { + const count = new m.default.Input().getPortCount(); + console.log('midi addon loaded; input ports:', count); + }).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. diff --git a/README.md b/README.md index 017bb9b..81a7970 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,15 @@ Underneath, the agent is driving the CLI documented below. Nothing is hidden — work by hand looks like this: ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +mft-config plan \ --snapshot twister-config.json \ --set bank.1.encoder.1.colors.active=green \ --set bank.1.encoder.1.colors.inactive=purple \ --out patch-plan.json -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` ## What this is not @@ -130,46 +130,45 @@ bootloader commands are always blocked. Live writes currently require firmware ## Install -Requirements: - -- Node.js 20 or newer -- a native MIDI toolchain supported by `@julusian/midi` -- on Linux, the ALSA development package required by RtMidi +Requires Node.js 20 or newer. Prebuilt MIDI binaries ship for macOS, Windows, +and Linux, so no compiler is needed on common platforms. ```sh -git clone https://github.com/oveddan/mft-api.git -cd mft-api -pnpm install -pnpm run check +npm install -g mft-config ``` -Build the CLI after making changes: +Or run it without installing anything: ```sh -pnpm run build +npx -y mft-config list ``` -The examples below use `node dist/cli.js`. After packaging or linking the -binary, the equivalent command name is `mft-export`. +Every example below uses `mft-config`. The old `mft-export` name still works as +a deprecated alias and will be removed in a future release. + +**Run every command from the same directory.** `mft-config` writes its backups +and its single-use plan journal to `.mft-state/` relative to the current working +directory, so switching directories between `plan` and `apply` consults a +different journal. Pick a directory and stay in it. ## Read the controller List connected Twisters: ```sh -node dist/cli.js list +mft-config list ``` Export the only connected device: ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json ``` If more than one Twister is connected, use the index shown by `list`: ```sh -node dist/cli.js export --device 1 --out twister-config.json +mft-config export --device 1 --out twister-config.json ``` The JSON includes the firmware and unit identity, detected bank count, all @@ -321,9 +320,9 @@ minutes and are bound to the snapshot hash, firmware version, and device ID. ### Make the top row green when active and purple when inactive ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +mft-config plan \ --snapshot twister-config.json \ --set bank.1.encoder.1.colors.active=green \ --set bank.1.encoder.1.colors.inactive=purple \ @@ -335,7 +334,7 @@ node dist/cli.js plan \ --set bank.1.encoder.4.colors.inactive=purple \ --out patch-plan.json -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` ### Make the first two push switches toggle on and off @@ -343,15 +342,15 @@ node dist/cli.js apply --plan patch-plan.json --yes Code `1` is `ccToggle`: ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +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 -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` ### Change a knob's rotary and push MIDI mappings @@ -360,9 +359,9 @@ This makes bank 2, encoder 5 send CC 20 on channel 3 when turned, and a momentary CC 40 on channel 4 when pressed: ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +mft-config plan \ --snapshot twister-config.json \ --set bank.2.encoder.5.encoder.type.code=1 \ --set bank.2.encoder.5.encoder.midiChannel=3 \ @@ -372,15 +371,15 @@ node dist/cli.js plan \ --set bank.2.encoder.5.switch.midiNumber=40 \ --out patch-plan.json -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` ### Set a velocity-sensitive spread indicator with a red detent ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +mft-config plan \ --snapshot twister-config.json \ --set bank.3.encoder.9.movement.code=2 \ --set bank.3.encoder.9.indicator.code=3 \ @@ -388,15 +387,15 @@ node dist/cli.js plan \ --set bank.3.encoder.9.detent.color=red \ --out patch-plan.json -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` ### Change global brightness and sleep behavior ```sh -node dist/cli.js export --out twister-config.json +mft-config export --out twister-config.json -node dist/cli.js plan \ +mft-config plan \ --snapshot twister-config.json \ --set global.brightness.rgb=96 \ --set global.brightness.indicator=80 \ @@ -404,7 +403,7 @@ node dist/cli.js plan \ --set global.sleep.animation.code=0 \ --out patch-plan.json -node dist/cli.js apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes ``` Timeout index `4` means 10 minutes; sleep animation code `0` turns the lights @@ -447,6 +446,24 @@ encoder-tag probe and then reads all 64 or 128 encoder records sequentially. - Restore is not exposed as a command yet. Backups are retained under `.mft-state/backups/` for a future confirmed restore workflow. +## Develop + +Working on the tool itself, rather than using it: + +```sh +git clone https://github.com/oveddan/mft-api.git +cd mft-api +pnpm install +pnpm run check +``` + +`pnpm run build` compiles to `dist/`, and `node dist/cli.js` is then equivalent +to the installed `mft-config` command. `pnpm pack` produces the publishable +tarball and runs the full check first. + +On Linux you need the ALSA development package required by RtMidi if you are +building the native dependency from source rather than using its prebuilds. + ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file diff --git a/docs/agent-skill.md b/docs/agent-skill.md index 6c24268..5d1ee51 100644 --- a/docs/agent-skill.md +++ b/docs/agent-skill.md @@ -1,30 +1,59 @@ # Agent skill installation -The canonical `mft-configurator` skill lives at -[`../.claude/skills/mft-configurator`](../.claude/skills/mft-configurator). -Claude Code discovers it automatically when working in this checkout. Its -`SKILL.md` uses the portable Agent Skills format understood by both Claude Code -and Codex. The optional `agents/openai.yaml` file, generated with Codex's skill -creator, adds Codex UI metadata; Claude Code ignores it. +The `mft-configurator` skill lets Claude Code or Codex inspect and configure a +MIDI Fighter Twister in plain language. It drives the `mft-config` CLI; it never +talks to the device directly. + +## 1. Install the CLI + +```sh +npm install -g mft-config +``` + +The skill also works with `npx -y mft-config` if you would rather not install +anything globally. + +## 2. Install the skill -Install it globally for Claude Code when it should be available outside this -checkout: +No clone and no build — this fetches only the skill directory. + +For Claude Code: ```sh -mkdir -p ~/.claude/skills -cp -R .claude/skills/mft-configurator ~/.claude/skills/ +mkdir -p ~/.claude/skills/mft-configurator && curl -fsSL https://github.com/oveddan/mft-api/archive/refs/heads/main.tar.gz | tar -xz --strip-components=4 -C ~/.claude/skills/mft-configurator mft-api-main/.claude/skills/mft-configurator ``` -Install a copy for Codex: +For Codex, the same command with a different destination: ```sh -mkdir -p ~/.codex/skills -cp -R .claude/skills/mft-configurator ~/.codex/skills/ +mkdir -p ~/.codex/skills/mft-configurator && curl -fsSL https://github.com/oveddan/mft-api/archive/refs/heads/main.tar.gz | tar -xz --strip-components=4 -C ~/.codex/skills/mft-configurator mft-api-main/.claude/skills/mft-configurator ``` -Replace an older installed copy before upgrading so files removed upstream do -not linger. After installation, invoke `/mft-configurator` in Claude Code or -`$mft-configurator` in Codex, or ask the agent to inspect or configure a MIDI -Fighter Twister. The skill defaults to discovery and export. It requires a -separate, explicit instruction before applying a reviewed configuration plan to -the controller. +Both commands overwrite in place, so re-running them upgrades. Delete the +destination directory first if you want files removed upstream to disappear too. + +Working inside a checkout of this repository, Claude Code discovers the skill +automatically and neither step 2 command is needed. + +## 3. Use it + +Invoke `/mft-configurator` in Claude Code or `$mft-configurator` in Codex, or +just ask the agent to inspect or configure a Twister. + +The skill defaults to discovery and export. Applying a configuration plan to the +controller requires a separate, explicit instruction. + +## Notes + +`SKILL.md` uses the portable Agent Skills format understood by both Claude Code +and Codex. The optional `agents/openai.yaml`, generated with Codex's skill +creator, adds Codex UI metadata; Claude Code ignores it. + +**Run the agent from one consistent working directory.** `mft-config` writes its +backups and its single-use plan journal to `.mft-state/` relative to the current +working directory, so an `apply` run from a different directory than an earlier +one consults a different journal. + +A Claude Code plugin that installs the skill and an MCP server in a single step +is planned; see [#8](https://github.com/oveddan/mft-api/issues/8) and +[#12](https://github.com/oveddan/mft-api/issues/12). diff --git a/package.json b/package.json index 75f2aa7..90ed4cf 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,36 @@ { - "name": "@djtechtools/mft-export", + "name": "mft-config", "version": "0.1.0", - "private": true, "license": "MIT", "type": "module", "description": "Guarded MIDI Fighter Twister configuration exporter and planner", + "keywords": [ + "midi-fighter-twister", + "midi", + "sysex", + "djtechtools", + "cli" + ], + "homepage": "https://github.com/oveddan/mft-api#readme", + "bugs": "https://github.com/oveddan/mft-api/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/oveddan/mft-api.git" + }, "packageManager": "pnpm@10.33.2", "bin": { + "mft-config": "./dist/cli.js", "mft-export": "./dist/cli.js" }, + "files": [ + "dist", + "schema" + ], "scripts": { "build": "tsc -p tsconfig.json", "test": "tsx --test test/*.test.ts", - "check": "tsc -p tsconfig.json && tsx --test test/*.test.ts" + "check": "tsc -p tsconfig.json && tsx --test test/*.test.ts", + "prepack": "pnpm run check" }, "engines": { "node": ">=20" diff --git a/src/cli.ts b/src/cli.ts index 9d2701d..f725c7d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,10 +22,10 @@ interface Arguments { function usage(): string { return `Usage: - mft-export list [--timeout ] - mft-export export [--device ] [--out ] [--timeout ] - mft-export plan --snapshot --set [--set ] [--out ] - mft-export apply --plan --yes [--device ] + mft-config list [--timeout ] + mft-config export [--device ] [--out ] [--timeout ] + mft-config plan --snapshot --set [--set ] [--out ] + mft-config apply --plan --yes [--device ] This tool only sends Universal Identity, global pull (0x02), encoder bulk-pull (0x04/0x01), and device-ID pull (0x05) messages. The plan command is offline. @@ -186,6 +186,6 @@ async function main(): Promise { } main().catch((error: Error) => { - process.stderr.write(`mft-export: ${error.message}\n`); + process.stderr.write(`mft-config: ${error.message}\n`); process.exitCode = 1; }); diff --git a/test/fixtures/synthetic-four-bank.json b/test/fixtures/synthetic-four-bank.json new file mode 100644 index 0000000..6ddc2b0 --- /dev/null +++ b/test/fixtures/synthetic-four-bank.json @@ -0,0 +1,3609 @@ +{ + "schemaVersion": "djtt.mft.config-export.v1", + "capturedAt": "2026-01-01T00:00:00.000Z", + "device": { + "manufacturer": "DJ TechTools", + "manufacturerId": [ + 0, + 1, + 121 + ], + "familyId": 5, + "modelId": 1, + "firmware": { + "date": "2026-07-02", + "identityBytes": [ + 32, + 38, + 7, + 2 + ] + }, + "unitId": { + "source": "synthetic", + "hex": "0000000000000000" + }, + "midiPorts": { + "input": "Midi Fighter Twister", + "output": "Midi Fighter Twister" + }, + "inputPort": "Synthetic Twister", + "outputPort": "Synthetic Twister" + }, + "capabilities": { + "bankCount": 4, + "encodersPerBank": 16 + }, + "globals": { + "midiChannel": 4, + "sideButtonsBanked": true, + "sideButtons": [ + { + "number": 1, + "action": { + "code": 1, + "name": "ccToggle" + } + }, + { + "number": 2, + "action": { + "code": 7, + "name": "shiftPage2Toggle" + } + }, + { + "number": 3, + "action": { + "code": 1, + "name": "ccToggle" + } + }, + { + "number": 4, + "action": { + "code": 1, + "name": "ccToggle" + } + }, + { + "number": 5, + "action": { + "code": 6, + "name": "shiftPage1Toggle" + } + }, + { + "number": 6, + "action": { + "code": 1, + "name": "ccToggle" + } + } + ], + "superKnob": { + "start": 63, + "end": 127 + }, + "brightness": { + "rgb": 127, + "indicator": 127 + }, + "colorMap": { + "code": 0, + "name": "classic" + }, + "animationChannels": { + "encoder": 6, + "switch": 3 + }, + "sleep": { + "timeoutIndex": 7, + "timeoutMinutes": 60, + "animation": { + "code": 1, + "name": "rainbowWave" + } + }, + "bankAnimationsEnabled": false, + "rawTags": { + "0": 4, + "1": 1, + "2": 1, + "3": 7, + "4": 1, + "5": 1, + "6": 6, + "7": 1, + "8": 63, + "9": 127, + "31": 127, + "32": 127, + "33": 0, + "34": 6, + "35": 3, + "36": 7, + "37": 1, + "38": 0 + } + }, + "banks": [ + { + "number": 1, + "encoders": [ + { + "number": 1, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 1, + "name": "ccToggle" + }, + "midiChannel": 2, + "midiNumber": 0 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 0, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 43, + "inactive": 107 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 0, + "15": 0, + "16": 1, + "17": 0, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 2, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 1, + "name": "ccToggle" + }, + "midiChannel": 2, + "midiNumber": 1 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 1, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 43, + "inactive": 107 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 1, + "15": 0, + "16": 1, + "17": 1, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 3, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 2 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 2, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 43, + "inactive": 107 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 2, + "15": 0, + "16": 1, + "17": 2, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 4, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 3 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 3, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 43, + "inactive": 107 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 3, + "15": 0, + "16": 1, + "17": 3, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 5, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 4 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 4, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 5 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 4, + "15": 0, + "16": 1, + "17": 4, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 6, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 5 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 5, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 5 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 5, + "15": 0, + "16": 1, + "17": 5, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 7, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 6 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 6, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 5 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 6, + "15": 0, + "16": 1, + "17": 6, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 8, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 7 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 7, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 5 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 7, + "15": 0, + "16": 1, + "17": 7, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 9, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 1, + "name": "ccToggle" + }, + "midiChannel": 2, + "midiNumber": 8 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 8, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 64 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 8, + "15": 0, + "16": 1, + "17": 8, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 10, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 9 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 9, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 64 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 9, + "15": 0, + "16": 1, + "17": 9, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 11, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 10 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 10, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 64 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 10, + "15": 0, + "16": 1, + "17": 10, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 12, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 11 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 11, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 64 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 11, + "15": 0, + "16": 1, + "17": 11, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 13, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 12 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 12, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 85 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 12, + "15": 0, + "16": 1, + "17": 12, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 14, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 13 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 13, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 127, + "inactive": 85 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 13, + "15": 0, + "16": 1, + "17": 13, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 15, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 14 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 14, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 64 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 14, + "15": 0, + "16": 1, + "17": 14, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 16, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 15 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 15, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 5 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 15, + "15": 0, + "16": 1, + "17": 15, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + } + ] + }, + { + "number": 2, + "encoders": [ + { + "number": 1, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 16 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 16, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 62 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 16, + "15": 0, + "16": 1, + "17": 16, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 2, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 17 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 17, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 17, + "15": 0, + "16": 1, + "17": 17, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 3, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 18 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 18, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 18, + "15": 0, + "16": 1, + "17": 18, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 4, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 19 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 19, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 19, + "15": 0, + "16": 1, + "17": 19, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 5, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 20 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 20, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 20, + "15": 0, + "16": 1, + "17": 20, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 6, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 21 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 21, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 21, + "15": 0, + "16": 1, + "17": 21, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 7, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 22 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 22, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 22, + "15": 0, + "16": 1, + "17": 22, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 8, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 23 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 23, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 23, + "15": 0, + "16": 1, + "17": 23, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 9, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 24 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 24, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 24, + "15": 0, + "16": 1, + "17": 24, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 10, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 25 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 25, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 25, + "15": 0, + "16": 1, + "17": 25, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 11, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 26 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 26, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 26, + "15": 0, + "16": 1, + "17": 26, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 12, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 27 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 27, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 27, + "15": 0, + "16": 1, + "17": 27, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 13, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 28 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 28, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 28, + "15": 0, + "16": 1, + "17": 28, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 14, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 29 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 29, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 29, + "15": 0, + "16": 1, + "17": 29, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 15, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 30 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 30, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 30, + "15": 0, + "16": 1, + "17": 30, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 16, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 31 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 31, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 84 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 31, + "15": 0, + "16": 1, + "17": 31, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + } + ] + }, + { + "number": 3, + "encoders": [ + { + "number": 1, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 32 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 32, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 32, + "15": 0, + "16": 1, + "17": 32, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 2, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 33 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 33, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 33, + "15": 0, + "16": 1, + "17": 33, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 3, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 34 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 34, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 34, + "15": 0, + "16": 1, + "17": 34, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 4, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 35 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 35, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 35, + "15": 0, + "16": 1, + "17": 35, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 5, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 36 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 36, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 36, + "15": 0, + "16": 1, + "17": 36, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 6, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 37 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 37, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 37, + "15": 0, + "16": 1, + "17": 37, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 7, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 38 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 38, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 38, + "15": 0, + "16": 1, + "17": 38, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 8, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 39 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 39, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 39, + "15": 0, + "16": 1, + "17": 39, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 9, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 40 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 40, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 40, + "15": 0, + "16": 1, + "17": 40, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 10, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 41 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 41, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 41, + "15": 0, + "16": 1, + "17": 41, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 11, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 42 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 42, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 42, + "15": 0, + "16": 1, + "17": 42, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 12, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 43 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 43, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 43, + "15": 0, + "16": 1, + "17": 43, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 13, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 44 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 44, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 44, + "15": 0, + "16": 1, + "17": 44, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 14, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 45 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 45, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 45, + "15": 0, + "16": 1, + "17": 45, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 15, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 46 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 46, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 46, + "15": 0, + "16": 1, + "17": 46, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 16, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 47 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 47, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 53 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 47, + "15": 0, + "16": 1, + "17": 47, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + } + ] + }, + { + "number": 4, + "encoders": [ + { + "number": 1, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 48 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 48, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 48, + "15": 0, + "16": 1, + "17": 48, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 2, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 49 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 49, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 49, + "15": 0, + "16": 1, + "17": 49, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 3, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 50 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 50, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 50, + "15": 0, + "16": 1, + "17": 50, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 4, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 51 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 51, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 51, + "15": 0, + "16": 1, + "17": 51, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 5, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 52 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 52, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 52, + "15": 0, + "16": 1, + "17": 52, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 6, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 53 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 53, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 53, + "15": 0, + "16": 1, + "17": 53, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 7, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 54 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 54, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 54, + "15": 0, + "16": 1, + "17": 54, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 8, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 55 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 55, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 55, + "15": 0, + "16": 1, + "17": 55, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 9, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 56 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 56, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 56, + "15": 0, + "16": 1, + "17": 56, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 10, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 57 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 57, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 57, + "15": 0, + "16": 1, + "17": 57, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 11, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 58 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 58, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 58, + "15": 0, + "16": 1, + "17": 58, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 12, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 59 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 59, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 59, + "15": 0, + "16": 1, + "17": 59, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 13, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 60 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 60, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 60, + "15": 0, + "16": 1, + "17": 60, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 14, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 61 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 61, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 61, + "15": 0, + "16": 1, + "17": 61, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 15, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 62 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 62, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 62, + "15": 0, + "16": 1, + "17": 62, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + }, + { + "number": 16, + "detent": { + "enabled": false, + "color": 63 + }, + "movement": { + "code": 0, + "name": "direct" + }, + "switch": { + "action": { + "code": 0, + "name": "ccHold" + }, + "midiChannel": 2, + "midiNumber": 63 + }, + "encoder": { + "midiChannel": 1, + "shiftedMidiChannel": 5, + "midiNumber": 63, + "type": { + "code": 1, + "name": "cc" + } + }, + "colors": { + "active": 25, + "inactive": 13 + }, + "indicator": { + "code": 2, + "name": "blendedBar" + }, + "superKnobEnabled": false, + "rawTags": { + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 63, + "15": 0, + "16": 1, + "17": 63, + "18": 0, + "19": 43, + "20": 0, + "21": 64, + "22": 0, + "23": 0, + "24": 5 + } + } + ] + } + ], + "warnings": [] +} From a2f61c6691066e5685456320d2426eda70f4c0b7 Mon Sep 17 00:00:00 2001 From: oveddan Date: Thu, 13 Aug 2026 22:19:42 -0600 Subject: [PATCH 2/6] fix: derive the CI fixture from the shipped normalizers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the previous commit found the synthetic fixture was only half synthetic. It was produced by rewriting `rawTags` on a real export, which left every normalized field holding the operator's actual mapping — the exact private device data the fixture existed to avoid shipping — and left the two halves disagreeing in 188 places. Generate it instead: `test/fixtures/generate-synthetic.ts` builds each record from deterministic tag values and runs it through `normalizeEncoder` and `normalizeGlobals`, so raw and normalized cannot drift. Reviewing a 50-line generator beats reviewing 3,600 lines of JSON. The scan now reports zero mismatches and the unit ID is all zeros. The first attempt at that generator was cast with `as ConfigExport`, which hid a missing `capabilities` field; the CLI then failed at runtime with "Cannot read properties of undefined". Dropping the cast for a plain type annotation surfaces it at compile time — but only if the file is actually type-checked, and `tsconfig.json` covers `src/**` alone because `rootDir` is `src` and tests must not land in `dist`. So add `tsconfig.test.json` and wire it into `check`. Verified by deleting `capabilities` again and confirming `check` now fails with TS2741. All existing test sources were already clean under it. Also make `prepack` call `npm run check` rather than `pnpm run check`, so `npm publish` from an environment without pnpm does not fail at pack time. Refs #3 Co-Authored-By: Claude Opus 5 --- package.json | 4 +- test/fixtures/generate-synthetic.ts | 82 ++ test/fixtures/synthetic-four-bank.json | 1366 ++++++++++++------------ tsconfig.test.json | 12 + 4 files changed, 778 insertions(+), 686 deletions(-) create mode 100644 test/fixtures/generate-synthetic.ts create mode 100644 tsconfig.test.json diff --git a/package.json b/package.json index 90ed4cf..59b6942 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "scripts": { "build": "tsc -p tsconfig.json", "test": "tsx --test test/*.test.ts", - "check": "tsc -p tsconfig.json && tsx --test test/*.test.ts", - "prepack": "pnpm run check" + "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsx --test test/*.test.ts", + "prepack": "npm run check" }, "engines": { "node": ">=20" diff --git a/test/fixtures/generate-synthetic.ts b/test/fixtures/generate-synthetic.ts new file mode 100644 index 0000000..6fb20d8 --- /dev/null +++ b/test/fixtures/generate-synthetic.ts @@ -0,0 +1,82 @@ +// Regenerates test/fixtures/synthetic-four-bank.json: +// +// npx tsx test/fixtures/generate-synthetic.ts +// +// The fixture is fully synthetic on purpose. A real export carries the unit ID +// and the operator's complete knob mapping, which the README and the skill both +// treat as private device data, so none of it belongs in the repository. +// +// Every record is built from deterministic raw tag values and then passed +// through the shipped normalizers, so the raw and normalized halves of the +// fixture cannot drift apart. Do not hand-edit the JSON. + +import { writeFileSync } from "node:fs"; + +import { normalizeEncoder, normalizeGlobals, rawTags, type ConfigExport } from "../../src/model.js"; + +const firmwareDate = "2026-07-02"; +const warnings: string[] = []; + +const globalValues = new Map([ + [0, 1], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], + [8, 0], [9, 127], [31, 127], [32, 127], + [33, 0], [34, 1], [35, 1], [36, 0], [37, 0], [38, 1], +]); + +// Distinct MIDI numbers per encoder keep the fixture useful for planning +// against a specific knob; every other field stays at a neutral default. +function encoderValues(index: number): Map { + return new Map([ + [10, 0], // detent.enabled + [11, 0], // movement + [12, 0], // switch.action + [13, 1], // switch.midiChannel + [14, index % 128], // switch.midiNumber + [15, 0], // deprecated; the normalizer warns on any other value + [16, 1], // encoder.midiChannel + [17, index % 128], // encoder.midiNumber + [18, 0], // encoder.type + [19, 43], // colors.active — green in the classic palette + [20, 0], // colors.inactive + [21, 64], // detent.color + [22, 0], // indicator + [23, 0], // superKnobEnabled + [24, 1], // encoder.shiftedMidiChannel + ]); +} + +let counter = 0; +const banks = [1, 2, 3, 4].map((number) => ({ + number, + encoders: Array.from({ length: 16 }, (_, slot) => { + const values = encoderValues(counter); + counter += 1; + return { ...normalizeEncoder(values, slot + 1, firmwareDate, warnings), rawTags: rawTags(values) }; + }), +})); + +// Deliberately not cast with `as ConfigExport`: the annotation is what catches +// a missing field such as `capabilities`, which a cast would silently allow +// through into a fixture that then fails only at CLI runtime. +const config: ConfigExport = { + schemaVersion: "djtt.mft.config-export.v1", + capturedAt: "2026-01-01T00:00:00.000Z", + device: { + manufacturer: "DJ TechTools", + manufacturerId: [0, 1, 121], + familyId: 5, + modelId: 1, + firmware: { date: firmwareDate, identityBytes: [32, 38, 7, 2] }, + // An all-zero identifier is obviously not a real unit, while still being a + // strong identity so plans built from this fixture are apply-eligible. + unitId: { source: "sysex-0x05", hex: "0000000000000000" }, + midiPorts: { input: "Synthetic Twister", output: "Synthetic Twister" }, + }, + capabilities: { bankCount: 4, encodersPerBank: 16 }, + globals: { ...normalizeGlobals(globalValues, warnings), rawTags: rawTags(globalValues) }, + banks, + warnings, +}; + +writeFileSync(new URL("synthetic-four-bank.json", import.meta.url), `${JSON.stringify(config, null, 2)}\n`); +process.stdout.write(`Wrote synthetic-four-bank.json: ${banks.length} banks, ${counter} encoders, ${warnings.length} warnings\n`); diff --git a/test/fixtures/synthetic-four-bank.json b/test/fixtures/synthetic-four-bank.json index 6ddc2b0..4693602 100644 --- a/test/fixtures/synthetic-four-bank.json +++ b/test/fixtures/synthetic-four-bank.json @@ -20,69 +20,67 @@ ] }, "unitId": { - "source": "synthetic", + "source": "sysex-0x05", "hex": "0000000000000000" }, "midiPorts": { - "input": "Midi Fighter Twister", - "output": "Midi Fighter Twister" - }, - "inputPort": "Synthetic Twister", - "outputPort": "Synthetic Twister" + "input": "Synthetic Twister", + "output": "Synthetic Twister" + } }, "capabilities": { "bankCount": 4, "encodersPerBank": 16 }, "globals": { - "midiChannel": 4, - "sideButtonsBanked": true, + "midiChannel": 1, + "sideButtonsBanked": false, "sideButtons": [ { "number": 1, "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" } }, { "number": 2, "action": { - "code": 7, - "name": "shiftPage2Toggle" + "code": 0, + "name": "ccHold" } }, { "number": 3, "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" } }, { "number": 4, "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" } }, { "number": 5, "action": { - "code": 6, - "name": "shiftPage1Toggle" + "code": 0, + "name": "ccHold" } }, { "number": 6, "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" } } ], "superKnob": { - "start": 63, + "start": 0, "end": 127 }, "brightness": { @@ -94,37 +92,37 @@ "name": "classic" }, "animationChannels": { - "encoder": 6, - "switch": 3 + "encoder": 1, + "switch": 1 }, "sleep": { - "timeoutIndex": 7, - "timeoutMinutes": 60, + "timeoutIndex": 0, + "timeoutMinutes": 0, "animation": { - "code": 1, - "name": "rainbowWave" + "code": 0, + "name": "lightsOff" } }, - "bankAnimationsEnabled": false, + "bankAnimationsEnabled": true, "rawTags": { - "0": 4, - "1": 1, - "2": 1, - "3": 7, - "4": 1, - "5": 1, - "6": 6, - "7": 1, - "8": 63, + "0": 1, + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, "9": 127, "31": 127, "32": 127, "33": 0, - "34": 6, - "35": 3, - "36": 7, - "37": 1, - "38": 0 + "34": 1, + "35": 1, + "36": 0, + "37": 0, + "38": 1 } }, "banks": [ @@ -135,7 +133,7 @@ "number": 1, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -143,28 +141,28 @@ }, "switch": { "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 0 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 0, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { "active": 43, - "inactive": 107 + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -182,14 +180,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 2, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -197,28 +195,28 @@ }, "switch": { "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 1 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 1, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { "active": 43, - "inactive": 107 + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -236,14 +234,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 3, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -254,25 +252,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 2 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 2, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { "active": 43, - "inactive": 107 + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -290,14 +288,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 4, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -308,25 +306,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 3 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 3, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { "active": 43, - "inactive": 107 + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -344,14 +342,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 5, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -362,25 +360,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 4 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 4, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 5 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -398,14 +396,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 6, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -416,25 +414,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 5 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 5, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 5 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -452,14 +450,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 7, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -470,25 +468,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 6 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 6, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 5 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -506,14 +504,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 8, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -524,25 +522,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 7 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 7, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 5 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -560,14 +558,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 9, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -575,28 +573,28 @@ }, "switch": { "action": { - "code": 1, - "name": "ccToggle" + "code": 0, + "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 8 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 8, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 64 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -614,14 +612,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 10, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -632,25 +630,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 9 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 9, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 64 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -668,14 +666,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 11, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -686,25 +684,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 10 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 10, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 64 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -722,14 +720,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 12, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -740,25 +738,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 11 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 11, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 64 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -776,14 +774,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 13, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -794,25 +792,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 12 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 12, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 85 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -830,14 +828,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 14, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -848,25 +846,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 13 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 13, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 127, - "inactive": 85 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -884,14 +882,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 15, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -902,25 +900,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 14 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 14, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 64 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -938,14 +936,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 16, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -956,25 +954,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 15 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 15, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 5 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -992,7 +990,7 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } } ] @@ -1004,7 +1002,7 @@ "number": 1, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1015,25 +1013,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 16 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 16, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 62 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1051,14 +1049,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 2, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1069,25 +1067,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 17 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 17, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1105,14 +1103,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 3, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1123,25 +1121,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 18 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 18, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1159,14 +1157,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 4, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1177,25 +1175,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 19 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 19, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1213,14 +1211,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 5, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1231,25 +1229,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 20 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 20, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1267,14 +1265,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 6, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1285,25 +1283,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 21 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 21, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1321,14 +1319,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 7, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1339,25 +1337,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 22 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 22, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1375,14 +1373,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 8, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1393,25 +1391,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 23 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 23, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1429,14 +1427,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 9, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1447,25 +1445,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 24 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 24, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1483,14 +1481,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 10, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1501,25 +1499,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 25 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 25, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1537,14 +1535,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 11, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1555,25 +1553,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 26 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 26, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1591,14 +1589,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 12, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1609,25 +1607,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 27 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 27, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1645,14 +1643,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 13, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1663,25 +1661,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 28 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 28, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1699,14 +1697,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 14, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1717,25 +1715,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 29 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 29, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1753,14 +1751,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 15, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1771,25 +1769,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 30 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 30, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1807,14 +1805,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 16, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1825,25 +1823,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 31 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 31, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 84 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1861,7 +1859,7 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } } ] @@ -1873,7 +1871,7 @@ "number": 1, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1884,25 +1882,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 32 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 32, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1920,14 +1918,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 2, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1938,25 +1936,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 33 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 33, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -1974,14 +1972,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 3, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -1992,25 +1990,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 34 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 34, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2028,14 +2026,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 4, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2046,25 +2044,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 35 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 35, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2082,14 +2080,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 5, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2100,25 +2098,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 36 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 36, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2136,14 +2134,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 6, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2154,25 +2152,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 37 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 37, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2190,14 +2188,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 7, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2208,25 +2206,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 38 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 38, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2244,14 +2242,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 8, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2262,25 +2260,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 39 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 39, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2298,14 +2296,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 9, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2316,25 +2314,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 40 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 40, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2352,14 +2350,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 10, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2370,25 +2368,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 41 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 41, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2406,14 +2404,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 11, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2424,25 +2422,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 42 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 42, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2460,14 +2458,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 12, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2478,25 +2476,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 43 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 43, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2514,14 +2512,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 13, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2532,25 +2530,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 44 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 44, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2568,14 +2566,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 14, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2586,25 +2584,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 45 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 45, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2622,14 +2620,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 15, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2640,25 +2638,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 46 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 46, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2676,14 +2674,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 16, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2694,25 +2692,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 47 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 47, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 53 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2730,7 +2728,7 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } } ] @@ -2742,7 +2740,7 @@ "number": 1, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2753,25 +2751,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 48 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 48, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2789,14 +2787,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 2, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2807,25 +2805,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 49 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 49, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2843,14 +2841,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 3, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2861,25 +2859,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 50 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 50, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2897,14 +2895,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 4, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2915,25 +2913,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 51 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 51, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -2951,14 +2949,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 5, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -2969,25 +2967,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 52 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 52, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3005,14 +3003,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 6, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3023,25 +3021,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 53 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 53, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3059,14 +3057,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 7, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3077,25 +3075,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 54 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 54, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3113,14 +3111,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 8, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3131,25 +3129,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 55 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 55, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3167,14 +3165,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 9, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3185,25 +3183,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 56 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 56, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3221,14 +3219,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 10, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3239,25 +3237,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 57 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 57, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3275,14 +3273,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 11, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3293,25 +3291,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 58 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 58, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3329,14 +3327,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 12, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3347,25 +3345,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 59 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 59, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3383,14 +3381,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 13, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3401,25 +3399,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 60 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 60, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3437,14 +3435,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 14, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3455,25 +3453,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 61 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 61, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3491,14 +3489,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 15, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3509,25 +3507,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 62 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 62, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3545,14 +3543,14 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } }, { "number": 16, "detent": { "enabled": false, - "color": 63 + "color": 64 }, "movement": { "code": 0, @@ -3563,25 +3561,25 @@ "code": 0, "name": "ccHold" }, - "midiChannel": 2, + "midiChannel": 1, "midiNumber": 63 }, "encoder": { "midiChannel": 1, - "shiftedMidiChannel": 5, + "shiftedMidiChannel": 1, "midiNumber": 63, "type": { - "code": 1, - "name": "cc" + "code": 0, + "name": "note" } }, "colors": { - "active": 25, - "inactive": 13 + "active": 43, + "inactive": 0 }, "indicator": { - "code": 2, - "name": "blendedBar" + "code": 0, + "name": "dot" }, "superKnobEnabled": false, "rawTags": { @@ -3599,7 +3597,7 @@ "21": 64, "22": 0, "23": 0, - "24": 5 + "24": 1 } } ] diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..6a05644 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,12 @@ +// Type-checks the test sources, which the build config deliberately excludes +// because `rootDir` is `src` and they must not be emitted into `dist`. Without +// this, a fixture generator could construct a malformed ConfigExport and only +// fail at CLI runtime. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} From 9efcc03a8969eabae7a6d48026384117548cbb92 Mon Sep 17 00:00:00 2001 From: oveddan Date: Thu, 13 Aug 2026 22:25:37 -0600 Subject: [PATCH 3/6] fix: make CI pass on Linux and Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new matrix job failed on both platforms on its first run. macOS, the only one I could exercise locally, passed — which is the argument for having the matrix. **Linux.** The addon check constructed an `Input` to read a port count. That needs a working MIDI backend, and hosted Linux runners have no ALSA sequencer (`/dev/snd/seq` is absent), so it failed with "Failed to initialise RtMidi" for a reason unrelated to whether the package shipped correctly. Call `verifyLibraryLoaded()` instead: it forces the prebuilt `.node` to be resolved and dlopen'd, which is the thing actually worth testing, without opening a sequencer client. **Windows.** `tsx --test test/*.test.ts` relies on the shell expanding the glob. pwsh does not, so tsx received the literal pattern and reported "Could not find 'test\*.test.ts'". Quoting it makes tsx do the expansion itself, which works identically on all three platforms. That second one is a pre-existing bug in the test script rather than anything this branch introduced — it was simply unreachable until there was CI running on Windows to reach it. Refs #3 Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 13 ++++++++++--- package.json | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0a198d..574831e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,15 +58,22 @@ jobs: # `--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. - # Load it explicitly and open a port count. + # + # 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 count = new m.default.Input().getPortCount(); - console.log('midi addon loaded; input ports:', count); + 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); }); " diff --git a/package.json b/package.json index 59b6942..a1c87c3 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "test": "tsx --test test/*.test.ts", - "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsx --test test/*.test.ts", + "test": "tsx --test \"test/*.test.ts\"", + "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsx --test \"test/*.test.ts\"", "prepack": "npm run check" }, "engines": { From 16dd26c27b454f06156e8193fd72d7e1dc4b67e8 Mon Sep 17 00:00:00 2001 From: oveddan Date: Thu, 13 Aug 2026 22:28:26 -0600 Subject: [PATCH 4/6] fix: run the test suite via an explicit file list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt quoted the glob so tsx would expand it. That works on Node 21 and newer, where the test runner learned to expand globs itself. It does not work on Node 20, which is this package's declared floor and what CI runs, so `check` then failed on Linux too. I verified that fix on Node 24 and concluded it worked. My shell had expanded the glob, not tsx. Verifying against the version in `engines` was the step that would have caught it. Enumerate the files in `test/run-tests.mjs` instead. No glob, no shell involvement, identical on every platform and version — and it exits non-zero when it finds nothing, so a renamed test file cannot leave CI green with an empty suite. Confirmed on Node 20.20.2 and Node 24.15.0: 18 passing on both. Also run the check job across Node 20 and 24. A version-specific bug in the test runner is exactly what just slipped through, and the floor in `engines` is not optional coverage. Refs #3 Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 10 ++++++++-- package.json | 4 ++-- test/run-tests.mjs | 29 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 test/run-tests.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 574831e..e426464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,20 @@ on: jobs: check: - name: Build and test + 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: 20 + node-version: ${{ matrix.node }} cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run check diff --git a/package.json b/package.json index a1c87c3..b27dcc1 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "test": "tsx --test \"test/*.test.ts\"", - "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsx --test \"test/*.test.ts\"", + "test": "node test/run-tests.mjs", + "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && node test/run-tests.mjs", "prepack": "npm run check" }, "engines": { diff --git a/test/run-tests.mjs b/test/run-tests.mjs new file mode 100644 index 0000000..c264310 --- /dev/null +++ b/test/run-tests.mjs @@ -0,0 +1,29 @@ +// Runs the test suite on every supported Node version and platform. +// +// `tsx --test test/*.test.ts` cannot do that on its own: +// - Windows shells do not expand globs, and npm hands the script text to +// cmd, so tsx receives the literal pattern. +// - Quoting it only helps on Node 21 and newer, where the test runner +// learned to expand globs itself. This package supports Node 20. +// +// So enumerate the files here instead, and exit non-zero if there are none — +// otherwise renaming a test file would leave CI green with nothing running. + +import { spawnSync } from "node:child_process"; +import { readdirSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const testDirectory = fileURLToPath(new URL(".", import.meta.url)); +const files = readdirSync(testDirectory) + .filter((name) => name.endsWith(".test.ts")) + .sort() + .map((name) => join("test", name)); + +if (files.length === 0) { + process.stderr.write("No test files matching *.test.ts were found in test/\n"); + process.exit(1); +} + +const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...files], { stdio: "inherit" }); +process.exit(result.status ?? 1); From d3b29ee422cdb8cb38ccde235ae8a0d9afa144ad Mon Sep 17 00:00:00 2001 From: oveddan Date: Fri, 14 Aug 2026 14:25:44 -0600 Subject: [PATCH 5/6] feat: withhold apply until the write-path defects are fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review of the plan made a fair objection to shipping this package with a working `apply`. The defects in #14 predate this work, but the reasoning that they were therefore "status quo, not a regression" was too convenient. Before publishing, operating the device meant working inside a checkout. After, `npx mft-config apply` from an arbitrary directory is the ordinary path — which is exactly the condition that makes the cwd-relative journal (#14 item 0d) bite, and puts a write-capable tool one command away for any agent. Publishing did not add the defects; it removed the friction containing them. So publish the read path and withhold the write. `list`, `export`, and `plan` are untouched, and they are where the install pain this project set out to fix actually lived. `apply` refuses with a message naming both defects and pointing at the issue. The gate runs before device discovery, so a refused apply never opens a MIDI port. `MFT_UNSAFE_APPLY=1` lifts it, so this project can still exercise its own write path against real hardware. It is documented under Develop and deliberately not in the agent skill; the skill instead tells the agent to plan as usual, report the plan, and say the write is unavailable rather than to go looking for a way around it. Add test/cli.test.ts — the CLI had no tests at all, and an untested safety control is one that stops working quietly. Four cases: apply refuses on the gate rather than incidentally on a missing file, the gate precedes discovery, help documents the state, and the offline commands are not caught by it. Verified by deleting the gate and watching the second case fail with "Device index 99 does not exist" instead of the gate message. Refs #3, #14 Co-Authored-By: Claude Opus 5 --- .claude/skills/mft-configurator/SKILL.md | 13 ++++-- README.md | 32 +++++++++++--- src/cli.ts | 39 +++++++++++++++-- test/cli.test.ts | 55 ++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 test/cli.test.ts diff --git a/.claude/skills/mft-configurator/SKILL.md b/.claude/skills/mft-configurator/SKILL.md index ecc74dc..62a641c 100644 --- a/.claude/skills/mft-configurator/SKILL.md +++ b/.claude/skills/mft-configurator/SKILL.md @@ -73,15 +73,22 @@ mft-config 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 mft-config list --timeout 1500 -mft-config apply --plan patch-plan.json --yes --device 0 --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. diff --git a/README.md b/README.md index 81a7970..7a3ccff 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ mft-config plan \ --set bank.1.encoder.1.colors.inactive=purple \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` ## What this is not @@ -128,6 +128,17 @@ Only `apply` can write configuration. System commands, reset commands, and bootloader commands are always blocked. Live writes currently require firmware `2026-07-02` and a strong command-`0x05` device identity. +> **`apply` is disabled in the current release.** Two defects in the write path +> ([#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, which +> bypasses the firmware allowlist; and `.mft-state` — the backups and the +> single-use plan journal — resolves against the current working directory, so +> applying from a different directory consults a different journal. That second +> one became easy to hit once the CLI could be installed and run from anywhere. +> +> `list`, `export`, and `plan` are unaffected and are the whole read path. To +> write settings meanwhile, use the vendor MIDI Fighter Utility. + ## Install Requires Node.js 20 or newer. Prebuilt MIDI binaries ship for macOS, Windows, @@ -317,6 +328,10 @@ Changes use a deliberate three-step workflow: export a fresh snapshot, create and inspect an offline plan, then explicitly apply it. Plans expire after 15 minutes and are bound to the snapshot hash, firmware version, and device ID. +**The third step is disabled in this release** — see the note under [What this +tool does](#what-this-tool-does). The recipes below still work through `plan`, +which is offline and never opens a MIDI port; only the final `apply` refuses. + ### Make the top row green when active and purple when inactive ```sh @@ -334,7 +349,7 @@ mft-config plan \ --set bank.1.encoder.4.colors.inactive=purple \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` ### Make the first two push switches toggle on and off @@ -350,7 +365,7 @@ mft-config plan \ --set bank.1.encoder.2.switch.action.code=1 \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` ### Change a knob's rotary and push MIDI mappings @@ -371,7 +386,7 @@ mft-config plan \ --set bank.2.encoder.5.switch.midiNumber=40 \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` ### Set a velocity-sensitive spread indicator with a red detent @@ -387,7 +402,7 @@ mft-config plan \ --set bank.3.encoder.9.detent.color=red \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` ### Change global brightness and sleep behavior @@ -403,7 +418,7 @@ mft-config plan \ --set global.sleep.animation.code=0 \ --out patch-plan.json -mft-config apply --plan patch-plan.json --yes +mft-config apply --plan patch-plan.json --yes # disabled: see #14 ``` Timeout index `4` means 10 minutes; sleep animation code `0` turns the lights @@ -461,6 +476,11 @@ pnpm run check to the installed `mft-config` command. `pnpm pack` produces the publishable tarball and runs the full check first. +`MFT_UNSAFE_APPLY=1` lifts the `apply` block so the write path can be exercised +against real hardware while [#14](https://github.com/oveddan/mft-api/issues/14) +is open. It is for developing this tool, not for getting work done — the defects +it steps around are real, and it is deliberately absent from the agent skill. + On Linux you need the ALSA development package required by RtMidi if you are building the native dependency from source rather than using its prebuilds. diff --git a/src/cli.ts b/src/cli.ts index f725c7d..820a04c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -25,12 +25,14 @@ function usage(): string { mft-config list [--timeout ] mft-config export [--device ] [--out ] [--timeout ] mft-config plan --snapshot --set [--set ] [--out ] - mft-config apply --plan --yes [--device ] + mft-config apply --plan --yes [--device ] (disabled) This tool only sends Universal Identity, global pull (0x02), encoder bulk-pull (0x04/0x01), and device-ID pull (0x05) messages. The plan command is offline. -Only apply can write settings, and it requires an eligible content-addressed plan -plus explicit --yes confirmation.`; + +apply is the only command that writes settings, and it is disabled in this +release while the write-path defects in issue #14 are open. list, export, and +plan are unaffected.`; } function parseArguments(argv: string[]): Arguments { @@ -81,6 +83,35 @@ function parseArguments(argv: string[]): Arguments { return result; } +// Live writes are withheld from the published package until the apply-boundary +// defects in issue #14 are fixed. Reading and planning are unaffected. +// +// Publishing to npm is what makes this worth gating rather than merely +// tracking: before, operating the device meant working inside a checkout, and +// now `npx mft-config apply` from any directory is the ordinary path. That +// removes the friction that was containing a forgeable eligibility flag and a +// journal whose location depends on the current directory. +// +// The escape hatch exists so this project can still exercise its own write path +// against real hardware. It is deliberately not documented in the agent skill. +function assertApplyEnabled(): void { + if (process.env.MFT_UNSAFE_APPLY === "1") return; + throw new Error( + [ + "apply is disabled in this release while the write-path defects in", + "https://github.com/oveddan/mft-api/issues/14 are open.", + "", + "Specifically: a plan file edited to set applyEligibility.eligible=true keeps a", + "valid planId, which bypasses the firmware allowlist; and .mft-state (backups and", + "the single-use plan journal) resolves against the current working directory, so", + "applying from a different directory consults a different journal.", + "", + "list, export, and plan are unaffected. To write settings meanwhile, use the", + "vendor MIDI Fighter Utility.", + ].join("\n"), + ); +} + function parseSet(expression: string): PatchInput { const separator = expression.indexOf("="); if (separator <= 0) throw new Error(`Invalid --set expression ${expression}; expected path=value`); @@ -106,6 +137,8 @@ async function main(): Promise { process.stdout.write(`${usage()}\n`); return; } + // Before discovery, so a disabled apply never opens a MIDI port. + if (args.command === "apply") assertApplyEnabled(); if (args.command === "plan") { const snapshot = JSON.parse(await readFile(resolve(args.snapshot!), "utf8")) as ConfigExport; diff --git a/test/cli.test.ts b/test/cli.test.ts new file mode 100644 index 0000000..9407b72 --- /dev/null +++ b/test/cli.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +// The apply gate is a safety control with no other coverage, and the kind of +// thing that silently stops working. Drive the real CLI rather than a unit, +// since what matters is that the shipped entry point refuses. +const cli = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); + +function runCli(args: string[], env: NodeJS.ProcessEnv = {}): { status: number | null; stderr: string; stdout: string } { + const result = spawnSync(process.execPath, ["--import", "tsx", cli, ...args], { + encoding: "utf8", + env: { ...process.env, MFT_UNSAFE_APPLY: "", ...env }, + }); + return { status: result.status, stderr: result.stderr, stdout: result.stdout }; +} + +test("apply refuses while the issue 14 write-path defects are open", () => { + const result = runCli(["apply", "--plan", "nonexistent-plan.json", "--yes"]); + + assert.equal(result.status, 1); + assert.match(result.stderr, /apply is disabled/); + assert.match(result.stderr, /issues\/14/); + // It must refuse on the gate, not incidentally because the plan file is + // missing — otherwise a real plan file would sail straight through. + assert.doesNotMatch(result.stderr, /ENOENT/); +}); + +test("the gate fires before any MIDI port is opened", () => { + // A device index far past any real port count: reaching discovery at all + // would surface a device error instead of the gate message. + const result = runCli(["apply", "--plan", "nonexistent-plan.json", "--yes", "--device", "99"]); + + assert.equal(result.status, 1); + assert.match(result.stderr, /apply is disabled/); + assert.doesNotMatch(result.stderr, /does not exist|No MIDI Fighter Twister/); +}); + +test("help still documents apply as disabled", () => { + const result = runCli(["--help"]); + + assert.equal(result.status, 0); + assert.match(result.stdout, /apply is the only command that writes settings, and it is disabled/); +}); + +test("read-only and offline commands are not gated", () => { + // `plan` never opens a MIDI port, so this stays hermetic. Its own argument + // validation is what rejects here, proving the gate did not intercept. + const result = runCli(["plan"]); + + assert.equal(result.status, 1); + assert.match(result.stderr, /plan requires --snapshot/); + assert.doesNotMatch(result.stderr, /apply is disabled/); +}); From 7c7c3cf8ef9091d4809cf3f13d53ea19d6113344 Mon Sep 17 00:00:00 2001 From: oveddan Date: Fri, 14 Aug 2026 14:35:02 -0600 Subject: [PATCH 6/6] fix: make the apply gate match what the docs claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of the gate found four problems, one of which mattered. The docs said writes were "disabled in this release", but the gate only covered the CLI entry point. The package ships dist/ with no `exports` field, so every module under it was public API: `import('mft-config/dist/ applier.js')` resolved and handed back `applyPatchPlan`. Verified against a packed install. Add an `exports` field so subpath imports are refused; confirmed the bin and a live `list` still work. `apply` without `--yes` reported "apply requires explicit confirmation with --yes", because argument validation ran before the gate. The tool coached the user into completing an invocation it was going to refuse anyway. Move the gate ahead of those checks. The test claiming offline commands are not gated invoked `plan` with no arguments, so it failed during argument parsing and its assertion could not have failed. Replaced with a real plan against the synthetic fixture, asserting a content-addressed plan comes back. test/run-tests.mjs derived an absolute test directory and then emitted cwd-relative paths. Making the paths absolute was not sufficient on its own — `--import tsx` resolves against the child's cwd — so anchor the child to the package root. Verified from both the repo root and /tmp: 22 passing. Refs #3, #14 Co-Authored-By: Claude Opus 5 --- package.json | 3 +++ src/cli.ts | 6 +++--- test/cli.test.ts | 17 ++++++++++------- test/run-tests.mjs | 12 ++++++++++-- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b27dcc1..58accba 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "dist", "schema" ], + "exports": { + "./package.json": "./package.json" + }, "scripts": { "build": "tsc -p tsconfig.json", "test": "node test/run-tests.mjs", diff --git a/src/cli.ts b/src/cli.ts index 820a04c..e608d7b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -78,6 +78,9 @@ function parseArguments(argv: string[]): Arguments { } if (command === "plan" && !result.snapshot) throw new Error("plan requires --snapshot "); if (command === "plan" && result.sets.length === 0) throw new Error("plan requires at least one --set "); + // Before the --plan and --yes checks: a disabled command must not coach the + // user into completing an invocation that is going to be refused anyway. + if (command === "apply") assertApplyEnabled(); if (command === "apply" && !result.plan) throw new Error("apply requires --plan "); if (command === "apply" && !result.yes) throw new Error("apply requires explicit confirmation with --yes"); return result; @@ -137,9 +140,6 @@ async function main(): Promise { process.stdout.write(`${usage()}\n`); return; } - // Before discovery, so a disabled apply never opens a MIDI port. - if (args.command === "apply") assertApplyEnabled(); - if (args.command === "plan") { const snapshot = JSON.parse(await readFile(resolve(args.snapshot!), "utf8")) as ConfigExport; const plan = createPatchPlan(snapshot, args.sets.map(parseSet)); diff --git a/test/cli.test.ts b/test/cli.test.ts index 9407b72..bfe79a4 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -44,12 +44,15 @@ test("help still documents apply as disabled", () => { assert.match(result.stdout, /apply is the only command that writes settings, and it is disabled/); }); -test("read-only and offline commands are not gated", () => { - // `plan` never opens a MIDI port, so this stays hermetic. Its own argument - // validation is what rejects here, proving the gate did not intercept. - const result = runCli(["plan"]); +test("plan still works while apply is gated", () => { + // Run a real plan to completion. Asserting on a `plan` invocation that fails + // during argument parsing would pass no matter what the gate did. + // `plan` never opens a MIDI port, so this stays hermetic. + const snapshot = fileURLToPath(new URL("fixtures/synthetic-four-bank.json", import.meta.url)); + const result = runCli(["plan", "--snapshot", snapshot, "--set", "bank.1.encoder.1.colors.active=blue"]); - assert.equal(result.status, 1); - assert.match(result.stderr, /plan requires --snapshot/); - assert.doesNotMatch(result.stderr, /apply is disabled/); + assert.equal(result.status, 0); + const plan = JSON.parse(result.stdout) as { planId: string; changes: unknown[] }; + assert.match(plan.planId, /^sha256:/); + assert.equal(plan.changes.length, 1); }); diff --git a/test/run-tests.mjs b/test/run-tests.mjs index c264310..944ad6a 100644 --- a/test/run-tests.mjs +++ b/test/run-tests.mjs @@ -14,16 +14,24 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +// Absolute throughout, so the runner works regardless of the caller's cwd. const testDirectory = fileURLToPath(new URL(".", import.meta.url)); const files = readdirSync(testDirectory) .filter((name) => name.endsWith(".test.ts")) .sort() - .map((name) => join("test", name)); + .map((name) => join(testDirectory, name)); if (files.length === 0) { process.stderr.write("No test files matching *.test.ts were found in test/\n"); process.exit(1); } -const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...files], { stdio: "inherit" }); +// Anchor the child to the package root: `--import tsx` is resolved against the +// child's cwd, so inheriting a foreign one makes tsx itself unresolvable even +// though the test paths above are absolute. +const packageRoot = fileURLToPath(new URL("..", import.meta.url)); +const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...files], { + stdio: "inherit", + cwd: packageRoot, +}); process.exit(result.status ?? 1);