#!/usr/bin/env node function normalizeBaseUrl(value) { return String(value ?? '').trim().replace(/\/+$/, ''); } function requireValue(name, value) { if (!value) throw new Error(`${name} requires a value`); return value; } export function parseMemoryV2AppCanaryArgs(argv = [], env = process.env) { const options = { baseUrl: normalizeBaseUrl(env.MEMORY_V2_APP_BASE_URL || 'http://127.0.0.1:8081'), requireEnabled: false, requireTargetHealthy: false, expectedBackend: null, expectedSelectedBackend: null, timeoutMs: Number(env.MEMORY_V2_APP_CANARY_TIMEOUT_MS || 8000), help: false, }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--base-url') { options.baseUrl = normalizeBaseUrl(requireValue(arg, argv[++i])); } else if (arg === '--require-enabled') { options.requireEnabled = true; } else if (arg === '--require-target-healthy') { options.requireTargetHealthy = true; } else if (arg === '--expect-backend') { options.expectedBackend = requireValue(arg, argv[++i]); } else if (arg === '--expect-selected-backend') { options.expectedSelectedBackend = requireValue(arg, argv[++i]); } else if (arg === '--timeout-ms') { options.timeoutMs = Number(requireValue(arg, argv[++i])); } else if (arg === '-h' || arg === '--help') { options.help = true; } else { throw new Error(`Unknown argument: ${arg}`); } } if (!options.baseUrl) throw new Error('--base-url is required'); if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) { throw new Error('--timeout-ms must be a positive number'); } return options; } function usage() { return `Usage: node scripts/check-memory-v2-app-canary.mjs [options] Options: --base-url Portal base URL. Defaults to MEMORY_V2_APP_BASE_URL or http://127.0.0.1:8081. --require-enabled Fail unless /api/runtime/status reports memory.enabled=true. --expect-backend Fail unless memory.backend equals this value. --expect-selected-backend Fail unless memory.selectedBackend equals this value. --require-target-healthy Fail unless at least one runtime target is healthy. --timeout-ms Request timeout. Defaults to 8000. `; } async function fetchJson(fetchImpl, url, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(url, { signal: controller.signal }); const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { ok: response.ok, status: response.status, text, json, }; } finally { clearTimeout(timer); } } function check(name, ok, details = {}) { return { name, ok: Boolean(ok), ...details }; } export async function runMemoryV2AppCanaryCli({ argv = process.argv.slice(2), env = process.env, stdout = process.stdout, fetchImpl = globalThis.fetch, } = {}) { const options = parseMemoryV2AppCanaryArgs(argv, env); if (options.help) { stdout.write(usage()); return 0; } if (typeof fetchImpl !== 'function') { throw new Error('fetch is not available in this Node.js runtime'); } const runtime = await fetchJson(fetchImpl, `${options.baseUrl}/api/runtime/status`, options.timeoutMs); const apiStatus = await fetchJson(fetchImpl, `${options.baseUrl}/api/status`, options.timeoutMs); const authStatus = await fetchJson(fetchImpl, `${options.baseUrl}/auth/status`, options.timeoutMs); const memory = runtime.json?.memory ?? null; const targets = Array.isArray(runtime.json?.targets) ? runtime.json.targets : []; const healthyTargets = targets.filter((target) => target?.healthy); const checks = [ check('runtime_status_http_ok', runtime.ok, { status: runtime.status }), check('runtime_status_ok', runtime.json?.ok === true), check('memory_status_present', Boolean(memory)), check('api_status_http_ok', apiStatus.ok, { status: apiStatus.status }), check('auth_status_http_ok', authStatus.ok, { status: authStatus.status }), ]; if (options.requireEnabled) { checks.push(check('memory_enabled', memory?.enabled === true, { actual: memory?.enabled ?? null })); } if (options.expectedBackend) { checks.push(check('memory_backend', memory?.backend === options.expectedBackend, { expected: options.expectedBackend, actual: memory?.backend ?? null, })); } if (options.expectedSelectedBackend) { checks.push(check('memory_selected_backend', memory?.selectedBackend === options.expectedSelectedBackend, { expected: options.expectedSelectedBackend, actual: memory?.selectedBackend ?? null, })); } if (options.requireTargetHealthy) { checks.push(check('runtime_target_healthy', healthyTargets.length > 0, { healthyTargets: healthyTargets.length, targets: targets.length, })); } const report = { ok: checks.every((item) => item.ok), baseUrl: options.baseUrl, summary: { memory: memory ? { enabled: memory.enabled, backend: memory.backend, selectedBackend: memory.selectedBackend, failOpen: memory.failOpen, vectorEnabled: memory.vectorEnabled, } : null, targets: { total: targets.length, healthy: healthyTargets.length, }, apiStatus: { status: apiStatus.status, body: apiStatus.text.slice(0, 80), }, authStatus: { status: authStatus.status, authenticated: authStatus.json?.authenticated ?? null, mode: authStatus.json?.mode ?? null, }, }, checks, }; stdout.write(`${JSON.stringify(report, null, 2)}\n`); return report.ok ? 0 : 1; } if (import.meta.url === `file://${process.argv[1]}`) { runMemoryV2AppCanaryCli().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err instanceof Error ? err.message : err); process.exitCode = 1; }); }