File size: 4,727 Bytes
3e8ea5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919485b
 
 
 
 
 
3e8ea5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const DEFAULT_MAX_BUFFER = 20 * 1024 * 1024;
const DEFAULT_FETCH_TIMEOUT_MS = 5000;

export function isMainModule(moduleUrl, argvPath) {
    return Boolean(argvPath) && moduleUrl === pathToFileURL(resolve(argvPath)).href;
}

export function runCommand(command, args = [], options = {}) {
    const startedAt = Date.now();
    const result = spawnSync(command, args, {
        cwd: options.cwd,
        encoding: options.encoding || 'utf8',
        env: options.env,
        input: options.input,
        maxBuffer: options.maxBuffer || DEFAULT_MAX_BUFFER,
        stdio: ['pipe', 'pipe', 'pipe'],
        timeout: options.timeoutMs
    });
    const elapsedMs = Date.now() - startedAt;
    if (result.error) {
        return {
            ok: false,
            command,
            args,
            status: result.status,
            stdout: String(result.stdout || ''),
            stderr: String(result.stderr || ''),
            elapsed_ms: elapsedMs,
            error: result.error.message,
            signal: result.signal
        };
    }
    return {
        ok: result.status === 0,
        command,
        args,
        status: result.status,
        stdout: String(result.stdout || ''),
        stderr: String(result.stderr || ''),
        elapsed_ms: elapsedMs,
        signal: result.signal
    };
}

export function runCommandStrict(command, args = [], options = {}) {
    const result = runCommand(command, args, options);
    if (!result.ok) throw new Error(commandFailureMessage(result));
    return result.stdout;
}

export function commandFailureMessage(result) {
    return pickFailureOutput(result) || `${result.command} ${result.args.join(' ')} failed`;
}

export function printJson(payload) {
    console.log(JSON.stringify(payload, null, 2));
}

export function parseJsonPayload(output, label = 'command') {
    const lines = output.split(/\r?\n/);
    let lastError;
    let sawCandidate = false;
    for (let start = 0; start < lines.length; start += 1) {
        const trimmed = lines[start].trimStart();
        if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) continue;
        sawCandidate = true;
        for (let end = lines.length; end > start; end -= 1) {
            try {
                return JSON.parse(lines.slice(start, end).join('\n'));
            } catch (error) {
                lastError = error;
            }
        }
    }
    if (!sawCandidate) throw new Error(`${label} did not return JSON output.`);
    throw new Error(`${label} returned invalid JSON: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
}

export function redactBaseUrl(value) {
    const parsed = new URL(value);
    const path = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
    return `${parsed.protocol}//${parsed.host}${path}`;
}

export async function fetchJsonWithTimeout(url, options = {}) {
    const timeoutMs = options.timeoutMs || DEFAULT_FETCH_TIMEOUT_MS;
    const pathname = safeUrlPathname(url);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const response = await fetch(url, { signal: controller.signal });
        const text = await response.text();
        const bodySnippet = formatResponseBodySnippet(text);
        if (!response.ok) throw new Error(`${pathname} failed with HTTP ${response.status}${bodySnippet}`);
        try {
            return JSON.parse(text);
        } catch (error) {
            if (error instanceof SyntaxError) throw new Error(`${pathname} did not return JSON${bodySnippet}`);
            throw error;
        }
    } catch (error) {
        if (error?.name === 'AbortError') throw new Error(`${pathname} timed out after ${timeoutMs}ms.`);
        throw error;
    } finally {
        clearTimeout(timer);
    }
}

function formatResponseBodySnippet(text) {
    if (!text || process.env.NODE_ENV === 'production') return '';
    return `: ${text.slice(0, 100)}`;
}

function safeUrlPathname(url) {
    try {
        return new URL(url).pathname;
    } catch {
        return String(url);
    }
}

export function pickFailureOutput(result, maxLength = 4000) {
    const output = [
        result.stdout,
        result.stderr,
        result.status !== undefined && result.status !== null ? `status: ${result.status}` : '',
        result.error ? `error: ${result.error}` : '',
        result.signal ? `signal: ${result.signal}` : ''
    ]
        .filter(Boolean)
        .join('\n')
        .trim();
    return output.length > maxLength ? `${output.slice(0, maxLength)}...` : output;
}