File size: 4,127 Bytes
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
#!/usr/bin/env node

import { setTimeout as delay } from 'node:timers/promises';

import { fetchJsonWithTimeout, isMainModule, pickFailureOutput, printJson, runCommand } from './command-center-utils.mjs';

const LOCAL_BASE_URL = 'http://localhost:4783';
const PROBE_PATHS = ['/api/auth-status', '/api/runtime-capabilities', '/api/agent/capabilities'];
const PROBE_ATTEMPTS = 30;
const PROBE_INTERVAL_MS = 2000;
const PROBE_TIMEOUT_MS = 5000;
const DOCKER_COMPOSE_TIMEOUT_MS = 10 * 60 * 1000;

export function buildDockerComposeArgs(options = {}) {
    const files = ['-f', 'docker-compose.yml'];
    if (options.memory) files.push('-f', 'docker-compose.memory.yml');
    return ['compose', ...files, 'up', '-d', '--build'];
}

export function buildDockerComposeEnv(env = process.env) {
    return { ...env, COMPOSE_PROGRESS: 'plain' };
}

function parseArgs(argv) {
    const unknown = argv.find((arg) => !['--help', '-h', '--memory', '--skip-probe'].includes(arg));
    if (unknown) throw new Error(`Unknown option: ${unknown}`);
    return {
        help: argv.includes('--help') || argv.includes('-h'),
        memory: argv.includes('--memory'),
        skipProbe: argv.includes('--skip-probe')
    };
}

function printHelp() {
    console.log(`Usage:
  npm run deploy:local
  npm run deploy:local -- --memory

Options:
  --memory       Use docker-compose.memory.yml overlay for HF Space-like memory mode.
  --skip-probe   Rebuild and start the container without HTTP endpoint probes.
  --help         Show this help.`);
}

async function fetchJson(path) {
    return fetchJsonWithTimeout(new URL(path, LOCAL_BASE_URL), { timeoutMs: PROBE_TIMEOUT_MS });
}

async function waitForLocalEndpoints() {
    let lastError = '';
    for (let attempt = 1; attempt <= PROBE_ATTEMPTS; attempt += 1) {
        try {
            const responses = {};
            for (const path of PROBE_PATHS) responses[path] = await fetchJson(path);
            return {
                attempts: attempt,
                baseUrl: LOCAL_BASE_URL,
                authRequired: responses['/api/auth-status'].passwordRequired,
                stateBackend: responses['/api/agent/capabilities'].defaults?.state_backend,
                imageStorageMode: responses['/api/agent/capabilities'].storage?.image_storage_mode,
                streamingBatch: responses['/api/runtime-capabilities'].streamingBatch
            };
        } catch (error) {
            lastError = error instanceof Error ? error.message : String(error);
            await delay(PROBE_INTERVAL_MS);
        }
    }
    throw new Error(`Local container did not pass HTTP probes: ${lastError}`);
}

export function assertLocalProbeMatchesMode(probe, options = {}) {
    if (!options.memory) return;
    const mismatches = [];
    if (probe.stateBackend !== 'memory') mismatches.push(`stateBackend=${probe.stateBackend ?? '<missing>'} expected memory`);
    if (probe.imageStorageMode !== 'indexeddb') {
        mismatches.push(`imageStorageMode=${probe.imageStorageMode ?? '<missing>'} expected indexeddb`);
    }
    if (mismatches.length) throw new Error(`Memory overlay did not take effect: ${mismatches.join(', ')}.`);
}

async function main() {
    const options = parseArgs(process.argv.slice(2));
    if (options.help) {
        printHelp();
        return;
    }

    const docker = runCommand('docker', buildDockerComposeArgs(options), {
        env: buildDockerComposeEnv(),
        timeoutMs: DOCKER_COMPOSE_TIMEOUT_MS
    });
    if (!docker.ok) {
        printJson({ ok: false, phase: 'docker-compose', output: pickFailureOutput(docker) });
        process.exit(1);
    }

    if (options.skipProbe) {
        printJson({ ok: true, phase: 'docker-compose', probe: 'skipped' });
        return;
    }

    const probe = await waitForLocalEndpoints();
    assertLocalProbeMatchesMode(probe, options);
    printJson({ ok: true, phase: 'ready', probe });
}

if (isMainModule(import.meta.url, process.argv[1])) {
    main().catch((error) => {
        printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
        process.exit(1);
    });
}