chore: migrate repo workflows to Bun-only runtime

This commit is contained in:
2026-02-22 20:43:54 -08:00
parent 1d3f099e44
commit f33b5e1e98
17 changed files with 415 additions and 208 deletions

View File

@@ -20,11 +20,6 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache dependencies
uses: actions/cache@v4
with:

View File

@@ -26,11 +26,6 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache dependencies
uses: actions/cache@v4
with:
@@ -78,11 +73,6 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache dependencies
uses: actions/cache@v4
with:
@@ -128,11 +118,6 @@ jobs:
with:
bun-version: 1.3.5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache dependencies
uses: actions/cache@v4
with:

View File

@@ -74,13 +74,14 @@ subminer video.mkv
## Requirements
| Required | Optional |
| ------------------------------------------ | ---------------------------- |
| `mpv` with IPC socket | `yt-dlp` |
| Required | Optional |
| ------------------------------------------ | -------------------------------------------------- |
| `bun` | |
| `mpv` with IPC socket | `yt-dlp` |
| `ffmpeg` | `guessit` (better AniSkip title/episode detection) |
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
| Linux: `hyprctl` or `xdotool` + `xwininfo` | `chafa`, `ffmpegthumbnailer` |
| macOS: Accessibility permission | |
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
| Linux: `hyprctl` or `xdotool` + `xwininfo` | `chafa`, `ffmpegthumbnailer` |
| macOS: Accessibility permission | |
## Documentation

View File

@@ -0,0 +1,61 @@
---
id: TASK-115
title: Migrate repository workflows to Bun-only JavaScript runtime
status: Done
assignee: []
created_date: '2026-02-23 04:26'
updated_date: '2026-02-23 04:42'
labels:
- tooling
- build
- ci
dependencies: []
references:
- package.json
- .github/workflows/ci.yml
- .github/workflows/release.yml
- docs/development.md
- docs/installation.md
- README.md
documentation:
- docs/plans/2026-02-23-bun-only-toolchain-migration.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Transition the project from mixed Bun/Node tooling to a Bun-only contributor and CI workflow so setup is simpler and runtime behavior is consistent across local and automation lanes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A contributor can install dependencies, build, run source tests, run dist smoke tests, and build docs using Bun without requiring a system Node.js install.
- [x] #2 CI and release workflows no longer require Node setup steps for standard project verification and packaging jobs.
- [x] #3 Dist and utility verification lanes remain covered and pass with Bun-based commands.
- [x] #4 Repository documentation clearly states the Bun-only prerequisite and removes conflicting Node requirement guidance.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Execution started in-session after user selected Subagent-Driven option. Tracking sequence: TASK-115.2 -> TASK-115.3/TASK-115.4 -> TASK-115.1.
Completed child sequence TASK-115.2 -> TASK-115.3/TASK-115.4 -> TASK-115.1 with Bun-only command/workflow migration.
Direct Node command usage removed from package dist and utility script lanes; CI/release Node setup steps removed; contributor docs aligned to Bun-only prerequisites.
Regression hardening: refactored AniList token-store tests and storage seam to remain stable under Bun dist test execution while preserving runtime behavior.
Post-migration docs polish: updated `README.md` requirements table to list Bun as a required dependency and promoted Bun from optional-tools section into system dependencies in `docs/installation.md` to remove ambiguity.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Migrated repository verification and workflow surfaces to Bun-first execution by replacing `node --test` dist lanes and remaining direct Node utility invocation (`generate:config-example`) with Bun commands.
Removed `actions/setup-node` from CI and release jobs and aligned docs (`docs/development.md`, `docs/installation.md`) to Bun-only setup guidance; launcher smoke fixtures now use Bun shebangs so smoke tests no longer depend on Node.
Validated full gate set under Bun-only command path: launcher smoke, fast source suite, build, dist config/core/smoke suites, docs build, plus config-example generation command.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,49 @@
---
id: TASK-115.1
title: Remove Node setup from workflows and finalize Bun-only docs
status: Done
assignee: []
created_date: '2026-02-23 04:26'
updated_date: '2026-02-23 04:36'
labels:
- ci
- docs
- tooling
dependencies:
- TASK-115.2
- TASK-115.3
- TASK-115.4
references:
- .github/workflows/ci.yml
- .github/workflows/release.yml
- docs/development.md
- docs/installation.md
- README.md
documentation:
- docs/plans/2026-02-23-bun-only-toolchain-migration.md
parent_task_id: TASK-115
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
After script/test migration parity is verified, remove Node setup from CI/release workflows and finalize documentation so contributors and automation use a consistent Bun-only prerequisite.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 CI and release workflows no longer include Node setup steps for routine build/test/package jobs.
- [x] #2 Primary local verification gates pass with the Bun-only command set.
- [x] #3 README and setup/install docs consistently describe Bun as the JS runtime prerequisite without conflicting Node requirement text.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Removed `actions/setup-node` from `.github/workflows/ci.yml` and `.github/workflows/release.yml` (quality-gate, build-linux, build-macos jobs).
Updated docs to align Bun-only prerequisite: removed Node from `docs/development.md` prerequisites and switched stale pnpm source-build snippet in `docs/installation.md` to Bun.
Validation gates: `bun run test:launcher:smoke:src && bun run test:fast && bun run build && bun run test:config:dist && bun run test:core:dist && bun run test:smoke:dist && bun run docs:build` all pass.
<!-- SECTION:NOTES:END -->

View File

@@ -0,0 +1,41 @@
---
id: TASK-115.2
title: Map Node touchpoints and define Bun parity matrix
status: Done
assignee: []
created_date: '2026-02-23 04:27'
updated_date: '2026-02-23 04:36'
labels:
- tooling
- planning
dependencies: []
references:
- package.json
- .github/workflows/ci.yml
- .github/workflows/release.yml
documentation:
- docs/plans/2026-02-23-bun-only-toolchain-migration.md
parent_task_id: TASK-115
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Document all Node-specific command and workflow touchpoints, define Bun replacements, and establish migration guardrails so follow-up implementation tasks have clear scope and risk controls.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A complete inventory of Node-dependent scripts and workflow steps is documented with proposed Bun equivalents.
- [x] #2 Migration risks and compatibility checkpoints are explicitly documented for dist and Electron-related lanes.
- [x] #3 Follow-up implementation tasks have clear boundaries and ordering based on the parity matrix.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Created `docs/plans/bun-only-parity-matrix.md` with direct Node touchpoint inventory, Bun replacements, risk checkpoints, and migration sequencing.
Parity matrix now links implementation ordering for TASK-115.3/115.4/115.1 and flags follow-up risk handling for third-party implicit Node assumptions.
<!-- SECTION:NOTES:END -->

View File

@@ -0,0 +1,44 @@
---
id: TASK-115.3
title: Migrate dist test lanes from Node runner to Bun
status: Done
assignee: []
created_date: '2026-02-23 04:27'
updated_date: '2026-02-23 04:36'
labels:
- test
- tooling
dependencies:
- TASK-115.2
references:
- package.json
- docs/development.md
- docs/installation.md
documentation:
- docs/plans/2026-02-23-bun-only-toolchain-migration.md
parent_task_id: TASK-115
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace Node-runner dist test commands with Bun-based dist verification while preserving existing test scope and confidence in compiled artifact behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Dist test scripts execute via Bun instead of node --test while preserving the current dist test file coverage.
- [x] #2 Dist smoke and full dist verification lanes pass under Bun-based commands.
- [x] #3 Documentation for dist verification commands reflects the updated Bun-based workflow.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Updated `package.json` dist lanes from `node --test` to `bun test` (`test:config:dist`, `test:config:smoke:dist`, `test:core:dist`, `test:core:smoke:dist`).
Resolved Bun dist compatibility gap by injecting `SafeStorageLike` into AniList token store and replacing brittle Electron global monkey-patching in `anilist-token-store` tests.
Validation: `bun run build && bun run test:config:dist && bun run test:core:dist && bun run test:smoke:dist` all pass.
<!-- SECTION:NOTES:END -->

View File

@@ -0,0 +1,44 @@
---
id: TASK-115.4
title: Migrate Node-invoked utility script commands to Bun
status: Done
assignee: []
created_date: '2026-02-23 04:27'
updated_date: '2026-02-23 04:36'
labels:
- tooling
- scripts
dependencies:
- TASK-115.2
references:
- package.json
- scripts/get_frequency.ts
- scripts/test-yomitan-parser.ts
documentation:
- docs/plans/2026-02-23-bun-only-toolchain-migration.md
parent_task_id: TASK-115
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Update utility command entrypoints that still rely on Node invocation so maintenance and diagnostics workflows can be run entirely through Bun.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Utility script commands that currently invoke Node have Bun-based equivalents as the default project commands.
- [x] #2 Electron-targeted utility flows remain functional after command migration.
- [x] #3 Contributor docs reflect the updated Bun-based utility command usage.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Updated `generate:config-example` command from `node dist/generate-config-example.js` to `bun dist/generate-config-example.js`.
Updated launcher smoke fixture executables from `#!/usr/bin/env node` to `#!/usr/bin/env bun` so smoke tests do not assume Node presence.
Validation: `bun run test:launcher:smoke:src` and `bun run generate:config-example` pass with Bun-first command path.
<!-- SECTION:NOTES:END -->

View File

@@ -2,7 +2,6 @@
## Prerequisites
- [Node.js](https://nodejs.org/) (LTS)
- [Bun](https://bun.sh)
## Setup
@@ -150,22 +149,22 @@ Run `make help` for a full list of targets. Key ones:
## Environment Variables
| Variable | Description |
| ---------------------------------- | ------------------------------------------------------------------------------- |
| `SUBMINER_APPIMAGE_PATH` | Override SubMiner app binary path for launcher playback commands |
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
| `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker |
| Variable | Description |
| ---------------------------------- | ------------------------------------------------------------------------------ |
| `SUBMINER_APPIMAGE_PATH` | Override SubMiner app binary path for launcher playback commands |
| `SUBMINER_BINARY_PATH` | Alias for `SUBMINER_APPIMAGE_PATH` |
| `SUBMINER_ROFI_THEME` | Override rofi theme path for launcher picker |
| `SUBMINER_LOG_LEVEL` | Override app logger level (`debug`, `info`, `warn`, `error`) |
| `SUBMINER_MPV_LOG` | Override mpv/app shared log file path |
| `SUBMINER_YT_SUBGEN_MODE` | Override `youtubeSubgen.mode` for launcher |
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |
| `SUBMINER_WHISPER_MODEL` | Override `youtubeSubgen.whisperModel` for launcher |
| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override generated subtitle output directory |
| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used for whisper fallback |
| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep temporary subtitle-generation workspace |
| `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads |
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime |
| `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL |
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Override Jellyfin access token (used before stored encrypted session fallback) |
| `SUBMINER_JELLYFIN_USER_ID` | Optional Jellyfin user ID override |
| `SUBMINER_MPV_LOG` | Override mpv/app shared log file path |
| `SUBMINER_YT_SUBGEN_MODE` | Override `youtubeSubgen.mode` for launcher |
| `SUBMINER_WHISPER_BIN` | Override `youtubeSubgen.whisperBin` for launcher |
| `SUBMINER_WHISPER_MODEL` | Override `youtubeSubgen.whisperModel` for launcher |
| `SUBMINER_YT_SUBGEN_OUT_DIR` | Override generated subtitle output directory |
| `SUBMINER_YT_SUBGEN_AUDIO_FORMAT` | Override extraction format used for whisper fallback |
| `SUBMINER_YT_SUBGEN_KEEP_TEMP` | Set to `1` to keep temporary subtitle-generation workspace |
| `SUBMINER_JIMAKU_API_KEY` | Override Jimaku API key for launcher subtitle downloads |
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command used to resolve Jimaku API key at runtime |
| `SUBMINER_JIMAKU_API_BASE_URL` | Override Jimaku API base URL |
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Override Jellyfin access token (used before stored encrypted session fallback) |
| `SUBMINER_JELLYFIN_USER_ID` | Optional Jellyfin user ID override |
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip building the macOS helper binary during `bun run build` |

View File

@@ -6,6 +6,7 @@
| Dependency | Required | Notes |
| -------------------- | ---------- | -------------------------------------------------------- |
| Bun | Yes | Required for `subminer` wrapper and source workflows |
| mpv | Yes | Must support IPC sockets (`--input-ipc-server`) |
| ffmpeg | For media | Audio extraction and screenshot generation |
| MeCab + mecab-ipadic | No | Optional fallback tokenizer for Japanese |
@@ -24,16 +25,15 @@
### Optional Tools
| Tool | Purpose |
| ----------------- | ------------------------------------------ |
| fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker |
| Tool | Purpose |
| ----------------- | ------------------------------------------------------------- |
| fzf | Terminal-based video picker (default) |
| rofi | GUI-based video picker |
| chafa | Thumbnail previews in fzf |
| ffmpegthumbnailer | Generate video thumbnails for picker |
| guessit | Better AniSkip title/season/episode parsing for file playback |
| alass | Subtitle sync engine (preferred) |
| ffsubsync | Subtitle sync engine (fallback) |
| Bun | Required for the `subminer` wrapper script |
| alass | Subtitle sync engine (preferred) |
| ffsubsync | Subtitle sync engine (fallback) |
## Linux
@@ -88,7 +88,7 @@ brew install mpv mecab mecab-ipadic
git clone https://github.com/ksyasuda/SubMiner.git
cd SubMiner
bun install
cd vendor/texthooker-ui && pnpm install --frozen-lockfile && pnpm run build && cd ../..
cd vendor/texthooker-ui && bun install --frozen-lockfile && bun run build && cd ../..
bun run build:mac
```

View File

@@ -94,3 +94,5 @@ Read first. Keep concise.
| `codex-architecture-doc-refresh-20260223T033941Z-d6se` | `codex-architecture-doc-refresh` | `Review repository architecture surfaces and refresh docs/architecture.md content to match current code state.` | `handoff` | `docs/subagents/agents/codex-architecture-doc-refresh-20260223T033941Z-d6se.md` | `2026-02-23T03:44:17Z` |
| `codex-docs-video-thumb-cache-20260223T033929Z-k8p2` | `codex-docs-video-thumb-cache` | `Fix docs landing page demo video thumbnail staleness after direct asset replacement.` | `handoff` | `docs/subagents/agents/codex-docs-video-thumb-cache-20260223T033929Z-k8p2.md` | `2026-02-23T03:44:04Z` |
| `codex-development-docs-review-20260223T034520Z-2ebb` | `codex-development-docs-review` | `Review codebase and refresh docs/development.md to match current project state.` | `done` | `docs/subagents/agents/codex-development-docs-review-20260223T034520Z-2ebb.md` | `2026-02-23T03:49:16Z` |
| `opencode-bun-migration-20260223T043000Z-k9m2` | `opencode-bun-migration` | `Execute TASK-115 Bun-only migration plan: parity map, dist/utility script migration, CI/docs cutover.` | `handoff` | `docs/subagents/agents/opencode-bun-migration-20260223T043000Z-k9m2.md` | `2026-02-23T04:36:00Z` |
| `opencode-initial-release-plan-20260223T044059Z-p7k2` | `opencode-initial-release-plan` | `Analyze main history and draft copy/paste initial-release history-cleanup plan.` | `planning` | `docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md` | `2026-02-23T04:40:59Z` |

View File

@@ -0,0 +1,61 @@
# Agent Log: opencode-bun-migration-20260223T043000Z-k9m2
- alias: `opencode-bun-migration`
- mission: `Execute TASK-115 Bun-only migration plan: parity map, dist/utility script migration, CI/docs cutover.`
- status: `handoff`
- started_utc: `2026-02-23T04:30:00Z`
- backlog: `TASK-115` (children: `TASK-115.2`, `TASK-115.3`, `TASK-115.4`, `TASK-115.1`)
## Intent
- Execute user-selected Subagent-Driven option for Bun-only migration.
- Sequence: `TASK-115.2` -> (`TASK-115.3` + `TASK-115.4`) -> `TASK-115.1`.
## Planned Files
- `package.json`
- `.github/workflows/ci.yml`
- `.github/workflows/release.yml`
- `docs/development.md`
- `docs/installation.md`
- `README.md`
- `docs/plans/bun-only-parity-matrix.md`
## Assumptions
- Bun can execute dist test files currently run by `node --test`.
- Electron-targeted utility scripts can keep Electron runtime while shifting command wrappers to Bun.
- No behavior changes intended; tooling/runtime-command migration only.
## Phase Updates
- `2026-02-23T04:30:00Z` start: loaded subagent index/collaboration, created agent row/file, beginning implementation on `TASK-115.2`.
- `2026-02-23T04:36:00Z` completed: migrated dist/utility scripts from Node invocations to Bun, removed Node setup from CI/release workflows, updated setup docs, added Bun parity matrix, and validated full Bun command gate suite.
## Files Touched
- `.github/workflows/ci.yml`
- `.github/workflows/release.yml`
- `package.json`
- `docs/development.md`
- `docs/installation.md`
- `docs/plans/bun-only-parity-matrix.md`
- `launcher/smoke.e2e.test.ts`
- `src/core/services/anilist/anilist-token-store.ts`
- `src/core/services/anilist/anilist-token-store.test.ts`
## Validation
- `bun run test:launcher:smoke:src`
- `bun run test:fast`
- `bun run build`
- `bun run test:config:dist`
- `bun run test:core:dist`
- `bun run test:smoke:dist`
- `bun run docs:build`
- `bun run generate:config-example`
## Handoff
- Backlog status synced: `TASK-115`, `TASK-115.1`, `TASK-115.2`, `TASK-115.3`, `TASK-115.4` set Done with AC evidence.
- Note: `config.example.jsonc` and `docs/public/config.example.jsonc` were regenerated by `bun run generate:config-example` during validation.

View File

@@ -166,3 +166,6 @@ Shared notes. Append-only.
- [2026-02-23T03:44:17Z] [codex-architecture-doc-refresh-20260223T033941Z-d6se|codex-architecture-doc-refresh] completed architecture drift pass: refreshed `docs/architecture.md` structure/service/composition/lifecycle content against current code (`src`, `launcher`, `plugin`), left mermaid sections untouched, and verified `bun run docs:build`; moved backlog linkage to `TASK-113` to avoid active `TASK-112` collision.
- [2026-02-23T03:46:06Z] [codex-development-docs-review-20260223T034520Z-2ebb|codex-development-docs-review] starting user-requested thorough codebase + `docs/development.md` drift audit; scope docs refresh + verification only, no runtime behavior changes expected.
- [2026-02-23T03:49:16Z] [codex-development-docs-review-20260223T034520Z-2ebb|codex-development-docs-review] completed `docs/development.md` refresh: setup/deps/submodule instructions corrected, CI-parity testing lane documented, placeholder subtitle test status clarified, Makefile reference adjusted, env variable table expanded to active launcher/runtime overrides; `bun run docs:build` passed.
- [2026-02-23T04:30:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] starting TASK-115 Bun-only migration execution; initial scope `package.json`, CI/release workflows, and setup docs to remove Node requirements after parity checks.
- [2026-02-23T04:36:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] completed TASK-115 Bun-only migration pass: dist/utility commands moved off direct Node invocation, CI/release Node setup removed, Bun parity matrix + docs updates landed, full Bun validation gate suite passed, and TASK-115 + child tasks finalized Done in Backlog.
- [2026-02-23T04:40:59Z] [opencode-initial-release-plan-20260223T044059Z-p7k2|opencode-initial-release-plan] starting user-requested release-history cleanup planning pass; scope git-history analysis + current-state review + `initial-release.md` command playbook.

View File

@@ -62,7 +62,7 @@ function createSmokeCase(name: string): SmokeCase {
writeExecutable(
fakeMpvPath,
`#!/usr/bin/env node
`#!/usr/bin/env bun
const fs = require('node:fs');
const net = require('node:net');
const path = require('node:path');
@@ -101,7 +101,7 @@ process.on('SIGTERM', closeAndExit);
writeExecutable(
fakeAppPath,
`#!/usr/bin/env node
`#!/usr/bin/env bun
const fs = require('node:fs');
const logPath = ${JSON.stringify(fakeAppLogPath)};

View File

@@ -17,13 +17,13 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts",
"test:config:dist": "node --test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js",
"test:config:smoke:dist": "node --test dist/config/path-resolution.test.js",
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js",
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:core:smoke:dist": "node --test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
"test": "bun run test:config && bun run test:core",
@@ -32,7 +32,7 @@
"test:core": "bun run test:core:src",
"test:subtitle": "bun run build && bun run test:subtitle:dist",
"test:fast": "bun run test:config:src && bun run test:core:src",
"generate:config-example": "bun run build && node dist/generate-config-example.js",
"generate:config-example": "bun run build && bun dist/generate-config-example.js",
"start": "bun run build && electron . --start",
"dev": "bun run build && electron . --start --dev",
"stop": "electron . --stop",

View File

@@ -3,9 +3,8 @@ import assert from 'node:assert/strict';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { safeStorage } from 'electron';
import { createAnilistTokenStore } from './anilist-token-store';
import { createAnilistTokenStore, type SafeStorageLike } from './anilist-token-store';
function createTempTokenFile(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-anilist-token-'));
@@ -20,151 +19,67 @@ function createLogger() {
};
}
type SafeStorageLike = {
isEncryptionAvailable: () => boolean;
encryptString: (value: string) => Buffer;
decryptString: (value: Buffer) => string;
};
const safeStorageApi = safeStorage as unknown as Partial<SafeStorageLike>;
const hasSafeStorage =
typeof safeStorageApi?.isEncryptionAvailable === 'function' &&
typeof safeStorageApi?.encryptString === 'function' &&
typeof safeStorageApi?.decryptString === 'function';
const originalSafeStorage: SafeStorageLike | null = hasSafeStorage
? {
isEncryptionAvailable: safeStorageApi.isEncryptionAvailable as () => boolean,
encryptString: safeStorageApi.encryptString as (value: string) => Buffer,
decryptString: safeStorageApi.decryptString as (value: Buffer) => string,
}
: null;
function mockSafeStorage(encryptionAvailable: boolean): void {
if (!hasSafeStorage) return;
(
safeStorage as unknown as {
isEncryptionAvailable: typeof safeStorage.isEncryptionAvailable;
encryptString: typeof safeStorage.encryptString;
decryptString: typeof safeStorage.decryptString;
}
).isEncryptionAvailable = () => encryptionAvailable;
(
safeStorage as unknown as {
encryptString: typeof safeStorage.encryptString;
decryptString: typeof safeStorage.decryptString;
}
).encryptString = (value: string) => Buffer.from(`enc:${value}`, 'utf-8');
(
safeStorage as unknown as {
decryptString: typeof safeStorage.decryptString;
}
).decryptString = (value: Buffer) => {
const raw = value.toString('utf-8');
return raw.startsWith('enc:') ? raw.slice(4) : raw;
function createStorage(encryptionAvailable: boolean): SafeStorageLike {
return {
isEncryptionAvailable: () => encryptionAvailable,
encryptString: (value: string) => Buffer.from(`enc:${value}`, 'utf-8'),
decryptString: (value: Buffer) => {
const raw = value.toString('utf-8');
return raw.startsWith('enc:') ? raw.slice(4) : raw;
},
};
}
function restoreSafeStorage(): void {
if (!hasSafeStorage || !originalSafeStorage) return;
(
safeStorage as unknown as {
isEncryptionAvailable: typeof safeStorage.isEncryptionAvailable;
encryptString: typeof safeStorage.encryptString;
decryptString: typeof safeStorage.decryptString;
}
).isEncryptionAvailable = originalSafeStorage.isEncryptionAvailable;
(
safeStorage as unknown as {
encryptString: typeof safeStorage.encryptString;
decryptString: typeof safeStorage.decryptString;
}
).encryptString = originalSafeStorage.encryptString;
(
safeStorage as unknown as {
decryptString: typeof safeStorage.decryptString;
}
).decryptString = originalSafeStorage.decryptString;
}
test('anilist token store saves and loads encrypted token', () => {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger(), createStorage(true));
store.saveToken(' demo-token ');
test('anilist token store saves and loads encrypted token', { skip: !hasSafeStorage }, () => {
mockSafeStorage(true);
try {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger());
store.saveToken(' demo-token ');
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
encryptedToken?: string;
plaintextToken?: string;
};
assert.equal(typeof payload.encryptedToken, 'string');
assert.equal(payload.plaintextToken, undefined);
assert.equal(store.loadToken(), 'demo-token');
} finally {
restoreSafeStorage();
}
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
encryptedToken?: string;
plaintextToken?: string;
};
assert.equal(typeof payload.encryptedToken, 'string');
assert.equal(payload.plaintextToken, undefined);
assert.equal(store.loadToken(), 'demo-token');
});
test(
'anilist token store falls back to plaintext when encryption unavailable',
{ skip: !hasSafeStorage },
() => {
mockSafeStorage(false);
try {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger());
store.saveToken('plain-token');
test('anilist token store falls back to plaintext when encryption unavailable', () => {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger(), createStorage(false));
store.saveToken('plain-token');
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
plaintextToken?: string;
};
assert.equal(payload.plaintextToken, 'plain-token');
assert.equal(store.loadToken(), 'plain-token');
} finally {
restoreSafeStorage();
}
},
);
test(
'anilist token store migrates legacy plaintext to encrypted',
{ skip: !hasSafeStorage },
() => {
const filePath = createTempTokenFile();
fs.writeFileSync(
filePath,
JSON.stringify({ plaintextToken: 'legacy-token', updatedAt: Date.now() }),
'utf-8',
);
mockSafeStorage(true);
try {
const store = createAnilistTokenStore(filePath, createLogger());
assert.equal(store.loadToken(), 'legacy-token');
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
encryptedToken?: string;
plaintextToken?: string;
};
assert.equal(typeof payload.encryptedToken, 'string');
assert.equal(payload.plaintextToken, undefined);
} finally {
restoreSafeStorage();
}
},
);
test('anilist token store clears persisted token file', { skip: !hasSafeStorage }, () => {
mockSafeStorage(true);
try {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger());
store.saveToken('to-clear');
assert.equal(fs.existsSync(filePath), true);
store.clearToken();
assert.equal(fs.existsSync(filePath), false);
} finally {
restoreSafeStorage();
}
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
plaintextToken?: string;
};
assert.equal(payload.plaintextToken, 'plain-token');
assert.equal(store.loadToken(), 'plain-token');
});
test('anilist token store migrates legacy plaintext to encrypted', () => {
const filePath = createTempTokenFile();
fs.writeFileSync(
filePath,
JSON.stringify({ plaintextToken: 'legacy-token', updatedAt: Date.now() }),
'utf-8',
);
const store = createAnilistTokenStore(filePath, createLogger(), createStorage(true));
assert.equal(store.loadToken(), 'legacy-token');
const payload = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as {
encryptedToken?: string;
plaintextToken?: string;
};
assert.equal(typeof payload.encryptedToken, 'string');
assert.equal(payload.plaintextToken, undefined);
});
test('anilist token store clears persisted token file', () => {
const filePath = createTempTokenFile();
const store = createAnilistTokenStore(filePath, createLogger(), createStorage(true));
store.saveToken('to-clear');
assert.equal(fs.existsSync(filePath), true);
store.clearToken();
assert.equal(fs.existsSync(filePath), false);
});

View File

@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { safeStorage } from 'electron';
import * as electron from 'electron';
interface PersistedTokenPayload {
encryptedToken?: string;
@@ -14,6 +14,12 @@ export interface AnilistTokenStore {
clearToken: () => void;
}
export interface SafeStorageLike {
isEncryptionAvailable: () => boolean;
encryptString: (value: string) => Buffer;
decryptString: (value: Buffer) => string;
}
function ensureDirectory(filePath: string): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
@@ -33,6 +39,7 @@ export function createAnilistTokenStore(
warn: (message: string, details?: unknown) => void;
error: (message: string, details?: unknown) => void;
},
storage: SafeStorageLike = electron.safeStorage,
): AnilistTokenStore {
return {
loadToken(): string | null {
@@ -44,11 +51,11 @@ export function createAnilistTokenStore(
const parsed = JSON.parse(raw) as PersistedTokenPayload;
if (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) {
const encrypted = Buffer.from(parsed.encryptedToken, 'base64');
if (!safeStorage.isEncryptionAvailable()) {
if (!storage.isEncryptionAvailable()) {
logger.warn('AniList token encryption is not available on this system.');
return null;
}
const decrypted = safeStorage.decryptString(encrypted).trim();
const decrypted = storage.decryptString(encrypted).trim();
return decrypted.length > 0 ? decrypted : null;
}
if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) {
@@ -70,7 +77,7 @@ export function createAnilistTokenStore(
return;
}
try {
if (!safeStorage.isEncryptionAvailable()) {
if (!storage.isEncryptionAvailable()) {
logger.warn('AniList token encryption unavailable; storing token in plaintext fallback.');
writePayload(filePath, {
plaintextToken: trimmed,
@@ -78,7 +85,7 @@ export function createAnilistTokenStore(
});
return;
}
const encrypted = safeStorage.encryptString(trimmed);
const encrypted = storage.encryptString(trimmed);
writePayload(filePath, {
encryptedToken: encrypted.toString('base64'),
updatedAt: Date.now(),