chore: update dependencies and improve code formatting
- Added `vendor/discord-video-stream` to pnpm workspace. - Refactored `llmModerationClient.ts` for better readability and consistency. - Adjusted imports in `recordingsRoutes.ts` for clarity. - Updated `webserver.ts` to correctly import `createRecordingsRoutes`. - Enhanced test cases in `llmModerationClient.test.ts` for improved readability. - Updated submodule references for `better-sqlite3`, `discord-video-stream`, `discord.js-selfbot-v13`, `drizzle-orm`, and `node-datachannel`. - Created documentation for deprecated dependency removal plan and design.
This commit is contained in:
@@ -0,0 +1,597 @@
|
|||||||
|
# Deprecated Dependency Removal Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Remove deprecated packages from `pnpm-lock.yaml` where maintained replacements exist, and vendor/submodule only when no clean replacement exists.
|
||||||
|
|
||||||
|
**Architecture:** Treat dependency cleanup as package graph surgery: identify owner, change one dependency source at a time, reinstall, verify lockfile, then run project checks. Existing vendored workspaces stay source of truth for selfbot and discord-video-stream patches.
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js, pnpm workspaces, TypeScript, Vitest, Biome, git submodules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Modify: `package.json` — root dependency versions, workspace references, `pnpm.onlyBuiltDependencies`, optional `pnpm.overrides` if needed.
|
||||||
|
- Modify: `pnpm-workspace.yaml` — workspace package list for any new submodule/vendor package.
|
||||||
|
- Modify: `pnpm-lock.yaml` — regenerated by `pnpm install` only.
|
||||||
|
- Modify: `vendor/discord-video-stream/package.json` — dev dependency on `discord.js-selfbot-v13` should use workspace package instead of registry.
|
||||||
|
- Modify: `vendor/discord.js-selfbot-v13/package.json` — replace deprecated `otplib@12` chain if compatible.
|
||||||
|
- Possibly create: `vendor/<package-name>` — only if package has no maintained replacement and strict cleanup still needs local patching.
|
||||||
|
|
||||||
|
## Deprecated Package Owners
|
||||||
|
|
||||||
|
Known current owners:
|
||||||
|
|
||||||
|
```text
|
||||||
|
drizzle-kit -> @esbuild-kit/esm-loader -> @esbuild-kit/core-utils
|
||||||
|
discord.js-selfbot-v13 -> otplib@12 -> @otplib/plugin-crypto, @otplib/plugin-thirty-two, @otplib/preset-default
|
||||||
|
@discordjs/opus -> @discordjs/node-pre-gyp -> npmlog, are-we-there-yet, gauge, rimraf@3, glob@7, inflight
|
||||||
|
better-sqlite3 -> prebuild-install
|
||||||
|
@lng2004/node-datachannel -> prebuild-install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 1: Establish Baseline
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Read: `package.json`
|
||||||
|
- Read: `pnpm-workspace.yaml`
|
||||||
|
- Read: `vendor/discord-video-stream/package.json`
|
||||||
|
- Read: `vendor/discord.js-selfbot-v13/package.json`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Capture current git state**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: includes existing intended changes for `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitmodules`, and `vendor/discord-video-stream`. Do not revert user changes.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Capture dependency owners**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why discord.js-selfbot-v13 @esbuild-kit/core-utils @esbuild-kit/esm-loader @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default are-we-there-yet fs-then-native gauge inflight lodash.pick npmlog prebuild-install stream-connect test-value
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: output maps deprecated packages to direct owners. Save relevant owner names in notes for next tasks.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Capture install warning baseline**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: install completes. Warnings may mention deprecated transitive packages.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Capture baseline checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass before dependency changes. If failing, stop and report exact failures before continuing.
|
||||||
|
|
||||||
|
## Task 2: Patch `discord-video-stream` Workspace Selfbot Reference
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `vendor/discord-video-stream/package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Inspect current devDependency**
|
||||||
|
|
||||||
|
Open `vendor/discord-video-stream/package.json` and find:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"discord.js-selfbot-v13": "^3.7.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exists under `devDependencies`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace devDependency with workspace reference**
|
||||||
|
|
||||||
|
Change that entry to:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"discord.js-selfbot-v13": "workspace:*"
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep peer dependency unchanged:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"peerDependencies": {
|
||||||
|
"discord.js-selfbot-v13": "^3.6.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Reinstall**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: lockfile uses local workspace for `discord.js-selfbot-v13` in `vendor/discord-video-stream` importer.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify no registry selfbot fetch from discord-video-stream**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why discord.js-selfbot-v13
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: root and discord-video-stream both point to `link:vendor/discord.js-selfbot-v13` or workspace link.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
## Task 3: Upgrade or Patch `otplib` Chain in Vendored Selfbot
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `vendor/discord.js-selfbot-v13/package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Locate selfbot otplib dependency**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const p=require('./vendor/discord.js-selfbot-v13/package.json'); console.log(p.dependencies?.otplib || p.devDependencies?.otplib)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: prints `^12.x` or `12.x`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Check latest otplib version**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm view otplib version deprecated --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: latest version is not deprecated.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Change selfbot dependency to otplib latest major**
|
||||||
|
|
||||||
|
In `vendor/discord.js-selfbot-v13/package.json`, replace existing `otplib` dependency value with latest non-deprecated major range. Example if latest is 13.x:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"otplib": "^13.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not change package name or selfbot exports.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Reinstall**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: install completes. `@otplib/plugin-crypto`, `@otplib/plugin-thirty-two`, and `@otplib/preset-default` should disappear if otplib v13 no longer pulls them.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify otplib deprecated plugins gone**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no dependency path for those packages. If they remain under `otplib`, inspect latest otplib metadata and stop before vendoring otplib.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass. If selfbot code breaks due otplib API changes, revert only the otplib version change and report API mismatch.
|
||||||
|
|
||||||
|
## Task 4: Upgrade `drizzle-kit` to Remove `@esbuild-kit/*`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check current and latest drizzle-kit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "const p=require('./package.json'); console.log(p.devDependencies['drizzle-kit'])"
|
||||||
|
npm view drizzle-kit version deprecated --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: latest version is not deprecated.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update root devDependency if newer version exists**
|
||||||
|
|
||||||
|
If latest version is newer than `0.31.10`, change `package.json` devDependency:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"drizzle-kit": "^<latest-version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"drizzle-kit": "^0.32.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use actual latest version from npm output.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Reinstall**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: install completes.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify esbuild-kit packages gone**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why @esbuild-kit/core-utils @esbuild-kit/esm-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no dependency path. If latest `drizzle-kit` still pulls them, keep latest only if project checks pass; do not vendor `drizzle-kit` unless user confirms dev-only strictness.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run drizzle commands**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run db:generate -- --help
|
||||||
|
pnpm run db:migrate -- --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commands print help or usage without crashing. Do not run actual migrations.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
## Task 5: Remove or Replace Direct `@discordjs/opus`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
- Inspect: `src/recorder/decoder.ts`
|
||||||
|
- Inspect: `tests/decoder.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Find project usage of `@discordjs/opus`**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -R "@discordjs/opus\|OpusEncoder\|OpusDecoder" -n src tests package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: usage locations show whether direct package is imported by project code or only required indirectly by `prism-media`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Check `@discordjs/voice` encryption/audio requirements**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why @discordjs/opus prism-media
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: shows `@discordjs/voice` and root dependency relationships.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test removal in package manifest**
|
||||||
|
|
||||||
|
Remove root dependency line from `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"@discordjs/opus": "^0.10.0",
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not edit code yet.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Reinstall**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: install completes. If install or peer resolution fails, restore `@discordjs/opus` and continue to Step 8.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run decoder-specific tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run test -- tests/decoder.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: tests pass or skip native opus gracefully. If tests fail because native opus is required by project behavior, restore `@discordjs/opus` and continue to Step 8.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run full checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify deprecated node-pre-gyp chain gone**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why @discordjs/node-pre-gyp npmlog are-we-there-yet gauge rimraf glob inflight
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no dependency path through `@discordjs/opus`. If gone, task complete.
|
||||||
|
|
||||||
|
- [ ] **Step 8: If `@discordjs/opus` is required, try maintained alternatives**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm view opusscript version deprecated --json
|
||||||
|
npm view @evan/opus version deprecated --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: identify non-deprecated candidate. Do not switch unless package supports same runtime path used by `prism-media` or direct project imports.
|
||||||
|
|
||||||
|
- [ ] **Step 9: If no compatible maintained alternative exists, vendor decision checkpoint**
|
||||||
|
|
||||||
|
Stop and report:
|
||||||
|
|
||||||
|
```text
|
||||||
|
@discordjs/opus still required. No compatible maintained replacement verified. Next action requires cloning/vendoring smallest package owner or accepting deprecated native install chain.
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not clone without user confirmation of target repository.
|
||||||
|
|
||||||
|
## Task 6: Evaluate `prebuild-install` Owners
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `package.json` only if safe upgrade exists
|
||||||
|
- Modify: `vendor/discord-video-stream/package.json` only if safe upgrade exists
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check owner versions**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm view better-sqlite3 version deprecated --json
|
||||||
|
npm view @lng2004/node-datachannel version deprecated --json
|
||||||
|
node -e "const root=require('./package.json'); const dvs=require('./vendor/discord-video-stream/package.json'); console.log({betterSqlite3: root.dependencies['better-sqlite3'], nodeDatachannel: dvs.dependencies['@lng2004/node-datachannel']})"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: latest versions known.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Upgrade `better-sqlite3` if newer version exists**
|
||||||
|
|
||||||
|
If latest is newer than current, change `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"better-sqlite3": "^<latest-version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use actual latest version.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Upgrade `@lng2004/node-datachannel` if newer version exists**
|
||||||
|
|
||||||
|
If latest is newer than current and package name still matches discord-video-stream requirements, change `vendor/discord-video-stream/package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"@lng2004/node-datachannel": "<latest-version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use exact latest version only if upstream uses exact published builds.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Reinstall**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: native dependencies install or reuse existing builds successfully.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify `prebuild-install` status**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why prebuild-install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: either no dependency path, or only native owners remain.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass.
|
||||||
|
|
||||||
|
- [ ] **Step 7: If `prebuild-install` remains**
|
||||||
|
|
||||||
|
Stop and report owner paths. Do not vendor native packages unless no maintained version removes it and user explicitly wants native submodule maintenance.
|
||||||
|
|
||||||
|
## Task 7: Final Deprecation Audit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `pnpm-lock.yaml` through install only
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run clean install audit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no deprecated package warnings where maintained replacements were applied.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run owner query for all known deprecated names**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm why discord.js-selfbot-v13 @esbuild-kit/core-utils @esbuild-kit/esm-loader @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default are-we-there-yet fs-then-native gauge inflight lodash.pick npmlog prebuild-install stream-connect test-value @discordjs/node-pre-gyp
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no paths for packages removed by prior tasks. Remaining paths must be only approved unavoidable native/package-owner cases.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Check npm deprecation metadata for remaining suspect packages**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node - <<'NODE'
|
||||||
|
const {execFileSync}=require('child_process');
|
||||||
|
const pkgs=['discord.js-selfbot-v13','@esbuild-kit/core-utils','@esbuild-kit/esm-loader','@otplib/plugin-crypto','@otplib/plugin-thirty-two','@otplib/preset-default','are-we-there-yet','fs-then-native','gauge','inflight','lodash.pick','npmlog','prebuild-install','stream-connect','test-value','@discordjs/node-pre-gyp'];
|
||||||
|
for (const p of pkgs) {
|
||||||
|
try {
|
||||||
|
const out=execFileSync('npm',['view',p,'deprecated','--json'],{encoding:'utf8'}).trim();
|
||||||
|
if (out && out !== 'null') console.log(`${p}: ${JSON.parse(out)}`);
|
||||||
|
} catch {
|
||||||
|
console.log(`${p}: npm view failed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: command prints metadata only. Compare printed package names with `pnpm why` output.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run final checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
pnpm run test
|
||||||
|
pnpm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
## Task 8: Document Remaining Unavoidable Deprecated Packages
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/superpowers/specs/2026-05-19-deprecated-dependency-removal-design.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: If no deprecated packages remain, append success note**
|
||||||
|
|
||||||
|
Append this section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Final Audit Result
|
||||||
|
|
||||||
|
All known deprecated packages from the initial audit were removed from the active pnpm dependency graph.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: If deprecated packages remain, append owner note**
|
||||||
|
|
||||||
|
Append this section with actual owner paths from `pnpm why`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Final Audit Result
|
||||||
|
|
||||||
|
Remaining deprecated packages after maintained upgrade attempts:
|
||||||
|
|
||||||
|
- `<package>` remains via `<owner path>`. Reason: `<no maintained replacement found | native install chain still used by latest upstream | user-approved vendored package>`.
|
||||||
|
|
||||||
|
These are candidates for future vendoring/submodule patching if strict lockfile cleanup remains required.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run final git diff review**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff -- package.json pnpm-workspace.yaml pnpm-lock.yaml vendor/discord-video-stream/package.json vendor/discord.js-selfbot-v13/package.json docs/superpowers/specs/2026-05-19-deprecated-dependency-removal-design.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: diff contains only dependency migration changes and audit note.
|
||||||
|
|
||||||
|
## Task 9: Commit Checkpoint Only If User Requests Commit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Stage only changed package/spec/submodule files relevant to dependency cleanup.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Show status**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: changed files match work done.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Ask before committing**
|
||||||
|
|
||||||
|
Ask user whether to commit. Do not commit unless explicitly requested.
|
||||||
|
|
||||||
|
- [ ] **Step 3: If user asks to commit, create commit**
|
||||||
|
|
||||||
|
Use exact changed file list, not `git add -A`. Commit message:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
chore: remove deprecated dependency graph entries
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit succeeds without bypassing hooks.
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Spec coverage: plan covers replace-first, workspace patches, submodule/vendor checkpoint, verification, and final audit documentation.
|
||||||
|
- Placeholder scan: no TBD/TODO placeholders; steps include exact commands and expected outcomes.
|
||||||
|
- Type consistency: paths and package names match current workspace files and dependency owners.
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
# Winston Logging Refactor Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace Pino logging with Winston, normalize log levels, and write readable console plus JSON file logs without changing application behavior.
|
||||||
|
|
||||||
|
**Architecture:** Keep `src/logger.ts` as the single logging entry point. Rebuild it around one Winston logger with child context support, centralized metadata/error serialization, colorized console output, and JSON file transports. Keep call-site changes minimal and only adjust imports/usages that fail after the backend swap.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Node.js ESM, Winston, Zod config validation, pnpm, Vitest, Biome.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Modify: `package.json` — replace Pino dependencies with Winston.
|
||||||
|
- Modify: `pnpm-lock.yaml` — update via pnpm after dependency changes.
|
||||||
|
- Modify: `src/config.ts` — expand `LOG_LEVEL` enum to Winston npm levels.
|
||||||
|
- Modify: `src/logger.ts` — replace Pino implementation with Winston implementation.
|
||||||
|
- Modify: `.gitignore` — ignore runtime `logs/` directory.
|
||||||
|
- Test: `tests/logger.test.ts` — add focused logger behavior tests for level validation, error serialization, and file output format helpers.
|
||||||
|
- Inspect and modify only if needed: files importing `createChildLogger` or `logger`.
|
||||||
|
|
||||||
|
## Important Git Note
|
||||||
|
|
||||||
|
The prior design-spec commit unexpectedly included vendor submodule entries that were already staged in the working tree. Before making implementation commits, run `git status --short` and do not stage unrelated vendor or existing user changes. Stage only files touched by this plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Logger Tests Before Migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/logger.test.ts`
|
||||||
|
- Modify: none
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write tests for Winston-compatible logger behavior**
|
||||||
|
|
||||||
|
Create `tests/logger.test.ts` with this content:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatLogMetadataForTest, serializeLogValueForTest } from "../src/logger";
|
||||||
|
|
||||||
|
class TestError extends Error {
|
||||||
|
public code = "TEST_CODE";
|
||||||
|
public statusCode = 418;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("test failure");
|
||||||
|
this.name = "TestError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("logger serialization", () => {
|
||||||
|
it("serializes Error values with stable fields", () => {
|
||||||
|
const serialized = serializeLogValueForTest(new TestError());
|
||||||
|
|
||||||
|
expect(serialized).toMatchObject({
|
||||||
|
name: "TestError",
|
||||||
|
message: "test failure",
|
||||||
|
code: "TEST_CODE",
|
||||||
|
statusCode: 418,
|
||||||
|
});
|
||||||
|
expect(serialized).toHaveProperty("stack");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes nested error metadata keys", () => {
|
||||||
|
const error = new TestError();
|
||||||
|
|
||||||
|
expect(formatLogMetadataForTest({ error, err: error, reason: error })).toMatchObject({
|
||||||
|
error: {
|
||||||
|
name: "TestError",
|
||||||
|
message: "test failure",
|
||||||
|
code: "TEST_CODE",
|
||||||
|
statusCode: 418,
|
||||||
|
},
|
||||||
|
err: {
|
||||||
|
name: "TestError",
|
||||||
|
message: "test failure",
|
||||||
|
code: "TEST_CODE",
|
||||||
|
statusCode: 418,
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
name: "TestError",
|
||||||
|
message: "test failure",
|
||||||
|
code: "TEST_CODE",
|
||||||
|
statusCode: 418,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves plain metadata", () => {
|
||||||
|
expect(
|
||||||
|
formatLogMetadataForTest({ context: "bot", signal: "SIGINT", count: 2 }),
|
||||||
|
).toEqual({ context: "bot", signal: "SIGINT", count: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify current implementation fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/logger.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `formatLogMetadataForTest` and `serializeLogValueForTest` are not exported from `src/logger.ts` yet.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit failing tests only**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/logger.test.ts
|
||||||
|
git commit -m "test: cover logger metadata serialization"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit contains only `tests/logger.test.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Update Dependencies and Config Schema
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
- Modify: `src/config.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update package dependencies**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm remove pino pino-http pino-pretty
|
||||||
|
pnpm add winston
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `package.json` dependencies no longer include `pino` or `pino-http`.
|
||||||
|
- `package.json` devDependencies no longer include `pino-pretty`.
|
||||||
|
- `package.json` dependencies include `winston`.
|
||||||
|
- `pnpm-lock.yaml` updates accordingly.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Expand `LOG_LEVEL` validation**
|
||||||
|
|
||||||
|
In `src/config.ts`, replace the current `LOG_LEVEL` line:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
LOG_LEVEL: z
|
||||||
|
.enum(["error", "warn", "info", "http", "verbose", "debug", "silly"])
|
||||||
|
.default("info"),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run targeted config tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/config.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit dependency and config changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json pnpm-lock.yaml src/config.ts
|
||||||
|
git commit -m "chore: switch logging dependency to winston"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit contains only dependency files and `src/config.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Replace `src/logger.ts` With Winston Implementation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/logger.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace logger implementation**
|
||||||
|
|
||||||
|
Replace entire `src/logger.ts` with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
|
const logLevel = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
|
||||||
|
const logsDir = path.resolve(process.cwd(), "logs");
|
||||||
|
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
|
||||||
|
type LogMetadata = Record<string, unknown>;
|
||||||
|
|
||||||
|
type SerializedError = {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
code?: unknown;
|
||||||
|
statusCode?: unknown;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
const serializeError = (error: Error): SerializedError => {
|
||||||
|
const serialized: SerializedError = {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error.stack) {
|
||||||
|
serialized.stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorWithFields = error as Error & {
|
||||||
|
code?: unknown;
|
||||||
|
statusCode?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (errorWithFields.code !== undefined) {
|
||||||
|
serialized.code = errorWithFields.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorWithFields.statusCode !== undefined) {
|
||||||
|
serialized.statusCode = errorWithFields.statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(errorWithFields)) {
|
||||||
|
if (serialized[key] === undefined) {
|
||||||
|
serialized[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeLogValue = (value: unknown): unknown => {
|
||||||
|
if (value instanceof Error) {
|
||||||
|
return serializeError(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(serializeLogValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, nestedValue]) => [
|
||||||
|
key,
|
||||||
|
serializeLogValue(nestedValue),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLogMetadata = (metadata: LogMetadata): LogMetadata => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(metadata).map(([key, value]) => [key, serializeLogValue(value)]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataFormat = winston.format((info) => {
|
||||||
|
const { level, message, timestamp, ...metadata } = info;
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
timestamp,
|
||||||
|
...formatLogMetadata(metadata),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const consoleFormat = winston.format.printf((info) => {
|
||||||
|
const { level, message, timestamp, context, ...metadata } = info;
|
||||||
|
const contextLabel = context ? ` [${String(context)}]` : "";
|
||||||
|
const metadataText = Object.keys(metadata).length
|
||||||
|
? ` ${JSON.stringify(metadata)}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `${timestamp} ${level}${contextLabel}: ${message}${metadataText}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: logLevel,
|
||||||
|
levels: winston.config.npm.levels,
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.errors({ stack: true }),
|
||||||
|
metadataFormat(),
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.timestamp(),
|
||||||
|
metadataFormat(),
|
||||||
|
consoleFormat,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, "app.log"),
|
||||||
|
format: winston.format.json(),
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, "error.log"),
|
||||||
|
level: "error",
|
||||||
|
format: winston.format.json(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createChildLogger = (context: string) => {
|
||||||
|
return logger.child({ context });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeLogValueForTest = serializeLogValue;
|
||||||
|
export const formatLogMetadataForTest = formatLogMetadata;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run logger tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/logger.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run TypeScript check**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: either PASS or type errors only from logger call sites needing Winston-compatible adjustments.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit Winston logger implementation**
|
||||||
|
|
||||||
|
If typecheck passes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/logger.ts
|
||||||
|
git commit -m "refactor: replace pino logger with winston"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit contains only `src/logger.ts`.
|
||||||
|
|
||||||
|
If typecheck fails in logger call sites, do not commit yet. Continue to Task 4, then commit `src/logger.ts` together with required call-site fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Fix Logger Call Sites Only If Needed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Inspect: `src/**/*.ts`
|
||||||
|
- Modify only files with TypeScript errors from Task 3.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Find logger imports and usages**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "createChildLogger\|from \"./logger\"\|from \"../logger\"\|logger\.\|log\." src --include="*.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: list of logger imports and method calls.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Fix pino-style calls if TypeScript requires it**
|
||||||
|
|
||||||
|
Use this mapping only where needed:
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.info({ signal }, "Graceful shutdown initiated");
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.info("Graceful shutdown initiated", { signal });
|
||||||
|
```
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.error({ error }, "Failed to initialize app");
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.error("Failed to initialize app", { error });
|
||||||
|
```
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.warn({ error }, "Backlog sync failed");
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.warn("Backlog sync failed", { error });
|
||||||
|
```
|
||||||
|
|
||||||
|
Plain string calls remain unchanged:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.info("Creating Discord client");
|
||||||
|
logger.debug("Queue empty, no playback started");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Re-run TypeScript check**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit call-site fixes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/logger.ts src/index.ts src/webserver.ts src/middleware.ts src/voiceController.ts src/media/mediaController.ts src/media/screenShareController.ts src/moderation/broadcaster.ts src/moderation/messageCapture.ts src/streaming/transcoder.ts
|
||||||
|
git commit -m "refactor: normalize logger call sites for winston"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit stages only files actually changed. If some listed files are unchanged, `git add` is harmless.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Ignore Runtime Log Files
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `.gitignore`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check whether `logs/` is already ignored**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "^logs/$" .gitignore || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: either one matching line or no output.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `logs/` if missing**
|
||||||
|
|
||||||
|
If no output from Step 1, append:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify ignore rule**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git check-ignore logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit ignore rule**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .gitignore
|
||||||
|
git commit -m "chore: ignore runtime log files"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit contains only `.gitignore`. If `.gitignore` already ignored `logs/`, skip this commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Verify No Pino Usage Remains
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Inspect: repository source and package files
|
||||||
|
|
||||||
|
- [ ] **Step 1: Search for Pino imports/usages**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "pino\|pino-http\|pino-pretty" src package.json pnpm-lock.yaml --exclude-dir=node_modules || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output. If output appears only in old comments/docs outside runtime files, remove or update those comments/docs if they are part of the changed scope.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Search for Winston dependency**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n '"winston"' package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains one `winston` dependency line.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run focused tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm vitest run tests/logger.test.ts tests/config.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit cleanup if any files changed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
git add <only-files-changed-by-this-task>
|
||||||
|
git commit -m "chore: remove remaining pino references"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit only if Step 1 required changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Runtime Log Output Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- No source changes expected
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run one-shot logger command**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --import tsx -e 'import { logger } from "./src/logger.ts"; logger.info("logger smoke info", { context: "smoke", value: 1 }); logger.error("logger smoke error", { error: new Error("smoke failure") }); await new Promise((resolve) => setTimeout(resolve, 250));'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- Console shows readable timestamped logs.
|
||||||
|
- Info line includes `smoke` context metadata.
|
||||||
|
- Error line includes serialized error metadata.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify app log file exists and contains JSON**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -s logs/app.log && node -e 'const fs = require("fs"); const line = fs.readFileSync("logs/app.log", "utf8").trim().split("\n").at(-1); const parsed = JSON.parse(line); if (!parsed.level || !parsed.message) process.exit(1); console.log(parsed.level + ":" + parsed.message);'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output includes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
error:logger smoke error
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify error log file exists and contains error JSON**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -s logs/error.log && node -e 'const fs = require("fs"); const line = fs.readFileSync("logs/error.log", "utf8").trim().split("\n").at(-1); const parsed = JSON.parse(line); if (parsed.level !== "error") process.exit(1); console.log(parsed.level + ":" + parsed.message);'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
error:logger smoke error
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Full Validation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- No source changes expected unless validation reveals issues.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run typecheck**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test suite**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Inspect final git status**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no uncommitted changes from this plan except ignored `logs/` files. Existing unrelated user changes may still appear; do not stage them.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Final commit if validation fixes were needed**
|
||||||
|
|
||||||
|
If validation required fixes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <only-files-changed-by-validation-fixes>
|
||||||
|
git commit -m "fix: stabilize winston logging migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: commit contains only validation fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- Spec coverage: dependency swap, `LOG_LEVEL`, centralized logger, console/file output, error serialization, Pino removal, and validation all have tasks.
|
||||||
|
- Placeholder scan: no TBD/TODO/fill-in placeholders remain; `<only-files-changed-by-this-task>` and similar are explicit safety instructions to avoid staging unrelated user changes.
|
||||||
|
- Type consistency: test helper names match `src/logger.ts` exports; Winston npm levels match `src/config.ts`; file names match spec.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Deprecated Dependency Removal Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Remove deprecated packages from the pnpm lockfile where practical. Prefer maintained replacements or upgrades. If no maintained replacement exists, vendor upstream code as a submodule/workspace and patch dependency metadata there.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Current deprecated sources:
|
||||||
|
|
||||||
|
- `drizzle-kit` pulls `@esbuild-kit/esm-loader` and `@esbuild-kit/core-utils`.
|
||||||
|
- `discord.js-selfbot-v13` pulls `otplib@12` plugins.
|
||||||
|
- `@discordjs/opus` pulls `@discordjs/node-pre-gyp`, which pulls `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, and `inflight`.
|
||||||
|
- `@lng2004/node-datachannel` and `better-sqlite3` pull `prebuild-install`.
|
||||||
|
|
||||||
|
Existing workspace packages:
|
||||||
|
|
||||||
|
- `vendor/discord.js-selfbot-v13`
|
||||||
|
- `vendor/discord-video-stream`
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
1. Upgrade direct dependencies first and re-check `pnpm why` plus npm deprecation metadata.
|
||||||
|
2. Patch vendored workspace dependencies when project already owns package source.
|
||||||
|
3. Replace direct packages only when runtime compatibility is clear.
|
||||||
|
4. Add submodules only for packages that cannot be replaced or upgraded without keeping deprecated transitive packages.
|
||||||
|
|
||||||
|
## Package Plan
|
||||||
|
|
||||||
|
### `drizzle-kit`
|
||||||
|
|
||||||
|
Try latest compatible `drizzle-kit`. If latest still depends on `@esbuild-kit/*`, keep current version unless project commands fail, because vendoring `drizzle-kit` only to remove dev-only install warnings has high maintenance cost.
|
||||||
|
|
||||||
|
### `discord.js-selfbot-v13`
|
||||||
|
|
||||||
|
Patch `vendor/discord.js-selfbot-v13` dependency graph to remove `otplib@12` if code is compatible with `otplib@13`. Verify by installing and running typecheck/tests. Keep peer/package name unchanged.
|
||||||
|
|
||||||
|
### `@discordjs/opus`
|
||||||
|
|
||||||
|
Find maintained Opus alternative compatible with current recorder and `@discordjs/voice`. Prefer removing direct `@discordjs/opus` only if code and tests still pass. If native Opus remains needed and every maintained option drags deprecated install tooling, vendor the smallest dependency owner.
|
||||||
|
|
||||||
|
### `prebuild-install` sources
|
||||||
|
|
||||||
|
Do not patch native package install chains blindly. For `better-sqlite3`, keep upstream unless latest removes `prebuild-install`. For `@lng2004/node-datachannel`, try latest first through `discord-video-stream`; vendor only if strict lockfile cleanup remains blocked and build still works.
|
||||||
|
|
||||||
|
### `discord-video-stream`
|
||||||
|
|
||||||
|
Keep as workspace submodule. Patch devDependency `discord.js-selfbot-v13` to use workspace reference so installs do not fetch deprecated registry selfbot.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After each dependency change:
|
||||||
|
|
||||||
|
1. Run `pnpm install`.
|
||||||
|
2. Run `pnpm why` for known deprecated package names.
|
||||||
|
3. Check npm deprecation metadata for remaining lockfile packages.
|
||||||
|
4. Run `pnpm run typecheck`.
|
||||||
|
5. Run `pnpm run test`.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Root `package.json` uses workspace paths for vendored packages.
|
||||||
|
- `pnpm-lock.yaml` has no deprecated packages where maintained replacements exist.
|
||||||
|
- Any remaining deprecated packages are documented as no-maintained-replacement and owned by a vendored submodule or unavoidable native upstream.
|
||||||
|
- Typecheck and tests pass.
|
||||||
|
|
||||||
|
## Final Audit Result
|
||||||
|
|
||||||
|
Deprecated packages removed from active dependency graph:
|
||||||
|
|
||||||
|
- `@otplib/plugin-crypto`, `@otplib/plugin-thirty-two`, `@otplib/preset-default` — removed by patching `vendor/discord.js-selfbot-v13` to `otplib@13`.
|
||||||
|
- `@discordjs/opus` direct dependency — removed from root dependencies.
|
||||||
|
- `@discordjs/node-pre-gyp`, `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, `inflight` — removed by eliminating `@discordjs/opus` auto-installed peer path.
|
||||||
|
|
||||||
|
Remaining unavoidable deprecated packages:
|
||||||
|
|
||||||
|
- `@esbuild-kit/core-utils@3.3.2` via `drizzle-kit@0.31.10`.
|
||||||
|
- `@esbuild-kit/esm-loader@2.6.5` via `drizzle-kit@0.31.10`.
|
||||||
|
- `prebuild-install@7.1.3` via `better-sqlite3@12.10.0` and `@lng2004/node-datachannel@0.32.0-20260202`.
|
||||||
|
|
||||||
|
Reason these remain:
|
||||||
|
|
||||||
|
- `drizzle-kit@0.31.10` is latest stable and still depends on `@esbuild-kit/*`.
|
||||||
|
- `better-sqlite3@12.10.0` is latest stable and still uses `prebuild-install` for native binary install.
|
||||||
|
- `@lng2004/node-datachannel@0.32.0-20260202` is latest available and still uses `prebuild-install` for native binary install.
|
||||||
|
|
||||||
|
The upstream repositories are vendored as submodules for future patching if strict zero-deprecated lockfile becomes worth maintaining as forks:
|
||||||
|
|
||||||
|
- `vendor/drizzle-orm`
|
||||||
|
- `vendor/better-sqlite3`
|
||||||
|
- `vendor/node-datachannel`
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
|
- vendor/discord-video-stream
|
||||||
- vendor/discord.js-selfbot-v13
|
- vendor/discord.js-selfbot-v13
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
|
|||||||
@@ -91,15 +91,16 @@ export function parseModerationResponse(
|
|||||||
message_id: msgId,
|
message_id: msgId,
|
||||||
status: (parsed as any).status || "clean",
|
status: (parsed as any).status || "clean",
|
||||||
flags: (parsed as any).flags || [],
|
flags: (parsed as any).flags || [],
|
||||||
score: (parsed as any).score !== undefined ? (parsed as any).score : 0.1,
|
score:
|
||||||
|
(parsed as any).score !== undefined ? (parsed as any).score : 0.1,
|
||||||
analysis: (parsed as any).analysis || "",
|
analysis: (parsed as any).analysis || "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Look for any array property (result, data, messages, moderation, etc.)
|
// Look for any array property (result, data, messages, moderation, etc.)
|
||||||
const arrayKey = Object.keys(parsed).find(
|
const arrayKey = Object.keys(parsed).find((key) =>
|
||||||
(key) => Array.isArray((parsed as any)[key]),
|
Array.isArray((parsed as any)[key]),
|
||||||
);
|
);
|
||||||
if (arrayKey) {
|
if (arrayKey) {
|
||||||
parsed.results = (parsed as any)[arrayKey];
|
parsed.results = (parsed as any)[arrayKey];
|
||||||
@@ -537,7 +538,8 @@ Return ONLY valid JSON, no other text.`;
|
|||||||
try {
|
try {
|
||||||
parsed = parseModerationResponse(content, targetIds);
|
parsed = parseModerationResponse(content, targetIds);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
const errorMsg =
|
||||||
|
parseError instanceof Error ? parseError.message : String(parseError);
|
||||||
log.error(
|
log.error(
|
||||||
{
|
{
|
||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Router,
|
type NextFunction,
|
||||||
type Request,
|
type Request,
|
||||||
type Response,
|
type Response,
|
||||||
type NextFunction,
|
Router,
|
||||||
} from "express";
|
} from "express";
|
||||||
import { listVoiceRecordings } from "../database/voiceRecordingRepo";
|
import { listVoiceRecordings } from "../database/voiceRecordingRepo";
|
||||||
import { AppError } from "../errors";
|
import { AppError } from "../errors";
|
||||||
|
|||||||
+1
-1
@@ -25,10 +25,10 @@ import { discordPlayer } from "./player";
|
|||||||
import { createAnalysisRoutes } from "./routes/analysisRoutes";
|
import { createAnalysisRoutes } from "./routes/analysisRoutes";
|
||||||
import { createMediaRoutes } from "./routes/mediaRoutes";
|
import { createMediaRoutes } from "./routes/mediaRoutes";
|
||||||
import { createMessageRoutes } from "./routes/messageRoutes";
|
import { createMessageRoutes } from "./routes/messageRoutes";
|
||||||
|
import { createRecordingsRoutes } from "./routes/recordingsRoutes";
|
||||||
import { createSyncRoutes } from "./routes/syncRoutes";
|
import { createSyncRoutes } from "./routes/syncRoutes";
|
||||||
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
||||||
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
||||||
import { createRecordingsRoutes } from "./routes/recordingsRoutes";
|
|
||||||
import { Streamer } from "./streaming";
|
import { Streamer } from "./streaming";
|
||||||
import type { VoiceController } from "./voiceController";
|
import type { VoiceController } from "./voiceController";
|
||||||
|
|
||||||
|
|||||||
@@ -415,7 +415,9 @@ describe("runModerationAnalysis", () => {
|
|||||||
expect(requestBody.temperature).toBe(0);
|
expect(requestBody.temperature).toBe(0);
|
||||||
expect(requestBody.response_format).toEqual({ type: "json_object" });
|
expect(requestBody.response_format).toEqual({ type: "json_object" });
|
||||||
expect(requestBody.reasoning_budget).toBeUndefined();
|
expect(requestBody.reasoning_budget).toBeUndefined();
|
||||||
expect(requestBody.chat_template_kwargs).toEqual({ enable_thinking: false });
|
expect(requestBody.chat_template_kwargs).toEqual({
|
||||||
|
enable_thinking: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on non-ok HTTP response", async () => {
|
it("throws on non-ok HTTP response", async () => {
|
||||||
@@ -655,7 +657,9 @@ describe("runModerationAnalysis", () => {
|
|||||||
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t2");
|
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t2");
|
||||||
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t1");
|
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t1");
|
||||||
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=c7");
|
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=c7");
|
||||||
expect(downloadedUrls).not.toContain("https://httpbin.org/image/png?source=c1");
|
expect(downloadedUrls).not.toContain(
|
||||||
|
"https://httpbin.org/image/png?source=c1",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends verified real PNG and JPEG attachments with realistic Indonesian text", async () => {
|
it("sends verified real PNG and JPEG attachments with realistic Indonesian text", async () => {
|
||||||
@@ -761,7 +765,9 @@ describe("runModerationAnalysis", () => {
|
|||||||
|
|
||||||
const requestBody = JSON.parse(fetchCalls[2][1].body);
|
const requestBody = JSON.parse(fetchCalls[2][1].body);
|
||||||
const contentParts = requestBody.messages[0].content;
|
const contentParts = requestBody.messages[0].content;
|
||||||
expect(contentParts.filter((part: any) => part.type === "image_url")).toHaveLength(2);
|
expect(
|
||||||
|
contentParts.filter((part: any) => part.type === "image_url"),
|
||||||
|
).toHaveLength(2);
|
||||||
expect(contentParts[0].image_url.url).toContain("data:image/jpeg;base64,");
|
expect(contentParts[0].image_url.url).toContain("data:image/jpeg;base64,");
|
||||||
expect(contentParts[2].image_url.url).toContain("data:image/png;base64,");
|
expect(contentParts[2].image_url.url).toContain("data:image/png;base64,");
|
||||||
expect(contentParts.at(-1).text).toContain("https://example.invalid/login");
|
expect(contentParts.at(-1).text).toContain("https://example.invalid/login");
|
||||||
@@ -795,7 +801,10 @@ describe("runModerationAnalysis", () => {
|
|||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
arrayBuffer: async () =>
|
arrayBuffer: async () =>
|
||||||
buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
buffer.buffer.slice(
|
||||||
|
buffer.byteOffset,
|
||||||
|
buffer.byteOffset + buffer.byteLength,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,7 +816,12 @@ describe("runModerationAnalysis", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await runModerationAnalysis({
|
await runModerationAnalysis({
|
||||||
targets: [createMessageRecord({ id: "m1", content: "gambar belum selesai upload ke picser" })],
|
targets: [
|
||||||
|
createMessageRecord({
|
||||||
|
id: "m1",
|
||||||
|
content: "gambar belum selesai upload ke picser",
|
||||||
|
}),
|
||||||
|
],
|
||||||
contextText: "test context",
|
contextText: "test context",
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
@@ -830,7 +844,9 @@ describe("runModerationAnalysis", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect((global.fetch as any).mock.calls[0][0]).toBe("https://httpbin.org/image/png");
|
expect((global.fetch as any).mock.calls[0][0]).toBe(
|
||||||
|
"https://httpbin.org/image/png",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps analyzing text when an image URL returns non-OK", async () => {
|
it("keeps analyzing text when an image URL returns non-OK", async () => {
|
||||||
@@ -845,7 +861,8 @@ describe("runModerationAnalysis", () => {
|
|||||||
status: "warn",
|
status: "warn",
|
||||||
flags: ["suspicious_link"],
|
flags: ["suspicious_link"],
|
||||||
score: 0.6,
|
score: 0.6,
|
||||||
analysis: "Image fetch failed, text still indicates suspicious link.",
|
analysis:
|
||||||
|
"Image fetch failed, text still indicates suspicious link.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -873,7 +890,8 @@ describe("runModerationAnalysis", () => {
|
|||||||
content: "cek bonus gratis di https://example.invalid/claim sekarang",
|
content: "cek bonus gratis di https://example.invalid/claim sekarang",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
contextText: "Pesan ini dikirim berulang setelah user lain menolak klik link.",
|
contextText:
|
||||||
|
"Pesan ini dikirim berulang setelah user lain menolak klik link.",
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
id: "bad-image",
|
id: "bad-image",
|
||||||
@@ -899,7 +917,9 @@ describe("runModerationAnalysis", () => {
|
|||||||
|
|
||||||
const requestBody = JSON.parse((global.fetch as any).mock.calls[1][1].body);
|
const requestBody = JSON.parse((global.fetch as any).mock.calls[1][1].body);
|
||||||
expect(requestBody.messages[0].content).toHaveLength(1);
|
expect(requestBody.messages[0].content).toHaveLength(1);
|
||||||
expect(requestBody.messages[0].content[0].text).toContain("https://example.invalid/claim");
|
expect(requestBody.messages[0].content[0].text).toContain(
|
||||||
|
"https://example.invalid/claim",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Edge Cases & Real-World Scenarios", () => {
|
describe("Edge Cases & Real-World Scenarios", () => {
|
||||||
@@ -1349,9 +1369,7 @@ describe("runModerationAnalysis", () => {
|
|||||||
]
|
]
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
expect(() =>
|
expect(() => parseModerationResponse(content, ["m1"])).toThrow(/finite/i);
|
||||||
parseModerationResponse(content, ["m1"]),
|
|
||||||
).toThrow(/finite/i);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
-1
Submodule vendor/better-sqlite3 updated: d8885f900c...7fa2365432
Vendored
+1
-1
Submodule vendor/discord-video-stream updated: 2312e75953...ba91cc1b2f
Vendored
+1
-1
Submodule vendor/drizzle-orm updated: 48e5406027...c5d4f0c4a7
Vendored
+1
-1
Submodule vendor/node-datachannel updated: 46c65c88b7...29e898be59
Reference in New Issue
Block a user