├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENCE ├── README.md ├── bin └── tube-manager.js ├── package-lock.json ├── package.json ├── samples ├── config.json ├── tube-manager.json └── tube-manager │ └── YrUSJQq8WOA.jpg ├── schemas ├── config.schema.json └── tube-manager.schema.json ├── src ├── config.ts ├── index.ts └── youtube.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.md] 11 | insert_final_newline = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folder view configuration files 2 | .DS_Store 3 | 4 | # Editor files 5 | *.code-workspace 6 | 7 | # Node files 8 | /node_modules 9 | npm-debug.log 10 | 11 | # Project files 12 | .env 13 | 14 | # Developement files 15 | build/ 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["embeddable"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - present Sun Knudsen and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tube-manager 2 | 3 | ## Manage YouTube videos using command line. 4 | 5 | This project was developed to streamline the process of managing YouTube titles, descriptions and tags. 6 | 7 | ## I can do this by hand, why would I use tube-manager? 8 | 9 | Because managing this by hand as a channel grows becomes exponentially overwhelming, especially when videos are associated to others and affiliate links are used over and over (what if a link changes?). 10 | 11 | Not mentioning associated video links need to be platform-specific and having all this in one place is amazing (and Git friendly). 12 | 13 | For example, here’s how the following data from dataset (see [sample](./samples/tube-manager.json)) is transpiled for YouTube. 14 | 15 | Notice how suggested video and affiliate links are automatically expanded? 16 | 17 | **Data** 18 | 19 | ```json 20 | { 21 | "id": "YrUSJQq8WOA", 22 | "publishedAt": "2022-11-03T14:43:45Z", 23 | "title": "macOS stores a copy of everything one prints forever", 24 | "description": "In this episode, we explore how macOS stores a copy of everything one prints forever.", 25 | "tags": ["Privacy", "Security", "macOS"], 26 | "categoryId": "27", 27 | "sections": [], 28 | "suggestedVideos": [], 29 | "links": [ 30 | { 31 | "label": "How to disable CUPS pinter job history on macOS", 32 | "url": "https://sunknudsen.com/guides/how-to-disable-cups-printer-job-history-on-macos" 33 | }, 34 | { 35 | "label": "Twitter (please follow @superbacked)", 36 | "url": "https://twitter.com/superbacked" 37 | }, 38 | { 39 | "label": "Superbacked (join waiting list)", 40 | "url": "https://superbacked.com/" 41 | } 42 | ], 43 | "credits": [], 44 | "affiliateLinks": ["amazon.brotherPrinter"], 45 | "footnotes": [], 46 | "support": [ 47 | { 48 | "label": "Support my work", 49 | "url": "https://sunknudsen.com/donate" 50 | } 51 | ], 52 | "thumbnailHash": "65c65f74ac181dcffc994d9f75ed88392bdcc434165bc59dda2a0553a0725ac6" 53 | } 54 | ``` 55 | 56 | **YouTube** 57 | 58 | ``` 59 | macOS stores a copy of everything one prints forever 60 | 61 | In this episode, we explore how macOS stores a copy of everything one prints forever. 62 | 63 | ============================== 64 | LINKS 65 | ============================== 66 | How to disable CUPS pinter job history on macOS 👉 https://sunknudsen.com/guides/how-to-disable-cups-printer-job-history-on-macos 67 | Twitter (please follow @superbacked) 👉 https://twitter.com/superbacked 68 | Superbacked (join waiting list) 👉 https://superbacked.com/ 69 | 70 | ============================== 71 | AFFILIATE LINKS 72 | ============================== 73 | Brother HL-L2460DW printer 74 | USA 👉 https://www.amazon.com/dp/B0CPL2N5H6?tag=sunknudsen06-20 75 | 76 | ============================== 77 | SUPPORT 78 | ============================== 79 | Support my work 👉 https://sunknudsen.com/donate 80 | ``` 81 | 82 | ## Installation 83 | 84 | ### Step 1: go to https://console.developers.google.com 85 | 86 | ### Step 2: create project, enable “YouTube Data API v3” and “YouTube Analytics API” APIs and create “OAuth client ID” credentials (required scopes: `.../auth/yt-analytics.readonly` and `.../auth/youtube.force-ssl`) 87 | 88 | This is where we get the values of `youtube.clientId` and `youtube.clientSecret`. 89 | 90 | ### Step 3: run following commands 91 | 92 | #### macOS 93 | 94 | ```shell 95 | npm install -g tube-manager 96 | mkdir -p ~/.tube-manager 97 | cp $(npm root -g)/tube-manager/samples/config.json ~/.tube-manager/config.json 98 | open -a "TextEdit" ~/.tube-manager/config.json 99 | ``` 100 | 101 | #### Linux 102 | 103 | > Heads up: if `nano` is not installed, please use `vi`. 104 | 105 | ```shell 106 | sudo npm install tube-manager -g 107 | mkdir -p ~/.tube-manager 108 | cp $(npm root -g)/tube-manager/config.json.sample ~/.tube-manager/config.json 109 | nano ~/.tube-manager/config.json 110 | ``` 111 | 112 | ### Step 4: edit `config.json` 113 | 114 | > Heads up: for increased security, saving `youtube.refreshToken` is optional (when omitted, a prompt will ask for refresh token at run time). 115 | 116 | Once YouTube client ID and secret are saved to `config.json`, run `tube-manager refresh-token youtube` to get values of `youtube.accessToken` and `youtube.refreshToken`. 117 | 118 | Once access and refresh token are saved to `config.json`, run `tube-manager channels youtube` to get value of `youtube.channelId`. 119 | 120 | ## Usage 121 | 122 | ```console 123 | $ tube-manager -h 124 | Usage: tube-manager [options] [command] 125 | 126 | Options: 127 | -h, --help display help for command 128 | 129 | Commands: 130 | refresh-token [options] get refresh token 131 | channels [options] get channels 132 | stats [options] get stats 133 | video [options] get video 134 | initialize [options] initialize dataset 135 | import [options] import video 136 | preview [options] preview video 137 | publish [options] [id] publish video(s) 138 | help [command] display help for command 139 | ``` 140 | 141 | **TL;DR** 142 | 143 | 1. Initialize dataset using `tube-manager initialize` 144 | 2. Upload video to YouTube as usual using [YouTube Studio](https://studio.youtube.com/) 145 | 3. Import video to dataset using `tube-manager import ` 146 | 4. Edit title, description, tags, **sections**, **suggestedVideos**, **links**, **credits**, **affiliateLinks**, **footnotes** and **support** (see [tube-manager.schema.json](./schemas/tube-manager.schema.json)) 147 | 5. Add thumbnail to thumbnail directory (see [sample](./samples/tube-manager) and `tube-manager publish --help`) 148 | 6. Publish video to YouTube using `tube-manager publish ` 149 | 150 | ## How to use [Visual Studio Code](https://code.visualstudio.com/) to edit config and dataset 151 | 152 | Editing config and dataset using Visual Studio Code makes things much more efficient thanks to [IntelliSense](https://code.visualstudio.com/Docs/languages/json). 153 | 154 | ### Step 1: download and install Visual Studio Code 155 | 156 | ### Step 2: enable `code` command 157 | 158 | Click “View”, then “Command Palette…”, type “install code”, select “Install 'code' command in PATH” and press enter. 159 | 160 | ### Step 3: download and install Prettier extension 161 | 162 | ### Step 4: add JSON schemas to user settings 163 | 164 | Click “View”, then “Command Palette…”, type “settings json”, select and press enter. 165 | 166 | ```json 167 | "json.schemas": [ 168 | { 169 | "fileMatch": [ 170 | ".tube-manager/config.json" 171 | ], 172 | "url": "https://raw.githubusercontent.com/sunknudsen/tube-manager/master/schemas/config.schema.json" 173 | }, 174 | { 175 | "fileMatch": [ 176 | "tube-manager.json" 177 | ], 178 | "url": "https://raw.githubusercontent.com/sunknudsen/tube-manager/master/schemas/tube-manager.schema.json" 179 | } 180 | ] 181 | ``` 182 | 183 | ### Step 5: edit config or dataset using `code ~/.tube-manager/config.json` and `code /path/to/tube-manager.json` 184 | 185 | ## Contributors 186 | 187 | [Sun Knudsen](https://sunknudsen.com/) 188 | 189 | ## Licence 190 | 191 | MIT 192 | -------------------------------------------------------------------------------- /bin/tube-manager.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "../build/index.js" 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tube-manager", 3 | "version": "0.7.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tube-manager", 9 | "version": "0.7.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chalk": "^5.4.1", 13 | "commander": "^13.1.0", 14 | "fs-extra": "^11.3.0", 15 | "got": "^14.4.7", 16 | "hasha": "^6.0.0", 17 | "open": "^10.1.2", 18 | "p-whilst": "^3.2.0", 19 | "prettier": "^3.5.3", 20 | "prompts": "^2.4.2", 21 | "query-string": "^9.1.2" 22 | }, 23 | "bin": { 24 | "tube-manager": "bin/tube-manager.js" 25 | }, 26 | "devDependencies": { 27 | "@types/fs-extra": "^11.0.4", 28 | "@types/node": "^22.15.17", 29 | "@types/prettier": "^2.7.3", 30 | "@types/prompts": "^2.4.9", 31 | "npm-check-updates": "^18.0.1", 32 | "typescript": "^5.8.3" 33 | }, 34 | "engines": { 35 | "node": ">=18" 36 | } 37 | }, 38 | "node_modules/@sec-ant/readable-stream": { 39 | "version": "0.4.1", 40 | "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", 41 | "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", 42 | "license": "MIT" 43 | }, 44 | "node_modules/@sindresorhus/is": { 45 | "version": "7.0.1", 46 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz", 47 | "integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==", 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=18" 51 | }, 52 | "funding": { 53 | "url": "https://github.com/sindresorhus/is?sponsor=1" 54 | } 55 | }, 56 | "node_modules/@szmarczak/http-timer": { 57 | "version": "5.0.1", 58 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", 59 | "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", 60 | "dependencies": { 61 | "defer-to-connect": "^2.0.1" 62 | }, 63 | "engines": { 64 | "node": ">=14.16" 65 | } 66 | }, 67 | "node_modules/@types/fs-extra": { 68 | "version": "11.0.4", 69 | "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", 70 | "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", 71 | "dev": true, 72 | "license": "MIT", 73 | "dependencies": { 74 | "@types/jsonfile": "*", 75 | "@types/node": "*" 76 | } 77 | }, 78 | "node_modules/@types/http-cache-semantics": { 79 | "version": "4.0.4", 80 | "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", 81 | "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", 82 | "license": "MIT" 83 | }, 84 | "node_modules/@types/jsonfile": { 85 | "version": "6.1.4", 86 | "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", 87 | "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", 88 | "dev": true, 89 | "license": "MIT", 90 | "dependencies": { 91 | "@types/node": "*" 92 | } 93 | }, 94 | "node_modules/@types/node": { 95 | "version": "22.15.17", 96 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", 97 | "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", 98 | "dev": true, 99 | "license": "MIT", 100 | "dependencies": { 101 | "undici-types": "~6.21.0" 102 | } 103 | }, 104 | "node_modules/@types/prettier": { 105 | "version": "2.7.3", 106 | "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", 107 | "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", 108 | "dev": true, 109 | "license": "MIT" 110 | }, 111 | "node_modules/@types/prompts": { 112 | "version": "2.4.9", 113 | "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", 114 | "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", 115 | "dev": true, 116 | "license": "MIT", 117 | "dependencies": { 118 | "@types/node": "*", 119 | "kleur": "^3.0.3" 120 | } 121 | }, 122 | "node_modules/bundle-name": { 123 | "version": "4.1.0", 124 | "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", 125 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 126 | "license": "MIT", 127 | "dependencies": { 128 | "run-applescript": "^7.0.0" 129 | }, 130 | "engines": { 131 | "node": ">=18" 132 | }, 133 | "funding": { 134 | "url": "https://github.com/sponsors/sindresorhus" 135 | } 136 | }, 137 | "node_modules/cacheable-lookup": { 138 | "version": "7.0.0", 139 | "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", 140 | "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", 141 | "engines": { 142 | "node": ">=14.16" 143 | } 144 | }, 145 | "node_modules/cacheable-request": { 146 | "version": "12.0.1", 147 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", 148 | "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", 149 | "license": "MIT", 150 | "dependencies": { 151 | "@types/http-cache-semantics": "^4.0.4", 152 | "get-stream": "^9.0.1", 153 | "http-cache-semantics": "^4.1.1", 154 | "keyv": "^4.5.4", 155 | "mimic-response": "^4.0.0", 156 | "normalize-url": "^8.0.1", 157 | "responselike": "^3.0.0" 158 | }, 159 | "engines": { 160 | "node": ">=18" 161 | } 162 | }, 163 | "node_modules/chalk": { 164 | "version": "5.4.1", 165 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", 166 | "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", 167 | "license": "MIT", 168 | "engines": { 169 | "node": "^12.17.0 || ^14.13 || >=16.0.0" 170 | }, 171 | "funding": { 172 | "url": "https://github.com/chalk/chalk?sponsor=1" 173 | } 174 | }, 175 | "node_modules/commander": { 176 | "version": "13.1.0", 177 | "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", 178 | "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", 179 | "license": "MIT", 180 | "engines": { 181 | "node": ">=18" 182 | } 183 | }, 184 | "node_modules/decode-uri-component": { 185 | "version": "0.4.1", 186 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", 187 | "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", 188 | "license": "MIT", 189 | "engines": { 190 | "node": ">=14.16" 191 | } 192 | }, 193 | "node_modules/decompress-response": { 194 | "version": "6.0.0", 195 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 196 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 197 | "dependencies": { 198 | "mimic-response": "^3.1.0" 199 | }, 200 | "engines": { 201 | "node": ">=10" 202 | }, 203 | "funding": { 204 | "url": "https://github.com/sponsors/sindresorhus" 205 | } 206 | }, 207 | "node_modules/decompress-response/node_modules/mimic-response": { 208 | "version": "3.1.0", 209 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 210 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 211 | "engines": { 212 | "node": ">=10" 213 | }, 214 | "funding": { 215 | "url": "https://github.com/sponsors/sindresorhus" 216 | } 217 | }, 218 | "node_modules/default-browser": { 219 | "version": "5.2.1", 220 | "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", 221 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 222 | "license": "MIT", 223 | "dependencies": { 224 | "bundle-name": "^4.1.0", 225 | "default-browser-id": "^5.0.0" 226 | }, 227 | "engines": { 228 | "node": ">=18" 229 | }, 230 | "funding": { 231 | "url": "https://github.com/sponsors/sindresorhus" 232 | } 233 | }, 234 | "node_modules/default-browser-id": { 235 | "version": "5.0.0", 236 | "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", 237 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", 238 | "license": "MIT", 239 | "engines": { 240 | "node": ">=18" 241 | }, 242 | "funding": { 243 | "url": "https://github.com/sponsors/sindresorhus" 244 | } 245 | }, 246 | "node_modules/defer-to-connect": { 247 | "version": "2.0.1", 248 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", 249 | "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", 250 | "engines": { 251 | "node": ">=10" 252 | } 253 | }, 254 | "node_modules/define-lazy-prop": { 255 | "version": "3.0.0", 256 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", 257 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", 258 | "license": "MIT", 259 | "engines": { 260 | "node": ">=12" 261 | }, 262 | "funding": { 263 | "url": "https://github.com/sponsors/sindresorhus" 264 | } 265 | }, 266 | "node_modules/filter-obj": { 267 | "version": "5.1.0", 268 | "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", 269 | "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", 270 | "license": "MIT", 271 | "engines": { 272 | "node": ">=14.16" 273 | }, 274 | "funding": { 275 | "url": "https://github.com/sponsors/sindresorhus" 276 | } 277 | }, 278 | "node_modules/form-data-encoder": { 279 | "version": "4.0.2", 280 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", 281 | "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", 282 | "license": "MIT", 283 | "engines": { 284 | "node": ">= 18" 285 | } 286 | }, 287 | "node_modules/fs-extra": { 288 | "version": "11.3.0", 289 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", 290 | "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", 291 | "license": "MIT", 292 | "dependencies": { 293 | "graceful-fs": "^4.2.0", 294 | "jsonfile": "^6.0.1", 295 | "universalify": "^2.0.0" 296 | }, 297 | "engines": { 298 | "node": ">=14.14" 299 | } 300 | }, 301 | "node_modules/get-stream": { 302 | "version": "9.0.1", 303 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", 304 | "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", 305 | "license": "MIT", 306 | "dependencies": { 307 | "@sec-ant/readable-stream": "^0.4.1", 308 | "is-stream": "^4.0.1" 309 | }, 310 | "engines": { 311 | "node": ">=18" 312 | }, 313 | "funding": { 314 | "url": "https://github.com/sponsors/sindresorhus" 315 | } 316 | }, 317 | "node_modules/got": { 318 | "version": "14.4.7", 319 | "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", 320 | "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", 321 | "license": "MIT", 322 | "dependencies": { 323 | "@sindresorhus/is": "^7.0.1", 324 | "@szmarczak/http-timer": "^5.0.1", 325 | "cacheable-lookup": "^7.0.0", 326 | "cacheable-request": "^12.0.1", 327 | "decompress-response": "^6.0.0", 328 | "form-data-encoder": "^4.0.2", 329 | "http2-wrapper": "^2.2.1", 330 | "lowercase-keys": "^3.0.0", 331 | "p-cancelable": "^4.0.1", 332 | "responselike": "^3.0.0", 333 | "type-fest": "^4.26.1" 334 | }, 335 | "engines": { 336 | "node": ">=20" 337 | }, 338 | "funding": { 339 | "url": "https://github.com/sindresorhus/got?sponsor=1" 340 | } 341 | }, 342 | "node_modules/graceful-fs": { 343 | "version": "4.2.10", 344 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", 345 | "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" 346 | }, 347 | "node_modules/hasha": { 348 | "version": "6.0.0", 349 | "resolved": "https://registry.npmjs.org/hasha/-/hasha-6.0.0.tgz", 350 | "integrity": "sha512-MLydoyGp9QJcjlhE5lsLHXYpWayjjWqkavzju2ZWD2tYa1CgmML1K1gWAu22BLFa2eZ0OfvJ/DlfoVjaD54U2Q==", 351 | "license": "MIT", 352 | "dependencies": { 353 | "is-stream": "^3.0.0", 354 | "type-fest": "^4.7.1" 355 | }, 356 | "engines": { 357 | "node": ">=18" 358 | }, 359 | "funding": { 360 | "url": "https://github.com/sponsors/sindresorhus" 361 | } 362 | }, 363 | "node_modules/hasha/node_modules/is-stream": { 364 | "version": "3.0.0", 365 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", 366 | "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", 367 | "license": "MIT", 368 | "engines": { 369 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 370 | }, 371 | "funding": { 372 | "url": "https://github.com/sponsors/sindresorhus" 373 | } 374 | }, 375 | "node_modules/http-cache-semantics": { 376 | "version": "4.2.0", 377 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", 378 | "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", 379 | "license": "BSD-2-Clause" 380 | }, 381 | "node_modules/http2-wrapper": { 382 | "version": "2.2.1", 383 | "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", 384 | "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", 385 | "license": "MIT", 386 | "dependencies": { 387 | "quick-lru": "^5.1.1", 388 | "resolve-alpn": "^1.2.0" 389 | }, 390 | "engines": { 391 | "node": ">=10.19.0" 392 | } 393 | }, 394 | "node_modules/is-docker": { 395 | "version": "3.0.0", 396 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", 397 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 398 | "license": "MIT", 399 | "bin": { 400 | "is-docker": "cli.js" 401 | }, 402 | "engines": { 403 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 404 | }, 405 | "funding": { 406 | "url": "https://github.com/sponsors/sindresorhus" 407 | } 408 | }, 409 | "node_modules/is-inside-container": { 410 | "version": "1.0.0", 411 | "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", 412 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 413 | "license": "MIT", 414 | "dependencies": { 415 | "is-docker": "^3.0.0" 416 | }, 417 | "bin": { 418 | "is-inside-container": "cli.js" 419 | }, 420 | "engines": { 421 | "node": ">=14.16" 422 | }, 423 | "funding": { 424 | "url": "https://github.com/sponsors/sindresorhus" 425 | } 426 | }, 427 | "node_modules/is-stream": { 428 | "version": "4.0.1", 429 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", 430 | "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", 431 | "license": "MIT", 432 | "engines": { 433 | "node": ">=18" 434 | }, 435 | "funding": { 436 | "url": "https://github.com/sponsors/sindresorhus" 437 | } 438 | }, 439 | "node_modules/is-wsl": { 440 | "version": "3.1.0", 441 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", 442 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 443 | "license": "MIT", 444 | "dependencies": { 445 | "is-inside-container": "^1.0.0" 446 | }, 447 | "engines": { 448 | "node": ">=16" 449 | }, 450 | "funding": { 451 | "url": "https://github.com/sponsors/sindresorhus" 452 | } 453 | }, 454 | "node_modules/json-buffer": { 455 | "version": "3.0.1", 456 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 457 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 458 | "license": "MIT" 459 | }, 460 | "node_modules/jsonfile": { 461 | "version": "6.1.0", 462 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 463 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 464 | "dependencies": { 465 | "universalify": "^2.0.0" 466 | }, 467 | "optionalDependencies": { 468 | "graceful-fs": "^4.1.6" 469 | } 470 | }, 471 | "node_modules/keyv": { 472 | "version": "4.5.4", 473 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 474 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 475 | "license": "MIT", 476 | "dependencies": { 477 | "json-buffer": "3.0.1" 478 | } 479 | }, 480 | "node_modules/kleur": { 481 | "version": "3.0.3", 482 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 483 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", 484 | "license": "MIT", 485 | "engines": { 486 | "node": ">=6" 487 | } 488 | }, 489 | "node_modules/lowercase-keys": { 490 | "version": "3.0.0", 491 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", 492 | "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", 493 | "license": "MIT", 494 | "engines": { 495 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 496 | }, 497 | "funding": { 498 | "url": "https://github.com/sponsors/sindresorhus" 499 | } 500 | }, 501 | "node_modules/mimic-response": { 502 | "version": "4.0.0", 503 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", 504 | "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", 505 | "license": "MIT", 506 | "engines": { 507 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 508 | }, 509 | "funding": { 510 | "url": "https://github.com/sponsors/sindresorhus" 511 | } 512 | }, 513 | "node_modules/normalize-url": { 514 | "version": "8.0.1", 515 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", 516 | "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", 517 | "license": "MIT", 518 | "engines": { 519 | "node": ">=14.16" 520 | }, 521 | "funding": { 522 | "url": "https://github.com/sponsors/sindresorhus" 523 | } 524 | }, 525 | "node_modules/npm-check-updates": { 526 | "version": "18.0.1", 527 | "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz", 528 | "integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==", 529 | "dev": true, 530 | "license": "Apache-2.0", 531 | "bin": { 532 | "ncu": "build/cli.js", 533 | "npm-check-updates": "build/cli.js" 534 | }, 535 | "engines": { 536 | "node": "^18.18.0 || >=20.0.0", 537 | "npm": ">=8.12.1" 538 | } 539 | }, 540 | "node_modules/open": { 541 | "version": "10.1.2", 542 | "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", 543 | "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", 544 | "license": "MIT", 545 | "dependencies": { 546 | "default-browser": "^5.2.1", 547 | "define-lazy-prop": "^3.0.0", 548 | "is-inside-container": "^1.0.0", 549 | "is-wsl": "^3.1.0" 550 | }, 551 | "engines": { 552 | "node": ">=18" 553 | }, 554 | "funding": { 555 | "url": "https://github.com/sponsors/sindresorhus" 556 | } 557 | }, 558 | "node_modules/p-cancelable": { 559 | "version": "4.0.1", 560 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", 561 | "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", 562 | "license": "MIT", 563 | "engines": { 564 | "node": ">=14.16" 565 | } 566 | }, 567 | "node_modules/p-whilst": { 568 | "version": "3.2.0", 569 | "resolved": "https://registry.npmjs.org/p-whilst/-/p-whilst-3.2.0.tgz", 570 | "integrity": "sha512-eqUz0XGVIwDDP2XZ2wg2nmi2a6pH3XJeub0z9l31OVnXCkyF+shVRe6qp8dJ85w7T5qJrfmIKAGbcdruDQQxMw==", 571 | "license": "MIT", 572 | "engines": { 573 | "node": ">=12" 574 | }, 575 | "funding": { 576 | "url": "https://github.com/sponsors/sindresorhus" 577 | } 578 | }, 579 | "node_modules/prettier": { 580 | "version": "3.5.3", 581 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 582 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 583 | "license": "MIT", 584 | "bin": { 585 | "prettier": "bin/prettier.cjs" 586 | }, 587 | "engines": { 588 | "node": ">=14" 589 | }, 590 | "funding": { 591 | "url": "https://github.com/prettier/prettier?sponsor=1" 592 | } 593 | }, 594 | "node_modules/prompts": { 595 | "version": "2.4.2", 596 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 597 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 598 | "license": "MIT", 599 | "dependencies": { 600 | "kleur": "^3.0.3", 601 | "sisteransi": "^1.0.5" 602 | }, 603 | "engines": { 604 | "node": ">= 6" 605 | } 606 | }, 607 | "node_modules/query-string": { 608 | "version": "9.1.2", 609 | "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", 610 | "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", 611 | "license": "MIT", 612 | "dependencies": { 613 | "decode-uri-component": "^0.4.1", 614 | "filter-obj": "^5.1.0", 615 | "split-on-first": "^3.0.0" 616 | }, 617 | "engines": { 618 | "node": ">=18" 619 | }, 620 | "funding": { 621 | "url": "https://github.com/sponsors/sindresorhus" 622 | } 623 | }, 624 | "node_modules/quick-lru": { 625 | "version": "5.1.1", 626 | "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", 627 | "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", 628 | "license": "MIT", 629 | "engines": { 630 | "node": ">=10" 631 | }, 632 | "funding": { 633 | "url": "https://github.com/sponsors/sindresorhus" 634 | } 635 | }, 636 | "node_modules/resolve-alpn": { 637 | "version": "1.2.1", 638 | "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", 639 | "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", 640 | "license": "MIT" 641 | }, 642 | "node_modules/responselike": { 643 | "version": "3.0.0", 644 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", 645 | "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", 646 | "license": "MIT", 647 | "dependencies": { 648 | "lowercase-keys": "^3.0.0" 649 | }, 650 | "engines": { 651 | "node": ">=14.16" 652 | }, 653 | "funding": { 654 | "url": "https://github.com/sponsors/sindresorhus" 655 | } 656 | }, 657 | "node_modules/run-applescript": { 658 | "version": "7.0.0", 659 | "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", 660 | "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", 661 | "license": "MIT", 662 | "engines": { 663 | "node": ">=18" 664 | }, 665 | "funding": { 666 | "url": "https://github.com/sponsors/sindresorhus" 667 | } 668 | }, 669 | "node_modules/sisteransi": { 670 | "version": "1.0.5", 671 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 672 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", 673 | "license": "MIT" 674 | }, 675 | "node_modules/split-on-first": { 676 | "version": "3.0.0", 677 | "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", 678 | "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", 679 | "license": "MIT", 680 | "engines": { 681 | "node": ">=12" 682 | }, 683 | "funding": { 684 | "url": "https://github.com/sponsors/sindresorhus" 685 | } 686 | }, 687 | "node_modules/type-fest": { 688 | "version": "4.41.0", 689 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", 690 | "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", 691 | "license": "(MIT OR CC0-1.0)", 692 | "engines": { 693 | "node": ">=16" 694 | }, 695 | "funding": { 696 | "url": "https://github.com/sponsors/sindresorhus" 697 | } 698 | }, 699 | "node_modules/typescript": { 700 | "version": "5.8.3", 701 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 702 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 703 | "dev": true, 704 | "license": "Apache-2.0", 705 | "bin": { 706 | "tsc": "bin/tsc", 707 | "tsserver": "bin/tsserver" 708 | }, 709 | "engines": { 710 | "node": ">=14.17" 711 | } 712 | }, 713 | "node_modules/undici-types": { 714 | "version": "6.21.0", 715 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 716 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 717 | "dev": true, 718 | "license": "MIT" 719 | }, 720 | "node_modules/universalify": { 721 | "version": "2.0.0", 722 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 723 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", 724 | "engines": { 725 | "node": ">= 10.0.0" 726 | } 727 | } 728 | }, 729 | "dependencies": { 730 | "@sec-ant/readable-stream": { 731 | "version": "0.4.1", 732 | "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", 733 | "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" 734 | }, 735 | "@sindresorhus/is": { 736 | "version": "7.0.1", 737 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz", 738 | "integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==" 739 | }, 740 | "@szmarczak/http-timer": { 741 | "version": "5.0.1", 742 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", 743 | "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", 744 | "requires": { 745 | "defer-to-connect": "^2.0.1" 746 | } 747 | }, 748 | "@types/fs-extra": { 749 | "version": "11.0.4", 750 | "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", 751 | "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", 752 | "dev": true, 753 | "requires": { 754 | "@types/jsonfile": "*", 755 | "@types/node": "*" 756 | } 757 | }, 758 | "@types/http-cache-semantics": { 759 | "version": "4.0.4", 760 | "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", 761 | "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" 762 | }, 763 | "@types/jsonfile": { 764 | "version": "6.1.4", 765 | "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", 766 | "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", 767 | "dev": true, 768 | "requires": { 769 | "@types/node": "*" 770 | } 771 | }, 772 | "@types/node": { 773 | "version": "22.15.17", 774 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", 775 | "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", 776 | "dev": true, 777 | "requires": { 778 | "undici-types": "~6.21.0" 779 | } 780 | }, 781 | "@types/prettier": { 782 | "version": "2.7.3", 783 | "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", 784 | "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", 785 | "dev": true 786 | }, 787 | "@types/prompts": { 788 | "version": "2.4.9", 789 | "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", 790 | "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", 791 | "dev": true, 792 | "requires": { 793 | "@types/node": "*", 794 | "kleur": "^3.0.3" 795 | } 796 | }, 797 | "bundle-name": { 798 | "version": "4.1.0", 799 | "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", 800 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 801 | "requires": { 802 | "run-applescript": "^7.0.0" 803 | } 804 | }, 805 | "cacheable-lookup": { 806 | "version": "7.0.0", 807 | "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", 808 | "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==" 809 | }, 810 | "cacheable-request": { 811 | "version": "12.0.1", 812 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", 813 | "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", 814 | "requires": { 815 | "@types/http-cache-semantics": "^4.0.4", 816 | "get-stream": "^9.0.1", 817 | "http-cache-semantics": "^4.1.1", 818 | "keyv": "^4.5.4", 819 | "mimic-response": "^4.0.0", 820 | "normalize-url": "^8.0.1", 821 | "responselike": "^3.0.0" 822 | } 823 | }, 824 | "chalk": { 825 | "version": "5.4.1", 826 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", 827 | "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" 828 | }, 829 | "commander": { 830 | "version": "13.1.0", 831 | "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", 832 | "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==" 833 | }, 834 | "decode-uri-component": { 835 | "version": "0.4.1", 836 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", 837 | "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==" 838 | }, 839 | "decompress-response": { 840 | "version": "6.0.0", 841 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 842 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 843 | "requires": { 844 | "mimic-response": "^3.1.0" 845 | }, 846 | "dependencies": { 847 | "mimic-response": { 848 | "version": "3.1.0", 849 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 850 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 851 | } 852 | } 853 | }, 854 | "default-browser": { 855 | "version": "5.2.1", 856 | "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", 857 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 858 | "requires": { 859 | "bundle-name": "^4.1.0", 860 | "default-browser-id": "^5.0.0" 861 | } 862 | }, 863 | "default-browser-id": { 864 | "version": "5.0.0", 865 | "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", 866 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==" 867 | }, 868 | "defer-to-connect": { 869 | "version": "2.0.1", 870 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", 871 | "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" 872 | }, 873 | "define-lazy-prop": { 874 | "version": "3.0.0", 875 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", 876 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" 877 | }, 878 | "filter-obj": { 879 | "version": "5.1.0", 880 | "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", 881 | "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==" 882 | }, 883 | "form-data-encoder": { 884 | "version": "4.0.2", 885 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", 886 | "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==" 887 | }, 888 | "fs-extra": { 889 | "version": "11.3.0", 890 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", 891 | "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", 892 | "requires": { 893 | "graceful-fs": "^4.2.0", 894 | "jsonfile": "^6.0.1", 895 | "universalify": "^2.0.0" 896 | } 897 | }, 898 | "get-stream": { 899 | "version": "9.0.1", 900 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", 901 | "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", 902 | "requires": { 903 | "@sec-ant/readable-stream": "^0.4.1", 904 | "is-stream": "^4.0.1" 905 | } 906 | }, 907 | "got": { 908 | "version": "14.4.7", 909 | "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", 910 | "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", 911 | "requires": { 912 | "@sindresorhus/is": "^7.0.1", 913 | "@szmarczak/http-timer": "^5.0.1", 914 | "cacheable-lookup": "^7.0.0", 915 | "cacheable-request": "^12.0.1", 916 | "decompress-response": "^6.0.0", 917 | "form-data-encoder": "^4.0.2", 918 | "http2-wrapper": "^2.2.1", 919 | "lowercase-keys": "^3.0.0", 920 | "p-cancelable": "^4.0.1", 921 | "responselike": "^3.0.0", 922 | "type-fest": "^4.26.1" 923 | } 924 | }, 925 | "graceful-fs": { 926 | "version": "4.2.10", 927 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", 928 | "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" 929 | }, 930 | "hasha": { 931 | "version": "6.0.0", 932 | "resolved": "https://registry.npmjs.org/hasha/-/hasha-6.0.0.tgz", 933 | "integrity": "sha512-MLydoyGp9QJcjlhE5lsLHXYpWayjjWqkavzju2ZWD2tYa1CgmML1K1gWAu22BLFa2eZ0OfvJ/DlfoVjaD54U2Q==", 934 | "requires": { 935 | "is-stream": "^3.0.0", 936 | "type-fest": "^4.7.1" 937 | }, 938 | "dependencies": { 939 | "is-stream": { 940 | "version": "3.0.0", 941 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", 942 | "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==" 943 | } 944 | } 945 | }, 946 | "http-cache-semantics": { 947 | "version": "4.2.0", 948 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", 949 | "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" 950 | }, 951 | "http2-wrapper": { 952 | "version": "2.2.1", 953 | "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", 954 | "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", 955 | "requires": { 956 | "quick-lru": "^5.1.1", 957 | "resolve-alpn": "^1.2.0" 958 | } 959 | }, 960 | "is-docker": { 961 | "version": "3.0.0", 962 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", 963 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==" 964 | }, 965 | "is-inside-container": { 966 | "version": "1.0.0", 967 | "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", 968 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 969 | "requires": { 970 | "is-docker": "^3.0.0" 971 | } 972 | }, 973 | "is-stream": { 974 | "version": "4.0.1", 975 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", 976 | "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==" 977 | }, 978 | "is-wsl": { 979 | "version": "3.1.0", 980 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", 981 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 982 | "requires": { 983 | "is-inside-container": "^1.0.0" 984 | } 985 | }, 986 | "json-buffer": { 987 | "version": "3.0.1", 988 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 989 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" 990 | }, 991 | "jsonfile": { 992 | "version": "6.1.0", 993 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 994 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 995 | "requires": { 996 | "graceful-fs": "^4.1.6", 997 | "universalify": "^2.0.0" 998 | } 999 | }, 1000 | "keyv": { 1001 | "version": "4.5.4", 1002 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 1003 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 1004 | "requires": { 1005 | "json-buffer": "3.0.1" 1006 | } 1007 | }, 1008 | "kleur": { 1009 | "version": "3.0.3", 1010 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 1011 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" 1012 | }, 1013 | "lowercase-keys": { 1014 | "version": "3.0.0", 1015 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", 1016 | "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" 1017 | }, 1018 | "mimic-response": { 1019 | "version": "4.0.0", 1020 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", 1021 | "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" 1022 | }, 1023 | "normalize-url": { 1024 | "version": "8.0.1", 1025 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", 1026 | "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==" 1027 | }, 1028 | "npm-check-updates": { 1029 | "version": "18.0.1", 1030 | "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.1.tgz", 1031 | "integrity": "sha512-MO7mLp/8nm6kZNLLyPgz4gHmr9tLoU+pWPLdXuGAx+oZydBHkHWN0ibTonsrfwC2WEQNIQxuZagYwB67JQpAuw==", 1032 | "dev": true 1033 | }, 1034 | "open": { 1035 | "version": "10.1.2", 1036 | "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", 1037 | "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", 1038 | "requires": { 1039 | "default-browser": "^5.2.1", 1040 | "define-lazy-prop": "^3.0.0", 1041 | "is-inside-container": "^1.0.0", 1042 | "is-wsl": "^3.1.0" 1043 | } 1044 | }, 1045 | "p-cancelable": { 1046 | "version": "4.0.1", 1047 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", 1048 | "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==" 1049 | }, 1050 | "p-whilst": { 1051 | "version": "3.2.0", 1052 | "resolved": "https://registry.npmjs.org/p-whilst/-/p-whilst-3.2.0.tgz", 1053 | "integrity": "sha512-eqUz0XGVIwDDP2XZ2wg2nmi2a6pH3XJeub0z9l31OVnXCkyF+shVRe6qp8dJ85w7T5qJrfmIKAGbcdruDQQxMw==" 1054 | }, 1055 | "prettier": { 1056 | "version": "3.5.3", 1057 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 1058 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==" 1059 | }, 1060 | "prompts": { 1061 | "version": "2.4.2", 1062 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 1063 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 1064 | "requires": { 1065 | "kleur": "^3.0.3", 1066 | "sisteransi": "^1.0.5" 1067 | } 1068 | }, 1069 | "query-string": { 1070 | "version": "9.1.2", 1071 | "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", 1072 | "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", 1073 | "requires": { 1074 | "decode-uri-component": "^0.4.1", 1075 | "filter-obj": "^5.1.0", 1076 | "split-on-first": "^3.0.0" 1077 | } 1078 | }, 1079 | "quick-lru": { 1080 | "version": "5.1.1", 1081 | "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", 1082 | "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" 1083 | }, 1084 | "resolve-alpn": { 1085 | "version": "1.2.1", 1086 | "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", 1087 | "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" 1088 | }, 1089 | "responselike": { 1090 | "version": "3.0.0", 1091 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", 1092 | "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", 1093 | "requires": { 1094 | "lowercase-keys": "^3.0.0" 1095 | } 1096 | }, 1097 | "run-applescript": { 1098 | "version": "7.0.0", 1099 | "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", 1100 | "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==" 1101 | }, 1102 | "sisteransi": { 1103 | "version": "1.0.5", 1104 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 1105 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 1106 | }, 1107 | "split-on-first": { 1108 | "version": "3.0.0", 1109 | "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", 1110 | "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==" 1111 | }, 1112 | "type-fest": { 1113 | "version": "4.41.0", 1114 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", 1115 | "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" 1116 | }, 1117 | "typescript": { 1118 | "version": "5.8.3", 1119 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1120 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1121 | "dev": true 1122 | }, 1123 | "undici-types": { 1124 | "version": "6.21.0", 1125 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 1126 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 1127 | "dev": true 1128 | }, 1129 | "universalify": { 1130 | "version": "2.0.0", 1131 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 1132 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 1133 | } 1134 | } 1135 | } 1136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tube-manager", 3 | "version": "0.7.0", 4 | "description": "Manage YouTube videos using command line.", 5 | "engines": { 6 | "node": ">=18" 7 | }, 8 | "bin": { 9 | "tube-manager": "./bin/tube-manager.js" 10 | }, 11 | "type": "module", 12 | "exports": "./build/index.js", 13 | "files": [ 14 | "bin/", 15 | "build/", 16 | "package.json", 17 | "schemas/", 18 | "LICENCE", 19 | "README.md" 20 | ], 21 | "dependencies": { 22 | "chalk": "^5.4.1", 23 | "commander": "^13.1.0", 24 | "fs-extra": "^11.3.0", 25 | "got": "^14.4.7", 26 | "hasha": "^6.0.0", 27 | "open": "^10.1.2", 28 | "p-whilst": "^3.2.0", 29 | "prettier": "^3.5.3", 30 | "prompts": "^2.4.2", 31 | "query-string": "^9.1.2" 32 | }, 33 | "scripts": { 34 | "code": "tsc -w", 35 | "build": "rm -fr build/*; tsc", 36 | "prepublishOnly": "npm run build", 37 | "ncu": "ncu --target minor --upgrade" 38 | }, 39 | "author": "Sun Knudsen ", 40 | "license": "MIT", 41 | "keywords": [ 42 | "cli", 43 | "youtube" 44 | ], 45 | "devDependencies": { 46 | "@types/fs-extra": "^11.0.4", 47 | "@types/node": "^22.15.17", 48 | "@types/prettier": "^2.7.3", 49 | "@types/prompts": "^2.4.9", 50 | "npm-check-updates": "^18.0.1", 51 | "typescript": "^5.8.3" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/sunknudsen/tube-manager.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/sunknudsen/tube-manager/issues" 59 | }, 60 | "homepage": "https://github.com/sunknudsen/tube-manager#readme", 61 | "prettier": { 62 | "endOfLine": "lf", 63 | "printWidth": 80, 64 | "semi": false, 65 | "tabWidth": 2, 66 | "trailingComma": "es5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "youtube": { 4 | "oauth2PrefixUrl": "https://accounts.google.com/o/oauth2", 5 | "apiPrefixUrl": "https://www.googleapis.com/youtube/v3", 6 | "clientId": "", 7 | "clientSecret": "", 8 | "accessToken": "", 9 | "refreshToken": "", 10 | "channelId": "", 11 | "channelWatchUrl": "https://www.youtube.com/watch?v=" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/tube-manager.json: -------------------------------------------------------------------------------- 1 | { 2 | "headings": { 3 | "sections": "TL;DR", 4 | "suggestedVideos": "Suggested", 5 | "links": "Links", 6 | "credits": "Credits", 7 | "affiliateLinks": "Affiliate links", 8 | "footnotes": "Change log", 9 | "support": "Support" 10 | }, 11 | "separator": "👉", 12 | "affiliateLinks": { 13 | "amazon": { 14 | "brotherPrinter": { 15 | "label": "Brother HL-L2460DW printer", 16 | "url": [ 17 | { 18 | "label": "USA", 19 | "url": "https://www.amazon.com/dp/B0CPL2N5H6?tag=sunknudsen06-20" 20 | } 21 | ] 22 | } 23 | } 24 | }, 25 | "videos": [ 26 | { 27 | "id": "YrUSJQq8WOA", 28 | "publishedAt": "2022-11-03T14:43:45Z", 29 | "title": "macOS stores a copy of everything one prints forever", 30 | "description": "In this episode, we explore how macOS stores a copy of everything one prints forever.", 31 | "tags": ["Privacy", "Security", "macOS"], 32 | "categoryId": "27", 33 | "sections": [], 34 | "suggestedVideos": [], 35 | "links": [ 36 | { 37 | "label": "How to disable CUPS pinter job history on macOS", 38 | "url": "https://sunknudsen.com/guides/how-to-disable-cups-printer-job-history-on-macos" 39 | }, 40 | { 41 | "label": "Twitter (please follow @superbacked)", 42 | "url": "https://twitter.com/superbacked" 43 | }, 44 | { 45 | "label": "Superbacked (join waiting list)", 46 | "url": "https://superbacked.com/" 47 | } 48 | ], 49 | "credits": [], 50 | "affiliateLinks": ["amazon.brotherPrinter"], 51 | "footnotes": [], 52 | "support": [ 53 | { 54 | "label": "Support my work", 55 | "url": "https://sunknudsen.com/donate" 56 | } 57 | ], 58 | "thumbnailHash": "65c65f74ac181dcffc994d9f75ed88392bdcc434165bc59dda2a0553a0725ac6" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /samples/tube-manager/YrUSJQq8WOA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunknudsen/tube-manager/f946326be15ee9bc45f10384ada28ee75db4db9d/samples/tube-manager/YrUSJQq8WOA.jpg -------------------------------------------------------------------------------- /schemas/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema#", 3 | "$id": "https://raw.githubusercontent.com/sunknudsen/tube-manager/master/schemas/tube-manager.schema.json", 4 | "title": "tube-manager config", 5 | "description": "Schema for tube-manager config", 6 | "type": "object", 7 | "definitions": { 8 | "profile": { 9 | "description": "Profile", 10 | "type": "object", 11 | "properties": { 12 | "youtube": { 13 | "description": "YouTube", 14 | "type": "object", 15 | "properties": { 16 | "oauth2PrefixUrl": { 17 | "description": "OAuth 2 prefix URL", 18 | "type": "string", 19 | "default": "https://accounts.google.com/o/oauth2" 20 | }, 21 | "apiPrefixUrl": { 22 | "description": "API prefix URL", 23 | "type": "string", 24 | "default": "https://www.googleapis.com/youtube/v3" 25 | }, 26 | "clientId": { 27 | "description": "Client ID", 28 | "type": "string" 29 | }, 30 | "clientSecret": { 31 | "description": "Client secret", 32 | "type": "string" 33 | }, 34 | "accessToken": { 35 | "description": "Access token", 36 | "type": "string" 37 | }, 38 | "refreshToken": { 39 | "description": "Refresh token", 40 | "type": "string" 41 | }, 42 | "channelId": { 43 | "description": "Channel ID", 44 | "type": "string" 45 | }, 46 | "channelWatchUrl": { 47 | "description": "Channel watch URL", 48 | "type": "string", 49 | "default": "https://www.youtube.com/watch?v=" 50 | } 51 | }, 52 | "required": [ 53 | "oauth2PrefixUrl", 54 | "apiPrefixUrl", 55 | "clientId", 56 | "clientSecret", 57 | "accessToken", 58 | "refreshToken", 59 | "channelId", 60 | "channelWatchUrl" 61 | ], 62 | "additionalProperties": false 63 | } 64 | }, 65 | "required": ["youtube"], 66 | "additionalProperties": false 67 | } 68 | }, 69 | "patternProperties": { 70 | "^default$": { 71 | "description": "Profile", 72 | "$ref": "#/definitions/profile" 73 | }, 74 | "^.*$": { 75 | "description": "Profile", 76 | "$ref": "#/definitions/profile" 77 | } 78 | }, 79 | "required": ["default"], 80 | "additionalProperties": false 81 | } 82 | -------------------------------------------------------------------------------- /schemas/tube-manager.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema#", 3 | "$id": "https://raw.githubusercontent.com/sunknudsen/tube-manager/master/schemas/tube-manager.schema.json", 4 | "title": "tube-manager dataset", 5 | "description": "Schema for tube-manager dataset", 6 | "type": "object", 7 | "definitions": { 8 | "labelAndUrl": { 9 | "type": "object", 10 | "properties": { 11 | "label": { 12 | "description": "Label", 13 | "type": "string" 14 | }, 15 | "url": { 16 | "description": "URL", 17 | "type": "string" 18 | } 19 | }, 20 | "required": ["label", "url"], 21 | "additionalProperties": false 22 | }, 23 | "affiliateProduct": { 24 | "type": "object", 25 | "properties": { 26 | "label": { 27 | "description": "Label", 28 | "type": "string" 29 | }, 30 | "url": { 31 | "anyOf": [ 32 | { 33 | "description": "URL", 34 | "type": "string" 35 | }, 36 | { 37 | "description": "URL list", 38 | "type": "array", 39 | "items": { 40 | "type": "object", 41 | "properties": { 42 | "label": { 43 | "description": "Label", 44 | "type": "string" 45 | }, 46 | "url": { 47 | "description": "URL", 48 | "type": "string" 49 | } 50 | } 51 | }, 52 | "uniqueItems": true 53 | } 54 | ] 55 | } 56 | }, 57 | "required": ["label", "url"], 58 | "additionalProperties": false 59 | } 60 | }, 61 | "properties": { 62 | "headings": { 63 | "description": "Heading titles", 64 | "type": "object", 65 | "properties": { 66 | "sections": { 67 | "description": "Sections title", 68 | "type": "string" 69 | }, 70 | "suggestedVideos": { 71 | "description": "Suggested videos title", 72 | "type": "string" 73 | }, 74 | "links": { 75 | "description": "Links title", 76 | "type": "string" 77 | }, 78 | "credits": { 79 | "description": "Credits title", 80 | "type": "string" 81 | }, 82 | "affiliateLinks": { 83 | "description": "Affiliate links title", 84 | "type": "string" 85 | }, 86 | "footnotes": { 87 | "description": "Footnotes title", 88 | "type": "string" 89 | }, 90 | "support": { 91 | "description": "Support title", 92 | "type": "string" 93 | } 94 | }, 95 | "required": [ 96 | "sections", 97 | "suggestedVideos", 98 | "links", 99 | "credits", 100 | "affiliateLinks", 101 | "footnotes" 102 | ], 103 | "additionalProperties": false 104 | }, 105 | "separator": { 106 | "description": "Heading separator", 107 | "type": "string" 108 | }, 109 | "affiliateLinks": { 110 | "description": "Affiliate links", 111 | "type": "object", 112 | "patternProperties": { 113 | "^.*$": { 114 | "description": "Platform", 115 | "type": "object", 116 | "patternProperties": { 117 | "^.*$": { 118 | "description": "Product", 119 | "$ref": "#/definitions/affiliateProduct" 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "videos": { 126 | "description": "Videos", 127 | "type": "array", 128 | "items": { 129 | "type": "object", 130 | "properties": { 131 | "id": { 132 | "description": "ID", 133 | "type": "string" 134 | }, 135 | "publishedAt": { 136 | "description": "Published at", 137 | "type": "string" 138 | }, 139 | "title": { 140 | "description": "Title", 141 | "type": "string" 142 | }, 143 | "description": { 144 | "description": "Description", 145 | "type": "string" 146 | }, 147 | "tags": { 148 | "description": "Tags", 149 | "type": "array", 150 | "items": { 151 | "description": "Tag", 152 | "type": "string" 153 | }, 154 | "uniqueItems": true 155 | }, 156 | "categoryId": { 157 | "description": "Category ID", 158 | "type": "string" 159 | }, 160 | "sections": { 161 | "description": "Sections", 162 | "type": "array", 163 | "items": { 164 | "description": "Section", 165 | "type": "object", 166 | "properties": { 167 | "timestamp": { 168 | "description": "Timestamp", 169 | "type": "string" 170 | }, 171 | "label": { 172 | "description": "Label", 173 | "type": "string" 174 | } 175 | } 176 | } 177 | }, 178 | "suggestedVideos": { 179 | "description": "Suggested videos", 180 | "type": "array", 181 | "items": { 182 | "anyOf": [ 183 | { 184 | "description": "Suggested video", 185 | "type": "string" 186 | }, 187 | { 188 | "description": "Suggested video with label", 189 | "$ref": "#/definitions/labelAndUrl" 190 | } 191 | ] 192 | } 193 | }, 194 | "links": { 195 | "description": "Links", 196 | "type": "array", 197 | "items": { 198 | "anyOf": [ 199 | { 200 | "description": "Link", 201 | "type": "string" 202 | }, 203 | { 204 | "description": "Link with label", 205 | "$ref": "#/definitions/labelAndUrl" 206 | } 207 | ] 208 | } 209 | }, 210 | "credits": { 211 | "description": "Credits", 212 | "type": "array", 213 | "items": { 214 | "anyOf": [ 215 | { 216 | "description": "Credit", 217 | "type": "string" 218 | }, 219 | { 220 | "description": "Credit with label", 221 | "$ref": "#/definitions/labelAndUrl" 222 | } 223 | ] 224 | } 225 | }, 226 | "affiliateLinks": { 227 | "description": "Affiliate links", 228 | "type": "array", 229 | "items": { 230 | "anyOf": [ 231 | { 232 | "description": "Affiliate link", 233 | "type": "string" 234 | }, 235 | { 236 | "description": "Affiliate product", 237 | "$ref": "#/definitions/affiliateProduct" 238 | } 239 | ] 240 | } 241 | }, 242 | "footnotes": { 243 | "description": "Footnotes", 244 | "type": "array", 245 | "items": { 246 | "anyOf": [ 247 | { 248 | "description": "Footnote", 249 | "type": "string" 250 | }, 251 | { 252 | "description": "Footnote with type", 253 | "type": "object", 254 | "properties": { 255 | "type": { 256 | "description": "Type", 257 | "type": "string", 258 | "enum": ["", "warning"] 259 | }, 260 | "timestamp": { 261 | "description": "Timestamp", 262 | "type": "string" 263 | }, 264 | "message": { 265 | "description": "Message", 266 | "type": "string" 267 | } 268 | }, 269 | "required": ["type", "timestamp", "message"], 270 | "additionalProperties": false 271 | } 272 | ] 273 | } 274 | }, 275 | "support": { 276 | "description": "Support", 277 | "type": "array", 278 | "items": { 279 | "anyOf": [ 280 | { 281 | "description": "Channel", 282 | "type": "string" 283 | }, 284 | { 285 | "description": "Channel with label", 286 | "$ref": "#/definitions/labelAndUrl" 287 | } 288 | ] 289 | } 290 | }, 291 | "thumbnailHash": { 292 | "description": "Thumbnail hash", 293 | "type": "string" 294 | } 295 | }, 296 | "required": [ 297 | "id", 298 | "publishedAt", 299 | "title", 300 | "description", 301 | "tags", 302 | "categoryId", 303 | "sections", 304 | "suggestedVideos", 305 | "links", 306 | "credits", 307 | "affiliateLinks", 308 | "footnotes" 309 | ], 310 | "additionalProperties": false 311 | } 312 | } 313 | }, 314 | "required": ["headings", "separator", "affiliateLinks", "videos"], 315 | "additionalProperties": false 316 | } 317 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra" 2 | import prettier from "prettier" 3 | 4 | const { readFile, writeFile } = fsExtra 5 | 6 | interface Props { 7 | youtube: { 8 | oauth2PrefixUrl: string 9 | apiPrefixUrl: string 10 | clientId: string 11 | clientSecret: string 12 | accessToken: string 13 | refreshToken: string 14 | channelId: string 15 | channelWatchUrl: string 16 | } 17 | } 18 | 19 | interface Profiles { 20 | [profile: string]: Props 21 | } 22 | 23 | type DeepPartial = { 24 | [P in keyof T]?: DeepPartial 25 | } 26 | 27 | export default class Config { 28 | readonly path: string 29 | readonly profile: string 30 | private profiles: Profiles 31 | public props: Props 32 | constructor(path: string, profile: string) { 33 | this.path = path 34 | this.profile = profile 35 | } 36 | async load() { 37 | const json = await readFile(this.path, "utf8") 38 | this.profiles = JSON.parse(json) 39 | this.props = this.profiles[this.profile] 40 | } 41 | set(props: DeepPartial) { 42 | // This doesn’t support nested platform properties 43 | Object.keys(this.props).forEach((platform: keyof Props) => { 44 | const platformProps = this.props[platform] 45 | if (props[platform]) { 46 | Object.assign(platformProps, props[platform]) 47 | } 48 | }) 49 | return this 50 | } 51 | async save() { 52 | const data = await prettier.format(JSON.stringify(this.profiles, null, 2), { 53 | parser: "json", 54 | }) 55 | await writeFile(this.path, data) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { program } from "commander" 3 | import fsExtra from "fs-extra" 4 | import { HTTPError } from "got" 5 | import { hashFile } from "hasha" 6 | import { homedir } from "os" 7 | import pWhilst from "p-whilst" 8 | import { join, resolve } from "path" 9 | import prettier from "prettier" 10 | import prompts from "prompts" 11 | import { inspect } from "util" 12 | import Config from "./config.js" 13 | import YouTube from "./youtube.js" 14 | 15 | const { existsSync, readFile, writeFile, emptyDir, ensureDir } = fsExtra 16 | 17 | const logError = (error: Error | HTTPError) => { 18 | if (error instanceof HTTPError) { 19 | const url = error.response.request.options.url 20 | console.error( 21 | inspect( 22 | { 23 | request: { 24 | method: error.response.request.options.method, 25 | url: typeof url === "object" ? url.href : url, 26 | headers: error.response.request.options.headers, 27 | json: error.response.request.options.json, 28 | body: error.response.request.options.body, 29 | }, 30 | response: { 31 | statusCode: error.response.statusCode, 32 | body: error.response.body, 33 | }, 34 | }, 35 | false, 36 | 10, 37 | true 38 | ) 39 | ) 40 | } else { 41 | console.error(error) 42 | } 43 | } 44 | 45 | type Section = { 46 | label: string 47 | timestamp: string 48 | } 49 | 50 | type Snippet = 51 | | string 52 | | { 53 | label: string 54 | url: string 55 | } 56 | 57 | type AffiliateSnippet = 58 | | Snippet 59 | | { 60 | label: string 61 | url: Snippet[] 62 | } 63 | 64 | type Footnote = 65 | | string 66 | | { 67 | type: string 68 | timestamp: string 69 | message: string 70 | } 71 | 72 | export interface Video { 73 | id: string 74 | publishedAt: string 75 | title: string 76 | description: string 77 | tags: string[] 78 | categoryId: string 79 | sections: Section[] 80 | suggestedVideos: Snippet[] 81 | links: Snippet[] 82 | credits: Snippet[] 83 | affiliateLinks: AffiliateSnippet[] 84 | footnotes: Footnote[] 85 | support: Snippet[] 86 | thumbnailHash?: string 87 | } 88 | 89 | export interface Dataset { 90 | headings: { 91 | sections: string 92 | suggestedVideos: string 93 | links: string 94 | credits: string 95 | affiliateLinks: string 96 | footnotes: string 97 | support: string 98 | } 99 | separator: string 100 | affiliateLinks: { 101 | [key: string]: { 102 | [key: string]: AffiliateSnippet 103 | } 104 | } 105 | videos: Video[] 106 | } 107 | 108 | program 109 | .command("refresh-token ") 110 | .description("get refresh token") 111 | .option( 112 | "--config ", 113 | "/path/to/config.json", 114 | resolve(homedir(), ".tube-manager/config.json") 115 | ) 116 | .option("--profile ", "configuration profile", "default") 117 | .action(async (platform, command) => { 118 | try { 119 | if (!["youtube"].includes(platform)) { 120 | throw new Error(`Invalid platform "${platform}"`) 121 | } 122 | const config = new Config(command.config, command.profile) 123 | await config.load() 124 | const youtube = new YouTube(config) 125 | if (platform === "youtube") { 126 | let refreshToken = await youtube.getRefreshToken() 127 | console.info(refreshToken) 128 | } 129 | } catch (error) { 130 | logError(error) 131 | } 132 | }) 133 | 134 | program 135 | .command("channels ") 136 | .description("get channels") 137 | .option( 138 | "--config ", 139 | "/path/to/config.json", 140 | resolve(homedir(), ".tube-manager/config.json") 141 | ) 142 | .option("--profile ", "configuration profile", "default") 143 | .action(async (platform, command) => { 144 | try { 145 | if (!["youtube"].includes(platform)) { 146 | throw new Error(`Invalid platform "${platform}"`) 147 | } 148 | const config = new Config(command.config, command.profile) 149 | await config.load() 150 | const youtube = new YouTube(config) 151 | if (platform === "youtube") { 152 | // See https://developers.google.com/youtube/v3/docs/channels/list 153 | const channelsResponse: any = await youtube.got.get("channels", { 154 | searchParams: { 155 | part: "id,snippet", 156 | mine: true, 157 | }, 158 | }) 159 | let channels: any[] = [] 160 | channelsResponse.body.items.forEach((item: any) => { 161 | channels.push({ 162 | id: item.id, 163 | title: item.snippet.title, 164 | description: item.snippet.description, 165 | }) 166 | }) 167 | console.info(channels) 168 | } 169 | } catch (error) { 170 | logError(error) 171 | } 172 | }) 173 | 174 | program 175 | .command("stats ") 176 | .description("get stats") 177 | .option( 178 | "--config ", 179 | "/path/to/config.json", 180 | resolve(homedir(), ".tube-manager/config.json") 181 | ) 182 | .option("--profile ", "configuration profile", "default") 183 | .action(async (platform, command) => { 184 | try { 185 | if (!["youtube"].includes(platform)) { 186 | throw new Error(`Invalid platform "${platform}"`) 187 | } 188 | const config = new Config(command.config, command.profile) 189 | await config.load() 190 | const youtube = new YouTube(config) 191 | if (platform === "youtube") { 192 | // See https://developers.google.com/youtube/v3/docs/channels/list 193 | const channelsResponse: any = await youtube.got.get("channels", { 194 | searchParams: { 195 | id: config.props.youtube.channelId, 196 | part: "statistics", 197 | }, 198 | }) 199 | if (channelsResponse.body.items.length !== 1) { 200 | throw new Error( 201 | `Could not find YouTube channel "${config.props.youtube.channelId}"` 202 | ) 203 | } 204 | const statistics = channelsResponse.body.items[0].statistics 205 | console.info({ 206 | viewCount: statistics.viewCount, 207 | subscriberCount: statistics.subscriberCount, 208 | }) 209 | } 210 | } catch (error) { 211 | logError(error) 212 | } 213 | }) 214 | 215 | program 216 | .command("video ") 217 | .description("get video") 218 | .option( 219 | "--config ", 220 | "/path/to/config.json", 221 | resolve(homedir(), ".tube-manager/config.json") 222 | ) 223 | .option("--profile ", "configuration profile", "default") 224 | .action(async (platform, id, command) => { 225 | try { 226 | if (!["youtube"].includes(platform)) { 227 | throw new Error(`Invalid platform "${platform}"`) 228 | } 229 | const config = new Config(command.config, command.profile) 230 | await config.load() 231 | const youtube = new YouTube(config) 232 | if (platform === "youtube") { 233 | // See https://developers.google.com/youtube/v3/docs/videos/list 234 | const videosResponse: any = await youtube.got.get("videos", { 235 | searchParams: { 236 | id: id, 237 | part: "id,snippet,processingDetails", 238 | }, 239 | }) 240 | if (videosResponse.body.items.length !== 1) { 241 | throw new Error("Could not find video") 242 | } 243 | console.info(inspect(videosResponse.body.items[0], false, 3, true)) 244 | } 245 | } catch (error) { 246 | logError(error) 247 | } 248 | }) 249 | 250 | program 251 | .command("initialize") 252 | .description("initialize dataset") 253 | .option( 254 | "--config ", 255 | "/path/to/config.json", 256 | resolve(homedir(), ".tube-manager/config.json") 257 | ) 258 | .option("--profile ", "configuration profile", "default") 259 | .option("--include ", "include videos IDs") 260 | .option("--exclude ", "exclude videos IDs") 261 | .option( 262 | "--dataset ", 263 | "/path/to/dataset.json", 264 | resolve(process.cwd(), "tube-manager.json") 265 | ) 266 | .option( 267 | "--thumbnail-dir ", 268 | "/path/to/tube-manager", 269 | resolve(process.cwd(), "tube-manager") 270 | ) 271 | .action(async (command) => { 272 | try { 273 | const config = new Config(command.config, command.profile) 274 | await config.load() 275 | const youtube = new YouTube(config) 276 | if (existsSync(command.dataset)) { 277 | const { confirmation } = await prompts({ 278 | message: `Do you wish to override ${chalk.bold(command.dataset)}?`, 279 | name: "confirmation", 280 | type: "confirm", 281 | }) 282 | if (confirmation !== true) { 283 | console.info(chalk.red("Cancelled")) 284 | process.exit(0) 285 | } 286 | } 287 | if (existsSync(command.thumbnailDir)) { 288 | const { confirmation } = await prompts({ 289 | message: `Do you wish to purge ${chalk.bold(command.thumbnailDir)}?`, 290 | name: "confirmation", 291 | type: "confirm", 292 | }) 293 | if (confirmation !== true) { 294 | console.info(chalk.red("Cancelled")) 295 | process.exit(0) 296 | } 297 | await emptyDir(command.thumbnailDir) 298 | } else { 299 | await ensureDir(command.thumbnailDir) 300 | } 301 | let ids: string[] 302 | if (command.include) { 303 | ids = command.include.replace(/ /g, "").split(",") 304 | } else { 305 | ids = [] 306 | // See https://developers.google.com/youtube/v3/docs/channels/list 307 | const channelsResponse: any = await youtube.got.get("channels", { 308 | searchParams: { 309 | id: config.props.youtube.channelId, 310 | part: "contentDetails", 311 | }, 312 | }) 313 | if (channelsResponse.body.items.length !== 1) { 314 | throw new Error( 315 | `Could not find YouTube channel "${config.props.youtube.channelId}"` 316 | ) 317 | } 318 | const playlistId = 319 | channelsResponse.body.items[0].contentDetails.relatedPlaylists.uploads 320 | let items: any[] = [] 321 | let pageToken: null | string = null 322 | await pWhilst( 323 | () => pageToken !== undefined, 324 | async () => { 325 | // See https://developers.google.com/youtube/v3/docs/playlistItems/list 326 | const playlistItemsResponse: any = await youtube.got.get( 327 | "playlistItems", 328 | { 329 | searchParams: { 330 | playlistId: playlistId, 331 | part: "id,snippet", 332 | maxResults: "50", 333 | pageToken: pageToken, 334 | }, 335 | } 336 | ) 337 | pageToken = playlistItemsResponse.body.nextPageToken 338 | playlistItemsResponse.body.items.forEach((item: any) => { 339 | items.push(item) 340 | }) 341 | } 342 | ) 343 | items.forEach((item) => { 344 | if (command.exclude) { 345 | const excluded = command.exclude.replace(/ /g, "").split(",") 346 | if (excluded.indexOf(item.snippet.resourceId.videoId) === -1) { 347 | ids.push(item.snippet.resourceId.videoId) 348 | } 349 | } else { 350 | ids.push(item.snippet.resourceId.videoId) 351 | } 352 | }) 353 | } 354 | let youtubeVideos: any[] = [] 355 | const chunk = 50 356 | for (let index = 0; index < ids.length; index += chunk) { 357 | let slice = ids.slice(index, index + chunk) 358 | // See https://developers.google.com/youtube/v3/docs/videos/list 359 | let videosResponse: any = await youtube.got.get("videos", { 360 | searchParams: { 361 | id: slice.join(","), 362 | part: "id,snippet", 363 | }, 364 | }) 365 | youtubeVideos = youtubeVideos.concat(videosResponse.body.items) 366 | } 367 | let headings: Dataset["headings"] = { 368 | sections: "Sections", 369 | suggestedVideos: "Suggested", 370 | links: "Links", 371 | credits: "Credits", 372 | affiliateLinks: "Affiliate links", 373 | footnotes: "Footnotes", 374 | support: "Support", 375 | } 376 | let dataset: any = { 377 | headings: headings, 378 | separator: "👉", 379 | affiliateLinks: { 380 | amazon: {}, 381 | }, 382 | videos: [], 383 | } 384 | for (const youtubeVideo of youtubeVideos) { 385 | let video: Video = { 386 | id: youtubeVideo.id, 387 | publishedAt: youtubeVideo.snippet.publishedAt, 388 | title: youtubeVideo.snippet.title, 389 | description: youtubeVideo.snippet.description ?? "", 390 | tags: youtubeVideo.snippet.tags ?? [], 391 | categoryId: youtubeVideo.snippet.categoryId, 392 | sections: [], 393 | suggestedVideos: [], 394 | links: [], 395 | credits: [], 396 | affiliateLinks: [], 397 | footnotes: [], 398 | support: [], 399 | } 400 | dataset.videos.push(video) 401 | } 402 | const data = await prettier.format(JSON.stringify(dataset, null, 2), { 403 | parser: "json", 404 | }) 405 | await writeFile(command.dataset, data) 406 | console.info( 407 | chalk.green( 408 | `Imported ${chalk.bold(dataset.videos.length)} videos to ${chalk.bold( 409 | command.dataset 410 | )}` 411 | ) 412 | ) 413 | } catch (error) { 414 | logError(error) 415 | } 416 | }) 417 | 418 | program 419 | .command("import ") 420 | .description("import video") 421 | .option( 422 | "--config ", 423 | "/path/to/config.json", 424 | resolve(homedir(), ".tube-manager/config.json") 425 | ) 426 | .option("--profile ", "configuration profile", "default") 427 | .option( 428 | "--dataset ", 429 | "/path/to/dataset.json", 430 | resolve(process.cwd(), "tube-manager.json") 431 | ) 432 | .action(async (id, command) => { 433 | try { 434 | const config = new Config(command.config, command.profile) 435 | await config.load() 436 | const youtube = new YouTube(config) 437 | const json = await readFile(command.dataset, "utf8") 438 | const dataset: Dataset = JSON.parse(json) 439 | if (getYouTubeVideo(dataset, id)) { 440 | throw new Error("Video already in dataset") 441 | } 442 | // See https://developers.google.com/youtube/v3/docs/videos/list 443 | const videosResponse: any = await youtube.got.get("videos", { 444 | searchParams: { 445 | id: id, 446 | part: "id,snippet", 447 | }, 448 | }) 449 | if (videosResponse.body.items.length !== 1) { 450 | throw new Error("Could not find video") 451 | } 452 | const video = videosResponse.body.items[0] 453 | dataset.videos.push({ 454 | id: video.id, 455 | publishedAt: video.snippet.publishedAt, 456 | title: video.snippet.title, 457 | description: video.snippet.description, 458 | tags: video.snippet.tags, 459 | categoryId: video.snippet.categoryId, 460 | sections: [], 461 | suggestedVideos: [], 462 | links: [], 463 | credits: [], 464 | affiliateLinks: [], 465 | footnotes: [], 466 | support: [], 467 | }) 468 | dataset.videos.sort((a, b) => { 469 | const dateA = new Date(a.publishedAt) 470 | const dateB = new Date(b.publishedAt) 471 | if (dateA > dateB) { 472 | return -1 473 | } 474 | if (dateA < dateB) { 475 | return 1 476 | } 477 | return 0 478 | }) 479 | const data = await prettier.format(JSON.stringify(dataset, null, 2), { 480 | parser: "json", 481 | }) 482 | await writeFile(command.dataset, data) 483 | console.info( 484 | chalk.green(`Imported video to ${chalk.bold(command.dataset)}`) 485 | ) 486 | } catch (error) { 487 | logError(error) 488 | } 489 | }) 490 | 491 | const getYouTubeVideo = (dataset: Dataset, id: string): Video => { 492 | for (const video of dataset.videos) { 493 | if (video.id === id) { 494 | return video 495 | } 496 | } 497 | } 498 | 499 | const heading = (value: string) => { 500 | return `\n\n==============================\n${value.toUpperCase()}\n==============================` 501 | } 502 | 503 | const support = (dataset: Dataset, video: Video) => { 504 | let content = "" 505 | video.support.forEach((option) => { 506 | if (typeof option === "string") { 507 | content += `\n${option}` 508 | } else { 509 | content += `\n${option.label} ${dataset.separator} ${option.url}` 510 | } 511 | }) 512 | return content 513 | } 514 | 515 | const description = ( 516 | config: Config, 517 | dataset: Dataset, 518 | platform: string, 519 | video: Video 520 | ) => { 521 | let content = video.description 522 | if (video.sections.length > 0) { 523 | content += heading(dataset.headings.sections) 524 | video.sections.forEach((section) => { 525 | content += `\n${section.timestamp} ${section.label}` 526 | }) 527 | } 528 | if (video.suggestedVideos.length > 0) { 529 | content += heading(dataset.headings.suggestedVideos) 530 | video.suggestedVideos.forEach((suggestedVideo) => { 531 | if ( 532 | typeof suggestedVideo === "string" && 533 | suggestedVideo.match(/^video\.[a-zA-Z0-9_\-]{11}/) 534 | ) { 535 | const suggestedVideoId = suggestedVideo.split(".")[1] 536 | const suggestedVideoAttributes = getYouTubeVideo( 537 | dataset, 538 | suggestedVideoId 539 | ) 540 | if (!suggestedVideoAttributes) { 541 | throw new Error(`Could not find suggested video "${suggestedVideo}"`) 542 | } 543 | if (platform === "youtube") { 544 | content += `\n${suggestedVideoAttributes.title} ${dataset.separator} ${config.props.youtube.channelWatchUrl}${suggestedVideoAttributes.id}` 545 | } 546 | } else if (typeof suggestedVideo === "string") { 547 | content += `\n${suggestedVideo}` 548 | } else { 549 | content += `\n${suggestedVideo.label} ${dataset.separator} ${suggestedVideo.url}` 550 | } 551 | }) 552 | } 553 | if (video.links.length > 0) { 554 | content += heading(dataset.headings.links) 555 | video.links.forEach((link) => { 556 | if (typeof link === "string") { 557 | content += `\n${link}` 558 | } else { 559 | content += `\n${link.label} ${dataset.separator} ${link.url}` 560 | } 561 | }) 562 | } 563 | if (video.credits.length > 0) { 564 | content += heading(dataset.headings.credits) 565 | video.credits.forEach((credit) => { 566 | if (typeof credit === "string") { 567 | content += `\n${credit}` 568 | } else { 569 | content += `\n${credit.label} ${dataset.separator} ${credit.url}` 570 | } 571 | }) 572 | } 573 | if (video.affiliateLinks.length > 0) { 574 | content += heading(dataset.headings.affiliateLinks) 575 | let count = 0 576 | video.affiliateLinks.forEach((affiliateLink) => { 577 | if ( 578 | typeof affiliateLink === "string" && 579 | affiliateLink.match(/^(\w+?)\.(\w+?)$/) 580 | ) { 581 | const affiliateLinkMatch = affiliateLink.match(/^(\w+?)\.(\w+?)$/) 582 | const affiliateLinkAttributes = 583 | dataset.affiliateLinks[affiliateLinkMatch[1]][affiliateLinkMatch[2]] 584 | if (!affiliateLinkAttributes) { 585 | throw new Error(`Could not find affiliate link "${affiliateLink}"`) 586 | } 587 | if (typeof affiliateLinkAttributes === "string") { 588 | content += `\n${affiliateLinkAttributes}` 589 | } else if (affiliateLinkAttributes.url instanceof Array) { 590 | if (count !== 0) { 591 | content += "\n" 592 | } 593 | content += `\n${affiliateLinkAttributes.label}` 594 | affiliateLinkAttributes.url.forEach((url) => { 595 | if (typeof url === "string") { 596 | content += `\n${url}` 597 | } else { 598 | content += `\n${url.label} ${dataset.separator} ${url.url}` 599 | } 600 | }) 601 | } else { 602 | content += `\n${affiliateLinkAttributes.label} ${dataset.separator} ${affiliateLinkAttributes.url}` 603 | } 604 | } else if (typeof affiliateLink === "string") { 605 | content += `\n${affiliateLink}` 606 | } else if (affiliateLink.url instanceof Array) { 607 | if (count !== 0) { 608 | content += "\n" 609 | } 610 | content += `\n${affiliateLink.label}` 611 | affiliateLink.url.forEach((_url) => { 612 | if (typeof _url === "string") { 613 | content += `\n${_url}` 614 | } else { 615 | content += `\n${_url.label} ${dataset.separator} ${_url.url}` 616 | } 617 | }) 618 | } else { 619 | content += `\n${affiliateLink.label} ${dataset.separator} ${affiliateLink.url}` 620 | } 621 | count++ 622 | }) 623 | } 624 | if (video.footnotes.length > 0) { 625 | content += heading(dataset.headings.footnotes) 626 | video.footnotes.forEach((footnote) => { 627 | if (typeof footnote === "string") { 628 | content += `\n${footnote}` 629 | } else { 630 | let emoji = "" 631 | if (footnote.type === "warning") { 632 | emoji = "⚠️ " 633 | } 634 | if (footnote.timestamp === "") { 635 | content += `\n${emoji}${footnote.message}` 636 | } else { 637 | content += `\n${emoji}${footnote.timestamp} ${footnote.message}` 638 | } 639 | } 640 | }) 641 | } 642 | if (video.support.length > 0) { 643 | content += heading(dataset.headings.support) 644 | content += support(dataset, video) 645 | } 646 | return content 647 | } 648 | 649 | const preview = ( 650 | config: Config, 651 | dataset: Dataset, 652 | platform: string, 653 | video: Video, 654 | metadata: boolean 655 | ) => { 656 | let content = `${chalk.bold(video.title)}` 657 | content += `\n\n${description(config, dataset, platform, video)}` 658 | if (metadata) { 659 | content += `\n\n${chalk.bold("Tags:")} ${video.tags.join(", ")}` 660 | } 661 | console.info(content) 662 | } 663 | 664 | program 665 | .command("preview ") 666 | .description("preview video") 667 | .option( 668 | "--config ", 669 | "/path/to/config.json", 670 | resolve(homedir(), ".tube-manager/config.json") 671 | ) 672 | .option("--profile ", "configuration profile", "default") 673 | .option( 674 | "--dataset ", 675 | "/path/to/tube-manager.json", 676 | resolve(process.cwd(), "tube-manager.json") 677 | ) 678 | .option("--metadata", "enabled metadata preview") 679 | .action(async (platform, id, command) => { 680 | try { 681 | if (!["youtube"].includes(platform)) { 682 | throw new Error(`Invalid platform "${platform}"`) 683 | } 684 | const config = new Config(command.config, command.profile) 685 | await config.load() 686 | const json = await readFile(command.dataset, "utf8") 687 | const dataset = JSON.parse(json) 688 | let video 689 | if (platform === "youtube") { 690 | video = getYouTubeVideo(dataset, id) 691 | } 692 | if (!video) { 693 | throw new Error("Could not find video") 694 | } 695 | preview( 696 | config, 697 | dataset, 698 | platform, 699 | video, 700 | command.metadata ? command.metadata : false 701 | ) 702 | } catch (error) { 703 | logError(error) 704 | } 705 | }) 706 | 707 | interface PublishOptions { 708 | embeddable: boolean 709 | public: boolean 710 | publishRelated: boolean 711 | } 712 | 713 | const publishVideo = async ( 714 | config: Config, 715 | youtube: YouTube, 716 | dataset: Dataset, 717 | thumbnailDir: string, 718 | video: Video, 719 | options: PublishOptions 720 | ) => { 721 | let json: any = { 722 | id: video.id, 723 | snippet: { 724 | title: video.title, 725 | description: description(config, dataset, "youtube", video), 726 | tags: video.tags, 727 | categoryId: video.categoryId, 728 | }, 729 | status: {}, 730 | } 731 | if (options.embeddable === true) { 732 | json.status.embeddable = true 733 | } 734 | if (options.public === true) { 735 | json.status.privacyStatus = "public" 736 | } 737 | let updatedThumbnailHash: string 738 | const thumbnail = join(thumbnailDir, `${video.id}.jpg`) 739 | if (existsSync(thumbnail)) { 740 | const thumbnailHash = await hashFile(thumbnail, { 741 | algorithm: "sha256", 742 | }) 743 | if (!video.thumbnailHash || video.thumbnailHash !== thumbnailHash) { 744 | updatedThumbnailHash = thumbnailHash 745 | } 746 | } 747 | // See https://developers.google.com/youtube/v3/docs/videos/update 748 | await youtube.got.put("videos", { 749 | searchParams: { 750 | part: "snippet,status", 751 | }, 752 | json: json, 753 | }) 754 | // Upload thumbnail to YouTube (if present or has changed) 755 | if (updatedThumbnailHash) { 756 | // See https://developers.google.com/youtube/v3/docs/thumbnails/set 757 | await youtube.got.post("thumbnails/set", { 758 | prefixUrl: config.props.youtube.apiPrefixUrl.replace( 759 | "youtube", 760 | "upload/youtube" 761 | ), 762 | searchParams: { 763 | videoId: video.id, 764 | }, 765 | body: await readFile(thumbnail), 766 | }) 767 | } 768 | console.info( 769 | chalk.green( 770 | `Published video to ${chalk.bold( 771 | `${config.props.youtube.channelWatchUrl}${video.id}` 772 | )}` 773 | ) 774 | ) 775 | // Save thumbnail hash to dataset (if present or has changed) 776 | if (updatedThumbnailHash) { 777 | video.thumbnailHash = updatedThumbnailHash 778 | } 779 | } 780 | 781 | program 782 | .command("publish [id]") 783 | .description("publish video(s)") 784 | .option( 785 | "--config ", 786 | "/path/to/config.json", 787 | resolve(homedir(), ".tube-manager/config.json") 788 | ) 789 | .option("--profile ", "configuration profile", "default") 790 | .option( 791 | "--dataset ", 792 | "/path/to/tube-manager.json", 793 | resolve(process.cwd(), "tube-manager.json") 794 | ) 795 | .option( 796 | "--thumbnail-dir ", 797 | "/path/to/tube-manager", 798 | resolve(process.cwd(), "tube-manager") 799 | ) 800 | .option("--public", "make video(s) public") 801 | .option("--no-embeddable", "do not enable embedding") 802 | .option("--no-publish-related", "do not publish related video(s)") 803 | .action(async (id, command) => { 804 | try { 805 | const config = new Config(command.config, command.profile) 806 | await config.load() 807 | const youtube = new YouTube(config) 808 | const json = await readFile(command.dataset, "utf8") 809 | const dataset: Dataset = JSON.parse(json) 810 | const thumbnailDir = command.thumbnailDir 811 | const options: PublishOptions = { 812 | public: command.public, 813 | embeddable: command.embeddable, 814 | publishRelated: command.publishRelated, 815 | } 816 | if (!id) { 817 | const { confirmation } = await prompts({ 818 | message: "Are you sure you wish to publish all videos?", 819 | name: "confirmation", 820 | type: "confirm", 821 | }) 822 | if (confirmation !== true) { 823 | process.exit(0) 824 | } 825 | console.info("Publishing all videos...") 826 | for (const video of dataset.videos) { 827 | await publishVideo( 828 | config, 829 | youtube, 830 | dataset, 831 | thumbnailDir, 832 | video, 833 | options 834 | ) 835 | } 836 | } else { 837 | const video = getYouTubeVideo(dataset, id) 838 | if (!video) { 839 | throw new Error("Could not find video") 840 | } 841 | await publishVideo( 842 | config, 843 | youtube, 844 | dataset, 845 | thumbnailDir, 846 | video, 847 | options 848 | ) 849 | if (options.publishRelated === true) { 850 | // Find suggested videos that reference video 851 | let relatedVideoIds: string[] = [] 852 | dataset.videos.forEach((video) => { 853 | video.suggestedVideos.forEach((suggestedVideo) => { 854 | if ( 855 | typeof suggestedVideo === "string" && 856 | suggestedVideo.match(id) 857 | ) { 858 | relatedVideoIds.push(video.id) 859 | } 860 | }) 861 | }) 862 | if (relatedVideoIds.length > 0) { 863 | console.info("Publishing related videos...") 864 | for (const suggestedVideoId of relatedVideoIds) { 865 | const video = getYouTubeVideo(dataset, suggestedVideoId) 866 | if (!video) { 867 | throw new Error("Could not find video") 868 | } 869 | await publishVideo( 870 | config, 871 | youtube, 872 | dataset, 873 | thumbnailDir, 874 | video, 875 | options 876 | ) 877 | } 878 | } 879 | } 880 | } 881 | const data = await prettier.format(JSON.stringify(dataset, null, 2), { 882 | parser: "json", 883 | }) 884 | await writeFile(command.dataset, data) 885 | console.info("Done") 886 | } catch (error) { 887 | logError(error) 888 | } 889 | }) 890 | 891 | program.parse(process.argv) 892 | -------------------------------------------------------------------------------- /src/youtube.ts: -------------------------------------------------------------------------------- 1 | import got, { Got } from "got" 2 | import open from "open" 3 | import prompts from "prompts" 4 | import queryString from "query-string" 5 | import Config from "./config.js" 6 | 7 | export default class YouTube { 8 | readonly config: Config 9 | public got: Got 10 | constructor(config: Config) { 11 | this.config = config 12 | this.got = got.extend({ 13 | mutableDefaults: true, 14 | prefixUrl: this.config.props.youtube.apiPrefixUrl, 15 | headers: { 16 | authorization: 17 | this.config.props.youtube.accessToken !== "" 18 | ? `Bearer ${this.config.props.youtube.accessToken}` 19 | : undefined, 20 | }, 21 | responseType: "json", 22 | hooks: { 23 | afterResponse: [ 24 | async (response, retryWithMergedOptions) => { 25 | if ([401, 403].includes(response.statusCode)) { 26 | const accessToken = await this.getAccessToken() 27 | const updatedOptions = { 28 | headers: { 29 | authorization: `Bearer ${accessToken}`, 30 | }, 31 | } 32 | this.got.defaults.options.merge(updatedOptions) 33 | return retryWithMergedOptions(updatedOptions) 34 | } 35 | return response 36 | }, 37 | ], 38 | }, 39 | retry: { 40 | limit: 2, 41 | }, 42 | }) 43 | } 44 | async getRefreshToken(): Promise<{ 45 | access_token: string 46 | refresh_token: string 47 | }> { 48 | try { 49 | // See https://developers.google.com/identity/protocols/oauth2/web-server 50 | // See https://developers.google.com/identity/protocols/oauth2/scopes#youtube 51 | open( 52 | `${this.config.props.youtube.oauth2PrefixUrl}/auth?client_id=${this.config.props.youtube.clientId}&redirect_uri=http://localhost:8080&response_type=code&scope=https://www.googleapis.com/auth/youtube.force-ssl https://www.googleapis.com/auth/yt-analytics.readonly https://www.googleapis.com/auth/yt-analytics-monetary.readonly&access_type=offline` 53 | ) 54 | const { redirectUri } = await prompts({ 55 | message: "Please paste redirect URI here and press enter", 56 | name: "redirectUri", 57 | type: "text", 58 | validate: (value) => { 59 | console.log(value) 60 | if (/http:\/\/localhost:8080\/\?code=/.test(value) === false) { 61 | return "Please enter a valid redirect URI" 62 | } 63 | return true 64 | }, 65 | }) 66 | const parsed = queryString.parseUrl(redirectUri) 67 | // See https://developers.google.com/identity/protocols/oauth2/web-server 68 | const response: any = await this.got.post(`token`, { 69 | prefixUrl: this.config.props.youtube.oauth2PrefixUrl, 70 | json: { 71 | client_id: this.config.props.youtube.clientId, 72 | client_secret: this.config.props.youtube.clientSecret, 73 | code: parsed.query.code as string, 74 | grant_type: "authorization_code", 75 | redirect_uri: "http://localhost:8080", 76 | }, 77 | responseType: "json", 78 | }) 79 | return { 80 | access_token: response.body.access_token, 81 | refresh_token: response.body.refresh_token, 82 | } 83 | } catch (error) { 84 | throw error 85 | } 86 | } 87 | async getAccessToken(): Promise { 88 | try { 89 | let refreshToken = this.config.props.youtube.refreshToken 90 | if (refreshToken === "") { 91 | const values = await prompts({ 92 | type: "password", 93 | name: "refreshToken", 94 | message: "Please paste YouTube refresh token here and press enter", 95 | }) 96 | refreshToken = values.refreshToken 97 | } 98 | // See https://developers.google.com/identity/protocols/oauth2/web-server 99 | const response: any = await this.got.post(`token`, { 100 | prefixUrl: this.config.props.youtube.oauth2PrefixUrl, 101 | json: { 102 | client_id: this.config.props.youtube.clientId, 103 | client_secret: this.config.props.youtube.clientSecret, 104 | refresh_token: refreshToken, 105 | grant_type: "refresh_token", 106 | }, 107 | responseType: "json", 108 | }) 109 | this.config 110 | .set({ 111 | youtube: { 112 | accessToken: response.body.access_token, 113 | }, 114 | }) 115 | .save() 116 | return response.body.access_token 117 | } catch (error) { 118 | throw error 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "module": "Node16", 6 | "moduleResolution": "Node16", 7 | "noImplicitAny": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "build", 11 | "resolveJsonModule": true, 12 | "rootDir": "src", 13 | "sourceMap": true, 14 | "target": "ES2020" 15 | }, 16 | "include": ["./src/**/*"] 17 | } 18 | --------------------------------------------------------------------------------