diff --git a/package-lock.json b/package-lock.json index 9276349..b3a0061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aloth/olcli", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aloth/olcli", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", @@ -16,14 +16,18 @@ "conf": "^13.0.0", "ignore": "^7.0.5", "ora": "^8.0.1", + "puppeteer": "^24.43.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "tough-cookie": "^4.1.4" }, "bin": { + "git-remote-overleaf": "dist/git-helper.js", "olcli": "dist/cli.js" }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/node": "^22.0.0", + "@types/node": "^22.19.18", "@types/tough-cookie": "^4.0.5", "tsx": "^4.7.0", "typescript": "^5.4.0" @@ -32,6 +36,29 @@ "node": ">=18" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -474,6 +501,33 @@ "node": ">=18" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.1.tgz", + "integrity": "sha512-zmS4RTK9fbrc++WlAJhxYbfz3IjDeOmkK/CwwbLmk7ydfS9e2CiEeRJHEPvjDVElO/bwXbidwGA37Bsm6LzCnQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/adm-zip": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", @@ -484,11 +538,26 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "dev": true, + "version": "22.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -501,6 +570,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/adm-zip": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", @@ -510,6 +589,15 @@ "node": ">=12.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -555,6 +643,48 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/atomically": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", @@ -565,12 +695,160 @@ "when-exit": "^2.1.4" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -625,6 +903,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -652,6 +943,95 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -661,6 +1041,12 @@ "node": ">=18" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/conf": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/conf/-/conf-13.1.0.tgz", @@ -684,6 +1070,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -712,6 +1133,15 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debounce-fn": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", @@ -727,6 +1157,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "license": "BSD-3-Clause" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -816,6 +1292,15 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -840,6 +1325,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -882,16 +1376,112 @@ "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -904,6 +1494,65 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -919,6 +1568,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -931,6 +1589,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz", @@ -944,6 +1617,47 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -975,6 +1689,32 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -996,6 +1736,78 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -1008,6 +1820,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unicode-supported": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", @@ -1020,6 +1844,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -1032,6 +1889,54 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -1060,6 +1965,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1072,6 +2000,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -1084,6 +2067,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -1122,6 +2114,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -1134,62 +2188,302 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "24.43.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.0.tgz", + "integrity": "sha512-DRnMFz+J3s4lFUQcjqKl0/7h0jzlCZuUFU9lNjtKrnMl5WI1RwCaIItpHVu9empuPyUreYueN0sUW3/pnfdqsg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.1", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.0", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.0.tgz", + "integrity": "sha512-cCRNXsUlhyPoKDz6+TiSpfZpRS3mD6Y1YFKhkdr6ik6TMfuJb7fAtXq9ThUFc4sphxObDk3BuAvdxc1Y6YOnqQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.1", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-extra": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", + "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "@types/puppeteer": "*", + "puppeteer": "*", + "puppeteer-core": "*" + }, + "peerDependenciesMeta": { + "@types/puppeteer": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz", + "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==", "license": "MIT", "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "merge-deep": "^3.0.1" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=9.11.2" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } } }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "node_modules/puppeteer-extra-plugin-stealth": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz", + "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==", "license": "MIT", "dependencies": { - "parse5": "^7.0.0" + "debug": "^4.1.1", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-preferences": "^2.4.1" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=8" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } } }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "node_modules/puppeteer-extra-plugin-user-data-dir": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz", + "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==", "license": "MIT", "dependencies": { - "punycode": "^2.3.1" + "debug": "^4.1.1", + "fs-extra": "^10.0.0", + "puppeteer-extra-plugin": "^3.2.3", + "rimraf": "^3.0.2" }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/puppeteer-extra-plugin-user-preferences": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz", + "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==", "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "deepmerge": "^4.2.2", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-data-dir": "^2.4.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } } }, "node_modules/querystringify": { @@ -1198,6 +2492,15 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1213,6 +2516,15 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1239,6 +2551,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1246,9 +2574,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1257,6 +2585,42 @@ "node": ">=10" } }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1269,6 +2633,54 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -1281,6 +2693,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -1328,6 +2751,50 @@ "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -1343,6 +2810,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1375,11 +2848,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -1414,7 +2893,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -1436,6 +2915,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -1463,6 +2948,187 @@ "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index dd3a46a..c36a777 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Command-line interface for Overleaf — Sync, manage, and compile LaTeX projects from your terminal", "type": "module", "bin": { - "olcli": "dist/cli.js" + "olcli": "dist/cli.js", + "git-remote-overleaf": "dist/git-helper.js" }, "scripts": { "build": "tsc", @@ -51,11 +52,14 @@ "conf": "^13.0.0", "ignore": "^7.0.5", "ora": "^8.0.1", + "puppeteer": "^24.43.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "tough-cookie": "^4.1.4" }, "devDependencies": { "@types/adm-zip": "^0.5.7", - "@types/node": "^22.0.0", + "@types/node": "^22.19.18", "@types/tough-cookie": "^4.0.5", "tsx": "^4.7.0", "typescript": "^5.4.0" diff --git a/src/cli.ts b/src/cli.ts index c352957..8597f7a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; import { join, dirname, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; import { OverleafClient } from './client.js'; +import { spawn } from 'node:child_process'; import { loadIgnore, shouldIgnore, @@ -26,8 +27,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')); const VERSION = pkg.version; import { - getSessionCookie, - setSessionCookie, + getSession, + setSession, getLastProject, setLastProject, getConfigPath, @@ -42,17 +43,67 @@ import { const program = new Command(); program - .name('olcli') - .description('Overleaf CLI - interact with Overleaf projects from the command line') - .version(VERSION) - .option('--base-url ', 'Overleaf instance base URL (overrides OVERLEAF_BASE_URL and config)') - .option('--cookie-name ', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)'); +.name('olcli') +.description('Overleaf CLI - interact with Overleaf projects from the command line') +.version(VERSION) +.option('--base-url ', 'Overleaf instance base URL (overrides OVERLEAF_BASE_URL and config)') +.option('--cookie-name ', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)'); + +/** + * Helper to watch the files and auto compile them + */ +async function runWatch(mainFile: string, useDocker: boolean) { + if (!existsSync(mainFile)) { + console.error(chalk.red(`\n❌ Error: Could not find main document '${mainFile}'`)); + process.exit(1); + } + const latexmkArgs = [ + '-pdf', // Compile to PDF + '-interaction=nonstopmode', // Don't pause terminal on syntax errors + '-synctex=1', // Enable SyncTeX (Click PDF to jump to code) + '-file-line-error', // Format errors nicely + '-auxdir=.aux', // Put all temp/aux files in a hidden .aux folder! + '-outdir=.build', // Keep the final PDF in the root folder! + '-pvc', // MAGIC FLAG: Preview Continuously (Watch mode!) + '-shell-escape', + mainFile + ]; + + console.log(chalk.cyan(`\nStarting local Overleaf compiler environment...`)); + console.log(chalk.gray(`Watching for file changes. Press Ctrl+C to stop.\n`)); + + let command = 'latexmk'; + let args = latexmkArgs; + + if (useDocker) { + command = 'docker'; + args =[ + 'run', '--rm', '-it', + '-v', `${process.cwd()}:/workdir`, // Mount current directory + '-w', '/workdir', // Set working directory + 'texlive/texlive:latest', // The official image Overleaf uses + 'latexmk', ...latexmkArgs + ]; + } + + const compiler = spawn(command, args, { stdio: 'inherit' }); + + compiler.on('error', (err: any) => { + if (err.code === 'ENOENT') { + console.error(chalk.red(`\n❌ Error: '${command}' is not installed on your system.`)); + } else { + console.error(chalk.red(`\nCompiler error: ${err.message}`)); + } + process.exit(1); + }); +} /** * Helper to get authenticated client */ -async function getClient(cookieOpt?: string, baseUrlOpt?: string): Promise { - const cookie = cookieOpt || getSessionCookie(); +async function getClient(baseUrlOpt?: string): Promise { + const baseUrl = baseUrlOpt || getBaseUrl(); + const cookie = getSession(baseUrl); if (!cookie) { console.error(chalk.red('No session cookie found.')); console.error('Set one with: olcli auth --cookie '); @@ -60,7 +111,6 @@ async function getClient(cookieOpt?: string, baseUrlOpt?: string): Promise', 'Session cookie (overleaf_session2 value)') - .option('--save-local', 'Save to .olauth in current directory') - .action(async (options) => { - if (!options.cookie) { - console.log(chalk.yellow('To authenticate, provide your session cookie:')); - console.log(); - console.log('1. Log into overleaf.com in your browser'); - console.log('2. Open Developer Tools (F12) → Application → Cookies'); - console.log('3. Find the cookie named "overleaf_session2"'); - console.log('4. Copy its value and run:'); - console.log(); - console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"')); - console.log(); - console.log('Or set OVERLEAF_SESSION environment variable'); - return; - } - - const spinner = ora('Verifying session...').start(); - try { - const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl(); - const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName(); - const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName); - const projects = await client.listProjects(); +.command('auth') +.description('Authenticate with Overleaf using session cookie') +.option('--cookie ', 'Session cookie (overleaf_session2 value)') +.option('--save-local', 'Save to .olauth in current directory') +.action(async (options) => { + if (!options.cookie) { + console.log(chalk.yellow('To authenticate, provide your session cookie:')); + console.log(); + console.log('1. Log into overleaf.com in your browser'); + console.log('2. Open Developer Tools (F12) → Application → Cookies'); + console.log('3. Find the cookie named "overleaf_session2"'); + console.log('4. Copy its value and run:'); + console.log(); + console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"')); + console.log(); + console.log('Or set OVERLEAF_SESSION environment variable'); + return; + } - setSessionCookie(options.cookie); + const spinner = ora('Verifying session...').start(); + try { + const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl(); + const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName(); + const client = await OverleafClient.fromSessionCookie(options.cookie, baseUrl, cookieName); + const projects = await client.listProjects(); - if (options.saveLocal) { - saveOlAuth(options.cookie); - spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`); - } else { - spinner.succeed(`Authenticated! Found ${projects.length} projects.`); - } + setSession(baseUrl, options.cookie); - console.log(chalk.dim(`Config saved to: ${getConfigPath()}`)); - } catch (error: any) { - spinner.fail(`Authentication failed: ${error.message}`); - process.exit(1); + if (options.saveLocal) { + saveOlAuth(options.cookie); + spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`); + } else { + spinner.succeed(`Authenticated! Found ${projects.length} projects.`); } - }); + + console.log(chalk.dim(`Config saved to: ${getConfigPath()}`)); + } catch (error: any) { + spinner.fail(`Authentication failed: ${error.message}`); + process.exit(1); + } +}); program - .command('whoami') - .description('Show current authentication status') - .action(async () => { - const cookie = getSessionCookie(); - if (!cookie) { - console.log(chalk.yellow('Not authenticated')); - return; - } +.command('whoami') +.description('Show current authentication status') +.action(async () => { + const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl(); + const cookie = getSession(baseUrl); + if (!cookie) { + console.log(chalk.yellow('Not authenticated')); + return; + } - const spinner = ora('Checking session...').start(); - try { - const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl(); - const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName(); - const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName); - const projects = await client.listProjects(); - spinner.succeed(`Authenticated with access to ${projects.length} projects`); - } catch (error: any) { - spinner.fail(`Session invalid: ${error.message}`); - } - }); + const spinner = ora('Checking session...').start(); + try { + const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName(); + const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName); + const projects = await client.listProjects(); + spinner.succeed(`Authenticated with access to ${projects.length} projects`); + } catch (error: any) { + spinner.fail(`Session invalid: ${error.message}`); + } +}); program - .command('logout') - .description('Clear stored credentials') - .action(() => { - clearConfig(); - console.log(chalk.green('Credentials cleared')); - }); +.command('logout') +.description('Clear stored credentials') +.action(() => { + clearConfig(); + console.log(chalk.green('Credentials cleared')); +}); // ───────────────────────────────────────────────────────────────────────────── // PROJECT COMMANDS // ───────────────────────────────────────────────────────────────────────────── program - .command('list') - .alias('ls') - .description('List all projects') - .option('--json', 'Output as JSON') - .option('-n, --limit ', 'Limit number of results', parseInt) - .option('--cookie ', 'Session cookie override') - .action(async (options) => { - const spinner = ora('Fetching projects...').start(); - try { - const client = await getClient(options.cookie); - let projects = await client.listProjects(); - - if (options.limit) { - projects = projects.slice(0, options.limit); - } +.command('list') +.alias('ls') +.description('List all projects') +.option('--json', 'Output as JSON') +.option('-n, --limit ', 'Limit number of results', parseInt) +.option('--cookie ', 'Session cookie override') +.action(async (options) => { + const spinner = ora('Fetching projects...').start(); + try { + const client = await getClient(options.cookie); + let projects = await client.listProjects(); + + if (options.limit) { + projects = projects.slice(0, options.limit); + } - spinner.stop(); + spinner.stop(); - if (options.json) { - console.log(JSON.stringify(projects, null, 2)); - return; - } + if (options.json) { + console.log(JSON.stringify(projects, null, 2)); + return; + } - if (projects.length === 0) { - console.log(chalk.yellow('No projects found')); - return; - } + if (projects.length === 0) { + console.log(chalk.yellow('No projects found')); + return; + } - console.log(chalk.bold(`Found ${projects.length} project(s):\n`)); - for (const p of projects) { - const date = new Date(p.lastUpdated).toLocaleDateString(); - console.log(` ${chalk.cyan(p.id)} - ${chalk.bold(p.name)}`); - console.log(` ${chalk.dim(`Last updated: ${date}`)}`); - } - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); + console.log(chalk.bold(`Found ${projects.length} project(s):\n`)); + for (const p of projects) { + const date = new Date(p.lastUpdated).toLocaleDateString(); + console.log(` ${chalk.cyan(p.id)} - ${chalk.bold(p.name)}`); + console.log(` ${chalk.dim(`Last updated: ${date}`)}`); } - }); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('info [project]') - .description('Show project details (by name or ID)') - .option('--json', 'Output as JSON') - .option('--cookie ', 'Session cookie override') - .action(async (project, options) => { - const spinner = ora('Fetching project info...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - // Get entities (works without parsing HTML) - const entities = await client.getEntities(proj.id); - spinner.stop(); +.command('info [project]') +.description('Show project details (by name or ID)') +.option('--json', 'Output as JSON') +.option('--cookie ', 'Session cookie override') +.action(async (project, options) => { + const spinner = ora('Fetching project info...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + // Get entities (works without parsing HTML) + const entities = await client.getEntities(proj.id); + spinner.stop(); + + if (options.json) { + console.log(JSON.stringify({ project: proj, entities }, null, 2)); + return; + } - if (options.json) { - console.log(JSON.stringify({ project: proj, entities }, null, 2)); - return; - } + console.log(chalk.bold(`Project: ${proj.name}`)); + console.log(` ID: ${chalk.cyan(proj.id)}`); + console.log(); - console.log(chalk.bold(`Project: ${proj.name}`)); - console.log(` ID: ${chalk.cyan(proj.id)}`); - console.log(); + // Print file list grouped by folder + console.log(chalk.bold('Files:')); - // Print file list grouped by folder - console.log(chalk.bold('Files:')); - - // Sort entities by path for nice display - const sorted = entities.sort((a, b) => a.path.localeCompare(b.path)); - - for (const entity of sorted) { - const icon = entity.type === 'doc' ? '📄' : '📎'; - console.log(` ${icon} ${entity.path}`); - } + // Sort entities by path for nice display + const sorted = entities.sort((a, b) => a.path.localeCompare(b.path)); - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); + for (const entity of sorted) { + const icon = entity.type === 'doc' ? '📄' : '📎'; + console.log(` ${icon} ${entity.path}`); } - }); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); function printFolder(folder: any, indent: string): void { // Print subfolders @@ -294,186 +344,186 @@ function printFolder(folder: any, indent: string): void { // ───────────────────────────────────────────────────────────────────────────── program - .command('download [project]') - .description('Download a single file from project') - .option('-o, --output ', 'Output path (default: same as file name)') - .option('--cookie ', 'Session cookie override') - .action(async (file, project, options) => { - const spinner = ora('Downloading file...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - const content = await client.downloadByPath(proj.id, file); - const outputPath = options.output || basename(file); - - writeFileSync(outputPath, content); - spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`); - - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); +.command('download [project]') +.description('Download a single file from project') +.option('-o, --output ', 'Output path (default: same as file name)') +.option('--cookie ', 'Session cookie override') +.action(async (file, project, options) => { + const spinner = ora('Downloading file...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + const content = await client.downloadByPath(proj.id, file); + const outputPath = options.output || basename(file); + + writeFileSync(outputPath, content); + spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('zip [project]') - .description('Download project as zip archive') - .option('-o, --output ', 'Output path (default: .zip)') - .option('--cookie ', 'Session cookie override') - .action(async (project, options) => { - const spinner = ora('Downloading project...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - const zip = await client.downloadProject(proj.id); - const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.zip`; - - writeFileSync(outputPath, zip); - spinner.succeed(`Downloaded: ${outputPath} (${(zip.length / 1024).toFixed(1)} KB)`); - - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); +.command('zip [project]') +.description('Download project as zip archive') +.option('-o, --output ', 'Output path (default: .zip)') +.option('--cookie ', 'Session cookie override') +.action(async (project, options) => { + const spinner = ora('Downloading project...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + const zip = await client.downloadProject(proj.id); + const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.zip`; + + writeFileSync(outputPath, zip); + spinner.succeed(`Downloaded: ${outputPath} (${(zip.length / 1024).toFixed(1)} KB)`); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('pdf [project]') - .description('Compile and download PDF') - .option('-o, --output ', 'Output path (default: .pdf)') - .option('--cookie ', 'Session cookie override') - .action(async (project, options) => { - const spinner = ora('Compiling project...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - spinner.text = 'Compiling...'; - const pdf = await client.downloadPdf(proj.id); - const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.pdf`; - - writeFileSync(outputPath, pdf); - spinner.succeed(`Downloaded PDF: ${outputPath} (${(pdf.length / 1024).toFixed(1)} KB)`); - - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); +.command('pdf [project]') +.description('Compile and download PDF') +.option('-o, --output ', 'Output path (default: .pdf)') +.option('--cookie ', 'Session cookie override') +.action(async (project, options) => { + const spinner = ora('Compiling project...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + spinner.text = 'Compiling...'; + const pdf = await client.downloadPdf(proj.id); + const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.pdf`; + + writeFileSync(outputPath, pdf); + spinner.succeed(`Downloaded PDF: ${outputPath} (${(pdf.length / 1024).toFixed(1)} KB)`); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('output [type]') - .description('Download compile output files (bbl, log, aux, etc.)') - .option('-o, --output ', 'Output path') - .option('--list', 'List available output files') - .option('--project ', 'Project name or ID') - .option('--cookie ', 'Session cookie override') - .action(async (type, options) => { - const spinner = ora('Compiling project...').start(); - try { - const client = await getClient(options.cookie); - - // If type looks like a project name (contains spaces or is in project list), treat it as project - let actualType = type; - let projectArg = options.project; - - if (type && !projectArg && !['bbl', 'log', 'aux', 'blg', 'pdf', 'out', 'fls', 'fdb_latexmk', 'stderr', 'pdfxref', 'chktex'].includes(type)) { - // Type might actually be a project name - const projects = await client.listProjects(); - const matchedProject = projects.find(p => p.name === type || p.id === type); - if (matchedProject) { - projectArg = type; - actualType = undefined; - } +.command('output [type]') +.description('Download compile output files (bbl, log, aux, etc.)') +.option('-o, --output ', 'Output path') +.option('--list', 'List available output files') +.option('--project ', 'Project name or ID') +.option('--cookie ', 'Session cookie override') +.action(async (type, options) => { + const spinner = ora('Compiling project...').start(); + try { + const client = await getClient(options.cookie); + + // If type looks like a project name (contains spaces or is in project list), treat it as project + let actualType = type; + let projectArg = options.project; + + if (type && !projectArg && !['bbl', 'log', 'aux', 'blg', 'pdf', 'out', 'fls', 'fdb_latexmk', 'stderr', 'pdfxref', 'chktex'].includes(type)) { + // Type might actually be a project name + const projects = await client.listProjects(); + const matchedProject = projects.find(p => p.name === type || p.id === type); + if (matchedProject) { + projectArg = type; + actualType = undefined; } + } - const proj = await resolveProject(client, projectArg); - const result = await client.compileWithOutputs(proj.id); + const proj = await resolveProject(client, projectArg); + const result = await client.compileWithOutputs(proj.id); - if (result.status !== 'success') { - spinner.warn(`Compilation ${result.status}, but output files may still be available`); - } + if (result.status !== 'success') { + spinner.warn(`Compilation ${result.status}, but output files may still be available`); + } - if (options.list || !actualType) { - spinner.stop(); - console.log(chalk.bold('Available output files:')); - for (const file of result.outputFiles) { - console.log(` ${chalk.cyan(file.type.padEnd(12))} ${file.path}`); - } - console.log(); - console.log(chalk.dim('Usage: olcli output ')); - console.log(chalk.dim('Example: olcli output bbl')); - return; + if (options.list || !actualType) { + spinner.stop(); + console.log(chalk.bold('Available output files:')); + for (const file of result.outputFiles) { + console.log(` ${chalk.cyan(file.type.padEnd(12))} ${file.path}`); } + console.log(); + console.log(chalk.dim('Usage: olcli output ')); + console.log(chalk.dim('Example: olcli output bbl')); + return; + } - // Find matching output file - const outputFile = result.outputFiles.find(f => f.type === actualType || f.path.endsWith(`.${actualType}`)); - if (!outputFile) { - spinner.fail(`Output file not found: ${actualType}`); - console.log(chalk.dim('Use --list to see available files')); - process.exit(1); - } + // Find matching output file + const outputFile = result.outputFiles.find(f => f.type === actualType || f.path.endsWith(`.${actualType}`)); + if (!outputFile) { + spinner.fail(`Output file not found: ${actualType}`); + console.log(chalk.dim('Use --list to see available files')); + process.exit(1); + } - spinner.text = `Downloading ${outputFile.path}...`; - const content = await client.downloadOutputFile(outputFile.url); - const outputPath = options.output || outputFile.path.replace('output.', ''); + spinner.text = `Downloading ${outputFile.path}...`; + const content = await client.downloadOutputFile(outputFile.url); + const outputPath = options.output || outputFile.path.replace('output.', ''); - writeFileSync(outputPath, content); - spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`); + writeFileSync(outputPath, content); + spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`); - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); // ───────────────────────────────────────────────────────────────────────────── // UPLOAD COMMANDS // ───────────────────────────────────────────────────────────────────────────── program - .command('upload [project]') - .description('Upload a file to a project') - .option('--folder ', 'Target folder ID (default: root)') - .option('--cookie ', 'Session cookie override') - .action(async (file, project, options) => { - const spinner = ora('Uploading file...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - if (!existsSync(file)) { - spinner.fail(`File not found: ${file}`); - process.exit(1); - } +.command('upload [project]') +.description('Upload a file to a project') +.option('--folder ', 'Target folder ID (default: root)') +.option('--cookie ', 'Session cookie override') +.action(async (file, project, options) => { + const spinner = ora('Uploading file...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + if (!existsSync(file)) { + spinner.fail(`File not found: ${file}`); + process.exit(1); + } - const content = readFileSync(file); - const fileName = basename(file); + const content = readFileSync(file); + const fileName = basename(file); - // Pass folder ID or null for root folder (client will compute it) - const folderId = options.folder || null; + // Pass folder ID or null for root folder (client will compute it) + const folderId = options.folder || null; - const result = await client.uploadFile(proj.id, folderId, fileName, content); + const result = await client.uploadFile(proj.id, folderId, fileName, content); - if (result.success) { - spinner.succeed(`Uploaded: ${fileName} → "${proj.name}"`); - } else { - spinner.fail(`Upload failed for: ${fileName}`); - process.exit(1); - } - - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); + if (result.success) { + spinner.succeed(`Uploaded: ${fileName} → "${proj.name}"`); + } else { + spinner.fail(`Upload failed for: ${fileName}`); process.exit(1); } - }); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); // ───────────────────────────────────────────────────────────────────────────── // DELETE / RENAME COMMANDS @@ -482,285 +532,285 @@ program // /project//entities, then call the documented delete/rename endpoints. program - .command('delete [project]') - .alias('rm') - .description('Delete a file or folder from a project') - .option('--cookie ', 'Session cookie override') - .action(async (file, project, options) => { - const spinner = ora('Deleting file...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - await client.deleteByPath(proj.id, file); - spinner.succeed(`Deleted: ${file}`); - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); +.command('delete [project]') +.alias('rm') +.description('Delete a file or folder from a project') +.option('--cookie ', 'Session cookie override') +.action(async (file, project, options) => { + const spinner = ora('Deleting file...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + await client.deleteByPath(proj.id, file); + spinner.succeed(`Deleted: ${file}`); + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('rename [project]') - .alias('mv') - .description('Rename a file or folder in a project') - .option('--cookie ', 'Session cookie override') - .action(async (oldname, newname, project, options) => { - const spinner = ora('Renaming file...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - await client.renameByPath(proj.id, oldname, newname); - spinner.succeed(`Renamed: ${oldname} → ${newname}`); - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); - } - }); +.command('rename [project]') +.alias('mv') +.description('Rename a file or folder in a project') +.option('--cookie ', 'Session cookie override') +.action(async (oldname, newname, project, options) => { + const spinner = ora('Renaming file...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + await client.renameByPath(proj.id, oldname, newname); + spinner.succeed(`Renamed: ${oldname} → ${newname}`); + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); // ───────────────────────────────────────────────────────────────────────────── // COMPILE COMMAND // ───────────────────────────────────────────────────────────────────────────── program - .command('compile [project]') - .description('Compile a project (trigger PDF generation)') - .option('--cookie ', 'Session cookie override') - .action(async (project, options) => { - const spinner = ora('Compiling...').start(); - try { - const client = await getClient(options.cookie); - const proj = await resolveProject(client, project); - - const result = await client.compileProject(proj.id); - spinner.succeed(`Compiled "${proj.name}"`); - console.log(chalk.dim(`PDF URL: ${result.pdfUrl}`)); - - setLastProject(proj.id); - } catch (error: any) { - spinner.fail(`Compilation failed: ${error.message}`); - process.exit(1); - } - }); +.command('compile [project]') +.description('Compile a project (trigger PDF generation)') +.option('--cookie ', 'Session cookie override') +.action(async (project, options) => { + const spinner = ora('Compiling...').start(); + try { + const client = await getClient(options.cookie); + const proj = await resolveProject(client, project); + + const result = await client.compileProject(proj.id); + spinner.succeed(`Compiled "${proj.name}"`); + console.log(chalk.dim(`PDF URL: ${result.pdfUrl}`)); + + setLastProject(proj.id); + } catch (error: any) { + spinner.fail(`Compilation failed: ${error.message}`); + process.exit(1); + } +}); // ───────────────────────────────────────────────────────────────────────────── // SYNC COMMANDS // ───────────────────────────────────────────────────────────────────────────── program - .command('pull [project] [dir]') - .description('Download project files to local directory') - .option('--force', 'Overwrite local files even if newer') - .option('--cookie ', 'Session cookie override') - .action(async (project, dir, options) => { - let targetDir = dir || '.'; - let projectId: string | undefined; - let projectName: string | undefined; - - // Check for existing .olcli.json if no project specified - const metaPath = join(targetDir, '.olcli.json'); - if (!project && existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - projectId = meta.projectId; - projectName = meta.projectName; - } else if (!project) { - console.error(chalk.red('No project specified.')); - console.error('Usage: olcli pull [dir]'); - console.error('Or run from a directory with .olcli.json'); - process.exit(1); - } +.command('pull [project] [dir]') +.description('Download project files to local directory') +.option('--force', 'Overwrite local files even if newer') +.option('--cookie ', 'Session cookie override') +.action(async (project, dir, options) => { + let targetDir = dir || '.'; + let projectId: string | undefined; + let projectName: string | undefined; + + // Check for existing .olcli.json if no project specified + const metaPath = join(targetDir, '.olcli.json'); + if (!project && existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + projectId = meta.projectId; + projectName = meta.projectName; + } else if (!project) { + console.error(chalk.red('No project specified.')); + console.error('Usage: olcli pull [dir]'); + console.error('Or run from a directory with .olcli.json'); + process.exit(1); + } - const spinner = ora('Fetching project...').start(); - try { - const client = await getClient(options.cookie); + const spinner = ora('Fetching project...').start(); + try { + const client = await getClient(options.cookie); - // Resolve project if needed - if (!projectId) { - let proj = await client.getProjectById(project!); - if (!proj) { - proj = await client.getProject(project!); - } - if (!proj) { - spinner.fail(`Project not found: ${project}`); - process.exit(1); - } - projectId = proj.id; - projectName = proj.name; - // Default directory is project name (sanitized) if not specified - if (!dir) { - targetDir = proj.name.replace(/[^a-zA-Z0-9-_]/g, '_'); - } + // Resolve project if needed + if (!projectId) { + let proj = await client.getProjectById(project!); + if (!proj) { + proj = await client.getProject(project!); } + if (!proj) { + spinner.fail(`Project not found: ${project}`); + process.exit(1); + } + projectId = proj.id; + projectName = proj.name; + // Default directory is project name (sanitized) if not specified + if (!dir) { + targetDir = proj.name.replace(/[^a-zA-Z0-9-_]/g, '_'); + } + } - spinner.text = 'Downloading project...'; - const zipBuffer = await client.downloadProject(projectId); + spinner.text = 'Downloading project...'; + const zipBuffer = await client.downloadProject(projectId); - // Extract zip - spinner.text = 'Extracting files...'; - const AdmZip = (await import('adm-zip')).default; - const zip = new AdmZip(zipBuffer); + // Extract zip + spinner.text = 'Extracting files...'; + const AdmZip = (await import('adm-zip')).default; + const zip = new AdmZip(zipBuffer); - // Create target directory - if (!existsSync(targetDir)) { - mkdirSync(targetDir, { recursive: true }); - } + // Create target directory + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } - // Get local file modification times for safety check - const { statSync } = await import('node:fs'); - const localMetaPath = join(targetDir, '.olcli.json'); - let lastPull: Date | undefined; - if (existsSync(localMetaPath)) { - const meta = JSON.parse(readFileSync(localMetaPath, 'utf-8')); - lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; - } + // Get local file modification times for safety check + const { statSync } = await import('node:fs'); + const localMetaPath = join(targetDir, '.olcli.json'); + let lastPull: Date | undefined; + if (existsSync(localMetaPath)) { + const meta = JSON.parse(readFileSync(localMetaPath, 'utf-8')); + lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; + } - // Extract files with safety check - const entries = zip.getEntries(); - let fileCount = 0; - let skippedCount = 0; - const skippedFiles: string[] = []; - - for (const entry of entries) { - if (!entry.isDirectory) { - const filePath = join(targetDir, entry.entryName); - const fileDir = dirname(filePath); - - // Check if local file exists and is newer than last pull - if (!options.force && existsSync(filePath) && lastPull) { - try { - const stats = statSync(filePath); - if (stats.mtime > lastPull) { - // Local file is newer - skip unless --force - skippedCount++; - skippedFiles.push(entry.entryName); - continue; - } - } catch (e) { - // File doesn't exist or can't stat, proceed with download + // Extract files with safety check + const entries = zip.getEntries(); + let fileCount = 0; + let skippedCount = 0; + const skippedFiles: string[] = []; + + for (const entry of entries) { + if (!entry.isDirectory) { + const filePath = join(targetDir, entry.entryName); + const fileDir = dirname(filePath); + + // Check if local file exists and is newer than last pull + if (!options.force && existsSync(filePath) && lastPull) { + try { + const stats = statSync(filePath); + if (stats.mtime > lastPull) { + // Local file is newer - skip unless --force + skippedCount++; + skippedFiles.push(entry.entryName); + continue; } + } catch (e) { + // File doesn't exist or can't stat, proceed with download } + } - if (!existsSync(fileDir)) { - mkdirSync(fileDir, { recursive: true }); - } - writeFileSync(filePath, entry.getData()); - fileCount++; + if (!existsSync(fileDir)) { + mkdirSync(fileDir, { recursive: true }); } + writeFileSync(filePath, entry.getData()); + fileCount++; } + } - // Save project metadata (with manifest of remote files for sync deletion tracking) - const remoteManifest: string[] = []; - for (const e of entries) { - if (!e.isDirectory) remoteManifest.push(e.entryName); + // Save project metadata (with manifest of remote files for sync deletion tracking) + const remoteManifest: string[] = []; + for (const e of entries) { + if (!e.isDirectory) remoteManifest.push(e.entryName); + } + writeFileSync(join(targetDir, '.olcli.json'), JSON.stringify({ + projectId, + projectName, + lastPull: new Date().toISOString(), + remoteManifest + }, null, 2)); + + if (skippedCount > 0) { + spinner.warn(`Downloaded ${fileCount} files, skipped ${skippedCount} locally modified files`); + console.log(chalk.yellow(' Skipped (local is newer):')); + for (const f of skippedFiles.slice(0, 5)) { + console.log(chalk.dim(` ${f}`)); } - writeFileSync(join(targetDir, '.olcli.json'), JSON.stringify({ - projectId, - projectName, - lastPull: new Date().toISOString(), - remoteManifest - }, null, 2)); - - if (skippedCount > 0) { - spinner.warn(`Downloaded ${fileCount} files, skipped ${skippedCount} locally modified files`); - console.log(chalk.yellow(' Skipped (local is newer):')); - for (const f of skippedFiles.slice(0, 5)) { - console.log(chalk.dim(` ${f}`)); - } - if (skippedFiles.length > 5) { - console.log(chalk.dim(` ... and ${skippedFiles.length - 5} more`)); - } - console.log(chalk.dim(' Use --force to overwrite')); - } else { - spinner.succeed(`Downloaded ${fileCount} files to ${targetDir}/`); + if (skippedFiles.length > 5) { + console.log(chalk.dim(` ... and ${skippedFiles.length - 5} more`)); } - - setLastProject(projectId); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); + console.log(chalk.dim(' Use --force to overwrite')); + } else { + spinner.succeed(`Downloaded ${fileCount} files to ${targetDir}/`); } - }); + + setLastProject(projectId); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('push [dir]') - .description('Upload local changes to Overleaf project') - .option('--project ', 'Project name or ID (overrides .olcli.json)') - .option('--all', 'Upload all files (not just changed)') - .option('--dry-run', 'Show what would be uploaded without uploading') - .option('--probe-folder', 'Probe for correct folder ID (use if uploads fail with folder_not_found)') - .option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)') - .option('--no-ignore', 'Disable all ignore filtering (escape hatch — uploads everything)') - .option('--show-ignored', 'Print files skipped by ignore rules') - .option('--cookie ', 'Session cookie override') - .action(async (dir, options) => { - const targetDir = dir || '.'; - const metaPath = join(targetDir, '.olcli.json'); - - // Check for project metadata - let projectId: string | undefined; - let projectName: string | undefined; - let lastPull: Date | undefined; - let rootFolderId: string | undefined; +.command('push [dir]') +.description('Upload local changes to Overleaf project') +.option('--project ', 'Project name or ID (overrides .olcli.json)') +.option('--all', 'Upload all files (not just changed)') +.option('--dry-run', 'Show what would be uploaded without uploading') +.option('--probe-folder', 'Probe for correct folder ID (use if uploads fail with folder_not_found)') +.option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)') +.option('--no-ignore', 'Disable all ignore filtering (escape hatch — uploads everything)') +.option('--show-ignored', 'Print files skipped by ignore rules') +.option('--cookie ', 'Session cookie override') +.action(async (dir, options) => { + const targetDir = dir || '.'; + const metaPath = join(targetDir, '.olcli.json'); + + // Check for project metadata + let projectId: string | undefined; + let projectName: string | undefined; + let lastPull: Date | undefined; + let rootFolderId: string | undefined; - if (existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - projectId = meta.projectId; - projectName = meta.projectName; - lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; - rootFolderId = meta.rootFolderId; - } + if (existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + projectId = meta.projectId; + projectName = meta.projectName; + lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; + rootFolderId = meta.rootFolderId; + } - if (options.project) { - // Override with command line option - projectId = undefined; - projectName = options.project; - } + if (options.project) { + // Override with command line option + projectId = undefined; + projectName = options.project; + } - if (!projectId && !projectName) { - console.error(chalk.red('No project specified.')); - console.error('Either run from a directory with .olcli.json or use --project'); - process.exit(1); - } + if (!projectId && !projectName) { + console.error(chalk.red('No project specified.')); + console.error('Either run from a directory with .olcli.json or use --project'); + process.exit(1); + } - const spinner = ora('Connecting...').start(); - try { - const client = await getClient(options.cookie); + const spinner = ora('Connecting...').start(); + try { + const client = await getClient(options.cookie); - // Resolve project if needed - if (!projectId) { - let proj = await client.getProjectById(projectName!); - if (!proj) { - proj = await client.getProject(projectName!); - } - if (!proj) { - spinner.fail(`Project not found: ${projectName}`); - process.exit(1); - } - projectId = proj.id; - projectName = proj.name; + // Resolve project if needed + if (!projectId) { + let proj = await client.getProjectById(projectName!); + if (!proj) { + proj = await client.getProject(projectName!); } + if (!proj) { + spinner.fail(`Project not found: ${projectName}`); + process.exit(1); + } + projectId = proj.id; + projectName = proj.name; + } - spinner.text = 'Scanning files...'; + spinner.text = 'Scanning files...'; - // Build ignore context (defaults + .olignore + .olignore.local) - const ignoreCtx = loadIgnore(targetDir, { - noDefaults: options.defaultIgnore === false, - disableAll: options.ignore === false, - }); + // Build ignore context (defaults + .olignore + .olignore.local) + const ignoreCtx = loadIgnore(targetDir, { + noDefaults: options.defaultIgnore === false, + disableAll: options.ignore === false, + }); - // Get list of files to upload - const { readdirSync, statSync } = await import('node:fs'); + // Get list of files to upload + const { readdirSync, statSync } = await import('node:fs'); - const filesToUpload: { path: string; relativePath: string }[] = []; - const filesIgnored: string[] = []; + const filesToUpload: { path: string; relativePath: string }[] = []; + const filesIgnored: string[] = []; - function scanDir(currentDir: string, relativeBase: string = '') { - const entries = readdirSync(currentDir, { withFileTypes: true }); - // Pre-compute sibling .tex set for the PDF special rule. - const texSiblings = buildTexSiblingSet( - entries.filter((e) => !e.isDirectory()).map((e) => e.name), - ); + function scanDir(currentDir: string, relativeBase: string = '') { + const entries = readdirSync(currentDir, { withFileTypes: true }); + // Pre-compute sibling .tex set for the PDF special rule. + const texSiblings = buildTexSiblingSet( + entries.filter((e) => !e.isDirectory()).map((e) => e.name), + ); for (const entry of entries) { // Skip hidden files and .olcli.json (always — predates ignore subsystem) if (entry.name.startsWith('.')) continue; @@ -791,189 +841,189 @@ program } } } - } + } - scanDir(targetDir); + scanDir(targetDir); - if (options.showIgnored && filesIgnored.length > 0) { - spinner.stop(); - console.log(chalk.bold(chalk.dim(`Ignored ${filesIgnored.length} file(s)/dir(s):`))); - for (const p of filesIgnored) { - console.log(chalk.dim(` ${p}`)); - } - spinner.start('Scanning files...'); + if (options.showIgnored && filesIgnored.length > 0) { + spinner.stop(); + console.log(chalk.bold(chalk.dim(`Ignored ${filesIgnored.length} file(s)/dir(s):`))); + for (const p of filesIgnored) { + console.log(chalk.dim(` ${p}`)); } + spinner.start('Scanning files...'); + } - if (filesToUpload.length === 0) { - spinner.info('No files to upload'); - return; - } + if (filesToUpload.length === 0) { + spinner.info('No files to upload'); + return; + } - if (options.dryRun) { - spinner.stop(); - console.log(chalk.bold(`Would upload ${filesToUpload.length} file(s) to "${projectName}":`)); - for (const f of filesToUpload) { - console.log(` ${chalk.cyan(f.relativePath)}`); - } - return; + if (options.dryRun) { + spinner.stop(); + console.log(chalk.bold(`Would upload ${filesToUpload.length} file(s) to "${projectName}":`)); + for (const f of filesToUpload) { + console.log(` ${chalk.cyan(f.relativePath)}`); } + return; + } - // If --probe-folder is set, or if we don't have a cached rootFolderId, try probing - if (options.probeFolder && !rootFolderId) { - spinner.text = 'Probing for correct folder ID...'; - rootFolderId = await client.probeRootFolderId(projectId!) ?? undefined; - if (rootFolderId) { - // Save the discovered folder ID - if (existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - meta.rootFolderId = rootFolderId; - writeFileSync(metaPath, JSON.stringify(meta, null, 2)); - } - spinner.succeed(`Found root folder ID: ${rootFolderId}`); - spinner.start(`Uploading ${filesToUpload.length} file(s)...`); - } else { - spinner.fail('Could not find valid root folder ID'); - console.log(chalk.yellow('Try manually specifying rootFolderId in .olcli.json')); - process.exit(1); + // If --probe-folder is set, or if we don't have a cached rootFolderId, try probing + if (options.probeFolder && !rootFolderId) { + spinner.text = 'Probing for correct folder ID...'; + rootFolderId = await client.probeRootFolderId(projectId!) ?? undefined; + if (rootFolderId) { + // Save the discovered folder ID + if (existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + meta.rootFolderId = rootFolderId; + writeFileSync(metaPath, JSON.stringify(meta, null, 2)); } + spinner.succeed(`Found root folder ID: ${rootFolderId}`); + spinner.start(`Uploading ${filesToUpload.length} file(s)...`); + } else { + spinner.fail('Could not find valid root folder ID'); + console.log(chalk.yellow('Try manually specifying rootFolderId in .olcli.json')); + process.exit(1); } + } - // Fetch folder tree once so uploads go into correct subfolders - spinner.text = 'Resolving folder structure...'; - let folderTree = await client.getFolderTreeFromSocket(projectId!); - if (!folderTree) { - // Fallback: build minimal tree with just root - const resolvedRootId = rootFolderId || await client.getRootFolderId(projectId!); - folderTree = { '': resolvedRootId }; - } - - spinner.text = `Uploading ${filesToUpload.length} file(s)...`; - - let uploaded = 0; - let failed = 0; - let folderNotFoundCount = 0; + // Fetch folder tree once so uploads go into correct subfolders + spinner.text = 'Resolving folder structure...'; + let folderTree = await client.getFolderTreeFromSocket(projectId!); + if (!folderTree) { + // Fallback: build minimal tree with just root + const resolvedRootId = rootFolderId || await client.getRootFolderId(projectId!); + folderTree = { '': resolvedRootId }; + } - for (const file of filesToUpload) { - try { - const content = readFileSync(file.path); - await client.uploadFile(projectId!, rootFolderId || null, file.relativePath, content, folderTree); - uploaded++; - spinner.text = `Uploading... (${uploaded}/${filesToUpload.length})`; - } catch (error: any) { - console.error(chalk.yellow(`\n Warning: Failed to upload ${file.relativePath}: ${error.message}`)); - failed++; - if (error.message.includes('folder_not_found')) { - folderNotFoundCount++; - } + spinner.text = `Uploading ${filesToUpload.length} file(s)...`; + + let uploaded = 0; + let failed = 0; + let folderNotFoundCount = 0; + + for (const file of filesToUpload) { + try { + const content = readFileSync(file.path); + await client.uploadFile(projectId!, rootFolderId || null, file.relativePath, content, folderTree); + uploaded++; + spinner.text = `Uploading... (${uploaded}/${filesToUpload.length})`; + } catch (error: any) { + console.error(chalk.yellow(`\n Warning: Failed to upload ${file.relativePath}: ${error.message}`)); + failed++; + if (error.message.includes('folder_not_found')) { + folderNotFoundCount++; } } + } - // Update last push time - if (existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - meta.lastPush = new Date().toISOString(); - writeFileSync(metaPath, JSON.stringify(meta, null, 2)); - } + // Update last push time + if (existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + meta.lastPush = new Date().toISOString(); + writeFileSync(metaPath, JSON.stringify(meta, null, 2)); + } - if (failed > 0) { - spinner.warn(`Uploaded ${uploaded} file(s), ${failed} failed`); - if (folderNotFoundCount > 0 && !rootFolderId) { - console.log(chalk.yellow(' Tip: Try running with --probe-folder to find the correct folder ID')); - } - } else { - spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`); + if (failed > 0) { + spinner.warn(`Uploaded ${uploaded} file(s), ${failed} failed`); + if (folderNotFoundCount > 0 && !rootFolderId) { + console.log(chalk.yellow(' Tip: Try running with --probe-folder to find the correct folder ID')); } - - setLastProject(projectId!); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); + } else { + spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`); } - }); + + setLastProject(projectId!); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); program - .command('sync [dir]') - .description('Pull then push (bidirectional sync, propagates local deletions)') - .option('--project ', 'Project name or ID') - .option('--verbose', 'Show detailed file operations') - .option('--no-delete', 'Do not propagate local deletions to the remote (safer)') - .option('--dry-run', 'Show what would change without applying') - .option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)') - .option('--no-ignore', 'Disable all ignore filtering (escape hatch — uploads everything)') - .option('--show-ignored', 'Print files skipped by ignore rules') - .option('--cookie ', 'Session cookie override') - .action(async (dir, options) => { - const targetDir = dir || '.'; - - // Check if this is an existing project directory - const metaPath = join(targetDir, '.olcli.json'); - let projectId: string | undefined; - let projectName: string | undefined; +.command('sync [dir]') +.description('Pull then push (bidirectional sync, propagates local deletions)') +.option('--project ', 'Project name or ID') +.option('--verbose', 'Show detailed file operations') +.option('--no-delete', 'Do not propagate local deletions to the remote (safer)') +.option('--dry-run', 'Show what would change without applying') +.option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)') +.option('--no-ignore', 'Disable all ignore filtering (escape hatch — uploads everything)') +.option('--show-ignored', 'Print files skipped by ignore rules') +.option('--cookie ', 'Session cookie override') +.action(async (dir, options) => { + const targetDir = dir || '.'; + + // Check if this is an existing project directory + const metaPath = join(targetDir, '.olcli.json'); + let projectId: string | undefined; + let projectName: string | undefined; - if (existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - projectId = meta.projectId; - projectName = meta.projectName; - } + if (existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + projectId = meta.projectId; + projectName = meta.projectName; + } - if (options.project) { - projectName = options.project; - projectId = undefined; - } + if (options.project) { + projectName = options.project; + projectId = undefined; + } - if (!projectId && !projectName) { - console.error(chalk.red('No project specified.')); - console.error('Either run from a directory with .olcli.json or use --project'); - process.exit(1); - } + if (!projectId && !projectName) { + console.error(chalk.red('No project specified.')); + console.error('Either run from a directory with .olcli.json or use --project'); + process.exit(1); + } - const spinner = ora('Connecting...').start(); - try { - const client = await getClient(options.cookie); + const spinner = ora('Connecting...').start(); + try { + const client = await getClient(options.cookie); - // Resolve project - if (!projectId) { - let proj = await client.getProjectById(projectName!); - if (!proj) { - proj = await client.getProject(projectName!); - } - if (!proj) { - spinner.fail(`Project not found: ${projectName}`); - process.exit(1); - } - projectId = proj.id; - projectName = proj.name; + // Resolve project + if (!projectId) { + let proj = await client.getProjectById(projectName!); + if (!proj) { + proj = await client.getProject(projectName!); + } + if (!proj) { + spinner.fail(`Project not found: ${projectName}`); + process.exit(1); } + projectId = proj.id; + projectName = proj.name; + } - // Step 1: Download current state - spinner.text = 'Downloading project...'; - const zipBuffer = await client.downloadProject(projectId); + // Step 1: Download current state + spinner.text = 'Downloading project...'; + const zipBuffer = await client.downloadProject(projectId); - const AdmZip = (await import('adm-zip')).default; - const zip = new AdmZip(zipBuffer); + const AdmZip = (await import('adm-zip')).default; + const zip = new AdmZip(zipBuffer); - // Create target directory - if (!existsSync(targetDir)) { - mkdirSync(targetDir, { recursive: true }); - } + // Create target directory + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } + + // Build ignore context (defaults + .olignore + .olignore.local) + const ignoreCtx = loadIgnore(targetDir, { + noDefaults: options.defaultIgnore === false, + disableAll: options.ignore === false, + }); - // Build ignore context (defaults + .olignore + .olignore.local) - const ignoreCtx = loadIgnore(targetDir, { - noDefaults: options.defaultIgnore === false, - disableAll: options.ignore === false, - }); - - // Track local modifications - const localFiles = new Map(); - const filesIgnored: string[] = []; - const { readdirSync, statSync } = await import('node:fs'); - - function scanLocalFiles(currentDir: string, relativeBase: string = '') { - if (!existsSync(currentDir)) return; - const entries = readdirSync(currentDir, { withFileTypes: true }); - const texSiblings = buildTexSiblingSet( - entries.filter((e) => !e.isDirectory()).map((e) => e.name), - ); + // Track local modifications + const localFiles = new Map(); + const filesIgnored: string[] = []; + const { readdirSync, statSync } = await import('node:fs'); + + function scanLocalFiles(currentDir: string, relativeBase: string = '') { + if (!existsSync(currentDir)) return; + const entries = readdirSync(currentDir, { withFileTypes: true }); + const texSiblings = buildTexSiblingSet( + entries.filter((e) => !e.isDirectory()).map((e) => e.name), + ); for (const entry of entries) { if (entry.name.startsWith('.')) continue; const fullPath = join(currentDir, entry.name); @@ -996,290 +1046,301 @@ program }); } } - } + } - // Read local files before overwriting - if (existsSync(metaPath)) { - scanLocalFiles(targetDir); + // Read local files before overwriting + if (existsSync(metaPath)) { + scanLocalFiles(targetDir); + } + + if (options.showIgnored && filesIgnored.length > 0) { + spinner.stop(); + console.log(chalk.bold(chalk.dim(`Ignored ${filesIgnored.length} local file(s)/dir(s):`))); + for (const p of filesIgnored) { + console.log(chalk.dim(` ${p}`)); } + spinner.start(); + } - if (options.showIgnored && filesIgnored.length > 0) { - spinner.stop(); - console.log(chalk.bold(chalk.dim(`Ignored ${filesIgnored.length} local file(s)/dir(s):`))); - for (const p of filesIgnored) { - console.log(chalk.dim(` ${p}`)); - } - spinner.start(); + // Extract remote files + const remoteFiles = new Map(); + for (const entry of zip.getEntries()) { + if (!entry.isDirectory) { + remoteFiles.set(entry.entryName, entry.getData()); } + } - // Extract remote files - const remoteFiles = new Map(); - for (const entry of zip.getEntries()) { - if (!entry.isDirectory) { - remoteFiles.set(entry.entryName, entry.getData()); - } + // Merge: local changes take precedence for files modified after last pull + let lastPull: Date | undefined; + let previousManifest: string[] = []; + if (existsSync(metaPath)) { + const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); + lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; + if (Array.isArray(meta.remoteManifest)) { + previousManifest = meta.remoteManifest as string[]; } + } - // Merge: local changes take precedence for files modified after last pull - let lastPull: Date | undefined; - let previousManifest: string[] = []; - if (existsSync(metaPath)) { - const meta = JSON.parse(readFileSync(metaPath, 'utf-8')); - lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined; - if (Array.isArray(meta.remoteManifest)) { - previousManifest = meta.remoteManifest as string[]; + const filesToUpload: { path: string; content: Buffer }[] = []; + const filesUpdatedLocally: string[] = []; + const filesKeptLocal: string[] = []; + const filesNewLocal: string[] = []; + const filesDeletedRemote: string[] = []; + const filesDeleteSkipped: { path: string; reason: string }[] = []; + + // Detect locally-deleted files: present in previous manifest, missing locally, + // still present on the remote. Propagate the deletion to the remote BEFORE + // we write remote contents back over the working tree (otherwise the file + // would be silently restored — the bug reported in #7). + // Conflict policy: if the project has no previous manifest yet (first sync), + // we cannot distinguish "never existed locally" from "deleted locally", so + // skip deletion propagation on the very first sync. + if (options.delete !== false && previousManifest.length > 0 && existsSync(metaPath)) { + const locallyDeleted: string[] = []; + for (const path of previousManifest) { + if (path === 'output.pdf' || path.endsWith('/output.pdf')) continue; + if (!localFiles.has(path) && remoteFiles.has(path)) { + locallyDeleted.push(path); } } - const filesToUpload: { path: string; content: Buffer }[] = []; - const filesUpdatedLocally: string[] = []; - const filesKeptLocal: string[] = []; - const filesNewLocal: string[] = []; - const filesDeletedRemote: string[] = []; - const filesDeleteSkipped: { path: string; reason: string }[] = []; - - // Detect locally-deleted files: present in previous manifest, missing locally, - // still present on the remote. Propagate the deletion to the remote BEFORE - // we write remote contents back over the working tree (otherwise the file - // would be silently restored — the bug reported in #7). - // Conflict policy: if the project has no previous manifest yet (first sync), - // we cannot distinguish "never existed locally" from "deleted locally", so - // skip deletion propagation on the very first sync. - if (options.delete !== false && previousManifest.length > 0 && existsSync(metaPath)) { - const locallyDeleted: string[] = []; - for (const path of previousManifest) { - if (path === 'output.pdf' || path.endsWith('/output.pdf')) continue; - if (!localFiles.has(path) && remoteFiles.has(path)) { - locallyDeleted.push(path); + if (locallyDeleted.length > 0) { + spinner.text = `Propagating ${locallyDeleted.length} local deletion(s) to remote...`; + for (const path of locallyDeleted) { + if (options.dryRun) { + filesDeletedRemote.push(path); + remoteFiles.delete(path); + continue; } - } - - if (locallyDeleted.length > 0) { - spinner.text = `Propagating ${locallyDeleted.length} local deletion(s) to remote...`; - for (const path of locallyDeleted) { - if (options.dryRun) { - filesDeletedRemote.push(path); - remoteFiles.delete(path); - continue; - } - try { - await client.deleteByPath(projectId, path); - filesDeletedRemote.push(path); - // Drop from remoteFiles so we don't re-extract it below - remoteFiles.delete(path); - } catch (err: any) { - filesDeleteSkipped.push({ path, reason: err.message || String(err) }); - } + try { + await client.deleteByPath(projectId, path); + filesDeletedRemote.push(path); + // Drop from remoteFiles so we don't re-extract it below + remoteFiles.delete(path); + } catch (err: any) { + filesDeleteSkipped.push({ path, reason: err.message || String(err) }); } } } + } - spinner.text = 'Comparing files...'; - - // Write remote files, but preserve local modifications - for (const [path, remoteContent] of remoteFiles) { - const filePath = join(targetDir, path); - const fileDir = dirname(filePath); - if (!existsSync(fileDir)) { - mkdirSync(fileDir, { recursive: true }); - } + spinner.text = 'Comparing files...'; - const localFile = localFiles.get(path); - if (localFile && lastPull && localFile.mtime > lastPull) { - // Local file was modified after last pull - keep local, queue for upload if different - if (!localFile.content.equals(remoteContent)) { - filesToUpload.push({ path, content: localFile.content }); - filesKeptLocal.push(path); - } - // Don't overwrite local file - } else { - // Write remote version - writeFileSync(filePath, remoteContent); - filesUpdatedLocally.push(path); - } + // Write remote files, but preserve local modifications + for (const [path, remoteContent] of remoteFiles) { + const filePath = join(targetDir, path); + const fileDir = dirname(filePath); + if (!existsSync(fileDir)) { + mkdirSync(fileDir, { recursive: true }); } - // Check for new local files (not in remote) - for (const [path, localFile] of localFiles) { - if (path === 'output.pdf' || path.endsWith('/output.pdf')) { - continue; - } - if (!remoteFiles.has(path)) { + const localFile = localFiles.get(path); + if (localFile && lastPull && localFile.mtime > lastPull) { + // Local file was modified after last pull - keep local, queue for upload if different + if (!localFile.content.equals(remoteContent)) { filesToUpload.push({ path, content: localFile.content }); - filesNewLocal.push(path); + filesKeptLocal.push(path); } + // Don't overwrite local file + } else { + // Write remote version + writeFileSync(filePath, remoteContent); + filesUpdatedLocally.push(path); } + } - // Upload local changes - if (filesToUpload.length > 0 && !options.dryRun) { - spinner.text = `Uploading ${filesToUpload.length} local change(s)...`; - for (const file of filesToUpload) { - await client.uploadFile(projectId, null, file.path, file.content); - } + // Check for new local files (not in remote) + for (const [path, localFile] of localFiles) { + if (path === 'output.pdf' || path.endsWith('/output.pdf')) { + continue; } - - // Refresh manifest of remote files post-sync (deletions out, new uploads in) - const newManifest = new Set(remoteFiles.keys()); - for (const f of filesToUpload) newManifest.add(f.path); - for (const p of filesDeletedRemote) newManifest.delete(p); - - // Update metadata - if (!options.dryRun) { - writeFileSync(metaPath, JSON.stringify({ - projectId, - projectName, - lastPull: new Date().toISOString(), - lastSync: new Date().toISOString(), - remoteManifest: Array.from(newManifest).sort() - }, null, 2)); + if (!remoteFiles.has(path)) { + filesToUpload.push({ path, content: localFile.content }); + filesNewLocal.push(path); } + } - if (options.dryRun) { - spinner.succeed(`Dry-run sync "${projectName}" (no changes applied)`); - } else { - spinner.succeed(`Synced "${projectName}"`); + // Upload local changes + if (filesToUpload.length > 0 && !options.dryRun) { + spinner.text = `Uploading ${filesToUpload.length} local change(s)...`; + for (const file of filesToUpload) { + await client.uploadFile(projectId, null, file.path, file.content); } + } + + // Refresh manifest of remote files post-sync (deletions out, new uploads in) + const newManifest = new Set(remoteFiles.keys()); + for (const f of filesToUpload) newManifest.add(f.path); + for (const p of filesDeletedRemote) newManifest.delete(p); + + // Update metadata + if (!options.dryRun) { + writeFileSync(metaPath, JSON.stringify({ + projectId, + projectName, + lastPull: new Date().toISOString(), + lastSync: new Date().toISOString(), + remoteManifest: Array.from(newManifest).sort() + }, null, 2)); + } + + if (options.dryRun) { + spinner.succeed(`Dry-run sync "${projectName}" (no changes applied)`); + } else { + spinner.succeed(`Synced "${projectName}"`); + } - // Summary - console.log(chalk.dim(` ↓ ${filesUpdatedLocally.length} pulled from remote`)); - console.log(chalk.dim(` ↑ ${filesToUpload.length} pushed to remote`)); + // Summary + console.log(chalk.dim(` ↓ ${filesUpdatedLocally.length} pulled from remote`)); + console.log(chalk.dim(` ↑ ${filesToUpload.length} pushed to remote`)); + if (filesDeletedRemote.length > 0) { + console.log(chalk.dim(` ✖ ${filesDeletedRemote.length} deleted on remote`)); + } + if (filesDeleteSkipped.length > 0) { + console.log(chalk.yellow(` ⚠ ${filesDeleteSkipped.length} deletion(s) failed (kept remote)`)); + } + + if (options.verbose) { if (filesDeletedRemote.length > 0) { - console.log(chalk.dim(` ✖ ${filesDeletedRemote.length} deleted on remote`)); + console.log(chalk.red('\n Deleted on remote (matched local deletion):')); + for (const f of filesDeletedRemote) { + console.log(chalk.dim(` ${f}`)); + } } if (filesDeleteSkipped.length > 0) { - console.log(chalk.yellow(` ⚠ ${filesDeleteSkipped.length} deletion(s) failed (kept remote)`)); - } - - if (options.verbose) { - if (filesDeletedRemote.length > 0) { - console.log(chalk.red('\n Deleted on remote (matched local deletion):')); - for (const f of filesDeletedRemote) { - console.log(chalk.dim(` ${f}`)); - } - } - if (filesDeleteSkipped.length > 0) { - console.log(chalk.yellow('\n Deletion skipped (will retry on next sync):')); - for (const { path, reason } of filesDeleteSkipped) { - console.log(chalk.dim(` ${path} — ${reason}`)); - } + console.log(chalk.yellow('\n Deletion skipped (will retry on next sync):')); + for (const { path, reason } of filesDeleteSkipped) { + console.log(chalk.dim(` ${path} — ${reason}`)); } - if (filesKeptLocal.length > 0) { - console.log(chalk.yellow('\n Local changes pushed (local was newer):')); - for (const f of filesKeptLocal) { - console.log(chalk.dim(` ${f}`)); - } + } + if (filesKeptLocal.length > 0) { + console.log(chalk.yellow('\n Local changes pushed (local was newer):')); + for (const f of filesKeptLocal) { + console.log(chalk.dim(` ${f}`)); } - if (filesNewLocal.length > 0) { - console.log(chalk.green('\n New local files pushed:')); - for (const f of filesNewLocal) { - console.log(chalk.dim(` ${f}`)); - } + } + if (filesNewLocal.length > 0) { + console.log(chalk.green('\n New local files pushed:')); + for (const f of filesNewLocal) { + console.log(chalk.dim(` ${f}`)); } } - - setLastProject(projectId); - } catch (error: any) { - spinner.fail(`Failed: ${error.message}`); - process.exit(1); } - }); + + setLastProject(projectId); + } catch (error: any) { + spinner.fail(`Failed: ${error.message}`); + process.exit(1); + } +}); // ───────────────────────────────────────────────────────────────────────────── // HELP // ───────────────────────────────────────────────────────────────────────────── const configCmd = program - .command('config') - .description('Manage olcli configuration'); +.command('config') +.description('Manage olcli configuration'); configCmd - .command('set-url ') - .description('Set the Overleaf instance base URL') - .action((url: string) => { - setBaseUrl(url); - console.log(chalk.green(`Base URL set to: ${url}`)); - }); +.command('set-url ') +.description('Set the Overleaf instance base URL') +.action((url: string) => { + setBaseUrl(url); + console.log(chalk.green(`Base URL set to: ${url}`)); +}); configCmd - .command('get-url') - .description('Get the current Overleaf instance base URL') - .action(() => { - console.log(getBaseUrl()); - }); +.command('get-url') +.description('Get the current Overleaf instance base URL') +.action(() => { + console.log(getBaseUrl()); +}); configCmd - .command('set-cookie-name ') - .description('Set the session cookie name (e.g. overleaf.sid for older instances)') - .action((name: string) => { - setSessionCookieName(name); - console.log(chalk.green(`Session cookie name set to: ${name}`)); - }); +.command('set-cookie-name ') +.description('Set the session cookie name (e.g. overleaf.sid for older instances)') +.action((name: string) => { + setSessionCookieName(name); + console.log(chalk.green(`Session cookie name set to: ${name}`)); +}); configCmd - .command('get-cookie-name') - .description('Get the current session cookie name') - .action(() => { - console.log(getSessionCookieName()); - }); +.command('get-cookie-name') +.description('Get the current session cookie name') +.action(() => { + console.log(getSessionCookieName()); +}); program - .command('ignored [dir]') - .description('Show ignore patterns currently in effect for a project directory') - .option('--no-default-ignore', 'Exclude built-in defaults from the listing') - .option('--no-ignore', 'Show what --no-ignore would do (lists nothing)') - .action((dir, options) => { - const targetDir = dir || '.'; - const ctx = loadIgnore(targetDir, { - noDefaults: options.defaultIgnore === false, - disableAll: options.ignore === false, - }); - if (!ctx.enabled) { - console.log(chalk.yellow('Ignore filtering is disabled (--no-ignore).')); - console.log(chalk.dim('Every local file would be uploaded.')); - return; - } - if (ctx.sources.length === 0) { - console.log(chalk.yellow('No ignore patterns active.')); - console.log(chalk.dim('Built-in defaults are disabled and no .olignore file was found.')); - return; - } - console.log(chalk.bold(`Ignore patterns in effect for ${targetDir}:`)); - console.log(chalk.dim('(later sources override earlier ones; ! prefix negates)')); - for (const src of ctx.sources) { - console.log(); - console.log(chalk.cyan(`── ${src.label} (${src.patterns.length}) ──`)); - for (const p of src.patterns) { - console.log(` ${p}`); - } - } +.command('ignored [dir]') +.description('Show ignore patterns currently in effect for a project directory') +.option('--no-default-ignore', 'Exclude built-in defaults from the listing') +.option('--no-ignore', 'Show what --no-ignore would do (lists nothing)') +.action((dir, options) => { + const targetDir = dir || '.'; + const ctx = loadIgnore(targetDir, { + noDefaults: options.defaultIgnore === false, + disableAll: options.ignore === false, + }); + if (!ctx.enabled) { + console.log(chalk.yellow('Ignore filtering is disabled (--no-ignore).')); + console.log(chalk.dim('Every local file would be uploaded.')); + return; + } + if (ctx.sources.length === 0) { + console.log(chalk.yellow('No ignore patterns active.')); + console.log(chalk.dim('Built-in defaults are disabled and no .olignore file was found.')); + return; + } + console.log(chalk.bold(`Ignore patterns in effect for ${targetDir}:`)); + console.log(chalk.dim('(later sources override earlier ones; ! prefix negates)')); + for (const src of ctx.sources) { console.log(); - console.log(chalk.dim(`Total: ${ctx.patterns.length} pattern(s)`)); - if (ctx.defaultsEnabled) { - console.log(chalk.dim('Note: *.pdf is also ignored when a same-named *.tex/.ltx exists in the same folder.')); + console.log(chalk.cyan(`── ${src.label} (${src.patterns.length}) ──`)); + for (const p of src.patterns) { + console.log(` ${p}`); } - }); + } + console.log(); + console.log(chalk.dim(`Total: ${ctx.patterns.length} pattern(s)`)); + if (ctx.defaultsEnabled) { + console.log(chalk.dim('Note: *.pdf is also ignored when a same-named *.tex/.ltx exists in the same folder.')); + } +}); program - .command('check') - .description('Show credential sources and config path') - .action(() => { - console.log(chalk.bold('Configuration:')); - console.log(` Config file: ${getConfigPath()}`); - console.log(); - - console.log(chalk.bold('Credential sources (in order):')); - console.log(' 1. OVERLEAF_SESSION environment variable'); - console.log(' 2. .olauth file in current directory'); - console.log(' 3. Global config file'); - console.log(); +.command('check') +.description('Show credential sources and config path') +.action(() => { + console.log(chalk.bold('Configuration:')); + console.log(` Config file: ${getConfigPath()}`); + console.log(); + + console.log(chalk.bold('Credential sources (in order):')); + console.log(' 1. OVERLEAF_SESSION environment variable'); + console.log(' 2. .olauth file in current directory'); + console.log(' 3. Global config file'); + console.log(); + + const baseUrl = (program.opts().baseUrl as string | undefined) || getBaseUrl(); + const cookie = getSession(baseUrl); + if (cookie) { + console.log(chalk.green('✓ Session cookie found')); + console.log(chalk.dim(` Value: ${cookie.substring(0, 20)}...`)); + } else { + console.log(chalk.yellow('✗ No session cookie found')); + } +}); - const cookie = getSessionCookie(); - if (cookie) { - console.log(chalk.green('✓ Session cookie found')); - console.log(chalk.dim(` Value: ${cookie.substring(0, 20)}...`)); - } else { - console.log(chalk.yellow('✗ No session cookie found')); - } - }); +program +.command('watch [mainFile]') +.description('Locally compile and watch LaTeX files on change') +.option('--docker', 'Use the official TeX Live Docker image (100% Overleaf identical)') +.action((mainFile, options) => { + // Default to main.tex if no file is provided + const targetFile = mainFile || 'main.tex'; + runWatch(targetFile, options.docker); +}); program.parse(process.argv); diff --git a/src/client.ts b/src/client.ts index 347a797..c1f33d4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,27 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as https from 'node:https'; import * as http from 'node:http'; +import { + getSession, + setSession, + getLastProject, + setLastProject, + getConfigPath, + saveOlAuth, + clearConfig, + getBaseUrl, + setBaseUrl, + getSessionCookieName, + setSessionCookieName +} from './config.js'; + +import vanillaPuppeteer from 'puppeteer'; +import { addExtra } from 'puppeteer-extra'; +import StealthPlugin from 'puppeteer-extra-plugin-stealth'; + +const puppeteer = addExtra(vanillaPuppeteer as any); + +puppeteer.use(StealthPlugin()); // Read version from package.json const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -23,8 +44,8 @@ export interface Project { id: string; name: string; lastUpdated: string; - lastUpdatedBy?: string; - owner?: { email: string; firstName?: string; lastName?: string }; + lastUpdatedBy?: {id?:string; email: string; firstName?: string; lastName?: string }; + owner?: {id?:string; email: string; firstName?: string; lastName?: string }; archived?: boolean; trashed?: boolean; } @@ -95,13 +116,31 @@ export class OverleafClient { return `${this.baseUrl}/project/${projectId}/compile?enable_pdf_caching=true`; } + /** + * Create client with a url + */ + static async fromUrl(baseUrl: string): Promise { + let cookie = getSession(baseUrl); + if(!cookie){ + cookie = await OverleafClient.loginViaBrowser(baseUrl); + setSession(baseUrl, cookie); + } + + if(!cookie){ + process.exit(1); + } + + const cookieName = getSessionCookieName(); + return OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName); + } + /** * Create client from session cookie string */ static async fromSessionCookie( sessionCookie: string, baseUrl: string = DEFAULT_BASE_URL, - cookieName: string = 'overleaf_session2' + cookieName: string = 'overleaf_session2' ): Promise { const cookies: Record = { [cookieName]: sessionCookie @@ -110,7 +149,7 @@ export class OverleafClient { // Fetch CSRF token from project page const initialHeaders: Record = { 'Cookie': Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; '), - 'User-Agent': USER_AGENT + 'User-Agent': USER_AGENT }; const bootstrapClient = new OverleafClient({ cookies, csrf: 'bootstrap', baseUrl }); const response = await bootstrapClient.httpRequest(`${baseUrl}/project`, { @@ -157,7 +196,20 @@ export class OverleafClient { // Update cookies if the bootstrap request added anything const updatedCookies = bootstrapClient.cookies; - return new OverleafClient({ cookies: updatedCookies, csrf, baseUrl }); + const client = new OverleafClient({ cookies: updatedCookies, csrf, baseUrl }); + if(true){ + //if(await client.verifySession()){ + return client; + }else{ + console.error("Cookie out of date, updating..."); + const newCookie = await OverleafClient.loginViaBrowser(baseUrl); + setSession(baseUrl, newCookie); + + if(!newCookie){ + process.exit(1); + } + return OverleafClient.fromSessionCookie(newCookie, baseUrl, cookieName); + } } private getCookieHeader(): string { @@ -281,6 +333,28 @@ export class OverleafClient { return doRequest(url, maxRedirects); } + /** + * Checks if the current session cookie is still valid + */ + async verifySession(): Promise { + try { + const response = await fetch(`${this.baseUrl}/project`, { + headers: this.getHeaders(), + redirect: 'follow' // Automatically follow redirects + }); + + // If Overleaf redirected us to the login page, the cookie is expired! + if (response.url.includes('/login')) { + return false; + } + + return response.ok; + } catch (error) { + // If the network fails entirely, assume the session check failed + return false; + } + } + /** * Get all projects (not archived, not trashed) */ @@ -346,16 +420,133 @@ export class OverleafClient { // Filter out archived and trashed return projectsData - .filter((p: any) => !p.archived && !p.trashed) - .map((p: any) => ({ - id: p.id || p._id, - name: p.name, - lastUpdated: p.lastUpdated, - lastUpdatedBy: p.lastUpdatedBy, - owner: p.owner, - archived: p.archived, - trashed: p.trashed - })); + .filter((p: any) => !p.archived && !p.trashed) + .map((p: any) => ({ + id: p.id || p._id, + name: p.name, + lastUpdated: p.lastUpdated, + lastUpdatedBy: p.lastUpdatedBy, + owner: p.owner, + archived: p.archived, + trashed: p.trashed + })); + } + + static async loginViaBrowser(baseUrl: string): Promise { + console.error(`\n🌐 Opening browser to log into ${baseUrl}...`); + console.error(`Please log in. The window will close automatically when finished.\n`); + + // 1. Launch a visible browser window + const browser = await puppeteer.launch({ + headless: false, + defaultViewport: null, + args:[ + `--app=${baseUrl}/login`, // Opens directly to the login page without tabs! + '--window-size=800,800' + ] + }); + + // 2. Use the single existing page instead of creating a new one + const page = (await browser.pages())[0]; + + try { + // 1. Wait for the user to reach the dashboard + await page.waitForFunction( + "window.location.pathname.startsWith('/project')", + { timeout: 300000 } + ); + + // 2. Poll the Cookie Jar until the REAL authenticated cookie arrives! + let sessionCookie; + + // Check every 500ms for up to 10 seconds + while (!sessionCookie) { + //for (let i = 0; i < 20; i++) { + const cookies = await page.cookies(baseUrl); + + sessionCookie = cookies.find(c => (c.name === 'overleaf_session2' || c.name === 'sharelatex_session') && !c.value.startsWith('s%3Ac%3A1%3A')); + + // Wait half a second before checking again + await new Promise(resolve => setTimeout(resolve, 500)); + } + await browser.close(); + return sessionCookie.value; + + + } catch (err: any) { + await browser.close().catch(() => {}); // Ensure browser closes + throw new Error(`Login aborted or timed out: ${err.message}`); + } + } + + /** + * Fetch the all the version changes from history for a project + */ + async getUpdates(projectId: string): Promise { + const url = `${this.baseUrl}/project/${projectId}/updates`; + const response = await fetch(url, { headers: this.getHeaders(true) }); + if (!response.ok) return[]; + + const data = await response.json() as any; + return data.updates ||[]; + } + + /** + * Fetch the history labels (Saved Versions) for a project + */ + async getLabels(projectId: string): Promise { + const url = `${this.baseUrl}/project/${projectId}/labels`; + const response = await fetch(url, { + headers: this.getHeaders(true) + }); + + if (!response.ok) { + return[]; // Return empty array if no labels exist or endpoint fails + } + + const data = await response.json() as any; + return data ||[]; + } + + /** + * Download the project ZIP at a specific historical version + */ + async downloadHistoricalZip(projectId: string, version: number): Promise { + const url = `${this.baseUrl}/project/${projectId}/version/${version}/zip`; + return this.downloadBuffer(url); + } + + /** + * Apply a Label to the current overleaf state + */ + async applyOverleafLabel(projectId: string, message: string, version: number): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + + const url = `${this.baseUrl}/project/${projectId}/labels`; + + const response = await fetch(url, { + method: 'POST', + headers: this.getHeaders(true), + body: JSON.stringify({ + comment: message, + version: version + }) + }); + + if (!response.ok) { + throw new Error(`Failed to create label: ${response.status}`); + } + } + + /** + * Forces Overleaf to flush real-time edits to the database by pinging the download endpoint + */ + async forceSave(projectId: string): Promise { + const url = this.downloadUrl(projectId); + await fetch(url, { + method: 'HEAD', // Only fetches headers, 0 bytes of data! + headers: this.getHeaders() + }); } /** @@ -747,7 +938,7 @@ export class OverleafClient { if (!sid) return null; const buildPollUrl = () => - `${this.baseUrl}/socket.io/1/xhr-polling/${sid}?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`; + `${this.baseUrl}/socket.io/1/xhr-polling/${sid}?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`; let discoveredRootFolderId: string | null = null; @@ -850,7 +1041,7 @@ export class OverleafClient { if (!sid) return null; const buildPollUrl = () => - `${this.baseUrl}/socket.io/1/xhr-polling/${sid}?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`; + `${this.baseUrl}/socket.io/1/xhr-polling/${sid}?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`; for (let attempt = 0; attempt < 3; attempt++) { const pollResponse = await this.httpRequest(buildPollUrl(), { @@ -992,13 +1183,13 @@ export class OverleafClient { */ async probeRootFolderId(projectId: string): Promise { const candidates: string[] = []; - + // Method 1: Try projectId - 1 (most common) candidates.push(this.computeRootFolderId(projectId)); - + const prefix = projectId.slice(0, 16); const suffix = parseInt(projectId.slice(16), 16); - + // Method 2: Try a wide range around the project ID // Some projects have root folder created with different offsets for (let i = 2; i <= 50; i++) { @@ -1229,7 +1420,7 @@ export class OverleafClient { const projectInfo = await this.getProjectInfo(projectId); const normalizedTarget = targetPath.replace(/^\//, ''); - function searchFolder(folder: FolderEntry, currentPath: string): { id: string; type: 'doc' | 'file' | 'folder'; name: string } | null { + function searchFolder(folder: FolderEntry, currentPath: string): { id: string; type: 'doc' | 'file' | 'folder'; name: string } | null { // Check docs for (const doc of folder.docs || []) { const docPath = currentPath ? `${currentPath}/${doc.name}` : doc.name; @@ -1342,39 +1533,39 @@ export class OverleafClient { async downloadByPath(projectId: string, path: string): Promise { const normalizedPath = path.replace(/^\//, ''); - // First check if file exists - const entities = await this.getEntities(projectId); - const entityExists = entities.find(e => - e.path.replace(/^\//, '') === normalizedPath || - e.path === `/${normalizedPath}` - ); - - if (!entityExists) { - throw new Error(`File not found: ${path}`); - } - - // Try to find entity with ID for direct download - try { - const entity = await this.findEntityByPath(projectId, path); - if (entity && entity.type !== 'folder') { - return await this.downloadFile(projectId, entity.id, entity.type); - } - } catch (e) { - // Fall through to zip method - } - - // Fallback: download zip and extract the file - const zipBuffer = await this.downloadProject(projectId); - const AdmZip = (await import('adm-zip')).default; - const zip = new AdmZip(zipBuffer); - - for (const entry of zip.getEntries()) { - if (entry.entryName === normalizedPath || entry.entryName === path) { - return entry.getData(); - } - } - - throw new Error(`File not found in archive: ${path}`); + // First check if file exists + const entities = await this.getEntities(projectId); + const entityExists = entities.find(e => + e.path.replace(/^\//, '') === normalizedPath || + e.path === `/${normalizedPath}` + ); + + if (!entityExists) { + throw new Error(`File not found: ${path}`); + } + + // Try to find entity with ID for direct download + try { + const entity = await this.findEntityByPath(projectId, path); + if (entity && entity.type !== 'folder') { + return await this.downloadFile(projectId, entity.id, entity.type); + } + } catch (e) { + // Fall through to zip method + } + + // Fallback: download zip and extract the file + const zipBuffer = await this.downloadProject(projectId); + const AdmZip = (await import('adm-zip')).default; + const zip = new AdmZip(zipBuffer); + + for (const entry of zip.getEntries()) { + if (entry.entryName === normalizedPath || entry.entryName === path) { + return entry.getData(); + } + } + + throw new Error(`File not found in archive: ${path}`); } /** diff --git a/src/config.ts b/src/config.ts index 07bb053..f653934 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,6 +8,7 @@ import { join } from 'node:path'; import { homedir } from 'node:os'; interface OlcliConfig { + sessions?: Record; sessionCookie?: string; csrf?: string; lastProject?: string; @@ -18,6 +19,7 @@ interface OlcliConfig { const config = new Conf({ projectName: 'olcli', schema: { + sessions: {}, sessionCookie: { type: 'string' }, csrf: { type: 'string' }, lastProject: { type: 'string' }, @@ -42,7 +44,7 @@ export function setSessionCookieName(name: string): void { config.set('sessionCookieName', name); } -export function getSessionCookie(): string | undefined { +export function getSession(baseUrl: string): string | undefined { // Check environment variable first if (process.env.OVERLEAF_SESSION) { return process.env.OVERLEAF_SESSION; @@ -69,10 +71,17 @@ export function getSessionCookie(): string | undefined { } // Check global config - return config.get('sessionCookie'); + //const normalizedUrl = baseUrl.replace(/\/$/, ''); + const sessions = config.get('sessions') || {}; + return sessions[baseUrl || 'https://www.overleaf.com']; + //return config.get('sessionCookie'); } -export function setSessionCookie(cookie: string): void { +export function setSession(baseUrl:string, cookie: string): void { + const sessions = config.get('sessions') || {}; + + sessions[baseUrl] = cookie; + config.set('sessions', sessions); config.set('sessionCookie', cookie); } diff --git a/src/git-helper.ts b/src/git-helper.ts new file mode 100644 index 0000000..3a91ccf --- /dev/null +++ b/src/git-helper.ts @@ -0,0 +1,356 @@ +#!/usr/bin/env node +import * as readline from 'node:readline'; +import { mkdtempSync, rmSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, relative } from 'node:path'; +import AdmZip from 'adm-zip'; +import { execSync } from 'node:child_process'; +import { OverleafClient } from './client.js'; + +const remote = process.argv[2] || 'origin'; +const url = process.argv[3]; + + +class GitRemoteHelper { + private remote: string; + private projectId: string; + private baseUrl: string; + private prefix: string; + private client?: OverleafClient; + + constructor(remote: string, url: string) { + this.remote = remote; + this.prefix = `refs/overleaf/${remote}`; + const urlT = url.split('/'); + this.projectId = urlT[urlT.length -1]; + this.baseUrl = urlT[0] + "//" + urlT[2]; + } + + public async initClient(): Promise { + if (!this.client) { + this.client = await OverleafClient.fromUrl(this.baseUrl); + } + return this.client; + } + + public runCapabilities() { + console.log('import'); + console.log('push'); + console.log(`refspec refs/heads/*:${this.prefix}/*`); + console.log('option'); + console.log('list'); + console.log(''); + } + + public runOption(argv: string[]) {//TODO: Implement the options + console.log('ok'); + } + + public runList(argv: string[]) { + const isPushing = argv.includes('for-push'); + if (isPushing) { + const hash = this.getLocalCommitHash(`${this.prefix}/main`); + if (hash) { + console.log(`${hash} refs/heads/main`); + } else { + console.log(`? refs/heads/main`); + } + } else { + console.log(`? refs/heads/main`); + } + console.log(`@refs/heads/main HEAD`); + console.log(''); + } + + public async runImport(refsToImport: string[]) { + let tempDir = ''; + try { + const client = await this.initClient(); + + await client.forceSave(this.projectId); //Force save state online + let project = await client.getProjectById(this.projectId); + if (!project) { + console.error(`\n[olcli] Error: Could not find project '${this.projectId}'`); + process.exit(1); + } + + const requestedRef = refsToImport[0] || 'refs/heads/main'; + const privateRef = requestedRef.replace('refs/heads/', `${this.prefix}/`); + + const overleafTime = Math.floor(new Date(project.lastUpdated).getTime() / 1000); + const lastSyncTime = this.getLastSyncTime(); + + if (lastSyncTime > 0 && overleafTime === lastSyncTime) { + const localHash = this.getLocalCommitHash(privateRef); + process.stdout.write(`feature done\n`); + process.stdout.write(`reset ${privateRef}\n`); + process.stdout.write(`from ${localHash}\n`); + process.stdout.write(`done\n`, () => console.log('')); + return; + } + + //TODO: Add with logs (options) + const zipBuffer = await client.downloadProject(this.projectId); + + tempDir = mkdtempSync(join(tmpdir(), 'overleaf-sync-')); + const zipPath = join(tempDir, 'project.zip'); + const extractDir = join(tempDir, 'extracted'); + + writeFileSync(zipPath, zipBuffer); + const zip = new AdmZip(zipPath); + zip.extractAllTo(extractDir, true); + + const files = this.getFilesRecursively(extractDir); + + const parentHash = this.getLocalCommitHash(privateRef); + + if (!files.some(f => f.endsWith('.gitignore')) && !parentHash) { + // Create the .gitignore file on disk inside the extracted directory + const gitignorePath = join(extractDir, '.gitignore'); + + // Add the hidden .aux folder to it + writeFileSync(gitignorePath, '.aux/\n.build/\n'); + + // Add it to our array so fast-import picks it up! + files.push(gitignorePath); + } + + const commitMsg = "Sync from Overleaf\n"; + + let streamData = ''; + streamData += `feature done\n`; + streamData += `commit ${privateRef}\n`; + streamData += `mark :1\n`; + if(!project.lastUpdatedBy){ + streamData += `author Overleaf Sync ${overleafTime} +0000\n`; + streamData += `committer Overleaf Sync ${overleafTime} +0000\n`; + }else{ + streamData += `author ${project.lastUpdatedBy.firstName} ${project.lastUpdatedBy.lastName} <${project.lastUpdatedBy.email}> ${overleafTime} +0000\n`; + streamData += `committer ${project.lastUpdatedBy.firstName} ${project.lastUpdatedBy.lastName} <${project.lastUpdatedBy.email}> ${overleafTime} +0000\n`; + } + streamData += `data ${Buffer.byteLength(commitMsg, 'utf8')}\n`; + streamData += commitMsg; + + if (parentHash) { + streamData += `from ${parentHash}\n`; + } + + process.stdout.write(streamData); + + for (const filePath of files) { + let repoPath = relative(extractDir, filePath).replace(/\\/g, '/'); + repoPath = repoPath.replace(/^\/+/, '').replace(/^\.\//, ''); + const formattedPath = repoPath.includes(' ') ? `"${repoPath}"` : repoPath; + const content = readFileSync(filePath); + + process.stdout.write(`M 100644 inline ${formattedPath}\n`); + process.stdout.write(`data ${content.length}\n`); + process.stdout.write(content); + process.stdout.write(`\n`); + } + + process.stdout.write(`done\n`, () => {console.log(''); + this.setLastSyncTime(overleafTime)}); + + } catch (error: any) { + console.error(`\n[olcli] Error importing from Overleaf: ${error.message}`); + process.exit(1); + } finally { + if (tempDir) rmSync(tempDir, { recursive: true, force: true }); + } + } + + public async runPush(refsToPush: string[]) { + const [localRef, remoteRef] = refsToPush[0].split(':'); + const privateRef = remoteRef.replace('refs/heads/', `${this.prefix}/`); + + try { + const client = await this.initClient(); + await client.forceSave(this.projectId); + let project = await client.getProjectById(this.projectId); + if (!project) { + console.error(`error ${remoteRef} Project not found`); + return; + } + + const overleafTime = Math.floor(new Date(project.lastUpdated).getTime() / 1000); + const lastSyncTime = this.getLastSyncTime(); + + if (lastSyncTime > 0 && overleafTime > lastSyncTime) { + console.log(`error ${remoteRef} fetch first`); + console.log(''); + return; + } + + let commitsStr = ''; + try { + commitsStr = execSync(`git rev-list --reverse ${privateRef}..${localRef}`, { stdio:['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim(); + } catch { + commitsStr = execSync(`git rev-list --reverse ${localRef}`, { encoding: 'utf8' }).trim(); + } + + if (!commitsStr) { + console.log(`ok ${remoteRef}`); + console.log(''); + return; + } + + const commits = commitsStr.split('\n'); + + const remoteFiles = new Map(); + const projectInfo = await client.getProjectInfo(this.projectId); + if (projectInfo?.rootFolder?.[0]) { + const buildFileMap = (folder: any, currentPath: string = '') => { + for (const doc of folder.docs ||[]) remoteFiles.set(currentPath ? `${currentPath}/${doc.name}` : doc.name, { id: doc._id, type: 'doc' }); + for (const file of folder.fileRefs ||[]) remoteFiles.set(currentPath ? `${currentPath}/${file.name}` : file.name, { id: file._id, type: 'file' }); + for (const sub of folder.folders ||[]) { + const subPath = currentPath ? `${currentPath}/${sub.name}` : sub.name; + remoteFiles.set(subPath, { id: sub._id, type: 'folder' }); + buildFileMap(sub, subPath); + } + }; + buildFileMap(projectInfo.rootFolder[0]); + } + + let folderTree = await client.getFolderTreeFromSocket(this.projectId) || {}; + + for (const hash of commits) { + const commitMsg = execSync(`git show -s --format=%s ${hash}`, { encoding: 'utf8' }).trim(); + const uploadStr = execSync(`git diff-tree --no-commit-id --name-only --diff-filter=ACMR -r ${hash}`, { encoding: 'utf8' }).trim(); + const filesToUpload = uploadStr ? uploadStr.split('\n') :[]; + const deleteStr = execSync(`git diff-tree --no-commit-id --name-only --diff-filter=D -r ${hash}`, { encoding: 'utf8' }).trim(); + const filesToDelete = deleteStr ? deleteStr.split('\n') :[]; + + for (const file of filesToUpload) { + if (file === ".gitignore") continue; + const content = execSync(`git show ${hash}:"${file}"`, { encoding: 'buffer' }); + await client.uploadFile(this.projectId, null, file, content, folderTree); + } + + for (const file of filesToDelete) { + const entity = remoteFiles.get(file); + if (entity) { + await client.deleteEntity(this.projectId, entity.id, entity.type); + remoteFiles.delete(file); + } + } + } + + const folderEntries = Array.from(remoteFiles.entries()).filter(([_, e]) => e.type === 'folder'); + folderEntries.sort(([pathA], [pathB]) => pathB.length - pathA.length); + for (const[folderPath, entity] of folderEntries) { + const hasChildren = Array.from(remoteFiles.keys()).some(k => k.startsWith(folderPath + '/')); + if (!hasChildren) { + try { + await client.deleteEntity(this.projectId, entity.id, 'folder'); + remoteFiles.delete(folderPath); + } catch {} + } + } + + const updatedProject = await client.getProjectById(this.projectId); + if (updatedProject) { + const newOverleafTime = Math.floor(new Date(updatedProject.lastUpdated).getTime() / 1000); + this.setLastSyncTime(newOverleafTime); + } + + execSync(`git update-ref ${privateRef} ${localRef}`); + + console.log(`ok ${remoteRef}`); + console.log(''); + + } catch (error: any) { + console.log(`error ${remoteRef} Push failed: ${error.message}`); + console.log(''); + } + } + + private getFilesRecursively(dir: string, fileList: string[] =[]) { + const items = readdirSync(dir, { withFileTypes: true }); + for (const item of items) { + const fullPath = join(dir, item.name); + if (item.isDirectory()) { + this.getFilesRecursively(fullPath, fileList); + } else { + fileList.push(fullPath); + } + } + return fileList; + } + + private getLocalCommitHash(ref: string): string { + try { + return execSync(`git rev-parse ${ref}`, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim(); + } catch { + return ''; + } + } + + private getLastSyncTime(): number { + try { + return parseInt(execSync(`git config overleaf.${this.projectId}.lastsync`, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim(), 10); + } catch { + return 0; + } + } + + private setLastSyncTime(time: number): void { + execSync(`git config overleaf.${this.projectId}.lastsync ${time}`); + } +} + +async function main() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false + }); + + const helper = new GitRemoteHelper(remote, url); + + let pendingImports: string[] = []; + let pendingPushes: string[] =[]; + + for await (const line of rl) { + if (line === '') { + if (pendingImports.length > 0) { + await helper.runImport(pendingImports); + pendingImports =[]; + } else if (pendingPushes.length > 0) { + await helper.runPush(pendingPushes); + pendingPushes =[]; + } else { + process.exit(0); + } + continue; + } + + const [cmd, ...args] = line.split(' '); + + switch (cmd) { + case 'capabilities': + helper.runCapabilities(); + break; + case 'option': + helper.runOption(args); + break; + case 'list': + helper.runList(args); + break; + case 'import': + pendingImports.push(args[0]); + break; + case 'push': + pendingPushes.push(args[0]); + break; + case 'fetch': + console.error('Fetch not supported. Use import.'); + process.exit(1); + break; + default: + console.error(`[olcli] Unknown command: ${line}`); + } + } +} + +main(); diff --git a/test/e2e.sh b/test/e2e.sh index 9d01668..9f40640 100755 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -18,12 +18,26 @@ TESTS_PASSED=0 TESTS_FAILED=0 CLEANUP_FILES=() CLEANUP_REMOTE_FILES=() +EXE="$(pwd)/dist/cli.js" +GIT="git" + +if test -f $EXE; then + if ! [[ -x "$EXE" ]] + then + chmod +x $EXE + fi +else + echo "Binary file does not exist, compile first." + exit +fi + # Test project name (override with OLCLI_E2E_PROJECT_NAME) PROJECT_NAME="${OLCLI_E2E_PROJECT_NAME:-olcli test}" # Temporary directory for test files TEST_DIR=$(mktemp -d) +TEST_GIT_DIR=$(mktemp -d) trap cleanup EXIT ####################################### @@ -58,16 +72,16 @@ run_test() { local name="$1" local cmd="$2" local expect_success="${3:-true}" - + TESTS_RUN=$((TESTS_RUN + 1)) - + echo -n " Testing: $name ... " - + local output local exit_code - + output=$(eval "$cmd" 2>&1) && exit_code=0 || exit_code=$? - + if [ "$expect_success" = "true" ]; then if [ $exit_code -eq 0 ]; then echo -e "${GREEN}✓${NC}" @@ -103,16 +117,16 @@ run_test_with_output() { local name="$1" local cmd="$2" local expected_pattern="$3" - + TESTS_RUN=$((TESTS_RUN + 1)) - + echo -n " Testing: $name ... " - + local output local exit_code - + output=$(eval "$cmd" 2>&1) && exit_code=0 || exit_code=$? - + if [ $exit_code -eq 0 ] && echo "$output" | grep -qE "$expected_pattern"; then echo -e "${GREEN}✓${NC}" TESTS_PASSED=$((TESTS_PASSED + 1)) @@ -132,18 +146,18 @@ run_test_with_output() { # Cleanup function cleanup() { log_section "Cleanup" - + # Remove local temp files if [ -d "$TEST_DIR" ]; then log_info "Removing temp directory: $TEST_DIR" rm -rf "$TEST_DIR" fi - + # Remove remote test files (best effort) for file in "${CLEANUP_REMOTE_FILES[@]}"; do log_info "Note: Test file '$file' may remain on Overleaf (delete manually if needed)" done - + # Summary echo "" log_section "Test Results" @@ -152,7 +166,7 @@ cleanup() { echo -e " ${GREEN}Passed:${NC} $TESTS_PASSED" echo -e " ${RED}Failed:${NC} $TESTS_FAILED" echo "" - + if [ $TESTS_FAILED -eq 0 ]; then log_success "All tests passed! 🎉" exit 0 @@ -178,12 +192,12 @@ log_info "Test directory: $TEST_DIR" log_info "Project: $PROJECT_NAME" # Verify olcli is available -if ! command -v olcli &> /dev/null; then +if ! command -v $EXE &> /dev/null; then log_fail "olcli command not found. Run 'npm link' first." exit 1 fi -log_info "olcli version: $(olcli --version)" +log_info "olcli version: $($EXE --version)" ####################################### # Test: Authentication @@ -192,11 +206,11 @@ log_info "olcli version: $(olcli --version)" log_section "Authentication Tests" run_test_with_output "whoami returns user info" \ - "olcli whoami" \ + "$EXE whoami" \ "(Logged in as|Email:|Authenticated)" run_test "check shows config info" \ - "olcli check" + "$EXE check" ####################################### # Test: Project Listing @@ -205,17 +219,17 @@ run_test "check shows config info" \ log_section "Project Listing Tests" run_test "list shows target project" \ - "olcli list | grep -F \"$PROJECT_NAME\"" + "$EXE list | grep -F \"$PROJECT_NAME\"" run_test_with_output "list --json returns valid JSON" \ - "olcli list --json | jq -e 'type == \"array\"'" \ + "$EXE list --json | jq -e 'type == \"array\"'" \ "true" # Get project ID for later tests log_info "Waiting 5s before API calls to avoid rate limiting..." sleep 5 -PROJECT_ID=$(olcli list --json | jq -r --arg project_name "$PROJECT_NAME" '.[] | select(.name == $project_name) | .id') +PROJECT_ID=$($EXE list --json | jq -r --arg project_name "$PROJECT_NAME" '.[] | select(.name == $project_name) | .id') if [ -z "$PROJECT_ID" ]; then log_fail "Could not find '$PROJECT_NAME' project. Please create it on Overleaf first." exit 1 @@ -231,15 +245,15 @@ log_info "Using project ID directly to minimize API calls" log_section "Project Info Tests" run_test_with_output "info by name" \ - "olcli info '$PROJECT_NAME'" \ + "$EXE info '$PROJECT_NAME'" \ "(Project:|Files:)" run_test_with_output "info by ID" \ - "olcli info '$PROJECT_ID'" \ + "$EXE info '$PROJECT_ID'" \ "(Project:|Files:)" run_test_with_output "info --json returns valid JSON" \ - "olcli info '$PROJECT_ID' --json | jq -e '.project.id'" \ + "$EXE info '$PROJECT_ID' --json | jq -e '.project.id'" \ "$PROJECT_ID" ####################################### @@ -254,7 +268,7 @@ echo "$TEST_CONTENT" > "$TEST_FILE" CLEANUP_REMOTE_FILES+=("${TEST_ID}.txt") run_test "upload file to project" \ - "olcli upload '$TEST_FILE' '$PROJECT_ID'" + "$EXE upload '$TEST_FILE' '$PROJECT_ID'" # Create file in subfolder test TEST_FILE2="$TEST_DIR/${TEST_ID}_2.txt" @@ -262,7 +276,7 @@ echo "Second test file - $TEST_CONTENT" > "$TEST_FILE2" CLEANUP_REMOTE_FILES+=("${TEST_ID}_2.txt") run_test "upload second file" \ - "olcli upload '$TEST_FILE2' '$PROJECT_ID'" + "$EXE upload '$TEST_FILE2' '$PROJECT_ID'" ####################################### # Test: File Download (single file) @@ -273,7 +287,7 @@ log_section "File Download Tests" DOWNLOAD_FILE="$TEST_DIR/downloaded_${TEST_ID}.txt" run_test "download single file" \ - "olcli download '${TEST_ID}.txt' '$PROJECT_ID' -o '$DOWNLOAD_FILE'" + "$EXE download '${TEST_ID}.txt' '$PROJECT_ID' -o '$DOWNLOAD_FILE'" # Verify content matches TESTS_RUN=$((TESTS_RUN + 1)) @@ -299,7 +313,7 @@ sleep 1 # Rate limit # Download second uploaded file (project-agnostic check) DOWNLOAD_FILE2="$TEST_DIR/downloaded_${TEST_ID}_2.txt" run_test "download second uploaded file" \ - "olcli download '${TEST_ID}_2.txt' '$PROJECT_ID' -o '$DOWNLOAD_FILE2'" + "$EXE download '${TEST_ID}_2.txt' '$PROJECT_ID' -o '$DOWNLOAD_FILE2'" run_test_with_output "second downloaded content matches marker" \ "grep -F \"Second test file - $TEST_CONTENT\" '$DOWNLOAD_FILE2'" \ @@ -314,7 +328,7 @@ log_section "Zip Archive Tests" ZIP_FILE="$TEST_DIR/project.zip" run_test "download project as zip" \ - "olcli zip '$PROJECT_ID' -o '$ZIP_FILE'" + "$EXE zip '$PROJECT_ID' -o '$ZIP_FILE'" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: zip file is valid ... " @@ -346,7 +360,7 @@ fi log_section "Compile Tests" run_test_with_output "compile project" \ - "olcli compile '$PROJECT_ID'" \ + "$EXE compile '$PROJECT_ID'" \ "(success|failure|Compiled)" ####################################### @@ -360,7 +374,7 @@ PDF_FILE="$TEST_DIR/output.pdf" # Note: This may fail if compilation fails TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: download PDF ... " -if olcli pdf "$PROJECT_ID" -o "$PDF_FILE" 2>&1; then +if $EXE pdf "$PROJECT_ID" -o "$PDF_FILE" 2>&1; then if [ -f "$PDF_FILE" ] && [ -s "$PDF_FILE" ]; then # Check PDF magic bytes if head -c 4 "$PDF_FILE" | grep -q "%PDF"; then @@ -390,13 +404,13 @@ sleep 1 # Rate limit log_section "Output Files Tests" run_test_with_output "output --list shows files" \ - "olcli output --list --project '$PROJECT_ID'" \ + "$EXE output --list --project '$PROJECT_ID'" \ "(log|aux|pdf)" # Download log file LOG_FILE="$TEST_DIR/output.log" run_test "download log output" \ - "olcli output log -o '$LOG_FILE' --project '$PROJECT_ID'" + "$EXE output log -o '$LOG_FILE' --project '$PROJECT_ID'" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: log file has content ... " @@ -414,7 +428,7 @@ sleep 1 # Rate limit BBL_FILE="$TEST_DIR/output.bbl" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: download bbl output (optional) ... " -if olcli output bbl -o "$BBL_FILE" --project "$PROJECT_ID" > /dev/null 2>&1; then +if $EXE output bbl -o "$BBL_FILE" --project "$PROJECT_ID" > /dev/null 2>&1; then if [ -f "$BBL_FILE" ] && [ -s "$BBL_FILE" ]; then echo -e "${GREEN}✓${NC}" TESTS_PASSED=$((TESTS_PASSED + 1)) @@ -438,7 +452,7 @@ PULL_DIR="$TEST_DIR/pulled_project" mkdir -p "$PULL_DIR" run_test "pull project to directory" \ - "olcli pull '$PROJECT_ID' '$PULL_DIR' --force" + "$EXE pull '$PROJECT_ID' '$PULL_DIR' --force" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: .olcli.json created ... " @@ -489,16 +503,16 @@ sleep 1 touch "$PUSH_TEST_FILE" run_test "push --dry-run shows changes" \ - "cd '$PULL_DIR' && olcli push --dry-run" + "cd '$PULL_DIR' && $EXE push --dry-run" run_test "push uploads changes" \ - "cd '$PULL_DIR' && olcli push --all" + "cd '$PULL_DIR' && $EXE push --all" # Verify by downloading VERIFY_FILE="$TEST_DIR/verify_push.txt" sleep 2 # Give Overleaf a moment run_test "download pushed file" \ - "olcli download '${TEST_ID}_push.txt' '$PROJECT_ID' -o '$VERIFY_FILE'" + "$EXE download '${TEST_ID}_push.txt' '$PROJECT_ID' -o '$VERIFY_FILE'" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: pushed content matches ... " @@ -534,13 +548,13 @@ if [ -f "$PULL_DIR/.olcli.json" ]; then fi run_test "push recovers from stale rootFolderId" \ - "cd '$PULL_DIR' && olcli push" + "cd '$PULL_DIR' && $EXE push" # Verify recovery upload by downloading the new file VERIFY_RECOVER_FILE="$TEST_DIR/verify_push_recover.txt" sleep 2 # Give Overleaf a moment run_test "download recovered push file" \ - "olcli download '${TEST_ID}_push_recover.txt' '$PROJECT_ID' -o '$VERIFY_RECOVER_FILE'" + "$EXE download '${TEST_ID}_push_recover.txt' '$PROJECT_ID' -o '$VERIFY_RECOVER_FILE'" TESTS_RUN=$((TESTS_RUN + 1)) echo -n " Testing: recovered push content matches ... " @@ -573,7 +587,7 @@ mkdir -p "$SYNC_DIR" # Initial pull run_test "sync (initial pull)" \ - "olcli pull '$PROJECT_ID' '$SYNC_DIR' --force" + "$EXE pull '$PROJECT_ID' '$SYNC_DIR' --force" # Create local file SYNC_TEST_FILE="$SYNC_DIR/${TEST_ID}_sync.txt" @@ -582,13 +596,13 @@ echo "$SYNC_CONTENT" > "$SYNC_TEST_FILE" CLEANUP_REMOTE_FILES+=("${TEST_ID}_sync.txt") run_test "sync bidirectional" \ - "cd '$SYNC_DIR' && olcli sync" + "cd '$SYNC_DIR' && $EXE sync" # Verify upload SYNC_VERIFY="$TEST_DIR/verify_sync.txt" sleep 2 run_test "verify synced file exists" \ - "olcli download '${TEST_ID}_sync.txt' '$PROJECT_ID' -o '$SYNC_VERIFY'" + "$EXE download '${TEST_ID}_sync.txt' '$PROJECT_ID' -o '$SYNC_VERIFY'" ####################################### # Test: Delete + Rename CLI commands (re-enabled in v0.2.0) @@ -723,11 +737,11 @@ run_test "cleanup: delete --no-delete test file" \ log_section "Error Handling Tests" run_test "download nonexistent file fails gracefully" \ - "olcli download 'nonexistent_file_xyz.tex' '$PROJECT_ID'" \ + "$EXE download 'nonexistent_file_xyz.tex' '$PROJECT_ID'" \ false run_test "info for nonexistent project fails gracefully" \ - "olcli info 'project_that_does_not_exist_xyz'" \ + "$EXE info 'project_that_does_not_exist_xyz'" \ false ####################################### @@ -738,7 +752,7 @@ log_section "Edge Case Tests" # Project by ID run_test "commands work with project ID" \ - "olcli info '$PROJECT_ID'" + "$EXE info '$PROJECT_ID'" # Special characters in filename (safe ones only) SPECIAL_FILE="$TEST_DIR/test-file_123.txt" @@ -746,10 +760,43 @@ echo "special filename test" > "$SPECIAL_FILE" CLEANUP_REMOTE_FILES+=("test-file_123.txt") run_test "upload file with dashes and underscores" \ - "olcli upload '$SPECIAL_FILE' '$PROJECT_ID'" + "$EXE upload '$SPECIAL_FILE' '$PROJECT_ID'" run_test "download file with dashes and underscores" \ - "olcli download 'test-file_123.txt' '$PROJECT_ID' -o '$TEST_DIR/dl_special.txt'" + "$EXE download 'test-file_123.txt' '$PROJECT_ID' -o '$TEST_DIR/dl_special.txt'" + +####################################### +# Test: Git Integration +####################################### + +log_section "Git commands testing (local only)" + + +# Cloning +run_test "Cloning the test dir" \ + "$GIT clone overleaf::https://overleaf.com/project/$PROJECT_ID $TEST_GIT_DIR" +cd $TEST_GIT_DIR + + +# Create test file with unique content +TEST_FILE="$TEST_GIT_DIR/${TEST_ID}git.txt" +echo "$TEST_CONTENT" > "$TEST_FILE" +CLEANUP_REMOTE_FILES+=("${TEST_ID}git.txt") + +# Adding a first file +run_test "Cloning the test dir" \ + "$GIT add $TEST_FILE ; $GIT commit -m 'Added file1' ; git push" + + +# Create file in subfolder test +mkdir $TEST_GIT_DIR/subfolder +TEST_FILE2="$TEST_GIT_DIR/subfolder/${TEST_ID}_2.txt" +echo "Second test file - $TEST_CONTENT" > "$TEST_FILE2" +CLEANUP_REMOTE_FILES+=("${TEST_ID}_git2.txt") + +# Adding a second file +run_test "Cloning the test dir" \ + "$GIT add $TEST_FILE2 ; $GIT commit -m 'Added file2 in subfolder' ; git push" ####################################### # Cleanup Note