# AGENTS.md Context for OpenCode sessions working on the **Dynamic Deck** Stream Deck plugin (`com.ryan-kegel.dynamic-deck`). ## What this is An Elgato Stream Deck plugin (SDK v3, Node.js 24) built with TypeScript + Rollup. Some actions are **Windows-only** because they shell out to PowerShell and use Win32 P/Invoke via the `koffi` FFI (`user32.dll`, `kernel32.dll`). Other actions are platform-agnostic. ## Commands - `npm run build` — rollup production build to `com.ryan-kegel.dynamic-deck.sdPlugin/bin/plugin.js` (terser-minified, no sourcemap). - `npm run watch` — rollup watch with sourcemaps; on every rebuild triggers `streamdeck restart com.ryan-kegel.dynamic-deck`. - `node generate-assets.cjs` — writes 1x1 placeholder PNGs into `imgs/plugin/` and `imgs/actions/`. Run once after cloning if those images are missing. - There is **no test runner, linter, or typecheck script**. The only verification is `npm run build` (rollup runs the TS plugin, so type errors will surface here). ## Source layout - `src/plugin.ts` — entrypoint. Registers actions, wires `AppMonitor` (1s polling for foreground-window changes) and `AppTrackerService` (persisted to `app-stats.json` in cwd), handles SIGINT/SIGTERM to flush stats, then `streamDeck.connect()`s. - `src/actions/.ts` — one `SingletonAction` subclass per Stream Deck action, each `@action({ UUID: "com.ryan-kegel.dynamic-deck." })`. - `src/services/` — `app-monitor.ts` (Win32 FFI foreground tracking), `app-tracker.ts` (launch stats / ranking), `blacklist-manager.ts`. - `src/utils/app-launcher.ts` — Start Menu `.lnk` scanning + 256x256 icon extraction via PowerShell + C# `PrivateExtractIcons` P/Invoke. - `src/ui/.html` + `*.js` — Property Inspector assets, copied as-is to the `.sdPlugin` bundle. - `com.ryan-kegel.dynamic-deck.sdPlugin/` — the built plugin bundle (output). `manifest.json`, `bin/plugin.js`, copied `ui/`, `imgs/`, `scripts/`, `services/`, `utils/`. This is what Stream Deck loads. ## Conventions and gotchas - **Bundle shape is non-obvious.** `rollup.config.mjs` copies `src/{ui,imgs,scripts,services,utils}` into the `.sdPlugin/` folder alongside the built `bin/plugin.js`. Edits to those files in the source tree won't appear until a rebuild copies them. The TS files copied under `services/` and `utils/` are **not** the ones Rollup bundles — they are inert copies; the bundled code is what ends up in `bin/plugin.js`. - `RollupWatchPlugin` emits `{"type":"module"}` into `bin/package.json` so the host can `import()` the bundle. Don't hand-edit that file. - `manifest.json` is treated as a watched file; changes trigger a rebuild. - Several actions are currently commented out in `plugin.ts` (e.g. `IncrementCounter`, `EcobeeModeAction`, `EcobeeAction`). To re-enable one, uncomment both the import and the `registerAction` line. - The `app-stats.json` tracker file is written next to `process.cwd()` — i.e. inside the `.sdPlugin/` folder at runtime. Don't delete it without expecting reset ranking data. - `launch-app.ts` uses `ev.action.setTitle()` to write to Stream Deck's built-in Title field — do **not** add a custom title `` in `launch-app.html`, you'll get two fields (see README §2). - `node` engine is pinned to v24 via `manifest.json` (`"Debug": "enabled"`); debug by attaching VS Code to the running plugin process (the "Attach to Plugin" launch config in `.vscode/launch.json`). - `tsconfig.json` extends `@tsconfig/node20` and uses `customConditions: ["node"]` + `moduleResolution: "Bundler"` to match Rollup's resolution mode. ## Things explicitly NOT to do - Don't run `tsc` directly — there's no script and no outDir. The TS plugin runs as part of Rollup. - Don't add a `test` / `lint` script "just in case" — verify with the user first; none have been configured and the user may not want them. - Don't assume macOS support exists for app launching features — `app-launcher.ts` and `app-monitor.ts` use Windows-specific APIs.