scripts/check-github-admin-ready.mjs
scripts/check-github-admin-ready.mjsBrowse 7 files
1,283 tokens
4,716 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1#!/usr/bin/env node2 3import { spawnSync } from 'node:child_process';4import { realpathSync } from 'node:fs';5import { resolve } from 'node:path';6import { fileURLToPath } from 'node:url';7 8const EXIT_SUCCESS = 0;9const EXIT_OPERATIONAL = 1;10const EXIT_CLI = 2;11const SUBPROCESS_TIMEOUT_MS = 30_000;12 13function parseCliArgs(argv) {14 const args = argv.slice(2);15 const result = { prUrl: null, help: false };16 if (args.includes('--help')) {17 result.help = true;18 return result;19 }20 for (let index = 0; index < args.length; index += 1) {21 const arg = args[index];22 if (arg !== '--pr') {23 throw { code: EXIT_CLI, message: `error: unknown flag "${arg}"` };24 }25 index += 1;26 if (index >= args.length) {27 throw { code: EXIT_CLI, message: 'error: --pr requires a value' };28 }29 result.prUrl = args[index];30 }31 if (!result.prUrl) {32 throw { code: EXIT_CLI, message: 'error: --pr is required' };33 }34 return result;35}36 37function getHelpText() {38 return [39 'Usage:',40 ' check-github-admin-ready.mjs --pr <PR_URL>',41 '',42 'Purpose:',43 ' Verify gh authentication + repo scopes and PR API access before implement phase.',44 ].join('\n');45}46 47function parsePrUrl(url) {48 const match = String(url)49 .trim()50 .match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/)?(?:#.*)?$/i);51 if (!match) {52 return null;53 }54 return {55 owner: match[1],56 repo: match[2].replace(/\.git$/, ''),57 number: Number.parseInt(match[3], 10),58 };59}60 61function run(command, args) {62 const result = spawnSync(command, args, { encoding: 'utf8', timeout: SUBPROCESS_TIMEOUT_MS });63 if (result.error?.code === 'ETIMEDOUT') {64 throw new Error(`error: ${command} timed out after ${SUBPROCESS_TIMEOUT_MS / 1000} seconds`);65 }66 if (result.signal) {67 throw new Error(`error: ${command} was terminated by signal ${result.signal}`);68 }69 return result;70}71 72function hasRepoScope(output) {73 const scopeLine = output.split(/\r?\n/).find((line) => line.includes('Token scopes:'));74 if (!scopeLine) {75 return false;76 }77 return scopeLine78 .replace(/^.*Token scopes:\s*/u, '')79 .split(',')80 .map((scope) => scope.trim().replace(/^['"]|['"]$/g, ''))81 .includes('repo');82}83 84function assertCommandAvailable(command, installHint) {85 const probe = run(command, ['--version']);86 if (probe.error || probe.status !== 0) {87 throw new Error(88 `error: required dependency "${command}" is not available. Install ${installHint} and retry.`,89 );90 }91}92 93function assertGhAuthAndScopes() {94 const auth = run('gh', ['auth', 'status']);95 if (auth.error) {96 throw new Error(`error: failed to execute gh: ${auth.error.message}`);97 }98 if (auth.status !== 0) {99 throw new Error('error: gh is not authenticated; run `gh auth login`.');100 }101 const output = `${auth.stdout}\n${auth.stderr}`;102 if (!hasRepoScope(output)) {103 throw new Error('error: gh token is missing `repo` scope required for review thread admin.');104 }105}106 107function assertPrApiAccess(owner, repo, number) {108 const query =109 'query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){id url state}}}';110 const result = run('gh', [111 'api',112 'graphql',113 '-f',114 `query=${query}`,115 '-F',116 `owner=${owner}`,117 '-F',118 `repo=${repo}`,119 '-F',120 `number=${number}`,121 ]);122 if (result.status !== 0) {123 throw new Error(124 `error: cannot access PR via gh api graphql: ${result.stderr || result.stdout}`.trim(),125 );126 }127}128 129async function main() {130 const args = parseCliArgs(process.argv);131 if (args.help) {132 process.stdout.write(`${getHelpText()}\n`);133 process.exit(EXIT_SUCCESS);134 }135 136 const parsed = parsePrUrl(args.prUrl);137 if (!parsed) {138 throw {139 code: EXIT_CLI,140 message: 'error: invalid PR URL (expected https://github.com/OWNER/REPO/pull/123)',141 };142 }143 144 assertCommandAvailable('gh', 'GitHub CLI (`gh`)');145 146 assertGhAuthAndScopes();147 assertPrApiAccess(parsed.owner, parsed.repo, parsed.number);148 process.stdout.write('ok: github admin preflight passed\n');149}150 151const isMain = (() => {152 try {153 const invokedScriptPath = process.argv[1] ? realpathSync(resolve(process.argv[1])) : null;154 const currentModulePath = realpathSync(fileURLToPath(import.meta.url));155 return invokedScriptPath !== null && invokedScriptPath === currentModulePath;156 } catch {157 return false;158 }159})();160 161if (isMain) {162 main().catch((error) => {163 const code = typeof error?.code === 'number' ? error.code : EXIT_OPERATIONAL;164 const message = error?.message ? String(error.message) : String(error);165 process.stderr.write(`${message}\n`);166 process.exit(code);167 });168}169