visual-journal / scripts /status.mjs
misonL's picture
Deploy 0720960 to Docker Space
09ad34c verified
Raw
History Blame
15.4 kB
#!/usr/bin/env node
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);
});
}