From 0f4391e2abf16ddc2dbc5db2c4acab4a73073ba1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 8 Nov 2025 12:29:38 +0000 Subject: [PATCH] chore: harden runner setup --- package.json | 2 + pnpm-lock.yaml | 25 ++++++++++++ scripts/docs-list.ts | 4 +- scripts/git-policy.ts | 6 +++ scripts/runner.ts | 88 +++++++++++++++++++++++++++++++++++++------ tsconfig.json | 2 +- 6 files changed, 113 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index edbe0e7..2247ec7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "clean": "rimraf dist", "dev": "tsc -w -p tsconfig.build.json", "prepublishOnly": "pnpm check && pnpm test && pnpm build", + "docs:list": "pnpm exec tsx scripts/docs-list.ts", "mcporter:list": "pnpm exec tsx src/cli.ts list", "mcporter:call": "pnpm exec tsx src/cli.ts call" }, @@ -50,6 +51,7 @@ "@types/express": "^5.0.5", "@types/node": "^24.10.0", "@typescript/native-preview": "7.0.0-dev.20251104.1", + "bun-types": "^1.3.2", "express": "^5.1.0", "oxlint": "^1.25.0", "oxlint-tsgolint": "^0.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4993993..d5bc2b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@typescript/native-preview': specifier: 7.0.0-dev.20251104.1 version: 7.0.0-dev.20251104.1 + bun-types: + specifier: ^1.3.2 + version: 1.3.2(@types/react@19.2.2) express: specifier: ^5.1.0 version: 5.1.0 @@ -631,6 +634,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -752,6 +758,11 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + bun-types@1.3.2: + resolution: {integrity: sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg==} + peerDependencies: + '@types/react': ^19 + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -815,6 +826,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1819,6 +1833,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react@19.2.2': + dependencies: + csstype: 3.1.3 + '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 @@ -1948,6 +1966,11 @@ snapshots: transitivePeerDependencies: - supports-color + bun-types@1.3.2(@types/react@19.2.2): + dependencies: + '@types/node': 24.10.0 + '@types/react': 19.2.2 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -1999,6 +2022,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.1.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/scripts/docs-list.ts b/scripts/docs-list.ts index 3fc9680..1cdc198 100755 --- a/scripts/docs-list.ts +++ b/scripts/docs-list.ts @@ -15,7 +15,9 @@ function walkMarkdownFiles(dir: string, base: string = dir): string[] { const entries = readdirSync(dir, { withFileTypes: true }); const files: string[] = []; for (const entry of entries) { - if (entry.name.startsWith('.')) continue; + if (entry.name.startsWith('.')) { + continue; + } const fullPath = join(dir, entry.name); if (entry.isDirectory()) { if (EXCLUDED_DIRS.has(entry.name)) { diff --git a/scripts/git-policy.ts b/scripts/git-policy.ts index 0e3b359..f5a16e5 100755 --- a/scripts/git-policy.ts +++ b/scripts/git-policy.ts @@ -56,6 +56,9 @@ export function findGitSubcommand(commandArgs: string[]): GitCommandInfo | null while (index < commandArgs.length) { const token = commandArgs[index]; + if (token === undefined) { + break; + } if (token === '--') { const next = commandArgs[index + 1]; return next ? { name: next, index: index + 1 } : null; @@ -83,6 +86,9 @@ export function determineGitWorkdir(baseDir: string, gitArgs: string[], command: while (index < limit) { const token = gitArgs[index]; + if (token === undefined) { + break; + } if (token === '-C') { const next = gitArgs[index + 1]; if (next) { diff --git a/scripts/runner.ts b/scripts/runner.ts index c332cc3..3fce0dd 100755 --- a/scripts/runner.ts +++ b/scripts/runner.ts @@ -156,13 +156,23 @@ function shouldExtendTimeout(commandArgs: string[]): boolean { return false; } - const [first, ...rest] = tokens; + const first = tokens[0]; + if (!first) { + return false; + } + const rest = tokens.slice(1); if (first === 'pnpm') { if (rest.length === 0) { return false; } const subcommand = rest[0]; + if (!subcommand) { + return false; + } + if (!subcommand) { + return false; + } if (subcommand === 'run') { const script = rest[1]; if (!script) { @@ -218,13 +228,20 @@ function shouldUseLintTimeout(commandArgs: string[]): boolean { return false; } - const [first, ...rest] = tokens; + const first = tokens[0]; + if (!first) { + return false; + } + const rest = tokens.slice(1); if (first === 'pnpm') { if (rest.length === 0) { return false; } const subcommand = rest[0]; + if (!subcommand) { + return false; + } if (subcommand === 'run') { const script = rest[1]; return typeof script === 'string' && script.startsWith('lint'); @@ -257,7 +274,11 @@ function isSingleTestInvocation(commandArgs: string[]): boolean { } } - const [first, ...rest] = tokens; + const first = tokens[0]; + if (!first) { + return false; + } + const rest = tokens.slice(1); if (first === 'pnpm') { if (rest[0] === 'test:file') { return true; @@ -293,6 +314,9 @@ function tokenReferencesIntegrationTest(token: string): boolean { function referencesIntegrationSpec(tokens: string[]): boolean { for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; + if (!token) { + continue; + } if (token === '--run' || token === '--include') { const next = tokens[index + 1]; if (next && tokenReferencesIntegrationTest(next)) { @@ -316,13 +340,34 @@ function matchesScriptKeyword(script: string, keywords: readonly string[]): bool function stripWrappersAndAssignments(args: string[]): string[] { const tokens = [...args]; - while (tokens.length > 0 && isEnvAssignment(tokens[0])) { + while (tokens.length > 0) { + const candidate = tokens[0]; + if (!candidate) { + break; + } + if (!isEnvAssignment(candidate)) { + break; + } tokens.shift(); } - while (tokens.length > 0 && WRAPPER_COMMANDS.has(tokens[0])) { + while (tokens.length > 0) { + const wrapper = tokens[0]; + if (!wrapper) { + break; + } + if (!WRAPPER_COMMANDS.has(wrapper)) { + break; + } tokens.shift(); - while (tokens.length > 0 && isEnvAssignment(tokens[0])) { + while (tokens.length > 0) { + const assignment = tokens[0]; + if (!assignment) { + break; + } + if (!isEnvAssignment(assignment)) { + break; + } tokens.shift(); } } @@ -344,6 +389,9 @@ function isTestRunnerSuiteInvocation(tokens: string[], suite: string): boolean { const normalizedSuite = suite.toLowerCase(); for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; + if (!token) { + continue; + } const normalizedToken = token.replace(/^[./\\]+/, ''); if (normalizedToken === 'scripts/test-runner.ts' || normalizedToken.endsWith('/scripts/test-runner.ts')) { const suiteToken = tokens[index + 1]?.toLowerCase(); @@ -363,7 +411,11 @@ function shouldUseLongTimeout(commandArgs: string[]): boolean { return false; } - const [first, ...rest] = tokens; + const first = tokens[0]; + if (!first) { + return false; + } + const rest = tokens.slice(1); const matches = (token: string): boolean => matchesScriptKeyword(token, LONG_SCRIPT_KEYWORDS); if (first === 'pnpm') { @@ -371,6 +423,9 @@ function shouldUseLongTimeout(commandArgs: string[]): boolean { return false; } const subcommand = rest[0]; + if (!subcommand) { + return false; + } if (subcommand === 'run') { const script = rest[1]; if (script && matches(script)) { @@ -506,19 +561,22 @@ function buildExecutionParams(commandArgs: string[]): { command: string; args: s for (const token of commandArgs) { if (!commandStarted && isEnvAssignment(token)) { const [key, ...rest] = token.split('='); - env[key] = rest.join('='); + if (key) { + env[key] = rest.join('='); + } continue; } commandStarted = true; args.push(token); } - if (args.length === 0) { + if (args.length === 0 || !args[0]) { printUsage('Missing command to execute.'); process.exit(1); } - return { command: args[0], args: args.slice(1), env }; + const [command, ...restArgs] = args; + return { command, args: restArgs, env }; } // Forwards termination signals to the child and returns an unregister hook. @@ -818,7 +876,7 @@ async function buildFindDeletePlan(findArgs: string[], workspaceDir: string): Pr process.exit(exitCode); } - const matches = stdoutBuf.split('\0').filter((entry) => entry.length > 0); + const matches = stdoutBuf.split('\0').filter((entry: string) => entry.length > 0); if (matches.length === 0) { return { paths: [] }; } @@ -853,6 +911,9 @@ function parseRmArguments(argv: string[]): { targets: string[]; force: boolean; let index = 1; while (index < argv.length) { const token = argv[index]; + if (token === undefined) { + break; + } if (!treatAsTarget && token === '--') { treatAsTarget = true; index += 1; @@ -894,6 +955,9 @@ function parseGitRmArguments(argv: string[], command: GitCommandInfo): GitRmPlan let index = command.index + 1; while (index < argv.length) { const token = argv[index]; + if (token === undefined) { + break; + } if (!treatAsPath && token === '--') { treatAsPath = true; index += 1; @@ -1002,7 +1066,7 @@ async function movePathsToTrash( stdout: 'ignore', stderr: 'pipe', }); - const [exitCode, stderrText] = await Promise.all([proc.exited, proc.stderr?.text() ?? Promise.resolve('')]); + const [exitCode, stderrText] = await Promise.all([proc.exited, readProcessStream(proc.stderr)]); if (exitCode === 0) { return { missing, errors: [] }; } diff --git a/tsconfig.json b/tsconfig.json index 122fd07..d3a98c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "noEmit": true, - "types": ["node"] + "types": ["node", "bun-types"] }, "include": ["src", "tests", "scripts", "docs"] }