Spaces:
Running
Running
| import { existsSync, readFileSync } from 'node:fs'; | |
| import net from 'node:net'; | |
| import { HF_SPACE_ID, HF_SPACE_URL } from './hf-space-doctor-utils.mjs'; | |
| import { isMainModule, parseJsonPayload, printJson, runCommand, runCommandStrict } from './command-center-utils.mjs'; | |
| import { CHANNEL_REQUEST_MODES, CHANNEL_REQUEST_MODE_ADMIN_CONTROL } from '../src/lib/channel-request-mode-values.mjs'; | |
| const REMOTE_STATUS_TIMEOUT_MS = 30_000; | |
| const LOCAL_ENDPOINT_TIMEOUT_MS = 500; | |
| const IMAGE_UPSTREAM_REAL_SMOKE_CASES = [ | |
| { id: 'original-images-json', prefix: 'IMAGE_REAL_SMOKE_ORIGINAL', requestMode: 'images-non-stream' }, | |
| { id: 'gaoren-images-sse', prefix: 'IMAGE_REAL_SMOKE_GAOREN', requestMode: 'images-sse' }, | |
| { id: 'sub2api-images-sse', prefix: 'IMAGE_REAL_SMOKE_SUB2API', requestMode: 'images-sse' }, | |
| { | |
| id: 'sub2api-responses-json', | |
| prefix: 'IMAGE_REAL_SMOKE_SUB2API_RESPONSES', | |
| fallbackPrefix: 'IMAGE_REAL_SMOKE_SUB2API', | |
| requiresResponsesModel: true, | |
| requestMode: 'responses-non-stream' | |
| }, | |
| { | |
| id: 'gpt2image-responses-sse', | |
| prefix: 'IMAGE_REAL_SMOKE_GPT2IMAGE', | |
| requiresResponsesModel: true, | |
| requestMode: 'responses-sse' | |
| }, | |
| { id: 'matsca-images-sse', prefix: 'IMAGE_REAL_SMOKE_MATSCA', requestMode: 'images-sse' } | |
| ]; | |
| const IMAGE_UPSTREAM_FINAL_GATE_COMMAND = | |
| CHANNEL_REQUEST_MODE_ADMIN_CONTROL.finalGateCommand; | |
| const STATUS_ENV_FILES = [ | |
| { path: '.env.local', override: false }, | |
| { path: '.env.real-smoke.local', override: true } | |
| ]; | |
| export function buildAdminCommands() { | |
| return { | |
| first_run: 'npm run first-run', | |
| doctor: 'npm run doctor', | |
| status: 'npm run status', | |
| env_summary: 'npm run env:summary', | |
| verify: 'npm run verify', | |
| deploy_local: 'npm run deploy:local', | |
| deploy_space: 'npm run deploy:space', | |
| docker_cleanup_fixtures: 'npm run docker:cleanup-fixtures', | |
| agent_doctor: 'npm run agent:doctor', | |
| hf_space_doctor: 'npm run doctor:hf-space', | |
| hf_space_local_smoke: 'npm run smoke:hf-space-local', | |
| hf_space_smoke_legacy_alias: 'npm run smoke:hf-space' | |
| }; | |
| } | |
| export function parseGitStatusEntries(output) { | |
| const entries = output.split('\0').filter(Boolean); | |
| const paths = []; | |
| for (let index = 0; index < entries.length; index += 1) { | |
| const entry = entries[index]; | |
| const status = entry.slice(0, 2); | |
| paths.push(entry.slice(3)); | |
| if (status.includes('R') || status.includes('C')) index += 1; | |
| } | |
| return paths; | |
| } | |
| function readEnv(env, key) { | |
| const value = env[key]; | |
| return typeof value === 'string' && value.trim() ? value.trim() : undefined; | |
| } | |
| function shouldSetStatusEnv(key, baseEnv, statusEnv, options) { | |
| if (baseEnv[key] !== undefined) return false; | |
| return options.override || statusEnv[key] === undefined; | |
| } | |
| function loadStatusEnvFile(statusEnv, baseEnv, options) { | |
| if (!existsSync(options.path)) return; | |
| for (const line of readFileSync(options.path, 'utf8').split(/\r?\n/)) { | |
| const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); | |
| if (!match || !shouldSetStatusEnv(match[1], baseEnv, statusEnv, options)) continue; | |
| const value = match[2].trim(); | |
| const quoted = | |
| value.length >= 2 && | |
| ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))); | |
| statusEnv[match[1]] = quoted ? value.slice(1, -1) : value; | |
| } | |
| } | |
| export function readStatusEnvFromFiles(baseEnv = process.env, envFiles = STATUS_ENV_FILES) { | |
| const statusEnv = { ...baseEnv }; | |
| for (const envFile of envFiles) { | |
| loadStatusEnvFile(statusEnv, baseEnv, envFile); | |
| } | |
| return statusEnv; | |
| } | |
| function readSmokeEnvAlternatives(testCase, suffix) { | |
| const keys = [`${testCase.prefix}_${suffix}`]; | |
| if (testCase.fallbackPrefix && testCase.fallbackPrefix !== testCase.prefix) { | |
| keys.push(`${testCase.fallbackPrefix}_${suffix}`); | |
| } | |
| return keys; | |
| } | |
| function readResponsesModelEnvAlternatives(testCase) { | |
| return [`${testCase.prefix}_RESPONSES_MODEL`, 'OPENAI_RESPONSES_API_MODEL']; | |
| } | |
| function readFirstStatusEnv(env, keys) { | |
| for (const key of keys) { | |
| const value = readEnv(env, key); | |
| if (value) return { key, value }; | |
| } | |
| return undefined; | |
| } | |
| function readBaseUrlValidationError(value) { | |
| let url; | |
| try { | |
| url = new URL(value); | |
| } catch { | |
| return 'must_be_http_or_https_absolute_url'; | |
| } | |
| if (url.protocol !== 'http:' && url.protocol !== 'https:') return 'must_use_http_or_https'; | |
| if (url.username || url.password) return 'must_not_include_credentials'; | |
| if (url.search || url.hash) return 'must_not_include_query_or_fragment'; | |
| return undefined; | |
| } | |
| function readTargetConfigured(testCase, env) { | |
| const baseUrl = readFirstStatusEnv(env, readSmokeEnvAlternatives(testCase, 'BASE_URL')); | |
| const apiKey = readFirstStatusEnv(env, readSmokeEnvAlternatives(testCase, 'API_KEY')); | |
| const responsesModel = testCase.requiresResponsesModel | |
| ? readFirstStatusEnv(env, readResponsesModelEnvAlternatives(testCase)) | |
| : undefined; | |
| const baseUrlError = baseUrl ? readBaseUrlValidationError(baseUrl.value) : undefined; | |
| return { | |
| baseUrl: Boolean(baseUrl), | |
| baseUrlValue: baseUrl?.value, | |
| apiKey: Boolean(apiKey), | |
| responsesModel: Boolean(responsesModel), | |
| invalidEnv: baseUrlError ? [{ key: baseUrl.key, reason: baseUrlError }] : [] | |
| }; | |
| } | |
| function readMissingEnvAny(testCase, target) { | |
| const groups = []; | |
| if (!target.baseUrl) groups.push(readSmokeEnvAlternatives(testCase, 'BASE_URL')); | |
| if (target.baseUrl && !target.apiKey) groups.push(readSmokeEnvAlternatives(testCase, 'API_KEY')); | |
| if (target.baseUrl && testCase.requiresResponsesModel && !target.responsesModel) { | |
| groups.push(readResponsesModelEnvAlternatives(testCase)); | |
| } | |
| return groups; | |
| } | |
| export function buildImageUpstreamRealSmokeStatus(env = process.env) { | |
| const caseSummaries = IMAGE_UPSTREAM_REAL_SMOKE_CASES.map((testCase) => { | |
| const target = readTargetConfigured(testCase, env); | |
| const missingEnvAny = readMissingEnvAny(testCase, target); | |
| return { | |
| id: testCase.id, | |
| request_mode: testCase.requestMode, | |
| configured: missingEnvAny.length === 0 && target.invalidEnv.length === 0, | |
| ...(missingEnvAny.length > 0 ? { missing_env_any: missingEnvAny } : {}), | |
| ...(target.invalidEnv.length > 0 ? { invalid_env: target.invalidEnv } : {}) | |
| }; | |
| }); | |
| const configuredCases = caseSummaries.filter((item) => item.configured).map((item) => item.id); | |
| const missingCases = caseSummaries.filter( | |
| (item) => Array.isArray(item.missing_env_any) && item.missing_env_any.length > 0 | |
| ); | |
| const invalidCases = caseSummaries.filter((item) => Array.isArray(item.invalid_env) && item.invalid_env.length > 0); | |
| return { | |
| required_count: IMAGE_UPSTREAM_REAL_SMOKE_CASES.length, | |
| required_cases: IMAGE_UPSTREAM_REAL_SMOKE_CASES.map((testCase) => testCase.id), | |
| configuration_complete: missingCases.length === 0 && invalidCases.length === 0, | |
| configured_count: configuredCases.length, | |
| configured_cases: configuredCases, | |
| missing_count: missingCases.length, | |
| missing_cases: missingCases.map((item) => item.id), | |
| missing_env_any: Object.fromEntries(missingCases.map((item) => [item.id, item.missing_env_any])), | |
| invalid_count: invalidCases.length, | |
| invalid_cases: invalidCases.map((item) => item.id), | |
| invalid_env: Object.fromEntries(invalidCases.map((item) => [item.id, item.invalid_env])), | |
| request_modes: summarizeRealSmokeRequestModes(caseSummaries), | |
| final_gate_command: IMAGE_UPSTREAM_FINAL_GATE_COMMAND | |
| }; | |
| } | |
| function summarizeRealSmokeRequestModes(caseSummaries) { | |
| return Object.fromEntries( | |
| CHANNEL_REQUEST_MODES.map((mode) => { | |
| const items = caseSummaries.filter((item) => item.request_mode === mode); | |
| const configured = items.filter((item) => item.configured); | |
| const missing = items.filter((item) => Array.isArray(item.missing_env_any) && item.missing_env_any.length > 0); | |
| const invalid = items.filter((item) => Array.isArray(item.invalid_env) && item.invalid_env.length > 0); | |
| return [ | |
| mode, | |
| { | |
| required_count: items.length, | |
| required_cases: items.map((item) => item.id), | |
| configuration_complete: missing.length === 0 && invalid.length === 0, | |
| configured_count: configured.length, | |
| configured_cases: configured.map((item) => item.id), | |
| missing_count: missing.length, | |
| missing_cases: missing.map((item) => item.id), | |
| invalid_count: invalid.length, | |
| invalid_cases: invalid.map((item) => item.id), | |
| smoke_state: 'not_run_by_status' | |
| } | |
| ]; | |
| }) | |
| ); | |
| } | |
| function normalizeLocalHostname(hostname) { | |
| return hostname.toLowerCase().replace(/^\[|\]$/g, ''); | |
| } | |
| function isLocalHostname(hostname) { | |
| const normalized = normalizeLocalHostname(hostname); | |
| return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1'; | |
| } | |
| function readUrlPort(url) { | |
| if (url.port) return Number(url.port); | |
| if (url.protocol === 'http:') return 80; | |
| if (url.protocol === 'https:') return 443; | |
| return undefined; | |
| } | |
| function readLocalEndpoint(testCase, env) { | |
| const target = readTargetConfigured(testCase, env); | |
| if (!target.baseUrlValue || target.invalidEnv.length > 0) return undefined; | |
| const url = new URL(target.baseUrlValue); | |
| if (!isLocalHostname(url.hostname)) return undefined; | |
| const port = readUrlPort(url); | |
| if (!port) return undefined; | |
| return { | |
| id: testCase.id, | |
| host: normalizeLocalHostname(url.hostname), | |
| port | |
| }; | |
| } | |
| function readLocalEndpointTargets(env) { | |
| return IMAGE_UPSTREAM_REAL_SMOKE_CASES.map((testCase) => readLocalEndpoint(testCase, env)).filter(Boolean); | |
| } | |
| function formatEndpoint(endpoint) { | |
| const host = endpoint.host.includes(':') ? `[${endpoint.host}]` : endpoint.host; | |
| return `${host}:${endpoint.port}`; | |
| } | |
| function readSocketFailureReason(error) { | |
| if (error?.code === 'ECONNREFUSED') return 'connection_refused'; | |
| if (error?.code === 'ETIMEDOUT') return 'timeout'; | |
| if (error?.code === 'EHOSTUNREACH') return 'host_unreachable'; | |
| if (error?.code === 'ENETUNREACH') return 'network_unreachable'; | |
| return 'connection_failed'; | |
| } | |
| function probeTcpEndpoint(endpoint, options = {}) { | |
| const timeoutMs = options.timeoutMs || LOCAL_ENDPOINT_TIMEOUT_MS; | |
| return new Promise((resolve) => { | |
| const socket = net.createConnection({ host: endpoint.host, port: endpoint.port }); | |
| let settled = false; | |
| const finish = (result) => { | |
| if (settled) return; | |
| settled = true; | |
| socket.destroy(); | |
| resolve(result); | |
| }; | |
| socket.setTimeout(timeoutMs, () => finish({ ok: false, reason: 'timeout' })); | |
| socket.once('connect', () => finish({ ok: true })); | |
| socket.once('error', (error) => finish({ ok: false, reason: readSocketFailureReason(error) })); | |
| }); | |
| } | |
| export async function buildImageUpstreamLocalEndpointStatus(env = process.env, options = {}) { | |
| const probe = options.probe || probeTcpEndpoint; | |
| const results = []; | |
| for (const endpoint of readLocalEndpointTargets(env)) { | |
| const result = await probe(endpoint, options); | |
| results.push({ | |
| id: endpoint.id, | |
| endpoint: formatEndpoint(endpoint), | |
| ok: result.ok === true, | |
| ...(result.ok === true ? {} : { reason: result.reason || 'connection_failed' }) | |
| }); | |
| } | |
| const unavailable = results.filter((item) => !item.ok); | |
| return { | |
| checked_count: results.length, | |
| unavailable_count: unavailable.length, | |
| unavailable_cases: unavailable.map((item) => item.id), | |
| results | |
| }; | |
| } | |
| function parseArgs(argv) { | |
| const unknown = argv.find((arg) => !['--help', '-h', '--remote'].includes(arg)); | |
| if (unknown) throw new Error(`Unknown option: ${unknown}`); | |
| return { | |
| help: argv.includes('--help') || argv.includes('-h'), | |
| remote: argv.includes('--remote') | |
| }; | |
| } | |
| function printHelp() { | |
| console.log(`Usage: | |
| npm run status | |
| npm run status -- --remote | |
| Options: | |
| --remote Include read-only Hugging Face Space runtime info. | |
| --help Show this help.`); | |
| } | |
| async function buildLocalStatus() { | |
| const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); | |
| const branch = runCommandStrict('git', ['branch', '--show-current']).trim(); | |
| const head = runCommandStrict('git', ['rev-parse', '--short', 'HEAD']).trim(); | |
| const changed = parseGitStatusEntries(runCommandStrict('git', ['status', '--porcelain=v1', '-z'])); | |
| const statusEnv = readStatusEnvFromFiles(process.env); | |
| const imageUpstreamRealSmoke = buildImageUpstreamRealSmokeStatus(statusEnv); | |
| return { | |
| product: packageJson.name, | |
| version: packageJson.version, | |
| branch, | |
| head, | |
| dirty: changed.length > 0, | |
| changed_files: changed, | |
| node: process.version, | |
| commands: buildAdminCommands(), | |
| space: { | |
| id: HF_SPACE_ID, | |
| url: HF_SPACE_URL | |
| }, | |
| agent: { | |
| capabilities: '/api/agent/capabilities', | |
| skill: 'skills/gpt-image-playground-agent/SKILL.md' | |
| }, | |
| image_upstream_real_smoke: { | |
| ...imageUpstreamRealSmoke, | |
| local_endpoint_checks: await buildImageUpstreamLocalEndpointStatus(statusEnv) | |
| } | |
| }; | |
| } | |
| export function readRemoteStatusFromResult(result) { | |
| if (!result.ok) { | |
| return { ok: false, error: result.error || result.stderr || result.stdout || 'Cannot read Hugging Face Space info.' }; | |
| } | |
| let info; | |
| try { | |
| info = parseJsonPayload(result.stdout, 'hf spaces info'); | |
| } catch (error) { | |
| return { ok: false, error: error instanceof Error ? error.message : String(error) }; | |
| } | |
| return { | |
| ok: true, | |
| stage: info.runtime?.stage || 'unknown', | |
| sha: info.sha || info.runtime?.raw?.sha || 'unknown', | |
| hardware: info.runtime?.hardware || info.hardware || 'unknown' | |
| }; | |
| } | |
| function readRemoteStatus() { | |
| return readRemoteStatusFromResult( | |
| runCommand('hf', ['spaces', 'info', HF_SPACE_ID, '--format', 'json'], { timeoutMs: REMOTE_STATUS_TIMEOUT_MS }) | |
| ); | |
| } | |
| async function main() { | |
| const options = parseArgs(process.argv.slice(2)); | |
| if (options.help) { | |
| printHelp(); | |
| return; | |
| } | |
| const status = await buildLocalStatus(); | |
| printJson({ | |
| ok: true, | |
| ...status, | |
| ...(options.remote ? { remote: readRemoteStatus() } : {}) | |
| }); | |
| } | |
| 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); | |
| }); | |
| } | |