diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d9d7e6..024f934 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' - run: npm ci diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7a50ee0..9bdc4c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' - run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a501f9b..df3dc8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b25ba9..e3539ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' - run: npm ci diff --git a/package-lock.json b/package-lock.json index b511f98..5cb1a43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "1.0.0-prerelease.1", "license": "MIT", "dependencies": { - "0g": "^0.4.2", "commander": "^12.1.0", "execa": "^9.3.0", "fs-extra": "^11.2.0", + "gh-pages": "^6.3.0", "inquirer": "^9.3.5" }, "bin": { @@ -40,12 +40,6 @@ "typescript-eslint": "^8.56.1" } }, - "node_modules/@a-type/utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@a-type/utils/-/utils-1.1.0.tgz", - "integrity": "sha512-dwM1bl6XQaC/X9RZQUp0LDGQF70nn0QV6TFq/bKsjqlt8OEBGWlr++um4oigl2QMKkwzto9XwoBJ/D38IvROIQ==", - "license": "ISC" - }, "node_modules/@actions/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", @@ -126,7 +120,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1761,6 +1754,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -1777,7 +1805,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2728,7 +2755,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2819,7 +2845,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -3057,24 +3082,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/0g": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/0g/-/0g-0.4.2.tgz", - "integrity": "sha512-IIVL9Lttg8Oob9xEsGpZG0lYyzXFE3ViaAGAtGKVGM9xLRhyB/ZHEpI9cV1eGjeseIRVd3Y+Ycj3aEK+1CTTnw==", - "license": "MIT", - "dependencies": { - "@a-type/utils": "1.1.0", - "mnemonist": "0.39.8", - "shortid": "^2.2.16" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3217,6 +3230,21 @@ "dev": true, "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -3410,7 +3438,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3439,7 +3466,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3831,6 +3857,12 @@ "node": ">=18" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -4184,7 +4216,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -4256,6 +4287,12 @@ "dev": true, "license": "ISC" }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -4486,7 +4523,6 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -4825,6 +4861,34 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "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", @@ -4839,6 +4903,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4877,11 +4950,36 @@ "node": ">=16.0.0" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4890,11 +4988,42 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -5104,6 +5233,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/git-log-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz", @@ -5154,6 +5314,26 @@ "node": ">=10.13.0" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5331,7 +5511,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -5534,7 +5713,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5563,7 +5741,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5585,7 +5762,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5767,7 +5943,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7456,7 +7631,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -7642,7 +7816,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -7734,11 +7907,19 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -7796,15 +7977,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.1" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7833,24 +8005,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9876,7 +10030,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10001,12 +10154,6 @@ "node": ">=0.10.0" } }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10160,7 +10307,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -10173,7 +10319,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -10225,7 +10370,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10303,7 +10447,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10339,7 +10482,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10356,7 +10498,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10476,7 +10617,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -10613,6 +10753,26 @@ ], "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -10851,6 +11011,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -10860,6 +11030,29 @@ "node": ">=0.12.0" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -10901,7 +11094,6 @@ "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -12946,7 +13138,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13679,7 +13870,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13749,15 +13939,6 @@ "node": ">=8" } }, - "node_modules/shortid": { - "version": "2.2.17", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", - "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -13900,7 +14081,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14128,6 +14308,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/super-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", @@ -14395,7 +14596,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14414,7 +14614,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -14436,6 +14635,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -14585,7 +14805,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 9b70b91..b0292b3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ] }, "dependencies": { - "0g": "^0.4.2", + "gh-pages": "^6.3.0", "commander": "^12.1.0", "execa": "^9.3.0", "fs-extra": "^11.2.0", diff --git a/src/__tests__/gh-pages.test.ts b/src/__tests__/gh-pages.test.ts new file mode 100644 index 0000000..97088f8 --- /dev/null +++ b/src/__tests__/gh-pages.test.ts @@ -0,0 +1,283 @@ +jest.mock('fs-extra', () => ({ + __esModule: true, + default: { + pathExists: jest.fn(), + readJson: jest.fn(), + }, +})); + +jest.mock('execa', () => ({ + __esModule: true, + execa: jest.fn(), +})); + +jest.mock('gh-pages', () => ({ + __esModule: true, + default: { + publish: jest.fn(), + }, +})); + +jest.mock('../github.js', () => ({ + checkGhAuth: jest.fn().mockResolvedValue({ ok: false }), +})); + +import path from 'path'; +import fs from 'fs-extra'; +import { execa } from 'execa'; +import ghPages from 'gh-pages'; +import { runDeployToGitHubPages } from '../gh-pages.js'; + +const mockPathExists = fs.pathExists as jest.MockedFunction; +const mockReadJson = fs.readJson as jest.MockedFunction; +const mockExeca = execa as jest.MockedFunction; +const mockGhPagesPublish = ghPages.publish as jest.MockedFunction; + +const cwd = '/tmp/my-app'; + +/** Setup pathExists to return values based on path (avoids brittle call-order chains) */ +function setupPathExists(checks: Record) { + mockPathExists.mockImplementation((p: string) => { + for (const [key, value] of Object.entries(checks)) { + if (p.includes(key) || p === path.join(cwd, key)) return Promise.resolve(value); + } + return Promise.resolve(checks['*'] ?? false); + }); +} + +describe('runDeployToGitHubPages', () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + consoleLogSpy.mockRestore(); + }); + + it('throws when package.json does not exist', async () => { + mockPathExists.mockImplementation((p: string) => + Promise.resolve(path.join(cwd, 'package.json') !== p) + ); + + await expect(runDeployToGitHubPages(cwd)).rejects.toThrow( + 'No package.json found in this directory' + ); + expect(mockPathExists).toHaveBeenCalledWith(path.join(cwd, 'package.json')); + expect(mockExeca).not.toHaveBeenCalled(); + }); + + it('throws when git remote origin is not configured', async () => { + setupPathExists({ 'package.json': true }); + mockExeca.mockRejectedValue(new Error('not a git repo')); + + await expect(runDeployToGitHubPages(cwd)).rejects.toThrow( + 'Please save your changes first, before deploying to GitHub Pages.' + ); + expect(mockExeca).toHaveBeenCalledWith('git', ['remote', 'get-url', 'origin'], { + cwd, + reject: true, + }); + }); + + it('throws when no build script and skipBuild is false', async () => { + setupPathExists({ 'package.json': true }); + mockReadJson.mockResolvedValueOnce({ scripts: {} }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + + await expect(runDeployToGitHubPages(cwd, { skipBuild: false })).rejects.toThrow( + 'No "build" script found in package.json' + ); + expect(mockReadJson).toHaveBeenCalledWith(path.join(cwd, 'package.json')); + expect(mockExeca).toHaveBeenCalledTimes(1); // only git + }); + + it('runs build then deploys when skipBuild is false (npm)', async () => { + setupPathExists({ 'package.json': true, 'dist': true }); + mockReadJson.mockResolvedValueOnce({ + scripts: { build: 'webpack --config webpack.prod.js' }, + }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => cb(null)); + + await runDeployToGitHubPages(cwd, { skipBuild: false }); + + expect(mockExeca).toHaveBeenCalledTimes(2); // git, npm run build + expect(mockExeca).toHaveBeenNthCalledWith(2, 'npm', ['run', 'build'], { + cwd, + stdio: 'inherit', + }); + expect(mockGhPagesPublish).toHaveBeenCalledWith( + path.join(cwd, 'dist'), + { branch: 'gh-pages', repo: 'https://github.com/user/repo.git' }, + expect.any(Function) + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Running build') + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Deployed to GitHub Pages') + ); + }); + + it('uses yarn when yarn.lock exists', async () => { + setupPathExists({ 'package.json': true, 'yarn.lock': true, 'dist': true }); + mockReadJson.mockResolvedValueOnce({ + scripts: { build: 'webpack' }, + }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => cb(null)); + + await runDeployToGitHubPages(cwd, { skipBuild: false }); + + expect(mockExeca).toHaveBeenNthCalledWith(2, 'yarn', ['build'], { + cwd, + stdio: 'inherit', + }); + }); + + it('uses pnpm when pnpm-lock.yaml exists (and no yarn.lock)', async () => { + setupPathExists({ 'package.json': true, 'pnpm-lock.yaml': true, 'dist': true }); + mockReadJson.mockResolvedValueOnce({ + scripts: { build: 'vite build' }, + }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => cb(null)); + + await runDeployToGitHubPages(cwd, { skipBuild: false }); + + expect(mockExeca).toHaveBeenNthCalledWith(2, 'pnpm', ['build'], { + cwd, + stdio: 'inherit', + }); + }); + + it('skips build and deploys when skipBuild is true', async () => { + setupPathExists({ 'package.json': true, 'dist': true }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => cb(null)); + + await runDeployToGitHubPages(cwd, { skipBuild: true }); + + expect(mockReadJson).not.toHaveBeenCalled(); + expect(mockExeca).toHaveBeenCalledTimes(1); // git only + expect(mockGhPagesPublish).toHaveBeenCalledWith( + path.join(cwd, 'dist'), + { branch: 'gh-pages', repo: 'https://github.com/user/repo.git' }, + expect.any(Function) + ); + }); + + it('throws when dist directory does not exist (after build)', async () => { + setupPathExists({ 'package.json': true, 'dist': false }); + mockReadJson.mockResolvedValueOnce({ + scripts: { build: 'npm run build' }, + }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + + await expect(runDeployToGitHubPages(cwd, { skipBuild: false })).rejects.toThrow( + 'Build output directory "dist" does not exist' + ); + expect(mockExeca).toHaveBeenCalledTimes(2); // git, npm run build + }); + + it('throws when dist directory does not exist with skipBuild true', async () => { + setupPathExists({ 'package.json': true, 'dist': false }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + + await expect(runDeployToGitHubPages(cwd, { skipBuild: true })).rejects.toThrow( + 'Build output directory "dist" does not exist' + ); + expect(mockExeca).toHaveBeenCalledTimes(1); // git only + }); + + it('uses custom distDir and branch options', async () => { + setupPathExists({ 'package.json': true, 'build': true }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => cb(null)); + + await runDeployToGitHubPages(cwd, { + skipBuild: true, + distDir: 'build', + branch: 'pages', + }); + + expect(mockGhPagesPublish).toHaveBeenCalledWith( + path.join(cwd, 'build'), + { branch: 'pages', repo: 'https://github.com/user/repo.git' }, + expect.any(Function) + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Deploying "build" to GitHub Pages (branch: pages)') + ); + }); + + it('propagates build failure', async () => { + setupPathExists({ 'package.json': true }); + mockReadJson.mockResolvedValueOnce({ + scripts: { build: 'webpack' }, + }); + mockExeca + .mockResolvedValueOnce({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>) + .mockRejectedValueOnce(new Error('Build failed')); + + await expect(runDeployToGitHubPages(cwd, { skipBuild: false })).rejects.toThrow( + 'Build failed' + ); + expect(mockExeca).toHaveBeenCalledTimes(2); // git, npm run build + }); + + it('propagates gh-pages deploy failure', async () => { + setupPathExists({ 'package.json': true, 'dist': true }); + mockExeca.mockResolvedValue({ + stdout: 'https://github.com/user/repo.git', + stderr: '', + exitCode: 0, + } as Awaited>); + mockGhPagesPublish.mockImplementation((_dir, _opts, cb) => + cb(new Error('Deploy failed')) + ); + + await expect(runDeployToGitHubPages(cwd, { skipBuild: true })).rejects.toThrow( + 'Deploy failed' + ); + expect(mockExeca).toHaveBeenCalledTimes(1); // git only + }); +}); diff --git a/src/cli.ts b/src/cli.ts index b8a7116..ef501cf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,6 +10,7 @@ import { mergeTemplates } from './template-loader.js'; import { offerAndCreateGitHubRepo } from './github.js'; import { runSave } from './save.js'; import { runLoad } from './load.js'; +import { runDeployToGitHubPages } from './gh-pages.js'; /** Project data provided by the user */ type ProjectData = { @@ -304,4 +305,30 @@ program } }); +/** Command to deploy the React app to GitHub Pages */ +program + .command('deploy') + .description('Build the app and deploy it to GitHub Pages (uses gh-pages branch)') + .argument('[path]', 'Path to the project (defaults to current directory)') + .option('-d, --dist-dir ', 'Build output directory to deploy', 'dist') + .option('--no-build', 'Skip running the build step (deploy existing output only)') + .option('-b, --branch ', 'Git branch to deploy to', 'gh-pages') + .action(async (projectPath, options) => { + const cwd = projectPath ? path.resolve(projectPath) : process.cwd(); + try { + await runDeployToGitHubPages(cwd, { + distDir: options.distDir, + skipBuild: options.build === false, + branch: options.branch, + }); + } catch (error) { + if (error instanceof Error) { + console.error(`\nāŒ ${error.message}\n`); + } else { + console.error(error); + } + process.exit(1); + } + }); + program.parse(process.argv); \ No newline at end of file diff --git a/src/gh-pages.ts b/src/gh-pages.ts new file mode 100644 index 0000000..fc03698 --- /dev/null +++ b/src/gh-pages.ts @@ -0,0 +1,170 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { execa } from 'execa'; +import ghPages from 'gh-pages'; +import { checkGhAuth } from './github.js'; + +export type DeployOptions = { + /** Build output directory to deploy (e.g. dist, build) */ + distDir: string; + /** Skip running the build step */ + skipBuild: boolean; + /** Branch to push to (default gh-pages) */ + branch: string; +}; + +const DEFAULT_DIST_DIR = 'dist'; +const DEFAULT_BRANCH = 'gh-pages'; + +/** + * Parse owner and repo name from a Git remote URL. + * Supports https://github.com/owner/repo, https://github.com/owner/repo.git, git@github.com:owner/repo.git + */ +function parseRepoFromUrl(repoUrl: string): { owner: string; repo: string } | null { + const trimmed = repoUrl.trim().replace(/\.git$/, ''); + // git@github.com:owner/repo or https://github.com/owner/repo + const sshMatch = trimmed.match(/git@github\.com:([^/]+)\/([^/]+)/); + if (sshMatch?.[1] && sshMatch[2]) { + return { owner: sshMatch[1], repo: sshMatch[2] }; + } + const httpsMatch = trimmed.match(/github\.com[/:]([^/]+)\/([^/#?]+)/); + if (httpsMatch?.[1] && httpsMatch[2]) { + return { owner: httpsMatch[1], repo: httpsMatch[2] }; + } + return null; +} + +/** + * Ensure GitHub Pages is enabled for the repository, configured to use the given branch. + * Uses the GitHub API via `gh` CLI. No-op if gh is not authenticated or on failure. + */ +async function ensurePagesEnabled(owner: string, repo: string, branch: string): Promise { + const auth = await checkGhAuth(); + if (!auth.ok) return; + + const body = JSON.stringify({ source: { branch, path: '/' } }); + try { + const getResult = await execa('gh', ['api', `repos/${owner}/${repo}/pages`, '--jq', '.source.branch'], { + reject: false, + encoding: 'utf8', + }); + if (getResult.exitCode === 0) { + const currentBranch = getResult.stdout?.trim(); + if (currentBranch === branch) return; + await execa('gh', [ + 'api', + '-X', + 'PUT', + `repos/${owner}/${repo}/pages`, + '--input', + '-', + ], { input: body }); + console.log(` GitHub Pages source updated to branch "${branch}".`); + } else { + await execa('gh', [ + 'api', + '-X', + 'POST', + `repos/${owner}/${repo}/pages`, + '--input', + '-', + ], { input: body }); + console.log(` GitHub Pages enabled (source: branch "${branch}").`); + } + } catch { + // Best-effort: continue without enabling; user can enable manually + } +} + +/** + * Detect package manager from lock files. + */ +async function getPackageManager(cwd: string): Promise<'yarn' | 'pnpm' | 'npm'> { + if (await fs.pathExists(path.join(cwd, 'yarn.lock'))) return 'yarn'; + if (await fs.pathExists(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'; + return 'npm'; +} + +/** + * Run build script in the project (npm run build / yarn build / pnpm build). + */ +async function runBuild(cwd: string): Promise { + const pkgPath = path.join(cwd, 'package.json'); + const pkg = await fs.readJson(pkgPath); + const scripts = (pkg.scripts as Record) || {}; + if (!scripts['build']) { + throw new Error( + 'No "build" script found in package.json. Add a build script or use --no-build and deploy an existing folder with -d/--dist-dir.' + ); + } + + const pm = await getPackageManager(cwd); + const runCmd = pm === 'npm' ? 'npm' : pm === 'yarn' ? 'yarn' : 'pnpm'; + const args = pm === 'npm' ? ['run', 'build'] : ['build']; + console.log(`šŸ“¦ Running build (${runCmd} ${args.join(' ')})...`); + await execa(runCmd, args, { cwd, stdio: 'inherit' }); + console.log('āœ… Build completed.\n'); +} + +/** + * Deploy the built app to GitHub Pages using the gh-pages package. + * Builds the project first unless skipBuild is true, then publishes distDir to the gh-pages branch. + */ +export async function runDeployToGitHubPages( + projectPath: string, + options: Partial = {} +): Promise { + const distDir = options.distDir ?? DEFAULT_DIST_DIR; + const skipBuild = options.skipBuild ?? false; + const branch = options.branch ?? DEFAULT_BRANCH; + + const cwd = path.resolve(projectPath); + const pkgPath = path.join(cwd, 'package.json'); + + if (!(await fs.pathExists(pkgPath))) { + throw new Error( + 'No package.json found in this directory. Run this command from your project root (or pass the project path).' + ); + } + + let repoUrl: string; + try { + const { stdout } = await execa('git', ['remote', 'get-url', 'origin'], { + cwd, + reject: true, + }); + repoUrl = stdout.trim(); + } catch { + throw new Error( + 'Please save your changes first, before deploying to GitHub Pages.' + ); + } + + if (!skipBuild) { + await runBuild(cwd); + } + + const absoluteDist = path.join(cwd, distDir); + if (!(await fs.pathExists(absoluteDist))) { + throw new Error( + `Build output directory "${distDir}" does not exist. Run a build first or specify the correct directory with -d/--dist-dir.` + ); + } + + const parsed = parseRepoFromUrl(repoUrl); + if (parsed) { + await ensurePagesEnabled(parsed.owner, parsed.repo, branch); + } + + console.log(`šŸš€ Deploying "${distDir}" to GitHub Pages (branch: ${branch})...`); + await new Promise((resolve, reject) => { + ghPages.publish( + absoluteDist, + { branch, repo: repoUrl }, + (err) => (err ? reject(err) : resolve()) + ); + }); + console.log('\nāœ… Deployed to GitHub Pages.'); + console.log(' Enable GitHub Pages in your repo: Settings → Pages → Source: branch "' + branch + '".'); + console.log(' If the site is at username.github.io/, set your app\'s base path (e.g. base: \'//\' in Vite).\n'); +}