From 7be5c3948f8111a3f55acf2561a22b235beb1435 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Fri, 13 Nov 2026 14:58:44 +0800 Subject: [PATCH 1/4] chore: harness toolchain (vite-plus, tsrs, TS 7, fallow); fix TS 7 unused-import error --- .fallowrc.jsonc | 7 + .gitignore | 1 + .vite-hooks/pre-commit | 1 + bun.lock | 396 ++++++++++++++++++++++++++- package.json | 83 +++--- packages/gkit/package.json | 110 ++++---- packages/gkit/src/execute-posthog.ts | 1 - packages/gkit/tsconfig.json | 17 +- vite.config.ts | 15 + 9 files changed, 511 insertions(+), 120 deletions(-) create mode 100644 .fallowrc.jsonc create mode 100755 .vite-hooks/pre-commit create mode 100644 vite.config.ts diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc new file mode 100644 index 0000000..78c21c1 --- /dev/null +++ b/.fallowrc.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", + // Generated by `bun run generate:*` (verified by `generate:check`). + "ignorePatterns": ["packages/gkit/generated/**", "packages/gkit/docs/providers/**", "packages/gkit/sources/**"], + // Local-only helper that renders .npmrc from the vault; not a dependency. + "ignoreBinaries": ["latch"], +} diff --git a/.gitignore b/.gitignore index 4761d02..09be76f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage *.tgz .npmrc +.claude/worktrees/ diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100755 index 0000000..85fb65b --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/bun.lock b/bun.lock index a20ee64..0b14612 100644 --- a/bun.lock +++ b/bun.lock @@ -5,16 +5,20 @@ "": { "name": "gkit", "devDependencies": { + "@celados/harness": "^0.1.3", "@types/bun": "catalog:", "@types/node": "catalog:", "bun-types": "catalog:", + "fallow": "^3.42.0", + "tsrs": "0.24.0-ts7.1.0-beta", "typescript": "catalog:", + "vite-plus": "^1.6.0", "vitest": "catalog:", }, }, "packages/gkit": { "name": "gkit", - "version": "0.3.6", + "version": "0.5.0", "bin": { "gkit": "dist/gkit.js", }, @@ -31,7 +35,7 @@ "google-auth-library": "^10.9.0", "robots-parser": "3.0.1", "saxes": "6.0.0", - "typescript": "^5", + "typescript": "catalog:", "valibot": "^1.2.0", "vitest": "^4.0.18", "yaml": "^2.9.0", @@ -48,17 +52,141 @@ "dataforseo-client": "^2.0.25", "dotenv": "^17.2.2", "google-auth-library": "^10.9.0", - "typescript": "^5", + "typescript": "^7.0.2", "valibot": "^1.2.0", "vitest": "^4.0.18", "yaml": "^2.9.0", }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/runtime": ["@babel/runtime@7.29.14", "", {}, "sha512-Y5tqevQaUAZhhj82BpYMNC1ssvLQTVAfwKdm8iy8qwmhvC2/CpcAu8OC+QEtjM7pMk7knx83u1Qj8gzkicyelw=="], + + "@blazediff/core": ["@blazediff/core@1.10.0", "", {}, "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ=="], + "@celados/argc": ["@celados/argc@11.0.0", "https://npm.celados.com/@celados/argc/-/@celados/argc-11.0.0.tgz", { "dependencies": { "yaml": "^2.9.1" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.8.0", "valibot": "^1.5.0" } }, "sha512-376S0EpKHXV00RLGmQuMfuYYZBLgWIAHngpdmGAbGAn5FBhHNeKd5Lg9XNdv/qtTfIwlONOLnfyt4QTM1Gw3ZA=="], + "@celados/harness": ["@celados/harness@0.1.3", "https://npm.celados.com/@celados/harness/-/@celados/harness-0.1.3.tgz", { "peerDependencies": { "@celados/glaze": ">=0.33.0", "@convex-dev/eslint-plugin": ">=5.0.0", "@effect/tsgo": ">=0.68.0", "@gdp-ts/core": ">=0.1.0", "@stylexjs/eslint-plugin": ">=0.19.0", "eslint-plugin-solid": ">=0.19.0", "fallow": ">=3.42.0", "tsrs": ">=0.24.0-ts7.1.0-beta", "typescript": ">=7.0.2", "vite-plus": ">=1.6.0" }, "optionalPeers": ["@celados/glaze", "@convex-dev/eslint-plugin", "@effect/tsgo", "@gdp-ts/core", "@stylexjs/eslint-plugin", "eslint-plugin-solid"], "bin": { "harness": "dist/cli.mjs" } }, "sha512-FFdYryrXOTBiCSaHweAprzWOiSMIc7KPWsDn+iGOiDfD2R7j1xf02Emr+eXVme50kPrfQPZeAFwAj7Bw/SNXOA=="], + + "@fallow-cli/darwin-arm64": ["@fallow-cli/darwin-arm64@3.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xxmT9R8jj+6Uc7eaIJSz5rb8fbUl90Qah+1XL0EpQWplr4oVugQs1pueHiiYDz013D93bwLVqe4y//zEtNWWtg=="], + + "@fallow-cli/darwin-x64": ["@fallow-cli/darwin-x64@3.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UXiXJPezGjkRYZK2f1ns7lC5erQC9add0CLqaEX0xSsvAU5nKVToVr4ibCq6+FMfeiBkc/7txUfpwG9/khJwIg=="], + + "@fallow-cli/linux-arm64-gnu": ["@fallow-cli/linux-arm64-gnu@3.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdUkcj/6pRQaUsLPT3yPimjQa+9J0b6QrzT+sV8Rngs7iihzej3cBMM/yqxoT9U7SsHiErvGIr2y6codUu321Q=="], + + "@fallow-cli/linux-arm64-musl": ["@fallow-cli/linux-arm64-musl@3.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-50JlZzhu6x+OnddmdhDwIEgtNdns2vxTkW4svI3kafFqZxBoW6gkYj0Lgy0I6he/3TmEiHk5GbJ8m+UtPTUzRg=="], + + "@fallow-cli/linux-x64-gnu": ["@fallow-cli/linux-x64-gnu@3.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9Hcg0sapABeAsY+2lanr0JyAZHucXMta+V0fAfqtCe7J7jR/oNc0xocO849lNt6s4Jc59TcOxajZArJZC7DRWQ=="], + + "@fallow-cli/linux-x64-musl": ["@fallow-cli/linux-x64-musl@3.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fMg7FJogrZx/qaoZEXxk7gDrWnXmFvQibaR1+hZz661qjqf04Ejr9+I5ymLKgNu9oWC2oy3kyqjYlNLrrO0fBA=="], + + "@fallow-cli/win32-arm64-msvc": ["@fallow-cli/win32-arm64-msvc@3.42.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-g/4FbRnli52w5fyZBuA9Tyj9AyRk5xWEwwkkmidd6Wz8s5IypO7VFXrX/RnSIfp583WJ1dDubtBe2/vTJtRccg=="], + + "@fallow-cli/win32-x64-msvc": ["@fallow-cli/win32-x64-msvc@3.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Cg7/te1DqFNivekXoI2Eg8V97nTugyeAd0/p0TO5MGSw3L0fN/8RwtgcCX7LTDlFLs0wEh8/Af0zfCsZFD49Q=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.3", "", {}, "sha512-lhTtjtoEArWBjismtfS761S9Hlyn+nIZqOZVYoZ1Xn82PMNMVnk+jj/NhC4fAXGTs7ydONcJiR5vyQBTMDu27g=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.158.0", "", {}, "sha512-OYWBlK/wnZHyol5jkvBJadD8KTR6DJfFDRClqrU6kBDQKHd4bXNZALg3qfXk0aFl6F5loSxMT24EZurNWE4MKQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.158.0", "", {}, "sha512-xZ66qA4WmLbTLlxH3mqMd8f9UKbaOzOT0ATwFf4DH/14pMgpUrtqtjyYzOQmgfl2a0iRAAMbQ9G1th/67jRzGw=="], + + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-1x9NPGQG5WMOOT8spRP4hPbeqwB/P9ayxFo4EUDSle6mLmtYRqE6suXgHr93/S2nnszy77LlIa8klkKdWtHbMw=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1muYaW9SiPEq8NIPGX0B20oUwCx6HWiCsZem/2jU7UsOzoXLT4YxWq6ybdhwKSKl0PyAfvyMyPF7FsI1To7AdA=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jl4KJdo8HrVpLmcSYS/7oDXPH8Hzd6VrzgUFRAbY0j6cSRjcDkUnpuAStuNLoqvMtZEi7Rdy68BBriCgWJcy2g=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RwOhQgAQi1qV3TeuTfrgGkCWEL3eDFIql/lo4rt2xpGHqyolKa+W5cFbb+M9IvwMwQbplii5Gbv1ZQQPTJlNug=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MAXydj01mLjMUlnmo7sYVm+7kdBdU9Nwaux2Q1f9oLNqt3CCdego1oMoZ30qDyZQGQXfVJZEU1yRV7mSzwKDVA=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-rz6FVZRiWsq3O2YQgCoqZRwaQQaPQ70NBks4+KINjjPJOjBTd8g/iq6ynS97ode+qKGlhXtpc/xCl8LHzfOjyQ=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-lIK0XJi4JP1FnhuLjR/JWhQHGghZc+ghFNnbZTQC8kxsmrkW3ex8CeF7fp0FT+rEP0Nm6voCYkasL8poVWA9sQ=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.78.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-o2LJIHfW3ZADOjDVk5c7MM1M8CiAG43GTc4mlJAK23fOHgru4CvnqWedCoCXpIsKokoRdEa7XdQywPGZv1BAiA=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.78.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-G+lUaQk94Vo+4byyd8X2+pPXtd7wObpdbvBC8xyozZD0ROSVBI3XpNNR6HECkJq4Q51AwLuJG81f4x+4eqvSrw=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.78.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-r76PAZf9/ZgF/iDRaIf8nLsr4x6ZHe/j2QLmWUjoQ46Sa11qMO68a5u5iux3y8vLzqYTS/xtKO9PjjMfyk5png=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.78.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-SQiXTt/c4aVoRXkJvhf4Y/sA1XYe3XjSMDbdX5PiAssBuHERoGtncQXz0KKWkXPejWTKlAL4mZSQZdHOCiw7TA=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.78.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-VNB3wFUx/rAqwVNW+uyXxb+c0HUrmcS51UPkFMDWJY95c6Tck6u6HOyVLsqiYoC8kh2mGlwanYmjb/R2McvN0A=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.78.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-1nbI0Y7SXwx2f4mJmKt58MaDdjAcoaYM90pBMGdcZCsTW2taFW1/MUi3uQGRPAN2n+3PldCdO9m+D6x+gU/heg=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.78.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-I2z2ISPjrl3CSRF5fmQkNnoh46UVU1GjM1V0+COJyNZcj0SDNM4DrPP2/gXCX38YJ1P2BL6sF2VdsJ+pEYEbJQ=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.78.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-YHt5nKKFxT4SruI57FHnEhGRSDuoFqNoAM3moYCClg70mjjS39gBBbdzaZv+N0otdzvQVuHTPu3jFu5tpqC3XQ=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-WH21lBMU+LYzF7aMeozTZlyc44oa2F+I5OtQ7RwuidCDoAxgTyN1s34rx0PBYrio56+2G88U9VOnYJsyV7VMHQ=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uNQqHJrtHIiSl8pMbhDktmGpA1hqwldWNO8kbNpNFhVt+BYk4C3Ge8nlJgkRtGjtMam0+542G5UQJJG6lLePbg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-W6omURRT39EHBPH4Z8sUPIOzWVjMbhCCCWcfT0sa6aeC+vOXNyBAJmpQRh5//KfPDX96jJlNith9i4N65wxB+Q=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-r9rWKeDVc7lIrj0Us9kP08W7FudYAoWyEdO40qnKk20bHiTbW/z2v4LoIxxUENQyFfApbeV2Gl9XsEgY/tY1dA=="], + + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2006", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uc0MEHXZUDW23Zmo5X28eSNeSnP17jKnh9GiehpVZqhfy99ks8spyXT5Llar3tjDN7ppnQN0Qd9FK6WNkdD+fQ=="], + + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2006", "", { "os": "darwin", "cpu": "x64" }, "sha512-+x1/pa3S3VMC9cif8XKTOCQUuqRT+wu1Ix+FK5svXa2xbXCuE7+4S0SfmuSnK/mk9AI5+xBxZFhILxmCPUZ1hA=="], + + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2006", "", { "os": "linux", "cpu": "arm64" }, "sha512-90v7HX8m54kHXn5DpjNEN18Fb7xwJMREe/wji3USyAf9pTMv7a3qZ3cPoLViIcIzxPLmkcYZIs1j1H3Z2gbVLA=="], + + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2006", "", { "os": "linux", "cpu": "x64" }, "sha512-/k0wsEqHiA5dWBJYuld3Vecv6bIhKCasxpho/aFkvh870Qox33MuTkKUGWIVm/njAxw7W4pu1a/Xfa3jwuKuPQ=="], + + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2006", "", { "os": "win32", "cpu": "arm64" }, "sha512-CQT5sqcLZLNZSWLzUUEjQmp74UX8h1agp3ZJ8pfRktHbx6gfBa8a7CjrnRrZ9VXlwnXVkpw7m5KBO6y+S5MQRA=="], + + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2006", "", { "os": "win32", "cpu": "x64" }, "sha512-VZsGowXcp3vQNkjoGeVCOTf4aGrRpRk6bBJWhefnJVPr6z+dX+LKqqHfwaqnFvg66Q2iJpXqklRW38YO21ee+Q=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.93.0", "", { "os": "android", "cpu": "arm" }, "sha512-KKw70pbD5Pvq3G2lJ+WQ71XPMjCmg37rkSe2+mDqCRP3PoTSdDMuEi4UOnE8JBtyNM7bek61EC2s0Jt7S7u+Sg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.93.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fvr1fTUZGelwRxKDt64pf28aL1WaWgviwz3X+E/FcRYvrlEahH0NECRz/c1SY+K+KWmZk5uAf6h1kQnyBkp/2A=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.93.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VdvSN9JRf4Es6ZzoAdvEeh8tgKdw8L4H82WmA8MMPVfPdZGCwE/nI27Z1HrRHYQhPxOh4kmnxQEioSUFrOvDaw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.93.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QR1V890Ml0YGRG0F/iV0K9igG+pOhe+VWd+aZnxY849rRHZf3F2gEoQks5+GVHf105X9Mh/6HdShTP5juekG2g=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.93.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jDfIbi30zZaKi+rTvig3sge+Xhi8pFFSgHxb0jzovc4qh8ETm/irKV9c7N/JsuwnYc5sbvf4IC8w1vQQ0IL+BA=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qtxZ714ND/koFgVH00QgxlzZiIbRPmguaPhnHRU5DFFtrtEdOrSnZD4U/H2AzXrXkZ59eQbMBnJwbum5XohrIQ=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-5pJ9NjMvH7WMUqXB6bTTOSemBm39w2cod27Hu3+Yom/GhbTeNXlYcpDx1o4CgBP6fhp0YHG66TNdVaUm6sHjsA=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.93.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-pRf+7jtbt2fHE23SX7qBxRgnBZicUgsoCK1PVIjnOk+t3a6UBkpt/GrKI99WlxsKUxftz/5nv/kfKvScmYywkw=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.93.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-zM8Fd/K8sqGvsBcJBZ3oVZ3ctGWbszA08IaGt5gFFF5RcunppAI/ZnAXbouABvnWeJ//S44KKlfPZnDDbeiphg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.93.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-7qKCnvhZKDXJG2pdbLazd2hw4FxjJHbZOFrrbzyy5PdHfPLH9vnNUa1le01A8uItZuYKPBPncH+HCoEiFT4SgA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.93.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-2pgulxC6G8V5wE750NJE/fRkf9aO6i6yoj10wyaR2EdiNtan4OemsOtsQ0ZTXGdY8Q3IoTmlND3GYvjJsvikYg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.93.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-yWsn6Gg2PnGG5jlyDz5+pHGiMTMiy4adaa4WRMzDgzB8efaShk1JMmTHIBSVLQMh0tYXRs4CN5uJCO79r4Mi4g=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.93.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-WCn5plwp/ezS2RDPQKp0/EXEVRr9/dDexUdRKmsMFqzNM5xBSZ1wbsi3PuMk1VfkMRnI5BuazCsziCWMXRDOlw=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.93.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-F2CGjb9LAkzH+IYTZSzd1klQ1vk5z6Utjax64WeLByCkYXbVDbsun+Oy2hLfW2MDSKCvW+jwy/9ld0EPgdeu+A=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.93.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-plf5DE2objcG8Tf7mXRuRa8tT8rzDkpjqXQtKoGsKHWkpgsialbON1+cCtoSEUTVbQUjXvuAm8vV9+3rR0yCBw=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.93.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Kp2yJrKvoTe9FfKdO/xwLNUbs2bYTMKx0+aoQD1s0h/Tx0kSgtHSdGmrk8tTKdluosjFbo88qYRIaNIX43AfFw=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.93.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vigjcHCFsR6KjnWTEVfaCxcMYapEuaK9FZa1MpewwQ1S+Xud3v0okfkj1RySsD6RF1NRHIjSlQSalTnKIaj3nA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.93.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0Ot3BEe/AAr2/uQhe55r8CmPlperYk2mQmxdutOru7oU0kir6IiEVHb1TG3GORRlfra97lPHVDLhgmihiKzLrg=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.93.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZzzIIVs/Qkm8trUWYKF0OYub5ALlv50/3gsEZbKAvWgz+yNJX6uRmQ8ySPXU2Q0/Y9m4JCOjhpmC8XUYbPHX5Q=="], + + "@oxlint/plugins": ["@oxlint/plugins@1.93.0", "", {}, "sha512-bKWQI1g9jn8bWUZsvhbOLfgva576Fbp1KwoTNSUCMdT9tlTAvQS2JkifX1UrhPPdyN4GHvM20HhvIMqqU1yotQ=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], @@ -92,6 +220,26 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@testing-library/dom": ["@testing-library/dom@10.4.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.10", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-tA4stGNp3mLGOGO+lh87UA56Tz4c8NVGBSkyIF58O+W/bs2Re9BCI9xihnL70+DsThncFznE0skS88SM1D2hyg=="], + + "@tsrs/darwin-arm64": ["@tsrs/darwin-arm64@0.24.0-ts7.1.0-beta", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GxGPRieCGIXevJgGlbne9pRqkfAN+VmBQvBHWWcF8rXLNX9yCxYo78/a7Auqjf723b4eFTlM5mgkclUit/Fu9w=="], + + "@tsrs/darwin-x64": ["@tsrs/darwin-x64@0.24.0-ts7.1.0-beta", "", { "os": "darwin", "cpu": "x64" }, "sha512-TrXdOyEq++blyYp6KphoHGmsGxAn1flYzjm0peLEI3c88z1UQa+pKkcuPgZ6FOxTW6fSaT9572n21kz+0bWi/g=="], + + "@tsrs/linux-arm64-gnu": ["@tsrs/linux-arm64-gnu@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Ph5NK0vnaR4gnBW2LgrZsUPx4mKF+9tCQUYZFOHmny6SMojSsm/5GCiBu0sHZEZhmyIqq8UbineUEA7T0QImnw=="], + + "@tsrs/linux-arm64-musl": ["@tsrs/linux-arm64-musl@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-vjgx8wGuAVSOPLlb2eVQpQVolImZa7vsH/1kvYOaokcP/KZXVUmHzgroxK0t9gxJ0SXvT4VKNjWZ+t+4tDNu1A=="], + + "@tsrs/linux-x64-gnu": ["@tsrs/linux-x64-gnu@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-tj1SNWBnJoBUzU2zrD18tUu3h6o36FXQGPpmIXsADHSfU5H9pVQdK/QzPIO76rb72P7foXHluE+xGTXXf8nNaA=="], + + "@tsrs/linux-x64-musl": ["@tsrs/linux-x64-musl@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-sizu0EdGw9ITqEaZKg60TDhDk5PXT9RzVkCdesBjk/BZhS0BVU2cRhvoRBmDbk+2vCUgW14j+Otg3tl+002qNQ=="], + + "@tsrs/win32-x64": ["@tsrs/win32-x64@0.24.0-ts7.1.0-beta", "", { "os": "win32", "cpu": "x64" }, "sha512-WAbzgd5JBOauQ/oL6DTdGEh09tM/q8tlGAcRORskhPK7eIQVVa8Js2pc7DVW0XokWTSADeD9v6HKtU2Ge0r/fQ=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -102,26 +250,114 @@ "@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.7.1", "", { "peerDependencies": { "valibot": "^1.4.0" } }, "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A=="], "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], - "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + "@vitest/mocker": ["@vitest/mocker@5.1.0", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.1.0", "estree-walker": "^3.0.3", "magic-string": "^1.4.3" }, "peerDependencies": { "msw": "^2.4.9 || ^3.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "@vitest/pretty-format": ["@vitest/pretty-format@5.1.0", "", { "dependencies": { "tinyrainbow": "^3.2.0" } }, "sha512-IRx5mc30CrFzyMNni6SZKQ1r53kv049Mcvvk0WbWcD6PUz/bFjG4zYv0KosQAeonPpGp5WxqYBMqdSOfJfvVWw=="], "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + "@vitest/snapshot": ["@vitest/snapshot@5.1.0", "", { "dependencies": { "@vitest/pretty-format": "5.1.0", "@vitest/utils": "5.1.0", "magic-string": "^1.4.3", "pathe": "^2.0.3" } }, "sha512-6tLM70+37LVXz+A6QmOlMQUuj+taODAyXZhz58lNNNNKHH0D81yzPBEezKQQGPlABc8OpAQBOBVvssVLLQqixw=="], + + "@vitest/spy": ["@vitest/spy@5.1.0", "", {}, "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A=="], + + "@vitest/utils": ["@vitest/utils@5.1.0", "", { "dependencies": { "@vitest/pretty-format": "5.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.2.0" } }, "sha512-V+qgCwkvVw+s1t1S5TaoBHsLi5Sz/YmohCMoOu0mZ8ZQst2bKhRYARAQucWpVg8D0mv0x5MHcWXY5QdOsoiGBQ=="], + + "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@1.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1IiE9eXsnLWesaHEI2l1L38+CChaQ82WJd3GpF+Ud7Kuq+T2x8TebUwLCPYatpTSDDs8z2w1yKAl7quoVZhetA=="], + + "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@1.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-oSsfR1ACQdFo60cuvInK5AZxPx7TAKyrt7NsUaNVZVj24ALsh+0hjZSRmWkJ3lF8aRtw1Lh8d7bkFXgQcQdKxQ=="], + + "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@1.6.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-6m2vRctdtQDl+ARQwGTF9X6hg+JaVFtj4vkvBLy/dWpqiJzseGebJwKUjys8QuhXgcJocn3a+N6oh27vvfINUQ=="], + + "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@1.6.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-ZHx0byqPOQ/WiAxHbfdvTp8HICWAzKje8kfZ6ihLu9ZLQ1nOo0Ri5FAgYTAmLMfMXgU5qZuNFQFdp4k5ONe3jQ=="], + + "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@1.6.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-de7/qKZN6y7G2OMnsCcmBXeXyS39G4r5uyiz8jRbxbObj2nqxmlNs48aHqOaKzqiJeSqip+z4ALY/AIAl6h4rw=="], + + "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@1.6.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-e+/9mDyhBeoDEwNPt4x8ubHztTsQE9kNHI9cBaE0Y+KIQPTxeV2W3OfPHfI5yTlKWtF8r3KN6kIRlXgaFvkBsw=="], + + "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@1.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xo4Ha1xDkmWp8LolKUtCsVifenjK3ReQNuJjcMt1PazAdd9QZR/Qn6Z8E3rbFjnWctSkhxR4wTd43z8Bkxb9vA=="], + + "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@1.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0tC6Bv1FT6Qz9YikQepOibsZa2HE0kxYSIK1jReQh2/3dBUGz2tHib5+G2rQ7ebkHxmwS9gQHAeMmm07XfemSQ=="], + + "@yuku-core/binding-android-arm64": ["@yuku-core/binding-android-arm64@0.19.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw=="], + + "@yuku-core/binding-darwin-arm64": ["@yuku-core/binding-darwin-arm64@0.19.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw=="], + + "@yuku-core/binding-darwin-x64": ["@yuku-core/binding-darwin-x64@0.19.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw=="], + + "@yuku-core/binding-freebsd-x64": ["@yuku-core/binding-freebsd-x64@0.19.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ=="], + + "@yuku-core/binding-linux-arm-gnu": ["@yuku-core/binding-linux-arm-gnu@0.19.0", "", { "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg=="], + + "@yuku-core/binding-linux-arm-musl": ["@yuku-core/binding-linux-arm-musl@0.19.0", "", { "os": "linux", "cpu": "arm", "libc": "musl" }, "sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg=="], + + "@yuku-core/binding-linux-arm64-gnu": ["@yuku-core/binding-linux-arm64-gnu@0.19.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA=="], + + "@yuku-core/binding-linux-arm64-musl": ["@yuku-core/binding-linux-arm64-musl@0.19.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ=="], + + "@yuku-core/binding-linux-x64-gnu": ["@yuku-core/binding-linux-x64-gnu@0.19.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ=="], + + "@yuku-core/binding-linux-x64-musl": ["@yuku-core/binding-linux-x64-musl@0.19.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ=="], + + "@yuku-core/binding-win32-arm64": ["@yuku-core/binding-win32-arm64@0.19.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg=="], - "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + "@yuku-core/binding-win32-x64": ["@yuku-core/binding-win32-x64@0.19.0", "", { "os": "win32", "cpu": "x64" }, "sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw=="], - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "@yuku-toolchain/types": ["@yuku-toolchain/types@0.19.0", "", {}, "sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "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=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -134,7 +370,7 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], "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=="], @@ -150,8 +386,12 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "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=="], @@ -176,6 +416,10 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fallow": ["fallow@3.42.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@fallow-cli/darwin-arm64": "3.42.0", "@fallow-cli/darwin-x64": "3.42.0", "@fallow-cli/linux-arm64-gnu": "3.42.0", "@fallow-cli/linux-arm64-musl": "3.42.0", "@fallow-cli/linux-x64-gnu": "3.42.0", "@fallow-cli/linux-x64-musl": "3.42.0", "@fallow-cli/win32-arm64-msvc": "3.42.0", "@fallow-cli/win32-x64-msvc": "3.42.0", "fallow-type-aware": "3.42.0" }, "bin": { "fallow": "bin/fallow", "fallow-lsp": "bin/fallow-lsp", "fallow-mcp": "bin/fallow-mcp" } }, "sha512-zVug6ujNPVVW706qDL5OZvjD+tINA/lav+PwaoM5yIZyLs2y4bspTZBqSggUi56AGpLuHDEerIJAYTIgQzHQfQ=="], + + "fallow-type-aware": ["fallow-type-aware@3.42.0", "", { "dependencies": { "typescript": "7.0.2" }, "peerDependencies": { "tsrs": "*" }, "optionalPeers": ["tsrs"], "bin": { "fallow-type-aware": "fallow-type-aware.mjs" } }, "sha512-TxCPA7ZGyTi30UQ3I20ybBrno6hWCOuxItd4g8GW8HkGrfwhepIsjgM1iK2Njv7rD8lgYHiIq+uVrHquXYm40w=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], @@ -184,6 +428,10 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -204,6 +452,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "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=="], @@ -236,11 +486,15 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -250,6 +504,12 @@ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "oxfmt": ["oxfmt@0.78.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.78.0", "@oxfmt/binding-android-arm64": "0.78.0", "@oxfmt/binding-darwin-arm64": "0.78.0", "@oxfmt/binding-darwin-x64": "0.78.0", "@oxfmt/binding-freebsd-x64": "0.78.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.78.0", "@oxfmt/binding-linux-arm-musleabihf": "0.78.0", "@oxfmt/binding-linux-arm64-gnu": "0.78.0", "@oxfmt/binding-linux-arm64-musl": "0.78.0", "@oxfmt/binding-linux-ppc64-gnu": "0.78.0", "@oxfmt/binding-linux-riscv64-gnu": "0.78.0", "@oxfmt/binding-linux-riscv64-musl": "0.78.0", "@oxfmt/binding-linux-s390x-gnu": "0.78.0", "@oxfmt/binding-linux-x64-gnu": "0.78.0", "@oxfmt/binding-linux-x64-musl": "0.78.0", "@oxfmt/binding-openharmony-arm64": "0.78.0", "@oxfmt/binding-win32-arm64-msvc": "0.78.0", "@oxfmt/binding-win32-ia32-msvc": "0.78.0", "@oxfmt/binding-win32-x64-msvc": "0.78.0" }, "peerDependencies": { "@astrojs/compiler-rs": "^0.5.0", "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["@astrojs/compiler-rs", "svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-uagX+70mKqh/YqxB6ON/lkxVPH6E1Z7pfP2x6XVRPPyiDCZtX6o1xIEeXAIXJ/2wA5ViX4RFuW+IAQn85yJdWg=="], + + "oxlint": ["oxlint@1.93.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.93.0", "@oxlint/binding-android-arm64": "1.93.0", "@oxlint/binding-darwin-arm64": "1.93.0", "@oxlint/binding-darwin-x64": "1.93.0", "@oxlint/binding-freebsd-x64": "1.93.0", "@oxlint/binding-linux-arm-gnueabihf": "1.93.0", "@oxlint/binding-linux-arm-musleabihf": "1.93.0", "@oxlint/binding-linux-arm64-gnu": "1.93.0", "@oxlint/binding-linux-arm64-musl": "1.93.0", "@oxlint/binding-linux-ppc64-gnu": "1.93.0", "@oxlint/binding-linux-riscv64-gnu": "1.93.0", "@oxlint/binding-linux-riscv64-musl": "1.93.0", "@oxlint/binding-linux-s390x-gnu": "1.93.0", "@oxlint/binding-linux-x64-gnu": "1.93.0", "@oxlint/binding-linux-x64-musl": "1.93.0", "@oxlint/binding-openharmony-arm64": "1.93.0", "@oxlint/binding-win32-arm64-msvc": "1.93.0", "@oxlint/binding-win32-ia32-msvc": "1.93.0", "@oxlint/binding-win32-x64-msvc": "1.93.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2005", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-19JMsQFX+YvEbcUTFQxjTwhvs0Ibg0D6CFneSLgpwnACWcY/6LjhpGpkPodh0T5aIT1pjMTLlYaeBFzaVgugTA=="], + + "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2006", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2006", "@oxlint-tsgolint/darwin-x64": "7.0.2006", "@oxlint-tsgolint/linux-arm64": "7.0.2006", "@oxlint-tsgolint/linux-x64": "7.0.2006", "@oxlint-tsgolint/win32-arm64": "7.0.2006", "@oxlint-tsgolint/win32-x64": "7.0.2006" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-5B3UtH71kycPHUC1kFEIffivEAs49SUmkJuHZOZnTY0NPj4legp0bcsCQucuLVNCoJzDucS68P2C4WIhP+LypQ=="], + "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=="], @@ -262,7 +522,13 @@ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -278,7 +544,9 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -290,9 +558,15 @@ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="], + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tsrs": ["tsrs@0.24.0-ts7.1.0-beta", "", { "optionalDependencies": { "@tsrs/darwin-arm64": "0.24.0-ts7.1.0-beta", "@tsrs/darwin-x64": "0.24.0-ts7.1.0-beta", "@tsrs/linux-arm64-gnu": "0.24.0-ts7.1.0-beta", "@tsrs/linux-arm64-musl": "0.24.0-ts7.1.0-beta", "@tsrs/linux-x64-gnu": "0.24.0-ts7.1.0-beta", "@tsrs/linux-x64-musl": "0.24.0-ts7.1.0-beta", "@tsrs/win32-x64": "0.24.0-ts7.1.0-beta" }, "bin": { "tsrs": "./bin/tsrs", "tsrs-lint": "./bin/tsrs-lint" } }, "sha512-Iy7q93qfRHcI0Y31IKx6fCsDQCsSjtrT01PIgOVolBXPJTMdm84oH1pEYyAMPQ/SnRqWDSWI8wEUHbv6e9TmHA=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], @@ -300,7 +574,9 @@ "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], - "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + "vite": ["@voidzero-dev/vite-plus-core@1.6.0", "", { "dependencies": { "@oxc-project/runtime": "=0.158.0", "@oxc-project/types": "=0.158.0", "lightningcss": "^1.33.0", "postcss": "^8.5.32", "yuku-parser": "0.19.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "1.6.0", "@voidzero-dev/vite-plus-darwin-x64": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-musl": "1.6.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "1.6.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "1.6.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@typescript/native-preview": "*", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "@volar/typescript": "~2.4.0", "@vue/language-core": "~3.2.0 || ~3.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*", "vue-tsc": "~3.2.0 || ~3.3.0", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@typescript/native-preview", "@vitejs/devtools", "@volar/typescript", "@vue/language-core", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "vue-tsc", "yaml"] }, "sha512-0UFh0AVjt3ZynvRk0BTCwysCeURhyZCoEGabEptwOzlvQjV2eGRcf5jPTfLGaKwgw2dKOQYH4vq/N3H7WUdIyw=="], + + "vite-plus": ["vite-plus@1.6.0", "", { "dependencies": { "@oxc-project/runtime": "=0.158.0", "@oxc-project/types": "=0.158.0", "@oxlint/plugins": "=1.93.0", "@vitest/browser": "5.1.0", "@vitest/browser-preview": "5.1.0", "@vitest/mocker": "5.1.0", "@vitest/pretty-format": "5.1.0", "@vitest/snapshot": "5.1.0", "@vitest/spy": "5.1.0", "@vitest/utils": "5.1.0", "oxfmt": "=0.78.0", "oxlint": "=1.93.0", "oxlint-tsgolint": "=7.0.2006", "vite": "npm:@voidzero-dev/vite-plus-core@1.6.0", "vitest": "5.1.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "1.6.0", "@voidzero-dev/vite-plus-darwin-x64": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-musl": "1.6.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "1.6.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "1.6.0" }, "peerDependencies": { "@vitest/browser-playwright": "5.1.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "playwright": "*" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio", "playwright"], "bin": { "vp": "./bin/vp", "vpr": "./bin/vpr", "vpx": "./bin/vpx" } }, "sha512-h6aIA80QcAhICBp2Xga8lPM9jOZm2VvPn7Ls531C7RjD9herrT1rPDUMY72IdFf1XyWDmFVEwO+W3EgXtSSMLQ=="], "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], @@ -312,14 +588,104 @@ "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=="], + "ws": ["ws@8.23.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-wCkmX8QMOrKjgO8HW2WyDbTSGeqkocxgDzS9iR79rEAexty6/2qzilx3Ag8Tqf9ykop3irLKle0MtVn5qjVnoQ=="], + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yuku-core": ["yuku-core@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0" }, "optionalDependencies": { "@yuku-core/binding-android-arm64": "0.19.0", "@yuku-core/binding-darwin-arm64": "0.19.0", "@yuku-core/binding-darwin-x64": "0.19.0", "@yuku-core/binding-freebsd-x64": "0.19.0", "@yuku-core/binding-linux-arm-gnu": "0.19.0", "@yuku-core/binding-linux-arm-musl": "0.19.0", "@yuku-core/binding-linux-arm64-gnu": "0.19.0", "@yuku-core/binding-linux-arm64-musl": "0.19.0", "@yuku-core/binding-linux-x64-gnu": "0.19.0", "@yuku-core/binding-linux-x64-musl": "0.19.0", "@yuku-core/binding-win32-arm64": "0.19.0", "@yuku-core/binding-win32-x64": "0.19.0" } }, "sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg=="], + + "yuku-parser": ["yuku-parser@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0", "yuku-core": "0.19.0" } }, "sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg=="], + "@celados/argc/yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + "@vitest/expect/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/expect/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "@vitest/mocker/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "@vitest/pretty-format/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + + "@vitest/runner/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "@vitest/snapshot/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "@vitest/utils/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + "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=="], + + "vite-plus/@vitest/browser": ["@vitest/browser@5.1.0", "", { "dependencies": { "@blazediff/core": "1.10.0", "@vitest/mocker": "5.1.0", "@vitest/ui": "5.1.0", "@vitest/utils": "5.1.0", "magic-string": "^1.4.3", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.2.0", "ws": "^8.22.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-2sjGVAC/Rs8Jwv+xxBgklNG2g1LGyNgcFMBw71AcfBstLz25UGv1+fVvvZ+z8IfSuNg3nipERijnFtoEIdRDEA=="], + + "vite-plus/@vitest/browser-preview": ["@vitest/browser-preview@5.1.0", "", { "dependencies": { "@testing-library/dom": "^10.4.2", "@testing-library/user-event": "^14.6.8", "@vitest/browser": "5.1.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-z9SbodBSMoJtYy5VTCaSP5SSZfNYRlfIlR0P/uH1PAuhUJbstSyOYvxXCYmqG+EemIiZzd3uGUtREX5zbXLR8A=="], + + "vite-plus/vitest": ["vitest@5.1.0", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.1.0", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.1.0", "@vitest/browser-preview": "5.1.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.1.0", "@vitest/coverage-v8": "5.1.0", "@vitest/ui": "5.1.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA=="], + + "vitest/@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "vitest/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "vitest/vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/mocker/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@vitest/runner/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/snapshot/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/@vitest/browser/@vitest/ui": ["@vitest/ui@5.1.0", "", { "dependencies": { "@vitest/utils": "5.1.0", "fflate": "^0.8.3", "flatted": "^3.4.4", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyrainbow": "^3.2.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-00S3x4NRmqIBUiTVWF9/xLaA9EP9KeKv0+1sBO7e2Y4luBISlW+2ttnhlk/zjbTqEvVhJWCCo7MXzu6zNbefqQ=="], + + "vite-plus/@vitest/browser/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "vite-plus/@vitest/browser/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + + "vite-plus/vitest/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "vite-plus/vitest/obug": ["obug@3.0.0", "", {}, "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="], + + "vite-plus/vitest/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "vite-plus/vitest/std-env": ["std-env@4.3.0", "", {}, "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag=="], + + "vite-plus/vitest/tinybench": ["tinybench@6.2.3", "", {}, "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw=="], + + "vite-plus/vitest/tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], + + "vite-plus/vitest/vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "vite-plus/vitest/why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="], + + "vitest/vite/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "vite-plus/@vitest/browser/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/vitest/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/vitest/vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "vite-plus/vitest/vite/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "vitest/vite/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "vitest/vite/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "vite-plus/vitest/vite/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "vite-plus/vitest/vite/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], } } diff --git a/package.json b/package.json index de9ce5e..f76c90b 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,47 @@ { - "name": "gkit", - "private": true, - "workspaces": { - "packages": [ - "packages/gkit" - ], - "catalog": { - "@types/bun": "^1.3.10", - "@types/node": "^25.6.0", - "@valibot/to-json-schema": "^1.6.0", - "ajv": "^8.20.0", - "@celados/argc": "^11.0.0", - "bun-types": "^1.3.13", - "dataforseo-client": "^2.0.25", - "dotenv": "^17.2.2", - "google-auth-library": "^10.9.0", - "typescript": "^5", - "valibot": "^1.2.0", - "vitest": "^4.0.18", - "yaml": "^2.9.0" - } - }, - "type": "module", - "scripts": { - "cli": "bun run --filter gkit cli", - "eval": "bun run --filter gkit eval:slice5", - "check-types": "bun run --filter gkit check-types", - "test": "bun run --filter gkit test:once", - "verify:package": "bun run ./packages/gkit/scripts/verify-package-artifact.ts" - }, - "devDependencies": { - "@types/bun": "catalog:", - "@types/node": "catalog:", - "bun-types": "catalog:", - "typescript": "catalog:", - "vitest": "catalog:" - }, - "packageManager": "bun@1.3.10" + "name": "gkit", + "private": true, + "workspaces": { + "packages": [ + "packages/gkit" + ], + "catalog": { + "@types/bun": "^1.3.10", + "@types/node": "^25.6.0", + "@valibot/to-json-schema": "^1.6.0", + "ajv": "^8.20.0", + "@celados/argc": "^11.0.0", + "bun-types": "^1.3.13", + "dataforseo-client": "^2.0.25", + "dotenv": "^17.2.2", + "google-auth-library": "^10.9.0", + "typescript": "^7.0.2", + "valibot": "^1.2.0", + "vitest": "^4.0.18", + "yaml": "^2.9.0" + } + }, + "type": "module", + "scripts": { + "cli": "bun run --filter gkit cli", + "eval": "bun run --filter gkit eval:slice5", + "typecheck": "harness typecheck", + "check": "vp check && harness typecheck && fallow dead-code && bun run generate:check && bun run test", + "generate:check": "bun run --filter gkit generate:check", + "test": "bun run --filter gkit test:once", + "verify:package": "bun run ./packages/gkit/scripts/verify-package-artifact.ts", + "prepare": "vp config" + }, + "devDependencies": { + "@celados/harness": "^0.1.3", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "bun-types": "catalog:", + "fallow": "^3.42.0", + "tsrs": "0.24.0-ts7.1.0-beta", + "typescript": "catalog:", + "vite-plus": "^1.6.0", + "vitest": "catalog:" + }, + "packageManager": "bun@1.3.10" } diff --git a/packages/gkit/package.json b/packages/gkit/package.json index 7c5c9d6..a1f3f16 100644 --- a/packages/gkit/package.json +++ b/packages/gkit/package.json @@ -1,57 +1,57 @@ { - "name": "gkit", - "version": "0.5.0", - "private": true, - "description": "Profile-bound, agent-first growth provider CLI.", - "bin": { - "gkit": "dist/gkit.js" - }, - "files": [ - "dist", - "docs", - "generated" - ], - "type": "module", - "sideEffects": false, - "scripts": { - "cli": "bun run ./src/cli.ts", - "build": "bun build ./bin/gkit.js --target=bun --minify --define GKIT_STANDALONE=true --outfile=dist/gkit.js && chmod +x dist/gkit.js", - "check-types": "tsc --noEmit", - "generate:dataforseo": "bun run ./scripts/generate-dataforseo.ts", - "generate:dataforseo:check": "bun run ./scripts/generate-dataforseo.ts --check", - "generate:bing": "bun run ./scripts/generate-bing.ts", - "generate:bing:check": "bun run ./scripts/generate-bing.ts --check", - "generate:google-ads": "bun run ./scripts/generate-google-ads.ts", - "generate:google-ads:check": "bun run ./scripts/generate-google-ads.ts --check", - "generate:gsc": "bun run ./scripts/generate-gsc.ts", - "generate:gsc:check": "bun run ./scripts/generate-gsc.ts --check", - "generate:hubspot": "bun run ./scripts/generate-hubspot.ts", - "generate:hubspot:check": "bun run ./scripts/generate-hubspot.ts --check", - "generate:pagespeed": "bun run ./scripts/generate-pagespeed.ts", - "generate:pagespeed:check": "bun run ./scripts/generate-pagespeed.ts --check", - "generate:posthog": "bun run ./scripts/generate-posthog.ts", - "generate:posthog:check": "bun run ./scripts/generate-posthog.ts --check", - "eval:slice5": "bun run ./src/eval.ts", - "test": "vitest", - "test:once": "vitest run && bun run test:site", - "test:site": "bun run ./scripts/test-site-snapshot.ts" - }, - "devDependencies": { - "@celados/argc": "^11.0.0", - "@standard-schema/spec": "^1.0.0", - "@types/bun": "^1.3.10", - "@types/node": "^25.6.0", - "@valibot/to-json-schema": "^1.6.0", - "ajv": "^8.20.0", - "bun-types": "^1.3.13", - "cheerio": "^1.2.0", - "dotenv": "^17.2.2", - "google-auth-library": "^10.9.0", - "robots-parser": "3.0.1", - "saxes": "6.0.0", - "typescript": "^5", - "valibot": "^1.2.0", - "vitest": "^4.0.18", - "yaml": "^2.9.0" - } + "name": "gkit", + "version": "0.5.0", + "private": true, + "description": "Profile-bound, agent-first growth provider CLI.", + "bin": { + "gkit": "dist/gkit.js" + }, + "files": [ + "dist", + "docs", + "generated" + ], + "type": "module", + "sideEffects": false, + "scripts": { + "cli": "bun run ./src/cli.ts", + "build": "bun build ./bin/gkit.js --target=bun --minify --define GKIT_STANDALONE=true --outfile=dist/gkit.js && chmod +x dist/gkit.js", + "generate:dataforseo": "bun run ./scripts/generate-dataforseo.ts", + "generate:dataforseo:check": "bun run ./scripts/generate-dataforseo.ts --check", + "generate:bing": "bun run ./scripts/generate-bing.ts", + "generate:bing:check": "bun run ./scripts/generate-bing.ts --check", + "generate:google-ads": "bun run ./scripts/generate-google-ads.ts", + "generate:google-ads:check": "bun run ./scripts/generate-google-ads.ts --check", + "generate:gsc": "bun run ./scripts/generate-gsc.ts", + "generate:gsc:check": "bun run ./scripts/generate-gsc.ts --check", + "generate:hubspot": "bun run ./scripts/generate-hubspot.ts", + "generate:hubspot:check": "bun run ./scripts/generate-hubspot.ts --check", + "generate:pagespeed": "bun run ./scripts/generate-pagespeed.ts", + "generate:pagespeed:check": "bun run ./scripts/generate-pagespeed.ts --check", + "generate:posthog": "bun run ./scripts/generate-posthog.ts", + "generate:posthog:check": "bun run ./scripts/generate-posthog.ts --check", + "eval:slice5": "bun run ./src/eval.ts", + "test": "vitest", + "test:once": "vitest run && bun run test:site", + "test:site": "bun run ./scripts/test-site-snapshot.ts", + "generate:check": "bun run generate:dataforseo:check && bun run generate:bing:check && bun run generate:google-ads:check && bun run generate:gsc:check && bun run generate:hubspot:check && bun run generate:pagespeed:check && bun run generate:posthog:check" + }, + "devDependencies": { + "@celados/argc": "^11.0.0", + "@standard-schema/spec": "^1.0.0", + "@types/bun": "^1.3.10", + "@types/node": "^25.6.0", + "@valibot/to-json-schema": "^1.6.0", + "ajv": "^8.20.0", + "bun-types": "^1.3.13", + "cheerio": "^1.2.0", + "dotenv": "^17.2.2", + "google-auth-library": "^10.9.0", + "robots-parser": "3.0.1", + "saxes": "6.0.0", + "typescript": "catalog:", + "valibot": "^1.2.0", + "vitest": "^4.0.18", + "yaml": "^2.9.0" + } } diff --git a/packages/gkit/src/execute-posthog.ts b/packages/gkit/src/execute-posthog.ts index 01aaaaf..f61dbad 100644 --- a/packages/gkit/src/execute-posthog.ts +++ b/packages/gkit/src/execute-posthog.ts @@ -12,7 +12,6 @@ import { import type { ParsedCommand } from "./args"; import { sha256CanonicalJson } from "./canonical"; import { - type Envelope, type EnvelopeMeta, GkitFailure, SecretRegistry, diff --git a/packages/gkit/tsconfig.json b/packages/gkit/tsconfig.json index 924f7bc..682ac40 100644 --- a/packages/gkit/tsconfig.json +++ b/packages/gkit/tsconfig.json @@ -1,13 +1,8 @@ { - "compilerOptions": { - "lib": ["ES2022", "DOM"], - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "noEmit": true, - "skipLibCheck": true, - "types": ["node", "bun-types"] - }, - "include": ["src/**/*.ts", "scripts/**/*.ts"] + "extends": "@celados/harness/tsconfig/bun.json", + "compilerOptions": { + "lib": ["ES2024", "DOM"], + "types": ["node", "bun-types"] + }, + "include": ["src/**/*.ts", "scripts/**/*.ts"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5fcf9d6 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,15 @@ +import { harness } from '@celados/harness' +import { defineConfig } from 'vite-plus' + +export default defineConfig( + harness({ + ignore: [ + '**/dist/**', + // Generated by `bun run generate:*`; `generate:check` verifies it is current. + 'packages/gkit/generated/**', + 'packages/gkit/docs/providers/**', + // Upstream provider specs, hashed into the generated manifests. + 'packages/gkit/sources/**', + ], + }), +) From 38c178094b782ba4326aa7cb47c27c2518e62e66 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Fri, 13 Nov 2026 14:59:38 +0800 Subject: [PATCH 2/4] style: harness oxfmt --- .fallowrc.jsonc | 6 +- README.md | 64 +- docs/cloud-provider-runtime.bdd.md | 108 +- .../clonesite-feedback-outreach-dogfood.md | 20 +- docs/gkit-product-architecture-context.md | 2 +- ...7-13-gkit-provider-native-agent-surface.md | 258 +-- .../2026-07-13-gkit-vertical-slice-plan.md | 160 +- packages/gkit/bin/gkit.js | 4 +- packages/gkit/docs/site.md | 27 +- packages/gkit/evals/baseline.md | 12 +- packages/gkit/evals/bing-migration-matrix.md | 40 +- .../gkit/evals/dataforseo-migration-matrix.md | 52 +- .../gkit/evals/google-ads-migration-matrix.md | 24 +- packages/gkit/evals/google-ads-rest-spike.md | 20 +- packages/gkit/evals/gsc-migration-matrix.md | 22 +- packages/gkit/evals/slice2-baseline.md | 10 +- .../gkit/evals/slice4-google-ads-baseline.md | 16 +- packages/gkit/evals/slice4-gsc-baseline.md | 14 +- packages/gkit/evals/slice5-baseline.md | 14 +- .../evals/slice5-final-migration-matrix.md | 32 +- packages/gkit/policy/bing.reviewed.json | 1329 +++++------ packages/gkit/policy/dataforseo.reviewed.json | 807 ++++--- packages/gkit/policy/google-ads.reviewed.json | 607 ++--- packages/gkit/policy/gsc.reviewed.json | 541 +++-- packages/gkit/policy/hubspot.reviewed.json | 737 +++--- packages/gkit/policy/pagespeed.reviewed.json | 127 +- packages/gkit/policy/posthog.reviewed.json | 160 +- packages/gkit/scripts/generate-bing.ts | 14 +- .../generate-contract-provider.test.ts | 51 +- .../scripts/generate-contract-provider.ts | 519 +++-- .../gkit/scripts/generate-dataforseo.test.ts | 72 +- packages/gkit/scripts/generate-dataforseo.ts | 531 +++-- .../gkit/scripts/generate-google-ads.test.ts | 84 +- packages/gkit/scripts/generate-google-ads.ts | 528 +++-- packages/gkit/scripts/generate-gsc.ts | 14 +- packages/gkit/scripts/generate-hubspot.ts | 14 +- packages/gkit/scripts/generate-pagespeed.ts | 14 +- .../gkit/scripts/generate-posthog.test.ts | 64 +- packages/gkit/scripts/generate-posthog.ts | 528 +++-- .../gkit/scripts/release-preflight.test.ts | 122 +- packages/gkit/scripts/release-preflight.ts | 416 ++-- packages/gkit/scripts/test-site-snapshot.ts | 656 +++--- .../gkit/scripts/verify-package-artifact.ts | 507 ++-- packages/gkit/src/args.test.ts | 518 +++-- packages/gkit/src/args.ts | 843 +++---- packages/gkit/src/artifact.test.ts | 330 +-- packages/gkit/src/artifact.ts | 1069 +++++---- packages/gkit/src/assets.embed.ts | 86 +- packages/gkit/src/assets.test.ts | 46 +- packages/gkit/src/assets.ts | 62 +- packages/gkit/src/canonical.ts | 22 +- packages/gkit/src/cli.test.ts | 1883 ++++++++------- packages/gkit/src/cli.ts | 1086 +++++---- packages/gkit/src/describe.ts | 43 +- packages/gkit/src/docs.test.ts | 52 +- packages/gkit/src/docs.ts | 146 +- packages/gkit/src/doctor-hubspot.test.ts | 263 ++- packages/gkit/src/doctor.ts | 1257 +++++----- packages/gkit/src/effects.test.ts | 512 ++-- packages/gkit/src/effects.ts | 452 ++-- packages/gkit/src/envelope.test.ts | 200 +- packages/gkit/src/envelope.ts | 397 ++-- packages/gkit/src/eval.test.ts | 44 +- packages/gkit/src/eval.ts | 483 ++-- packages/gkit/src/execute-bing.ts | 128 +- packages/gkit/src/execute-google-ads.test.ts | 437 ++-- packages/gkit/src/execute-google-ads.ts | 978 ++++---- packages/gkit/src/execute-gsc.test.ts | 553 +++-- packages/gkit/src/execute-gsc.ts | 162 +- packages/gkit/src/execute-hubspot.test.ts | 528 +++-- packages/gkit/src/execute-hubspot.ts | 125 +- packages/gkit/src/execute-pagespeed.test.ts | 305 +-- packages/gkit/src/execute-pagespeed.ts | 97 +- packages/gkit/src/execute-posthog.ts | 942 ++++---- packages/gkit/src/execute-read-provider.ts | 998 ++++---- packages/gkit/src/execute.test.ts | 1783 +++++++------- packages/gkit/src/execute.ts | 1603 ++++++------- packages/gkit/src/google-ads-credentials.ts | 128 +- packages/gkit/src/gsc-credentials.ts | 124 +- packages/gkit/src/hubspot-manifest.test.ts | 82 +- packages/gkit/src/journal.test.ts | 906 ++++---- packages/gkit/src/journal.ts | 2052 ++++++++++------- packages/gkit/src/manifest.test.ts | 316 +-- packages/gkit/src/manifest.ts | 498 ++-- packages/gkit/src/operation-recorder.ts | 124 +- packages/gkit/src/operations-view.ts | 146 +- packages/gkit/src/profile.test.ts | 864 +++---- packages/gkit/src/profile.ts | 897 +++---- packages/gkit/src/providers/bing.test.ts | 318 +-- packages/gkit/src/providers/bing.ts | 623 ++--- .../gkit/src/providers/dataforseo.test.ts | 1096 ++++----- packages/gkit/src/providers/dataforseo.ts | 1371 +++++------ .../gkit/src/providers/google-ads-auth.ts | 22 +- .../gkit/src/providers/google-ads.test.ts | 295 +-- packages/gkit/src/providers/google-ads.ts | 751 +++--- packages/gkit/src/providers/gsc-auth.ts | 28 +- packages/gkit/src/providers/gsc.test.ts | 476 ++-- packages/gkit/src/providers/gsc.ts | 616 ++--- packages/gkit/src/providers/hubspot.test.ts | 634 ++--- packages/gkit/src/providers/hubspot.ts | 1377 ++++++----- packages/gkit/src/providers/pagespeed.test.ts | 170 +- packages/gkit/src/providers/pagespeed.ts | 404 ++-- packages/gkit/src/providers/posthog.test.ts | 228 +- packages/gkit/src/providers/posthog.ts | 468 ++-- packages/gkit/src/providers/raw-json.ts | 34 +- packages/gkit/src/schema.ts | 503 ++-- packages/gkit/src/site/audit.ts | 283 +-- packages/gkit/src/site/capture.ts | 731 +++--- packages/gkit/src/site/commands.ts | 316 +-- packages/gkit/src/site/compare.ts | 331 +-- packages/gkit/src/site/config.ts | 92 +- packages/gkit/src/site/discover.test.ts | 441 ++-- packages/gkit/src/site/discover.ts | 603 ++--- packages/gkit/src/site/execute.ts | 518 +++-- packages/gkit/src/site/explain.ts | 416 ++-- packages/gkit/src/site/git-context.ts | 391 ++-- packages/gkit/src/site/model.ts | 346 +-- packages/gkit/src/site/schema.ts | 169 +- packages/gkit/src/site/site.test.ts | 416 ++-- packages/gkit/src/site/snapshot.ts | 310 +-- packages/gkit/src/skill.embed.ts | 6 +- packages/gkit/src/text-modules.d.ts | 6 +- 122 files changed, 26091 insertions(+), 22615 deletions(-) diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 78c21c1..e75cf22 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -1,7 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", // Generated by `bun run generate:*` (verified by `generate:check`). - "ignorePatterns": ["packages/gkit/generated/**", "packages/gkit/docs/providers/**", "packages/gkit/sources/**"], + "ignorePatterns": [ + "packages/gkit/generated/**", + "packages/gkit/docs/providers/**", + "packages/gkit/sources/**", + ], // Local-only helper that renders .npmrc from the vault; not a dependency. "ignoreBinaries": ["latch"], } diff --git a/README.md b/README.md index 22072a2..3689acc 100644 --- a/README.md +++ b/README.md @@ -84,38 +84,38 @@ Provider execution must bind exactly one App profile. Create one JSON file at ```json { - "version": 1, - "name": "my-app", - "providers": { - "posthog": { - "config": { - "host": "https://us.posthog.com", - "projectId": "12345" - }, - "secrets": { - "apiToken": "env:MY_APP_POSTHOG_TOKEN" - } - }, - "gsc": { - "config": { - "siteUrl": "sc-domain:example.com" - }, - "secrets": { - "serviceAccountFile": "env:MY_APP_GSC_SERVICE_ACCOUNT_FILE" - } - }, - "hubspot": { - "config": {}, - "secrets": { - "accessToken": "env:MY_APP_HUBSPOT_ACCESS_TOKEN" - } - }, - "pagespeed": { - "secrets": { - "apiKey": "env:MY_APP_PAGESPEED_API_KEY" - } - } - } + "version": 1, + "name": "my-app", + "providers": { + "posthog": { + "config": { + "host": "https://us.posthog.com", + "projectId": "12345" + }, + "secrets": { + "apiToken": "env:MY_APP_POSTHOG_TOKEN" + } + }, + "gsc": { + "config": { + "siteUrl": "sc-domain:example.com" + }, + "secrets": { + "serviceAccountFile": "env:MY_APP_GSC_SERVICE_ACCOUNT_FILE" + } + }, + "hubspot": { + "config": {}, + "secrets": { + "accessToken": "env:MY_APP_HUBSPOT_ACCESS_TOKEN" + } + }, + "pagespeed": { + "secrets": { + "apiKey": "env:MY_APP_PAGESPEED_API_KEY" + } + } + } } ``` diff --git a/docs/cloud-provider-runtime.bdd.md b/docs/cloud-provider-runtime.bdd.md index d5fe0da..786770d 100644 --- a/docs/cloud-provider-runtime.bdd.md +++ b/docs/cloud-provider-runtime.bdd.md @@ -6,6 +6,7 @@ ## 范围边界 **包含:** + - 云端按业务 profile 选择 provider 配置 - 云端通过平台 secret 注入 provider 凭证 - Agent 在任何真实读取前先执行 readiness 检查 @@ -14,6 +15,7 @@ - 多业务、多环境并存时的隔离规则 **不包含:** + - secret manager 的具体供应商实现 - provider 业务指标解释、报告、归因判断 - gkit provider capability 的具体查询字段设计 @@ -35,25 +37,25 @@ **场景 1.1:任务带有明确业务 profile 时启动** Given 云端任务声明了业务 profile - And 该 profile 对应的 provider secrets 已在平台中配置 +And 该 profile 对应的 provider secrets 已在平台中配置 When Agent 启动 provider 读取流程 Then Agent 使用该业务 profile 作为本次任务的唯一业务归属 - And 后续读取只使用该 profile 绑定的 provider secrets - And Agent 不需要知道 secret 的明文内容 +And 后续读取只使用该 profile 绑定的 provider secrets +And Agent 不需要知道 secret 的明文内容 **场景 1.2:任务没有声明业务 profile 时停止** Given 云端任务没有声明业务 profile When Agent 准备读取任意真实 provider 数据 Then Agent 停止读取流程 - And 返回“缺少业务 profile”的诊断 - And 不尝试使用默认业务或本地配置兜底 +And 返回“缺少业务 profile”的诊断 +And 不尝试使用默认业务或本地配置兜底 **场景 1.3:profile 存在但没有绑定目标 provider** Given 云端任务声明了业务 profile - And 该 profile 没有绑定目标 provider 的 secret +And 该 profile 没有绑定目标 provider 的 secret When Agent 请求该 provider 的真实数据 Then Agent 返回“该业务未配置目标 provider”的诊断 - And 不从其他业务 profile 借用 secret +And 不从其他业务 profile 借用 secret --- @@ -63,43 +65,43 @@ Then Agent 返回“该业务未配置目标 provider”的诊断 **场景 2.1:通过环境变量注入 service account JSON** Given 平台 secret 中保存了目标 provider 的 service account JSON - And 云端任务声明了正确业务 profile +And 云端任务声明了正确业务 profile When 运行时启动 gkit provider command Then CLI 可以从运行时环境读取该 provider 的凭证 - And 仓库中不会出现真实凭证文件 - And 输出日志不会打印 secret 明文 +And 仓库中不会出现真实凭证文件 +And 输出日志不会打印 secret 明文 **场景 2.2:必须使用临时文件时只写入短生命周期目录** Given 某个 provider 只能通过文件路径读取凭证 - And 平台 secret 中保存了 service account JSON +And 平台 secret 中保存了 service account JSON When 运行时准备 gkit provider 输入 Then 运行时把凭证写入任务级临时目录 - And CLI 只接收该临时文件路径 - And 任务结束后该临时文件随运行环境销毁 +And CLI 只接收该临时文件路径 +And 任务结束后该临时文件随运行环境销毁 **场景 2.3:本地 `.env.live` 不参与云端运行** Given 仓库中存在本地 live validation 文档 - And 开发机可以使用 `.env.live` +And 开发机可以使用 `.env.live` When 云端任务启动 Then 云端不读取 `.env.live` - And 云端只使用平台注入的 profile 与 secrets - And 本地配置缺失不会影响云端任务 +And 云端只使用平台注入的 profile 与 secrets +And 本地配置缺失不会影响云端任务 **场景 2.4:本地开发也按业务 profile 读取凭证** Given 开发机已经创建业务 profile 目录 - And profile 目录中保存了本地只读 provider 凭证 +And profile 目录中保存了本地只读 provider 凭证 When Agent 在本地执行 gkit provider command Then CLI 按业务 profile 读取 provider 配置 - And 不要求每个 provider 维护自己的凭证目录 - And repo-local `.env.live` 不是默认配置来源 +And 不要求每个 provider 维护自己的凭证目录 +And repo-local `.env.live` 不是默认配置来源 **场景 2.5:repo 只声明 profile id,不声明 provider 凭证** Given 开发机已经创建业务 profile 目录 - And 当前 repo 的 ignored `.env.local` 只声明了业务 profile id +And 当前 repo 的 ignored `.env.local` 只声明了业务 profile id When Agent 执行 gkit provider command Then CLI 先读取 repo 中的 profile id - And 再读取该 profile 目录下的 provider 配置 - And profile 中的 provider 配置不会被 repo fallback 配置覆盖 +And 再读取该 profile 目录下的 provider 配置 +And profile 中的 provider 配置不会被 repo fallback 配置覆盖 --- @@ -109,27 +111,27 @@ Then CLI 先读取 repo 中的 profile id **场景 3.1:readiness 通过后执行低量真实读取** Given 云端任务声明了业务 profile - And 目标 provider 的必要 secrets 已注入 +And 目标 provider 的必要 secrets 已注入 When Agent 准备读取真实 provider 数据 Then Agent 先执行 readiness 检查 - And readiness 通过后才执行低量真实读取 - And 低量读取成功后才允许执行更宽的数据读取 +And readiness 通过后才执行低量真实读取 +And 低量读取成功后才允许执行更宽的数据读取 **场景 3.2:readiness 未通过时停止业务读取** Given 云端任务声明了业务 profile - And 目标 provider 的 readiness 检查未通过 +And 目标 provider 的 readiness 检查未通过 When Agent 准备读取真实 provider 数据 Then Agent 停止后续 provider 读取 - And 返回可恢复诊断 - And 不用其他 provider 的数据推断该 provider 的 truth +And 返回可恢复诊断 +And 不用其他 provider 的数据推断该 provider 的 truth **场景 3.3:可选配置缺失不阻断不需要它的读取** Given 云端任务声明了业务 profile - And 某个 provider 的可选配置未注入 - And 本次读取不需要该可选配置 +And 某个 provider 的可选配置未注入 +And 本次读取不需要该可选配置 When Agent 执行 readiness 检查 Then readiness 可以带 warning 通过 - And Agent 可以继续执行不依赖该可选配置的读取 +And Agent 可以继续执行不依赖该可选配置的读取 --- @@ -139,27 +141,27 @@ Then readiness 可以带 warning 通过 **场景 4.1:缺失必要 secret 时返回配置诊断** Given 云端任务声明了业务 profile - And 目标 provider 缺少必要 secret +And 目标 provider 缺少必要 secret When Agent 执行 readiness 检查 Then Agent 收到“必要 secret 缺失”的诊断 - And 诊断包含缺失项名称 - And 不发起真实 provider 查询 +And 诊断包含缺失项名称 +And 不发起真实 provider 查询 **场景 4.2:凭证存在但无权访问目标业务资源** Given 目标 provider 的凭证已注入 - And 该凭证无权访问当前 profile 绑定的业务资源 +And 该凭证无权访问当前 profile 绑定的业务资源 When Agent 执行低量真实读取 Then Agent 收到“provider 权限不足”的诊断 - And 诊断能与“查询结果为空”区分开 - And Agent 不把该失败解释为业务资源没有数据 +And 诊断能与“查询结果为空”区分开 +And Agent 不把该失败解释为业务资源没有数据 **场景 4.3:provider 暂时不可用时保留原始失败** Given 目标 provider 的凭证和 profile 配置都正确 - And provider 返回临时失败或限流 +And provider 返回临时失败或限流 When Agent 执行真实读取 Then Agent 返回“provider 暂不可用”的诊断 - And 标记该失败是否可以重试 - And 不用缓存、猜测或其他 provider 数据替代本次 provider truth +And 标记该失败是否可以重试 +And 不用缓存、猜测或其他 provider 数据替代本次 provider truth --- @@ -169,36 +171,36 @@ Then Agent 返回“provider 暂不可用”的诊断 **场景 5.1:两个业务 profile 同时存在时互不串用** Given 平台中同时存在多个业务 profile 的 secrets - And 云端任务声明使用其中一个 profile +And 云端任务声明使用其中一个 profile When Agent 读取 provider truth Then Agent 只使用该 profile 绑定的 secrets - And 不读取其他 profile 的 provider secrets - And 输出中标明本次使用的业务 profile +And 不读取其他 profile 的 provider secrets +And 输出中标明本次使用的业务 profile **场景 5.2:生产与测试环境同时存在时必须显式选择** Given 同一业务同时存在生产和测试 secrets When Agent 准备读取真实 provider 数据 Then Agent 必须使用任务声明的环境 - And 未声明环境时停止读取流程 - And 不默认使用生产环境 +And 未声明环境时停止读取流程 +And 不默认使用生产环境 **场景 5.3:只读 provider 读取不产生业务侧副作用** Given Agent 使用业务 profile 读取 provider truth When Agent 执行只读 gkit provider command Then provider 侧不会产生写入、变更、投放调整或账务动作 - And 本次任务只产出可审计的事实输出或诊断 +And 本次任务只产出可审计的事实输出或诊断 --- ## 配置状态真值表 -| 业务 profile | 必要 secrets | readiness | 真实读取 | Agent 行为 | -|---|---|---|---|---| -| 已声明 | 完整 | 通过 | 允许低量读取 | 先验证再扩大读取 | -| 已声明 | 缺失 | 失败 | 不允许 | 返回缺失配置诊断 | -| 未声明 | * | 不执行 | 不允许 | 返回缺少 profile 诊断 | -| 已声明 | 完整但无权限 | 可通过或读取失败 | 不允许扩大读取 | 返回权限诊断 | -| 已声明 | 完整但 provider 限流 | 失败或读取失败 | 可稍后重试 | 返回可重试诊断 | +| 业务 profile | 必要 secrets | readiness | 真实读取 | Agent 行为 | +| ------------ | -------------------- | ---------------- | -------------- | --------------------- | +| 已声明 | 完整 | 通过 | 允许低量读取 | 先验证再扩大读取 | +| 已声明 | 缺失 | 失败 | 不允许 | 返回缺失配置诊断 | +| 未声明 | * | 不执行 | 不允许 | 返回缺少 profile 诊断 | +| 已声明 | 完整但无权限 | 可通过或读取失败 | 不允许扩大读取 | 返回权限诊断 | +| 已声明 | 完整但 provider 限流 | 失败或读取失败 | 可稍后重试 | 返回可重试诊断 | ## 验收说明 diff --git a/docs/evals/clonesite-feedback-outreach-dogfood.md b/docs/evals/clonesite-feedback-outreach-dogfood.md index 5da39e9..e966619 100644 --- a/docs/evals/clonesite-feedback-outreach-dogfood.md +++ b/docs/evals/clonesite-feedback-outreach-dogfood.md @@ -6,7 +6,7 @@ description: > contacting, and learning from paid Clonesite customers. status: conditional-pass # conditional-pass | passed | failed version: 1.0 -generated: { by: codex/gpt-5, at: "2026-08-14T21:30:00+08:00" } +generated: { by: codex/gpt-5, at: '2026-08-14T21:30:00+08:00' } tags: [gkit, growth-workspace, gmail, posthog, clonesite, dogfood] --- @@ -81,15 +81,15 @@ commercial outcome. ## Acceptance gates -| Gate | Passing evidence | -| --- | --- | -| Eligibility | Product state supports the selected cohort and exclusions | -| Contact safety | No refund, suppression, active support conflict, or duplicate direct thread | -| Personalization | PostHog context is bounded, identity-grounded, and not exposed as surveillance | -| Copy | One primary question, founder voice, verified preview URL, truthful service scope | -| Authority | Draft and send permissions are explicit and separate | -| Send | Intended recipient, subject, thread, and `SENT` label read back from Gmail | -| Evaluation | Reply, bounce, complaint, later Product outcome, and service interest tracked separately | +| Gate | Passing evidence | +| --------------- | ---------------------------------------------------------------------------------------- | +| Eligibility | Product state supports the selected cohort and exclusions | +| Contact safety | No refund, suppression, active support conflict, or duplicate direct thread | +| Personalization | PostHog context is bounded, identity-grounded, and not exposed as surveillance | +| Copy | One primary question, founder voice, verified preview URL, truthful service scope | +| Authority | Draft and send permissions are explicit and separate | +| Send | Intended recipient, subject, thread, and `SENT` label read back from Gmail | +| Evaluation | Reply, bounce, complaint, later Product outcome, and service interest tracked separately | ## Product findings, not workflow facts diff --git a/docs/gkit-product-architecture-context.md b/docs/gkit-product-architecture-context.md index 1a5d290..d3942e9 100644 --- a/docs/gkit-product-architecture-context.md +++ b/docs/gkit-product-architecture-context.md @@ -4,7 +4,7 @@ title: gkit Product Architecture Context description: The confirmed minimum product boundary and first vertical slice for gkit. status: active version: 0.4 -generated: { by: codex/gpt-5, at: "2026-08-14T14:39:07+08:00" } +generated: { by: codex/gpt-5, at: '2026-08-14T14:39:07+08:00' } tags: [gkit, growth-workspace, agent-native] --- diff --git a/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md b/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md index 87be614..5149e45 100644 --- a/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md +++ b/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md @@ -103,18 +103,18 @@ gkit --profile app-a bing sites list 搜索结果必须区分 contract owner: ```ts -type CapabilityKind = "curated" | "provider"; +type CapabilityKind = 'curated' | 'provider' type CapabilitySummary = { - id: string; - provider: string; - kind: CapabilityKind; - contractOwner: "gkit" | "provider"; - description: string; - command: string; - schemaSelector: string; - effects: Effect[]; -}; + id: string + provider: string + kind: CapabilityKind + contractOwner: 'gkit' | 'provider' + description: string + command: string + schemaSelector: string + effects: Effect[] +} ``` Curated command 是 gkit 承诺的稳定、normalized contract;provider capability 明确跟随 provider 版本和原生返回。 @@ -124,31 +124,31 @@ Curated command 是 gkit 承诺的稳定、normalized contract;provider capabi 统一到 module interface 就停止: ```ts -type Effect = "read" | "spend" | "write" | "destructive"; +type Effect = 'read' | 'spend' | 'write' | 'destructive' type ProviderModule = { - id: string; - description: string; - schema: CommandTree; - handlers: HandlerTree; - profileSchema: JsonSchema; - runtime: ProviderRuntime; - catalog: { - search(query: string, limit: number): Promise; - describe(id: string): Promise; - }; - doctor(context: ProviderContext): Promise; -}; + id: string + description: string + schema: CommandTree + handlers: HandlerTree + profileSchema: JsonSchema + runtime: ProviderRuntime + catalog: { + search(query: string, limit: number): Promise + describe(id: string): Promise + } + doctor(context: ProviderContext): Promise +} type ProviderRuntime = - | { kind: "in-process"; language: "typescript" } - | { - kind: "process"; - language: "python" | "other"; - protocol: "gkit-provider-v1"; - setup: () => Promise; - probe: () => Promise; - }; + | { kind: 'in-process'; language: 'typescript' } + | { + kind: 'process' + language: 'python' | 'other' + protocol: 'gkit-provider-v1' + setup: () => Promise + probe: () => Promise + } ``` `ProviderRuntime` 只统一进程边界,不统一 provider API。DataForSEO、GSC、PostHog 可以使用 TypeScript in-process adapter;Google Ads 使用官方 Python SDK subprocess。 @@ -195,25 +195,25 @@ Core 不拥有: Profile 是真实业务维度:它回答“这次调用代表哪个 App,以及该 App 在每个 provider 中对应哪个 account/resource”。 ```ts -type SecretRef = `env:${string}`; +type SecretRef = `env:${string}` type GkitProfile = { - version: 1; - name: string; - providers: Record< - string, - { - config: Record; - secrets: Record; - } - >; -}; + version: 1 + name: string + providers: Record< + string, + { + config: Record + secrets: Record + } + > +} type ProviderContext = { - profile: string; - config: Record; - resolveSecret(name: string): string; -}; + profile: string + config: Record + resolveSecret(name: string): string +} ``` Profile 文件位于: @@ -227,37 +227,37 @@ $XDG_CONFIG_HOME/gkit/profiles/.json # XDG_CONFIG_HOME 已设置 ```json { - "version": 1, - "name": "app-a", - "providers": { - "posthog": { - "config": { - "host": "https://us.posthog.com", - "projectId": "12345" - }, - "secrets": { - "apiToken": "env:APP_A_POSTHOG_TOKEN" - } - }, - "gsc": { - "config": { - "siteUrl": "sc-domain:app-a.com" - }, - "secrets": { - "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" - } - }, - "google-ads": { - "config": { - "customerId": "1234567890", - "loginCustomerId": "0987654321" - }, - "secrets": { - "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", - "credentialsJson": "env:APP_A_GOOGLE_ADS_CREDENTIALS_JSON" - } - } - } + "version": 1, + "name": "app-a", + "providers": { + "posthog": { + "config": { + "host": "https://us.posthog.com", + "projectId": "12345" + }, + "secrets": { + "apiToken": "env:APP_A_POSTHOG_TOKEN" + } + }, + "gsc": { + "config": { + "siteUrl": "sc-domain:app-a.com" + }, + "secrets": { + "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" + } + }, + "google-ads": { + "config": { + "customerId": "1234567890", + "loginCustomerId": "0987654321" + }, + "secrets": { + "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", + "credentialsJson": "env:APP_A_GOOGLE_ADS_CREDENTIALS_JSON" + } + } + } } ``` @@ -294,13 +294,13 @@ Effect 必须由 provider module 显式标注;生成器只能提供候选, ## 3. Provider 映射 -| Provider | Discovery source | Native execution | Curated 的角色 | -|---|---|---|---| -| DataForSEO | 官方 OpenAPI | operation ID + native JSON input | 高频 SEO/GEO reads 的稳定 contract | -| GSC | Google Discovery | resource/method call | 常用 Search Analytics、Inspection 等默认值与输出 | -| Google Ads | 官方 Python SDK + field metadata | GAQL;未来 SDK service methods | 常用 performance、Keyword Planner reads | -| PostHog | instance/OpenAPI + query kinds | HogQL/query;read API | 常用 event/funnel/instrumentation reads | -| Bing Webmaster | WSDL/interface inventory | provider method + native input | 常用 site/traffic/crawl/link reads | +| Provider | Discovery source | Native execution | Curated 的角色 | +| -------------- | -------------------------------- | -------------------------------- | ------------------------------------------------ | +| DataForSEO | 官方 OpenAPI | operation ID + native JSON input | 高频 SEO/GEO reads 的稳定 contract | +| GSC | Google Discovery | resource/method call | 常用 Search Analytics、Inspection 等默认值与输出 | +| Google Ads | 官方 Python SDK + field metadata | GAQL;未来 SDK service methods | 常用 performance、Keyword Planner reads | +| PostHog | instance/OpenAPI + query kinds | HogQL/query;read API | 常用 event/funnel/instrumentation reads | +| Bing Webmaster | WSDL/interface inventory | provider method + native input | 常用 site/traffic/crawl/link reads | ### DataForSEO @@ -348,18 +348,18 @@ Discovery methods 可以生成 provider capabilities;read 与 write scope 分 ```json { - "ok": true, - "data": {}, - "meta": { - "profile": "app-a", - "provider": "dataforseo", - "capability": "dataforseo.backlinks.bulk_ranks.live", - "kind": "provider", - "effects": ["read", "spend"], - "requestId": "optional", - "cost": {"amount": 0.02, "currency": "USD"}, - "artifact": null - } + "ok": true, + "data": {}, + "meta": { + "profile": "app-a", + "provider": "dataforseo", + "capability": "dataforseo.backlinks.bulk_ranks.live", + "kind": "provider", + "effects": ["read", "spend"], + "requestId": "optional", + "cost": { "amount": 0.02, "currency": "USD" }, + "artifact": null + } } ``` @@ -367,14 +367,14 @@ Discovery methods 可以生成 provider capabilities;read 与 write scope 分 ```json { - "ok": false, - "error": { - "code": "EFFECT_NOT_ALLOWED", - "message": "This capability can spend provider credits.", - "hint": "Review the request, then rerun with --allow-spend.", - "retryable": false, - "providerCode": null - } + "ok": false, + "error": { + "code": "EFFECT_NOT_ALLOWED", + "message": "This capability can spend provider credits.", + "hint": "Review the request, then rerun with --allow-spend.", + "retryable": false, + "providerCode": null + } } ``` @@ -415,14 +415,14 @@ Provider 原始错误保留在 `details`,不伪装成统一业务错误。 ### Polyglot runtime failure modes -| Failure | Result | Required handling | -|---|---|---| -| Provider runtime 未安装 | 仅该 provider blocked | doctor 返回 setup command;root discovery 继续工作 | -| SDK/API version 不兼容 | 仅该 provider blocked | setup/doctor 校验 compatibility manifest,不尝试降级猜测 | -| Child process hang | 当前调用失败 | timeout 后终止整个 process group,返回 retryable runtime error | -| stdout 非法或过大 | 当前调用失败 | 拒绝解析并保留截断后的非敏感诊断 | -| stderr 包含敏感值 | 安全失败 | 输出前经过已解析 secret redaction,测试覆盖异常路径 | -| Python provider crash | 当前调用失败 | 非零 exit 映射为 provider runtime error,不影响其他 modules | +| Failure | Result | Required handling | +| ----------------------- | --------------------- | -------------------------------------------------------------- | +| Provider runtime 未安装 | 仅该 provider blocked | doctor 返回 setup command;root discovery 继续工作 | +| SDK/API version 不兼容 | 仅该 provider blocked | setup/doctor 校验 compatibility manifest,不尝试降级猜测 | +| Child process hang | 当前调用失败 | timeout 后终止整个 process group,返回 retryable runtime error | +| stdout 非法或过大 | 当前调用失败 | 拒绝解析并保留截断后的非敏感诊断 | +| stderr 包含敏感值 | 安全失败 | 输出前经过已解析 secret redaction,测试覆盖异常路径 | +| Python provider crash | 当前调用失败 | 非零 exit 映射为 provider runtime error,不影响其他 modules | ## 6. ADR-001: 选择一个 public binary + polyglot provider-owned execution @@ -479,11 +479,11 @@ Provider 原始错误保留在 `details`,不伪装成统一业务错误。 **Step 1: 写失败测试** ```ts -expect(run(["--schema"]).stdout).toContain("search"); -expect(run(["--schema"]).stdout).toContain("describe"); -expect(run(["--schema"]).stdout).toContain("profile"); -expect(run(["--schema"]).stdout).toContain("dataforseo"); -expect(run(["--schema=.dataforseo"]).stdout).not.toContain("posthog"); +expect(run(['--schema']).stdout).toContain('search') +expect(run(['--schema']).stdout).toContain('describe') +expect(run(['--schema']).stdout).toContain('profile') +expect(run(['--schema']).stdout).toContain('dataforseo') +expect(run(['--schema=.dataforseo']).stdout).not.toContain('posthog') ``` **Step 2: 建立 root contract** @@ -492,8 +492,8 @@ Root package identity 固定为: ```json { - "name": "@deniffer/gkit", - "bin": {"gkit": "bin/gkit.js"} + "name": "@deniffer/gkit", + "bin": { "gkit": "bin/gkit.js" } } ``` @@ -553,9 +553,9 @@ git commit -m "feat: establish single gkit runtime" **Tests:** ```ts -expect(search("campaign performance")[0].kind).toBe("curated"); -expect(describe(id).command).toMatch(/^gkit google-ads /); -expect(() => describe("missing")).toThrowError("CAPABILITY_NOT_FOUND"); +expect(search('campaign performance')[0].kind).toBe('curated') +expect(describe(id).command).toMatch(/^gkit google-ads /) +expect(() => describe('missing')).toThrowError('CAPABILITY_NOT_FOUND') ``` ```bash @@ -663,15 +663,15 @@ git commit -m "feat: enforce gkit execution policy" ```ts type ProviderProcessRequest = { - version: 1; - command: string; - input: unknown; - context: { profile: string; config: Record }; -}; + version: 1 + command: string + input: unknown + context: { profile: string; config: Record } +} type ProviderProcessResponse = - | { ok: true; data: unknown; meta?: Record } - | { ok: false; error: ProviderError }; + | { ok: true; data: unknown; meta?: Record } + | { ok: false; error: ProviderError } ``` Secret value 不属于 request type。Provider-owned environment mapper 从 `ProviderContext.resolveSecret()` 构造 allowlisted child env;不继承完整 parent environment。 diff --git a/docs/plans/2026-07-13-gkit-vertical-slice-plan.md b/docs/plans/2026-07-13-gkit-vertical-slice-plan.md index b9794da..07b0f8c 100644 --- a/docs/plans/2026-07-13-gkit-vertical-slice-plan.md +++ b/docs/plans/2026-07-13-gkit-vertical-slice-plan.md @@ -58,35 +58,35 @@ Profile 文件:`$XDG_CONFIG_HOME/gkit/profiles/.json`(未设置时 `~/.con ```json { - "version": 1, - "name": "app-a", - "providers": { - "dataforseo": { - "config": {}, - "policy": { "maxSpendUsdPerCall": "0.50" }, - "secrets": { - "login": "env:APP_A_DATAFORSEO_LOGIN", - "password": "env:APP_A_DATAFORSEO_PASSWORD" - } - }, - "posthog": { - "config": { "host": "https://us.posthog.com", "projectId": "12345" }, - "secrets": { "apiToken": "env:APP_A_POSTHOG_TOKEN" } - }, - "gsc": { - "config": { "siteUrl": "sc-domain:app-a.com" }, - "secrets": { "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" } - }, - "google-ads": { - "config": { - "customerId": "1234567890" - }, - "secrets": { - "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", - "serviceAccountFile": "env:APP_A_GOOGLE_ADS_SERVICE_ACCOUNT_FILE" - } - } - } + "version": 1, + "name": "app-a", + "providers": { + "dataforseo": { + "config": {}, + "policy": { "maxSpendUsdPerCall": "0.50" }, + "secrets": { + "login": "env:APP_A_DATAFORSEO_LOGIN", + "password": "env:APP_A_DATAFORSEO_PASSWORD" + } + }, + "posthog": { + "config": { "host": "https://us.posthog.com", "projectId": "12345" }, + "secrets": { "apiToken": "env:APP_A_POSTHOG_TOKEN" } + }, + "gsc": { + "config": { "siteUrl": "sc-domain:app-a.com" }, + "secrets": { "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" } + }, + "google-ads": { + "config": { + "customerId": "1234567890" + }, + "secrets": { + "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", + "serviceAccountFile": "env:APP_A_GOOGLE_ADS_SERVICE_ACCOUNT_FILE" + } + } + } } ``` @@ -117,19 +117,19 @@ Effect 不可按 HTTP method 推断(GSC Search Analytics 是 POST+read;DataForSE ```json { - "ok": true, - "data": {}, - "meta": { - "profile": "app-a", - "provider": "dataforseo", - "capability": "dataforseo.backlinks.bulk_ranks.live", - "effects": ["read", "spend"], - "cost": { "amount": "0.020000", "currency": "USD" }, - "artifact": null, - "attemptId": "01J...", - "spendOutcome": "confirmed_charged", - "providerRequestId": "provider-request-id" - } + "ok": true, + "data": {}, + "meta": { + "profile": "app-a", + "provider": "dataforseo", + "capability": "dataforseo.backlinks.bulk_ranks.live", + "effects": ["read", "spend"], + "cost": { "amount": "0.020000", "currency": "USD" }, + "artifact": null, + "attemptId": "01J...", + "spendOutcome": "confirmed_charged", + "providerRequestId": "provider-request-id" + } } ``` @@ -137,15 +137,15 @@ Effect 不可按 HTTP method 推断(GSC Search Analytics 是 POST+read;DataForSE ```json { - "ok": false, - "error": { - "code": "EFFECT_NOT_ALLOWED", - "message": "This capability can spend provider credits.", - "hint": "Review the request, then rerun with --allow-spend.", - "retryable": false, - "outcome": "not_dispatched", - "details": null - } + "ok": false, + "error": { + "code": "EFFECT_NOT_ALLOWED", + "message": "This capability can spend provider credits.", + "hint": "Review the request, then rerun with --allow-spend.", + "retryable": false, + "outcome": "not_dispatched", + "details": null + } } ``` @@ -241,13 +241,13 @@ gkit docs [--provider

] # 打印文档目录路径,agent 自行 r ## 4. Provider 映射(全部 in-process TypeScript) -| Provider | Contract 来源 | 执行面 | 认证 | -| -------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| DataForSEO | 官方 OpenAPI(pinned) | `api call --operation-id --input @req.json` | basic auth(login/password) | -| GSC | Google Discovery | `search-analytics query` 等 resource/method | service account JSON | -| PostHog | Query API + OpenAPI reads | `query hogql --input @query.json` | personal API token | +| Provider | Contract 来源 | 执行面 | 认证 | +| -------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| DataForSEO | 官方 OpenAPI(pinned) | `api call --operation-id --input @req.json` | basic auth(login/password) | +| GSC | Google Discovery | `search-analytics query` 等 resource/method | service account JSON | +| PostHog | Query API + OpenAPI reads | `query hogql --input @query.json` | personal API token | | Google Ads | 官方 REST reference(`googleAds:search`、`googleAdsFields:search`、Keyword Plan services),固定 API major version | `query gaql`(默认显式分页)、`fields search/describe`、经保留决策的 Keyword Planner read | service-account JSON(当前 profile)或 OAuth2 refresh token + developer token;access token 作为派生 secret | -| Bing Webmaster | 现有 JSON REST API contract,WSDL 仅在 REST 不覆盖时考虑 | provider method + native input | 现有 API key 走 `apikey` query parameter(完整 URL 永不打印);未来采用 OAuth 时才用 Bearer | +| Bing Webmaster | 现有 JSON REST API contract,WSDL 仅在 REST 不覆盖时考虑 | provider method + native input | 现有 API key 走 `apikey` query parameter(完整 URL 永不打印);未来采用 OAuth 时才用 Bearer | 关于 Google Ads 的决策:v1 的 bounded read surface 走 REST;默认使用可控的 `googleAds:search` 分页,不以 `searchStream` 一次吞入任意大结果。Slice 1.5 已用真实账号覆盖 service-account OAuth、field metadata、request ID/error projection、分页和当前 Keyword Planner 用法;真实 profile 没有 MCC,因此 manager account header 仍是暴露该能力前的独立 live gate。当前唯一消费者的单账户 surface 不被这个缺口阻断。未来若开放 mutate、REST 无法覆盖必需能力或引入不可信依赖,再设计 polyglot/isolated runtime。 @@ -342,29 +342,29 @@ Raw dogfood event 已追加到 `$XDG_STATE_HOME/gkit/dogfood/events.jsonl`(未 ```json { - "ts": "", - "windowId": "slice1-2026-07-14", - "taskId": "", - "kind": "explicit_provider|business_goal|negative", - "eligible": true, - "route": "gkit|legacy_keep|blocked|break_glass", - "profile": "clonesite.ai|null", - "capability": "", - "targetCount": 1, - "discoverySteps": 0, - "firstExecutableCommandCorrect": true, - "paidDispatch": false, - "providerNetworkCalls": 0, - "attemptId": null, - "artifactBytes": null, - "artifactSha256": null, - "externalMaxCostMicros": null, - "externalActualCostMicros": null, - "externalEvidenceRef": null, - "result": "success|safe_block|failure", - "artifactUsedDownstream": false, - "fallbackReason": null, - "friction": null + "ts": "", + "windowId": "slice1-2026-07-14", + "taskId": "", + "kind": "explicit_provider|business_goal|negative", + "eligible": true, + "route": "gkit|legacy_keep|blocked|break_glass", + "profile": "clonesite.ai|null", + "capability": "", + "targetCount": 1, + "discoverySteps": 0, + "firstExecutableCommandCorrect": true, + "paidDispatch": false, + "providerNetworkCalls": 0, + "attemptId": null, + "artifactBytes": null, + "artifactSha256": null, + "externalMaxCostMicros": null, + "externalActualCostMicros": null, + "externalEvidenceRef": null, + "result": "success|safe_block|failure", + "artifactUsedDownstream": false, + "fallbackReason": null, + "friction": null } ``` diff --git a/packages/gkit/bin/gkit.js b/packages/gkit/bin/gkit.js index e10dbb2..bb95b41 100755 --- a/packages/gkit/bin/gkit.js +++ b/packages/gkit/bin/gkit.js @@ -1,4 +1,4 @@ #!/usr/bin/env bun -import { main } from "../src/cli.ts"; +import { main } from '../src/cli.ts' -await main(); +await main() diff --git a/packages/gkit/docs/site.md b/packages/gkit/docs/site.md index 467ea76..1a23ff6 100644 --- a/packages/gkit/docs/site.md +++ b/packages/gkit/docs/site.md @@ -73,16 +73,23 @@ Site configuration defaults to `//site.json`; t ```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"] }] + "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"] + } + ] } ``` diff --git a/packages/gkit/evals/baseline.md b/packages/gkit/evals/baseline.md index 47ca8ef..138c757 100644 --- a/packages/gkit/evals/baseline.md +++ b/packages/gkit/evals/baseline.md @@ -20,12 +20,12 @@ negative tasks made no provider calls and added no ledger attempts. ## Results -| Task | Kind | Discovery | Provider calls | Result | Cost | -| --- | --- | ---: | ---: | --- | ---: | -| `no_spend_negative` | negative | 1 step | 0 | discovery only; safely blocked | `$0` | -| `unsupported_mutation_negative` | negative | 1 step | 0 | exit 1, `CAPABILITY_NOT_FOUND`, `not_dispatched` | `$0` | -| `dfs_bulk_ranks_explicit` | explicit provider | 1 step | 1 | first live attempt succeeded | `$0.024072` | -| `compare_domain_authority_goal` | business goal | 2 steps | 1 | first live attempt succeeded | `$0.024108` | +| Task | Kind | Discovery | Provider calls | Result | Cost | +| ------------------------------- | ----------------- | --------: | -------------: | ------------------------------------------------ | ----------: | +| `no_spend_negative` | negative | 1 step | 0 | discovery only; safely blocked | `$0` | +| `unsupported_mutation_negative` | negative | 1 step | 0 | exit 1, `CAPABILITY_NOT_FOUND`, `not_dispatched` | `$0` | +| `dfs_bulk_ranks_explicit` | explicit provider | 1 step | 1 | first live attempt succeeded | `$0.024072` | +| `compare_domain_authority_goal` | business goal | 2 steps | 1 | first live attempt succeeded | `$0.024108` | Authorized exposure and actual cost were both `$0.048180`, below the `$0.15` gate cap. diff --git a/packages/gkit/evals/bing-migration-matrix.md b/packages/gkit/evals/bing-migration-matrix.md index 5fd9fad..3366a68 100644 --- a/packages/gkit/evals/bing-migration-matrix.md +++ b/packages/gkit/evals/bing-migration-matrix.md @@ -26,26 +26,26 @@ doctor. ## `@deniffer/bing-webmaster-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | keep | `gkit bing doctor` validates local config but intentionally does not preserve the legacy network probe | -| `site dataset sites` | replace | `bing.sites.list`; both returned the same five sites | -| `traffic dataset rank` | replace | `bing.traffic.rank`; same seven rows | -| `traffic dataset queries` | replace | `bing.traffic.queries`; same 35 rows, with provider order varying between requests | -| `traffic dataset pages` | replace | `bing.traffic.pages`; same 28 rows | -| `traffic entity query` | replace | `bing.traffic.query`; same six rows for the selected query | -| `traffic entity pageQueries` | replace | `bing.traffic.page-queries`; same 26 rows, with provider order varying between requests | -| `traffic entity queryPages` | replace | `bing.traffic.query-pages`; same three rows | -| `traffic entity queryPage` | replace | `bing.traffic.query-page`; both returned the same valid empty result | -| `crawl dataset stats` | replace | `bing.crawl.stats`; same six rows | -| `crawl dataset issues` | replace | `bing.crawl.issues`; both returned the same valid empty result | -| `crawl entity settings` | replace | `bing.crawl.settings`; same settings object | -| `link dataset pages` | replace | `bing.links.pages`; same link-count object | -| `link entity url` | replace | `bing.links.url`; same URL-link object | -| `sitemap dataset feeds` | replace | `bing.sitemaps.list`; same one-sitemap result | -| `sitemap entity feed` | replace | `bing.sitemaps.get`; same four detail rows | -| `url entity info` | replace | `bing.urls.info`; same URL index object | -| `url entity traffic` | replace | `bing.urls.traffic`; same URL traffic object | +| Legacy command | Decision | gkit target or missing gate | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| `doctor dataset readiness` | keep | `gkit bing doctor` validates local config but intentionally does not preserve the legacy network probe | +| `site dataset sites` | replace | `bing.sites.list`; both returned the same five sites | +| `traffic dataset rank` | replace | `bing.traffic.rank`; same seven rows | +| `traffic dataset queries` | replace | `bing.traffic.queries`; same 35 rows, with provider order varying between requests | +| `traffic dataset pages` | replace | `bing.traffic.pages`; same 28 rows | +| `traffic entity query` | replace | `bing.traffic.query`; same six rows for the selected query | +| `traffic entity pageQueries` | replace | `bing.traffic.page-queries`; same 26 rows, with provider order varying between requests | +| `traffic entity queryPages` | replace | `bing.traffic.query-pages`; same three rows | +| `traffic entity queryPage` | replace | `bing.traffic.query-page`; both returned the same valid empty result | +| `crawl dataset stats` | replace | `bing.crawl.stats`; same six rows | +| `crawl dataset issues` | replace | `bing.crawl.issues`; both returned the same valid empty result | +| `crawl entity settings` | replace | `bing.crawl.settings`; same settings object | +| `link dataset pages` | replace | `bing.links.pages`; same link-count object | +| `link entity url` | replace | `bing.links.url`; same URL-link object | +| `sitemap dataset feeds` | replace | `bing.sitemaps.list`; same one-sitemap result | +| `sitemap entity feed` | replace | `bing.sitemaps.get`; same four detail rows | +| `url entity info` | replace | `bing.urls.info`; same URL index object | +| `url entity traffic` | replace | `bing.urls.traffic`; same URL traffic object | ## Package retirement verdict diff --git a/packages/gkit/evals/dataforseo-migration-matrix.md b/packages/gkit/evals/dataforseo-migration-matrix.md index fe042ef..5c6aad1 100644 --- a/packages/gkit/evals/dataforseo-migration-matrix.md +++ b/packages/gkit/evals/dataforseo-migration-matrix.md @@ -25,43 +25,43 @@ missing. No command is dropped in Slice 2. ## `@deniffer/backlink-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `domain dataset summary` | replace | `dataforseo.backlinks.summary.live`; domain live gate passed | -| `domain dataset referringDomains` | replace | `dataforseo.backlinks.referring_domains.live`; limit/order live gate passed | -| `domain dataset anchors` | keep | no reviewed executable manifest record | -| `page dataset summary` | keep | endpoint is exposed, but page-target behavior has no dedicated golden/live gate | -| `page dataset backlinks` | keep | no reviewed executable manifest record | -| `page dataset anchors` | keep | no reviewed executable manifest record | +| Legacy command | Decision | gkit target or missing gate | +| --------------------------------- | -------- | ------------------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `domain dataset summary` | replace | `dataforseo.backlinks.summary.live`; domain live gate passed | +| `domain dataset referringDomains` | replace | `dataforseo.backlinks.referring_domains.live`; limit/order live gate passed | +| `domain dataset anchors` | keep | no reviewed executable manifest record | +| `page dataset summary` | keep | endpoint is exposed, but page-target behavior has no dedicated golden/live gate | +| `page dataset backlinks` | keep | no reviewed executable manifest record | +| `page dataset anchors` | keep | no reviewed executable manifest record | The package stays because three command families remain `keep` and page summary has not passed its own behavior gate. ## `@deniffer/serp-snapshot-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `query dataset results` | replace | `dataforseo.serp.google.organic.live.advanced`; US/en/desktop/windows/depth-10 live gate passed | -| `batch dataset results` | keep | gkit exposes one reviewed provider task per invocation; batch artifact behavior is not replaced | +| Legacy command | Decision | gkit target or missing gate | +| -------------------------- | -------- | ----------------------------------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `query dataset results` | replace | `dataforseo.serp.google.organic.live.advanced`; US/en/desktop/windows/depth-10 live gate passed | +| `batch dataset results` | keep | gkit exposes one reviewed provider task per invocation; batch artifact behavior is not replaced | The package stays because batch behavior is still `keep`. ## `@deniffer/ai-optimization-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `llmResponse dataset models` | keep | no reviewed executable manifest record | -| `llmResponse entity live` | keep | no reviewed executable manifest record or live gate | -| `llmMention dataset locationsAndLanguages` | keep | no reviewed executable manifest record | -| `llmMention dataset availableFilters` | keep | no reviewed executable manifest record | -| `llmMention dataset search` | keep | inventory-only; `$0.10` request floor exceeds the `$0.03` profile cap | -| `llmMention dataset topPages` | keep | no reviewed executable manifest record | -| `llmMention dataset topDomains` | keep | no reviewed executable manifest record | -| `llmMention dataset aggregatedMetrics` | keep | no reviewed executable manifest record | -| `llmMention dataset crossAggregatedMetrics` | keep | no reviewed executable manifest record | +| Legacy command | Decision | gkit target or missing gate | +| ------------------------------------------- | -------- | --------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `llmResponse dataset models` | keep | no reviewed executable manifest record | +| `llmResponse entity live` | keep | no reviewed executable manifest record or live gate | +| `llmMention dataset locationsAndLanguages` | keep | no reviewed executable manifest record | +| `llmMention dataset availableFilters` | keep | no reviewed executable manifest record | +| `llmMention dataset search` | keep | inventory-only; `$0.10` request floor exceeds the `$0.03` profile cap | +| `llmMention dataset topPages` | keep | no reviewed executable manifest record | +| `llmMention dataset topDomains` | keep | no reviewed executable manifest record | +| `llmMention dataset aggregatedMetrics` | keep | no reviewed executable manifest record | +| `llmMention dataset crossAggregatedMetrics` | keep | no reviewed executable manifest record | The package stays because every data workflow remains `keep`. diff --git a/packages/gkit/evals/google-ads-migration-matrix.md b/packages/gkit/evals/google-ads-migration-matrix.md index 560de14..6464e55 100644 --- a/packages/gkit/evals/google-ads-migration-matrix.md +++ b/packages/gkit/evals/google-ads-migration-matrix.md @@ -26,18 +26,18 @@ legacy runtime-only operation in the REST product. ## `@deniffer/google-ads-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `provider action install` | drop | REST is in-process and has no package-local Python environment to install | -| `doctor dataset readiness` | keep | gkit now validates the local single-account service-account profile, but it intentionally does not preserve legacy network/MCC readiness semantics | -| `customer dataset accounts` | replace | `google-ads.customers.list-accessible`; implemented contract and live artifact passed | -| `query dataset gaql` | replace | `google-ads.query.gaql`; pinned search, explicit pagination, raw page bundle, negative error, and live gate passed | -| `keywordPlan dataset ideas` | replace | `google-ads.keyword-plan.generate-ideas`; provider-native input, full-page live result, artifact, and prior legacy comparison passed | -| `keywordPlan dataset historicalMetrics` | replace | `google-ads.keyword-plan.generate-historical-metrics`; live REST and legacy comparison passed | -| `campaign dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `adGroup dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `keyword dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `searchTerm dataset performance` | keep | the larger REST query proved capacity, not the curated command's complete behavior contract | +| Legacy command | Decision | gkit target or missing gate | +| --------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `provider action install` | drop | REST is in-process and has no package-local Python environment to install | +| `doctor dataset readiness` | keep | gkit now validates the local single-account service-account profile, but it intentionally does not preserve legacy network/MCC readiness semantics | +| `customer dataset accounts` | replace | `google-ads.customers.list-accessible`; implemented contract and live artifact passed | +| `query dataset gaql` | replace | `google-ads.query.gaql`; pinned search, explicit pagination, raw page bundle, negative error, and live gate passed | +| `keywordPlan dataset ideas` | replace | `google-ads.keyword-plan.generate-ideas`; provider-native input, full-page live result, artifact, and prior legacy comparison passed | +| `keywordPlan dataset historicalMetrics` | replace | `google-ads.keyword-plan.generate-historical-metrics`; live REST and legacy comparison passed | +| `campaign dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `adGroup dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `keyword dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `searchTerm dataset performance` | keep | the larger REST query proved capacity, not the curated command's complete behavior contract | ## Package retirement verdict diff --git a/packages/gkit/evals/google-ads-rest-spike.md b/packages/gkit/evals/google-ads-rest-spike.md index b205950..b96ddd9 100644 --- a/packages/gkit/evals/google-ads-rest-spike.md +++ b/packages/gkit/evals/google-ads-rest-spike.md @@ -45,16 +45,16 @@ not evidence that the manager scenario passed. The isolated spike made ten REST calls: nine expected `200` responses and one expected `400` negative response. -| Surface | Observed result | -| --- | --- | -| OAuth | service-account credentials produced a derived access token; the token was registered as a secret immediately | -| Accessible customers | one directly accessible account; zero directly accessible manager accounts | -| Field metadata | two pages and two rows with a real `pageToken` loop | -| Small GAQL | one page and 12 campaign rows; legacy and REST row counts matched | -| Larger GAQL | one page and 850 search-term rows; peak observed heap delta was 1,266,192 bytes | -| Keyword Ideas | two pages and four rows using a real `pageToken`; legacy and REST counts matched | -| Historical metrics | one page and one row; legacy and REST counts matched | -| Negative GAQL | HTTP `400`, status `INVALID_ARGUMENT`, allowlisted code `queryError:UNRECOGNIZED_FIELD`, and a request ID | +| Surface | Observed result | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | +| OAuth | service-account credentials produced a derived access token; the token was registered as a secret immediately | +| Accessible customers | one directly accessible account; zero directly accessible manager accounts | +| Field metadata | two pages and two rows with a real `pageToken` loop | +| Small GAQL | one page and 12 campaign rows; legacy and REST row counts matched | +| Larger GAQL | one page and 850 search-term rows; peak observed heap delta was 1,266,192 bytes | +| Keyword Ideas | two pages and four rows using a real `pageToken`; legacy and REST counts matched | +| Historical metrics | one page and one row; legacy and REST counts matched | +| Negative GAQL | HTTP `400`, status `INVALID_ARGUMENT`, allowlisted code `queryError:UNRECOGNIZED_FIELD`, and a request ID | REST response fields are provider-native camelCase. The legacy Python wrapper converted protobuf messages to snake_case; gkit should preserve the REST diff --git a/packages/gkit/evals/gsc-migration-matrix.md b/packages/gkit/evals/gsc-migration-matrix.md index de3796a..e6cdefa 100644 --- a/packages/gkit/evals/gsc-migration-matrix.md +++ b/packages/gkit/evals/gsc-migration-matrix.md @@ -18,17 +18,17 @@ resource: ../../../docs/plans/2026-07-13-gkit-vertical-slice-plan.md ## `@deniffer/gsc-cli` -| Legacy command | Decision | gkit target or evidence | -| --- | --- | --- | -| `skill path` | drop | local package skill distribution is not a provider capability; generated docs replace discovery | -| `skill print` | drop | `gkit docs --provider gsc` and `gkit describe` are the canonical discovery surfaces | -| `skill install` | drop | the sole consumer uses the linked `gkit` binary; no package-local skill install is needed | -| `doctor dataset readiness` | keep | `gkit gsc doctor` validates local files, but intentionally does not preserve the legacy network probe | -| `property dataset sites` | replace | `gsc.properties.list`; legacy and gkit both returned two properties | -| `search dataset analytics` | replace | `gsc.search-analytics.query`; same dates, query dimension, and limit returned the same 25 rows | -| `sitemap dataset sitemaps` | replace | `gsc.sitemaps.list`; both returned one sitemap with the same raw provider facts | -| `sitemap entity sitemap` | replace | `gsc.sitemaps.get`; both returned the selected sitemap object | -| `inspection entity url` | replace | `gsc.url-inspection.inspect`; both returned the same indexed URL result | +| Legacy command | Decision | gkit target or evidence | +| -------------------------- | -------- | ----------------------------------------------------------------------------------------------------- | +| `skill path` | drop | local package skill distribution is not a provider capability; generated docs replace discovery | +| `skill print` | drop | `gkit docs --provider gsc` and `gkit describe` are the canonical discovery surfaces | +| `skill install` | drop | the sole consumer uses the linked `gkit` binary; no package-local skill install is needed | +| `doctor dataset readiness` | keep | `gkit gsc doctor` validates local files, but intentionally does not preserve the legacy network probe | +| `property dataset sites` | replace | `gsc.properties.list`; legacy and gkit both returned two properties | +| `search dataset analytics` | replace | `gsc.search-analytics.query`; same dates, query dimension, and limit returned the same 25 rows | +| `sitemap dataset sitemaps` | replace | `gsc.sitemaps.list`; both returned one sitemap with the same raw provider facts | +| `sitemap entity sitemap` | replace | `gsc.sitemaps.get`; both returned the selected sitemap object | +| `inspection entity url` | replace | `gsc.url-inspection.inspect`; both returned the same indexed URL result | ## Package retirement verdict diff --git a/packages/gkit/evals/slice2-baseline.md b/packages/gkit/evals/slice2-baseline.md index 7e0e007..0995148 100644 --- a/packages/gkit/evals/slice2-baseline.md +++ b/packages/gkit/evals/slice2-baseline.md @@ -35,11 +35,11 @@ the active profile hard cap of `$0.03` per call. ## Live gates -| Capability | Requested bound | Actual cost | Provider result | Artifact | -| --- | ---: | ---: | --- | --- | -| `dataforseo.backlinks.summary.live` | `$0.024036` | `$0.024036` | one summary result | 2,846 bytes, SHA-256 `061c05629f2a5857e52899caf9c96c8b029598990274116a4ae4bd4253564463` | -| `dataforseo.backlinks.referring_domains.live` | `$0.024072` | `$0.024036` | requested limit 2, returned 1 row | 2,921 bytes, SHA-256 `d1e83a6f24b9bfdb4f36b6398ecdbe960a58c45337e47c5f0dc4f451d19d223b` | -| `dataforseo.serp.google.organic.live.advanced` | `$0.002000` | `$0.002000` | one page, 11 SERP elements | 25,043 bytes, SHA-256 `f557cc9f963a888d47f8d4ac333fef873ed71f955290f90f9f06227aba26e6cb` | +| Capability | Requested bound | Actual cost | Provider result | Artifact | +| ---------------------------------------------- | --------------: | ----------: | --------------------------------- | ---------------------------------------------------------------------------------------- | +| `dataforseo.backlinks.summary.live` | `$0.024036` | `$0.024036` | one summary result | 2,846 bytes, SHA-256 `061c05629f2a5857e52899caf9c96c8b029598990274116a4ae4bd4253564463` | +| `dataforseo.backlinks.referring_domains.live` | `$0.024072` | `$0.024036` | requested limit 2, returned 1 row | 2,921 bytes, SHA-256 `d1e83a6f24b9bfdb4f36b6398ecdbe960a58c45337e47c5f0dc4f451d19d223b` | +| `dataforseo.serp.google.organic.live.advanced` | `$0.002000` | `$0.002000` | one page, 11 SERP elements | 25,043 bytes, SHA-256 `f557cc9f963a888d47f8d4ac333fef873ed71f955290f90f9f06227aba26e6cb` | Actual Slice 2 live cost was `$0.050072`, below the `$0.050108` aggregate authorization bound. Every call succeeded on its first dispatch. diff --git a/packages/gkit/evals/slice4-google-ads-baseline.md b/packages/gkit/evals/slice4-google-ads-baseline.md index c7f1b43..caaf679 100644 --- a/packages/gkit/evals/slice4-google-ads-baseline.md +++ b/packages/gkit/evals/slice4-google-ads-baseline.md @@ -48,14 +48,14 @@ and argv contracts until the separate real-MCC gate passes. ## Live gate -| Capability | Pages | Rows | Artifact bytes | -| --- | ---: | ---: | ---: | -| `google-ads.customers.list-accessible` | 1 | 1 | 58 | -| `google-ads.fields.describe` | 1 | 1 | 280 | -| `google-ads.fields.search` | 2 | 2 | 662 | -| `google-ads.query.gaql` | 1 | 12 | 2,654 | -| `google-ads.keyword-plan.generate-ideas` | 1 | 12 | 20,917 | -| `google-ads.keyword-plan.generate-historical-metrics` | 1 | 1 | 1,904 | +| Capability | Pages | Rows | Artifact bytes | +| ----------------------------------------------------- | ----: | ---: | -------------: | +| `google-ads.customers.list-accessible` | 1 | 1 | 58 | +| `google-ads.fields.describe` | 1 | 1 | 280 | +| `google-ads.fields.search` | 2 | 2 | 662 | +| `google-ads.query.gaql` | 1 | 12 | 2,654 | +| `google-ads.keyword-plan.generate-ideas` | 1 | 12 | 20,917 | +| `google-ads.keyword-plan.generate-historical-metrics` | 1 | 1 | 1,904 | The field search deliberately used a one-row page size and proved a real two-page `pageToken` loop. Each command returned a safe provider request ID and diff --git a/packages/gkit/evals/slice4-gsc-baseline.md b/packages/gkit/evals/slice4-gsc-baseline.md index 1b26a0a..328a659 100644 --- a/packages/gkit/evals/slice4-gsc-baseline.md +++ b/packages/gkit/evals/slice4-gsc-baseline.md @@ -32,13 +32,13 @@ reads, an inaccessible-property negative, and same-input legacy comparisons. ## Real and legacy comparison gate -| Capability | gkit rows | Legacy result | Artifact bytes | -| --- | ---: | --- | ---: | -| `gsc.properties.list` | 2 | 2 properties | 218 | -| `gsc.search-analytics.query` | 25 | same 25-row request | 4,393 | -| `gsc.sitemaps.list` | 1 | same one-sitemap result | 424 | -| `gsc.sitemaps.get` | 1 | same selected sitemap object | 337 | -| `gsc.url-inspection.inspect` | 1 | same indexed URL result | 930 | +| Capability | gkit rows | Legacy result | Artifact bytes | +| ---------------------------- | --------: | ---------------------------- | -------------: | +| `gsc.properties.list` | 2 | 2 properties | 218 | +| `gsc.search-analytics.query` | 25 | same 25-row request | 4,393 | +| `gsc.sitemaps.list` | 1 | same one-sitemap result | 424 | +| `gsc.sitemaps.get` | 1 | same selected sitemap object | 337 | +| `gsc.url-inspection.inspect` | 1 | same indexed URL result | 930 | The negative Search Analytics request used an inaccessible property. It exited 1 with HTTP `403`, `AUTH_FAILED`, `outcome:"confirmed"`, and a 474-byte raw diff --git a/packages/gkit/evals/slice5-baseline.md b/packages/gkit/evals/slice5-baseline.md index 5c04927..4435c2a 100644 --- a/packages/gkit/evals/slice5-baseline.md +++ b/packages/gkit/evals/slice5-baseline.md @@ -17,13 +17,13 @@ resource: ../../../docs/plans/2026-07-13-gkit-vertical-slice-plan.md `bun run --filter gkit eval:slice5` passed the committed answer keys and observations: -| Metric | Result | Gate | -| --- | ---: | ---: | -| Task distribution | 10 explicit / 15 business / 10 long-tail / 5 negative | exact | -| Provider top-1 | 100% (35/35) | at least 95% | -| Discovery within two steps | 100% (35/35) | at least 90% | -| First executable command | 100% (33/33) | at least 90% | -| Negative precision | 100% (5/5) | at least 95% | +| Metric | Result | Gate | +| -------------------------- | ----------------------------------------------------: | -----------: | +| Task distribution | 10 explicit / 15 business / 10 long-tail / 5 negative | exact | +| Provider top-1 | 100% (35/35) | at least 95% | +| Discovery within two steps | 100% (35/35) | at least 90% | +| First executable command | 100% (33/33) | at least 90% | +| Negative precision | 100% (5/5) | at least 95% | Two positive tasks are inventory-only DataForSEO LLM Mention requests, so they correctly have no executable-command denominator. All 33 executable positive diff --git a/packages/gkit/evals/slice5-final-migration-matrix.md b/packages/gkit/evals/slice5-final-migration-matrix.md index 0c7f3d5..a80d3e7 100644 --- a/packages/gkit/evals/slice5-final-migration-matrix.md +++ b/packages/gkit/evals/slice5-final-migration-matrix.md @@ -29,25 +29,25 @@ replaced. Their final transformation is deterministic: ## Provider packages -| Deleted package | Replace | Final drop | Evidence | -| --- | ---: | ---: | --- | -| `@deniffer/backlink-cli` | 3 | 4 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/serp-snapshot-cli` | 2 | 1 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/ai-optimization-cli` | 1 | 9 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/posthog-cli` | 1 | 12 | [`posthog-migration-matrix.md`](./posthog-migration-matrix.md) | -| `@deniffer/google-ads-cli` | 4 | 6 | [`google-ads-migration-matrix.md`](./google-ads-migration-matrix.md) | -| `@deniffer/gsc-cli` | 5 | 4 | [`gsc-migration-matrix.md`](./gsc-migration-matrix.md) | -| `@deniffer/bing-webmaster-cli` | 17 | 1 | [`bing-migration-matrix.md`](./bing-migration-matrix.md) | -| **Provider total** | **33** | **37** | all seven packages deleted | +| Deleted package | Replace | Final drop | Evidence | +| ------------------------------- | ------: | ---------: | -------------------------------------------------------------------- | +| `@deniffer/backlink-cli` | 3 | 4 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/serp-snapshot-cli` | 2 | 1 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/ai-optimization-cli` | 1 | 9 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/posthog-cli` | 1 | 12 | [`posthog-migration-matrix.md`](./posthog-migration-matrix.md) | +| `@deniffer/google-ads-cli` | 4 | 6 | [`google-ads-migration-matrix.md`](./google-ads-migration-matrix.md) | +| `@deniffer/gsc-cli` | 5 | 4 | [`gsc-migration-matrix.md`](./gsc-migration-matrix.md) | +| `@deniffer/bing-webmaster-cli` | 17 | 1 | [`bing-migration-matrix.md`](./bing-migration-matrix.md) | +| **Provider total** | **33** | **37** | all seven packages deleted | ## Non-provider tools -| Deleted command | Final decision | Consequence accepted by sole consumer | -| --- | --- | --- | -| `page-extract page entity extract` | drop | normalized ctx-backed SEO/GEO artifact removed; use ctx directly when needed | -| `sitemap-watch registry dataset competitors` | drop | local competitor registry command removed | -| `sitemap-watch snapshot dataset pages` | drop | recursive normalized sitemap snapshot command removed | -| `sitemap-watch snapshot entity page` | drop | single-page snapshot selection removed | +| Deleted command | Final decision | Consequence accepted by sole consumer | +| -------------------------------------------- | -------------- | ---------------------------------------------------------------------------- | +| `page-extract page entity extract` | drop | normalized ctx-backed SEO/GEO artifact removed; use ctx directly when needed | +| `sitemap-watch registry dataset competitors` | drop | local competitor registry command removed | +| `sitemap-watch snapshot dataset pages` | drop | recursive normalized sitemap snapshot command removed | +| `sitemap-watch snapshot entity page` | drop | single-page snapshot selection removed | Final old-command total: **33 replace / 41 drop**. The repository now contains only `packages/gkit`; the private gkit package stays in the workspace layout. diff --git a/packages/gkit/policy/bing.reviewed.json b/packages/gkit/policy/bing.reviewed.json index ef0c8b6..8e8a775 100644 --- a/packages/gkit/policy/bing.reviewed.json +++ b/packages/gkit/policy/bing.reviewed.json @@ -1,658 +1,675 @@ { - "version": 1, - "provider": "bing", - "manifestRevision": "2026-10-27.bing.search-health.1", - "reviewedAt": "2026-10-27T04:00:00.000Z", - "entries": [ - { - "path": "/GetUserSites", - "method": "get", - "operationId": "GetUserSites", - "exposure": "executable", - "capability": { - "id": "bing.sites.list", - "provider": "bing", - "revision": "1", - "adapterKey": "sites.list", - "title": "List accessible Bing Webmaster sites", - "description": "List every site accessible to the profile-bound Bing Webmaster API key.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.sites.list --input '{}' --out bing-sites.json --dry-run" - } - ] - } - }, - { - "path": "/GetRankAndTrafficStats", - "method": "get", - "operationId": "GetRankAndTrafficStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.rank", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.rank", - "title": "Read Bing rank and traffic statistics", - "description": "Read rank and traffic statistics for the profile-bound or explicitly selected site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.rank --input '{}' --out bing-rank.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryStats", - "method": "get", - "operationId": "GetQueryStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.queries", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.queries", - "title": "Read top Bing query statistics", - "description": "Read top query traffic statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.queries --input '{}' --out bing-queries.json --dry-run" - } - ] - } - }, - { - "path": "/GetPageStats", - "method": "get", - "operationId": "GetPageStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.pages", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.pages", - "title": "Read top Bing page statistics", - "description": "Read top page traffic statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.pages --input '{}' --out bing-pages.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryTrafficStats", - "method": "get", - "operationId": "GetQueryTrafficStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query", - "title": "Read one Bing query's traffic", - "description": "Read daily traffic statistics for one exact query.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "examples": [ - { - "input": { "query": "website cloner" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query --input @request.json --out bing-query.json --dry-run" - } - ] - } - }, - { - "path": "/GetPageQueryStats", - "method": "get", - "operationId": "GetPageQueryStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.page-queries", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.page-queries", - "title": "Read Bing queries for one page", - "description": "Read query statistics for one exact page URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["pageUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "pageUrl": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.page-queries --input @request.json --out bing-page-queries.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryPageStats", - "method": "get", - "operationId": "GetQueryPageStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query-pages", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query-pages", - "title": "Read Bing pages for one query", - "description": "Read page statistics for one exact query.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "examples": [ - { - "input": { "query": "website cloner" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query-pages --input @request.json --out bing-query-pages.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryPageDetailStats", - "method": "get", - "operationId": "GetQueryPageDetailStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query-page", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query-page", - "title": "Read detailed Bing query-page statistics", - "description": "Read detailed statistics for one exact query and page URL pair.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query", "pageUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 }, - "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "query": "website cloner", "pageUrl": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query-page --input @request.json --out bing-query-page.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlStats", - "method": "get", - "operationId": "GetCrawlStats", - "exposure": "executable", - "capability": { - "id": "bing.crawl.stats", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.stats", - "title": "Read Bing crawl statistics", - "description": "Read crawl statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.stats --input '{}' --out bing-crawl-stats.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlIssues", - "method": "get", - "operationId": "GetCrawlIssues", - "exposure": "executable", - "capability": { - "id": "bing.crawl.issues", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.issues", - "title": "Read Bing crawl issues", - "description": "Read current crawl issue URLs for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.issues --input '{}' --out bing-crawl-issues.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlSettings", - "method": "get", - "operationId": "GetCrawlSettings", - "exposure": "executable", - "capability": { - "id": "bing.crawl.settings", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.settings", - "title": "Read Bing crawl settings", - "description": "Read crawl settings for one site without exposing mutation methods.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.settings --input '{}' --out bing-crawl-settings.json --dry-run" - } - ] - } - }, - { - "path": "/GetLinkCounts", - "method": "get", - "operationId": "GetLinkCounts", - "exposure": "executable", - "capability": { - "id": "bing.links.pages", - "provider": "bing", - "revision": "1", - "adapterKey": "links.pages", - "title": "Read Bing inbound link counts", - "description": "Read one provider page of URLs and inbound link counts.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "page": { "type": "integer", "minimum": 0, "maximum": 100000 } - } - }, - "examples": [ - { - "input": { "page": 0 }, - "command": "gkit --profile bing api call --capability-id bing.links.pages --input '{\"page\":0}' --out bing-links.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlLinks", - "method": "get", - "operationId": "GetUrlLinks", - "exposure": "executable", - "capability": { - "id": "bing.links.url", - "provider": "bing", - "revision": "1", - "adapterKey": "links.url", - "title": "Read Bing inbound links for one URL", - "description": "Read one provider page of inbound link details for an exact URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["link"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "link": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "page": { "type": "integer", "minimum": 0, "maximum": 100000 } - } - }, - "examples": [ - { - "input": { "link": "https://example.com/docs", "page": 0 }, - "command": "gkit --profile bing api call --capability-id bing.links.url --input @request.json --out bing-url-links.json --dry-run" - } - ] - } - }, - { - "path": "/GetFeeds", - "method": "get", - "operationId": "GetFeeds", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.list", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.list", - "title": "List Bing feeds and sitemaps", - "description": "List top-level feeds or sitemaps for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.list --input '{}' --out bing-sitemaps.json --dry-run" - } - ] - } - }, - { - "path": "/GetFeedDetails", - "method": "get", - "operationId": "GetFeedDetails", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.get", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.get", - "title": "Read one Bing feed", - "description": "Read details for one exact sitemap or feed URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "feedUrl": "https://example.com/sitemap.xml" }, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.get --input @request.json --out bing-sitemap.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlInfo", - "method": "get", - "operationId": "GetUrlInfo", - "exposure": "executable", - "capability": { - "id": "bing.urls.info", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.info", - "title": "Read Bing index information for a URL", - "description": "Read Bing index information for one URL or domain target.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "url": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.urls.info --input @request.json --out bing-url-info.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlTrafficInfo", - "method": "get", - "operationId": "GetUrlTrafficInfo", - "exposure": "executable", - "capability": { - "id": "bing.urls.traffic", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.traffic", - "title": "Read Bing traffic for a URL", - "description": "Read Bing traffic information for one URL or domain target.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "url": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.urls.traffic --input @request.json --out bing-url-traffic.json --dry-run" - } - ] - } - }, - { - "path": "/GetBlockedUrls", - "method": "get", - "operationId": "GetBlockedUrls", - "exposure": "executable", - "capability": { - "id": "bing.crawl.blocked-urls", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.blocked-urls", - "title": "Read Bing blocked URLs", - "description": "Read URLs and directories blocked from Bing results for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.blocked-urls --input '{}' --out bing-blocked-urls.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlSubmissionQuota", - "method": "get", - "operationId": "GetUrlSubmissionQuota", - "exposure": "executable", - "capability": { - "id": "bing.urls.submission-quota", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.submission-quota", - "title": "Read Bing URL submission quota", - "description": "Read the remaining daily and monthly URL submission quota for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.urls.submission-quota --input '{}' --out bing-url-quota.json --dry-run" - } - ] - } - }, - { - "path": "/SubmitUrlbatch", - "method": "post", - "operationId": "SubmitUrlbatch", - "exposure": "executable", - "capability": { - "id": "bing.urls.submit", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.submit", - "title": "Submit URLs to Bing", - "description": "Submit up to 500 URLs of one site for Bing crawling; consumes the daily URL submission quota.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["urlList"], - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "urlList": { - "type": "array", - "minItems": 1, - "maxItems": 500, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - } - }, - "examples": [ - { - "input": { - "urlList": ["https://example.com/new-page"] - }, - "command": "gkit --profile bing api call --capability-id bing.urls.submit --input @request.json --allow-write --out bing-url-submit.json --dry-run" - } - ] - } - }, - { - "path": "/SubmitFeed", - "method": "post", - "operationId": "SubmitFeed", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.submit", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.submit", - "title": "Submit a sitemap to Bing", - "description": "Submit or resubmit one sitemap or feed URL for one site.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedUrl"], - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "feedUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": { - "feedUrl": "https://example.com/sitemap.xml" - }, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.submit --input @request.json --allow-write --out bing-sitemap-submit.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "bing", + "manifestRevision": "2026-10-27.bing.search-health.1", + "reviewedAt": "2026-10-27T04:00:00.000Z", + "entries": [ + { + "path": "/GetUserSites", + "method": "get", + "operationId": "GetUserSites", + "exposure": "executable", + "capability": { + "id": "bing.sites.list", + "provider": "bing", + "revision": "1", + "adapterKey": "sites.list", + "title": "List accessible Bing Webmaster sites", + "description": "List every site accessible to the profile-bound Bing Webmaster API key.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.sites.list --input '{}' --out bing-sites.json --dry-run" + } + ] + } + }, + { + "path": "/GetRankAndTrafficStats", + "method": "get", + "operationId": "GetRankAndTrafficStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.rank", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.rank", + "title": "Read Bing rank and traffic statistics", + "description": "Read rank and traffic statistics for the profile-bound or explicitly selected site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.rank --input '{}' --out bing-rank.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryStats", + "method": "get", + "operationId": "GetQueryStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.queries", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.queries", + "title": "Read top Bing query statistics", + "description": "Read top query traffic statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.queries --input '{}' --out bing-queries.json --dry-run" + } + ] + } + }, + { + "path": "/GetPageStats", + "method": "get", + "operationId": "GetPageStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.pages", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.pages", + "title": "Read top Bing page statistics", + "description": "Read top page traffic statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.pages --input '{}' --out bing-pages.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryTrafficStats", + "method": "get", + "operationId": "GetQueryTrafficStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query", + "title": "Read one Bing query's traffic", + "description": "Read daily traffic statistics for one exact query.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 } + } + }, + "examples": [ + { + "input": { "query": "website cloner" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query --input @request.json --out bing-query.json --dry-run" + } + ] + } + }, + { + "path": "/GetPageQueryStats", + "method": "get", + "operationId": "GetPageQueryStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.page-queries", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.page-queries", + "title": "Read Bing queries for one page", + "description": "Read query statistics for one exact page URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["pageUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "pageUrl": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.page-queries --input @request.json --out bing-page-queries.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryPageStats", + "method": "get", + "operationId": "GetQueryPageStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query-pages", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query-pages", + "title": "Read Bing pages for one query", + "description": "Read page statistics for one exact query.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 } + } + }, + "examples": [ + { + "input": { "query": "website cloner" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query-pages --input @request.json --out bing-query-pages.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryPageDetailStats", + "method": "get", + "operationId": "GetQueryPageDetailStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query-page", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query-page", + "title": "Read detailed Bing query-page statistics", + "description": "Read detailed statistics for one exact query and page URL pair.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query", "pageUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 }, + "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { + "query": "website cloner", + "pageUrl": "https://example.com/docs" + }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query-page --input @request.json --out bing-query-page.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlStats", + "method": "get", + "operationId": "GetCrawlStats", + "exposure": "executable", + "capability": { + "id": "bing.crawl.stats", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.stats", + "title": "Read Bing crawl statistics", + "description": "Read crawl statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.stats --input '{}' --out bing-crawl-stats.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlIssues", + "method": "get", + "operationId": "GetCrawlIssues", + "exposure": "executable", + "capability": { + "id": "bing.crawl.issues", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.issues", + "title": "Read Bing crawl issues", + "description": "Read current crawl issue URLs for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.issues --input '{}' --out bing-crawl-issues.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlSettings", + "method": "get", + "operationId": "GetCrawlSettings", + "exposure": "executable", + "capability": { + "id": "bing.crawl.settings", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.settings", + "title": "Read Bing crawl settings", + "description": "Read crawl settings for one site without exposing mutation methods.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.settings --input '{}' --out bing-crawl-settings.json --dry-run" + } + ] + } + }, + { + "path": "/GetLinkCounts", + "method": "get", + "operationId": "GetLinkCounts", + "exposure": "executable", + "capability": { + "id": "bing.links.pages", + "provider": "bing", + "revision": "1", + "adapterKey": "links.pages", + "title": "Read Bing inbound link counts", + "description": "Read one provider page of URLs and inbound link counts.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "page": { "type": "integer", "minimum": 0, "maximum": 100000 } + } + }, + "examples": [ + { + "input": { "page": 0 }, + "command": "gkit --profile bing api call --capability-id bing.links.pages --input '{\"page\":0}' --out bing-links.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlLinks", + "method": "get", + "operationId": "GetUrlLinks", + "exposure": "executable", + "capability": { + "id": "bing.links.url", + "provider": "bing", + "revision": "1", + "adapterKey": "links.url", + "title": "Read Bing inbound links for one URL", + "description": "Read one provider page of inbound link details for an exact URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["link"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "link": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "page": { "type": "integer", "minimum": 0, "maximum": 100000 } + } + }, + "examples": [ + { + "input": { "link": "https://example.com/docs", "page": 0 }, + "command": "gkit --profile bing api call --capability-id bing.links.url --input @request.json --out bing-url-links.json --dry-run" + } + ] + } + }, + { + "path": "/GetFeeds", + "method": "get", + "operationId": "GetFeeds", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.list", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.list", + "title": "List Bing feeds and sitemaps", + "description": "List top-level feeds or sitemaps for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.list --input '{}' --out bing-sitemaps.json --dry-run" + } + ] + } + }, + { + "path": "/GetFeedDetails", + "method": "get", + "operationId": "GetFeedDetails", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.get", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.get", + "title": "Read one Bing feed", + "description": "Read details for one exact sitemap or feed URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "feedUrl": "https://example.com/sitemap.xml" }, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.get --input @request.json --out bing-sitemap.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlInfo", + "method": "get", + "operationId": "GetUrlInfo", + "exposure": "executable", + "capability": { + "id": "bing.urls.info", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.info", + "title": "Read Bing index information for a URL", + "description": "Read Bing index information for one URL or domain target.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "url": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.urls.info --input @request.json --out bing-url-info.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlTrafficInfo", + "method": "get", + "operationId": "GetUrlTrafficInfo", + "exposure": "executable", + "capability": { + "id": "bing.urls.traffic", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.traffic", + "title": "Read Bing traffic for a URL", + "description": "Read Bing traffic information for one URL or domain target.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "url": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.urls.traffic --input @request.json --out bing-url-traffic.json --dry-run" + } + ] + } + }, + { + "path": "/GetBlockedUrls", + "method": "get", + "operationId": "GetBlockedUrls", + "exposure": "executable", + "capability": { + "id": "bing.crawl.blocked-urls", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.blocked-urls", + "title": "Read Bing blocked URLs", + "description": "Read URLs and directories blocked from Bing results for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.blocked-urls --input '{}' --out bing-blocked-urls.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlSubmissionQuota", + "method": "get", + "operationId": "GetUrlSubmissionQuota", + "exposure": "executable", + "capability": { + "id": "bing.urls.submission-quota", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.submission-quota", + "title": "Read Bing URL submission quota", + "description": "Read the remaining daily and monthly URL submission quota for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.urls.submission-quota --input '{}' --out bing-url-quota.json --dry-run" + } + ] + } + }, + { + "path": "/SubmitUrlbatch", + "method": "post", + "operationId": "SubmitUrlbatch", + "exposure": "executable", + "capability": { + "id": "bing.urls.submit", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.submit", + "title": "Submit URLs to Bing", + "description": "Submit up to 500 URLs of one site for Bing crawling; consumes the daily URL submission quota.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["urlList"], + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "urlList": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + } + }, + "examples": [ + { + "input": { + "urlList": ["https://example.com/new-page"] + }, + "command": "gkit --profile bing api call --capability-id bing.urls.submit --input @request.json --allow-write --out bing-url-submit.json --dry-run" + } + ] + } + }, + { + "path": "/SubmitFeed", + "method": "post", + "operationId": "SubmitFeed", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.submit", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.submit", + "title": "Submit a sitemap to Bing", + "description": "Submit or resubmit one sitemap or feed URL for one site.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedUrl"], + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "feedUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": { + "feedUrl": "https://example.com/sitemap.xml" + }, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.submit --input @request.json --allow-write --out bing-sitemap-submit.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/dataforseo.reviewed.json b/packages/gkit/policy/dataforseo.reviewed.json index 5f775b7..b59bacc 100644 --- a/packages/gkit/policy/dataforseo.reviewed.json +++ b/packages/gkit/policy/dataforseo.reviewed.json @@ -1,371 +1,440 @@ { - "version": 1, - "provider": "dataforseo", - "manifestRevision": "2026-07-15.llm-mentions.2", - "reviewedAt": "2026-07-15T04:00:00.000Z", - "entries": [ - { - "path": "/v3/backlinks/bulk_ranks/live", - "method": "post", - "operationId": "BulkRanksLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.bulk_ranks.live", - "provider": "dataforseo", - "revision": "2", - "title": "Bulk backlink ranks", - "description": "Return DataForSEO backlink ranks for up to 1,000 targets in one live request.", - "adapterKey": "backlinks.bulk_ranks.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["targets"], - "properties": { - "targets": { - "type": "array", - "description": "Domains, subdomains, or absolute URLs to rank.", - "minItems": 1, - "maxItems": 1000, - "items": { "type": "string", "minLength": 1 } - }, - "rank_scale": { - "type": "string", - "enum": ["one_hundred", "one_thousand"] - }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { - "type": "linear-items", - "baseMicros": 24000, - "perItemMicros": 36, - "itemsJsonPointer": "/targets", - "maxItems": 1000 - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "targets": ["clonesite.ai", "example.com"], - "rank_scale": "one_hundred", - "tag": "slice2-smoke" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "targets": ["clonesite.ai", "example.com"], - "rank_scale": "one_hundred", - "tag": "slice2-smoke" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json" - } - ] - } - }, - { - "path": "/v3/backlinks/summary/live", - "method": "post", - "operationId": "SummaryLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.summary.live", - "provider": "dataforseo", - "revision": "1", - "title": "Backlink summary", - "description": "Return the reviewed live backlink summary for one domain, subdomain, or page.", - "adapterKey": "backlinks.summary.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target"], - "properties": { - "target": { "type": "string", "minLength": 1 }, - "include_subdomains": { "type": "boolean" }, - "exclude_internal_backlinks": { "type": "boolean" }, - "internal_list_limit": { "type": "integer", "minimum": 1, "maximum": 10 }, - "backlinks_status_type": { "type": "string", "enum": ["all", "live", "lost"] }, - "rank_scale": { "type": "string", "enum": ["one_hundred", "one_thousand"] }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { "type": "fixed", "micros": 24036 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": "clonesite.ai", - "include_subdomains": true, - "backlinks_status_type": "live" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": "clonesite.ai", - "include_subdomains": true, - "backlinks_status_type": "live" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json" - } - ] - } - }, - { - "path": "/v3/backlinks/referring_domains/live", - "method": "post", - "operationId": "ReferringDomainsLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.referring_domains.live", - "provider": "dataforseo", - "revision": "1", - "title": "Referring domains", - "description": "Return up to 100 reviewed referring-domain rows for one target.", - "adapterKey": "backlinks.referring_domains.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target", "limit"], - "properties": { - "target": { "type": "string", "minLength": 1 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, - "order_by": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "string", - "enum": ["rank,asc", "rank,desc", "backlinks,asc", "backlinks,desc"] - } - }, - "include_subdomains": { "type": "boolean" }, - "exclude_internal_backlinks": { "type": "boolean" }, - "backlinks_status_type": { "type": "string", "enum": ["all", "live", "lost"] }, - "rank_scale": { "type": "string", "enum": ["one_hundred", "one_thousand"] }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { - "type": "linear-number", - "baseMicros": 24000, - "perUnitMicros": 36, - "valueJsonPointer": "/limit", - "maxValue": 100 - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": "clonesite.ai", - "limit": 20, - "order_by": ["rank,desc"], - "exclude_internal_backlinks": true - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": "clonesite.ai", - "limit": 20, - "order_by": ["rank,desc"], - "exclude_internal_backlinks": true - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json" - } - ] - } - }, - { - "path": "/v3/serp/google/organic/live/advanced", - "method": "post", - "operationId": "GoogleOrganicLiveAdvanced", - "exposure": "executable", - "capability": { - "id": "dataforseo.serp.google.organic.live.advanced", - "provider": "dataforseo", - "revision": "1", - "title": "Google Organic live advanced SERP", - "description": "Return one Google Organic live advanced SERP with a reviewed maximum depth of 10.", - "adapterKey": "serp.google.organic.live.advanced", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["keyword", "location_code", "language_code", "device", "os", "depth"], - "properties": { - "keyword": { - "type": "string", - "minLength": 1, - "maxLength": 700, - "not": { - "pattern": "(?:[Aa][Ll][Ll][Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Aa][Ll][Ll][Ii][Nn][Tt][Ee][Xx][Tt]|[Aa][Ll][Ll][Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Aa][Ll][Ll][Ii][Nn][Uu][Rr][Ll]|[Dd][Ee][Ff][Ii][Nn][Ee]|[Dd][Ee][Ff][Ii][Nn][Ii][Tt][Ii][Oo][Nn]|[Ff][Ii][Ll][Ee][Tt][Yy][Pp][Ee]|[Ii][Dd]|[Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Ii][Nn][Ff][Oo]|[Ii][Nn][Tt][Ee][Xx][Tt]|[Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Ii][Nn][Uu][Rr][Ll]|[Ll][Ii][Nn][Kk]|[Rr][Ee][Ll][Aa][Tt][Ee][Dd]|[Ss][Ii][Tt][Ee]|[Cc][Aa][Cc][Hh][Ee]):" - } - }, - "location_code": { "type": "integer", "minimum": 1 }, - "language_code": { "type": "string", "minLength": 2, "maxLength": 10 }, - "device": { "type": "string", "enum": ["desktop", "mobile"] }, - "os": { "type": "string", "enum": ["windows", "macos", "android", "ios"] }, - "depth": { "type": "integer", "minimum": 1, "maximum": 10 }, - "tag": { "type": "string", "maxLength": 255 } - }, - "allOf": [ - { - "if": { "properties": { "device": { "const": "desktop" } } }, - "then": { "properties": { "os": { "enum": ["windows", "macos"] } } }, - "else": { "properties": { "os": { "enum": ["android", "ios"] } } } - } - ] - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-google-organic-live-pricing-2026-07-14-v1", - "model": { "type": "fixed", "micros": 2000 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "keyword": "website cloner", - "location_code": 2840, - "language_code": "en", - "device": "desktop", - "os": "windows", - "depth": 10 - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "keyword": "website cloner", - "location_code": 2840, - "language_code": "en", - "device": "desktop", - "os": "windows", - "depth": 10 - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json" - } - ] - } - }, - { - "path": "/v3/ai_optimization/llm_mentions/search/live", - "method": "post", - "operationId": "LlmMentionsSearchLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.ai_optimization.llm_mentions.search.live", - "provider": "dataforseo", - "revision": "1", - "title": "LLM mentions search", - "description": "Search reviewed ChatGPT or Google AI mention records for one domain in United States English results.", - "adapterKey": "ai_optimization.llm_mentions.search.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target", "location_code", "language_code", "platform", "limit"], - "properties": { - "target": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["domain"], - "properties": { - "domain": { - "type": "string", - "minLength": 1, - "maxLength": 63, - "pattern": "^(?!https?://)(?!www\\.)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$" - }, - "search_filter": { "type": "string", "enum": ["include"] }, - "search_scope": { - "type": "array", - "minItems": 1, - "maxItems": 5, - "uniqueItems": true, - "items": { - "type": "string", - "enum": ["any", "question", "answer", "brand_entities", "fan_out_queries"] - } - }, - "include_subdomains": { "type": "boolean" } - } - } - }, - "location_code": { "type": "integer", "const": 2840 }, - "language_code": { "type": "string", "const": "en" }, - "platform": { "type": "string", "enum": ["chat_gpt", "google"] }, - "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, - "order_by": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { "type": "string", "enum": ["ai_search_volume,asc", "ai_search_volume,desc", "last_response_at,asc", "last_response_at,desc"] } - }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-ai-optimization-llm-mentions-pricing-2026-07-15-v2", - "model": { "type": "fixed", "micros": 150000 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": [{ "domain": "clonesite.ai", "search_filter": "include", "search_scope": ["any"] }], - "location_code": 2840, - "language_code": "en", - "platform": "chat_gpt", - "limit": 20, - "order_by": ["ai_search_volume,desc"] - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": [{ "domain": "clonesite.ai", "search_filter": "include", "search_scope": ["any"] }], - "location_code": 2840, - "language_code": "en", - "platform": "chat_gpt", - "limit": 20, - "order_by": ["ai_search_volume,desc"] - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json" - } - ] - } - } - ] + "version": 1, + "provider": "dataforseo", + "manifestRevision": "2026-07-15.llm-mentions.2", + "reviewedAt": "2026-07-15T04:00:00.000Z", + "entries": [ + { + "path": "/v3/backlinks/bulk_ranks/live", + "method": "post", + "operationId": "BulkRanksLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.bulk_ranks.live", + "provider": "dataforseo", + "revision": "2", + "title": "Bulk backlink ranks", + "description": "Return DataForSEO backlink ranks for up to 1,000 targets in one live request.", + "adapterKey": "backlinks.bulk_ranks.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["targets"], + "properties": { + "targets": { + "type": "array", + "description": "Domains, subdomains, or absolute URLs to rank.", + "minItems": 1, + "maxItems": 1000, + "items": { "type": "string", "minLength": 1 } + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { + "type": "linear-items", + "baseMicros": 24000, + "perItemMicros": 36, + "itemsJsonPointer": "/targets", + "maxItems": 1000 + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "targets": ["clonesite.ai", "example.com"], + "rank_scale": "one_hundred", + "tag": "slice2-smoke" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "targets": ["clonesite.ai", "example.com"], + "rank_scale": "one_hundred", + "tag": "slice2-smoke" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json" + } + ] + } + }, + { + "path": "/v3/backlinks/summary/live", + "method": "post", + "operationId": "SummaryLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.summary.live", + "provider": "dataforseo", + "revision": "1", + "title": "Backlink summary", + "description": "Return the reviewed live backlink summary for one domain, subdomain, or page.", + "adapterKey": "backlinks.summary.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["target"], + "properties": { + "target": { "type": "string", "minLength": 1 }, + "include_subdomains": { "type": "boolean" }, + "exclude_internal_backlinks": { "type": "boolean" }, + "internal_list_limit": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "backlinks_status_type": { + "type": "string", + "enum": ["all", "live", "lost"] + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { "type": "fixed", "micros": 24036 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": "clonesite.ai", + "include_subdomains": true, + "backlinks_status_type": "live" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": "clonesite.ai", + "include_subdomains": true, + "backlinks_status_type": "live" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json" + } + ] + } + }, + { + "path": "/v3/backlinks/referring_domains/live", + "method": "post", + "operationId": "ReferringDomainsLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.referring_domains.live", + "provider": "dataforseo", + "revision": "1", + "title": "Referring domains", + "description": "Return up to 100 reviewed referring-domain rows for one target.", + "adapterKey": "backlinks.referring_domains.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["target", "limit"], + "properties": { + "target": { "type": "string", "minLength": 1 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, + "order_by": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "enum": [ + "rank,asc", + "rank,desc", + "backlinks,asc", + "backlinks,desc" + ] + } + }, + "include_subdomains": { "type": "boolean" }, + "exclude_internal_backlinks": { "type": "boolean" }, + "backlinks_status_type": { + "type": "string", + "enum": ["all", "live", "lost"] + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { + "type": "linear-number", + "baseMicros": 24000, + "perUnitMicros": 36, + "valueJsonPointer": "/limit", + "maxValue": 100 + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": "clonesite.ai", + "limit": 20, + "order_by": ["rank,desc"], + "exclude_internal_backlinks": true + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": "clonesite.ai", + "limit": 20, + "order_by": ["rank,desc"], + "exclude_internal_backlinks": true + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json" + } + ] + } + }, + { + "path": "/v3/serp/google/organic/live/advanced", + "method": "post", + "operationId": "GoogleOrganicLiveAdvanced", + "exposure": "executable", + "capability": { + "id": "dataforseo.serp.google.organic.live.advanced", + "provider": "dataforseo", + "revision": "1", + "title": "Google Organic live advanced SERP", + "description": "Return one Google Organic live advanced SERP with a reviewed maximum depth of 10.", + "adapterKey": "serp.google.organic.live.advanced", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "keyword", + "location_code", + "language_code", + "device", + "os", + "depth" + ], + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "maxLength": 700, + "not": { + "pattern": "(?:[Aa][Ll][Ll][Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Aa][Ll][Ll][Ii][Nn][Tt][Ee][Xx][Tt]|[Aa][Ll][Ll][Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Aa][Ll][Ll][Ii][Nn][Uu][Rr][Ll]|[Dd][Ee][Ff][Ii][Nn][Ee]|[Dd][Ee][Ff][Ii][Nn][Ii][Tt][Ii][Oo][Nn]|[Ff][Ii][Ll][Ee][Tt][Yy][Pp][Ee]|[Ii][Dd]|[Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Ii][Nn][Ff][Oo]|[Ii][Nn][Tt][Ee][Xx][Tt]|[Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Ii][Nn][Uu][Rr][Ll]|[Ll][Ii][Nn][Kk]|[Rr][Ee][Ll][Aa][Tt][Ee][Dd]|[Ss][Ii][Tt][Ee]|[Cc][Aa][Cc][Hh][Ee]):" + } + }, + "location_code": { "type": "integer", "minimum": 1 }, + "language_code": { + "type": "string", + "minLength": 2, + "maxLength": 10 + }, + "device": { "type": "string", "enum": ["desktop", "mobile"] }, + "os": { + "type": "string", + "enum": ["windows", "macos", "android", "ios"] + }, + "depth": { "type": "integer", "minimum": 1, "maximum": 10 }, + "tag": { "type": "string", "maxLength": 255 } + }, + "allOf": [ + { + "if": { "properties": { "device": { "const": "desktop" } } }, + "then": { + "properties": { "os": { "enum": ["windows", "macos"] } } + }, + "else": { "properties": { "os": { "enum": ["android", "ios"] } } } + } + ] + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-google-organic-live-pricing-2026-07-14-v1", + "model": { "type": "fixed", "micros": 2000 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "keyword": "website cloner", + "location_code": 2840, + "language_code": "en", + "device": "desktop", + "os": "windows", + "depth": 10 + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "keyword": "website cloner", + "location_code": 2840, + "language_code": "en", + "device": "desktop", + "os": "windows", + "depth": 10 + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json" + } + ] + } + }, + { + "path": "/v3/ai_optimization/llm_mentions/search/live", + "method": "post", + "operationId": "LlmMentionsSearchLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.ai_optimization.llm_mentions.search.live", + "provider": "dataforseo", + "revision": "1", + "title": "LLM mentions search", + "description": "Search reviewed ChatGPT or Google AI mention records for one domain in United States English results.", + "adapterKey": "ai_optimization.llm_mentions.search.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "target", + "location_code", + "language_code", + "platform", + "limit" + ], + "properties": { + "target": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["domain"], + "properties": { + "domain": { + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^(?!https?://)(?!www\\.)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$" + }, + "search_filter": { "type": "string", "enum": ["include"] }, + "search_scope": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "any", + "question", + "answer", + "brand_entities", + "fan_out_queries" + ] + } + }, + "include_subdomains": { "type": "boolean" } + } + } + }, + "location_code": { "type": "integer", "const": 2840 }, + "language_code": { "type": "string", "const": "en" }, + "platform": { "type": "string", "enum": ["chat_gpt", "google"] }, + "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, + "order_by": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "enum": [ + "ai_search_volume,asc", + "ai_search_volume,desc", + "last_response_at,asc", + "last_response_at,desc" + ] + } + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-ai-optimization-llm-mentions-pricing-2026-07-15-v2", + "model": { "type": "fixed", "micros": 150000 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": [ + { + "domain": "clonesite.ai", + "search_filter": "include", + "search_scope": ["any"] + } + ], + "location_code": 2840, + "language_code": "en", + "platform": "chat_gpt", + "limit": 20, + "order_by": ["ai_search_volume,desc"] + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": [ + { + "domain": "clonesite.ai", + "search_filter": "include", + "search_scope": ["any"] + } + ], + "location_code": 2840, + "language_code": "en", + "platform": "chat_gpt", + "limit": 20, + "order_by": ["ai_search_volume,desc"] + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/google-ads.reviewed.json b/packages/gkit/policy/google-ads.reviewed.json index c6e319c..1e63bf7 100644 --- a/packages/gkit/policy/google-ads.reviewed.json +++ b/packages/gkit/policy/google-ads.reviewed.json @@ -1,290 +1,321 @@ { - "version": 1, - "provider": "google-ads", - "manifestRevision": "2026-07-14.slice4.google-ads.1", - "reviewedAt": "2026-07-14T04:10:00.000Z", - "sourceChecksum": "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", - "entries": [ - { - "path": "v24/customers:listAccessibleCustomers", - "method": "get", - "operationId": "googleads.customers.listAccessibleCustomers", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.customers.list-accessible", - "provider": "google-ads", - "revision": "1", - "title": "List directly accessible Google Ads customers", - "description": "List customer resource names directly accessible to the authenticated service account.", - "adapterKey": "customers.list-accessible", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "title": "Safe preflight", - "input": {}, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json --dry-run" - }, - { - "title": "Live read", - "input": {}, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json" - } - ] - } - ] - }, - { - "path": "v24/googleAdsFields:search", - "method": "post", - "operationId": "googleads.googleAdsFields.search", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.fields.describe", - "provider": "google-ads", - "revision": "1", - "title": "Describe one Google Ads field", - "description": "Read metadata for one exact Google Ads field name through the pinned field service.", - "adapterKey": "fields.describe", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "pattern": "^[a-z][a-z0-9_.]*$" - } - } - }, - "examples": [ - { - "title": "Describe campaign.id", - "input": { "name": "campaign.id" }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.describe --input '{\"name\":\"campaign.id\"}' --out campaign-id-field.json --dry-run" - } - ] - }, - { - "id": "google-ads.fields.search", - "provider": "google-ads", - "revision": "1", - "title": "Search Google Ads field metadata", - "description": "Run one provider-native GoogleAdsField query and explicitly paginate all result pages.", - "adapterKey": "fields.search", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "query": { "type": "string", "minLength": 1, "maxLength": 20000 }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "examples": [ - { - "title": "Search campaign fields", - "input": { "query": "SELECT name, category, data_type WHERE name LIKE 'campaign.%'" }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.search --input @fields.json --out fields-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}/googleAds:search", - "method": "post", - "operationId": "googleads.customers.googleAds.search", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.query.gaql", - "provider": "google-ads", - "revision": "1", - "title": "Run a paginated GAQL read", - "description": "Run one provider-native GAQL query against the profile-bound customer and stream every REST page into one raw page bundle.", - "adapterKey": "query.gaql", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "query": { - "type": "string", - "minLength": 1, - "maxLength": 200000, - "pattern": "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt]\\b" - } - } - }, - "examples": [ - { - "title": "Read campaigns", - "input": { - "query": "SELECT campaign.id, campaign.name FROM campaign ORDER BY campaign.id LIMIT 25" - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.query.gaql --input @gaql.json --out gaql-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}:generateKeywordIdeas", - "method": "post", - "operationId": "googleads.customers.generateKeywordIdeas", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.keyword-plan.generate-ideas", - "provider": "google-ads", - "revision": "1", - "title": "Generate keyword ideas", - "description": "Generate and explicitly paginate provider-native Keyword Planner ideas for the profile-bound customer.", - "adapterKey": "keyword-plan.generate-ideas", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "keywordSeed": { - "type": "object", - "additionalProperties": false, - "required": ["keywords"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - } - } - }, - "urlSeed": { - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { "url": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "keywordAndUrlSeed": { - "type": "object", - "additionalProperties": false, - "required": ["keywords", "url"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "geoTargetConstants": { - "type": "array", - "maxItems": 10, - "items": { "type": "string", "pattern": "^geoTargetConstants/[1-9][0-9]*$" } - }, - "language": { "type": "string", "pattern": "^languageConstants/[1-9][0-9]*$" }, - "keywordPlanNetwork": { "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] }, - "includeAdultKeywords": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } - }, - "oneOf": [ - { "required": ["keywordSeed"], "properties": { "keywordSeed": {} } }, - { "required": ["urlSeed"], "properties": { "urlSeed": {} } }, - { - "required": ["keywordAndUrlSeed"], - "properties": { "keywordAndUrlSeed": {} } - } - ] - }, - "examples": [ - { - "title": "Generate US English ideas", - "input": { - "keywordSeed": { "keywords": ["website cloner"] }, - "geoTargetConstants": ["geoTargetConstants/2840"], - "language": "languageConstants/1000", - "keywordPlanNetwork": "GOOGLE_SEARCH" - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-ideas --input @ideas.json --out keyword-ideas-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}:generateKeywordHistoricalMetrics", - "method": "post", - "operationId": "googleads.customers.generateKeywordHistoricalMetrics", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.keyword-plan.generate-historical-metrics", - "provider": "google-ads", - "revision": "1", - "title": "Generate keyword historical metrics", - "description": "Read provider-native Keyword Planner historical metrics for an explicit bounded keyword set.", - "adapterKey": "keyword-plan.generate-historical-metrics", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["keywords"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - }, - "geoTargetConstants": { - "type": "array", - "maxItems": 10, - "items": { "type": "string", "pattern": "^geoTargetConstants/[1-9][0-9]*$" } - }, - "language": { "type": "string", "pattern": "^languageConstants/[1-9][0-9]*$" }, - "keywordPlanNetwork": { "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] }, - "historicalMetricsOptions": { - "type": "object", - "additionalProperties": false, - "properties": { "includeAverageCpc": { "type": "boolean" } } - } - } - }, - "examples": [ - { - "title": "Read US English metrics", - "input": { - "keywords": ["website cloner", "clone website"], - "geoTargetConstants": ["geoTargetConstants/2840"], - "language": "languageConstants/1000", - "keywordPlanNetwork": "GOOGLE_SEARCH", - "historicalMetricsOptions": { "includeAverageCpc": true } - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-historical-metrics --input @historical.json --out keyword-history-pages.json --dry-run" - } - ] - } - ] - } - ] + "version": 1, + "provider": "google-ads", + "manifestRevision": "2026-07-14.slice4.google-ads.1", + "reviewedAt": "2026-07-14T04:10:00.000Z", + "sourceChecksum": "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", + "entries": [ + { + "path": "v24/customers:listAccessibleCustomers", + "method": "get", + "operationId": "googleads.customers.listAccessibleCustomers", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.customers.list-accessible", + "provider": "google-ads", + "revision": "1", + "title": "List directly accessible Google Ads customers", + "description": "List customer resource names directly accessible to the authenticated service account.", + "adapterKey": "customers.list-accessible", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "title": "Safe preflight", + "input": {}, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json --dry-run" + }, + { + "title": "Live read", + "input": {}, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json" + } + ] + } + ] + }, + { + "path": "v24/googleAdsFields:search", + "method": "post", + "operationId": "googleads.googleAdsFields.search", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.fields.describe", + "provider": "google-ads", + "revision": "1", + "title": "Describe one Google Ads field", + "description": "Read metadata for one exact Google Ads field name through the pinned field service.", + "adapterKey": "fields.describe", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-z][a-z0-9_.]*$" + } + } + }, + "examples": [ + { + "title": "Describe campaign.id", + "input": { "name": "campaign.id" }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.describe --input '{\"name\":\"campaign.id\"}' --out campaign-id-field.json --dry-run" + } + ] + }, + { + "id": "google-ads.fields.search", + "provider": "google-ads", + "revision": "1", + "title": "Search Google Ads field metadata", + "description": "Run one provider-native GoogleAdsField query and explicitly paginate all result pages.", + "adapterKey": "fields.search", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "query": { "type": "string", "minLength": 1, "maxLength": 20000 }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } + } + }, + "examples": [ + { + "title": "Search campaign fields", + "input": { + "query": "SELECT name, category, data_type WHERE name LIKE 'campaign.%'" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.search --input @fields.json --out fields-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}/googleAds:search", + "method": "post", + "operationId": "googleads.customers.googleAds.search", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.query.gaql", + "provider": "google-ads", + "revision": "1", + "title": "Run a paginated GAQL read", + "description": "Run one provider-native GAQL query against the profile-bound customer and stream every REST page into one raw page bundle.", + "adapterKey": "query.gaql", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 200000, + "pattern": "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt]\\b" + } + } + }, + "examples": [ + { + "title": "Read campaigns", + "input": { + "query": "SELECT campaign.id, campaign.name FROM campaign ORDER BY campaign.id LIMIT 25" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.query.gaql --input @gaql.json --out gaql-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}:generateKeywordIdeas", + "method": "post", + "operationId": "googleads.customers.generateKeywordIdeas", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.keyword-plan.generate-ideas", + "provider": "google-ads", + "revision": "1", + "title": "Generate keyword ideas", + "description": "Generate and explicitly paginate provider-native Keyword Planner ideas for the profile-bound customer.", + "adapterKey": "keyword-plan.generate-ideas", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "keywordSeed": { + "type": "object", + "additionalProperties": false, + "required": ["keywords"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + } + }, + "urlSeed": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "keywordAndUrlSeed": { + "type": "object", + "additionalProperties": false, + "required": ["keywords", "url"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "geoTargetConstants": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "pattern": "^geoTargetConstants/[1-9][0-9]*$" + } + }, + "language": { + "type": "string", + "pattern": "^languageConstants/[1-9][0-9]*$" + }, + "keywordPlanNetwork": { + "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] + }, + "includeAdultKeywords": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } + }, + "oneOf": [ + { + "required": ["keywordSeed"], + "properties": { "keywordSeed": {} } + }, + { "required": ["urlSeed"], "properties": { "urlSeed": {} } }, + { + "required": ["keywordAndUrlSeed"], + "properties": { "keywordAndUrlSeed": {} } + } + ] + }, + "examples": [ + { + "title": "Generate US English ideas", + "input": { + "keywordSeed": { "keywords": ["website cloner"] }, + "geoTargetConstants": ["geoTargetConstants/2840"], + "language": "languageConstants/1000", + "keywordPlanNetwork": "GOOGLE_SEARCH" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-ideas --input @ideas.json --out keyword-ideas-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}:generateKeywordHistoricalMetrics", + "method": "post", + "operationId": "googleads.customers.generateKeywordHistoricalMetrics", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.keyword-plan.generate-historical-metrics", + "provider": "google-ads", + "revision": "1", + "title": "Generate keyword historical metrics", + "description": "Read provider-native Keyword Planner historical metrics for an explicit bounded keyword set.", + "adapterKey": "keyword-plan.generate-historical-metrics", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["keywords"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { "type": "string", "minLength": 1, "maxLength": 80 } + }, + "geoTargetConstants": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "pattern": "^geoTargetConstants/[1-9][0-9]*$" + } + }, + "language": { + "type": "string", + "pattern": "^languageConstants/[1-9][0-9]*$" + }, + "keywordPlanNetwork": { + "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] + }, + "historicalMetricsOptions": { + "type": "object", + "additionalProperties": false, + "properties": { "includeAverageCpc": { "type": "boolean" } } + } + } + }, + "examples": [ + { + "title": "Read US English metrics", + "input": { + "keywords": ["website cloner", "clone website"], + "geoTargetConstants": ["geoTargetConstants/2840"], + "language": "languageConstants/1000", + "keywordPlanNetwork": "GOOGLE_SEARCH", + "historicalMetricsOptions": { "includeAverageCpc": true } + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-historical-metrics --input @historical.json --out keyword-history-pages.json --dry-run" + } + ] + } + ] + } + ] } diff --git a/packages/gkit/policy/gsc.reviewed.json b/packages/gkit/policy/gsc.reviewed.json index 1762bb1..a606556 100644 --- a/packages/gkit/policy/gsc.reviewed.json +++ b/packages/gkit/policy/gsc.reviewed.json @@ -1,254 +1,291 @@ { - "version": 1, - "provider": "gsc", - "manifestRevision": "2026-10-21.gsc.3", - "reviewedAt": "2026-10-21T00:00:00.000Z", - "entries": [ - { - "path": "/webmasters/v3/sites", - "method": "get", - "operationId": "sites.list", - "exposure": "executable", - "capability": { - "id": "gsc.properties.list", - "provider": "gsc", - "revision": "1", - "adapterKey": "properties.list", - "title": "List accessible Search Console properties", - "description": "List Search Console properties accessible to the profile-bound service account.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.properties.list --input '{}' --out gsc-properties.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "get", - "operationId": "sites.get", - "exposure": "inventory", - "reason": "The legacy CLI does not expose a distinct property-get workflow; properties.list carries the reviewed inventory use case." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "put", - "operationId": "sites.add", - "exposure": "inventory", - "reason": "Write operation is outside Slice 4." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "delete", - "operationId": "sites.delete", - "exposure": "inventory", - "reason": "Destructive operation is outside Slice 4." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/searchAnalytics/query", - "method": "post", - "operationId": "searchanalytics.query", - "exposure": "executable", - "capability": { - "id": "gsc.search-analytics.query", - "provider": "gsc", - "revision": "1", - "adapterKey": "search-analytics.query", - "title": "Query Search Console performance", - "description": "Run one provider-native Search Analytics read for the profile-bound or explicitly selected property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["startDate", "endDate"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "startDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, - "endDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, - "dimensions": { - "type": "array", - "maxItems": 8, - "uniqueItems": true, - "items": { - "enum": ["country", "date", "device", "hour", "page", "query", "searchAppearance"] - } - }, - "type": { "enum": ["discover", "googleNews", "image", "news", "video", "web"] }, - "dataState": { "enum": ["all", "final", "hourly_all"] }, - "aggregationType": { "enum": ["auto", "byNewsShowcasePanel", "byPage", "byProperty"] }, - "rowLimit": { "type": "integer", "minimum": 1, "maximum": 25000 }, - "startRow": { "type": "integer", "minimum": 0, "maximum": 100000000 }, - "dimensionFilterGroups": { - "type": "array", - "maxItems": 10, - "items": { "type": "object" } - } - } - }, - "examples": [ - { - "input": { - "startDate": "2026-07-01", - "endDate": "2026-07-07", - "dimensions": ["query"], - "rowLimit": 25 - }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.search-analytics.query --input @request.json --out gsc-search.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps", - "method": "get", - "operationId": "sitemaps.list", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.list", - "provider": "gsc", - "revision": "1", - "adapterKey": "sitemaps.list", - "title": "List Search Console sitemaps", - "description": "List sitemaps for one Search Console property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "sitemapIndex": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.list --input '{}' --out gsc-sitemaps.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "get", - "operationId": "sitemaps.get", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.get", - "provider": "gsc", - "revision": "2", - "adapterKey": "sitemaps.get", - "title": "Read one Search Console sitemap", - "description": "Read details for one exact sitemap URL under one Search Console property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedpath"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedpath": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "title": "Read a sitemap returned by gsc.sitemaps.list for openclaw-web", - "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.get --input @request.json --out gsc-sitemap.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "put", - "operationId": "sitemaps.submit", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.submit", - "provider": "gsc", - "revision": "1", - "adapterKey": "sitemaps.submit", - "title": "Submit one sitemap to Search Console", - "description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedpath"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedpath": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+" - } - } - }, - "examples": [ - { - "title": "Submit the openclaw-web sitemap index", - "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "delete", - "operationId": "sitemaps.delete", - "exposure": "inventory", - "reason": "Destructive operation is outside Slice 4." - }, - { - "path": "/v1/urlInspection/index:inspect", - "method": "post", - "operationId": "urlInspection.index.inspect", - "exposure": "executable", - "capability": { - "id": "gsc.url-inspection.inspect", - "provider": "gsc", - "revision": "2", - "adapterKey": "url-inspection.inspect", - "title": "Inspect one URL in Google's index", - "description": "Read Google's indexed URL inspection result for one URL under the selected property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["inspectionUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "inspectionUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "languageCode": { - "type": "string", - "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$", - "maxLength": 35 - } - } - }, - "examples": [ - { - "title": "Inspect a URL under the openclaw-web Search Console property", - "input": { "inspectionUrl": "https://openclawai.io/" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.url-inspection.inspect --input @request.json --out gsc-inspection.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "gsc", + "manifestRevision": "2026-10-21.gsc.3", + "reviewedAt": "2026-10-21T00:00:00.000Z", + "entries": [ + { + "path": "/webmasters/v3/sites", + "method": "get", + "operationId": "sites.list", + "exposure": "executable", + "capability": { + "id": "gsc.properties.list", + "provider": "gsc", + "revision": "1", + "adapterKey": "properties.list", + "title": "List accessible Search Console properties", + "description": "List Search Console properties accessible to the profile-bound service account.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.properties.list --input '{}' --out gsc-properties.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "get", + "operationId": "sites.get", + "exposure": "inventory", + "reason": "The legacy CLI does not expose a distinct property-get workflow; properties.list carries the reviewed inventory use case." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "put", + "operationId": "sites.add", + "exposure": "inventory", + "reason": "Write operation is outside Slice 4." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "delete", + "operationId": "sites.delete", + "exposure": "inventory", + "reason": "Destructive operation is outside Slice 4." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/searchAnalytics/query", + "method": "post", + "operationId": "searchanalytics.query", + "exposure": "executable", + "capability": { + "id": "gsc.search-analytics.query", + "provider": "gsc", + "revision": "1", + "adapterKey": "search-analytics.query", + "title": "Query Search Console performance", + "description": "Run one provider-native Search Analytics read for the profile-bound or explicitly selected property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["startDate", "endDate"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "startDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "endDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "dimensions": { + "type": "array", + "maxItems": 8, + "uniqueItems": true, + "items": { + "enum": [ + "country", + "date", + "device", + "hour", + "page", + "query", + "searchAppearance" + ] + } + }, + "type": { + "enum": [ + "discover", + "googleNews", + "image", + "news", + "video", + "web" + ] + }, + "dataState": { "enum": ["all", "final", "hourly_all"] }, + "aggregationType": { + "enum": ["auto", "byNewsShowcasePanel", "byPage", "byProperty"] + }, + "rowLimit": { "type": "integer", "minimum": 1, "maximum": 25000 }, + "startRow": { + "type": "integer", + "minimum": 0, + "maximum": 100000000 + }, + "dimensionFilterGroups": { + "type": "array", + "maxItems": 10, + "items": { "type": "object" } + } + } + }, + "examples": [ + { + "input": { + "startDate": "2026-07-01", + "endDate": "2026-07-07", + "dimensions": ["query"], + "rowLimit": 25 + }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.search-analytics.query --input @request.json --out gsc-search.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps", + "method": "get", + "operationId": "sitemaps.list", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.list", + "provider": "gsc", + "revision": "1", + "adapterKey": "sitemaps.list", + "title": "List Search Console sitemaps", + "description": "List sitemaps for one Search Console property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "sitemapIndex": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.list --input '{}' --out gsc-sitemaps.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "get", + "operationId": "sitemaps.get", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.get", + "provider": "gsc", + "revision": "2", + "adapterKey": "sitemaps.get", + "title": "Read one Search Console sitemap", + "description": "Read details for one exact sitemap URL under one Search Console property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedpath"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedpath": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "title": "Read a sitemap returned by gsc.sitemaps.list for openclaw-web", + "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.get --input @request.json --out gsc-sitemap.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "put", + "operationId": "sitemaps.submit", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.submit", + "provider": "gsc", + "revision": "1", + "adapterKey": "sitemaps.submit", + "title": "Submit one sitemap to Search Console", + "description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedpath"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedpath": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+" + } + } + }, + "examples": [ + { + "title": "Submit the openclaw-web sitemap index", + "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "delete", + "operationId": "sitemaps.delete", + "exposure": "inventory", + "reason": "Destructive operation is outside Slice 4." + }, + { + "path": "/v1/urlInspection/index:inspect", + "method": "post", + "operationId": "urlInspection.index.inspect", + "exposure": "executable", + "capability": { + "id": "gsc.url-inspection.inspect", + "provider": "gsc", + "revision": "2", + "adapterKey": "url-inspection.inspect", + "title": "Inspect one URL in Google's index", + "description": "Read Google's indexed URL inspection result for one URL under the selected property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["inspectionUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "inspectionUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "languageCode": { + "type": "string", + "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$", + "maxLength": 35 + } + } + }, + "examples": [ + { + "title": "Inspect a URL under the openclaw-web Search Console property", + "input": { "inspectionUrl": "https://openclawai.io/" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.url-inspection.inspect --input @request.json --out gsc-inspection.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/hubspot.reviewed.json b/packages/gkit/policy/hubspot.reviewed.json index c04ff33..80c5798 100644 --- a/packages/gkit/policy/hubspot.reviewed.json +++ b/packages/gkit/policy/hubspot.reviewed.json @@ -1,300 +1,441 @@ { - "version": 1, - "provider": "hubspot", - "manifestRevision": "2026-08-30.hubspot.read-v1.1", - "reviewedAt": "2026-08-30T05:00:00.000Z", - "entries": [ - { - "path": "/crm/properties/2026-03/{objectType}", - "method": "get", - "operationId": "crm.properties.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.properties.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.properties.list", - "title": "List CRM property metadata", - "description": "List non-sensitive property definitions for one reviewed CRM object type.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType"], - "properties": { "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] } } - }, - "examples": [{ - "input": { "objectType": "contacts" }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.properties.list --input '{\"objectType\":\"contacts\"}' --out hubspot-contact-properties.json --dry-run" - }] - } - }, - { - "path": "/crm/properties/2026-03/{objectType}", - "method": "post", - "operationId": "crm.properties.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}", - "method": "get", - "operationId": "crm.objects.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.objects.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.objects.list", - "title": "List CRM records", - "description": "List bounded records for one reviewed CRM object type and reviewed properties.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType", "properties", "limit"], - "properties": { - "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "properties": { "type": "array", "minItems": 1, "uniqueItems": true, "maxItems": 50, "items": { "$ref": "#/definitions/property" } }, - "archived": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - }, - "definitions": { "property": { "enum": ["amount", "city", "closedate", "content", "country", "createdate", "dealname", "dealstage", "domain", "email", "firstname", "hs_analytics_source", "hs_analytics_source_data_1", "hs_analytics_source_data_2", "hs_lastmodifieddate", "hs_lead_status", "hs_object_id", "hs_pipeline", "hs_pipeline_stage", "hs_ticket_category", "hs_ticket_priority", "hubspot_owner_id", "industry", "lastmodifieddate", "lastname", "lifecyclestage", "name", "numberofemployees", "pipeline", "state", "subject"] } } - }, - "examples": [{ - "input": { "objectType": "contacts", "properties": ["email", "lifecyclestage"], "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.list --input @request.json --out hubspot-contacts.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{objectType}", - "method": "post", - "operationId": "crm.objects.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}/search", - "method": "post", - "operationId": "crm.objects.search", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.objects.search", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.objects.search", - "title": "Search CRM records", - "description": "Run one bounded HubSpot CRM Search POST over reviewed objects, properties, filters, and sorts.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType", "properties", "limit"], - "properties": { - "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "properties": { "type": "array", "minItems": 1, "uniqueItems": true, "maxItems": 50, "items": { "type": "string", "minLength": 1, "maxLength": 100 } }, - "query": { "type": "string", "minLength": 1, "maxLength": 3000 }, - "filterGroups": { "type": "array", "maxItems": 5, "items": { "type": "object" } }, - "sorts": { "type": "array", "maxItems": 1, "items": { "type": "object" } }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 200 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "examples": [{ - "input": { "objectType": "contacts", "properties": ["email", "lifecyclestage"], "filterGroups": [{ "filters": [{ "propertyName": "lifecyclestage", "operator": "EQ", "value": "customer" }] }], "limit": 200 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.search --input @request.json --out hubspot-contact-search.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{objectType}/{recordId}", - "method": "patch", - "operationId": "crm.objects.update", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}/{recordId}", - "method": "delete", - "operationId": "crm.objects.archive", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}", - "method": "get", - "operationId": "crm.associations.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.associations.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.associations.list", - "title": "List record associations", - "description": "List bounded associations from one reviewed CRM record to one reviewed object type.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["fromObjectType", "objectId", "toObjectType", "limit"], - "properties": { - "fromObjectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, - "toObjectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "fromObjectType": "contacts", "objectId": "123", "toObjectType": "companies", "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.associations.list --input @request.json --out hubspot-associations.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/default/{toObjectType}/{toObjectId}", - "method": "put", - "operationId": "crm.associations.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}/{toObjectId}", - "method": "delete", - "operationId": "crm.associations.delete", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/events/event-occurrences/2026-03", - "method": "get", - "operationId": "events.occurrences.list", - "exposure": "executable", - "capability": { - "id": "hubspot.events.occurrences.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "events.occurrences.list", - "title": "List event occurrences", - "description": "List bounded HubSpot event occurrences within an explicit time window.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["occurredAfter", "occurredBefore", "properties", "limit"], - "properties": { - "occurredAfter": { "type": "string", "format": "date-time", "maxLength": 64 }, - "occurredBefore": { "type": "string", "format": "date-time", "maxLength": 64 }, - "eventType": { "type": "string", "minLength": 1, "maxLength": 200 }, - "objectType": { "enum": ["company", "contact", "deal", "ticket"] }, - "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, - "properties": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "uniqueItems": true, - "items": { "enum": ["hs_browser", "hs_city", "hs_content_type", "hs_country", "hs_device_name", "hs_device_type", "hs_page_title", "hs_referrer", "hs_touchpoint_source", "hs_url", "hs_utm_campaign", "hs_utm_medium", "hs_utm_source"] } - }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "occurredAfter": "2026-08-01T00:00:00Z", "occurredBefore": "2026-08-02T00:00:00Z", "eventType": "e_visited_page", "properties": ["hs_url", "hs_page_title"], "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.events.occurrences.list --input @request.json --out hubspot-events.json --dry-run" - }] - } - }, - { - "path": "/events/2026-03/send", - "method": "post", - "operationId": "events.occurrences.send", - "exposure": "inventory", - "reason": "Event sending is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/pipelines/2026-03/{objectType}", - "method": "get", - "operationId": "crm.pipelines.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.pipelines.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.pipelines.list", - "title": "List CRM pipelines", - "description": "List pipelines and stages for deals or tickets.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType"], - "properties": { "objectType": { "enum": ["deals", "tickets"] } } - }, - "examples": [{ - "input": { "objectType": "deals" }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.pipelines.list --input '{\"objectType\":\"deals\"}' --out hubspot-deal-pipelines.json --dry-run" - }] - } - }, - { - "path": "/crm/pipelines/2026-03/{objectType}", - "method": "post", - "operationId": "crm.pipelines.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/pipelines/2026-03/{objectType}/{pipelineId}", - "method": "delete", - "operationId": "crm.pipelines.delete", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/owners/2026-03", - "method": "get", - "operationId": "crm.owners.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.owners.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.owners.list", - "title": "List CRM owners", - "description": "List bounded active or archived HubSpot owners for attribution joins.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["limit"], - "properties": { - "archived": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.owners.list --input '{\"limit\":100}' --out hubspot-owners.json --dry-run" - }] - } - }, - { - "path": "/crm/imports/2026-03/imports", - "method": "post", - "operationId": "crm.imports.create", - "exposure": "inventory", - "reason": "Imports are mutations and are outside the read-only HubSpot V1 provider." - } - ] + "version": 1, + "provider": "hubspot", + "manifestRevision": "2026-08-30.hubspot.read-v1.1", + "reviewedAt": "2026-08-30T05:00:00.000Z", + "entries": [ + { + "path": "/crm/properties/2026-03/{objectType}", + "method": "get", + "operationId": "crm.properties.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.properties.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.properties.list", + "title": "List CRM property metadata", + "description": "List non-sensitive property definitions for one reviewed CRM object type.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + } + } + }, + "examples": [ + { + "input": { "objectType": "contacts" }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.properties.list --input '{\"objectType\":\"contacts\"}' --out hubspot-contact-properties.json --dry-run" + } + ] + } + }, + { + "path": "/crm/properties/2026-03/{objectType}", + "method": "post", + "operationId": "crm.properties.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}", + "method": "get", + "operationId": "crm.objects.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.objects.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.objects.list", + "title": "List CRM records", + "description": "List bounded records for one reviewed CRM object type and reviewed properties.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType", "properties", "limit"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "properties": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "maxItems": 50, + "items": { "$ref": "#/definitions/property" } + }, + "archived": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + }, + "definitions": { + "property": { + "enum": [ + "amount", + "city", + "closedate", + "content", + "country", + "createdate", + "dealname", + "dealstage", + "domain", + "email", + "firstname", + "hs_analytics_source", + "hs_analytics_source_data_1", + "hs_analytics_source_data_2", + "hs_lastmodifieddate", + "hs_lead_status", + "hs_object_id", + "hs_pipeline", + "hs_pipeline_stage", + "hs_ticket_category", + "hs_ticket_priority", + "hubspot_owner_id", + "industry", + "lastmodifieddate", + "lastname", + "lifecyclestage", + "name", + "numberofemployees", + "pipeline", + "state", + "subject" + ] + } + } + }, + "examples": [ + { + "input": { + "objectType": "contacts", + "properties": ["email", "lifecyclestage"], + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.list --input @request.json --out hubspot-contacts.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{objectType}", + "method": "post", + "operationId": "crm.objects.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}/search", + "method": "post", + "operationId": "crm.objects.search", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.objects.search", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.objects.search", + "title": "Search CRM records", + "description": "Run one bounded HubSpot CRM Search POST over reviewed objects, properties, filters, and sorts.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType", "properties", "limit"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "properties": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "maxItems": 50, + "items": { "type": "string", "minLength": 1, "maxLength": 100 } + }, + "query": { "type": "string", "minLength": 1, "maxLength": 3000 }, + "filterGroups": { + "type": "array", + "maxItems": 5, + "items": { "type": "object" } + }, + "sorts": { + "type": "array", + "maxItems": 1, + "items": { "type": "object" } + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 200 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 10000 } + } + }, + "examples": [ + { + "input": { + "objectType": "contacts", + "properties": ["email", "lifecyclestage"], + "filterGroups": [ + { + "filters": [ + { + "propertyName": "lifecyclestage", + "operator": "EQ", + "value": "customer" + } + ] + } + ], + "limit": 200 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.search --input @request.json --out hubspot-contact-search.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{objectType}/{recordId}", + "method": "patch", + "operationId": "crm.objects.update", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}/{recordId}", + "method": "delete", + "operationId": "crm.objects.archive", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}", + "method": "get", + "operationId": "crm.associations.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.associations.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.associations.list", + "title": "List record associations", + "description": "List bounded associations from one reviewed CRM record to one reviewed object type.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["fromObjectType", "objectId", "toObjectType", "limit"], + "properties": { + "fromObjectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, + "toObjectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { + "fromObjectType": "contacts", + "objectId": "123", + "toObjectType": "companies", + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.associations.list --input @request.json --out hubspot-associations.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/default/{toObjectType}/{toObjectId}", + "method": "put", + "operationId": "crm.associations.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}/{toObjectId}", + "method": "delete", + "operationId": "crm.associations.delete", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/events/event-occurrences/2026-03", + "method": "get", + "operationId": "events.occurrences.list", + "exposure": "executable", + "capability": { + "id": "hubspot.events.occurrences.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "events.occurrences.list", + "title": "List event occurrences", + "description": "List bounded HubSpot event occurrences within an explicit time window.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "occurredAfter", + "occurredBefore", + "properties", + "limit" + ], + "properties": { + "occurredAfter": { + "type": "string", + "format": "date-time", + "maxLength": 64 + }, + "occurredBefore": { + "type": "string", + "format": "date-time", + "maxLength": 64 + }, + "eventType": { "type": "string", "minLength": 1, "maxLength": 200 }, + "objectType": { "enum": ["company", "contact", "deal", "ticket"] }, + "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, + "properties": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "enum": [ + "hs_browser", + "hs_city", + "hs_content_type", + "hs_country", + "hs_device_name", + "hs_device_type", + "hs_page_title", + "hs_referrer", + "hs_touchpoint_source", + "hs_url", + "hs_utm_campaign", + "hs_utm_medium", + "hs_utm_source" + ] + } + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { + "occurredAfter": "2026-08-01T00:00:00Z", + "occurredBefore": "2026-08-02T00:00:00Z", + "eventType": "e_visited_page", + "properties": ["hs_url", "hs_page_title"], + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.events.occurrences.list --input @request.json --out hubspot-events.json --dry-run" + } + ] + } + }, + { + "path": "/events/2026-03/send", + "method": "post", + "operationId": "events.occurrences.send", + "exposure": "inventory", + "reason": "Event sending is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/pipelines/2026-03/{objectType}", + "method": "get", + "operationId": "crm.pipelines.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.pipelines.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.pipelines.list", + "title": "List CRM pipelines", + "description": "List pipelines and stages for deals or tickets.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType"], + "properties": { "objectType": { "enum": ["deals", "tickets"] } } + }, + "examples": [ + { + "input": { "objectType": "deals" }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.pipelines.list --input '{\"objectType\":\"deals\"}' --out hubspot-deal-pipelines.json --dry-run" + } + ] + } + }, + { + "path": "/crm/pipelines/2026-03/{objectType}", + "method": "post", + "operationId": "crm.pipelines.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/pipelines/2026-03/{objectType}/{pipelineId}", + "method": "delete", + "operationId": "crm.pipelines.delete", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/owners/2026-03", + "method": "get", + "operationId": "crm.owners.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.owners.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.owners.list", + "title": "List CRM owners", + "description": "List bounded active or archived HubSpot owners for attribution joins.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["limit"], + "properties": { + "archived": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { "limit": 100 }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.owners.list --input '{\"limit\":100}' --out hubspot-owners.json --dry-run" + } + ] + } + }, + { + "path": "/crm/imports/2026-03/imports", + "method": "post", + "operationId": "crm.imports.create", + "exposure": "inventory", + "reason": "Imports are mutations and are outside the read-only HubSpot V1 provider." + } + ] } diff --git a/packages/gkit/policy/pagespeed.reviewed.json b/packages/gkit/policy/pagespeed.reviewed.json index 77697a9..f345b35 100644 --- a/packages/gkit/policy/pagespeed.reviewed.json +++ b/packages/gkit/policy/pagespeed.reviewed.json @@ -1,63 +1,68 @@ { - "version": 1, - "provider": "pagespeed", - "manifestRevision": "2026-10-27.pagespeed.read-v1.1", - "reviewedAt": "2026-10-27T05:00:00.000Z", - "entries": [ - { - "path": "/pagespeedonline/v5/runPagespeed", - "method": "get", - "operationId": "pagespeedapi.runpagespeed", - "exposure": "executable", - "capability": { - "id": "pagespeed.run", - "provider": "pagespeed", - "revision": "1", - "adapterKey": "run", - "title": "Run a PageSpeed Insights analysis", - "description": "Run Lighthouse for one URL and return category scores plus Chrome UX Report field data for the URL and its origin when available.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "url": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "strategy": { - "enum": ["mobile", "desktop"] - }, - "categories": { - "type": "array", - "minItems": 1, - "maxItems": 4, - "uniqueItems": true, - "items": { - "enum": ["performance", "seo", "accessibility", "best-practices"] - } - }, - "locale": { - "type": "string", - "minLength": 2, - "maxLength": 16 - } - } - }, - "examples": [ - { - "input": { - "url": "https://example.com/", - "strategy": "mobile", - "categories": ["performance", "seo"] - }, - "command": "gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out pagespeed.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "pagespeed", + "manifestRevision": "2026-10-27.pagespeed.read-v1.1", + "reviewedAt": "2026-10-27T05:00:00.000Z", + "entries": [ + { + "path": "/pagespeedonline/v5/runPagespeed", + "method": "get", + "operationId": "pagespeedapi.runpagespeed", + "exposure": "executable", + "capability": { + "id": "pagespeed.run", + "provider": "pagespeed", + "revision": "1", + "adapterKey": "run", + "title": "Run a PageSpeed Insights analysis", + "description": "Run Lighthouse for one URL and return category scores plus Chrome UX Report field data for the URL and its origin when available.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "strategy": { + "enum": ["mobile", "desktop"] + }, + "categories": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "performance", + "seo", + "accessibility", + "best-practices" + ] + } + }, + "locale": { + "type": "string", + "minLength": 2, + "maxLength": 16 + } + } + }, + "examples": [ + { + "input": { + "url": "https://example.com/", + "strategy": "mobile", + "categories": ["performance", "seo"] + }, + "command": "gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out pagespeed.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/posthog.reviewed.json b/packages/gkit/policy/posthog.reviewed.json index 4172a6a..95b4126 100644 --- a/packages/gkit/policy/posthog.reviewed.json +++ b/packages/gkit/policy/posthog.reviewed.json @@ -1,82 +1,82 @@ { - "version": 1, - "provider": "posthog", - "manifestRevision": "2026-07-14.slice3.1", - "reviewedAt": "2026-07-14T03:04:00.000Z", - "entries": [ - { - "path": "/api/projects/{project_id}/query/", - "method": "post", - "operationId": "query_create", - "exposure": "executable", - "capability": { - "id": "posthog.query.run", - "provider": "posthog", - "revision": "1", - "title": "Run a bounded HogQL read", - "description": "Run one read-only HogQL SELECT or WITH query with an explicit result-row limit.", - "adapterKey": "query.run", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query", "limit"], - "properties": { - "query": { - "type": "string", - "minLength": 1, - "maxLength": 20000, - "pattern": "^\\s*(?:[Ss][Ee][Ll][Ee][Cc][Tt]|[Ww][Ii][Tt][Hh])\\b", - "allOf": [ - { - "not": { - "pattern": "[Ll][Ii][Mm][Ii][Tt]\\s+[0-9]+" - } - }, - { - "not": { - "pattern": ";" - } - }, - { - "not": { - "pattern": "(?:--|/\\*|\\*/|#)" - } - }, - { - "not": { - "pattern": "\\b(?:[Ii][Nn][Ss][Ee][Rr][Tt]|[Uu][Pp][Dd][Aa][Tt][Ee]|[Dd][Ee][Ll][Ee][Tt][Ee]|[Dd][Rr][Oo][Pp]|[Aa][Ll][Tt][Ee][Rr]|[Tt][Rr][Uu][Nn][Cc][Aa][Tt][Ee]|[Cc][Rr][Ee][Aa][Tt][Ee])\\b" - } - } - ] - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 1000 - } - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", - "limit": 20 - }, - "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", - "limit": 20 - }, - "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json" - } - ] - } - } - ] + "version": 1, + "provider": "posthog", + "manifestRevision": "2026-07-14.slice3.1", + "reviewedAt": "2026-07-14T03:04:00.000Z", + "entries": [ + { + "path": "/api/projects/{project_id}/query/", + "method": "post", + "operationId": "query_create", + "exposure": "executable", + "capability": { + "id": "posthog.query.run", + "provider": "posthog", + "revision": "1", + "title": "Run a bounded HogQL read", + "description": "Run one read-only HogQL SELECT or WITH query with an explicit result-row limit.", + "adapterKey": "query.run", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query", "limit"], + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 20000, + "pattern": "^\\s*(?:[Ss][Ee][Ll][Ee][Cc][Tt]|[Ww][Ii][Tt][Hh])\\b", + "allOf": [ + { + "not": { + "pattern": "[Ll][Ii][Mm][Ii][Tt]\\s+[0-9]+" + } + }, + { + "not": { + "pattern": ";" + } + }, + { + "not": { + "pattern": "(?:--|/\\*|\\*/|#)" + } + }, + { + "not": { + "pattern": "\\b(?:[Ii][Nn][Ss][Ee][Rr][Tt]|[Uu][Pp][Dd][Aa][Tt][Ee]|[Dd][Ee][Ll][Ee][Tt][Ee]|[Dd][Rr][Oo][Pp]|[Aa][Ll][Tt][Ee][Rr]|[Tt][Rr][Uu][Nn][Cc][Aa][Tt][Ee]|[Cc][Rr][Ee][Aa][Tt][Ee])\\b" + } + } + ] + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", + "limit": 20 + }, + "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", + "limit": 20 + }, + "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json" + } + ] + } + } + ] } diff --git a/packages/gkit/scripts/generate-bing.ts b/packages/gkit/scripts/generate-bing.ts index 8c136fe..4e63573 100644 --- a/packages/gkit/scripts/generate-bing.ts +++ b/packages/gkit/scripts/generate-bing.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "bing", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'bing', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-contract-provider.test.ts b/packages/gkit/scripts/generate-contract-provider.test.ts index c48f188..8c02ca9 100644 --- a/packages/gkit/scripts/generate-contract-provider.test.ts +++ b/packages/gkit/scripts/generate-contract-provider.test.ts @@ -1,26 +1,35 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateContractProviderArtifacts } from "./generate-contract-provider"; +import { generateContractProviderArtifacts } from './generate-contract-provider' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname describe.each([ - ["bing", 21, "bing.sites.list"], - ["gsc", 6, "gsc.properties.list"], - ["pagespeed", 1, "pagespeed.run"], -] as const)("%s contract generator", (provider, expectedCount, expectedCapability) => { - it("is deterministic and binds reviewed capabilities to the pinned source", async () => { - const first = await generateContractProviderArtifacts({ packageRoot, provider }); - const second = await generateContractProviderArtifacts({ packageRoot, provider }); - expect(second).toEqual(first); + ['bing', 21, 'bing.sites.list'], + ['gsc', 6, 'gsc.properties.list'], + ['pagespeed', 1, 'pagespeed.run'], +] as const)( + '%s contract generator', + (provider, expectedCount, expectedCapability) => { + it('is deterministic and binds reviewed capabilities to the pinned source', async () => { + const first = await generateContractProviderArtifacts({ + packageRoot, + provider, + }) + const second = await generateContractProviderArtifacts({ + packageRoot, + provider, + }) + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string; effects: string[] }>; - }; - expect(manifest.capabilities).toHaveLength(expectedCount); - expect(manifest.capabilities).toContainEqual( - expect.objectContaining({ id: expectedCapability, effects: ["read"] }), - ); - expect(first.docs).toContain(expectedCapability); - }); -}); + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string; effects: string[] }> + } + expect(manifest.capabilities).toHaveLength(expectedCount) + expect(manifest.capabilities).toContainEqual( + expect.objectContaining({ id: expectedCapability, effects: ['read'] }), + ) + expect(first.docs).toContain(expectedCapability) + }) + }, +) diff --git a/packages/gkit/scripts/generate-contract-provider.ts b/packages/gkit/scripts/generate-contract-provider.ts index aa1391d..ba8e971 100644 --- a/packages/gkit/scripts/generate-contract-provider.ts +++ b/packages/gkit/scripts/generate-contract-provider.ts @@ -1,279 +1,334 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; -}; + url: string + revision: string + checksum: string +} type SourceOperation = { - path: string; - method: string; - operationId: string; -}; + path: string + method: string + operationId: string +} type SourceContract = { - version: 1; - provider: string; - operations: SourceOperation[]; -}; + version: 1 + provider: string + operations: SourceOperation[] +} type ReviewedEntry = SourceOperation & { - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: string; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: string + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = SourceOperation & { - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedContractProviderArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} export async function generateContractProviderArtifacts(options: { - packageRoot: string; - provider: string; + packageRoot: string + provider: string }): Promise { - const sourceDirectory = join(options.packageRoot, "sources", options.provider); - const [sourceBytes, metadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "contract.json")), - readJson(join(sourceDirectory, "source.json")), - readJson( - join(options.packageRoot, "policy", `${options.provider}.reviewed.json`), - ), - ]); - const source = JSON.parse(sourceBytes.toString("utf8")) as SourceContract; - verifyInputs(options.provider, sourceBytes, source, metadata, policy); + const sourceDirectory = join(options.packageRoot, 'sources', options.provider) + const [sourceBytes, metadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'contract.json')), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(options.packageRoot, 'policy', `${options.provider}.reviewed.json`), + ), + ]) + const source = JSON.parse(sourceBytes.toString('utf8')) as SourceContract + verifyInputs(options.provider, sourceBytes, source, metadata, policy) - const reviewed = new Map(policy.entries.map((entry) => [operationKey(entry), entry])); - if (reviewed.size !== policy.entries.length) { - throw new Error(`Reviewed ${options.provider} policy contains duplicate operations.`); - } - const inventoryOperations = [...source.operations] - .sort(compareOperations) - .map((operation): InventoryOperation => { - const entry = reviewed.get(operationKey(operation)); - if (!entry) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (entry.operationId !== operation.operationId) { - throw new Error(`Reviewed operation ${operationKey(operation)} has a stale operation ID.`); - } - if (entry.exposure === "inventory") { - if (!entry.reason) - throw new Error(`Inventory operation ${operationKey(entry)} needs a reason.`); - return { ...operation, exposure: "inventory", reason: entry.reason }; - } - if (!entry.capability) { - throw new Error(`Executable operation ${operationKey(entry)} has no capability.`); - } - return { - ...operation, - exposure: "executable", - capabilityId: String(entry.capability.id), - reason: "Reviewed adapter, input, effect, and response contracts are committed.", - }; - }); + const reviewed = new Map( + policy.entries.map((entry) => [operationKey(entry), entry]), + ) + if (reviewed.size !== policy.entries.length) { + throw new Error( + `Reviewed ${options.provider} policy contains duplicate operations.`, + ) + } + const inventoryOperations = [...source.operations] + .sort(compareOperations) + .map((operation): InventoryOperation => { + const entry = reviewed.get(operationKey(operation)) + if (!entry) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (entry.operationId !== operation.operationId) { + throw new Error( + `Reviewed operation ${operationKey(operation)} has a stale operation ID.`, + ) + } + if (entry.exposure === 'inventory') { + if (!entry.reason) + throw new Error( + `Inventory operation ${operationKey(entry)} needs a reason.`, + ) + return { ...operation, exposure: 'inventory', reason: entry.reason } + } + if (!entry.capability) { + throw new Error( + `Executable operation ${operationKey(entry)} has no capability.`, + ) + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(entry.capability.id), + reason: + 'Reviewed adapter, input, effect, and response contracts are committed.', + } + }) - for (const entry of policy.entries) { - if (!source.operations.some((operation) => operationKey(operation) === operationKey(entry))) { - throw new Error( - `Reviewed operation ${operationKey(entry)} is absent from the pinned source.`, - ); - } - } + for (const entry of policy.entries) { + if ( + !source.operations.some( + (operation) => operationKey(operation) === operationKey(entry), + ) + ) { + throw new Error( + `Reviewed operation ${operationKey(entry)} is absent from the pinned source.`, + ) + } + } - const sourceDescriptor = { - url: metadata.url, - revision: metadata.revision, - checksum: metadata.checksum, - }; - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const manifest = stableJson({ - version: 1, - provider: options.provider, - revision: policy.manifestRevision, - source: sourceDescriptor, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiled = compileExecutableManifest(JSON.parse(manifest)); - return { - manifest, - inventory: stableJson({ - version: 1, - provider: options.provider, - revision: policy.manifestRevision, - source: sourceDescriptor, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiled), - inventoryDocs: renderInventoryDocs( - options.provider, - policy.manifestRevision, - inventoryOperations, - ), - }; + const sourceDescriptor = { + url: metadata.url, + revision: metadata.revision, + checksum: metadata.checksum, + } + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const manifest = stableJson({ + version: 1, + provider: options.provider, + revision: policy.manifestRevision, + source: sourceDescriptor, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiled = compileExecutableManifest(JSON.parse(manifest)) + return { + manifest, + inventory: stableJson({ + version: 1, + provider: options.provider, + revision: policy.manifestRevision, + source: sourceDescriptor, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiled), + inventoryDocs: renderInventoryDocs( + options.provider, + policy.manifestRevision, + inventoryOperations, + ), + } } export async function writeContractProviderArtifacts(options: { - packageRoot: string; - provider: string; - check: boolean; + packageRoot: string + provider: string + check: boolean }): Promise { - const artifacts = await generateContractProviderArtifacts(options); - const outputs = [ - [join(options.packageRoot, "generated", options.provider, "manifest.json"), artifacts.manifest], - [ - join(options.packageRoot, "generated", options.provider, "inventory.json"), - artifacts.inventory, - ], - [ - join(options.packageRoot, "docs/providers", options.provider, "capabilities.md"), - artifacts.docs, - ], - [ - join(options.packageRoot, "docs/providers", options.provider, "inventory.md"), - artifacts.inventoryDocs, - ], - ] as const; + const artifacts = await generateContractProviderArtifacts(options) + const outputs = [ + [ + join(options.packageRoot, 'generated', options.provider, 'manifest.json'), + artifacts.manifest, + ], + [ + join( + options.packageRoot, + 'generated', + options.provider, + 'inventory.json', + ), + artifacts.inventory, + ], + [ + join( + options.packageRoot, + 'docs/providers', + options.provider, + 'capabilities.md', + ), + artifacts.docs, + ], + [ + join( + options.packageRoot, + 'docs/providers', + options.provider, + 'inventory.md', + ), + artifacts.inventoryDocs, + ], + ] as const - if (options.check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated ${options.provider} artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (options.check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated ${options.provider} artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function verifyInputs( - provider: string, - sourceBytes: Uint8Array, - source: SourceContract, - metadata: SourceMetadata, - policy: ReviewedPolicy, + provider: string, + sourceBytes: Uint8Array, + source: SourceContract, + metadata: SourceMetadata, + policy: ReviewedPolicy, ): void { - const checksum = `sha256:${createHash("sha256").update(sourceBytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned ${provider} source checksum mismatch: expected ${metadata.checksum}.`); - } - if (source.version !== 1 || source.provider !== provider || !Array.isArray(source.operations)) { - throw new Error(`Pinned ${provider} source contract is structurally invalid.`); - } - if ( - policy.version !== 1 || - policy.provider !== provider || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error(`Reviewed ${provider} policy is structurally invalid.`); - } + const checksum = `sha256:${createHash('sha256').update(sourceBytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned ${provider} source checksum mismatch: expected ${metadata.checksum}.`, + ) + } + if ( + source.version !== 1 || + source.provider !== provider || + !Array.isArray(source.operations) + ) { + throw new Error( + `Pinned ${provider} source contract is structurally invalid.`, + ) + } + if ( + policy.version !== 1 || + policy.provider !== provider || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error(`Reviewed ${provider} policy is structurally invalid.`) + } } function renderInventoryDocs( - provider: string, - revision: string, - operations: InventoryOperation[], + provider: string, + revision: string, + operations: InventoryOperation[], ): string { - const executable = operations.filter((operation) => operation.exposure === "executable").length; - const title = - provider === "gsc" - ? "Google Search Console" - : provider === "hubspot" - ? "HubSpot" - : provider === "pagespeed" - ? "PageSpeed Insights" - : "Bing Webmaster"; - const lines = [ - "---", - "type: Reference", - `title: ${title} operation inventory`, - "description: >", - ` Generated inventory of pinned ${title} operations and their gkit exposure decisions.`, - `provider: ${provider}`, - `inventoryRevision: ${revision}`, - "---", - "", - `# ${title} operation inventory`, - "", - `This pinned inventory contains ${operations.length} operations: ${executable} executable and ${operations.length - executable} inventory-only.`, - `Inventory-only operations cannot be routed by \`gkit ${provider} api call\`.`, - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - if (provider === "hubspot") { - lines.splice( - lines.length - 2, - 0, - "HubSpot record, owner, event, association, pipeline, and property artifacts may contain PII or confidential business data. The inventory records endpoint exposure only; it does not authorize copying provider data into logs or prompts.", - "", - ); - } - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; + const executable = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const title = + provider === 'gsc' + ? 'Google Search Console' + : provider === 'hubspot' + ? 'HubSpot' + : provider === 'pagespeed' + ? 'PageSpeed Insights' + : 'Bing Webmaster' + const lines = [ + '---', + 'type: Reference', + `title: ${title} operation inventory`, + 'description: >', + ` Generated inventory of pinned ${title} operations and their gkit exposure decisions.`, + `provider: ${provider}`, + `inventoryRevision: ${revision}`, + '---', + '', + `# ${title} operation inventory`, + '', + `This pinned inventory contains ${operations.length} operations: ${executable} executable and ${operations.length - executable} inventory-only.`, + `Inventory-only operations cannot be routed by \`gkit ${provider} api call\`.`, + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + if (provider === 'hubspot') { + lines.splice( + lines.length - 2, + 0, + 'HubSpot record, owner, event, association, pipeline, and property artifacts may contain PII or confidential business data. The inventory records endpoint exposure only; it does not authorize copying provider data into logs or prompts.', + '', + ) + } + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } -function operationKey(operation: Pick): string { - return `${operation.method.toLowerCase()} ${operation.path}`; +function operationKey( + operation: Pick, +): string { + return `${operation.method.toLowerCase()} ${operation.path}` } -function compareOperations(left: SourceOperation, right: SourceOperation): number { - return left.path.localeCompare(right.path) || left.method.localeCompare(right.method); +function compareOperations( + left: SourceOperation, + right: SourceOperation, +): number { + return ( + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method) + ) } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } diff --git a/packages/gkit/scripts/generate-dataforseo.test.ts b/packages/gkit/scripts/generate-dataforseo.test.ts index a5dcc81..5084e47 100644 --- a/packages/gkit/scripts/generate-dataforseo.test.ts +++ b/packages/gkit/scripts/generate-dataforseo.test.ts @@ -1,44 +1,40 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateDataForSeoArtifacts } from "./generate-dataforseo"; +import { generateDataForSeoArtifacts } from './generate-dataforseo' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("DataForSEO artifact generator", () => { - it( - "projects one pinned source and reviewed policy into executable and inventory surfaces", - async () => { - const first = await generateDataForSeoArtifacts(packageRoot); - const second = await generateDataForSeoArtifacts(packageRoot); +describe('DataForSEO artifact generator', () => { + it('projects one pinned source and reviewed policy into executable and inventory surfaces', async () => { + const first = await generateDataForSeoArtifacts(packageRoot) + const second = await generateDataForSeoArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ - operationId: string; - exposure: "executable" | "inventory"; - reason: string; - }>; - }; + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + reason: string + }> + } - expect(manifest.capabilities.map((record) => record.id)).toEqual([ - "dataforseo.ai_optimization.llm_mentions.search.live", - "dataforseo.backlinks.bulk_ranks.live", - "dataforseo.backlinks.referring_domains.live", - "dataforseo.backlinks.summary.live", - "dataforseo.serp.google.organic.live.advanced", - ]); - expect(inventory.operations).toContainEqual( - expect.objectContaining({ - operationId: "LlmMentionsSearchLive", - exposure: "executable", - capabilityId: "dataforseo.ai_optimization.llm_mentions.search.live", - }), - ); - }, - 15_000, - ); -}); + expect(manifest.capabilities.map((record) => record.id)).toEqual([ + 'dataforseo.ai_optimization.llm_mentions.search.live', + 'dataforseo.backlinks.bulk_ranks.live', + 'dataforseo.backlinks.referring_domains.live', + 'dataforseo.backlinks.summary.live', + 'dataforseo.serp.google.organic.live.advanced', + ]) + expect(inventory.operations).toContainEqual( + expect.objectContaining({ + operationId: 'LlmMentionsSearchLive', + exposure: 'executable', + capabilityId: 'dataforseo.ai_optimization.llm_mentions.search.live', + }), + ) + }, 15_000) +}) diff --git a/packages/gkit/scripts/generate-dataforseo.ts b/packages/gkit/scripts/generate-dataforseo.ts index e0c1402..cdb4c89 100644 --- a/packages/gkit/scripts/generate-dataforseo.ts +++ b/packages/gkit/scripts/generate-dataforseo.ts @@ -1,295 +1,344 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { parse } from 'yaml' -import { parse } from "yaml"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; - -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - repository: string; - path: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + repository: string + path: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: string; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: string + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - tags: string[]; - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + path: string + method: string + operationId: string + tags: string[] + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedDataForSeoArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} -const httpMethods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]); +const httpMethods = new Set([ + 'delete', + 'get', + 'head', + 'options', + 'patch', + 'post', + 'put', +]) export async function generateDataForSeoArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/dataforseo"); - const sourcePath = join(sourceDirectory, "openapi.yaml"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(sourcePath), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/dataforseo.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/dataforseo') + const sourcePath = join(sourceDirectory, 'openapi.yaml') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(sourcePath), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(packageRoot, 'policy/dataforseo.reviewed.json'), + ), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy) - const openApi = asRecord(parse(sourceBytes.toString("utf8"))); - const operations = collectOperations(openApi); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); + const openApi = asRecord(parse(sourceBytes.toString('utf8'))) + const operations = collectOperations(openApi) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) - const reviewedByKey = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewedByKey.has(key)) { - throw new Error(`Reviewed DataForSEO policy contains duplicate operation ${key}.`); - } - const operation = operationsByKey.get(key); - if (!operation) { - throw new Error(`Reviewed DataForSEO operation ${key} is absent from the pinned source.`); - } - if (operation.operationId !== entry.operationId) { - throw new Error( - `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, - ); - } - if (entry.exposure === "executable" && !entry.capability) { - throw new Error(`Executable DataForSEO operation ${key} has no capability record.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only DataForSEO operation ${key} requires a reason.`); - } - reviewedByKey.set(key, entry); - } + const reviewedByKey = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewedByKey.has(key)) { + throw new Error( + `Reviewed DataForSEO policy contains duplicate operation ${key}.`, + ) + } + const operation = operationsByKey.get(key) + if (!operation) { + throw new Error( + `Reviewed DataForSEO operation ${key} is absent from the pinned source.`, + ) + } + if (operation.operationId !== entry.operationId) { + throw new Error( + `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, + ) + } + if (entry.exposure === 'executable' && !entry.capability) { + throw new Error( + `Executable DataForSEO operation ${key} has no capability record.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only DataForSEO operation ${key} requires a reason.`, + ) + } + reviewedByKey.set(key, entry) + } - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: `${sourceMetadata.repository}/blob/${sourceMetadata.revision}/${sourceMetadata.path}`, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifestDocument = { - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }; - const manifest = stableJson(manifestDocument); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: `${sourceMetadata.repository}/blob/${sourceMetadata.revision}/${sourceMetadata.path}`, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifestDocument = { + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + } + const manifest = stableJson(manifestDocument) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { - ...operation, - exposure: "inventory", - reason: reviewed.reason!, - }; - } - return { - ...operation, - exposure: "executable", - capabilityId: String(reviewed.capability!.id), - reason: "Reviewed adapter, input, effect, and cost contracts are committed.", - }; - }); + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { + ...operation, + exposure: 'inventory', + reason: reviewed.reason!, + } + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(reviewed.capability!.id), + reason: + 'Reviewed adapter, input, effect, and cost contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generateDataForSeoArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/dataforseo/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/dataforseo/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/dataforseo/backlinks.md"), artifacts.docs], - [join(packageRoot, "docs/providers/dataforseo/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generateDataForSeoArtifacts(packageRoot) + const outputs = [ + [ + join(packageRoot, 'generated/dataforseo/manifest.json'), + artifacts.manifest, + ], + [ + join(packageRoot, 'generated/dataforseo/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/dataforseo/backlinks.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/dataforseo/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated DataForSEO artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated DataForSEO artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: DataForSEO operation inventory", - "description: >", - " Generated inventory of pinned DataForSEO operations and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# DataForSEO operation inventory", - "", - `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only operations cannot be routed by `gkit dataforseo api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: DataForSEO operation inventory', + 'description: >', + ' Generated inventory of pinned DataForSEO operations and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# DataForSEO operation inventory', + '', + `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only operations cannot be routed by `gkit dataforseo api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } function collectOperations(openApi: JsonRecord): InventoryOperation[] { - const paths = asRecord(openApi.paths); - return Object.entries(paths) - .flatMap(([path, pathValue]) => { - const pathRecord = asRecord(pathValue); - return Object.entries(pathRecord) - .filter(([method]) => httpMethods.has(method.toLowerCase())) - .map(([method, operationValue]) => { - const operation = asRecord(operationValue); - const operationId = operation.operationId; - if (typeof operationId !== "string" || operationId.length === 0) { - throw new Error( - `Pinned DataForSEO operation ${method.toUpperCase()} ${path} has no operationId.`, - ); - } - const tags = Array.isArray(operation.tags) - ? operation.tags.filter((tag): tag is string => typeof tag === "string").sort() - : []; - return { - path, - method: method.toLowerCase(), - operationId, - tags, - exposure: "inventory" as const, - reason: "", - }; - }); - }) - .sort( - (left, right) => - left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const paths = asRecord(openApi.paths) + return Object.entries(paths) + .flatMap(([path, pathValue]) => { + const pathRecord = asRecord(pathValue) + return Object.entries(pathRecord) + .filter(([method]) => httpMethods.has(method.toLowerCase())) + .map(([method, operationValue]) => { + const operation = asRecord(operationValue) + const operationId = operation.operationId + if (typeof operationId !== 'string' || operationId.length === 0) { + throw new Error( + `Pinned DataForSEO operation ${method.toUpperCase()} ${path} has no operationId.`, + ) + } + const tags = Array.isArray(operation.tags) + ? operation.tags + .filter((tag): tag is string => typeof tag === 'string') + .sort() + : [] + return { + path, + method: method.toLowerCase(), + operationId, + tags, + exposure: 'inventory' as const, + reason: '', + } + }) + }) + .sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned DataForSEO source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned DataForSEO source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy): void { - if ( - policy.version !== 1 || - policy.provider !== "dataforseo" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed DataForSEO policy is structurally invalid."); - } + if ( + policy.version !== 1 || + policy.provider !== 'dataforseo' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed DataForSEO policy is structurally invalid.') + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned DataForSEO source is not an OpenAPI object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned DataForSEO source is not an OpenAPI object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } if (import.meta.main) { - await run(); + await run() } diff --git a/packages/gkit/scripts/generate-google-ads.test.ts b/packages/gkit/scripts/generate-google-ads.test.ts index b167836..41fd748 100644 --- a/packages/gkit/scripts/generate-google-ads.test.ts +++ b/packages/gkit/scripts/generate-google-ads.test.ts @@ -1,43 +1,53 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateGoogleAdsArtifacts } from "./generate-google-ads"; +import { generateGoogleAdsArtifacts } from './generate-google-ads' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("Google Ads artifact generator", () => { - it("projects five pinned REST methods into six reviewed read capabilities", async () => { - const first = await generateGoogleAdsArtifacts(packageRoot); - const second = await generateGoogleAdsArtifacts(packageRoot); +describe('Google Ads artifact generator', () => { + it('projects five pinned REST methods into six reviewed read capabilities', async () => { + const first = await generateGoogleAdsArtifacts(packageRoot) + const second = await generateGoogleAdsArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - source: { revision: string; checksum: string }; - capabilities: Array<{ id: string; adapterKey: string; effects: string[] }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ operationId: string; exposure: "executable" | "inventory" }>; - }; + const manifest = JSON.parse(first.manifest) as { + source: { revision: string; checksum: string } + capabilities: Array<{ id: string; adapterKey: string; effects: string[] }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + }> + } - expect(manifest.source).toMatchObject({ - revision: "20260624", - checksum: "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", - }); - expect(manifest.capabilities.map((capability) => capability.id)).toEqual([ - "google-ads.customers.list-accessible", - "google-ads.fields.describe", - "google-ads.fields.search", - "google-ads.keyword-plan.generate-historical-metrics", - "google-ads.keyword-plan.generate-ideas", - "google-ads.query.gaql", - ]); - expect(manifest.capabilities.every((capability) => capability.effects.join() === "read")).toBe( - true, - ); - expect( - inventory.operations.filter((operation) => operation.exposure === "executable"), - ).toHaveLength(5); - expect(first.docs).toContain("google-ads.query.gaql"); - expect(first.inventoryDocs).toContain("googleads.customers.googleAds.search"); - }, 20_000); -}); + expect(manifest.source).toMatchObject({ + revision: '20260624', + checksum: + 'sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7', + }) + expect(manifest.capabilities.map((capability) => capability.id)).toEqual([ + 'google-ads.customers.list-accessible', + 'google-ads.fields.describe', + 'google-ads.fields.search', + 'google-ads.keyword-plan.generate-historical-metrics', + 'google-ads.keyword-plan.generate-ideas', + 'google-ads.query.gaql', + ]) + expect( + manifest.capabilities.every( + (capability) => capability.effects.join() === 'read', + ), + ).toBe(true) + expect( + inventory.operations.filter( + (operation) => operation.exposure === 'executable', + ), + ).toHaveLength(5) + expect(first.docs).toContain('google-ads.query.gaql') + expect(first.inventoryDocs).toContain( + 'googleads.customers.googleAds.search', + ) + }, 20_000) +}) diff --git a/packages/gkit/scripts/generate-google-ads.ts b/packages/gkit/scripts/generate-google-ads.ts index 0d4895a..35003d2 100644 --- a/packages/gkit/scripts/generate-google-ads.ts +++ b/packages/gkit/scripts/generate-google-ads.ts @@ -1,293 +1,343 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + url: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capabilities?: JsonRecord[]; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capabilities?: JsonRecord[] +} type ReviewedPolicy = { - version: 1; - provider: "google-ads"; - manifestRevision: string; - reviewedAt: string; - sourceChecksum: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: 'google-ads' + manifestRevision: string + reviewedAt: string + sourceChecksum: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - capabilityIds?: string[]; - reason: string; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + capabilityIds?: string[] + reason: string +} export type GeneratedGoogleAdsArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} export async function generateGoogleAdsArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/google-ads"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "discovery-v24.json")), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/google-ads.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/google-ads') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'discovery-v24.json')), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(packageRoot, 'policy/google-ads.reviewed.json'), + ), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy, sourceMetadata); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy, sourceMetadata) - const discovery = asRecord(JSON.parse(sourceBytes.toString("utf8"))); - if (discovery.version !== "v24" || discovery.revision !== sourceMetadata.revision) { - throw new Error("Pinned Google Ads discovery version or revision does not match metadata."); - } - const operations = collectOperations(discovery); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); - const reviewedByKey = reviewEntries(policy, operationsByKey); - const capabilities = policy.entries - .flatMap((entry) => entry.capabilities ?? []) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: sourceMetadata.url, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifest = stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { ...operation, exposure: "inventory", reason: reviewed.reason! }; - } - return { - ...operation, - exposure: "executable", - capabilityIds: reviewed.capabilities!.map((capability) => String(capability.id)).sort(), - reason: - "Reviewed adapter, input, read effect, pagination, and response contracts are committed.", - }; - }); + const discovery = asRecord(JSON.parse(sourceBytes.toString('utf8'))) + if ( + discovery.version !== 'v24' || + discovery.revision !== sourceMetadata.revision + ) { + throw new Error( + 'Pinned Google Ads discovery version or revision does not match metadata.', + ) + } + const operations = collectOperations(discovery) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) + const reviewedByKey = reviewEntries(policy, operationsByKey) + const capabilities = policy.entries + .flatMap((entry) => entry.capabilities ?? []) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: sourceMetadata.url, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifest = stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { ...operation, exposure: 'inventory', reason: reviewed.reason! } + } + return { + ...operation, + exposure: 'executable', + capabilityIds: reviewed + .capabilities!.map((capability) => String(capability.id)) + .sort(), + reason: + 'Reviewed adapter, input, read effect, pagination, and response contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generateGoogleAdsArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/google-ads/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/google-ads/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/google-ads/capabilities.md"), artifacts.docs], - [join(packageRoot, "docs/providers/google-ads/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generateGoogleAdsArtifacts(packageRoot) + const outputs = [ + [ + join(packageRoot, 'generated/google-ads/manifest.json'), + artifacts.manifest, + ], + [ + join(packageRoot, 'generated/google-ads/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/google-ads/capabilities.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/google-ads/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated Google Ads artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated Google Ads artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function collectOperations(discovery: JsonRecord): InventoryOperation[] { - const operations: InventoryOperation[] = []; - walkResources(asRecord(discovery.resources), operations); - return operations.sort( - (left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const operations: InventoryOperation[] = [] + walkResources(asRecord(discovery.resources), operations) + return operations.sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } -function walkResources(resources: JsonRecord, operations: InventoryOperation[]): void { - for (const resourceValue of Object.values(resources)) { - const resource = asRecord(resourceValue); - const methods = asRecord(resource.methods ?? {}); - for (const methodValue of Object.values(methods)) { - const method = asRecord(methodValue); - if ( - typeof method.path !== "string" || - typeof method.httpMethod !== "string" || - typeof method.id !== "string" - ) { - throw new Error("Pinned Google Ads discovery contains an invalid REST method."); - } - operations.push({ - path: method.path, - method: method.httpMethod.toLowerCase(), - operationId: method.id, - exposure: "inventory", - reason: "", - }); - } - walkResources(asRecord(resource.resources ?? {}), operations); - } +function walkResources( + resources: JsonRecord, + operations: InventoryOperation[], +): void { + for (const resourceValue of Object.values(resources)) { + const resource = asRecord(resourceValue) + const methods = asRecord(resource.methods ?? {}) + for (const methodValue of Object.values(methods)) { + const method = asRecord(methodValue) + if ( + typeof method.path !== 'string' || + typeof method.httpMethod !== 'string' || + typeof method.id !== 'string' + ) { + throw new Error( + 'Pinned Google Ads discovery contains an invalid REST method.', + ) + } + operations.push({ + path: method.path, + method: method.httpMethod.toLowerCase(), + operationId: method.id, + exposure: 'inventory', + reason: '', + }) + } + walkResources(asRecord(resource.resources ?? {}), operations) + } } function reviewEntries( - policy: ReviewedPolicy, - operationsByKey: Map, + policy: ReviewedPolicy, + operationsByKey: Map, ): Map { - const reviewed = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewed.has(key)) throw new Error(`Duplicate reviewed Google Ads method ${key}.`); - const operation = operationsByKey.get(key); - if (!operation) throw new Error(`Reviewed Google Ads method ${key} is absent from source.`); - if (operation.operationId !== entry.operationId) { - throw new Error(`Reviewed Google Ads method ${key} changed operation ID.`); - } - if ( - entry.exposure === "executable" && - (!entry.capabilities || entry.capabilities.length === 0) - ) { - throw new Error(`Executable Google Ads method ${key} has no capability records.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only Google Ads method ${key} requires a reason.`); - } - reviewed.set(key, entry); - } - return reviewed; + const reviewed = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewed.has(key)) + throw new Error(`Duplicate reviewed Google Ads method ${key}.`) + const operation = operationsByKey.get(key) + if (!operation) + throw new Error( + `Reviewed Google Ads method ${key} is absent from source.`, + ) + if (operation.operationId !== entry.operationId) { + throw new Error(`Reviewed Google Ads method ${key} changed operation ID.`) + } + if ( + entry.exposure === 'executable' && + (!entry.capabilities || entry.capabilities.length === 0) + ) { + throw new Error( + `Executable Google Ads method ${key} has no capability records.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only Google Ads method ${key} requires a reason.`, + ) + } + reviewed.set(key, entry) + } + return reviewed } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: Google Ads REST operation inventory", - "description: >", - " Generated inventory of pinned Google Ads v24 REST methods and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# Google Ads REST operation inventory", - "", - `This pinned inventory contains ${operations.length} methods: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only methods cannot be routed by `gkit google-ads api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capabilities = operation.capabilityIds?.length - ? `; capabilities: ${operation.capabilityIds.map((id) => `\`${id}\``).join(", ")}` - : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capabilities} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: Google Ads REST operation inventory', + 'description: >', + ' Generated inventory of pinned Google Ads v24 REST methods and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# Google Ads REST operation inventory', + '', + `This pinned inventory contains ${operations.length} methods: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only methods cannot be routed by `gkit google-ads api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capabilities = operation.capabilityIds?.length + ? `; capabilities: ${operation.capabilityIds.map((id) => `\`${id}\``).join(', ')}` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capabilities} |`, + ) + } + return `${lines.join('\n')}\n` } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned Google Ads source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned Google Ads source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy, metadata: SourceMetadata): void { - if ( - policy.version !== 1 || - policy.provider !== "google-ads" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed Google Ads policy is structurally invalid."); - } - if (policy.sourceChecksum !== metadata.checksum) { - throw new Error( - "Reviewed Google Ads policy approval does not match the pinned source checksum.", - ); - } + if ( + policy.version !== 1 || + policy.provider !== 'google-ads' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed Google Ads policy is structurally invalid.') + } + if (policy.sourceChecksum !== metadata.checksum) { + throw new Error( + 'Reviewed Google Ads policy approval does not match the pinned source checksum.', + ) + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned Google Ads discovery is not an object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned Google Ads discovery is not an object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } -if (import.meta.main) await run(); +if (import.meta.main) await run() diff --git a/packages/gkit/scripts/generate-gsc.ts b/packages/gkit/scripts/generate-gsc.ts index e4343b2..ba6a464 100644 --- a/packages/gkit/scripts/generate-gsc.ts +++ b/packages/gkit/scripts/generate-gsc.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "gsc", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'gsc', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-hubspot.ts b/packages/gkit/scripts/generate-hubspot.ts index bac6044..1f535b2 100644 --- a/packages/gkit/scripts/generate-hubspot.ts +++ b/packages/gkit/scripts/generate-hubspot.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "hubspot", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'hubspot', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-pagespeed.ts b/packages/gkit/scripts/generate-pagespeed.ts index f552c85..2d1ef76 100644 --- a/packages/gkit/scripts/generate-pagespeed.ts +++ b/packages/gkit/scripts/generate-pagespeed.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "pagespeed", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'pagespeed', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-posthog.test.ts b/packages/gkit/scripts/generate-posthog.test.ts index 6ac2bb6..a620154 100644 --- a/packages/gkit/scripts/generate-posthog.test.ts +++ b/packages/gkit/scripts/generate-posthog.test.ts @@ -1,38 +1,38 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generatePostHogArtifacts } from "./generate-posthog"; +import { generatePostHogArtifacts } from './generate-posthog' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("PostHog artifact generator", () => { - it("projects the pinned query endpoint into one executable capability and inventory", async () => { - const first = await generatePostHogArtifacts(packageRoot); - const second = await generatePostHogArtifacts(packageRoot); +describe('PostHog artifact generator', () => { + it('projects the pinned query endpoint into one executable capability and inventory', async () => { + const first = await generatePostHogArtifacts(packageRoot) + const second = await generatePostHogArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string; adapterKey: string }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ - operationId: string; - exposure: "executable" | "inventory"; - }>; - }; + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string; adapterKey: string }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + }> + } - expect(manifest.capabilities).toEqual([ - expect.objectContaining({ - id: "posthog.query.run", - adapterKey: "query.run", - }), - ]); - expect(inventory.operations).toContainEqual( - expect.objectContaining({ - operationId: "query_create", - exposure: "executable", - }), - ); - expect(first.docs).toContain("posthog.query.run"); - }, 15_000); -}); + expect(manifest.capabilities).toEqual([ + expect.objectContaining({ + id: 'posthog.query.run', + adapterKey: 'query.run', + }), + ]) + expect(inventory.operations).toContainEqual( + expect.objectContaining({ + operationId: 'query_create', + exposure: 'executable', + }), + ) + expect(first.docs).toContain('posthog.query.run') + }, 15_000) +}) diff --git a/packages/gkit/scripts/generate-posthog.ts b/packages/gkit/scripts/generate-posthog.ts index ef2f124..145662c 100644 --- a/packages/gkit/scripts/generate-posthog.ts +++ b/packages/gkit/scripts/generate-posthog.ts @@ -1,297 +1,341 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { parse } from 'yaml' -import { parse } from "yaml"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; - -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + url: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: "posthog"; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: 'posthog' + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - tags: string[]; - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + path: string + method: string + operationId: string + tags: string[] + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedPostHogArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} -const httpMethods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]); +const httpMethods = new Set([ + 'delete', + 'get', + 'head', + 'options', + 'patch', + 'post', + 'put', +]) export async function generatePostHogArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/posthog"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "openapi.yaml")), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/posthog.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/posthog') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'openapi.yaml')), + readJson(join(sourceDirectory, 'source.json')), + readJson(join(packageRoot, 'policy/posthog.reviewed.json')), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy) - const openApi = asRecord(parse(sourceBytes.toString("utf8"))); - const operations = collectOperations(openApi); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); - const reviewedByKey = reviewEntries(policy, operationsByKey); - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: sourceMetadata.url, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifest = stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { - ...operation, - exposure: "inventory", - reason: reviewed.reason!, - }; - } - return { - ...operation, - exposure: "executable", - capabilityId: String(reviewed.capability!.id), - reason: "Reviewed adapter, input, effect, and response contracts are committed.", - }; - }); + const openApi = asRecord(parse(sourceBytes.toString('utf8'))) + const operations = collectOperations(openApi) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) + const reviewedByKey = reviewEntries(policy, operationsByKey) + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: sourceMetadata.url, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifest = stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { + ...operation, + exposure: 'inventory', + reason: reviewed.reason!, + } + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(reviewed.capability!.id), + reason: + 'Reviewed adapter, input, effect, and response contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generatePostHogArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/posthog/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/posthog/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/posthog/capabilities.md"), artifacts.docs], - [join(packageRoot, "docs/providers/posthog/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generatePostHogArtifacts(packageRoot) + const outputs = [ + [join(packageRoot, 'generated/posthog/manifest.json'), artifacts.manifest], + [ + join(packageRoot, 'generated/posthog/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/posthog/capabilities.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/posthog/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated PostHog artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated PostHog artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function reviewEntries( - policy: ReviewedPolicy, - operationsByKey: Map, + policy: ReviewedPolicy, + operationsByKey: Map, ): Map { - const reviewedByKey = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewedByKey.has(key)) { - throw new Error(`Reviewed PostHog policy contains duplicate operation ${key}.`); - } - const operation = operationsByKey.get(key); - if (!operation) { - throw new Error(`Reviewed PostHog operation ${key} is absent from the pinned source.`); - } - if (operation.operationId !== entry.operationId) { - throw new Error( - `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, - ); - } - if (entry.exposure === "executable" && !entry.capability) { - throw new Error(`Executable PostHog operation ${key} has no capability record.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only PostHog operation ${key} requires a reason.`); - } - reviewedByKey.set(key, entry); - } - return reviewedByKey; + const reviewedByKey = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewedByKey.has(key)) { + throw new Error( + `Reviewed PostHog policy contains duplicate operation ${key}.`, + ) + } + const operation = operationsByKey.get(key) + if (!operation) { + throw new Error( + `Reviewed PostHog operation ${key} is absent from the pinned source.`, + ) + } + if (operation.operationId !== entry.operationId) { + throw new Error( + `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, + ) + } + if (entry.exposure === 'executable' && !entry.capability) { + throw new Error( + `Executable PostHog operation ${key} has no capability record.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only PostHog operation ${key} requires a reason.`, + ) + } + reviewedByKey.set(key, entry) + } + return reviewedByKey } function collectOperations(openApi: JsonRecord): InventoryOperation[] { - const paths = asRecord(openApi.paths); - return Object.entries(paths) - .flatMap(([path, pathValue]) => { - const pathRecord = asRecord(pathValue); - return Object.entries(pathRecord) - .filter(([method]) => httpMethods.has(method.toLowerCase())) - .map(([method, operationValue]) => { - const operation = asRecord(operationValue); - const operationId = operation.operationId; - if (typeof operationId !== "string" || operationId.length === 0) { - throw new Error( - `Pinned PostHog operation ${method.toUpperCase()} ${path} has no operationId.`, - ); - } - const tags = Array.isArray(operation.tags) - ? operation.tags.filter((tag): tag is string => typeof tag === "string").sort() - : []; - return { - path, - method: method.toLowerCase(), - operationId, - tags, - exposure: "inventory" as const, - reason: "", - }; - }); - }) - .sort( - (left, right) => - left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const paths = asRecord(openApi.paths) + return Object.entries(paths) + .flatMap(([path, pathValue]) => { + const pathRecord = asRecord(pathValue) + return Object.entries(pathRecord) + .filter(([method]) => httpMethods.has(method.toLowerCase())) + .map(([method, operationValue]) => { + const operation = asRecord(operationValue) + const operationId = operation.operationId + if (typeof operationId !== 'string' || operationId.length === 0) { + throw new Error( + `Pinned PostHog operation ${method.toUpperCase()} ${path} has no operationId.`, + ) + } + const tags = Array.isArray(operation.tags) + ? operation.tags + .filter((tag): tag is string => typeof tag === 'string') + .sort() + : [] + return { + path, + method: method.toLowerCase(), + operationId, + tags, + exposure: 'inventory' as const, + reason: '', + } + }) + }) + .sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: PostHog operation inventory", - "description: >", - " Generated inventory of pinned PostHog operations and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# PostHog operation inventory", - "", - `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only operations cannot be routed by `gkit posthog api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: PostHog operation inventory', + 'description: >', + ' Generated inventory of pinned PostHog operations and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# PostHog operation inventory', + '', + `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only operations cannot be routed by `gkit posthog api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned PostHog source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned PostHog source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy): void { - if ( - policy.version !== 1 || - policy.provider !== "posthog" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed PostHog policy is structurally invalid."); - } + if ( + policy.version !== 1 || + policy.provider !== 'posthog' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed PostHog policy is structurally invalid.') + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned PostHog source is not an OpenAPI object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned PostHog source is not an OpenAPI object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } if (import.meta.main) { - await run(); + await run() } diff --git a/packages/gkit/scripts/release-preflight.test.ts b/packages/gkit/scripts/release-preflight.test.ts index 9a26b26..62ffea0 100644 --- a/packages/gkit/scripts/release-preflight.test.ts +++ b/packages/gkit/scripts/release-preflight.test.ts @@ -1,62 +1,72 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { compareSemVer, decideRelease, parseSemVer } from "./release-preflight"; +import { compareSemVer, decideRelease, parseSemVer } from './release-preflight' -describe("release preflight", () => { - it("implements SemVer precedence including prereleases", () => { - const ordered = [ - "1.0.0-alpha", - "1.0.0-alpha.1", - "1.0.0-alpha.beta", - "1.0.0-beta", - "1.0.0-beta.2", - "1.0.0-beta.11", - "1.0.0-rc.1", - "1.0.0", - ].map(parseSemVer); - for (let index = 1; index < ordered.length; index += 1) { - expect(compareSemVer(ordered[index - 1]!, ordered[index]!)).toBe(-1); - } - expect( - compareSemVer( - parseSemVer("999999999999999999999999.0.0"), - parseSemVer("1000000000000000000000000.0.0"), - ), - ).toBe(-1); - }); +describe('release preflight', () => { + it('implements SemVer precedence including prereleases', () => { + const ordered = [ + '1.0.0-alpha', + '1.0.0-alpha.1', + '1.0.0-alpha.beta', + '1.0.0-beta', + '1.0.0-beta.2', + '1.0.0-beta.11', + '1.0.0-rc.1', + '1.0.0', + ].map(parseSemVer) + for (let index = 1; index < ordered.length; index += 1) { + expect(compareSemVer(ordered[index - 1]!, ordered[index]!)).toBe(-1) + } + expect( + compareSemVer( + parseSemVer('999999999999999999999999.0.0'), + parseSemVer('1000000000000000000000000.0.0'), + ), + ).toBe(-1) + }) - it("rejects invalid SemVer values", () => { - for (const version of ["v1.0.0", "1.0", "01.0.0", "1.0.0-01", "1.0.0-"]) { - expect(() => parseSemVer(version)).toThrow("Invalid SemVer"); - } - }); + it('rejects invalid SemVer values', () => { + for (const version of ['v1.0.0', '1.0', '01.0.0', '1.0.0-01', '1.0.0-']) { + expect(() => parseSemVer(version)).toThrow('Invalid SemVer') + } + }) - it("allows a new version only when it is greater than every release tag", () => { - expect(decideRelease("1.2.0", ["not-a-release", "v1.1.9"], "abc", new Map())).toEqual({ - version: "1.2.0", - tag: "v1.2.0", - prerelease: false, - tagExists: false, - }); - expect(() => decideRelease("1.1.8", ["v1.1.9"], "abc", new Map())).toThrow( - "must be greater", - ); - }); + it('allows a new version only when it is greater than every release tag', () => { + expect( + decideRelease('1.2.0', ['not-a-release', 'v1.1.9'], 'abc', new Map()), + ).toEqual({ + version: '1.2.0', + tag: 'v1.2.0', + prerelease: false, + tagExists: false, + }) + expect(() => decideRelease('1.1.8', ['v1.1.9'], 'abc', new Map())).toThrow( + 'must be greater', + ) + }) - it("permits an idempotent rerun only when the tag points to the same commit", () => { - expect(decideRelease("1.2.0-beta.1", ["v1.2.0-beta.1"], "abc", new Map([ - ["v1.2.0-beta.1", "abc"], - ]))).toMatchObject({ prerelease: true, tagExists: true }); - expect(() => - decideRelease("1.2.0-beta.1", ["v1.2.0-beta.1"], "abc", new Map([ - ["v1.2.0-beta.1", "different"], - ])), - ).toThrow("not abc"); - }); + it('permits an idempotent rerun only when the tag points to the same commit', () => { + expect( + decideRelease( + '1.2.0-beta.1', + ['v1.2.0-beta.1'], + 'abc', + new Map([['v1.2.0-beta.1', 'abc']]), + ), + ).toMatchObject({ prerelease: true, tagExists: true }) + expect(() => + decideRelease( + '1.2.0-beta.1', + ['v1.2.0-beta.1'], + 'abc', + new Map([['v1.2.0-beta.1', 'different']]), + ), + ).toThrow('not abc') + }) - it("does not let build metadata bypass equal precedence", () => { - expect(() => decideRelease("1.2.0+new", ["v1.2.0+old"], "abc", new Map())).toThrow( - "must be greater", - ); - }); -}); + it('does not let build metadata bypass equal precedence', () => { + expect(() => + decideRelease('1.2.0+new', ['v1.2.0+old'], 'abc', new Map()), + ).toThrow('must be greater') + }) +}) diff --git a/packages/gkit/scripts/release-preflight.ts b/packages/gkit/scripts/release-preflight.ts index e104bf3..44909c3 100644 --- a/packages/gkit/scripts/release-preflight.ts +++ b/packages/gkit/scripts/release-preflight.ts @@ -1,241 +1,265 @@ -import { appendFile, readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { appendFile, readFile } from 'node:fs/promises' +import { resolve } from 'node:path' type SemVer = Readonly<{ - raw: string; - major: bigint; - minor: bigint; - patch: bigint; - prerelease: readonly string[]; -}>; + raw: string + major: bigint + minor: bigint + patch: bigint + prerelease: readonly string[] +}> type ReleaseDecision = Readonly<{ - version: string; - tag: string; - prerelease: boolean; - tagExists: boolean; -}>; + version: string + tag: string + prerelease: boolean + tagExists: boolean +}> type CommandResult = Readonly<{ - stdout: string; - stderr: string; -}>; + stdout: string + stderr: string +}> -const packagePath = "packages/gkit/package.json"; +const packagePath = 'packages/gkit/package.json' const semVerPattern = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ export function parseSemVer(value: string): SemVer { - const match = semVerPattern.exec(value); - if (!match) { - throw new Error(`Invalid SemVer version: ${value}`); - } - return Object.freeze({ - raw: value, - major: BigInt(match[1]), - minor: BigInt(match[2]), - patch: BigInt(match[3]), - prerelease: Object.freeze(match[4]?.split(".") ?? []), - }); + const match = semVerPattern.exec(value) + if (!match) { + throw new Error(`Invalid SemVer version: ${value}`) + } + return Object.freeze({ + raw: value, + major: BigInt(match[1]), + minor: BigInt(match[2]), + patch: BigInt(match[3]), + prerelease: Object.freeze(match[4]?.split('.') ?? []), + }) } export function compareSemVer(left: SemVer, right: SemVer): number { - for (const key of ["major", "minor", "patch"] as const) { - if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1; - } - if (left.prerelease.length === 0 || right.prerelease.length === 0) { - if (left.prerelease.length === right.prerelease.length) return 0; - return left.prerelease.length === 0 ? 1 : -1; - } - const length = Math.max(left.prerelease.length, right.prerelease.length); - for (let index = 0; index < length; index += 1) { - const leftIdentifier = left.prerelease[index]; - const rightIdentifier = right.prerelease[index]; - if (leftIdentifier === rightIdentifier) continue; - if (leftIdentifier === undefined) return -1; - if (rightIdentifier === undefined) return 1; - const leftNumeric = /^\d+$/.test(leftIdentifier); - const rightNumeric = /^\d+$/.test(rightIdentifier); - if (leftNumeric && rightNumeric) { - if (leftIdentifier.length !== rightIdentifier.length) { - return leftIdentifier.length < rightIdentifier.length ? -1 : 1; - } - return leftIdentifier < rightIdentifier ? -1 : 1; - } - if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1; - return leftIdentifier < rightIdentifier ? -1 : 1; - } - return 0; + for (const key of ['major', 'minor', 'patch'] as const) { + if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1 + } + if (left.prerelease.length === 0 || right.prerelease.length === 0) { + if (left.prerelease.length === right.prerelease.length) return 0 + return left.prerelease.length === 0 ? 1 : -1 + } + const length = Math.max(left.prerelease.length, right.prerelease.length) + for (let index = 0; index < length; index += 1) { + const leftIdentifier = left.prerelease[index] + const rightIdentifier = right.prerelease[index] + if (leftIdentifier === rightIdentifier) continue + if (leftIdentifier === undefined) return -1 + if (rightIdentifier === undefined) return 1 + const leftNumeric = /^\d+$/.test(leftIdentifier) + const rightNumeric = /^\d+$/.test(rightIdentifier) + if (leftNumeric && rightNumeric) { + if (leftIdentifier.length !== rightIdentifier.length) { + return leftIdentifier.length < rightIdentifier.length ? -1 : 1 + } + return leftIdentifier < rightIdentifier ? -1 : 1 + } + if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1 + return leftIdentifier < rightIdentifier ? -1 : 1 + } + return 0 } export function decideRelease( - versionValue: string, - tags: readonly string[], - targetSha: string, - tagTargets: ReadonlyMap, + versionValue: string, + tags: readonly string[], + targetSha: string, + tagTargets: ReadonlyMap, ): ReleaseDecision { - const version = parseSemVer(versionValue); - const tag = `v${version.raw}`; - const releaseTags = tags.flatMap((candidate) => { - if (!candidate.startsWith("v")) return []; - try { - return [{ tag: candidate, version: parseSemVer(candidate.slice(1)) }]; - } catch { - return []; - } - }); - const existingTarget = tagTargets.get(tag); - if (existingTarget !== undefined && existingTarget !== targetSha) { - throw new Error(`Existing tag ${tag} points to ${existingTarget}, not ${targetSha}.`); - } - for (const existing of releaseTags) { - if (existing.tag === tag) continue; - if (compareSemVer(version, existing.version) <= 0) { - throw new Error( - `Version ${version.raw} must be greater than existing release tag ${existing.tag}.`, - ); - } - } - return Object.freeze({ - version: version.raw, - tag, - prerelease: version.prerelease.length > 0, - tagExists: existingTarget !== undefined, - }); + const version = parseSemVer(versionValue) + const tag = `v${version.raw}` + const releaseTags = tags.flatMap((candidate) => { + if (!candidate.startsWith('v')) return [] + try { + return [{ tag: candidate, version: parseSemVer(candidate.slice(1)) }] + } catch { + return [] + } + }) + const existingTarget = tagTargets.get(tag) + if (existingTarget !== undefined && existingTarget !== targetSha) { + throw new Error( + `Existing tag ${tag} points to ${existingTarget}, not ${targetSha}.`, + ) + } + for (const existing of releaseTags) { + if (existing.tag === tag) continue + if (compareSemVer(version, existing.version) <= 0) { + throw new Error( + `Version ${version.raw} must be greater than existing release tag ${existing.tag}.`, + ) + } + } + return Object.freeze({ + version: version.raw, + tag, + prerelease: version.prerelease.length > 0, + tagExists: existingTarget !== undefined, + }) } -if (import.meta.main) await main(); +if (import.meta.main) await main() async function main(): Promise { - const options = parseOptions(Bun.argv.slice(2)); - if (options.ref !== "refs/heads/main") { - throw new Error(`Releases must run from refs/heads/main, received ${options.ref}.`); - } - const packageJson = await readPackageJson(resolve(packagePath)); - const version = requireVersion(packageJson); - if (options.eventName === "push" && options.beforeSha !== undefined) { - const previousPackageJson = await readPackageJsonAtCommit(options.beforeSha); - if (previousPackageJson !== undefined && requireVersion(previousPackageJson) === version) { - await writeOutputs(options.output, { release: "false" }); - process.stdout.write("Package version did not change; release skipped.\n"); - return; - } - } - const tags = (await runCommand(["git", "tag", "--list"], process.cwd())).stdout - .split("\n") - .filter(Boolean); - const tagTargets = new Map(); - for (const tag of tags) { - const target = ( - await runCommand(["git", "rev-list", "-n", "1", tag], process.cwd()) - ).stdout.trim(); - tagTargets.set(tag, target); - } - const decision = decideRelease(version, tags, options.targetSha, tagTargets); - await writeOutputs(options.output, { - release: "true", - version: decision.version, - tag: decision.tag, - prerelease: String(decision.prerelease), - tag_exists: String(decision.tagExists), - }); - process.stdout.write( - `Release preflight passed for ${decision.tag} at ${options.targetSha}.\n`, - ); + const options = parseOptions(Bun.argv.slice(2)) + if (options.ref !== 'refs/heads/main') { + throw new Error( + `Releases must run from refs/heads/main, received ${options.ref}.`, + ) + } + const packageJson = await readPackageJson(resolve(packagePath)) + const version = requireVersion(packageJson) + if (options.eventName === 'push' && options.beforeSha !== undefined) { + const previousPackageJson = await readPackageJsonAtCommit(options.beforeSha) + if ( + previousPackageJson !== undefined && + requireVersion(previousPackageJson) === version + ) { + await writeOutputs(options.output, { release: 'false' }) + process.stdout.write('Package version did not change; release skipped.\n') + return + } + } + const tags = ( + await runCommand(['git', 'tag', '--list'], process.cwd()) + ).stdout + .split('\n') + .filter(Boolean) + const tagTargets = new Map() + for (const tag of tags) { + const target = ( + await runCommand(['git', 'rev-list', '-n', '1', tag], process.cwd()) + ).stdout.trim() + tagTargets.set(tag, target) + } + const decision = decideRelease(version, tags, options.targetSha, tagTargets) + await writeOutputs(options.output, { + release: 'true', + version: decision.version, + tag: decision.tag, + prerelease: String(decision.prerelease), + tag_exists: String(decision.tagExists), + }) + process.stdout.write( + `Release preflight passed for ${decision.tag} at ${options.targetSha}.\n`, + ) } function parseOptions(arguments_: readonly string[]): { - eventName: string; - beforeSha?: string; - targetSha: string; - ref: string; - output: string; + eventName: string + beforeSha?: string + targetSha: string + ref: string + output: string } { - const values = new Map(); - for (let index = 0; index < arguments_.length; index += 2) { - const key = arguments_[index]; - const value = arguments_[index + 1]; - if (!key?.startsWith("--") || value === undefined) { - throw new Error("Release preflight arguments must be --key value pairs."); - } - values.set(key.slice(2), value); - } - const eventName = requireOption(values, "event-name"); - const targetSha = requireOption(values, "target-sha"); - const ref = requireOption(values, "ref"); - const output = requireOption(values, "output"); - const beforeSha = values.get("before-sha"); - return { eventName, targetSha, ref, output, ...(beforeSha ? { beforeSha } : {}) }; + const values = new Map() + for (let index = 0; index < arguments_.length; index += 2) { + const key = arguments_[index] + const value = arguments_[index + 1] + if (!key?.startsWith('--') || value === undefined) { + throw new Error('Release preflight arguments must be --key value pairs.') + } + values.set(key.slice(2), value) + } + const eventName = requireOption(values, 'event-name') + const targetSha = requireOption(values, 'target-sha') + const ref = requireOption(values, 'ref') + const output = requireOption(values, 'output') + const beforeSha = values.get('before-sha') + return { + eventName, + targetSha, + ref, + output, + ...(beforeSha ? { beforeSha } : {}), + } } -function requireOption(values: ReadonlyMap, name: string): string { - const value = values.get(name); - if (!value) throw new Error(`Missing required option --${name}.`); - return value; +function requireOption( + values: ReadonlyMap, + name: string, +): string { + const value = values.get(name) + if (!value) throw new Error(`Missing required option --${name}.`) + return value } async function readPackageJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as unknown; + return JSON.parse(await readFile(path, 'utf8')) as unknown } -async function readPackageJsonAtCommit(commit: string): Promise { - if (/^0+$/.test(commit)) return undefined; - const result = await runCommand( - ["git", "show", `${commit}:${packagePath}`], - process.cwd(), - true, - ); - if (!result) return undefined; - return JSON.parse(result.stdout) as unknown; +async function readPackageJsonAtCommit( + commit: string, +): Promise { + if (/^0+$/.test(commit)) return undefined + const result = await runCommand( + ['git', 'show', `${commit}:${packagePath}`], + process.cwd(), + true, + ) + if (!result) return undefined + return JSON.parse(result.stdout) as unknown } function requireVersion(value: unknown): string { - if (!value || typeof value !== "object" || !("version" in value)) { - throw new Error(`${packagePath} must contain a version.`); - } - const version = (value as { version?: unknown }).version; - if (typeof version !== "string") throw new Error("Package version must be a string."); - parseSemVer(version); - return version; + if (!value || typeof value !== 'object' || !('version' in value)) { + throw new Error(`${packagePath} must contain a version.`) + } + const version = (value as { version?: unknown }).version + if (typeof version !== 'string') + throw new Error('Package version must be a string.') + parseSemVer(version) + return version } async function writeOutputs( - outputPath: string, - values: Readonly>, + outputPath: string, + values: Readonly>, ): Promise { - const content = Object.entries(values) - .map(([key, value]) => `${key}=${value}\n`) - .join(""); - await appendFile(outputPath, content, "utf8"); + const content = Object.entries(values) + .map(([key, value]) => `${key}=${value}\n`) + .join('') + await appendFile(outputPath, content, 'utf8') } -function runCommand(command: readonly string[], cwd: string): Promise; function runCommand( - command: readonly string[], - cwd: string, - allowFailure: true, -): Promise; + command: readonly string[], + cwd: string, +): Promise +function runCommand( + command: readonly string[], + cwd: string, + allowFailure: true, +): Promise async function runCommand( - command: readonly string[], - cwd: string, - allowFailure = false, + command: readonly string[], + cwd: string, + allowFailure = false, ): Promise { - const child = Bun.spawn([...command], { - cwd, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - if (exitCode !== 0) { - if (allowFailure) return undefined; - throw new Error( - `Command failed (${exitCode}): ${command.join(" ")}\n${stdout}${stderr}`.trimEnd(), - ); - } - return { stdout, stderr }; + const child = Bun.spawn([...command], { + cwd, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + if (exitCode !== 0) { + if (allowFailure) return undefined + throw new Error( + `Command failed (${exitCode}): ${command.join(' ')}\n${stdout}${stderr}`.trimEnd(), + ) + } + return { stdout, stderr } } diff --git a/packages/gkit/scripts/test-site-snapshot.ts b/packages/gkit/scripts/test-site-snapshot.ts index 6f9f5f9..4e2b39e 100644 --- a/packages/gkit/scripts/test-site-snapshot.ts +++ b/packages/gkit/scripts/test-site-snapshot.ts @@ -1,322 +1,364 @@ -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 }); +import assert from 'node:assert/strict' +import { execFile } from 'node:child_process' +import { + mkdtemp, + mkdir, + writeFile, + readFile, + readdir, + rm, + cp, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +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: {} }), -); + 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); + 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 + 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 }, -); + { 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, -}; + ...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 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; + 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 })); + 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 }); + 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 3aa0f62..f9511cb 100644 --- a/packages/gkit/scripts/verify-package-artifact.ts +++ b/packages/gkit/scripts/verify-package-artifact.ts @@ -1,257 +1,312 @@ -import { constants } from "node:fs"; -import { chmod, 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"; +import { createHash } from 'node:crypto' +import { constants } from 'node:fs' +import { + chmod, + copyFile, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join, resolve } from 'node:path' type PackResult = Readonly<{ - filename: string; - files?: readonly Readonly<{ path: string }>[]; -}>; + filename: string + files?: readonly Readonly<{ path: string }>[] +}> type PackageJson = Readonly<{ - name?: unknown; - version?: unknown; - private?: unknown; -}>; + name?: unknown + version?: unknown + private?: unknown +}> -const allowedTopLevelEntries = new Set(["dist", "docs", "generated", "package.json"]); +const allowedTopLevelEntries = new Set([ + 'dist', + 'docs', + 'generated', + 'package.json', +]) const forbiddenPathPattern = - /(^|\/)(?:\.env(?:\..*)?|\.npmrc|bunfig\.toml|[^/]*\.test\.[^/]+|__tests__|evals?|scripts?|sources?|policy)(?:\/|$)/i; + /(^|\/)(?:\.env(?:\..*)?|\.npmrc|bunfig\.toml|[^/]*\.test\.[^/]+|__tests__|evals?|scripts?|sources?|policy)(?:\/|$)/i const secretMaterialPatterns = [ - /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, - /\bgh(?:p|o|u|s|r)_[A-Za-z0-9]{20,}\b/, - /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, -] as const; + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, + /\bgh(?:p|o|u|s|r)_[A-Za-z0-9]{20,}\b/, + /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, +] as const -const packageRoot = resolve(import.meta.dir, ".."); -const outputDirectory = parseOutputDirectory(Bun.argv.slice(2)); -const temporaryRoot = await mkdtemp(join(tmpdir(), "gkit-package-artifact-")); -const packDirectory = join(temporaryRoot, "pack"); -const consumerDirectory = join(temporaryRoot, "consumer"); +const packageRoot = resolve(import.meta.dir, '..') +const outputDirectory = parseOutputDirectory(Bun.argv.slice(2)) +const temporaryRoot = await mkdtemp(join(tmpdir(), 'gkit-package-artifact-')) +const packDirectory = join(temporaryRoot, 'pack') +const consumerDirectory = join(temporaryRoot, 'consumer') try { - await Promise.all([ - mkdir(packDirectory, { recursive: true }), - mkdir(consumerDirectory, { recursive: true }), - ]); - const packageJson = JSON.parse( - await readFile(join(packageRoot, "package.json"), "utf8"), - ) as PackageJson; - if (packageJson.name !== "gkit" || typeof packageJson.version !== "string") { - throw new Error("The package must have name gkit and a string version."); - } - if (packageJson.private !== true) { - throw new Error("The gkit package must remain private to prevent registry publication."); - } + await Promise.all([ + mkdir(packDirectory, { recursive: true }), + mkdir(consumerDirectory, { recursive: true }), + ]) + const packageJson = JSON.parse( + await readFile(join(packageRoot, 'package.json'), 'utf8'), + ) as PackageJson + if (packageJson.name !== 'gkit' || typeof packageJson.version !== 'string') { + throw new Error('The package must have name gkit and a string version.') + } + if (packageJson.private !== true) { + throw new Error( + 'The gkit package must remain private to prevent registry publication.', + ) + } - // The artifact ships a Bun bundle: runtime deps (incl. the private @celados/argc) - // are inlined, so public consumers install with no registry auth. - await runCommand(["bun", "run", "build"], packageRoot); - const packed = await runCommand( - ["npm", "pack", "--json", "--pack-destination", packDirectory, packageRoot], - temporaryRoot, - ); - const packResults = JSON.parse(packed.stdout) as PackResult[]; - const packResult = packResults[0]; - if (!packResult?.filename || !packResult.files) { - throw new Error("npm pack did not report the package filename and contents."); - } - await verifyPackageContents(packResult.files.map((file) => file.path)); + // The artifact ships a Bun bundle: runtime deps (incl. the private @celados/argc) + // are inlined, so public consumers install with no registry auth. + await runCommand(['bun', 'run', 'build'], packageRoot) + const packed = await runCommand( + ['npm', 'pack', '--json', '--pack-destination', packDirectory, packageRoot], + temporaryRoot, + ) + const packResults = JSON.parse(packed.stdout) as PackResult[] + const packResult = packResults[0] + if (!packResult?.filename || !packResult.files) { + throw new Error( + 'npm pack did not report the package filename and contents.', + ) + } + await verifyPackageContents(packResult.files.map((file) => file.path)) - const tarballPath = join(packDirectory, packResult.filename); - const globalDirectory = join(consumerDirectory, "global"); - const globalBinDirectory = join(consumerDirectory, "bin"); - await runCommand( - [ - "bun", - "add", - "--global", - "--registry", - "https://registry.npmjs.org", - "--network-concurrency", - "8", - `gkit@${tarballPath}`, - ], - consumerDirectory, - { - BUN_INSTALL_GLOBAL_DIR: globalDirectory, - BUN_INSTALL_BIN: globalBinDirectory, - BUN_INSTALL_CACHE_DIR: join(consumerDirectory, "cache"), - }, - ); + const tarballPath = join(packDirectory, packResult.filename) + const globalDirectory = join(consumerDirectory, 'global') + const globalBinDirectory = join(consumerDirectory, 'bin') + await runCommand( + [ + 'bun', + 'add', + '--global', + '--registry', + 'https://registry.npmjs.org', + '--network-concurrency', + '8', + `gkit@${tarballPath}`, + ], + consumerDirectory, + { + BUN_INSTALL_GLOBAL_DIR: globalDirectory, + BUN_INSTALL_BIN: globalBinDirectory, + BUN_INSTALL_CACHE_DIR: join(consumerDirectory, 'cache'), + }, + ) - const installedPackageJson = await readFile( - join(globalDirectory, "node_modules", "gkit", "package.json"), - "utf8", - ); - if (installedPackageJson.includes('"catalog:"')) { - throw new Error("The installed gkit artifact still contains workspace-only catalog ranges."); - } + const installedPackageJson = await readFile( + join(globalDirectory, 'node_modules', 'gkit', 'package.json'), + 'utf8', + ) + if (installedPackageJson.includes('"catalog:"')) { + throw new Error( + 'The installed gkit artifact still contains workspace-only catalog ranges.', + ) + } - 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"], - consumerDirectory, - ); - const capability = JSON.parse(described.stdout) as { id?: unknown }; - if (capability.id !== "gsc.properties.list") { - throw new Error("The installed gkit artifact returned an unexpected capability."); - } - const docs = await runCommand([gkit, "docs", "--provider", "gsc"], consumerDirectory); - const capabilities = await readFile(join(docs.stdout.trim(), "capabilities.md"), "utf8"); - if (!capabilities.includes("gsc.properties.list")) { - throw new Error("The installed gkit artifact could not read its provider documentation."); - } + 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'], + consumerDirectory, + ) + const capability = JSON.parse(described.stdout) as { id?: unknown } + if (capability.id !== 'gsc.properties.list') { + throw new Error( + 'The installed gkit artifact returned an unexpected capability.', + ) + } + const docs = await runCommand( + [gkit, 'docs', '--provider', 'gsc'], + consumerDirectory, + ) + const capabilities = await readFile( + join(docs.stdout.trim(), 'capabilities.md'), + 'utf8', + ) + if (!capabilities.includes('gsc.properties.list')) { + throw new Error( + 'The installed gkit artifact could not read its provider documentation.', + ) + } - // The bare bundle is the team-machine install: it must run with nothing next to it. - const bundleDirectory = join(consumerDirectory, "standalone"); - await mkdir(bundleDirectory, { recursive: true }); - const bundlePath = join(bundleDirectory, "gkit"); - await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath); - await chmod(bundlePath, 0o755); - const standaloneEnvironment = { XDG_CACHE_HOME: join(consumerDirectory, "standalone-cache") }; - await runCommand([bundlePath, "--schema", "gsc"], bundleDirectory, standaloneEnvironment); - const standaloneDocs = await runCommand( - [bundlePath, "docs", "--provider", "gsc"], - bundleDirectory, - standaloneEnvironment, - ); - const standaloneCapabilities = await readFile( - join(standaloneDocs.stdout.trim(), "capabilities.md"), - "utf8", - ); - if (!standaloneCapabilities.includes("gsc.properties.list")) { - throw new Error("The standalone gkit bundle could not read its embedded documentation."); - } + // The bare bundle is the team-machine install: it must run with nothing next to it. + const bundleDirectory = join(consumerDirectory, 'standalone') + await mkdir(bundleDirectory, { recursive: true }) + const bundlePath = join(bundleDirectory, 'gkit') + await copyFile(join(packageRoot, 'dist', 'gkit.js'), bundlePath) + await chmod(bundlePath, 0o755) + const standaloneEnvironment = { + XDG_CACHE_HOME: join(consumerDirectory, 'standalone-cache'), + } + await runCommand( + [bundlePath, '--schema', 'gsc'], + bundleDirectory, + standaloneEnvironment, + ) + const standaloneDocs = await runCommand( + [bundlePath, 'docs', '--provider', 'gsc'], + bundleDirectory, + standaloneEnvironment, + ) + const standaloneCapabilities = await readFile( + join(standaloneDocs.stdout.trim(), 'capabilities.md'), + 'utf8', + ) + if (!standaloneCapabilities.includes('gsc.properties.list')) { + throw new Error( + 'The standalone gkit bundle could not read its embedded documentation.', + ) + } - if (outputDirectory) { - await writeReleaseAssets(tarballPath, packageJson.version, outputDirectory); - } - process.stdout.write(`Verified installable gkit artifact: ${packResult.filename}\n`); + if (outputDirectory) { + await writeReleaseAssets(tarballPath, packageJson.version, outputDirectory) + } + process.stdout.write( + `Verified installable gkit artifact: ${packResult.filename}\n`, + ) } finally { - await rm(temporaryRoot, { recursive: true, force: true }); + await rm(temporaryRoot, { recursive: true, force: true }) } -function parseOutputDirectory(arguments_: readonly string[]): string | undefined { - if (arguments_.length === 0) return undefined; - if (arguments_.length !== 2 || arguments_[0] !== "--output-dir" || !arguments_[1]) { - throw new Error("Usage: verify-package-artifact.ts [--output-dir ]"); - } - return resolve(arguments_[1]); +function parseOutputDirectory( + arguments_: readonly string[], +): string | undefined { + if (arguments_.length === 0) return undefined + if ( + arguments_.length !== 2 || + arguments_[0] !== '--output-dir' || + !arguments_[1] + ) { + throw new Error( + 'Usage: verify-package-artifact.ts [--output-dir ]', + ) + } + return resolve(arguments_[1]) } async function verifyPackageContents(paths: readonly string[]): Promise { - if (!paths.includes("dist/gkit.js") || !paths.includes("package.json")) { - throw new Error("The package is missing its binary or package manifest."); - } - if (!paths.some((path) => path.startsWith("generated/"))) { - throw new Error("The package is missing generated provider manifests."); - } - if (!paths.some((path) => path.startsWith("docs/"))) { - throw new Error("The package is missing provider documentation."); - } - for (const path of paths) { - const topLevelEntry = path.split("/", 1)[0]; - if (!topLevelEntry || !allowedTopLevelEntries.has(topLevelEntry)) { - throw new Error(`Unexpected package entry: ${path}`); - } - if (forbiddenPathPattern.test(path)) { - throw new Error(`Forbidden package entry: ${path}`); - } - const content = await readFile(join(packageRoot, path), "utf8"); - if (secretMaterialPatterns.some((pattern) => pattern.test(content))) { - throw new Error(`Potential secret material found in package entry: ${path}`); - } - } + if (!paths.includes('dist/gkit.js') || !paths.includes('package.json')) { + throw new Error('The package is missing its binary or package manifest.') + } + if (!paths.some((path) => path.startsWith('generated/'))) { + throw new Error('The package is missing generated provider manifests.') + } + if (!paths.some((path) => path.startsWith('docs/'))) { + throw new Error('The package is missing provider documentation.') + } + for (const path of paths) { + const topLevelEntry = path.split('/', 1)[0] + if (!topLevelEntry || !allowedTopLevelEntries.has(topLevelEntry)) { + throw new Error(`Unexpected package entry: ${path}`) + } + if (forbiddenPathPattern.test(path)) { + throw new Error(`Forbidden package entry: ${path}`) + } + const content = await readFile(join(packageRoot, path), 'utf8') + if (secretMaterialPatterns.some((pattern) => pattern.test(content))) { + throw new Error( + `Potential secret material found in package entry: ${path}`, + ) + } + } } async function writeReleaseAssets( - tarballPath: string, - version: string, - destination: string, + tarballPath: string, + version: string, + destination: string, ): Promise { - await mkdir(destination, { recursive: true }); - const exactName = `gkit-${version}.tgz`; - const stableName = "gkit.tgz"; - const exactPath = join(destination, exactName); - const stablePath = join(destination, stableName); - await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL); - await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL); - const bundleName = "gkit"; - const bundlePath = join(destination, bundleName); - await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath, constants.COPYFILE_EXCL); - await chmod(bundlePath, 0o755); - const digest = createHash("sha256") - .update(await readFile(tarballPath)) - .digest("hex"); - const bundleDigest = createHash("sha256") - .update(await readFile(bundlePath)) - .digest("hex"); - await writeFile( - join(destination, "SHA256SUMS"), - `${digest} ${exactName}\n${digest} ${stableName}\n${bundleDigest} ${bundleName}\n`, - { encoding: "utf8", flag: "wx" }, - ); + await mkdir(destination, { recursive: true }) + const exactName = `gkit-${version}.tgz` + const stableName = 'gkit.tgz' + const exactPath = join(destination, exactName) + const stablePath = join(destination, stableName) + await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL) + await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL) + const bundleName = 'gkit' + const bundlePath = join(destination, bundleName) + await copyFile( + join(packageRoot, 'dist', 'gkit.js'), + bundlePath, + constants.COPYFILE_EXCL, + ) + await chmod(bundlePath, 0o755) + const digest = createHash('sha256') + .update(await readFile(tarballPath)) + .digest('hex') + const bundleDigest = createHash('sha256') + .update(await readFile(bundlePath)) + .digest('hex') + await writeFile( + join(destination, 'SHA256SUMS'), + `${digest} ${exactName}\n${digest} ${stableName}\n${bundleDigest} ${bundleName}\n`, + { encoding: 'utf8', flag: 'wx' }, + ) } async function runCommand( - command: readonly string[], - cwd: string, - additionalEnvironment: Readonly> = {}, + command: readonly string[], + cwd: string, + additionalEnvironment: Readonly> = {}, ): Promise<{ stdout: string; stderr: string }> { - const child = Bun.spawn([...command], { - cwd, - env: { ...Bun.env, ...additionalEnvironment, CI: "1" }, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - if (exitCode !== 0) { - throw new Error( - `Command failed (${exitCode}): ${command.map((value) => basename(value)).join(" ")}\n${stdout}${stderr}`.trimEnd(), - ); - } - return { stdout, stderr }; + const child = Bun.spawn([...command], { + cwd, + env: { ...Bun.env, ...additionalEnvironment, CI: '1' }, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + if (exitCode !== 0) { + throw new Error( + `Command failed (${exitCode}): ${command.map((value) => basename(value)).join(' ')}\n${stdout}${stderr}`.trimEnd(), + ) + } + return { stdout, stderr } } diff --git a/packages/gkit/src/args.test.ts b/packages/gkit/src/args.test.ts index d44892d..d0c0dad 100644 --- a/packages/gkit/src/args.test.ts +++ b/packages/gkit/src/args.test.ts @@ -1,244 +1,292 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { parseArgs } from "./args"; +import { parseArgs } from './args' -describe("gkit argv parser", () => { - it("accepts the public spaced command and global profile", () => { - expect( - parseArgs([ - "--profile", - "app-a", - "dataforseo", - "api", - "call", - "--capability-id", - "dataforseo.backlinks.bulk_ranks.live", - "--input", - "@req.json", - "--allow-spend", - "--max-spend-usd", - "0.05", - "--out", - "r.json", - ]), - ).toEqual({ - kind: "dataforseo-call", - profileFlag: "app-a", - capabilityId: "dataforseo.backlinks.bulk_ranks.live", - input: "@req.json", - allowSpend: true, - maxSpendUsd: "0.05", - out: "r.json", - force: false, - dryRun: false, - }); - }); +describe('gkit argv parser', () => { + it('accepts the public spaced command and global profile', () => { + expect( + parseArgs([ + '--profile', + 'app-a', + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'dataforseo.backlinks.bulk_ranks.live', + '--input', + '@req.json', + '--allow-spend', + '--max-spend-usd', + '0.05', + '--out', + 'r.json', + ]), + ).toEqual({ + kind: 'dataforseo-call', + profileFlag: 'app-a', + capabilityId: 'dataforseo.backlinks.bulk_ranks.live', + input: '@req.json', + allowSpend: true, + maxSpendUsd: '0.05', + out: 'r.json', + force: false, + dryRun: false, + }) + }) - it("keeps discovery commands profile-free", () => { - expect(parseArgs(["--schema"])).toEqual({ kind: "schema", selector: null }); - expect(parseArgs(["operations", "--limit", "5"])).toEqual({ kind: "operations-list", limit: 5 }); - expect(parseArgs(["--version"])).toEqual({ kind: "version" }); - expect(parseArgs(["@skill"])).toEqual({ kind: "skill", path: null }); - expect(parseArgs(["@skill", "SKILL.md"])).toEqual({ kind: "skill", path: "SKILL.md" }); - expect(parseArgs(["describe", "--id", "capability"])).toEqual({ - kind: "describe", - id: "capability", - }); - expect(() => parseArgs(["--profile", "app-a", "--schema"])).toThrow( - "--schema does not load a profile", - ); - expect(() => parseArgs(["--profile", "app-a", "@skill"])).toThrow( - "@skill does not load a profile", - ); - expect(parseArgs(["operations"])).toEqual({ kind: "operations-list", limit: 20 }); - expect(parseArgs(["operations", "list", "--limit", "5"])).toEqual({ - kind: "operations-list", - limit: 5, - }); - expect(parseArgs(["operations", "show", "--id", "op-1"])).toEqual({ - kind: "operations-show", - operationId: "op-1", - }); - }); + it('keeps discovery commands profile-free', () => { + expect(parseArgs(['--schema'])).toEqual({ kind: 'schema', selector: null }) + expect(parseArgs(['operations', '--limit', '5'])).toEqual({ + kind: 'operations-list', + limit: 5, + }) + expect(parseArgs(['--version'])).toEqual({ kind: 'version' }) + expect(parseArgs(['@skill'])).toEqual({ kind: 'skill', path: null }) + expect(parseArgs(['@skill', 'SKILL.md'])).toEqual({ + kind: 'skill', + path: 'SKILL.md', + }) + expect(parseArgs(['describe', '--id', 'capability'])).toEqual({ + kind: 'describe', + id: 'capability', + }) + expect(() => parseArgs(['--profile', 'app-a', '--schema'])).toThrow( + '--schema does not load a profile', + ) + expect(() => parseArgs(['--profile', 'app-a', '@skill'])).toThrow( + '@skill does not load a profile', + ) + expect(parseArgs(['operations'])).toEqual({ + kind: 'operations-list', + limit: 20, + }) + expect(parseArgs(['operations', 'list', '--limit', '5'])).toEqual({ + kind: 'operations-list', + limit: 5, + }) + expect(parseArgs(['operations', 'show', '--id', 'op-1'])).toEqual({ + kind: 'operations-show', + operationId: 'op-1', + }) + }) - it("accepts the read-only PostHog command without spend flags", () => { - expect( - parseArgs([ - "--profile", - "app-a", - "posthog", - "api", - "call", - "--capability-id", - "posthog.query.run", - "--input", - "@req.json", - "--out", - "result.json", - "--dry-run", - ]), - ).toEqual({ - kind: "posthog-call", - profileFlag: "app-a", - capabilityId: "posthog.query.run", - input: "@req.json", - out: "result.json", - force: false, - dryRun: true, - }); - expect(() => - parseArgs([ - "posthog", - "api", - "call", - "--capability-id", - "posthog.query.run", - "--input", - "@req.json", - "--allow-spend", - ]), - ).toThrow("Unknown flag"); - }); + it('accepts the read-only PostHog command without spend flags', () => { + expect( + parseArgs([ + '--profile', + 'app-a', + 'posthog', + 'api', + 'call', + '--capability-id', + 'posthog.query.run', + '--input', + '@req.json', + '--out', + 'result.json', + '--dry-run', + ]), + ).toEqual({ + kind: 'posthog-call', + profileFlag: 'app-a', + capabilityId: 'posthog.query.run', + input: '@req.json', + out: 'result.json', + force: false, + dryRun: true, + }) + expect(() => + parseArgs([ + 'posthog', + 'api', + 'call', + '--capability-id', + 'posthog.query.run', + '--input', + '@req.json', + '--allow-spend', + ]), + ).toThrow('Unknown flag') + }) - it("accepts the reviewed Google Ads call without spend or manager flags", () => { - expect( - parseArgs([ - "--profile", - "openclaw-web", - "google-ads", - "api", - "call", - "--capability-id", - "google-ads.query.gaql", - "--input", - "@gaql.json", - "--out", - "pages.json", - "--dry-run", - ]), - ).toEqual({ - kind: "google-ads-call", - profileFlag: "openclaw-web", - capabilityId: "google-ads.query.gaql", - input: "@gaql.json", - out: "pages.json", - force: false, - dryRun: true, - }); - expect(() => - parseArgs([ - "google-ads", - "api", - "call", - "--capability-id", - "google-ads.query.gaql", - "--input", - "{}", - "--login-customer-id", - "0987654321", - ]), - ).toThrow("Unknown flag"); - }); + it('accepts the reviewed Google Ads call without spend or manager flags', () => { + expect( + parseArgs([ + '--profile', + 'openclaw-web', + 'google-ads', + 'api', + 'call', + '--capability-id', + 'google-ads.query.gaql', + '--input', + '@gaql.json', + '--out', + 'pages.json', + '--dry-run', + ]), + ).toEqual({ + kind: 'google-ads-call', + profileFlag: 'openclaw-web', + capabilityId: 'google-ads.query.gaql', + input: '@gaql.json', + out: 'pages.json', + force: false, + dryRun: true, + }) + expect(() => + parseArgs([ + 'google-ads', + 'api', + 'call', + '--capability-id', + 'google-ads.query.gaql', + '--input', + '{}', + '--login-customer-id', + '0987654321', + ]), + ).toThrow('Unknown flag') + }) - it.each([ - ["bing", "bing.sites.list", "bing-call"], - ["gsc", "gsc.properties.list", "gsc-call"], - ["hubspot", "hubspot.crm.objects.list", "hubspot-call"], - ["pagespeed", "pagespeed.run", "pagespeed-call"], - ] as const)( - "accepts the read-only %s call without spend flags", - (provider, capabilityId, kind) => { - expect( - parseArgs([ - "--profile", - "app-a", - provider, - "api", - "call", - "--capability-id", - capabilityId, - "--input", - "{}", - "--out", - "result.json", - "--dry-run", - ]), - ).toEqual({ - kind, - profileFlag: "app-a", - capabilityId, - input: "{}", - ...(provider === "hubspot" || provider === "pagespeed" ? {} : { allowWrite: false }), - out: "result.json", - force: false, - dryRun: true, - }); - }, - ); + it.each([ + ['bing', 'bing.sites.list', 'bing-call'], + ['gsc', 'gsc.properties.list', 'gsc-call'], + ['hubspot', 'hubspot.crm.objects.list', 'hubspot-call'], + ['pagespeed', 'pagespeed.run', 'pagespeed-call'], + ] as const)( + 'accepts the read-only %s call without spend flags', + (provider, capabilityId, kind) => { + expect( + parseArgs([ + '--profile', + 'app-a', + provider, + 'api', + 'call', + '--capability-id', + capabilityId, + '--input', + '{}', + '--out', + 'result.json', + '--dry-run', + ]), + ).toEqual({ + kind, + profileFlag: 'app-a', + capabilityId, + input: '{}', + ...(provider === 'hubspot' || provider === 'pagespeed' + ? {} + : { allowWrite: false }), + out: 'result.json', + force: false, + dryRun: true, + }) + }, + ) - it("parses --allow-write for GSC and Bing calls so the effect gate receives it", () => { - expect( - parseArgs(["bing", "api", "call", "--capability-id", "bing.urls.submit", "--input", "{}", "--allow-write"]), - ).toMatchObject({ kind: "bing-call", allowWrite: true }); - expect( - parseArgs([ - "--profile", - "app-a", - "gsc", - "api", - "call", - "--capability-id", - "gsc.sitemaps.submit", - "--input", - "{}", - "--allow-write", - "--out", - "receipt.json", - ]), - ).toMatchObject({ kind: "gsc-call", capabilityId: "gsc.sitemaps.submit", allowWrite: true }); - expect(() => - parseArgs(["gsc", "api", "call", "--capability-id", "x", "--input", "{}", "--allow-write=yes"]), - ).toThrow("does not accept a value"); - }); + it('parses --allow-write for GSC and Bing calls so the effect gate receives it', () => { + expect( + parseArgs([ + 'bing', + 'api', + 'call', + '--capability-id', + 'bing.urls.submit', + '--input', + '{}', + '--allow-write', + ]), + ).toMatchObject({ kind: 'bing-call', allowWrite: true }) + expect( + parseArgs([ + '--profile', + 'app-a', + 'gsc', + 'api', + 'call', + '--capability-id', + 'gsc.sitemaps.submit', + '--input', + '{}', + '--allow-write', + '--out', + 'receipt.json', + ]), + ).toMatchObject({ + kind: 'gsc-call', + capabilityId: 'gsc.sitemaps.submit', + allowWrite: true, + }) + expect(() => + parseArgs([ + 'gsc', + 'api', + 'call', + '--capability-id', + 'x', + '--input', + '{}', + '--allow-write=yes', + ]), + ).toThrow('does not accept a value') + }) - it.each(["hubspot", "dataforseo"] as const)( - "rejects --allow-write for %s, which has no write capabilities", - (provider) => { - expect(() => - parseArgs([provider, "api", "call", "--capability-id", "x", "--input", "{}", "--allow-write"]), - ).toThrow("Unknown flag: --allow-write"); - }, - ); + it.each(['hubspot', 'dataforseo'] as const)( + 'rejects --allow-write for %s, which has no write capabilities', + (provider) => { + expect(() => + parseArgs([ + provider, + 'api', + 'call', + '--capability-id', + 'x', + '--input', + '{}', + '--allow-write', + ]), + ).toThrow('Unknown flag: --allow-write') + }, + ) - it("rejects unknown or duplicate flags before execution", () => { - expect(() => - parseArgs([ - "dataforseo", - "api", - "call", - "--capability-id", - "id", - "--capability-id", - "id-2", - "--input", - "@req.json", - ]), - ).toThrow("Duplicate flag"); - expect(() => - parseArgs([ - "dataforseo", - "api", - "call", - "--capability-id", - "id", - "--input", - "@req.json", - "--unknown", - "value", - ]), - ).toThrow("Unknown flag"); - expect(() => - parseArgs(["--profile", "app-a", "--profile=app-b", "dataforseo", "doctor"]), - ).toThrow("--profile may be provided only once"); - }); -}); + it('rejects unknown or duplicate flags before execution', () => { + expect(() => + parseArgs([ + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'id', + '--capability-id', + 'id-2', + '--input', + '@req.json', + ]), + ).toThrow('Duplicate flag') + expect(() => + parseArgs([ + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'id', + '--input', + '@req.json', + '--unknown', + 'value', + ]), + ).toThrow('Unknown flag') + expect(() => + parseArgs([ + '--profile', + 'app-a', + '--profile=app-b', + 'dataforseo', + 'doctor', + ]), + ).toThrow('--profile may be provided only once') + }) +}) diff --git a/packages/gkit/src/args.ts b/packages/gkit/src/args.ts index 76df5ac..47debf6 100644 --- a/packages/gkit/src/args.ts +++ b/packages/gkit/src/args.ts @@ -1,442 +1,475 @@ -import { parseSiteArgs, type SiteCommand } from "./site/commands"; -import { GkitFailure } from "./envelope"; +import { GkitFailure } from './envelope' +import { parseSiteArgs, type SiteCommand } from './site/commands' export type ParsedCommand = - | SiteCommand - | { kind: "help" } - | { kind: "version" } - | { kind: "schema"; selector: string | null } - | { kind: "skill"; path: string | null } - | { kind: "describe"; id: string } - | { kind: "docs"; provider: string | null } - | { kind: "operations-list"; limit: number } - | { kind: "operations-show"; operationId: string } - | { - kind: "operations-reconcile"; - operationId: string; - providerResult: "succeeded" | "failed" | "not_dispatched" | null; - spendOutcome: "confirmed_charged" | "confirmed_not_charged" | null; - costUsd: string | null; - evidenceRef: string; - providerRequestId: string | null; - } - | { kind: "dataforseo-doctor"; profileFlag: string | null } - | { kind: "bing-doctor"; profileFlag: string | null } - | { kind: "google-ads-doctor"; profileFlag: string | null } - | { kind: "gsc-doctor"; profileFlag: string | null } - | { kind: "posthog-doctor"; profileFlag: string | null } - | { kind: "hubspot-doctor"; profileFlag: string | null } - | { kind: "pagespeed-doctor"; profileFlag: string | null } - | { - kind: "dataforseo-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowSpend: boolean; - maxSpendUsd: string | null; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "bing-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowWrite: boolean; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "google-ads-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "gsc-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowWrite: boolean; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "posthog-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "pagespeed-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "hubspot-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - }; + | SiteCommand + | { kind: 'help' } + | { kind: 'version' } + | { kind: 'schema'; selector: string | null } + | { kind: 'skill'; path: string | null } + | { kind: 'describe'; id: string } + | { kind: 'docs'; provider: string | null } + | { kind: 'operations-list'; limit: number } + | { kind: 'operations-show'; operationId: string } + | { + kind: 'operations-reconcile' + operationId: string + providerResult: 'succeeded' | 'failed' | 'not_dispatched' | null + spendOutcome: 'confirmed_charged' | 'confirmed_not_charged' | null + costUsd: string | null + evidenceRef: string + providerRequestId: string | null + } + | { kind: 'dataforseo-doctor'; profileFlag: string | null } + | { kind: 'bing-doctor'; profileFlag: string | null } + | { kind: 'google-ads-doctor'; profileFlag: string | null } + | { kind: 'gsc-doctor'; profileFlag: string | null } + | { kind: 'posthog-doctor'; profileFlag: string | null } + | { kind: 'hubspot-doctor'; profileFlag: string | null } + | { kind: 'pagespeed-doctor'; profileFlag: string | null } + | { + kind: 'dataforseo-call' + profileFlag: string | null + capabilityId: string + input: string + allowSpend: boolean + maxSpendUsd: string | null + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'bing-call' + profileFlag: string | null + capabilityId: string + input: string + allowWrite: boolean + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'google-ads-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'gsc-call' + profileFlag: string | null + capabilityId: string + input: string + allowWrite: boolean + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'posthog-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'pagespeed-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'hubspot-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } -type FlagValue = string | true; +type FlagValue = string | true function invalid(message: string): never { - throw new GkitFailure({ - code: "INVALID_INPUT", - message, - hint: "Run gkit --help or gkit --schema to inspect the accepted command surface.", - }); + throw new GkitFailure({ + code: 'INVALID_INPUT', + message, + hint: 'Run gkit --help or gkit --schema to inspect the accepted command surface.', + }) } function takeGlobalProfile(argv: string[]): { - profileFlag: string | null; - rest: string[]; + profileFlag: string | null + rest: string[] } { - let profileFlag: string | null = null; - const rest: string[] = []; - for (let index = 0; index < argv.length; index++) { - const token = argv[index]!; - if (token === "--profile") { - if (profileFlag !== null) invalid("--profile may be provided only once."); - const value = argv[index + 1]; - if (!value || value.startsWith("--")) invalid("--profile requires a value."); - profileFlag = value; - index++; - continue; - } - if (token.startsWith("--profile=")) { - if (profileFlag !== null) invalid("--profile may be provided only once."); - const value = token.slice("--profile=".length); - if (!value) invalid("--profile requires a value."); - profileFlag = value; - continue; - } - rest.push(token); - } - return { profileFlag, rest }; + let profileFlag: string | null = null + const rest: string[] = [] + for (let index = 0; index < argv.length; index++) { + const token = argv[index]! + if (token === '--profile') { + if (profileFlag !== null) invalid('--profile may be provided only once.') + const value = argv[index + 1] + if (!value || value.startsWith('--')) + invalid('--profile requires a value.') + profileFlag = value + index++ + continue + } + if (token.startsWith('--profile=')) { + if (profileFlag !== null) invalid('--profile may be provided only once.') + const value = token.slice('--profile='.length) + if (!value) invalid('--profile requires a value.') + profileFlag = value + continue + } + rest.push(token) + } + return { profileFlag, rest } } -function parseFlags(argv: string[], booleanNames: ReadonlySet): Map { - const flags = new Map(); - for (let index = 0; index < argv.length; index++) { - const token = argv[index]!; - if (!token.startsWith("--")) invalid(`Unexpected argument: ${token}`); - const equal = token.indexOf("="); - const name = equal === -1 ? token : token.slice(0, equal); - if (flags.has(name)) invalid(`Duplicate flag: ${name}`); +function parseFlags( + argv: string[], + booleanNames: ReadonlySet, +): Map { + const flags = new Map() + for (let index = 0; index < argv.length; index++) { + const token = argv[index]! + if (!token.startsWith('--')) invalid(`Unexpected argument: ${token}`) + const equal = token.indexOf('=') + const name = equal === -1 ? token : token.slice(0, equal) + if (flags.has(name)) invalid(`Duplicate flag: ${name}`) - if (booleanNames.has(name)) { - if (equal !== -1) invalid(`${name} does not accept a value.`); - flags.set(name, true); - continue; - } + if (booleanNames.has(name)) { + if (equal !== -1) invalid(`${name} does not accept a value.`) + flags.set(name, true) + continue + } - const value = equal === -1 ? argv[index + 1] : token.slice(equal + 1); - if (!value || (equal === -1 && value.startsWith("--"))) { - invalid(`${name} requires a value.`); - } - flags.set(name, value); - if (equal === -1) index++; - } - return flags; + const value = equal === -1 ? argv[index + 1] : token.slice(equal + 1) + if (!value || (equal === -1 && value.startsWith('--'))) { + invalid(`${name} requires a value.`) + } + flags.set(name, value) + if (equal === -1) index++ + } + return flags } function requireOnly(flags: Map, allowed: string[]): void { - const allowedSet = new Set(allowed); - for (const name of flags.keys()) { - if (!allowedSet.has(name)) invalid(`Unknown flag: ${name}`); - } + const allowedSet = new Set(allowed) + for (const name of flags.keys()) { + if (!allowedSet.has(name)) invalid(`Unknown flag: ${name}`) + } } function requiredString(flags: Map, name: string): string { - const value = flags.get(name); - if (typeof value !== "string") invalid(`${name} is required.`); - return value; + const value = flags.get(name) + if (typeof value !== 'string') invalid(`${name} is required.`) + return value } -function optionalString(flags: Map, name: string): string | null { - const value = flags.get(name); - return typeof value === "string" ? value : null; +function optionalString( + flags: Map, + name: string, +): string | null { + const value = flags.get(name) + return typeof value === 'string' ? value : null } export function parseArgs(argv: string[]): ParsedCommand { - const { profileFlag, rest } = takeGlobalProfile(argv); - if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") { - if (rest.length > 1) invalid("--help does not accept additional arguments."); - return { kind: "help" }; - } + const { profileFlag, rest } = takeGlobalProfile(argv) + if (rest.length === 0 || rest[0] === '--help' || rest[0] === '-h') { + if (rest.length > 1) invalid('--help does not accept additional arguments.') + return { kind: 'help' } + } - if (rest[0] === "--version") { - if (profileFlag || rest.length > 1) invalid("--version takes no other arguments."); - return { kind: "version" }; - } + if (rest[0] === '--version') { + if (profileFlag || rest.length > 1) + invalid('--version takes no other arguments.') + return { kind: 'version' } + } - if (rest[0] === "--schema" || rest[0]!.startsWith("--schema=")) { - if (profileFlag) invalid("--schema does not load a profile."); - const first = rest[0]!; - const inline = first.startsWith("--schema=") ? first.slice("--schema=".length) : null; - const selector = inline || rest[1] || null; - if (rest.length > (selector && !inline ? 2 : 1)) { - invalid("--schema accepts at most one selector."); - } - return { kind: "schema", selector }; - } + if (rest[0] === '--schema' || rest[0]!.startsWith('--schema=')) { + if (profileFlag) invalid('--schema does not load a profile.') + const first = rest[0]! + const inline = first.startsWith('--schema=') + ? first.slice('--schema='.length) + : null + const selector = inline || rest[1] || null + if (rest.length > (selector && !inline ? 2 : 1)) { + invalid('--schema accepts at most one selector.') + } + return { kind: 'schema', selector } + } - // First-token builtin matching argc's @skill contract; gkit keeps its own - // dispatcher, so the skill must be served here rather than via cli(). - if (rest[0] === "@skill") { - if (profileFlag) invalid("@skill does not load a profile."); - if (rest.length > 2) invalid("@skill takes at most one path."); - return { kind: "skill", path: rest[1] ?? null }; - } + // First-token builtin matching argc's @skill contract; gkit keeps its own + // dispatcher, so the skill must be served here rather than via cli(). + if (rest[0] === '@skill') { + if (profileFlag) invalid('@skill does not load a profile.') + if (rest.length > 2) invalid('@skill takes at most one path.') + return { kind: 'skill', path: rest[1] ?? null } + } - if (rest[0] === "describe") { - if (profileFlag) invalid("describe does not load a profile."); - const flags = parseFlags(rest.slice(1), new Set()); - requireOnly(flags, ["--id"]); - return { kind: "describe", id: requiredString(flags, "--id") }; - } + if (rest[0] === 'describe') { + if (profileFlag) invalid('describe does not load a profile.') + const flags = parseFlags(rest.slice(1), new Set()) + requireOnly(flags, ['--id']) + return { kind: 'describe', id: requiredString(flags, '--id') } + } - if (rest[0] === "docs") { - if (profileFlag) invalid("docs does not load a profile."); - const flags = parseFlags(rest.slice(1), new Set()); - requireOnly(flags, ["--provider"]); - return { kind: "docs", provider: optionalString(flags, "--provider") }; - } + if (rest[0] === 'docs') { + if (profileFlag) invalid('docs does not load a profile.') + const flags = parseFlags(rest.slice(1), new Set()) + requireOnly(flags, ['--provider']) + return { kind: 'docs', provider: optionalString(flags, '--provider') } + } - if (rest[0] === "site") return parseSiteArgs(rest.slice(1), profileFlag); + if (rest[0] === 'site') return parseSiteArgs(rest.slice(1), profileFlag) - if (rest[0] === "operations") { - if (profileFlag) invalid("operations commands do not load a profile."); - if (rest.length === 1 || rest[1] === "list" || rest[1]!.startsWith("--")) { - const flags = parseFlags(rest.slice(rest[1] === "list" ? 2 : 1), new Set()); - requireOnly(flags, ["--limit"]); - const limitText = optionalString(flags, "--limit") ?? "20"; - if (!/^[1-9]\d{0,3}$/.test(limitText)) invalid("--limit must be an integer from 1 to 9999."); - return { kind: "operations-list", limit: Number(limitText) }; - } - if (rest[1] === "show") { - const flags = parseFlags(rest.slice(2), new Set()); - requireOnly(flags, ["--id"]); - return { kind: "operations-show", operationId: requiredString(flags, "--id") }; - } - if (rest[1] !== "reconcile") invalid(`Unknown operations command: ${rest[1]}`); - const flags = parseFlags(rest.slice(2), new Set()); - requireOnly(flags, [ - "--id", - "--result", - "--spend-outcome", - "--evidence-ref", - "--cost-usd", - "--provider-request-id", - ]); - const providerResult = optionalString(flags, "--result"); - if ( - providerResult !== null && - providerResult !== "succeeded" && - providerResult !== "failed" && - providerResult !== "not_dispatched" - ) { - invalid("--result must be succeeded, failed, or not_dispatched."); - } - const spendOutcome = optionalString(flags, "--spend-outcome"); - if ( - spendOutcome !== null && - spendOutcome !== "confirmed_charged" && - spendOutcome !== "confirmed_not_charged" - ) { - invalid("--spend-outcome must be confirmed_charged or confirmed_not_charged."); - } - if (providerResult === null && spendOutcome === null) { - invalid("operations reconcile requires --result, --spend-outcome, or both."); - } - return { - kind: "operations-reconcile", - operationId: requiredString(flags, "--id"), - providerResult, - spendOutcome, - costUsd: optionalString(flags, "--cost-usd"), - evidenceRef: requiredString(flags, "--evidence-ref"), - providerRequestId: optionalString(flags, "--provider-request-id"), - }; - } + if (rest[0] === 'operations') { + if (profileFlag) invalid('operations commands do not load a profile.') + if (rest.length === 1 || rest[1] === 'list' || rest[1]!.startsWith('--')) { + const flags = parseFlags( + rest.slice(rest[1] === 'list' ? 2 : 1), + new Set(), + ) + requireOnly(flags, ['--limit']) + const limitText = optionalString(flags, '--limit') ?? '20' + if (!/^[1-9]\d{0,3}$/.test(limitText)) + invalid('--limit must be an integer from 1 to 9999.') + return { kind: 'operations-list', limit: Number(limitText) } + } + if (rest[1] === 'show') { + const flags = parseFlags(rest.slice(2), new Set()) + requireOnly(flags, ['--id']) + return { + kind: 'operations-show', + operationId: requiredString(flags, '--id'), + } + } + if (rest[1] !== 'reconcile') + invalid(`Unknown operations command: ${rest[1]}`) + const flags = parseFlags(rest.slice(2), new Set()) + requireOnly(flags, [ + '--id', + '--result', + '--spend-outcome', + '--evidence-ref', + '--cost-usd', + '--provider-request-id', + ]) + const providerResult = optionalString(flags, '--result') + if ( + providerResult !== null && + providerResult !== 'succeeded' && + providerResult !== 'failed' && + providerResult !== 'not_dispatched' + ) { + invalid('--result must be succeeded, failed, or not_dispatched.') + } + const spendOutcome = optionalString(flags, '--spend-outcome') + if ( + spendOutcome !== null && + spendOutcome !== 'confirmed_charged' && + spendOutcome !== 'confirmed_not_charged' + ) { + invalid( + '--spend-outcome must be confirmed_charged or confirmed_not_charged.', + ) + } + if (providerResult === null && spendOutcome === null) { + invalid( + 'operations reconcile requires --result, --spend-outcome, or both.', + ) + } + return { + kind: 'operations-reconcile', + operationId: requiredString(flags, '--id'), + providerResult, + spendOutcome, + costUsd: optionalString(flags, '--cost-usd'), + evidenceRef: requiredString(flags, '--evidence-ref'), + providerRequestId: optionalString(flags, '--provider-request-id'), + } + } - if ( - rest[0] !== "bing" && - rest[0] !== "dataforseo" && - rest[0] !== "google-ads" && - rest[0] !== "gsc" && - rest[0] !== "hubspot" && - rest[0] !== "pagespeed" && - rest[0] !== "posthog" - ) { - invalid(`Unknown command: ${rest[0]}`); - } - const provider = rest[0]; - if (rest[1] === "doctor" && rest.length === 2) { - return { - kind: - provider === "dataforseo" - ? "dataforseo-doctor" - : provider === "bing" - ? "bing-doctor" - : provider === "google-ads" - ? "google-ads-doctor" - : provider === "gsc" - ? "gsc-doctor" - : provider === "hubspot" - ? "hubspot-doctor" - : provider === "pagespeed" - ? "pagespeed-doctor" - : "posthog-doctor", - profileFlag, - }; - } - if (rest[1] !== "api" || rest[2] !== "call") { - invalid(`Expected ${provider} doctor or ${provider} api call.`); - } + if ( + rest[0] !== 'bing' && + rest[0] !== 'dataforseo' && + rest[0] !== 'google-ads' && + rest[0] !== 'gsc' && + rest[0] !== 'hubspot' && + rest[0] !== 'pagespeed' && + rest[0] !== 'posthog' + ) { + invalid(`Unknown command: ${rest[0]}`) + } + const provider = rest[0] + if (rest[1] === 'doctor' && rest.length === 2) { + return { + kind: + provider === 'dataforseo' + ? 'dataforseo-doctor' + : provider === 'bing' + ? 'bing-doctor' + : provider === 'google-ads' + ? 'google-ads-doctor' + : provider === 'gsc' + ? 'gsc-doctor' + : provider === 'hubspot' + ? 'hubspot-doctor' + : provider === 'pagespeed' + ? 'pagespeed-doctor' + : 'posthog-doctor', + profileFlag, + } + } + if (rest[1] !== 'api' || rest[2] !== 'call') { + invalid(`Expected ${provider} doctor or ${provider} api call.`) + } - const booleanNames = new Set(["--allow-spend", "--allow-write", "--force", "--dry-run"]); - const flags = parseFlags(rest.slice(3), booleanNames); - if (provider === "gsc" || provider === "bing") { - requireOnly(flags, [ - "--capability-id", - "--input", - "--allow-write", - "--out", - "--force", - "--dry-run", - ]); - return { - kind: provider === "gsc" ? "gsc-call" : "bing-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - allowWrite: flags.get("--allow-write") === true, - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; - } - if (provider !== "dataforseo") { - requireOnly(flags, ["--capability-id", "--input", "--out", "--force", "--dry-run"]); - return { - kind: - provider === "posthog" - ? "posthog-call" - : provider === "hubspot" - ? "hubspot-call" - : provider === "pagespeed" - ? "pagespeed-call" - : "google-ads-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; - } - requireOnly(flags, [ - "--capability-id", - "--input", - "--allow-spend", - "--max-spend-usd", - "--out", - "--force", - "--dry-run", - ]); - return { - kind: "dataforseo-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - allowSpend: flags.get("--allow-spend") === true, - maxSpendUsd: optionalString(flags, "--max-spend-usd"), - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; + const booleanNames = new Set([ + '--allow-spend', + '--allow-write', + '--force', + '--dry-run', + ]) + const flags = parseFlags(rest.slice(3), booleanNames) + if (provider === 'gsc' || provider === 'bing') { + requireOnly(flags, [ + '--capability-id', + '--input', + '--allow-write', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: provider === 'gsc' ? 'gsc-call' : 'bing-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + allowWrite: flags.get('--allow-write') === true, + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } + } + if (provider !== 'dataforseo') { + requireOnly(flags, [ + '--capability-id', + '--input', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: + provider === 'posthog' + ? 'posthog-call' + : provider === 'hubspot' + ? 'hubspot-call' + : provider === 'pagespeed' + ? 'pagespeed-call' + : 'google-ads-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } + } + requireOnly(flags, [ + '--capability-id', + '--input', + '--allow-spend', + '--max-spend-usd', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: 'dataforseo-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + allowSpend: flags.get('--allow-spend') === true, + maxSpendUsd: optionalString(flags, '--max-spend-usd'), + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } } export function renderHelp(): string { - return [ - "gkit — profile-bound growth provider CLI", - "", - "Discovery:", - " gkit --schema [selector]", - " gkit @skill [path]", - " gkit --version", - " gkit describe --id ", - " gkit docs [--provider ]", - "", - "DataForSEO:", - " gkit --profile dataforseo doctor", - " gkit --profile dataforseo api call --capability-id --input @request.json --allow-spend --max-spend-usd --out --dry-run", - " gkit --profile dataforseo api call --capability-id --input @request.json --allow-spend --max-spend-usd --out ", - " Default artifact behavior is no-replace; add --force only after reviewing the destination.", - "", - "Bing Webmaster:", - " gkit --profile bing doctor", - " gkit --profile bing api call --capability-id --input @request.json --out --dry-run", - " gkit --profile bing api call --capability-id --input @request.json --out ", - " Write capabilities (bing.urls.submit, bing.sitemaps.submit) also require --allow-write.", - "", - "PostHog:", - " gkit --profile posthog doctor", - " gkit --profile posthog api call --capability-id posthog.query.run --input @request.json --out --dry-run", - " gkit --profile posthog api call --capability-id posthog.query.run --input @request.json --out ", - "", - "Google Ads:", - " gkit --profile google-ads doctor", - " gkit --profile google-ads api call --capability-id --input @request.json --out --dry-run", - " gkit --profile google-ads api call --capability-id --input @request.json --out ", - "", - "Google Search Console:", - " gkit --profile gsc doctor", - " gkit --profile gsc api call --capability-id --input @request.json --out --dry-run", - " gkit --profile gsc api call --capability-id --input @request.json --out ", - " Write capabilities (gsc.sitemaps.submit) also require --allow-write, including --dry-run.", - "", - "HubSpot:", - " gkit --profile hubspot doctor", - " gkit --profile hubspot api call --capability-id --input @request.json --out --dry-run", - " gkit --profile hubspot api call --capability-id --input @request.json --out ", - "", - "PageSpeed Insights:", - " gkit --profile pagespeed doctor", - " gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out --dry-run", - "", - "Site facts:", - " gkit --schema site", - " gkit --profile site doctor", - " gkit --profile site discover --out ", - " gkit --profile site snapshot --build .output --revision --context