this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add TS7, ESLint 9, Prettier, knip; fix all errors

- TypeScript 7 (@typescript/native-preview) with strict settings
- ESLint 9 flat config with strictTypeChecked rules
- Prettier (semi, single quotes, 120 width)
- knip for dead code detection
- Remove unused exports (checkTweets, TweetHeader, etc.)
- Fix all type errors and lint warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

TKTK 46d8bb52 cc90c24a

+976 -583
+1 -1
.gitignore
··· 33 33 # Finder (MacOS) folder config 34 34 .DS_Store 35 35 36 - output/ 36 + output*/
+7
.prettierrc
··· 1 + { 2 + "semi": true, 3 + "singleQuote": true, 4 + "printWidth": 120, 5 + "tabWidth": 4, 6 + "trailingComma": "es5" 7 + }
+39 -23
README.md
··· 23 23 24 24 ```bash 25 25 cd circle-filter 26 + bun install 26 27 bun run src/detect.ts --archive ../data 27 28 ``` 28 29 29 30 This will: 31 + 30 32 - Load all tweets from the Circle era (May 2022 - Nov 2023) 31 33 - Skip retweets (can't be Circle tweets) 32 - - Check each tweet against the syndication API 33 - - Save progress every 50 tweets (resumable if interrupted) 34 + - Check each tweet against the syndication API (20 concurrent requests) 35 + - Save progress after each batch (resumable if interrupted) 34 36 35 - **Runtime:** ~2-3 hours for a typical archive (32k tweets to check at ~4 req/s) 37 + **Runtime:** ~30-60 min for a typical archive (32k tweets with 20 concurrent requests) 36 38 37 39 **Output:** 40 + 38 41 - `output/circle-tweets.json` - Array of Circle tweet IDs 39 42 - `output/detection-log.json` - Full log of every API check 40 43 - `output/progress.json` - Resume checkpoint ··· 48 51 This generates cleaned `tweets*.js` files in `output/` with Circle tweets removed. 49 52 50 53 **Output:** 54 + 51 55 - `output/tweets.js` - Cleaned tweets (same format as original) 52 - - `output/tweets-part1.js` 53 - - `output/tweets-part2.js` 56 + - `output/tweets-part1.js`, `output/tweets-part2.js`, etc. 54 57 - `output/clean-summary.json` - Summary of removal 55 58 56 59 ### For Future Archives ··· 75 78 ``` 76 79 77 80 The token is calculated as `(id / 1e15) * π` in base-36. Tweet IDs exceed `Number.MAX_SAFE_INTEGER`, so we use BigInt with hi/lo split to preserve precision: 81 + 78 82 ```typescript 79 - const bigId = BigInt(id) 80 - const hi = Number(bigId / 1_000_000_000_000_000n) 81 - const lo = Number(bigId % 1_000_000_000_000_000n) / 1e15 82 - token = ((hi + lo) * Math.PI).toString(36).replace(/(0+|\.)/g, "") 83 + const bigId = BigInt(id); 84 + const hi = Number(bigId / 1_000_000_000_000_000n); 85 + const lo = Number(bigId % 1_000_000_000_000_000n) / 1e15; 86 + token = ((hi + lo) * Math.PI).toString(36).replace(/(0+|\.)/g, ''); 83 87 ``` 84 88 85 89 ## Rate Limiting 86 90 87 - - Base delay: 250ms between requests (~4 req/s) 91 + - Concurrent requests: 20 88 92 - On HTTP 429: Exponential backoff (1s, 2s, 4s, 8s...) 89 93 - Max retries per tweet: 5 90 94 - Request timeout: 30s (prevents hung connections) 91 - - Progress saved every 50 tweets for resume after interruption 95 + - Progress saved after each batch for resume after interruption 92 96 - Transient errors (429, 5xx, network) are retried on resume 93 97 94 98 ## Verification ··· 97 101 98 102 ```json 99 103 { 100 - "startedAt": "2025-12-07T...", 101 - "completedAt": "2025-12-07T...", 102 - "totalCandidates": 32150, 103 - "circleCount": 847, 104 - "results": { 105 - "1718307594148651356": { 106 - "id": "1718307594148651356", 107 - "typename": "TweetTombstone", 108 - "tombstoneText": "This Post is unavailable. Learn more", 109 - "retries": 0, 110 - "timestamp": "2025-12-07T..." 104 + "startedAt": "2025-12-07T...", 105 + "completedAt": "2025-12-07T...", 106 + "totalCandidates": 32150, 107 + "circleCount": 847, 108 + "results": { 109 + "1718307594148651356": { 110 + "id": "1718307594148651356", 111 + "typename": "TweetTombstone", 112 + "tombstoneText": "This Post is unavailable. Learn more", 113 + "retries": 0, 114 + "timestamp": "2025-12-07T..." 115 + } 111 116 } 112 - } 113 117 } 114 118 ``` 115 119 120 + ## Development 121 + 122 + ```bash 123 + bun run typecheck # TypeScript 7 (tsgo) 124 + bun run lint # ESLint 9 with strict rules 125 + bun run format # Prettier 126 + bun run knip # Dead code detection 127 + ``` 128 + 116 129 ## File Structure 117 130 118 131 ``` ··· 124 137 │ ├── detect.ts # Main detection script 125 138 │ └── clean.ts # Archive cleaner script 126 139 ├── output/ # Generated files go here 140 + ├── eslint.config.mjs 141 + ├── knip.json 127 142 ├── package.json 128 143 ├── tsconfig.json 129 144 └── README.md ··· 132 147 ## Credits 133 148 134 149 Detection method based on research into the Twitter syndication API. See also: 150 + 135 151 - [community-archive circle-mitigation scripts](https://github.com/TheExGenesis/community-archive/tree/main/scripts/circle-mitigation) 136 152 - [twittxr - Twitter Syndication API wrapper](https://github.com/Owen3H/twittxr) 137 153
+340 -3
bun.lock
··· 5 5 "": { 6 6 "name": "circle-filter", 7 7 "devDependencies": { 8 + "@eslint/js": "^9.17.0", 8 9 "@types/bun": "latest", 9 - }, 10 - "peerDependencies": { 11 - "typescript": "^5", 10 + "@typescript/native-preview": "latest", 11 + "eslint": "^9.17.0", 12 + "knip": "^5.71.0", 13 + "prettier": "^3.4.2", 14 + "typescript-eslint": "^8.18.1", 12 15 }, 13 16 }, 14 17 }, 15 18 "packages": { 19 + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], 20 + 21 + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], 22 + 23 + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], 24 + 25 + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], 26 + 27 + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], 28 + 29 + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], 30 + 31 + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], 32 + 33 + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], 34 + 35 + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], 36 + 37 + "@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="], 38 + 39 + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], 40 + 41 + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], 42 + 43 + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 44 + 45 + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], 46 + 47 + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 48 + 49 + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 50 + 51 + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], 52 + 53 + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 54 + 55 + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 56 + 57 + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 58 + 59 + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.15.0", "", { "os": "android", "cpu": "arm" }, "sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw=="], 60 + 61 + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.15.0", "", { "os": "android", "cpu": "arm64" }, "sha512-vbdBttesHR0W1oJaxgWVTboyMUuu+VnPsHXJ6jrXf4czELzB6GIg5DrmlyhAmFBhjwov+yJH/DfTnHS+2sDgOw=="], 62 + 63 + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.15.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R67lsOe1UzNjqVBCwCZX1rlItTsj/cVtBw4Uy19CvTicqEWvwaTn8t34zLD75LQwDDPCY3C8n7NbD+LIdw+ZoA=="], 64 + 65 + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.15.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-77mya5F8WV0EtCxI0MlVZcqkYlaQpfNwl/tZlfg4jRsoLpFbaTeWv75hFm6TE84WULVlJtSgvf7DhoWBxp9+ZQ=="], 66 + 67 + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.15.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-X1Sz7m5PC+6D3KWIDXMUtux+0Imj6HfHGdBStSvgdI60OravzI1t83eyn6eN0LPTrynuPrUgjk7tOnOsBzSWHw=="], 68 + 69 + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.15.0", "", { "os": "linux", "cpu": "arm" }, "sha512-L1x/wCaIRre+18I4cH/lTqSAymlV0k4HqfSYNNuI9oeL28Ks86lI6O5VfYL6sxxWYgjuWB98gNGo7tq7d4GarQ=="], 70 + 71 + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.15.0", "", { "os": "linux", "cpu": "arm" }, "sha512-abGXd/zMGa0tH8nKlAXdOnRy4G7jZmkU0J85kMKWns161bxIgGn/j7zxqh3DKEW98wAzzU9GofZMJ0P5YCVPVw=="], 72 + 73 + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.15.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg=="], 74 + 75 + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.15.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g=="], 76 + 77 + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.15.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg=="], 78 + 79 + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.15.0", "", { "os": "linux", "cpu": "none" }, "sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg=="], 80 + 81 + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.15.0", "", { "os": "linux", "cpu": "none" }, "sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg=="], 82 + 83 + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.15.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q=="], 84 + 85 + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.15.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg=="], 86 + 87 + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.15.0", "", { "os": "linux", "cpu": "x64" }, "sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg=="], 88 + 89 + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.15.0", "", { "os": "none", "cpu": "arm64" }, "sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw=="], 90 + 91 + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.15.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-q5rn2eIMQLuc/AVGR2rQKb2EVlgreATGG8xXg8f4XbbYCVgpxaq+dgMbiPStyNywW1MH8VU2T09UEm30UtOQvg=="], 92 + 93 + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.15.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yCAh2RWjU/8wWTxQDgGPgzV9QBv0/Ojb5ej1c/58iOjyTuy/J1ZQtYi2SpULjKmwIxLJdTiCHpMilauWimE31w=="], 94 + 95 + "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.15.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-lmXKb6lvA6M6QIbtYfgjd+AryJqExZVSY2bfECC18OPu7Lv1mHFF171Mai5l9hG3r4IhHPPIwT10EHoilSCYeA=="], 96 + 97 + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.15.0", "", { "os": "win32", "cpu": "x64" }, "sha512-HZsfne0s/tGOcJK9ZdTGxsNU2P/dH0Shf0jqrPvsC6wX0Wk+6AyhSpHFLQCnLOuFQiHHU0ePfM8iYsoJb5hHpQ=="], 98 + 99 + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 100 + 16 101 "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 17 102 103 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 104 + 105 + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 106 + 18 107 "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 19 108 109 + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="], 110 + 111 + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="], 112 + 113 + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.48.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.48.1", "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w=="], 114 + 115 + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1" } }, "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w=="], 116 + 117 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.48.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw=="], 118 + 119 + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg=="], 120 + 121 + "@typescript-eslint/types": ["@typescript-eslint/types@8.48.1", "", {}, "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q=="], 122 + 123 + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.48.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.48.1", "@typescript-eslint/tsconfig-utils": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg=="], 124 + 125 + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.48.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA=="], 126 + 127 + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.1", "", { "dependencies": { "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q=="], 128 + 129 + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="], 130 + 131 + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="], 132 + 133 + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3bkD9QuIjxETtp6J1l5X2oKgudJ8z+8fwUq0izCjK1JrIs2vW1aQnbzxhynErSyHWH7URGhHHzcsXHbikckAsg=="], 134 + 135 + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OjrZBq8XJkB7uCQvT1AZ1FPsp+lT0cHxY5SisE+ZTAU6V0IHAZMwJ7J/mnwlGsBcCKRLBT+lX3hgEuOTSwHr9w=="], 136 + 137 + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qhp06OObkwy5B+PlAhAmq+Ls3GVt4LHAovrTRcpLB3Mk3yJ0h9DnIQwPQiayp16TdvTsGHI3jdIX4MGm5L/ghA=="], 138 + 139 + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fPRw0zfTBeVmrkgi5Le+sSwoeAz6pIdvcsa1OYZcrspueS9hn3qSC5bLEc5yX4NJP1vItadBqyGLUQ7u8FJjow=="], 140 + 141 + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-KxY1i+HxeSFfzZ+HVsKwMGBM79laTRZv1ibFqHu22CEsfSPDt4yiV1QFis8Nw7OBXswNqJG/UGqY47VP8FeTvw=="], 142 + 143 + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="], 144 + 145 + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 146 + 147 + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 148 + 149 + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], 150 + 151 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 152 + 153 + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 154 + 155 + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 156 + 157 + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], 158 + 159 + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 160 + 20 161 "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 21 162 163 + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 164 + 165 + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 166 + 167 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 168 + 169 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 170 + 171 + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 172 + 173 + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 174 + 175 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 176 + 177 + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 178 + 179 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 180 + 181 + "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], 182 + 183 + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 184 + 185 + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 186 + 187 + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 188 + 189 + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], 190 + 191 + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 192 + 193 + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 194 + 195 + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 196 + 197 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 198 + 199 + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 200 + 201 + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 202 + 203 + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 204 + 205 + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 206 + 207 + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], 208 + 209 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 210 + 211 + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 212 + 213 + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 214 + 215 + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 216 + 217 + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 218 + 219 + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 220 + 221 + "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], 222 + 223 + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 224 + 225 + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 226 + 227 + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 228 + 229 + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 230 + 231 + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 232 + 233 + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 234 + 235 + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 236 + 237 + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 238 + 239 + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 240 + 241 + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 242 + 243 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 244 + 245 + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 246 + 247 + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], 248 + 249 + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 250 + 251 + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 252 + 253 + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 254 + 255 + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 256 + 257 + "knip": ["knip@5.71.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.1", "minimist": "^1.2.8", "oxc-resolver": "^11.13.2", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-hwgdqEJ+7DNJ5jE8BCPu7b57TY7vUwP6MzWYgCgPpg6iPCee/jKPShDNIlFER2koti4oz5xF88VJbKCb4Wl71g=="], 258 + 259 + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], 260 + 261 + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 262 + 263 + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 264 + 265 + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 266 + 267 + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 268 + 269 + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 270 + 271 + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 272 + 273 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 274 + 275 + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 276 + 277 + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 278 + 279 + "oxc-resolver": ["oxc-resolver@11.15.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.15.0", "@oxc-resolver/binding-android-arm64": "11.15.0", "@oxc-resolver/binding-darwin-arm64": "11.15.0", "@oxc-resolver/binding-darwin-x64": "11.15.0", "@oxc-resolver/binding-freebsd-x64": "11.15.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.15.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.15.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.15.0", "@oxc-resolver/binding-linux-arm64-musl": "11.15.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.15.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.15.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.15.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.15.0", "@oxc-resolver/binding-linux-x64-gnu": "11.15.0", "@oxc-resolver/binding-linux-x64-musl": "11.15.0", "@oxc-resolver/binding-openharmony-arm64": "11.15.0", "@oxc-resolver/binding-wasm32-wasi": "11.15.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.15.0", "@oxc-resolver/binding-win32-ia32-msvc": "11.15.0", "@oxc-resolver/binding-win32-x64-msvc": "11.15.0" } }, "sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw=="], 280 + 281 + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 282 + 283 + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 284 + 285 + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 286 + 287 + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 288 + 289 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 290 + 291 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 292 + 293 + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 294 + 295 + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 296 + 297 + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], 298 + 299 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 300 + 301 + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 302 + 303 + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 304 + 305 + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 306 + 307 + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 308 + 309 + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 310 + 311 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 312 + 313 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 314 + 315 + "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], 316 + 317 + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], 318 + 319 + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 320 + 321 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 322 + 323 + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 324 + 325 + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], 326 + 327 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 328 + 329 + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 330 + 22 331 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 332 + 333 + "typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="], 23 334 24 335 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 336 + 337 + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 338 + 339 + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], 340 + 341 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 342 + 343 + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 344 + 345 + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 346 + 347 + "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], 348 + 349 + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 350 + 351 + "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 352 + 353 + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 354 + 355 + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 356 + 357 + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 358 + 359 + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 360 + 361 + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 25 362 } 26 363 }
+39
eslint.config.mjs
··· 1 + // @ts-check 2 + import eslint from '@eslint/js'; 3 + import { defineConfig } from 'eslint/config'; 4 + import tseslint from 'typescript-eslint'; 5 + 6 + export default defineConfig( 7 + { ignores: ['**/dist/**', '**/build/**', '**/node_modules/**'] }, 8 + eslint.configs.recommended, 9 + tseslint.configs.strictTypeChecked, 10 + tseslint.configs.stylisticTypeChecked, 11 + { 12 + languageOptions: { 13 + parserOptions: { 14 + projectService: true, 15 + }, 16 + }, 17 + rules: { 18 + // Let TSC handle these via strict mode 19 + '@typescript-eslint/no-unused-vars': 'off', 20 + 21 + // Stricter rules 22 + '@typescript-eslint/no-explicit-any': 'error', 23 + '@typescript-eslint/explicit-function-return-type': 'error', 24 + '@typescript-eslint/no-non-null-assertion': 'error', 25 + '@typescript-eslint/strict-boolean-expressions': 'error', 26 + '@typescript-eslint/no-floating-promises': 'error', 27 + '@typescript-eslint/await-thenable': 'error', 28 + '@typescript-eslint/no-misused-promises': 'error', 29 + '@typescript-eslint/prefer-nullish-coalescing': 'error', 30 + '@typescript-eslint/prefer-optional-chain': 'error', 31 + 'no-console': 'warn', 32 + eqeqeq: ['error', 'always'], 33 + }, 34 + }, 35 + { 36 + files: ['**/*.js', '**/*.mjs'], 37 + extends: [tseslint.configs.disableTypeChecked], 38 + } 39 + );
-1
index.ts
··· 1 - console.log("Hello via Bun!");
+3
knip.json
··· 1 + { 2 + "entry": ["src/detect.ts", "src/clean.ts"] 3 + }
+21 -10
package.json
··· 1 1 { 2 - "name": "circle-filter", 3 - "module": "index.ts", 4 - "type": "module", 5 - "private": true, 6 - "devDependencies": { 7 - "@types/bun": "latest" 8 - }, 9 - "peerDependencies": { 10 - "typescript": "^5" 11 - } 2 + "name": "circle-filter", 3 + "module": "index.ts", 4 + "type": "module", 5 + "private": true, 6 + "scripts": { 7 + "typecheck": "tsgo", 8 + "lint": "eslint .", 9 + "lint:fix": "eslint . --fix", 10 + "format": "prettier --write .", 11 + "format:check": "prettier --check .", 12 + "knip": "knip" 13 + }, 14 + "devDependencies": { 15 + "@eslint/js": "^9.17.0", 16 + "@types/bun": "latest", 17 + "@typescript/native-preview": "latest", 18 + "eslint": "^9.17.0", 19 + "knip": "^5.71.0", 20 + "prettier": "^3.4.2", 21 + "typescript-eslint": "^8.18.1" 22 + } 12 23 }
+132 -113
src/clean.ts
··· 1 - import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs" 2 - import { parseArgs } from "util" 3 - import type { Tweet } from "./types" 1 + /* eslint-disable no-console */ 2 + import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; 3 + import { parseArgs } from 'util'; 4 + import type { Tweet } from './types'; 4 5 5 6 // Dynamically discover tweet files (tweets.js, tweets-part1.js, tweets-part2.js, etc.) 6 7 function discoverTweetFiles(archivePath: string): string[] { 7 - const allFiles = readdirSync(archivePath) 8 - return allFiles 9 - .filter((f) => f === "tweets.js" || f.match(/^tweets-part\d+\.js$/)) 10 - .sort() 8 + const allFiles = readdirSync(archivePath); 9 + return allFiles.filter((f) => f === 'tweets.js' || /^tweets-part\d+\.js$/.exec(f) !== null).sort(); 11 10 } 12 11 13 - function parseCliArgs() { 14 - const { values } = parseArgs({ 15 - args: Bun.argv.slice(2), 16 - options: { 17 - archive: { type: "string", short: "a" }, 18 - circles: { type: "string", short: "c" }, 19 - output: { type: "string", short: "o", default: "./output" }, 20 - }, 21 - strict: true, 22 - }) 12 + interface CliArgs { 13 + archivePath: string; 14 + circlesPath: string; 15 + outputPath: string; 16 + } 23 17 24 - if (!values.archive || !values.circles) { 25 - console.error("Usage: bun run src/clean.ts --archive <path> --circles <circle-tweets.json>") 26 - console.error("Example: bun run src/clean.ts --archive ../data --circles ./output/circle-tweets.json") 27 - process.exit(1) 28 - } 18 + function parseCliArgs(): CliArgs { 19 + const { values } = parseArgs({ 20 + args: Bun.argv.slice(2), 21 + options: { 22 + archive: { type: 'string', short: 'a' }, 23 + circles: { type: 'string', short: 'c' }, 24 + output: { type: 'string', short: 'o', default: './output' }, 25 + }, 26 + strict: true, 27 + }); 28 + 29 + if ( 30 + values.archive === undefined || 31 + values.archive === '' || 32 + values.circles === undefined || 33 + values.circles === '' 34 + ) { 35 + console.error('Usage: bun run src/clean.ts --archive <path> --circles <circle-tweets.json>'); 36 + console.error('Example: bun run src/clean.ts --archive ../data --circles ./output/circle-tweets.json'); 37 + process.exit(1); 38 + } 29 39 30 - return { 31 - archivePath: values.archive, 32 - circlesPath: values.circles, 33 - outputPath: values.output!, 34 - } 40 + return { 41 + archivePath: values.archive, 42 + circlesPath: values.circles, 43 + outputPath: values.output, 44 + }; 35 45 } 36 46 37 47 function loadCircleIds(path: string): Set<string> { 38 - if (!existsSync(path)) { 39 - console.error(`Circle tweets file not found: ${path}`) 40 - console.error("Run detect.ts first to generate this file.") 41 - process.exit(1) 42 - } 43 - const ids = JSON.parse(readFileSync(path, "utf8")) as string[] 44 - return new Set(ids) 48 + if (!existsSync(path)) { 49 + console.error(`Circle tweets file not found: ${path}`); 50 + console.error('Run detect.ts first to generate this file.'); 51 + process.exit(1); 52 + } 53 + const ids = JSON.parse(readFileSync(path, 'utf8')) as string[]; 54 + return new Set(ids); 55 + } 56 + 57 + interface TweetsFileData { 58 + tweets: Tweet[]; 59 + varName: string; 45 60 } 46 61 47 - function loadTweetsFile(archivePath: string, fileName: string): { tweets: Tweet[]; varName: string } | null { 48 - const path = `${archivePath}/${fileName}` 49 - if (!existsSync(path)) return null 62 + function loadTweetsFile(archivePath: string, fileName: string): TweetsFileData | null { 63 + const path = `${archivePath}/${fileName}`; 64 + if (!existsSync(path)) return null; 65 + 66 + const content = readFileSync(path, 'utf8'); 67 + const match = /^(window\.YTD\.\w+\.part\d+) = /.exec(content); 68 + if (match === null) throw new Error(`Invalid JS file format: ${path}`); 50 69 51 - const content = readFileSync(path, "utf8") 52 - const match = content.match(/^(window\.YTD\.\w+\.part\d+) = /) 53 - if (!match) throw new Error(`Invalid JS file format: ${path}`) 70 + const varName = match[1]; 71 + if (varName === undefined) throw new Error(`Invalid JS file format: ${path}`); 54 72 55 - const varName = match[1] 56 - const json = content.slice(match[0].length) 57 - const tweets = JSON.parse(json) as Tweet[] 73 + const json = content.slice(match[0].length); 74 + const tweets = JSON.parse(json) as Tweet[]; 58 75 59 - return { tweets, varName } 76 + return { tweets, varName }; 60 77 } 61 78 62 - function writeTweetsFile(outputPath: string, fileName: string, varName: string, tweets: Tweet[]) { 63 - const content = `${varName} = ${JSON.stringify(tweets, null, 2)}` 64 - writeFileSync(`${outputPath}/${fileName}`, content) 79 + function writeTweetsFile(outputPath: string, fileName: string, varName: string, tweets: Tweet[]): void { 80 + const content = `${varName} = ${JSON.stringify(tweets, null, 2)}`; 81 + writeFileSync(`${outputPath}/${fileName}`, content); 65 82 } 66 83 67 - function main() { 68 - const { archivePath, circlesPath, outputPath } = parseCliArgs() 84 + function main(): void { 85 + const { archivePath, circlesPath, outputPath } = parseCliArgs(); 69 86 70 - console.log("Circle Tweet Cleaner") 71 - console.log("====================") 72 - console.log(`Archive: ${archivePath}`) 73 - console.log(`Circle tweets: ${circlesPath}`) 74 - console.log(`Output: ${outputPath}`) 75 - console.log() 87 + console.log('Circle Tweet Cleaner'); 88 + console.log('===================='); 89 + console.log(`Archive: ${archivePath}`); 90 + console.log(`Circle tweets: ${circlesPath}`); 91 + console.log(`Output: ${outputPath}`); 92 + console.log(); 76 93 77 - // Ensure output directory exists 78 - if (!existsSync(outputPath)) { 79 - mkdirSync(outputPath, { recursive: true }) 80 - } 94 + // Ensure output directory exists 95 + if (!existsSync(outputPath)) { 96 + mkdirSync(outputPath, { recursive: true }); 97 + } 81 98 82 - // Load circle tweet IDs 83 - const circleIds = loadCircleIds(circlesPath) 84 - console.log(`Loaded ${circleIds.size} circle tweet IDs to filter`) 85 - console.log() 99 + // Load circle tweet IDs 100 + const circleIds = loadCircleIds(circlesPath); 101 + console.log(`Loaded ${String(circleIds.size)} circle tweet IDs to filter`); 102 + console.log(); 86 103 87 - let totalOriginal = 0 88 - let totalRemoved = 0 89 - let totalCleaned = 0 104 + let totalOriginal = 0; 105 + let totalRemoved = 0; 106 + let totalCleaned = 0; 90 107 91 - // Discover and process each tweets file dynamically 92 - const tweetFiles = discoverTweetFiles(archivePath) 93 - console.log(`Found ${tweetFiles.length} tweet file(s): ${tweetFiles.join(", ")}`) 94 - console.log() 108 + // Discover and process each tweets file dynamically 109 + const tweetFiles = discoverTweetFiles(archivePath); 110 + console.log(`Found ${String(tweetFiles.length)} tweet file(s): ${tweetFiles.join(', ')}`); 111 + console.log(); 95 112 96 - for (const file of tweetFiles) { 97 - const data = loadTweetsFile(archivePath, file) 98 - if (!data) { 99 - console.log(`[SKIP] ${file} (not found)`) 100 - continue 101 - } 113 + for (const file of tweetFiles) { 114 + const data = loadTweetsFile(archivePath, file); 115 + if (data === null) { 116 + console.log(`[SKIP] ${file} (not found)`); 117 + continue; 118 + } 102 119 103 - const { tweets, varName } = data 104 - const originalCount = tweets.length 120 + const { tweets, varName } = data; 121 + const originalCount = tweets.length; 105 122 106 - // Filter out circle tweets 107 - const cleaned = tweets.filter((item) => !circleIds.has(item.tweet.id_str)) 108 - const removedCount = originalCount - cleaned.length 123 + // Filter out circle tweets 124 + const cleaned = tweets.filter((item) => !circleIds.has(item.tweet.id_str)); 125 + const removedCount = originalCount - cleaned.length; 109 126 110 - // Write cleaned file 111 - writeTweetsFile(outputPath, file, varName, cleaned) 127 + // Write cleaned file 128 + writeTweetsFile(outputPath, file, varName, cleaned); 112 129 113 - console.log(`[OK] ${file}: ${originalCount} -> ${cleaned.length} (removed ${removedCount})`) 130 + console.log( 131 + `[OK] ${file}: ${String(originalCount)} -> ${String(cleaned.length)} (removed ${String(removedCount)})` 132 + ); 114 133 115 - totalOriginal += originalCount 116 - totalRemoved += removedCount 117 - totalCleaned += cleaned.length 118 - } 134 + totalOriginal += originalCount; 135 + totalRemoved += removedCount; 136 + totalCleaned += cleaned.length; 137 + } 119 138 120 - console.log() 121 - console.log("=== SUMMARY ===") 122 - console.log(`Original tweets: ${totalOriginal}`) 123 - console.log(`Circle tweets removed: ${totalRemoved}`) 124 - console.log(`Cleaned tweets: ${totalCleaned}`) 125 - console.log() 126 - console.log(`Cleaned files saved to: ${outputPath}/`) 127 - console.log() 139 + console.log(); 140 + console.log('=== SUMMARY ==='); 141 + console.log(`Original tweets: ${String(totalOriginal)}`); 142 + console.log(`Circle tweets removed: ${String(totalRemoved)}`); 143 + console.log(`Cleaned tweets: ${String(totalCleaned)}`); 144 + console.log(); 145 + console.log(`Cleaned files saved to: ${outputPath}/`); 146 + console.log(); 128 147 129 - // Verification 130 - if (totalRemoved !== circleIds.size) { 131 - console.log(`[WARN] Removed ${totalRemoved} tweets but had ${circleIds.size} circle IDs`) 132 - console.log(" Some circle tweets may be in other files or already deleted") 133 - } 148 + // Verification 149 + if (totalRemoved !== circleIds.size) { 150 + console.log(`[WARN] Removed ${String(totalRemoved)} tweets but had ${String(circleIds.size)} circle IDs`); 151 + console.log(' Some circle tweets may be in other files or already deleted'); 152 + } 134 153 135 - // Write summary 136 - const summary = { 137 - timestamp: new Date().toISOString(), 138 - archivePath, 139 - circlesPath, 140 - totalOriginal, 141 - totalRemoved, 142 - totalCleaned, 143 - circleIdsCount: circleIds.size, 144 - } 145 - writeFileSync(`${outputPath}/clean-summary.json`, JSON.stringify(summary, null, 2)) 154 + // Write summary 155 + const summary = { 156 + timestamp: new Date().toISOString(), 157 + archivePath, 158 + circlesPath, 159 + totalOriginal, 160 + totalRemoved, 161 + totalCleaned, 162 + circleIdsCount: circleIds.size, 163 + }; 164 + writeFileSync(`${outputPath}/clean-summary.json`, JSON.stringify(summary, null, 2)); 146 165 } 147 166 148 - main() 167 + main();
+220 -223
src/detect.ts
··· 1 - import { existsSync, mkdirSync, writeFileSync } from "fs"; 2 - import { parseArgs } from "util"; 3 - import { checkTweet } from "./syndication"; 1 + /* eslint-disable no-console */ 2 + import { existsSync, mkdirSync, writeFileSync } from 'fs'; 3 + import { parseArgs } from 'util'; 4 + import { checkTweet } from './syndication'; 4 5 import { 5 - loadTweets, 6 - loadDeletedTweetIds, 7 - loadProgress, 8 - saveProgress, 9 - isInDateRange, 10 - isRetweet, 11 - formatDuration, 12 - formatProgress, 13 - } from "./utils"; 14 - import type { Progress, SyndicationResult, DetectionStats } from "./types"; 6 + loadTweets, 7 + loadDeletedTweetIds, 8 + loadProgress, 9 + saveProgress, 10 + isInDateRange, 11 + isRetweet, 12 + formatDuration, 13 + formatProgress, 14 + } from './utils'; 15 + import type { Progress, DetectionStats } from './types'; 15 16 16 17 // Circle tweet date range (conservative) 17 - const CIRCLE_START = new Date("2022-05-02"); 18 - const CIRCLE_END = new Date("2023-11-15"); 18 + const CIRCLE_START = new Date('2022-05-02'); 19 + const CIRCLE_END = new Date('2023-11-15'); 19 20 20 - const CONCURRENCY = 10; // Concurrent API requests 21 + const CONCURRENCY = 20; // Concurrent API requests 21 22 22 - function parseCliArgs() { 23 - const { values } = parseArgs({ 24 - args: Bun.argv.slice(2), 25 - options: { 26 - archive: { type: "string", short: "a" }, 27 - output: { type: "string", short: "o", default: "./output" }, 28 - }, 29 - strict: true, 30 - }); 23 + interface CliArgs { 24 + archivePath: string; 25 + outputPath: string; 26 + } 27 + 28 + function parseCliArgs(): CliArgs { 29 + const { values } = parseArgs({ 30 + args: Bun.argv.slice(2), 31 + options: { 32 + archive: { type: 'string', short: 'a' }, 33 + output: { type: 'string', short: 'o', default: './output' }, 34 + }, 35 + strict: true, 36 + }); 31 37 32 - if (!values.archive) { 33 - console.error( 34 - "Usage: bun run src/detect.ts --archive <path-to-data-folder>", 35 - ); 36 - console.error("Example: bun run src/detect.ts --archive ../data"); 37 - process.exit(1); 38 - } 38 + if (values.archive === undefined || values.archive === '') { 39 + console.error('Usage: bun run src/detect.ts --archive <path-to-data-folder>'); 40 + console.error('Example: bun run src/detect.ts --archive ../data'); 41 + process.exit(1); 42 + } 39 43 40 - return { 41 - archivePath: values.archive, 42 - outputPath: values.output!, 43 - }; 44 + return { 45 + archivePath: values.archive, 46 + outputPath: values.output, 47 + }; 44 48 } 45 49 46 - async function main() { 47 - const { archivePath, outputPath } = parseCliArgs(); 48 - liveOutputPath = outputPath; // For SIGINT handler 50 + async function main(): Promise<void> { 51 + const { archivePath, outputPath } = parseCliArgs(); 52 + liveOutputPath = outputPath; // For SIGINT handler 49 53 50 - console.log("Circle Tweet Detector"); 51 - console.log("====================="); 52 - console.log(`Archive: ${archivePath}`); 53 - console.log(`Output: ${outputPath}`); 54 - console.log( 55 - `Date range: ${CIRCLE_START.toISOString().split("T")[0]} to ${CIRCLE_END.toISOString().split("T")[0]}`, 56 - ); 57 - console.log(); 54 + console.log('Circle Tweet Detector'); 55 + console.log('====================='); 56 + console.log(`Archive: ${archivePath}`); 57 + console.log(`Output: ${outputPath}`); 58 + const startDate = CIRCLE_START.toISOString().split('T')[0] ?? 'unknown'; 59 + const endDate = CIRCLE_END.toISOString().split('T')[0] ?? 'unknown'; 60 + console.log(`Date range: ${startDate} to ${endDate}`); 61 + console.log(); 58 62 59 - // Ensure output directory exists 60 - if (!existsSync(outputPath)) { 61 - mkdirSync(outputPath, { recursive: true }); 62 - } 63 + // Ensure output directory exists 64 + if (!existsSync(outputPath)) { 65 + mkdirSync(outputPath, { recursive: true }); 66 + } 63 67 64 - // Load deleted tweet IDs (to exclude from Circle detection) 65 - console.log("Loading deleted tweets..."); 66 - const deletedIds = loadDeletedTweetIds(archivePath); 67 - console.log(`Found ${deletedIds.size} deleted tweets`); 68 + // Load deleted tweet IDs (to exclude from Circle detection) 69 + console.log('Loading deleted tweets...'); 70 + const deletedIds = loadDeletedTweetIds(archivePath); 71 + console.log(`Found ${String(deletedIds.size)} deleted tweets`); 68 72 69 - // Load or resume progress 70 - let progress = loadProgress(outputPath); 71 - let isResuming = false; 73 + // Load or resume progress 74 + let progress = loadProgress(outputPath); 72 75 73 - if (progress) { 74 - isResuming = true; 75 - console.log(`\nResuming from previous run (started ${progress.startedAt})`); 76 - console.log(`Already checked: ${Object.keys(progress.checked).length}`); 77 - console.log(`Circle tweets found so far: ${progress.circleIds.length}`); 78 - } else { 79 - // Load tweets and build candidate list 80 - console.log("\nLoading tweets..."); 81 - const tweets = loadTweets(archivePath); 82 - console.log(`Loaded ${tweets.length} tweets`); 76 + if (progress !== null) { 77 + console.log(`\nResuming from previous run (started ${progress.startedAt})`); 78 + console.log(`Already checked: ${String(Object.keys(progress.checked).length)}`); 79 + console.log(`Circle tweets found so far: ${String(progress.circleIds.length)}`); 80 + } else { 81 + // Load tweets and build candidate list 82 + console.log('\nLoading tweets...'); 83 + const tweets = loadTweets(archivePath); 84 + console.log(`Loaded ${String(tweets.length)} tweets`); 83 85 84 - // Filter to Circle era candidates 85 - console.log("\nFiltering to Circle era candidates..."); 86 - const candidates: string[] = []; 86 + // Filter to Circle era candidates 87 + console.log('\nFiltering to Circle era candidates...'); 88 + const candidates: string[] = []; 87 89 88 - for (const item of tweets) { 89 - const t = item.tweet; 90 - const text = t.full_text || ""; 90 + for (const item of tweets) { 91 + const t = item.tweet; 92 + const text = t.full_text; 91 93 92 - // Skip retweets (can't be Circle tweets) 93 - if (isRetweet(text)) continue; 94 + // Skip retweets (can't be Circle tweets) 95 + if (isRetweet(text)) continue; 94 96 95 - // Check date range 96 - if (!isInDateRange(t.created_at, CIRCLE_START, CIRCLE_END)) continue; 97 + // Check date range 98 + if (!isInDateRange(t.created_at, CIRCLE_START, CIRCLE_END)) continue; 97 99 98 - candidates.push(t.id_str); 100 + candidates.push(t.id_str); 101 + } 102 + 103 + console.log(`Found ${String(candidates.length)} candidates to check`); 104 + 105 + progress = { 106 + checked: {}, 107 + circleIds: [], 108 + candidates, 109 + startedAt: new Date().toISOString(), 110 + lastUpdated: new Date().toISOString(), 111 + }; 99 112 } 100 113 101 - console.log(`Found ${candidates.length} candidates to check`); 114 + liveProgress = progress; // For SIGINT handler 102 115 103 - progress = { 104 - checked: {}, 105 - circleIds: [], 106 - candidates, 107 - startedAt: new Date().toISOString(), 108 - lastUpdated: new Date().toISOString(), 109 - }; 110 - } 116 + // Filter out already checked candidates (but retry errors from transient failures) 117 + const remaining = progress.candidates.filter((id) => { 118 + const result = progress.checked[id]; 119 + return result === undefined || result.typename === 'error'; 120 + }); 121 + console.log(`\nRemaining to check: ${String(remaining.length)}`); 111 122 112 - liveProgress = progress; // For SIGINT handler 123 + if (remaining.length === 0) { 124 + console.log('All candidates already checked!'); 125 + writeFinalResults(outputPath, progress); 126 + return; 127 + } 113 128 114 - // Filter out already checked candidates (but retry errors from transient failures) 115 - const remaining = progress.candidates.filter((id) => { 116 - const result = progress!.checked[id]; 117 - return !result || result.typename === "error"; 118 - }); 119 - console.log(`\nRemaining to check: ${remaining.length}`); 129 + // Estimate time (rough: ~50ms per request with concurrency) 130 + const estimatedMs = (remaining.length / CONCURRENCY) * 50; 131 + console.log(`Estimated time: ${formatDuration(estimatedMs)} (${String(CONCURRENCY)} concurrent)`); 132 + console.log(); 120 133 121 - if (remaining.length === 0) { 122 - console.log("All candidates already checked!"); 123 - writeFinalResults(outputPath, progress); 124 - return; 125 - } 134 + // Stats tracking (exclude errors from checked count since they'll be retried) 135 + const errorCount = Object.values(progress.checked).filter((r) => r.typename === 'error').length; 136 + const stats: DetectionStats = { 137 + total: progress.candidates.length, 138 + checked: Object.keys(progress.checked).length - errorCount, 139 + public: 0, 140 + circle: progress.circleIds.length, 141 + errors: 0, 142 + skipped: 0, 143 + }; 126 144 127 - // Estimate time (rough: ~50ms per request with concurrency) 128 - const estimatedMs = (remaining.length / CONCURRENCY) * 50; 129 - console.log(`Estimated time: ${formatDuration(estimatedMs)} (${CONCURRENCY} concurrent)`); 130 - console.log(); 145 + // Recount from existing progress (errors will be retried, don't count them) 146 + for (const result of Object.values(progress.checked)) { 147 + if (result.typename === 'Tweet') stats.public++; 148 + // errors not counted here since they'll be retried 149 + } 131 150 132 - // Stats tracking (exclude errors from checked count since they'll be retried) 133 - const errorCount = Object.values(progress.checked).filter( 134 - (r) => r.typename === "error", 135 - ).length; 136 - const stats: DetectionStats = { 137 - total: progress.candidates.length, 138 - checked: Object.keys(progress.checked).length - errorCount, 139 - public: 0, 140 - circle: progress.circleIds.length, 141 - errors: 0, 142 - skipped: 0, 143 - }; 151 + const startTime = Date.now(); 144 152 145 - // Recount from existing progress (errors will be retried, don't count them) 146 - for (const result of Object.values(progress.checked)) { 147 - if (result.typename === "Tweet") stats.public++; 148 - // errors not counted here since they'll be retried 149 - } 153 + // First pass: handle deleted tweets synchronously (no API needed) 154 + const toCheck: string[] = []; 155 + for (const id of remaining) { 156 + if (deletedIds.has(id)) { 157 + stats.skipped++; 158 + stats.checked++; 159 + progress.checked[id] = { 160 + id, 161 + typename: 'Tweet', // Treat as public (just deleted) 162 + retries: 0, 163 + timestamp: new Date().toISOString(), 164 + }; 165 + } else { 166 + toCheck.push(id); 167 + } 168 + } 150 169 151 - const startTime = Date.now(); 170 + // Process in concurrent batches 171 + for (let i = 0; i < toCheck.length; i += CONCURRENCY) { 172 + const batch = toCheck.slice(i, i + CONCURRENCY); 152 173 153 - // First pass: handle deleted tweets synchronously (no API needed) 154 - const toCheck: string[] = []; 155 - for (const id of remaining) { 156 - if (deletedIds.has(id)) { 157 - stats.skipped++; 158 - stats.checked++; 159 - progress.checked[id] = { 160 - id, 161 - typename: "Tweet", // Treat as public (just deleted) 162 - retries: 0, 163 - timestamp: new Date().toISOString(), 164 - }; 165 - } else { 166 - toCheck.push(id); 167 - } 168 - } 174 + const results = await Promise.all(batch.map((id) => checkTweet(id))); 169 175 170 - // Process in concurrent batches 171 - for (let i = 0; i < toCheck.length; i += CONCURRENCY) { 172 - const batch = toCheck.slice(i, i + CONCURRENCY); 176 + // Process results 177 + for (let j = 0; j < batch.length; j++) { 178 + const id = batch[j]; 179 + const result = results[j]; 173 180 174 - const results = await Promise.all(batch.map((id) => checkTweet(id))); 181 + if (id === undefined || result === undefined) continue; 175 182 176 - // Process results 177 - for (let j = 0; j < batch.length; j++) { 178 - const id = batch[j]; 179 - const result = results[j]; 180 - progress.checked[id] = result; 181 - stats.checked++; 183 + progress.checked[id] = result; 184 + stats.checked++; 182 185 183 - if (result.typename === "TweetTombstone") { 184 - progress.circleIds.push(id); 185 - stats.circle++; 186 - console.log(`[CIRCLE] ${id}`); 187 - } else if (result.typename === "Tweet") { 188 - stats.public++; 189 - } else { 190 - stats.errors++; 191 - console.log(`[ERROR] ${id}: ${result.error}`); 192 - } 193 - } 186 + if (result.typename === 'TweetTombstone') { 187 + progress.circleIds.push(id); 188 + stats.circle++; 189 + console.log(`[CIRCLE] ${id}`); 190 + } else if (result.typename === 'Tweet') { 191 + stats.public++; 192 + } else { 193 + stats.errors++; 194 + console.log(`[ERROR] ${id}: ${result.error ?? 'Unknown error'}`); 195 + } 196 + } 194 197 195 - // Save after each batch 196 - saveProgress(outputPath, progress); 198 + // Save after each batch 199 + saveProgress(outputPath, progress); 197 200 198 - // Progress update every 10 batches (~100 tweets) 199 - const processed = Math.min(i + CONCURRENCY, toCheck.length); 200 - if (processed % 100 < CONCURRENCY) { 201 - const elapsed = Date.now() - startTime; 202 - const rate = processed / (elapsed / 1000); 203 - const eta = (toCheck.length - processed) / rate; 204 - console.log( 205 - `Progress: ${formatProgress(stats.checked, stats.total)} | ` + 206 - `Circle: ${stats.circle} | ` + 207 - `Rate: ${rate.toFixed(1)}/s | ` + 208 - `ETA: ${formatDuration(eta * 1000)}`, 209 - ); 201 + // Progress update every 10 batches (~100 tweets) 202 + const processed = Math.min(i + CONCURRENCY, toCheck.length); 203 + if (processed % 100 < CONCURRENCY) { 204 + const elapsed = Date.now() - startTime; 205 + const rate = processed / (elapsed / 1000); 206 + const eta = (toCheck.length - processed) / rate; 207 + console.log( 208 + `Progress: ${formatProgress(stats.checked, stats.total)} | ` + 209 + `Circle: ${String(stats.circle)} | ` + 210 + `Rate: ${rate.toFixed(1)}/s | ` + 211 + `ETA: ${formatDuration(eta * 1000)}` 212 + ); 213 + } 210 214 } 211 - } 212 215 213 - // Final save 214 - saveProgress(outputPath, progress); 215 - writeFinalResults(outputPath, progress); 216 + // Final save 217 + saveProgress(outputPath, progress); 218 + writeFinalResults(outputPath, progress); 216 219 217 - // Summary 218 - const elapsed = Date.now() - startTime; 219 - console.log(); 220 - console.log("=== DETECTION COMPLETE ==="); 221 - console.log(`Total candidates: ${stats.total}`); 222 - console.log(`Checked: ${stats.checked}`); 223 - console.log(`Public tweets: ${stats.public}`); 224 - console.log(`Circle tweets: ${stats.circle}`); 225 - console.log(`Errors: ${stats.errors}`); 226 - console.log(`Skipped (deleted): ${stats.skipped}`); 227 - console.log(`Time: ${formatDuration(elapsed)}`); 228 - console.log(); 229 - console.log(`Results saved to:`); 230 - console.log(` ${outputPath}/circle-tweets.json`); 231 - console.log(` ${outputPath}/detection-log.json`); 220 + // Summary 221 + const elapsed = Date.now() - startTime; 222 + console.log(); 223 + console.log('=== DETECTION COMPLETE ==='); 224 + console.log(`Total candidates: ${String(stats.total)}`); 225 + console.log(`Checked: ${String(stats.checked)}`); 226 + console.log(`Public tweets: ${String(stats.public)}`); 227 + console.log(`Circle tweets: ${String(stats.circle)}`); 228 + console.log(`Errors: ${String(stats.errors)}`); 229 + console.log(`Skipped (deleted): ${String(stats.skipped)}`); 230 + console.log(`Time: ${formatDuration(elapsed)}`); 231 + console.log(); 232 + console.log(`Results saved to:`); 233 + console.log(` ${outputPath}/circle-tweets.json`); 234 + console.log(` ${outputPath}/detection-log.json`); 232 235 } 233 236 234 - function writeFinalResults(outputPath: string, progress: Progress) { 235 - // Write circle tweet IDs 236 - writeFileSync( 237 - `${outputPath}/circle-tweets.json`, 238 - JSON.stringify(progress.circleIds, null, 2), 239 - ); 237 + function writeFinalResults(outputPath: string, progress: Progress): void { 238 + // Write circle tweet IDs 239 + writeFileSync(`${outputPath}/circle-tweets.json`, JSON.stringify(progress.circleIds, null, 2)); 240 240 241 - // Write full detection log 242 - const log = { 243 - startedAt: progress.startedAt, 244 - completedAt: new Date().toISOString(), 245 - totalCandidates: progress.candidates.length, 246 - circleCount: progress.circleIds.length, 247 - results: progress.checked, 248 - }; 249 - writeFileSync( 250 - `${outputPath}/detection-log.json`, 251 - JSON.stringify(log, null, 2), 252 - ); 241 + // Write full detection log 242 + const log = { 243 + startedAt: progress.startedAt, 244 + completedAt: new Date().toISOString(), 245 + totalCandidates: progress.candidates.length, 246 + circleCount: progress.circleIds.length, 247 + results: progress.checked, 248 + }; 249 + writeFileSync(`${outputPath}/detection-log.json`, JSON.stringify(log, null, 2)); 253 250 } 254 251 255 252 // Live state for SIGINT handler ··· 257 254 let liveOutputPath: string | null = null; 258 255 259 256 // Handle Ctrl+C gracefully - save live in-memory progress, not reloaded from disk 260 - process.on("SIGINT", () => { 261 - console.log("\n\nInterrupted! Saving progress..."); 262 - if (liveProgress && liveOutputPath) { 263 - liveProgress.lastUpdated = new Date().toISOString(); 264 - saveProgress(liveOutputPath, liveProgress); 265 - console.log( 266 - `Progress saved (${Object.keys(liveProgress.checked).length} checked). Run again to resume.`, 267 - ); 268 - } 269 - process.exit(0); 257 + process.on('SIGINT', () => { 258 + console.log('\n\nInterrupted! Saving progress...'); 259 + if (liveProgress !== null && liveOutputPath !== null && liveOutputPath !== '') { 260 + liveProgress.lastUpdated = new Date().toISOString(); 261 + saveProgress(liveOutputPath, liveProgress); 262 + console.log( 263 + `Progress saved (${String(Object.keys(liveProgress.checked).length)} checked). Run again to resume.` 264 + ); 265 + } 266 + process.exit(0); 270 267 }); 271 268 272 - main().catch((e) => { 273 - console.error("Fatal error:", e); 274 - process.exit(1); 269 + main().catch((e: unknown) => { 270 + console.error('Fatal error:', e); 271 + process.exit(1); 275 272 });
+69 -92
src/syndication.ts
··· 1 - import type { SyndicationResult, ApiResponseType } from "./types" 1 + /* eslint-disable no-console */ 2 + import type { SyndicationResult, ApiResponseType } from './types'; 2 3 3 - const TWEET_URL = "https://cdn.syndication.twimg.com/tweet-result" 4 - const BASE_DELAY = 250 // ms between requests 5 - const MAX_RETRIES = 5 6 - const FETCH_TIMEOUT_MS = 30000 // 30s timeout per request 4 + const TWEET_URL = 'https://cdn.syndication.twimg.com/tweet-result'; 5 + const MAX_RETRIES = 5; 6 + const FETCH_TIMEOUT_MS = 30000; // 30s timeout per request 7 7 8 8 function getToken(id: string): string { 9 - // Use BigInt to preserve precision for tweet IDs > 2^53 (Number.MAX_SAFE_INTEGER) 10 - // Original formula: (id / 1e15) * PI 11 - // Split hi/lo to avoid BigInt truncation losing lower digits 12 - const bigId = BigInt(id) 13 - const hi = Number(bigId / 1_000_000_000_000_000n) 14 - const lo = Number(bigId % 1_000_000_000_000_000n) / 1e15 15 - return ((hi + lo) * Math.PI).toString(36).replace(/(0+|\.)/g, "") 9 + // Use BigInt to preserve precision for tweet IDs > 2^53 (Number.MAX_SAFE_INTEGER) 10 + // Original formula: (id / 1e15) * PI 11 + // Split hi/lo to avoid BigInt truncation losing lower digits 12 + const bigId = BigInt(id); 13 + const hi = Number(bigId / 1_000_000_000_000_000n); 14 + const lo = Number(bigId % 1_000_000_000_000_000n) / 1e15; 15 + return ((hi + lo) * Math.PI).toString(36).replace(/(0+|\.)/g, ''); 16 16 } 17 17 18 18 function sleep(ms: number): Promise<void> { 19 - return new Promise((resolve) => setTimeout(resolve, ms)) 19 + return new Promise((resolve) => { 20 + setTimeout(resolve, ms); 21 + }); 20 22 } 21 23 22 24 export async function checkTweet(id: string): Promise<SyndicationResult> { 23 - const token = getToken(id) 24 - const url = `${TWEET_URL}?id=${id}&token=${token}` 25 + const token = getToken(id); 26 + const url = `${TWEET_URL}?id=${id}&token=${token}`; 25 27 26 - let lastError: string | undefined 27 - let retries = 0 28 + let lastError: string | undefined; 29 + let retries = 0; 30 + 31 + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { 32 + const controller = new AbortController(); 33 + const timeout = setTimeout(() => { 34 + controller.abort(); 35 + }, FETCH_TIMEOUT_MS); 36 + try { 37 + const resp = await fetch(url, { signal: controller.signal }); 28 38 29 - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { 30 - const controller = new AbortController() 31 - const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) 32 - try { 33 - const resp = await fetch(url, { signal: controller.signal }) 39 + // Rate limited - exponential backoff 40 + if (resp.status === 429) { 41 + const backoff = Math.pow(2, attempt) * 1000; 42 + console.log(` Rate limited, waiting ${String(backoff / 1000)}s...`); 43 + await sleep(backoff); 44 + retries++; 45 + continue; 46 + } 34 47 35 - // Rate limited - exponential backoff 36 - if (resp.status === 429) { 37 - const backoff = Math.pow(2, attempt) * 1000 38 - console.log(` Rate limited, waiting ${backoff / 1000}s...`) 39 - await sleep(backoff) 40 - retries++ 41 - continue 42 - } 48 + // Other HTTP errors 49 + if (!resp.ok) { 50 + lastError = `HTTP ${String(resp.status)}`; 51 + retries++; 52 + await sleep(1000 * attempt); 53 + continue; 54 + } 43 55 44 - // Other HTTP errors 45 - if (!resp.ok) { 46 - lastError = `HTTP ${resp.status}` 47 - retries++ 48 - await sleep(1000 * attempt) 49 - continue 50 - } 56 + const data = (await resp.json()) as { __typename?: string; tombstone?: { text?: { text?: string } } }; 51 57 52 - const data = await resp.json() as { __typename?: string; tombstone?: { text?: { text?: string } } } 58 + const typename: ApiResponseType = 59 + data.__typename === 'Tweet' 60 + ? 'Tweet' 61 + : data.__typename === 'TweetTombstone' 62 + ? 'TweetTombstone' 63 + : 'error'; 53 64 54 - const typename: ApiResponseType = 55 - data.__typename === "Tweet" ? "Tweet" : 56 - data.__typename === "TweetTombstone" ? "TweetTombstone" : 57 - "error" 65 + return { 66 + id, 67 + typename, 68 + tombstoneText: data.tombstone?.text?.text, 69 + retries, 70 + timestamp: new Date().toISOString(), 71 + }; 72 + } catch (e) { 73 + lastError = e instanceof Error ? e.message : String(e); 74 + retries++; 75 + await sleep(1000 * attempt); 76 + } finally { 77 + clearTimeout(timeout); 78 + } 79 + } 58 80 59 - return { 81 + return { 60 82 id, 61 - typename, 62 - tombstoneText: data.tombstone?.text?.text, 83 + typename: 'error', 84 + error: lastError ?? 'Max retries exceeded', 63 85 retries, 64 - timestamp: new Date().toISOString() 65 - } 66 - } catch (e) { 67 - lastError = e instanceof Error ? e.message : String(e) 68 - retries++ 69 - await sleep(1000 * attempt) 70 - } finally { 71 - clearTimeout(timeout) 72 - } 73 - } 74 - 75 - return { 76 - id, 77 - typename: "error", 78 - error: lastError || "Max retries exceeded", 79 - retries, 80 - timestamp: new Date().toISOString() 81 - } 82 - } 83 - 84 - export interface CheckOptions { 85 - onProgress?: (checked: number, total: number, result: SyndicationResult) => void 86 - delayMs?: number 87 - } 88 - 89 - export async function checkTweets( 90 - ids: string[], 91 - options: CheckOptions = {} 92 - ): Promise<SyndicationResult[]> { 93 - const { onProgress, delayMs = BASE_DELAY } = options 94 - const results: SyndicationResult[] = [] 95 - 96 - for (let i = 0; i < ids.length; i++) { 97 - const result = await checkTweet(ids[i]) 98 - results.push(result) 99 - 100 - if (onProgress) { 101 - onProgress(i + 1, ids.length, result) 102 - } 103 - 104 - // Delay between requests (skip on last) 105 - if (i < ids.length - 1) { 106 - await sleep(delayMs) 107 - } 108 - } 109 - 110 - return results 86 + timestamp: new Date().toISOString(), 87 + }; 111 88 }
+30 -38
src/types.ts
··· 1 - export interface TweetHeader { 2 - tweet: { 3 - tweet_id: string 4 - user_id: string 5 - created_at: string 6 - } 7 - } 8 - 9 1 export interface Tweet { 10 - tweet: { 11 - id_str: string 12 - created_at: string 13 - full_text: string 14 - in_reply_to_status_id_str?: string 15 - in_reply_to_user_id_str?: string 16 - [key: string]: unknown 17 - } 2 + tweet: { 3 + id_str: string; 4 + created_at: string; 5 + full_text: string; 6 + in_reply_to_status_id_str?: string; 7 + in_reply_to_user_id_str?: string; 8 + [key: string]: unknown; 9 + }; 18 10 } 19 11 20 12 export interface DeletedTweet { 21 - tweet: { 22 - tweet_id?: string 23 - id_str?: string 24 - } 13 + tweet: { 14 + tweet_id?: string; 15 + id_str?: string; 16 + }; 25 17 } 26 18 27 - export type ApiResponseType = "Tweet" | "TweetTombstone" | "error" 19 + export type ApiResponseType = 'Tweet' | 'TweetTombstone' | 'error'; 28 20 29 21 export interface SyndicationResult { 30 - id: string 31 - typename: ApiResponseType 32 - tombstoneText?: string 33 - error?: string 34 - retries: number 35 - timestamp: string 22 + id: string; 23 + typename: ApiResponseType; 24 + tombstoneText?: string | undefined; 25 + error?: string | undefined; 26 + retries: number; 27 + timestamp: string; 36 28 } 37 29 38 30 export interface Progress { 39 - checked: Record<string, SyndicationResult> 40 - circleIds: string[] 41 - candidates: string[] 42 - startedAt: string 43 - lastUpdated: string 31 + checked: Record<string, SyndicationResult>; 32 + circleIds: string[]; 33 + candidates: string[]; 34 + startedAt: string; 35 + lastUpdated: string; 44 36 } 45 37 46 38 export interface DetectionStats { 47 - total: number 48 - checked: number 49 - public: number 50 - circle: number 51 - errors: number 52 - skipped: number 39 + total: number; 40 + checked: number; 41 + public: number; 42 + circle: number; 43 + errors: number; 44 + skipped: number; 53 45 }
+49 -55
src/utils.ts
··· 1 - import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs" 2 - import type { TweetHeader, Tweet, DeletedTweet, Progress } from "./types" 1 + import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs'; 2 + import type { Tweet, DeletedTweet, Progress } from './types'; 3 3 4 - export function loadJsData<T>(filePath: string): T[] { 5 - const content = readFileSync(filePath, "utf8") 6 - const match = content.match(/^window\.YTD\.\w+\.part\d+ = /) 7 - if (!match) throw new Error(`Invalid JS file format: ${filePath}`) 8 - const json = content.slice(match[0].length) 9 - return JSON.parse(json) 10 - } 11 - 12 - export function loadTweetHeaders(archivePath: string): TweetHeader[] { 13 - return loadJsData<TweetHeader>(`${archivePath}/tweet-headers.js`) 4 + function loadJsData<T>(filePath: string): T[] { 5 + const content = readFileSync(filePath, 'utf8'); 6 + const match = /^window\.YTD\.\w+\.part\d+ = /.exec(content); 7 + if (match === null) throw new Error(`Invalid JS file format: ${filePath}`); 8 + const json = content.slice(match[0].length); 9 + return JSON.parse(json) as T[]; 14 10 } 15 11 16 12 export function loadTweets(archivePath: string): Tweet[] { 17 - // Dynamically find all tweet files (tweets.js, tweets-part1.js, tweets-part2.js, etc.) 18 - const allFiles = readdirSync(archivePath) 19 - const tweetFiles = allFiles 20 - .filter((f) => f === "tweets.js" || f.match(/^tweets-part\d+\.js$/)) 21 - .sort() // Ensure consistent ordering 13 + // Dynamically find all tweet files (tweets.js, tweets-part1.js, tweets-part2.js, etc.) 14 + const allFiles = readdirSync(archivePath); 15 + const tweetFiles = allFiles.filter((f) => f === 'tweets.js' || /^tweets-part\d+\.js$/.exec(f) !== null).sort(); // Ensure consistent ordering 22 16 23 - const tweets: Tweet[] = [] 24 - for (const file of tweetFiles) { 25 - tweets.push(...loadJsData<Tweet>(`${archivePath}/${file}`)) 26 - } 17 + const tweets: Tweet[] = []; 18 + for (const file of tweetFiles) { 19 + tweets.push(...loadJsData<Tweet>(`${archivePath}/${file}`)); 20 + } 27 21 28 - return tweets 22 + return tweets; 29 23 } 30 24 31 25 export function loadDeletedTweetIds(archivePath: string): Set<string> { 32 - // Dynamically find all deleted-tweets files (deleted-tweets.js, deleted-tweets-part1.js, etc.) 33 - const allFiles = readdirSync(archivePath) 34 - const deletedFiles = allFiles 35 - .filter((f) => f === "deleted-tweets.js" || f.match(/^deleted-tweets-part\d+\.js$/)) 36 - .sort() 26 + // Dynamically find all deleted-tweets files (deleted-tweets.js, deleted-tweets-part1.js, etc.) 27 + const allFiles = readdirSync(archivePath); 28 + const deletedFiles = allFiles 29 + .filter((f) => f === 'deleted-tweets.js' || /^deleted-tweets-part\d+\.js$/.exec(f) !== null) 30 + .sort(); 37 31 38 - if (deletedFiles.length === 0) return new Set() 32 + if (deletedFiles.length === 0) return new Set(); 39 33 40 - const allDeleted: DeletedTweet[] = [] 41 - for (const file of deletedFiles) { 42 - allDeleted.push(...loadJsData<DeletedTweet>(`${archivePath}/${file}`)) 43 - } 34 + const allDeleted: DeletedTweet[] = []; 35 + for (const file of deletedFiles) { 36 + allDeleted.push(...loadJsData<DeletedTweet>(`${archivePath}/${file}`)); 37 + } 44 38 45 - return new Set(allDeleted.map((d) => d.tweet.tweet_id || d.tweet.id_str || "")) 39 + return new Set(allDeleted.map((d) => d.tweet.tweet_id ?? d.tweet.id_str ?? '')); 46 40 } 47 41 48 42 export function loadProgress(outputPath: string): Progress | null { 49 - const path = `${outputPath}/progress.json` 50 - if (!existsSync(path)) return null 51 - return JSON.parse(readFileSync(path, "utf8")) 43 + const path = `${outputPath}/progress.json`; 44 + if (!existsSync(path)) return null; 45 + return JSON.parse(readFileSync(path, 'utf8')) as Progress; 52 46 } 53 47 54 48 export function saveProgress(outputPath: string, progress: Progress): void { 55 - progress.lastUpdated = new Date().toISOString() 56 - writeFileSync(`${outputPath}/progress.json`, JSON.stringify(progress, null, 2)) 49 + progress.lastUpdated = new Date().toISOString(); 50 + writeFileSync(`${outputPath}/progress.json`, JSON.stringify(progress, null, 2)); 57 51 } 58 52 59 - export function parseDate(dateStr: string): Date { 60 - // Twitter date format: "Sun Oct 29 23:42:00 +0000 2023" 61 - return new Date(dateStr) 53 + function parseDate(dateStr: string): Date { 54 + // Twitter date format: "Sun Oct 29 23:42:00 +0000 2023" 55 + return new Date(dateStr); 62 56 } 63 57 64 58 export function isInDateRange(dateStr: string, start: Date, end: Date): boolean { 65 - const date = parseDate(dateStr) 66 - return date >= start && date <= end 59 + const date = parseDate(dateStr); 60 + return date >= start && date <= end; 67 61 } 68 62 69 63 export function isRetweet(text: string): boolean { 70 - return text.startsWith("RT @") 64 + return text.startsWith('RT @'); 71 65 } 72 66 73 67 export function formatDuration(ms: number): string { 74 - const seconds = Math.floor(ms / 1000) 75 - const minutes = Math.floor(seconds / 60) 76 - const hours = Math.floor(minutes / 60) 68 + const seconds = Math.floor(ms / 1000); 69 + const minutes = Math.floor(seconds / 60); 70 + const hours = Math.floor(minutes / 60); 77 71 78 - if (hours > 0) { 79 - return `${hours}h ${minutes % 60}m` 80 - } else if (minutes > 0) { 81 - return `${minutes}m ${seconds % 60}s` 82 - } 83 - return `${seconds}s` 72 + if (hours > 0) { 73 + return `${String(hours)}h ${String(minutes % 60)}m`; 74 + } else if (minutes > 0) { 75 + return `${String(minutes)}m ${String(seconds % 60)}s`; 76 + } 77 + return `${String(seconds)}s`; 84 78 } 85 79 86 80 export function formatProgress(current: number, total: number): string { 87 - const pct = ((current / total) * 100).toFixed(1) 88 - return `${current}/${total} (${pct}%)` 81 + const pct = ((current / total) * 100).toFixed(1); 82 + return `${String(current)}/${String(total)} (${pct}%)`; 89 83 }
+26 -24
tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - // Environment setup & latest features 4 - "lib": ["ESNext"], 5 - "target": "ESNext", 6 - "module": "Preserve", 7 - "moduleDetection": "force", 8 - "jsx": "react-jsx", 9 - "allowJs": true, 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 10 11 - // Bundler mode 12 - "moduleResolution": "bundler", 13 - "allowImportingTsExtensions": true, 14 - "verbatimModuleSyntax": true, 15 - "noEmit": true, 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 16 17 - // Best practices 18 - "strict": true, 19 - "skipLibCheck": true, 20 - "noFallthroughCasesInSwitch": true, 21 - "noUncheckedIndexedAccess": true, 22 - "noImplicitOverride": true, 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 23 24 - // Some stricter flags (disabled by default) 25 - "noUnusedLocals": false, 26 - "noUnusedParameters": false, 27 - "noPropertyAccessFromIndexSignature": false 28 - } 24 + // Stricter flags - TSC handles these 25 + "noUnusedLocals": true, 26 + "noUnusedParameters": true, 27 + "noPropertyAccessFromIndexSignature": true, 28 + "exactOptionalPropertyTypes": true, 29 + "noImplicitReturns": true 30 + } 29 31 }