#!/usr/bin/env node import { execFile as execFileCallback } from 'node:child_process'; import { readFile as fsReadFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; const execFileAsync = promisify(execFileCallback); const npmCheckUpdatesRejectArgs = ['--reject', 'fumadocs-core,fumadocs-ui']; function ktxRootDir() { return resolve(dirname(fileURLToPath(import.meta.url)), '..'); } function failureText(error) { const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : ''; const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : ''; const message = error instanceof Error ? error.message.trim() : String(error); return [stderr, stdout, message].filter((line) => line.length > 0).join('\n') || 'Command failed'; } function commandText(command, args) { return [command, ...args].join(' '); } function pythonDependencyUpdatePhases() { const manifests = ['pyproject.toml', 'python/ktx-sl/pyproject.toml', 'python/ktx-daemon/pyproject.toml']; return manifests.map((manifest) => ({ name: `Python dependency constraints: ${manifest}`, command: 'uvx', args: ['dependency-check-updates', '--manifest', manifest, '-u'], retry: commandText('uvx', ['dependency-check-updates', '--manifest', manifest, '-u']), })); } async function pnpmMinimumReleaseAgeCooldown(rootDir, readFile) { let workspaceConfig; try { workspaceConfig = await readFile(resolve(rootDir, 'pnpm-workspace.yaml'), 'utf8'); } catch (error) { if (error?.code === 'ENOENT') { return []; } throw error; } const match = workspaceConfig.match(/^\s*minimumReleaseAge:\s*(\d+)\s*$/m); if (!match) { return []; } return ['--cooldown', `${match[1]}m`]; } export async function runDependencyUpgrade(options = {}) { const rootDir = options.rootDir ?? ktxRootDir(); const execFile = options.execFile ?? execFileAsync; const readFile = options.readFile ?? fsReadFile; const log = options.log ?? ((line) => process.stdout.write(`${line}\n`)); const npmCheckUpdatesCooldownArgs = await pnpmMinimumReleaseAgeCooldown(rootDir, readFile); const phases = [ { name: 'TypeScript dependency constraints', command: 'pnpm', args: ['dlx', 'npm-check-updates', '-u', '--deep', ...npmCheckUpdatesRejectArgs, ...npmCheckUpdatesCooldownArgs], retry: commandText('pnpm', [ 'dlx', 'npm-check-updates', '-u', '--deep', ...npmCheckUpdatesRejectArgs, ...npmCheckUpdatesCooldownArgs, ]), }, ...pythonDependencyUpdatePhases(), { name: 'TypeScript lockfile', command: 'pnpm', args: ['install'], retry: 'pnpm install', }, { name: 'Python lockfile', command: 'uv', args: ['lock', '--upgrade'], retry: 'uv lock --upgrade', }, ]; for (const phase of phases) { log(`RUN ${phase.name}: ${commandText(phase.command, phase.args)}`); try { await execFile(phase.command, phase.args, { cwd: rootDir, maxBuffer: 1024 * 1024 * 64 }); log(`PASS ${phase.name}`); } catch (error) { log(`FAIL ${phase.name}: ${failureText(error)}`); log(`Retry: ${phase.retry}`); return { ok: false, failedPhase: phase }; } } log('Dependency manifests and lockfiles were updated. Run `pnpm run check` before committing.'); return { ok: true }; } if (import.meta.url === pathToFileURL(process.argv[1]).href) { const result = await runDependencyUpgrade(); if (!result.ok) { process.exitCode = 1; } }