From d97c7c70eb54be10e24ffdb035cb861d81d5f1d0 Mon Sep 17 00:00:00 2001 From: Deniffer Date: Tue, 8 Sep 2026 23:41:03 +0800 Subject: [PATCH] feat(site): release portable site snapshots and technical seo audits in v0.2.0 --- .github/workflows/release.yml | 1 + bun.lock | 63 ++- package.json | 2 +- packages/gkit/docs/site.md | 146 +++++++ packages/gkit/package.json | 10 +- packages/gkit/scripts/test-site-snapshot.ts | 322 ++++++++++++++++ .../gkit/scripts/verify-package-artifact.ts | 59 ++- packages/gkit/src/args.ts | 13 + packages/gkit/src/cli.ts | 8 + packages/gkit/src/profile.ts | 3 + packages/gkit/src/schema.ts | 10 +- packages/gkit/src/site/audit.ts | 137 +++++++ packages/gkit/src/site/capture.ts | 359 ++++++++++++++++++ packages/gkit/src/site/commands.ts | 139 +++++++ packages/gkit/src/site/compare.ts | 157 ++++++++ packages/gkit/src/site/config.ts | 46 +++ packages/gkit/src/site/discover.test.ts | 206 ++++++++++ packages/gkit/src/site/discover.ts | 292 ++++++++++++++ packages/gkit/src/site/execute.ts | 234 ++++++++++++ packages/gkit/src/site/explain.ts | 204 ++++++++++ packages/gkit/src/site/git-context.ts | 188 +++++++++ packages/gkit/src/site/model.ts | 180 +++++++++ packages/gkit/src/site/schema.ts | 59 +++ packages/gkit/src/site/site.test.ts | 187 +++++++++ packages/gkit/src/site/snapshot.ts | 155 ++++++++ 25 files changed, 3161 insertions(+), 19 deletions(-) create mode 100644 packages/gkit/docs/site.md create mode 100644 packages/gkit/scripts/test-site-snapshot.ts create mode 100644 packages/gkit/src/site/audit.ts create mode 100644 packages/gkit/src/site/capture.ts create mode 100644 packages/gkit/src/site/commands.ts create mode 100644 packages/gkit/src/site/compare.ts create mode 100644 packages/gkit/src/site/config.ts create mode 100644 packages/gkit/src/site/discover.test.ts create mode 100644 packages/gkit/src/site/discover.ts create mode 100644 packages/gkit/src/site/execute.ts create mode 100644 packages/gkit/src/site/explain.ts create mode 100644 packages/gkit/src/site/git-context.ts create mode 100644 packages/gkit/src/site/model.ts create mode 100644 packages/gkit/src/site/schema.ts create mode 100644 packages/gkit/src/site/site.test.ts create mode 100644 packages/gkit/src/site/snapshot.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66201a7..31c7bb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -216,6 +216,7 @@ jobs: done [[ "$installed" == "true" ]] "$BUN_INSTALL_BIN/gkit" --schema gsc >/dev/null + "$BUN_INSTALL_BIN/gkit" --schema site >/dev/null "$BUN_INSTALL_BIN/gkit" describe --id gsc.properties.list \ | jq -e '.id == "gsc.properties.list"' >/dev/null docs_dir=$("$BUN_INSTALL_BIN/gkit" docs --provider gsc) diff --git a/bun.lock b/bun.lock index 544c0a2..fa1730b 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "packages/gkit": { "name": "gkit", - "version": "0.1.3", + "version": "0.2.0", "bin": { "gkit": "bin/gkit.js", }, @@ -22,9 +22,12 @@ "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", "ajv": "^8.20.0", - "argc": "github:ethan-huo/argc#v7.11.0", + "argc": "github:ethan-huo/argc#v7.11.1", + "cheerio": "^1.2.0", "dotenv": "^17.2.2", "google-auth-library": "^10.9.0", + "robots-parser": "3.0.1", + "saxes": "6.0.0", "valibot": "^1.2.0", "yaml": "^2.9.0", }, @@ -42,7 +45,7 @@ "@types/node": "^25.6.0", "@valibot/to-json-schema": "^1.6.0", "ajv": "^8.20.0", - "argc": "github:ethan-huo/argc#v7.11.0", + "argc": "github:ethan-huo/argc#v7.11.1", "bun-types": "^1.3.13", "dataforseo-client": "^2.0.25", "dotenv": "^17.2.2", @@ -169,7 +172,7 @@ "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "argc": ["@celados/argc@github:ethan-huo/argc#1463e0b", { "dependencies": { "oxc-parser": "0.137.0", "yaml": "2.9.0" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-1463e0b", "sha512-UnpEGhU0d22g7TG62Me7/8XU9ZmQOMziARXnDnxW+fiFlAYg2y8IJ8E7+LughYYDg//I4FlqVk3cDiGGHStx4A=="], + "argc": ["@celados/argc@github:ethan-huo/argc#5d52bd5", { "dependencies": { "oxc-parser": "0.137.0", "yaml": "2.9.0" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "ethan-huo-argc-5d52bd5", "sha512-ezA721dld0eCDsgTKcCDTaAzpBpst/li5SZL9i3s3i/j3wVuCpv8uiOa1+kNZY5FZDc2TJO+R/HD+EKVbjhVYg=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], @@ -177,24 +180,46 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], @@ -225,8 +250,12 @@ "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -269,10 +298,18 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], "oxc-parser": ["oxc-parser@0.137.0", "", { "dependencies": { "@oxc-project/types": "^0.137.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.137.0", "@oxc-parser/binding-android-arm64": "0.137.0", "@oxc-parser/binding-darwin-arm64": "0.137.0", "@oxc-parser/binding-darwin-x64": "0.137.0", "@oxc-parser/binding-freebsd-x64": "0.137.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0", "@oxc-parser/binding-linux-arm64-gnu": "0.137.0", "@oxc-parser/binding-linux-arm64-musl": "0.137.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0", "@oxc-parser/binding-linux-riscv64-musl": "0.137.0", "@oxc-parser/binding-linux-s390x-gnu": "0.137.0", "@oxc-parser/binding-linux-x64-gnu": "0.137.0", "@oxc-parser/binding-linux-x64-musl": "0.137.0", "@oxc-parser/binding-openharmony-arm64": "0.137.0", "@oxc-parser/binding-wasm32-wasi": "0.137.0", "@oxc-parser/binding-win32-arm64-msvc": "0.137.0", "@oxc-parser/binding-win32-ia32-msvc": "0.137.0", "@oxc-parser/binding-win32-x64-msvc": "0.137.0" } }, "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -283,10 +320,16 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="], + "rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -307,6 +350,8 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], @@ -317,10 +362,20 @@ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], } } diff --git a/package.json b/package.json index 1ac1278..5051285 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/node": "^25.6.0", "@valibot/to-json-schema": "^1.6.0", "ajv": "^8.20.0", - "argc": "github:ethan-huo/argc#v7.11.0", + "argc": "github:ethan-huo/argc#v7.11.1", "bun-types": "^1.3.13", "dataforseo-client": "^2.0.25", "dotenv": "^17.2.2", diff --git a/packages/gkit/docs/site.md b/packages/gkit/docs/site.md new file mode 100644 index 0000000..467ea76 --- /dev/null +++ b/packages/gkit/docs/site.md @@ -0,0 +1,146 @@ +--- +type: Reference +title: SiteSnapshot CLI +description: Portable page facts, offline SEO audits and comparisons, and optional Git-backed Grok explanations. +status: implemented-in-source +version: 0.3 +generated: { by: codex, at: 2026-09-08 } +--- + +# Contract + +`gkit site` consumes and produces explicit artifacts. A **SiteSnapshot** directory is the durable input to every audit and comparison. The caller chooses and stores both snapshots. There is no internal database, release cursor, or selected baseline. + +```text +Published sitemap XML → Routes + discovery evidence +Nuxt .output or public HTTP → SiteSnapshot +SiteSnapshot + rules → Audit +SiteSnapshot A + B → Page and finding diff +Diff + exact Git trees → Optional Grok explanation +``` + +The CLI does not build source commits, discover every route, schedule itself, select the latest deployment, or infer that a supplied SHA is serving production. Capture an actual build while it exists; rebuilding an old SHA today creates a new observation under today's inputs. + +# Commands + +```bash +gkit --schema site +gkit --profile example site doctor +gkit --profile example site discover --out /absolute/path/routes + +gkit --profile example site snapshot \ + --build /absolute/path/.output --revision release-tag \ + --context fixed-api-fixture-v1 --routes @routes.json \ + --out /absolute/path/snapshot-a + +gkit site audit /absolute/path/snapshot-a --out audit.json +gkit site diff /absolute/path/snapshot-a /absolute/path/snapshot-b --out diff.json + +gkit --profile example site explain /absolute/path/snapshot-a /absolute/path/snapshot-b \ + --repo /absolute/path/checkout --model MODEL --dry-run --out explain-input.json +``` + +`doctor`, `discover`, `snapshot`, and `explain` require an explicit profile. `snapshot` requires exactly one of `--build` or `--http`, an explicit `--context`, and a new output directory. Build capture also requires `--revision`. `--repo` resolves that revision locally; omitting it uses `gh api repos/OWNER/REPO/commits/REF`. Both resolve to an exact SHA. The profile declares the repository; the local checkout and artifact remain caller-supplied associations, not attested build provenance. + +For public endpoint observations: + +```bash +gkit --profile example site snapshot --http --context production-public \ + --out /absolute/path/production-2026-09-08 +``` + +HTTP capture may omit `--revision` and record an unassociated version. Supplying one labels the observation without proving a deployment. Capture respects declared origins and robots policy, uses `GkitSiteSnapshot/1`, and sends no application login credentials. + +`audit` and `diff` need no profile, network, Git, or model. `--rules @rules.json` re-evaluates existing facts. JSON is the default; `--format markdown` requires `--out`. Existing output paths are rejected. A valid artifact can contain failed pages or findings: inspect coverage/results, not just `ok` or exit code. + +`snapshot --dry-run` validates local configuration and prepares routes without resolving refs, starting servers, fetching pages, or writing a snapshot. `explain --dry-run` writes the bounded model input and response schema; it reads Git and may fetch repository objects when `--repo` is omitted, but does not invoke Grok. + +# Route discovery + +`site discover --out ` reads `/sitemap.xml` from the first configured origin by default. `--sitemaps @urls.json` supplies explicit sitemap roots. Nested indexes and gzip files are supported; a strict XML parser rejects malformed input, missing/duplicate loc entries, unexpected roots and DTD declarations. XML namespace extensions are not mistaken for page URLs. The parser does not certify every sitemap protocol or search-engine requirement. + +Sitemap requests stay within configured site origins and explicit root origins. To read a separately hosted CDN sitemap, pass `--sitemap-origins @origins.json`, containing extra HTTP(S) origins. This does not widen the allowed **page** origins. Redirects use the same source-origin boundary. Fetching sitemaps performs no page crawl and does not execute JavaScript. + +The output directory contains `routes.json`, `manifest.json`, and `documents/` with the fetched source bytes, including failed HTTP responses when available. The manifest records input seeds, roots, source/page origins, timestamps, source observations, checksums, rejected URLs and budget omissions. The manifest is written last. Configured seeds are included; duplicate URLs and cyclic indexes are deduplicated. + +Defaults are 50 sitemap documents and 100,000 candidate URLs. `--max-sitemaps` accepts 1–500; `--max-urls` accepts 1–1,000,000. Request timeout, pacing and compressed/decompressed byte limits use the site configuration. A failed/deferred/excluded sitemap or a URL budget omission produces partial discovery coverage, with successful evidence preserved. Discovery completeness only describes the declared source traversal; it does not prove URL existence, indexability, deployment version or whole-site completeness. Rejected page URLs are retained separately. + +`--dry-run` prepares inputs without network or output creation. Existing output directories are rejected before requests. Pass `--routes @/routes.json` to capture the selected candidates. Snapshot currently accepts at most 10,000 explicit routes per invocation; larger catalogs must be partitioned into explicit batches by the caller. Discovery and page coverage are separate facts, even if one is complete. + +# Profile configuration + +Site configuration defaults to `//site.json`; the profile may instead declare `site.configFile`. Provider secrets remain managed by the existing profile implementation. + +```json +{ + "version": 1, + "site": "example.com", + "origins": ["https://example.com"], + "seeds": ["https://example.com/", "https://example.com/fr/"], + "locales": ["en", "fr"], + "github": { "repository": "owner/site" }, + "timeoutMs": 15000, + "delayMs": 200, + "maxBytes": 5000000, + "rules": [{ "pathPrefix": "/", "indexable": true, "canonical": "self", "required": ["title", "description", "h1"] }] +} +``` + +`--routes @routes.json` replaces seeds with a JSON array of unique absolute URLs in the declared origins. Query-bearing URLs are unsupported. Locale paths and trailing-slash variants retain separate identities. `locales` documents intended scope; it does not generate routes. Rules use the longest matching path prefix. Canonical expectations are `self`, `present`, or `ignore`; `indexable: false` expects a noindex directive. + +# Artifact layout + +```text +snapshot-a/ + manifest.json + pages.jsonl + content/ +``` + +The manifest records schema/extractor versions, source kind, exact source version when supplied, build digest when applicable, runtime context label, timestamps, declared routes, coverage, default expectations, and the page index checksum. The context label is a caller assertion about inputs, not a capture of environment variables or external API/CMS versions. Change it when inputs are known to differ; matching labels do not prove reproducibility or causation. + +Each JSONL row has URL, observation time, capture mode, status/error, HTTP status, final URL, redirect chain, selected response headers, extracted fields, and a document hash. Content is the UTF-8 document used by the extractor, including unsuccessful HTTP responses when available. Fields cover title, description, H1, canonical, robots, HTML language, hreflang, links, and normalized body-text hash. HTML remains available for future extraction; body text itself is not embedded in the diff. + +The manifest is written last. Readers verify its fingerprint, page index checksum, route/coverage consistency, and referenced content hashes. Moving the entire directory preserves identity and offline usability. Checksums detect corruption, not authorship. `complete` means all **declared routes** were observed, not that every public URL was discovered or every SEO rule passed. + +# Nuxt adapter + +The adapter reads matching `.output/public/index.html`, `path/index.html`, or `path.html` when present. These observations use `prerender` mode and have no invented HTTP status or response headers. + +For remaining URLs, it starts that artifact's `.output/server/index.mjs` with the host Node runtime on loopback, supplies logical host/protocol headers, and fetches server-rendered HTML in `server-http` mode. The child inherits the caller's runtime environment; runtime API requests may occur. The build digest is checked before and after capture. The server stops on completion or cancellation. This adapter targets Node server output; it does not execute hydration, browser-only JavaScript, or other deployment presets. + +Build snapshots reveal generated/SSR content under supplied inputs. Production HTTP snapshots reveal actual endpoint responses at observation time. CDN behavior, served-SHA attestation, CMS replay, screenshots, and performance measurements require additional evidence. + +# Audit and comparison + +Deterministic checks cover HTTP errors, redirect chains, required fields, index directives, canonical count/target, hreflang syntax/targets/return links, and broken links to observed targets. Unvisited targets are unknown. Sitemap collection/parsing errors are recorded by `discover`; they are separate from page findings. Browser rendering remains outside this slice. + +Page comparison requires matching site/origins, source kind, context label, extractor version, and per-page capture mode. Missing or failed observations are `unobserved`; different capture conditions are `incomparable`. Missing routes do not prove deletion. + +Both snapshots are audited with the same current implementation. Finding transitions require matching expectations and sufficient observations on both sides, including relevant relationship targets. States are `new`, `persistent`, `resolved`, or `observed_only`. A target losing HTML cannot demonstrate a repaired hreflang return link; a 404 replacing a page cannot demonstrate repaired metadata. + +`resolved` means the selected rule no longer fails in comparable observations. Issue/PR progress, deployment, production verification, and business performance are separate facts. + +# Git and Grok + +Explanation compares exact commit trees with `git diff A B`, with external diff/textconv disabled. A local checkout must contain both objects. Without `--repo`, the command fetches those objects into a temporary bare repository using host `gh` credentials, then removes it. There is no checkout mutation or first-commit history replay. + +The model gets at most 40 useful changed-file patches, each limited to 12,000 characters, plus small Nuxt/layout/config context within a shared 100,000-character source budget. Environment files, lockfiles, maps, generated output, and dependencies are excluded. Page changes and findings have separate count/character limits; omitted records and file names are counted. Filtering bounds relevance and size; it is not a general secret scanner for arbitrary source files. `--dry-run` exposes the exact input. + +Remove `--dry-run` and choose a new output path to invoke the installed host `grok` CLI. `--model` is required. gkit disables tools, subagents, web search, and planning, requests one turn and structured output, and validates cited page URLs/code paths. Interpretations include confidence, caveats, and unknowns. + +Successful results are cached at `//cache/site-explanations/.json`. The key includes exact prompt/input, schema, model, Grok CLI version, and flags. Cache entries are validated, disposable, and regenerated after corruption/deletion. Cache write failure does not discard a valid explanation. The requested report is a separate artifact. + +Grok uses host authentication/billing. This integration does **not** pass through gkit provider metering or enforce a gkit dollar budget; envelope cost is unknown (`null`). Automated tests exercise a subprocess fixture; a paid live model call has not been verified in this slice. + +# Warehouse and migration + +Ingest each validated artifact as an immutable batch. Use `snapshot_id` for snapshots and `(snapshot_id, url)` for pages. Join manifest source metadata onto page rows and retain content under the artifact prefix or a hash-addressed object store. Audit/diff reports have separate IDs, input references, and rule versions. SQLite or warehouse indexes can be rebuilt from artifacts. + +Do not make `(commit_sha, url)` unique: a SHA can have multiple observations. GSC/PostHog join by site/URL/time; deployments join by repository/SHA plus event evidence. The CLI performs no warehouse upload, metric ingestion, scheduling, or PR/issue synchronization. + +The earlier unshipped `sync`, `releases`, `findings`, and `export` ledger commands are replaced. Existing ledger data is not modified or deleted. Historical observations may be converted once with original page timestamps and provenance; unavailable historical pages must stay unavailable. + +# Validation + +From the repository root: `bun run check-types`, `bun run test`, and `bun run verify:package`. The package check installs the tarball in an isolated directory and loads discovery with an isolated profile, without network capture. Current validation includes 190 tests and 15 CLI scenarios covering static/SSR capture, relocation, offline audit/diff, coverage, corruption, output reservation, exact Git input, and disposable caching. Real Nuxt 4.4.2 build evidence lives in the product workspace, outside this generic package. diff --git a/packages/gkit/package.json b/packages/gkit/package.json index b1868f2..e9477d2 100644 --- a/packages/gkit/package.json +++ b/packages/gkit/package.json @@ -1,6 +1,6 @@ { "name": "gkit", - "version": "0.1.3", + "version": "0.2.0", "private": true, "description": "Profile-bound, agent-first growth provider CLI.", "bin": { @@ -32,15 +32,19 @@ "generate:posthog:check": "bun run ./scripts/generate-posthog.ts --check", "eval:slice5": "bun run ./src/eval.ts", "test": "vitest", - "test:once": "vitest run" + "test:once": "vitest run && bun run test:site", + "test:site": "bun run ./scripts/test-site-snapshot.ts" }, "dependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.6.0", "ajv": "^8.20.0", - "argc": "github:ethan-huo/argc#v7.11.0", + "argc": "github:ethan-huo/argc#v7.11.1", + "cheerio": "^1.2.0", "dotenv": "^17.2.2", "google-auth-library": "^10.9.0", + "robots-parser": "3.0.1", + "saxes": "6.0.0", "valibot": "^1.2.0", "yaml": "^2.9.0" }, diff --git a/packages/gkit/scripts/test-site-snapshot.ts b/packages/gkit/scripts/test-site-snapshot.ts new file mode 100644 index 0000000..6f9f5f9 --- /dev/null +++ b/packages/gkit/scripts/test-site-snapshot.ts @@ -0,0 +1,322 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile, readFile, readdir, rm, cp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { readSnapshot } from "../src/site/snapshot"; +const exec = promisify(execFile); +const root = await mkdtemp(join(tmpdir(), "gkit-snapshot-test-")); +const repo = join(root, "repo"); +await mkdir(repo); +const git = async (args: string[]) => (await exec("git", ["-C", repo, ...args])).stdout.trim(); +const cli = new URL("../src/cli.ts", import.meta.url).pathname; +const config = join(root, "config"); +const profile = join(config, "gkit/profiles/app"); +await mkdir(profile, { recursive: true }); +await writeFile( + join(config, "gkit/profiles/app.json"), + JSON.stringify({ version: 1, name: "app", providers: {} }), +); +await writeFile( + join(profile, "site.json"), + JSON.stringify({ + version: 1, + site: "example.com", + origins: ["https://example.com"], + seeds: ["https://example.com/", "https://example.com/fr"], + delayMs: 0, + github: { repository: "owner/site" }, + }), +); +const bin = join(root, "bin"); +await mkdir(bin); +await writeFile( + join(bin, "grok"), + `#!/usr/bin/env bun +import {appendFileSync} from "node:fs"; +if(process.argv.includes("--version")){console.log("fixture-grok-1");process.exit(0)} +const i=process.argv.indexOf("--tools");if(i<0||process.argv[i+1]!=="")process.exit(2); +appendFileSync(process.env.SITE_TEST_ROOT+"/grok-calls","call\\n"); +console.log(JSON.stringify({summary:"The title changed.",evidence:[{page_url:"https://example.com/",claim:"The snapshot title changed.",code_paths:["app/pages/index.vue"],confidence:"medium",caveat:"Runtime inputs may also affect output."}],unknowns:[]})); +`, + { mode: 0o700 }, +); +const env = { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + XDG_CONFIG_HOME: config, + XDG_STATE_HOME: join(root, "unused-state"), + SITE_TEST_ROOT: root, +}; +const run = async (args: string[], expected = 0) => { + const child = Bun.spawn([Bun.which("bun")!, cli, ...args], { + env, + stdout: "pipe", + stderr: "pipe", + }); + const [out, err, code] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]); + assert.equal(code, expected, out + err); + return JSON.parse(out); +}; +const build = async (name: string, title: string) => { + const path = join(root, name); + await mkdir(join(path, "public"), { recursive: true }); + await mkdir(join(path, "server")); + const html = `${title}

Example

`; + await writeFile(join(path, "public/index.html"), html); + await writeFile( + join(path, "server/index.mjs"), + `import http from "node:http";const server=http.createServer((req,res)=>{res.setHeader("content-type","text/html");res.end(${JSON.stringify(html.replace('href="https://example.com/"', 'href="https://example.com/fr"'))})});server.listen(Number(process.env.PORT),"127.0.0.1");process.on("SIGTERM",()=>server.close());`, + ); + return path; +}; +let scenarios = 0; +try { + await git(["init", "-b", "main"]); + await git(["config", "user.email", "fixture@example.invalid"]); + await git(["config", "user.name", "Fixture"]); + await mkdir(join(repo, "app/pages"), { recursive: true }); + await writeFile(join(repo, "app/pages/index.vue"), "\n"); + await git(["add", "."]); + await git(["commit", "-m", "A"]); + const a = await git(["rev-parse", "HEAD"]); + await writeFile(join(repo, "app/pages/index.vue"), "\n"); + await git(["commit", "-am", "B"]); + const b = await git(["rev-parse", "HEAD"]); + const buildA = await build("output-a", "A"); + const buildB = await build("output-b", "B"); + const snapA = join(root, "a"); + const snapB = join(root, "b"); + await run([ + "--profile", + "app", + "site", + "snapshot", + "--build", + buildA, + "--revision", + a, + "--repo", + repo, + "--context", + "fixture", + "--out", + snapA, + "--dry-run", + ]); + await assert.rejects(readFile(join(snapA, "manifest.json"))); + scenarios++; + for (const [output, sha, out] of [ + [buildA, a, snapA], + [buildB, b, snapB], + ]) { + const result = await run([ + "--profile", + "app", + "site", + "snapshot", + "--build", + output!, + "--revision", + sha!, + "--repo", + repo, + "--context", + "fixture", + "--out", + out!, + ]); + assert.equal(result.data.coverage.observed, 2); + } + const captured = await readSnapshot(snapA); + assert.deepEqual( + captured.pages.map((page) => page.mode), + ["prerender", "server-http"], + ); + assert.equal(captured.pages[0]?.http_status, null); + scenarios++; + const first = await readFile(join(snapA, "manifest.json"), "utf8"); + await run( + [ + "--profile", + "app", + "site", + "snapshot", + "--build", + buildB, + "--revision", + b, + "--repo", + repo, + "--context", + "fixture", + "--out", + snapA, + ], + 1, + ); + assert.equal(await readFile(join(snapA, "manifest.json"), "utf8"), first); + scenarios++; + const moved = join(root, "moved"); + await cp(snapA, moved, { recursive: true }); + await rm(snapA, { recursive: true }); + assert.equal((await readSnapshot(moved)).manifest.snapshot_id, captured.manifest.snapshot_id); + scenarios++; + const diff = await run(["site", "diff", moved, snapB]); + assert.equal(diff.data.summary.changed_pages, 2); + assert.equal(diff.data.coverage.comparable_urls, 2); + scenarios++; + await rm(profile, { recursive: true }); + const audited = await run(["site", "audit", moved]); + assert.equal(audited.ok, true); + scenarios++; + await mkdir(profile); + await writeFile( + join(profile, "site.json"), + JSON.stringify({ + version: 1, + site: "example.com", + origins: ["https://example.com"], + seeds: ["https://example.com/"], + github: { repository: "owner/site" }, + }), + ); + const input = join(root, "input.json"); + await run([ + "--profile", + "app", + "site", + "explain", + moved, + snapB, + "--repo", + repo, + "--model", + "fixture-model", + "--out", + input, + "--dry-run", + ]); + const prepared = JSON.parse(await readFile(input, "utf8")); + assert.equal(prepared.input.code.method, "git-diff-two-trees"); + await assert.rejects(readFile(join(root, "grok-calls"))); + scenarios++; + for (const suffix of ["first", "cached"]) { + const result = await run([ + "--profile", + "app", + "site", + "explain", + moved, + snapB, + "--repo", + repo, + "--model", + "fixture-model", + "--out", + join(root, `${suffix}.json`), + ]); + assert.equal(result.data.status, suffix === "first" ? "generated" : "cached"); + } + assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim(), "call"); + scenarios++; + await run( + [ + "--profile", + "app", + "site", + "explain", + moved, + snapB, + "--repo", + repo, + "--model", + "fixture-model", + "--out", + join(root, "first.json"), + ], + 1, + ); + assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim(), "call"); + scenarios++; + await rm(join(profile, "cache"), { recursive: true }); + await run([ + "--profile", + "app", + "site", + "explain", + moved, + snapB, + "--repo", + repo, + "--model", + "fixture-model", + "--out", + join(root, "rebuilt.json"), + ]); + assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim().split("\n").length, 2); + scenarios++; + const cacheDirectory = join(profile, "cache/site-explanations"); + const cacheFiles = await readdir(cacheDirectory); + assert.equal(cacheFiles.length, 1); + await writeFile(join(cacheDirectory, cacheFiles[0]!), "damaged"); + for (const suffix of ["repaired", "repair-cached"]) { + const result = await run([ + "--profile", + "app", + "site", + "explain", + moved, + snapB, + "--repo", + repo, + "--model", + "fixture-model", + "--out", + join(root, `${suffix}.json`), + ]); + assert.equal(result.data.status, suffix === "repaired" ? "generated" : "cached"); + } + assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim().split("\n").length, 3); + scenarios++; + await rm(join(buildA, "public"), { recursive: true }); + await run([ + "--profile", + "app", + "site", + "snapshot", + "--build", + buildA, + "--revision", + a, + "--repo", + repo, + "--context", + "fixture", + "--out", + join(root, "ssr-only"), + ]); + const ssrOnly = await readSnapshot(join(root, "ssr-only")); + assert.equal(ssrOnly.pages[0]?.mode, "server-http"); + assert.equal(ssrOnly.manifest.coverage.observed, 1); + scenarios++; + const original = await readSnapshot(moved); + await writeFile(join(moved, "content", original.pages[0]!.html_hash!), "tampered"); + await run(["site", "audit", moved], 1); + scenarios++; + const incomplete = join(root, "incomplete"); + await mkdir(incomplete); + await writeFile(join(incomplete, "pages.jsonl"), ""); + await run(["site", "audit", incomplete], 1); + scenarios++; + await assert.rejects(readdir(join(root, "unused-state"))); + scenarios++; + console.log(JSON.stringify({ ok: true, scenarios })); +} finally { + await rm(root, { recursive: true, force: true }); +} diff --git a/packages/gkit/scripts/verify-package-artifact.ts b/packages/gkit/scripts/verify-package-artifact.ts index 6c82c1c..47d24bd 100644 --- a/packages/gkit/scripts/verify-package-artifact.ts +++ b/packages/gkit/scripts/verify-package-artifact.ts @@ -1,12 +1,5 @@ import { constants } from "node:fs"; -import { - copyFile, - mkdir, - mkdtemp, - readFile, - rm, - writeFile, -} from "node:fs/promises"; +import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; @@ -67,7 +60,16 @@ try { const globalDirectory = join(consumerDirectory, "global"); const globalBinDirectory = join(consumerDirectory, "bin"); await runCommand( - ["bun", "add", "--global", `gkit@${tarballPath}`], + [ + "bun", + "add", + "--global", + "--registry", + "https://registry.npmjs.org", + "--network-concurrency", + "8", + `gkit@${tarballPath}`, + ], consumerDirectory, { BUN_INSTALL_GLOBAL_DIR: globalDirectory, @@ -85,6 +87,41 @@ try { } const gkit = join(globalBinDirectory, "gkit"); + await runCommand([gkit, "--schema", "site"], consumerDirectory); + const isolatedConfig = join(consumerDirectory, "config"); + const siteProfile = join(isolatedConfig, "gkit", "profiles", "package-test"); + await mkdir(siteProfile, { recursive: true }); + await writeFile( + join(siteProfile, "..", "package-test.json"), + JSON.stringify({ version: 1, name: "package-test", providers: {} }), + ); + await writeFile( + join(siteProfile, "site.json"), + JSON.stringify({ + version: 1, + site: "example.test", + origins: ["https://example.test"], + seeds: ["https://example.test/"], + github: { repository: "fixture/site" }, + }), + ); + const discovery = await runCommand( + [ + gkit, + "--profile", + "package-test", + "site", + "discover", + "--dry-run", + "--out", + join(consumerDirectory, "discovery"), + ], + consumerDirectory, + { XDG_CONFIG_HOME: isolatedConfig }, + ); + if (JSON.parse(discovery.stdout).data?.status !== "prepared") { + throw new Error("The installed artifact could not load the site discovery runtime."); + } await runCommand([gkit, "--schema", "gsc"], consumerDirectory); const described = await runCommand( [gkit, "describe", "--id", "gsc.properties.list"], @@ -153,7 +190,9 @@ async function writeReleaseAssets( const stablePath = join(destination, stableName); await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL); await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL); - const digest = createHash("sha256").update(await readFile(tarballPath)).digest("hex"); + const digest = createHash("sha256") + .update(await readFile(tarballPath)) + .digest("hex"); await writeFile( join(destination, "SHA256SUMS"), `${digest} ${exactName}\n${digest} ${stableName}\n`, diff --git a/packages/gkit/src/args.ts b/packages/gkit/src/args.ts index 1661b14..d0f2025 100644 --- a/packages/gkit/src/args.ts +++ b/packages/gkit/src/args.ts @@ -1,6 +1,8 @@ +import { parseSiteArgs, type SiteCommand } from "./site/commands"; import { GkitFailure } from "./envelope"; export type ParsedCommand = + | SiteCommand | { kind: "help" } | { kind: "schema"; selector: string | null } | { kind: "skill"; path: string | null } @@ -199,6 +201,8 @@ export function parseArgs(argv: string[]): ParsedCommand { return { kind: "docs", provider: optionalString(flags, "--provider") }; } + if (rest[0] === "site") return parseSiteArgs(rest.slice(1), profileFlag); + if (rest[0] === "ledger") { if (profileFlag) invalid("ledger commands do not load a profile."); if (rest.length === 1) return { kind: "ledger-status" }; @@ -346,6 +350,15 @@ export function renderHelp(): string { " gkit --profile hubspot api call --operation-id --input @request.json --out --dry-run", " gkit --profile hubspot api call --operation-id --input @request.json --out ", "", + "Site facts:", + " gkit --schema site", + " gkit --profile site doctor", + " gkit --profile site discover --out ", + " gkit --profile site snapshot --build .output --revision --context