From cf53912a332ee831708491735c1ebe1db3877904 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:01:15 -0700 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20Audio=20Preview=20Player=20?= =?UTF-8?q?=E2=80=94=20browse=20and=20play=20WAV=20samples=20via=20Web=20A?= =?UTF-8?q?udio=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: #9 Backend: - GET /api/list-wavs?dir= — lists .wav files in a directory - GET /wavs/ — static file serving from ./output/wavs Web UI: - WAV Browser section with path input + Browse button - AudioContext-based playback (zero deps) — progress bar, time display - WAV file grid with Play buttons, active item highlighting Tests: - 3 new tests for handleListWavs (wav files, empty dir, nonexistent dir) - Fix TestFindAkaiutil: add t.Chdir(tmp) so relative path lookups don't accidentally find the repo's third_party/akaiutil/akaiutil binary --- serve.go | 34 +++++++++++++ serve_test.go | 64 ++++++++++++++++++++++++ util_test.go | 7 +++ web/ui/app.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++ web/ui/index.html | 16 ++++++ web/ui/style.css | 105 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 349 insertions(+) diff --git a/serve.go b/serve.go index 8672db2..5cdb4b5 100644 --- a/serve.go +++ b/serve.go @@ -54,10 +54,20 @@ func cmdServe(args []string) { mux.HandleFunc("/api/search", handleSearch) mux.HandleFunc("/api/list", handleList) + mux.HandleFunc("/api/list-wavs", handleListWavs) mux.HandleFunc("/api/download", handleAPIDownload) mux.HandleFunc("/api/extract", handleAPIExtract) mux.HandleFunc("/api/progress", handleProgress) + wavsBase := "./output/wavs" + if abs, err := filepath.Abs(wavsBase); err == nil { + wavsBase = abs + } + if err := os.MkdirAll(wavsBase, 0755); err != nil { + fatal("create wavs dir: %v", err) + } + mux.Handle("/wavs/", http.StripPrefix("/wavs/", http.FileServer(http.Dir(wavsBase)))) + addr := fmt.Sprintf("%s:%d", host, port) listener, err := net.Listen("tcp", addr) if err != nil { @@ -254,6 +264,30 @@ func handleAPIDownload(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"jobs": jobIDs}) } +func handleListWavs(w http.ResponseWriter, r *http.Request) { + dir := r.URL.Query().Get("dir") + if dir == "" { + dir = "./output/wavs" + } + if abs, err := filepath.Abs(dir); err == nil { + dir = abs + } + + entries, err := os.ReadDir(dir) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + + var files []string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".wav") { + files = append(files, e.Name()) + } + } + writeJSON(w, files) +} + func handleProgress(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/serve_test.go b/serve_test.go index eed25e2..20e3ffc 100644 --- a/serve_test.go +++ b/serve_test.go @@ -442,4 +442,68 @@ func TestHandleAPIExtract_ExtractISOError(t *testing.T) { if !strings.Contains(msg, "FAIL bad.iso") { t.Errorf("expected FAIL message, got: %s", msg) } +} + +func TestHandleListWavs_ReturnsWavFiles(t *testing.T) { + tmp := t.TempDir() + wavDir := filepath.Join(tmp, "wavs") + if err := os.MkdirAll(wavDir, 0755); err != nil { + t.Fatal(err) + } + for _, name := range []string{"kick.wav", "snare.wav", "hihat.wav"} { + fd, _ := os.Create(filepath.Join(wavDir, name)) + _ = fd.Close() + } + fd, _ := os.Create(filepath.Join(wavDir, "readme.txt")) + _ = fd.Close() + + req := httptest.NewRequest("GET", "/api/list-wavs?dir="+wavDir, nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var files []string + if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(files) != 3 { + t.Errorf("expected 3 wav files, got %d: %v", len(files), files) + } +} + +func TestHandleListWavs_EmptyDir(t *testing.T) { + tmp := t.TempDir() + req := httptest.NewRequest("GET", "/api/list-wavs?dir="+tmp, nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var files []string + if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } +} + +func TestHandleListWavs_NonExistentDir(t *testing.T) { + req := httptest.NewRequest("GET", "/api/list-wavs?dir=/nonexistent/wavs", nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rr.Code) + } + var resp map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if _, ok := resp["error"]; !ok { + t.Error("expected error for nonexistent dir") + } } \ No newline at end of file diff --git a/util_test.go b/util_test.go index 44a36f1..c17d8a2 100644 --- a/util_test.go +++ b/util_test.go @@ -99,6 +99,8 @@ func TestFindAkaiutil(t *testing.T) { }() t.Run("env var set to missing file", func(t *testing.T) { + tmp := t.TempDir() + t.Chdir(tmp) _ = os.Setenv("AKAIUTIL", "/nonexistent/akaiutil") os.Args[0] = "/some/binary" _, err := findAkaiutil() @@ -109,6 +111,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("env var set to existing file", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) f := tmp + "/myutil" fd, _ := os.Create(f) defer func() { _ = fd.Close() }() @@ -124,6 +127,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("finds binary next to argv[0]", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/mybinary" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() @@ -143,6 +147,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("finds binary in third_party relative to argv[0]", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/somebin" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() @@ -166,6 +171,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("env var takes priority over path candidates", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) envFile := tmp + "/envutil" pathFile := tmp + "/pathutil" fd1, _ := os.Create(envFile) @@ -185,6 +191,7 @@ func TestFindAkaiutil(t *testing.T) { t.Run("no akaiutil found returns error", func(t *testing.T) { tmp := t.TempDir() + t.Chdir(tmp) exe := tmp + "/nobinaryhere" fd, _ := os.Create(exe) defer func() { _ = fd.Close() }() diff --git a/web/ui/app.js b/web/ui/app.js index b5f92e9..7f7e980 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -183,6 +183,129 @@ async function doExtract() { } } +// ─── WAV Browser ────────────────────────────────────────────────── + +let audioCtx = null; +let currentSource = null; +let currentAudioBuffer = null; +let startTime = 0; +let pauseOffset = 0; +let isPlaying = false; +let animationFrame = null; + +document.getElementById('wav-browse-btn').addEventListener('click', browseWavs); + +async function browseWavs() { + const dir = document.getElementById('wav-dir').value; + const list = document.getElementById('wav-list'); + list.innerHTML = 'Loading...'; + + try { + const resp = await fetch(`${API}/list-wavs?dir=${encodeURIComponent(dir)}`); + const data = await resp.json(); + if (data.error) { + list.innerHTML = 'Error: ' + data.error + ''; + return; + } + if (!Array.isArray(data) || data.length === 0) { + list.innerHTML = 'No WAV files found'; + return; + } + list.innerHTML = data.map(f => ` +
+ ${esc(f)} + +
+ `).join(''); + } catch (e) { + list.innerHTML = 'Failed: ' + e.message; + } +} + +async function playWav(filename) { + if (!audioCtx) { + audioCtx = new AudioContext(); + } + + if (currentSource) { + currentSource.stop(); + currentSource = null; + } + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + + const nowPlaying = document.getElementById('wav-now-playing'); + nowPlaying.textContent = 'Loading: ' + filename; + + try { + const resp = await fetch(`/wavs/${encodeURIComponent(filename)}`); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const arrayBuffer = await resp.arrayBuffer(); + currentAudioBuffer = await audioCtx.decodeAudioData(arrayBuffer); + + currentSource = audioCtx.createBufferSource(); + currentSource.buffer = currentAudioBuffer; + currentSource.connect(audioCtx.destination); + + currentSource.onended = () => { + if (isPlaying) { + isPlaying = false; + pauseOffset = 0; + nowPlaying.textContent = ''; + document.getElementById('wav-progress-fill').style.width = '0%'; + document.getElementById('wav-time').textContent = formatTime(0); + clearActiveItem(); + } + }; + + startTime = audioCtx.currentTime - pauseOffset; + currentSource.start(0, pauseOffset); + isPlaying = true; + pauseOffset = 0; + + nowPlaying.textContent = '▶ ' + filename; + setActiveItem(filename); + updateProgress(); + } catch (e) { + nowPlaying.textContent = 'Error: ' + e.message; + } +} + +function updateProgress() { + if (!isPlaying || !currentAudioBuffer) return; + + const elapsed = audioCtx.currentTime - startTime; + const duration = currentAudioBuffer.duration; + const pct = Math.min((elapsed / duration) * 100, 100); + + document.getElementById('wav-progress-fill').style.width = pct + '%'; + document.getElementById('wav-time').textContent = formatTime(elapsed); + + if (elapsed < duration) { + animationFrame = requestAnimationFrame(updateProgress); + } +} + +function formatTime(secs) { + const m = Math.floor(secs / 60); + const s = Math.floor(secs % 60); + return m + ':' + s.toString().padStart(2, '0'); +} + +function setActiveItem(filename) { + document.querySelectorAll('.wav-item').forEach(el => { + el.classList.toggle('playing', el.dataset.file === filename); + }); +} + +function clearActiveItem() { + document.querySelectorAll('.wav-item').forEach(el => { + el.classList.remove('playing'); + }); +} + // ─── Helpers ────────────────────────────────────────────────────── function esc(s) { diff --git a/web/ui/index.html b/web/ui/index.html index c0a6431..3a6e677 100644 --- a/web/ui/index.html +++ b/web/ui/index.html @@ -39,6 +39,22 @@
+
+

WAV Browser

+
+ + +
+
+
+
+
+ 0:00 +
+
+
+
+

Results

diff --git a/web/ui/style.css b/web/ui/style.css index 82bdc98..0262c08 100644 --- a/web/ui/style.css +++ b/web/ui/style.css @@ -205,4 +205,109 @@ h2 { margin-top: 10px; font-size: 13px; color: var(--text2); +} + +.wav-controls { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.wav-controls input { + flex: 1; + min-width: 200px; + padding: 10px 14px; + border: 1px solid var(--bg3); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); + font-size: 14px; +} +.wav-controls button { + padding: 10px 20px; + border: none; + border-radius: var(--radius); + background: var(--accent); + color: white; + font-size: 14px; + cursor: pointer; +} + +.wav-player { + margin-top: 14px; +} + +.now-playing { + font-size: 13px; + color: var(--green); + margin-bottom: 6px; + min-height: 18px; +} + +.audio-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.audio-progress { + flex: 1; + height: 6px; + background: var(--bg3); + border-radius: 3px; + overflow: hidden; +} + +.audio-progress-fill { + height: 100%; + background: var(--green); + border-radius: 3px; + width: 0%; + transition: width 0.1s; +} + +#wav-time { + font-size: 12px; + color: var(--text2); + min-width: 40px; +} + +.wav-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; +} + +.wav-item { + background: var(--bg); + border: 1px solid var(--bg3); + border-radius: var(--radius); + padding: 10px 12px; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} +.wav-item:hover { + border-color: var(--accent); +} +.wav-item.playing { + border-color: var(--green); + background: rgba(78, 204, 163, 0.1); +} +.wav-item .wav-name { + flex: 1; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.wav-item .wav-play-btn { + background: var(--accent2); + color: white; + border: none; + border-radius: 4px; + padding: 4px 10px; + font-size: 11px; + cursor: pointer; } \ No newline at end of file -- 2.52.0 From f716902580fe37d0a53ea94afec79d15b9e71c3b Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:02:32 -0700 Subject: [PATCH 02/10] fix: handle null data in browseWavs, add HTTP status check Avoid 'Cannot read properties of null (reading error)' when server returns null or non-JSON. Also check resp.ok before parsing JSON. --- web/ui/app.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/ui/app.js b/web/ui/app.js index 7f7e980..fe7c542 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -202,9 +202,13 @@ async function browseWavs() { try { const resp = await fetch(`${API}/list-wavs?dir=${encodeURIComponent(dir)}`); + if (!resp.ok) { + list.innerHTML = 'HTTP ' + resp.status + ''; + return; + } const data = await resp.json(); - if (data.error) { - list.innerHTML = 'Error: ' + data.error + ''; + if (!data || data.error) { + list.innerHTML = 'Error: ' + (data && data.error ? data.error : 'server error') + ''; return; } if (!Array.isArray(data) || data.length === 0) { -- 2.52.0 From 4f03524668fc74038361c709d42d43915c019697 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:10:49 -0700 Subject: [PATCH 03/10] chore: add JS/ESLint toolchain, refactor onclick to event delegation - Add ESLint config for web/ui (flat config, golangci-lint-compatible) - Add js-lint task to mise.toml (npm run lint in web/ui) - Add ESLint check to check-toolchain.sh - Add .golangci.yml for golangci-lint v2 with correct exclusions - Refactor app.js: replace inline onclick with data-action + event delegation - Add null checks and HTTP status checks to browseWavs - Add web/ui/node_modules/ to .gitignore --- .gitignore | 3 + .golangci.yml | 14 + mise.toml | 3 +- scripts/check-toolchain.sh | 11 + web/ui/app.js | 20 +- web/ui/eslint.config.js | 45 ++ web/ui/package-lock.json | 1098 ++++++++++++++++++++++++++++++++++++ web/ui/package.json | 11 + 8 files changed, 1201 insertions(+), 4 deletions(-) create mode 100644 .golangci.yml create mode 100644 web/ui/eslint.config.js create mode 100644 web/ui/package-lock.json create mode 100644 web/ui/package.json diff --git a/.gitignore b/.gitignore index 3aadeeb..0ecb5b5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,8 @@ wavs .DS_Store Thumbs.db +# JS dependencies +web/ui/node_modules/ + # Temp /tmp/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..00daf0e --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,14 @@ +version: "2" + +linters: + default: none + enable: + - errcheck + - govet + - staticcheck + - unused + exclusions: + paths: + - web/ui + - electron + - third_party/akaiutil/ \ No newline at end of file diff --git a/mise.toml b/mise.toml index 9cd4483..cc61b08 100644 --- a/mise.toml +++ b/mise.toml @@ -11,7 +11,8 @@ build = "go build -buildvcs=false -o fetch ." akaiutil = "make -C third_party/akaiutil" all = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiutil" } serve = { run = "go build -buildvcs=false -o fetch . && ./fetch serve" } -lint = "go vet ./..." +lint = "golangci-lint run" +js-lint = "cd web/ui && npm run lint" electron-deps = "cd electron && npm install" electron-dev = { run = "go build -buildvcs=false -o fetch . && cd electron && npx electron ." } electron-build = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiutil && cd electron && npx electron-builder --dir" } diff --git a/scripts/check-toolchain.sh b/scripts/check-toolchain.sh index 8ec0112..a7f7c9d 100755 --- a/scripts/check-toolchain.sh +++ b/scripts/check-toolchain.sh @@ -123,5 +123,16 @@ else fi fi +echo "" +echo "=== JS Linting (ESLint) ===" +if [ -f web/ui/node_modules/.bin/eslint ]; then + ok "ESLint installed at web/ui/node_modules/.bin/eslint" + eslint_ver=$(cd web/ui && ./node_modules/.bin/eslint --version 2>/dev/null) + ok "ESLint: $eslint_ver" +else + warn "ESLint not installed (needed for web UI linting)" + echo " Install: cd web/ui && npm install" +fi + echo "" echo "=== Core toolchain check complete ===" diff --git a/web/ui/app.js b/web/ui/app.js index fe7c542..3edeffe 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -46,8 +46,8 @@ function renderResults(items) { ${r.downloads.toLocaleString()} downloads
- - + +
@@ -218,7 +218,7 @@ async function browseWavs() { list.innerHTML = data.map(f => `
${esc(f)} - +
`).join(''); } catch (e) { @@ -332,3 +332,17 @@ function formatBytes(bytes) { // Initial load doSearch(); + +// Event delegation for dynamically generated buttons +document.addEventListener('click', function(e) { + const btn = e.target.closest('[data-action]'); + if (!btn) return; + const action = btn.dataset.action; + if (action === 'download') { + downloadItem(btn.dataset.identifier); + } else if (action === 'list') { + listItem(btn.dataset.identifier); + } else if (action === 'play') { + playWav(btn.dataset.wav); + } +}); diff --git a/web/ui/eslint.config.js b/web/ui/eslint.config.js new file mode 100644 index 0000000..2707048 --- /dev/null +++ b/web/ui/eslint.config.js @@ -0,0 +1,45 @@ +const globals = require('globals'); + +module.exports = [ + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'script', + globals: { + ...globals.browser, + AudioContext: 'readonly', + requestAnimationFrame: 'readonly', + cancelAnimationFrame: 'readonly', + fetch: 'readonly', + EventSource: 'readonly', + document: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + Math: 'readonly', + JSON: 'readonly', + location: 'readonly', + encodeURIComponent: 'readonly', + decodeURIComponent: 'readonly', + parseInt: 'readonly', + Number: 'readonly', + Promise: 'readonly', + Object: 'readonly', + Array: 'readonly', + Map: 'readonly', + process: 'readonly', + __dirname: 'readonly', + require: 'readonly', + module: 'readonly', + exports: 'readonly' + } + }, + rules: { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "no-redeclare": "warn", + "no-unreachable": "warn", + "eqeqeq": ["error", "always"], + "prefer-const": "warn" + } + } +]; \ No newline at end of file diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json new file mode 100644 index 0000000..5cd4fa2 --- /dev/null +++ b/web/ui/package-lock.json @@ -0,0 +1,1098 @@ +{ + "name": "akai-utils-web-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "akai-utils-web-ui", + "version": "1.0.0", + "devDependencies": { + "eslint": "^9.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/ui/package.json b/web/ui/package.json new file mode 100644 index 0000000..30e3e05 --- /dev/null +++ b/web/ui/package.json @@ -0,0 +1,11 @@ +{ + "name": "akai-utils-web-ui", + "version": "1.0.0", + "description": "Web UI for AKAI Utils", + "scripts": { + "lint": "eslint *.js" + }, + "devDependencies": { + "eslint": "^9.0.0" + } +} \ No newline at end of file -- 2.52.0 From d194a25bf89966b9d3872644b547366e9751f150 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:16:42 -0700 Subject: [PATCH 04/10] chore: pin node to 22.12 to fix electron-build engine requirements @electron/rebuild and node-abi require node >=22.12.0. System node (22.9.0) caused EBADENGINE errors during electron-build. --- mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mise.toml b/mise.toml index cc61b08..881ee5b 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,7 @@ [tools] go = "1.26" golangci-lint = "latest" -node = "22" +node = "22.12" [env] CGO_ENABLED = "0" -- 2.52.0 From cd8484de0c2bf14c83843356adcead6e03dc5a77 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:22:19 -0700 Subject: [PATCH 05/10] fix: pin electron to exact version 33.4.11 electron-builder requires an exact version (not a range like ^33.0.0) because it downloads platform-specific binaries for a specific release. --- electron/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/package.json b/electron/package.json index 6c79520..b6afa03 100644 --- a/electron/package.json +++ b/electron/package.json @@ -60,7 +60,7 @@ "build:linux": "electron-builder --linux" }, "devDependencies": { - "electron": "^33.0.0", + "electron": "33.4.11", "electron-builder": "^25.0.0" } } -- 2.52.0 From b69e1d81ab0ad19ad5bd0148e62a3d5b9f2a8dda Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:42:18 -0700 Subject: [PATCH 06/10] debug: add console logging to browseWavs and playWav --- web/ui/app.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/ui/app.js b/web/ui/app.js index 3edeffe..9a4cac1 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -199,14 +199,20 @@ async function browseWavs() { const dir = document.getElementById('wav-dir').value; const list = document.getElementById('wav-list'); list.innerHTML = 'Loading...'; + console.log('browseWavs: dir =', dir); try { - const resp = await fetch(`${API}/list-wavs?dir=${encodeURIComponent(dir)}`); + const url = `${API}/list-wavs?dir=${encodeURIComponent(dir)}`; + console.log('browseWavs: fetching', url); + const resp = await fetch(url); + console.log('browseWavs: response status', resp.status, 'ok', resp.ok); + if (!resp.ok) { list.innerHTML = 'HTTP ' + resp.status + ''; return; } const data = await resp.json(); + console.log('browseWavs: parsed data', data, 'type', typeof data, 'isArray', Array.isArray(data)); if (!data || data.error) { list.innerHTML = 'Error: ' + (data && data.error ? data.error : 'server error') + ''; return; @@ -215,6 +221,7 @@ async function browseWavs() { list.innerHTML = 'No WAV files found'; return; } + console.log('browseWavs: found', data.length, 'wavs'); list.innerHTML = data.map(f => `
${esc(f)} @@ -222,13 +229,16 @@ async function browseWavs() {
`).join(''); } catch (e) { + console.error('browseWavs: error', e); list.innerHTML = 'Failed: ' + e.message; } } async function playWav(filename) { + console.log('playWav:', filename); if (!audioCtx) { audioCtx = new AudioContext(); + console.log('playWav: created AudioContext'); } if (currentSource) { -- 2.52.0 From 371feaf0cb009dfc0cd19293fe678368335ff7d5 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 00:46:30 -0700 Subject: [PATCH 07/10] fix: initialize files as empty slice instead of nil var var files []string marshals to JSON null, breaking the frontend which expects an array. Using files := []string{} ensures []. --- serve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serve.go b/serve.go index 5cdb4b5..ec2df45 100644 --- a/serve.go +++ b/serve.go @@ -279,7 +279,7 @@ func handleListWavs(w http.ResponseWriter, r *http.Request) { return } - var files []string + files := []string{} for _, e := range entries { if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".wav") { files = append(files, e.Name()) -- 2.52.0 From fe3e92ce33a09b1ae46a4d5114c00c3112ad7538 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 01:05:44 -0700 Subject: [PATCH 08/10] feat: recursive WAV browsing in subdirectories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle ListWavs now uses filepath.WalkDir to find .wav files in all subdirectories. Returns relative paths (e.g. Kicks/kick.wav). Serves them correctly via /wavs/ prefix. Fixed: WalkDir swallows root errors differently than ReadDir — now detects and reports non-existent root directory errors. Added test for recursive subdirectory traversal. --- serve.go | 28 ++++++++++++++++++++-------- serve_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/serve.go b/serve.go index ec2df45..aad5e39 100644 --- a/serve.go +++ b/serve.go @@ -273,18 +273,30 @@ func handleListWavs(w http.ResponseWriter, r *http.Request) { dir = abs } - entries, err := os.ReadDir(dir) + files := []string{} + var walkErr error + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + if path == dir { + walkErr = err + return filepath.SkipAll + } + return nil + } + if !d.IsDir() && strings.HasSuffix(strings.ToLower(d.Name()), ".wav") { + rel, _ := filepath.Rel(dir, path) + files = append(files, rel) + } + return nil + }) + if walkErr != nil { + writeJSON(w, map[string]any{"error": walkErr.Error()}) + return + } if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - - files := []string{} - for _, e := range entries { - if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".wav") { - files = append(files, e.Name()) - } - } writeJSON(w, files) } diff --git a/serve_test.go b/serve_test.go index 20e3ffc..1ea68d1 100644 --- a/serve_test.go +++ b/serve_test.go @@ -473,6 +473,41 @@ func TestHandleListWavs_ReturnsWavFiles(t *testing.T) { } } +func TestHandleListWavs_RecursiveSubdirs(t *testing.T) { + tmp := t.TempDir() + wavDir := filepath.Join(tmp, "wavs") + subDir := filepath.Join(wavDir, "Kicks") + if err := os.MkdirAll(subDir, 0755); err != nil { + t.Fatal(err) + } + fd, _ := os.Create(filepath.Join(wavDir, "root.wav")) + _ = fd.Close() + fd2, _ := os.Create(filepath.Join(subDir, "kick1.wav")) + _ = fd2.Close() + + req := httptest.NewRequest("GET", "/api/list-wavs?dir="+wavDir, nil) + rr := httptest.NewRecorder() + handleListWavs(rr, req) + + var files []string + if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(files) != 2 { + t.Errorf("expected 2 wav files, got %d: %v", len(files), files) + } + found := make(map[string]bool) + for _, f := range files { + found[f] = true + } + if !found["root.wav"] { + t.Errorf("missing root.wav, got %v", files) + } + if !found[filepath.Join("Kicks", "kick1.wav")] { + t.Errorf("missing Kicks/kick1.wav, got %v", files) + } +} + func TestHandleListWavs_EmptyDir(t *testing.T) { tmp := t.TempDir() req := httptest.NewRequest("GET", "/api/list-wavs?dir="+tmp, nil) @@ -486,6 +521,9 @@ func TestHandleListWavs_EmptyDir(t *testing.T) { if err := json.Unmarshal(rr.Body.Bytes(), &files); err != nil { t.Fatalf("invalid JSON: %v", err) } + if files == nil { + t.Fatal("expected empty array [], got null") + } if len(files) != 0 { t.Errorf("expected 0 files, got %d", len(files)) } -- 2.52.0 From d7a17eb05421abc9f94b63effb5be5b5fd963ee8 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 01:10:40 -0700 Subject: [PATCH 09/10] fix: show extract API errors in UI instead of false 'Extraction complete' doExtract() only checked data.message, so JSON responses with error key (e.g. 'no ISO files found') would show green success. Now checks data.error first and displays it in red. --- web/ui/app.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/ui/app.js b/web/ui/app.js index 9a4cac1..6d3ed62 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -175,6 +175,11 @@ async function doExtract() { try { const resp = await fetch(`${API}/extract?dir=${encodeURIComponent(isoDir)}&out=${encodeURIComponent(wavDir)}`); const data = await resp.json(); + if (data.error) { + status.textContent = data.error; + status.style.color = 'var(--accent)'; + return; + } status.textContent = data.message || 'Extraction complete'; status.style.color = 'var(--green)'; } catch (e) { -- 2.52.0 From b5650c53bab9af87cabf002fbae593be4040f9e4 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 02:23:24 -0700 Subject: [PATCH 10/10] test: add JS test framework using Node built-in test runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted pure utility functions (esc, sanitize, formatBytes, formatTime) into web/ui/util.js with CommonJS export for Node testing compatibility. Added 19 tests across 4 suites using node:test + node:assert (zero deps). - util.js: browser-compatible utility functions - util.test.js: 19 tests (5 esc, 3 sanitize, 6 formatBytes, 5 formatTime) - package.json: 'test' script → node --test util.test.js - mise.toml: js-test task - app.js: removed duplicate helper functions (now in util.js) - index.html: load util.js before app.js --- mise.toml | 1 + web/ui/app.js | 26 ------------- web/ui/index.html | 3 +- web/ui/package.json | 3 +- web/ui/util.js | 29 +++++++++++++++ web/ui/util.test.js | 91 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 web/ui/util.js create mode 100644 web/ui/util.test.js diff --git a/mise.toml b/mise.toml index 881ee5b..3221f7d 100644 --- a/mise.toml +++ b/mise.toml @@ -13,6 +13,7 @@ all = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiut serve = { run = "go build -buildvcs=false -o fetch . && ./fetch serve" } lint = "golangci-lint run" js-lint = "cd web/ui && npm run lint" +js-test = "cd web/ui && npm test" electron-deps = "cd electron && npm install" electron-dev = { run = "go build -buildvcs=false -o fetch . && cd electron && npx electron ." } electron-build = { run = "go build -buildvcs=false -o fetch . && make -C third_party/akaiutil && cd electron && npx electron-builder --dir" } diff --git a/web/ui/app.js b/web/ui/app.js index 6d3ed62..29d57f2 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -307,12 +307,6 @@ function updateProgress() { } } -function formatTime(secs) { - const m = Math.floor(secs / 60); - const s = Math.floor(secs % 60); - return m + ':' + s.toString().padStart(2, '0'); -} - function setActiveItem(filename) { document.querySelectorAll('.wav-item').forEach(el => { el.classList.toggle('playing', el.dataset.file === filename); @@ -325,26 +319,6 @@ function clearActiveItem() { }); } -// ─── Helpers ────────────────────────────────────────────────────── - -function esc(s) { - if (!s) return ''; - return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); -} - -function sanitize(s) { - return s.replace(/[^a-zA-Z0-9_-]/g, '_'); -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} - // Initial load doSearch(); diff --git a/web/ui/index.html b/web/ui/index.html index 3a6e677..1794e9b 100644 --- a/web/ui/index.html +++ b/web/ui/index.html @@ -60,6 +60,7 @@
- + + diff --git a/web/ui/package.json b/web/ui/package.json index 30e3e05..8e99ccc 100644 --- a/web/ui/package.json +++ b/web/ui/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "Web UI for AKAI Utils", "scripts": { - "lint": "eslint *.js" + "lint": "eslint *.js", + "test": "node --test util.test.js" }, "devDependencies": { "eslint": "^9.0.0" diff --git a/web/ui/util.js b/web/ui/util.js new file mode 100644 index 0000000..a40a83c --- /dev/null +++ b/web/ui/util.js @@ -0,0 +1,29 @@ +// AKAI Utils — shared utility functions + +function esc(s) { + if (!s) return '' + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') +} + +function sanitize(s) { + return s.replace(/[^a-zA-Z0-9_-]/g, '_') +} + +function formatBytes(bytes) { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] +} + +function formatTime(secs) { + const m = Math.floor(secs / 60) + const s = Math.floor(secs % 60) + return m + ':' + s.toString().padStart(2, '0') +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { esc, sanitize, formatBytes, formatTime } +} \ No newline at end of file diff --git a/web/ui/util.test.js b/web/ui/util.test.js new file mode 100644 index 0000000..26b461b --- /dev/null +++ b/web/ui/util.test.js @@ -0,0 +1,91 @@ +const { describe, it } = require('node:test') +const assert = require('node:assert/strict') +const { esc, sanitize, formatBytes, formatTime } = require('./util.js') + +describe('esc', () => { + it('escapes HTML entities', () => { + assert.strictEqual(esc(''), + '<script>alert("xss")</script>') + }) + + it('handles ampersands', () => { + assert.strictEqual(esc('a & b'), 'a & b') + }) + + it('handles single quotes', () => { + assert.strictEqual(esc("it's"), 'it's') + }) + + it('returns empty string for falsy input', () => { + assert.strictEqual(esc(''), '') + assert.strictEqual(esc(null), '') + assert.strictEqual(esc(undefined), '') + }) + + it('passes through safe strings', () => { + assert.strictEqual(esc('Hello World'), 'Hello World') + }) +}) + +describe('sanitize', () => { + it('replaces special characters with underscores', () => { + assert.strictEqual(sanitize('hello world!'), 'hello_world_') + }) + + it('preserves alphanumeric and dashes', () => { + assert.strictEqual(sanitize('test-123_abc'), 'test-123_abc') + }) + + it('sanitizes dots and slashes', () => { + assert.strictEqual(sanitize('path/to/file.name'), + 'path_to_file_name') + }) +}) + +describe('formatBytes', () => { + it('formats zero', () => { + assert.strictEqual(formatBytes(0), '0 B') + }) + + it('formats bytes', () => { + assert.strictEqual(formatBytes(500), '500 B') + }) + + it('formats KB', () => { + assert.strictEqual(formatBytes(1024), '1 KB') + }) + + it('formats MB', () => { + assert.strictEqual(formatBytes(1048576), '1 MB') + }) + + it('formats GB', () => { + assert.strictEqual(formatBytes(1073741824), '1 GB') + }) + + it('formats fractional', () => { + assert.strictEqual(formatBytes(1536), '1.5 KB') + }) +}) + +describe('formatTime', () => { + it('formats zero', () => { + assert.strictEqual(formatTime(0), '0:00') + }) + + it('formats seconds', () => { + assert.strictEqual(formatTime(5), '0:05') + }) + + it('formats minutes and seconds', () => { + assert.strictEqual(formatTime(65), '1:05') + }) + + it('formats double-digit seconds', () => { + assert.strictEqual(formatTime(75), '1:15') + }) + + it('formats large times', () => { + assert.strictEqual(formatTime(3661), '61:01') + }) +}) \ No newline at end of file -- 2.52.0