diff --git a/package-lock.json b/package-lock.json index a1227b7..7f109f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/archiver": "^8.0.0", "@types/node": "^22.19.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -35,6 +36,7 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", + "archiver": "^8.0.0", "electron": "^39.2.6", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", @@ -2986,6 +2988,17 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@types/archiver": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/readdir-glob": "*" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3154,6 +3167,16 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -3636,6 +3659,19 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -3883,6 +3919,85 @@ "node": ">=10" } }, + "node_modules/archiver": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "is-stream": "^4.0.0", + "lazystream": "^1.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^3.0.0", + "tar-stream": "^3.0.0", + "zip-stream": "^7.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archiver/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4148,6 +4263,21 @@ "dev": true, "license": "MIT" }, + "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==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -4158,6 +4288,91 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "dev": true, + "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.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "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.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4264,6 +4479,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4634,6 +4874,71 @@ "node": ">=0.10.0" } }, + "node_modules/compress-commons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz", + "integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^7.0.1", + "is-stream": "^4.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4655,6 +4960,81 @@ "dev": true, "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-7.0.1.tgz", + "integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/crc32-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -6123,6 +6503,36 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -6174,6 +6584,13 @@ "dev": true, "license": "Apache-2.0" }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -6927,6 +7344,27 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7326,6 +7764,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -7711,6 +8162,19 @@ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8509,6 +8973,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -9035,6 +9509,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9224,6 +9708,22 @@ "dev": true, "license": "MIT" }, + "node_modules/readdir-glob": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-3.0.0.tgz", + "integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/yqnn" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -9873,6 +10373,18 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -10145,6 +10657,19 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -10155,6 +10680,16 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -10219,6 +10754,16 @@ "node": ">= 10.0.0" } }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -11651,6 +12196,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", + "integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "compress-commons": "^7.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/zip-stream/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/zip-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 082d778..e90f965 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/archiver": "^8.0.0", "@types/node": "^22.19.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -63,6 +64,7 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", + "archiver": "^8.0.0", "electron": "^39.2.6", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", diff --git a/scripts/archive-directory.cjs b/scripts/archive-directory.cjs new file mode 100644 index 0000000..1283416 --- /dev/null +++ b/scripts/archive-directory.cjs @@ -0,0 +1,42 @@ +'use strict' + +const { ZipArchive } = require('archiver') +const { createWriteStream } = require('fs') +const { lstat, readlink, readdir } = require('fs/promises') +const { join, relative, sep } = require('path') + +function archiveName(root, value) { + return relative(root, value).split(sep).join('/') +} + +async function appendDirectory(archive, root, directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const absolute = join(directory, entry.name) + const name = archiveName(root, absolute) + const stats = await lstat(absolute) + if (stats.isSymbolicLink()) { + archive.symlink(name, await readlink(absolute), stats.mode) + } else if (stats.isDirectory()) { + archive.append('', { name: `${name}/` }) + await appendDirectory(archive, root, absolute) + } else if (stats.isFile()) { + archive.file(absolute, { name, mode: stats.mode }) + } + } +} + +async function archiveDirectory(source, output) { + const destination = createWriteStream(output) + const archive = new ZipArchive({ zlib: { level: 9 } }) + const completed = new Promise((resolve, reject) => { + destination.once('close', resolve) + destination.once('error', reject) + archive.once('error', reject) + }) + archive.pipe(destination) + await appendDirectory(archive, source, source) + await archive.finalize() + await completed +} + +module.exports = archiveDirectory diff --git a/scripts/package-connector.cjs b/scripts/package-connector.cjs index 2bc0ef6..0536876 100644 --- a/scripts/package-connector.cjs +++ b/scripts/package-connector.cjs @@ -2,7 +2,7 @@ const { mkdirSync, rmSync } = require('fs') const { join, resolve } = require('path') -const { spawnSync } = require('child_process') +const archiveDirectory = require('./archive-directory.cjs') const root = resolve(__dirname, '..') const source = join(root, 'packages', 'aurora-dockside-connector') @@ -10,10 +10,9 @@ const outputDir = join(root, 'dist', 'connectors') const output = join(outputDir, 'aurora-dockside-connector-0.1.0.zip') mkdirSync(outputDir, { recursive: true }) rmSync(output, { force: true }) -const result = spawnSync('zip', ['-qr', output, 'aurora-dockside-connector'], { - cwd: join(root, 'packages'), - stdio: 'inherit' -}) -if (result.error) throw result.error -if (result.status !== 0) throw new Error(`zip failed with exit code ${result.status}`) -process.stdout.write(`${output}\n`) +archiveDirectory(source, output) + .then(() => process.stdout.write(`${output}\n`)) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/scripts/package-modules.cjs b/scripts/package-modules.cjs index 0c0b9a9..f80d3c6 100644 --- a/scripts/package-modules.cjs +++ b/scripts/package-modules.cjs @@ -2,9 +2,9 @@ const { mkdirSync, readFileSync, readdirSync, rmSync } = require('fs') const { join, resolve } = require('path') -const { spawnSync } = require('child_process') +const archiveDirectory = require('./archive-directory.cjs') -function packageModules() { +async function packageModules() { const projectRoot = resolve(__dirname, '..') const packagesRoot = join(projectRoot, 'packages') const catalogRoot = join(projectRoot, 'dist', 'module-catalog') @@ -22,13 +22,15 @@ function packageModules() { if (!manifest.id || !manifest.version) throw new Error(`Invalid module manifest in ${source}`) const output = join(catalogRoot, `${manifest.id}-${manifest.version}.pac`) rmSync(output, { force: true }) - const result = spawnSync('zip', ['-qr', output, '.'], { cwd: source, stdio: 'inherit' }) - if (result.error) throw result.error - if (result.status !== 0) throw new Error(`zip failed for ${manifest.id} with exit code ${result.status}`) + await archiveDirectory(source, output) process.stdout.write(` • packaged module ${manifest.id}@${manifest.version} → ${output}\n`) } } module.exports = packageModules -if (require.main === module) packageModules() +if (require.main === module) + packageModules().catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/src/main/moduleRegistry.test.ts b/src/main/moduleRegistry.test.ts index b6d9a11..853d4df 100644 --- a/src/main/moduleRegistry.test.ts +++ b/src/main/moduleRegistry.test.ts @@ -1,33 +1,68 @@ -import { execFile } from 'child_process' +import { createRequire } from 'module' import { mkdtemp, mkdir, readFile, symlink, writeFile } from 'fs/promises' -import { promisify } from 'util' import { join, resolve } from 'path' import { tmpdir } from 'os' import { afterEach, describe, expect, it, vi } from 'vitest' vi.mock('electron', () => ({ app: { getPath: () => '/unused', getAppPath: () => '/unused/app' } })) -import { getAvailableModulePackages, getModuleRegistry, installModulePackage, uninstallModulePackage, validateModuleManifest } from './moduleRegistry' +import { + getAvailableModulePackages, + getModuleRegistry, + installModulePackage, + uninstallModulePackage, + validateModuleManifest +} from './moduleRegistry' const roots: string[] = [] -const execFileAsync = promisify(execFile) -async function temp(prefix: string): Promise { const root = await mkdtemp(join(tmpdir(), prefix)); roots.push(root); return root } -const manifest = { id: 'sample-app', name: 'Sample', version: '1.0.0', category: 'application', description: 'test', aurora: { core: '2.0.0-alpha.24', moduleApi: '1.0.0' }, dependencies: [], conflicts: [], settings: [] } -async function packageDir(value = manifest): Promise { const root = await temp('aurora-package-'); await writeFile(join(root, 'manifest.json'), JSON.stringify(value)); return root } +const archiveDirectory = createRequire(import.meta.url)('../../scripts/archive-directory.cjs') as ( + source: string, + output: string +) => Promise +async function temp(prefix: string): Promise { + const root = await mkdtemp(join(tmpdir(), prefix)) + roots.push(root) + return root +} +const manifest = { + id: 'sample-app', + name: 'Sample', + version: '1.0.0', + category: 'application', + description: 'test', + aurora: { core: '2.0.0-alpha.24', moduleApi: '1.0.0' }, + dependencies: [], + conflicts: [], + settings: [] +} +async function packageDir(value = manifest): Promise { + const root = await temp('aurora-package-') + await writeFile(join(root, 'manifest.json'), JSON.stringify(value)) + return root +} async function pacFile(value = manifest): Promise { const source = await packageDir(value) const output = join(await temp('aurora-pac-'), `${value.id}.pac`) - await execFileAsync('zip', ['-qr', output, '.'], { cwd: source }) + await archiveDirectory(source, output) return output } -afterEach(async () => { const { rm } = await import('fs/promises'); await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) }) +afterEach(async () => { + const { rm } = await import('fs/promises') + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) describe('external module registry', () => { - it('starts empty', async () => expect(await getModuleRegistry(await temp('aurora-user-'))).toEqual([])) + it('starts empty', async () => + expect(await getModuleRegistry(await temp('aurora-user-'))).toEqual([])) it('lists discoverable packages for the module library', async () => { - const catalog = await temp('aurora-catalog-'); const source = join(catalog, 'sample-app'); await mkdir(source); await writeFile(join(source, 'manifest.json'), JSON.stringify(manifest)) + const catalog = await temp('aurora-catalog-') + const source = join(catalog, 'sample-app') + await mkdir(source) + await writeFile(join(source, 'manifest.json'), JSON.stringify(manifest)) process.env.AURORA_MODULE_PATH = catalog - expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') + expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain( + 'sample-app' + ) delete process.env.AURORA_MODULE_PATH }) it('discovers and installs a compressed .pac package', async () => { @@ -36,7 +71,9 @@ describe('external module registry', () => { const catalogPac = join(catalog, 'sample-app.pac') await import('fs/promises').then(({ copyFile }) => copyFile(pac, catalogPac)) process.env.AURORA_MODULE_PATH = catalog - expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') + expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain( + 'sample-app' + ) delete process.env.AURORA_MODULE_PATH const userData = await temp('aurora-user-') const installed = await installModulePackage(catalogPac, userData) @@ -46,9 +83,13 @@ describe('external module registry', () => { it('discovers a .pac distributed beside the AppImage', async () => { const release = await temp('aurora-release-') const pac = await pacFile() - await import('fs/promises').then(({ copyFile }) => copyFile(pac, join(release, 'sample-app.pac'))) + await import('fs/promises').then(({ copyFile }) => + copyFile(pac, join(release, 'sample-app.pac')) + ) process.env.APPIMAGE = join(release, 'aurora-dockside.AppImage') - expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') + expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain( + 'sample-app' + ) delete process.env.APPIMAGE }) it('discovers a .pac embedded in the packaged application resources', async () => { @@ -56,31 +97,49 @@ describe('external module registry', () => { const catalog = join(resources, 'module-catalog') await mkdir(catalog) const pac = await pacFile() - await import('fs/promises').then(({ copyFile }) => copyFile(pac, join(catalog, 'sample-app.pac'))) + await import('fs/promises').then(({ copyFile }) => + copyFile(pac, join(catalog, 'sample-app.pac')) + ) const previous = Object.getOwnPropertyDescriptor(process, 'resourcesPath') Object.defineProperty(process, 'resourcesPath', { value: resources, configurable: true }) try { - expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') + expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain( + 'sample-app' + ) } finally { if (previous) Object.defineProperty(process, 'resourcesPath', previous) else Reflect.deleteProperty(process, 'resourcesPath') } }) it('installs a valid local package and refreshes after install', async () => { - const userData = await temp('aurora-user-'); const source = await packageDir() + const userData = await temp('aurora-user-') + const source = await packageDir() await installModulePackage(source, userData) expect((await getModuleRegistry(userData)).map((item) => item.id)).toEqual(['sample-app']) }) it('rejects invalid and incompatible manifests', () => { expect(() => validateModuleManifest({ ...manifest, id: '../escape' })).toThrow(/module id/) - expect(() => validateModuleManifest({ ...manifest, aurora: { core: '9.0.0', moduleApi: '1.0.0' } })).toThrow(/requires Aurora Core/) - expect(() => validateModuleManifest({ ...manifest, main: '../outside.cjs' })).toThrow(/may not leave/) - expect(() => validateModuleManifest({ ...manifest, dependencies: ['../escape'] })).toThrow(/module id array/) - expect(() => validateModuleManifest({ ...manifest, project: { adminPath: 'admin' } })).toThrow(/must start with/) - expect(() => validateModuleManifest({ ...manifest, creation: { phpVersions: ['latest'] } })).toThrow(/PHP minor versions/) + expect(() => + validateModuleManifest({ ...manifest, aurora: { core: '9.0.0', moduleApi: '1.0.0' } }) + ).toThrow(/requires Aurora Core/) + expect(() => validateModuleManifest({ ...manifest, main: '../outside.cjs' })).toThrow( + /may not leave/ + ) + expect(() => validateModuleManifest({ ...manifest, dependencies: ['../escape'] })).toThrow( + /module id array/ + ) + expect(() => validateModuleManifest({ ...manifest, project: { adminPath: 'admin' } })).toThrow( + /must start with/ + ) + expect(() => + validateModuleManifest({ ...manifest, creation: { phpVersions: ['latest'] } }) + ).toThrow(/PHP minor versions/) }) it('rejects symbolic links in package paths', async () => { - const userData = await temp('aurora-user-'); const source = await packageDir(); await mkdir(join(source, 'main')); await symlink('/tmp', join(source, 'main', 'escape')) + const userData = await temp('aurora-user-') + const source = await packageDir() + await mkdir(join(source, 'main')) + await symlink('/tmp', join(source, 'main', 'escape')) await expect(installModulePackage(source, userData)).rejects.toThrow(/symbolic links/) }) it('rejects symbolic links and traversal paths inside .pac archives', async () => { @@ -88,25 +147,37 @@ describe('external module registry', () => { const linkedSource = await packageDir() await symlink('/tmp', join(linkedSource, 'escape')) const linkedPac = join(await temp('aurora-pac-'), 'linked.pac') - await execFileAsync('zip', ['-qry', linkedPac, '.'], { cwd: linkedSource }) + await archiveDirectory(linkedSource, linkedPac) await expect(installModulePackage(linkedPac, userData)).rejects.toThrow(/symbolic links/) const traversalSource = await packageDir() await mkdir(join(traversalSource, 'xx')) await writeFile(join(traversalSource, 'xx', 'evil'), 'unsafe') const traversalPac = join(await temp('aurora-pac-'), 'traversal.pac') - await execFileAsync('zip', ['-qr', traversalPac, '.'], { cwd: traversalSource }) + await archiveDirectory(traversalSource, traversalPac) const archive = await readFile(traversalPac) const original = Buffer.from('xx/evil') const unsafe = Buffer.from('../evil') - for (let offset = archive.indexOf(original); offset !== -1; offset = archive.indexOf(original, offset + unsafe.length)) unsafe.copy(archive, offset) + for ( + let offset = archive.indexOf(original); + offset !== -1; + offset = archive.indexOf(original, offset + unsafe.length) + ) + unsafe.copy(archive, offset) await writeFile(traversalPac, archive) - await expect(installModulePackage(traversalPac, userData)).rejects.toThrow(/(?:Unsafe \.pac entry path|invalid relative path)/) + await expect(installModulePackage(traversalPac, userData)).rejects.toThrow( + /(?:Unsafe \.pac entry path|invalid relative path)/ + ) }) it('refreshes after uninstall without touching source', async () => { - const userData = await temp('aurora-user-'); const source = await packageDir(); await installModulePackage(source, userData); await uninstallModulePackage('sample-app', userData) + const userData = await temp('aurora-user-') + const source = await packageDir() + await installModulePackage(source, userData) + await uninstallModulePackage('sample-app', userData) expect(await getModuleRegistry(userData)).toEqual([]) - expect(await import('fs/promises').then(({ stat }) => stat(join(source, 'manifest.json')))).toBeTruthy() + expect( + await import('fs/promises').then(({ stat }) => stat(join(source, 'manifest.json'))) + ).toBeTruthy() }) it('updates an installed package atomically at the same registry id', async () => { @@ -131,7 +202,7 @@ describe('external module registry', () => { const actual = JSON.parse(await readFile(join(packageRoot, 'manifest.json'), 'utf8')) expect(validateModuleManifest(actual).id).toBe('wordpress') const archive = join(await temp('aurora-pac-'), 'wordpress.pac') - await execFileAsync('zip', ['-qr', archive, '.'], { cwd: packageRoot }) + await archiveDirectory(packageRoot, archive) const userData = await temp('aurora-user-') expect((await installModulePackage(archive, userData)).manifest.id).toBe('wordpress') expect((await getModuleRegistry(userData)).map((item) => item.id)).toEqual(['wordpress']) @@ -140,9 +211,12 @@ describe('external module registry', () => { it('accepts and packages the Drupal application module contract', async () => { const packageRoot = resolve(process.cwd(), 'packages/aurora-module-drupal') const actual = JSON.parse(await readFile(join(packageRoot, 'manifest.json'), 'utf8')) - expect(validateModuleManifest(actual)).toMatchObject({ id: 'drupal', defaults: { docroot: 'web' } }) + expect(validateModuleManifest(actual)).toMatchObject({ + id: 'drupal', + defaults: { docroot: 'web' } + }) const archive = join(await temp('aurora-pac-'), 'drupal.pac') - await execFileAsync('zip', ['-qr', archive, '.'], { cwd: packageRoot }) + await archiveDirectory(packageRoot, archive) const userData = await temp('aurora-user-') expect((await installModulePackage(archive, userData)).manifest.id).toBe('drupal') })