/** * Subprocess helper - utilities for running protocol clients in tests */ export interface ICommandResult { success: boolean; stdout: string; stderr: string; code: number; signal?: Deno.Signal; } export interface ICommandOptions { cwd?: string; env?: Record; timeout?: number; stdin?: string; } /** * Execute a command and return the result */ export async function runCommand( cmd: string[], options: ICommandOptions = {} ): Promise { const { cwd, env, timeout = 60000, stdin } = options; const command = new Deno.Command(cmd[0], { args: cmd.slice(1), cwd, env: { ...Deno.env.toObject(), ...env }, stdin: stdin ? 'piped' : 'null', stdout: 'piped', stderr: 'piped', }); const child = command.spawn(); if (stdin && child.stdin) { const writer = child.stdin.getWriter(); await writer.write(new TextEncoder().encode(stdin)); await writer.close(); } const timeoutId = setTimeout(() => { try { child.kill('SIGTERM'); } catch { /* ignore */ } }, timeout); const output = await child.output(); clearTimeout(timeoutId); return { success: output.success, stdout: new TextDecoder().decode(output.stdout), stderr: new TextDecoder().decode(output.stderr), code: output.code, signal: output.signal ?? undefined, }; } /** * Check if a command is available */ export async function commandExists(cmd: string): Promise { try { const result = await runCommand(['which', cmd], { timeout: 5000 }); return result.success; } catch { return false; } } /** * Protocol client wrappers */ export const clients = { npm: { check: () => commandExists('npm'), publish: (dir: string, registry: string, token: string) => runCommand(['npm', 'publish', '--registry', registry], { cwd: dir, env: { NPM_TOKEN: token, npm_config__authToken: token }, }), install: (pkg: string, registry: string, dir: string) => runCommand(['npm', 'install', pkg, '--registry', registry], { cwd: dir }), unpublish: (pkg: string, registry: string, token: string) => runCommand(['npm', 'unpublish', pkg, '--registry', registry, '--force'], { env: { NPM_TOKEN: token, npm_config__authToken: token }, }), pack: (dir: string) => runCommand(['npm', 'pack'], { cwd: dir }), }, docker: { check: () => commandExists('docker'), build: (dockerfile: string, tag: string, context: string) => runCommand(['docker', 'build', '-f', dockerfile, '-t', tag, context]), push: (image: string) => runCommand(['docker', 'push', image]), pull: (image: string) => runCommand(['docker', 'pull', image]), rmi: (image: string, force = false) => runCommand(['docker', 'rmi', ...(force ? ['-f'] : []), image]), login: (registry: string, username: string, password: string) => runCommand(['docker', 'login', registry, '-u', username, '--password-stdin'], { stdin: password, }), tag: (source: string, target: string) => runCommand(['docker', 'tag', source, target]), }, cargo: { check: () => commandExists('cargo'), package: (dir: string) => runCommand(['cargo', 'package', '--allow-dirty'], { cwd: dir }), publish: (dir: string, registry: string, token: string) => runCommand( ['cargo', 'publish', '--registry', 'stack-test', '--token', token, '--allow-dirty'], { cwd: dir } ), yank: (crate: string, version: string, token: string) => runCommand([ 'cargo', 'yank', crate, '--version', version, '--registry', 'stack-test', '--token', token, ]), }, pip: { check: () => commandExists('pip'), build: (dir: string) => runCommand(['python', '-m', 'build', dir]), upload: (dist: string, repository: string, token: string) => runCommand([ 'python', '-m', 'twine', 'upload', '--repository-url', repository, '-u', '__token__', '-p', token, `${dist}/*`, ]), install: (pkg: string, indexUrl: string) => runCommand(['pip', 'install', pkg, '--index-url', indexUrl]), }, composer: { check: () => commandExists('composer'), install: (pkg: string, repository: string, dir: string) => runCommand( [ 'composer', 'require', pkg, '--repository', JSON.stringify({ type: 'composer', url: repository }), ], { cwd: dir } ), }, gem: { check: () => commandExists('gem'), build: (gemspec: string, dir: string) => runCommand(['gem', 'build', gemspec], { cwd: dir }), push: (gemFile: string, host: string, key: string) => runCommand(['gem', 'push', gemFile, '--host', host, '--key', key]), install: (gemName: string, source: string) => runCommand(['gem', 'install', gemName, '--source', source]), yank: (gemName: string, version: string, host: string, key: string) => runCommand(['gem', 'yank', gemName, '-v', version, '--host', host, '--key', key]), }, maven: { check: () => commandExists('mvn'), deploy: (dir: string, repositoryUrl: string, username: string, password: string) => runCommand( [ 'mvn', 'deploy', `-DaltDeploymentRepository=stack-test::default::${repositoryUrl}`, `-Dusername=${username}`, `-Dpassword=${password}`, ], { cwd: dir } ), package: (dir: string) => runCommand(['mvn', 'package', '-DskipTests'], { cwd: dir }), }, }; /** * Skip test if command is not available */ export async function skipIfMissing(cmd: string): Promise { const exists = await commandExists(cmd); if (!exists) { console.warn(`[Skip] ${cmd} not available`); } return !exists; }