import { exec, ExecException } from 'child_process';
import { getConfig, getAzureScopeArgs } from './config.js';
import { withRetry, isTransientError } from './retry.js';
import { logger } from './logger.js';
export interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
success: boolean;
parsedOutput?: unknown;
parseError?: string;
}
export interface ExecuteOptions {
timeout?: number;
cwd?: string;
env?: Record<string, string>;
applyScope?: boolean;
enableRetry?: boolean;
}
async function execInternal(command: string, options: ExecuteOptions = {}): Promise<CommandResult> {
const config = getConfig();
const timeout = options.timeout ?? config.commandTimeoutMs;
logger.debug('Executing', { command, timeout });
return new Promise(resolve => {
exec(command, {
timeout,
cwd: options.cwd,
env: { ...process.env, ...options.env },
maxBuffer: 10 * 1024 * 1024,
}, (error: ExecException | null, stdout: string, stderr: string) => {
const exitCode = error?.code ?? 0;
const success = exitCode === 0;
let parsedOutput: unknown;
let parseError: string | undefined;
if (stdout.trim()) {
try {
parsedOutput = JSON.parse(stdout);
} catch (e) {
parseError = `JSON parse failed: ${e instanceof Error ? e.message : 'unknown'}`;
}
}
const result = { stdout: stdout.trim(), stderr: stderr.trim(), exitCode, success, parsedOutput, parseError };
if (success) {
logger.debug('Command succeeded', { command, exitCode });
} else {
logger.warn('Command failed', { command, exitCode, stderr: stderr.trim() });
}
resolve(result);
});
});
}
export async function executeCommand(command: string, options: ExecuteOptions = {}): Promise<CommandResult> {
const { enableRetry = false } = options;
if (!enableRetry) return execInternal(command, options);
return withRetry(
async () => {
const result = await execInternal(command, options);
if (!result.success && isTransientError(result.stderr)) throw new Error(result.stderr);
return result;
},
'executeCommand',
{ isRetryable: e => isTransientError(e) }
);
}
export async function executeAzCommand(command: string, options: ExecuteOptions = {}): Promise<CommandResult> {
const { applyScope = false, enableRetry = true } = options;
let cmd = command;
if (!cmd.includes('--output') && !cmd.includes('-o ')) cmd += ' --output json';
if (applyScope) {
const scope = getAzureScopeArgs();
if (scope.length) cmd += ' ' + scope.join(' ');
}
return executeCommand(cmd, { ...options, enableRetry });
}
export async function validateAzureCLI(): Promise<boolean> {
const result = await executeCommand('az --version', { enableRetry: false });
return result.success;
}
export async function getAzureAccountInfo(): Promise<{ user?: string; tenantId?: string; subscriptionId?: string } | null> {
const result = await executeAzCommand('az account show', { enableRetry: false });
if (!result.success || !result.parsedOutput) return null;
const account = result.parsedOutput as { user?: { name?: string }; tenantId?: string; id?: string };
return { user: account.user?.name, tenantId: account.tenantId, subscriptionId: account.id };
}