run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
654 lines • 35.9 kB
JavaScript
/**
* `r.doctor()` — local health and configuration diagnostics, owned by the
* Node SDK: config directory, wallet, local project keys, API reachability,
* tier and organization lifecycle, account health, function runtime
* staleness, recovery posture, the vault, and the scoped source scan.
*
* The report is `{ ok, blocking[], warnings[], checks[] }`. `ok` answers ONE
* question — can this agent ship from here — and is true exactly when
* `blocking[]` is empty. Every check carries a `severity`:
* blocking would stop a deploy
* advisory a gap worth surfacing that never stops a deploy (`warnings[]`)
* info nothing to act on
* A status outside the known vocabulary fails closed as blocking.
*
* `run402 doctor` is a shim over this; the checks that belong to the calling
* program itself (whether the installed CLI is current, its resident vault
* helper) are hooks the caller supplies.
*/
import { existsSync, statSync } from "node:fs";
import { getConfigDir } from "../../core-dist/config.js";
import { readLocalWallet } from "./wallets.js";
import { loadKeyStore } from "../../core-dist/keystore.js";
import { LocalError } from "../errors.js";
import { VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT } from "../namespaces/vault.crypto.js";
import { resolveApplicationScope, loadApplicationScanInput } from "./app-scope.js";
import { resolveScanRoot, scanDeploymentSources, scanSourceTree, SCAN_SEVERITY } from "./source-scan.js";
import { resolveVaultTarget } from "./vault-target.js";
/**
* The stable, complete registry of ordinary-mode check names, in the order
* each normally runs: the one list `only` validates against.
*/
export const DOCTOR_CHECK_NAMES = [
"config_dir",
"cli_update",
"wallet",
"projects",
"api_reachable",
"tier",
"account_health",
"runtime_staleness",
"retiring_routes",
"recovery_posture",
"vault",
"source_scan",
];
const INFO_STATUSES = new Set(["ok", "skipped", "unknown"]);
const TIER_LIFECYCLE_STATUSES = new Set(["past_due", "frozen", "dormant", "purged"]);
function severityOf(check) {
if (check.severity === "blocking" || check.severity === "advisory" || check.severity === "info")
return check.severity;
if (INFO_STATUSES.has(check.status))
return "info";
if (check.status === "warning")
return "advisory";
return "blocking";
}
/** Stable codes for advisory checks that report ONE finding (no `value.gaps`). */
const ADVISORY_CODES = {
cli_update: "CLI_UPDATE_AVAILABLE",
runtime_staleness: "FUNCTION_RUNTIME_STALE",
source_scan: "SOURCE_SCAN_WARNINGS",
tier: "TIER_MISSING_ON_OWN_ORG",
};
/**
* Fold per-check results into `{ ok, blocking[], warnings[], checks[] }`:
* one warning per gap string of every advisory check (one for an advisory
* check with no gaps), one blocking entry per blocking check. Pure.
*/
export function buildDoctorReport(rawChecks) {
const checks = rawChecks.map((check) => {
const { name, status, severity: _pinned, ...rest } = check;
return { name, status, severity: severityOf(check), ...rest };
});
const blocking = [];
const warnings = [];
for (const check of checks) {
if (check.severity === "blocking") {
blocking.push({
check: check.name,
status: check.status,
message: check.message ?? `${check.name}: ${check.status}`,
...(check.hint && { hint: check.hint }),
});
continue;
}
if (check.severity !== "advisory")
continue;
const value = check.value;
const gaps = Array.isArray(value?.gaps) ? value.gaps.filter((g) => typeof g === "string" && g.length > 0) : [];
if (gaps.length > 0) {
for (const gap of gaps)
warnings.push({ check: check.name, message: gap, ...(check.hint && { hint: check.hint }) });
}
else {
const code = check.code ?? ADVISORY_CODES[check.name];
const message = check.message ?? check.hint ?? `${check.name}: ${check.status}`;
warnings.push({
check: check.name,
...(code && { code }),
message,
...(check.hint && check.hint !== message && { hint: check.hint }),
});
}
}
return { ok: blocking.length === 0, blocking, warnings, checks };
}
/** Refuse an `only` name outside {@link DOCTOR_CHECK_NAMES} (`BAD_USAGE`, listing the valid names). */
export function assertDoctorCheckNames(names) {
for (const name of names) {
if (!DOCTOR_CHECK_NAMES.includes(name)) {
throw new LocalError(`Unknown doctor check: '${name}'.`, "running doctor", {
code: "BAD_USAGE",
hint: `Valid check names: ${DOCTOR_CHECK_NAMES.join(", ")}.`,
details: { check: name, known_checks: [...DOCTOR_CHECK_NAMES] },
});
}
}
}
function redactWalletForDiagnostics(localWallet) {
const safe = { ...localWallet };
delete safe.privateKey;
if (typeof safe.funded === "boolean")
safe.faucet_used = safe.funded;
delete safe.funded;
return safe;
}
/**
* A check-failure message, context first: the SDK's `<message> while
* <context>` reads as two jammed fragments, so the trailing context is cut
* and the check label leads.
*/
function describeCheckFailure(label, err) {
const raw = err instanceof Error ? err.message : String(err);
const context = typeof err?.context === "string" && (err.context).length > 0
? err.context
: null;
let reason = raw;
if (context) {
const marker = ` while ${context}`;
const idx = raw.indexOf(marker);
if (idx !== -1)
reason = (raw.slice(0, idx) + raw.slice(idx + marker.length)).trim();
}
return `${label} failed: ${reason}`;
}
const message = (err) => (err instanceof Error ? err.message : String(err));
/** Run the ordinary-mode checks against `r` and fold them into the report. */
export async function runDoctor(r, opts = {}) {
const verbose = opts.verbose === true;
const refresh = opts.refresh === true;
const cwd = opts.cwd ?? process.cwd();
const only = new Set(opts.only ?? []);
assertDoctorCheckNames([...only]);
const wanted = (name) => only.size === 0 || only.has(name);
const checks = [];
const CONFIG_DIR = getConfigDir();
// 1. Config directory.
if (wanted("config_dir"))
try {
if (existsSync(CONFIG_DIR) && statSync(CONFIG_DIR).isDirectory()) {
checks.push({ name: "config_dir", status: "ok", value: CONFIG_DIR });
}
else {
checks.push({ name: "config_dir", status: "missing", value: CONFIG_DIR, hint: "Run 'run402 init' to set up the config directory." });
}
}
catch (err) {
checks.push({ name: "config_dir", status: "error", message: message(err) });
}
// 1b. The calling program's update state: advisory, never hides the rest.
if (wanted("cli_update")) {
if (opts.cliUpdate) {
try {
checks.push(await opts.cliUpdate({ refresh }));
}
catch (err) {
checks.push({ name: "cli_update", status: "unknown", message: message(err) });
}
}
else {
checks.push({ name: "cli_update", status: "skipped", message: "No CLI update check on this surface." });
}
}
// 2. Wallet.
let walletConfigured = false;
if (wanted("wallet"))
try {
const localWallet = readLocalWallet();
if (localWallet) {
walletConfigured = true;
checks.push({
name: "wallet",
status: "ok",
value: {
rail: localWallet.rail,
// Never keystore secrets, even verbose.
...(verbose && { details: redactWalletForDiagnostics(localWallet) }),
},
});
}
else {
checks.push({ name: "wallet", status: "missing", hint: "Run 'run402 init' to create a wallet." });
}
}
catch (err) {
// A wallet file that exists but is unreadable stops the run: nothing
// after it can be trusted to describe this machine.
if (err?.code === "BAD_WALLET_FILE")
throw err;
checks.push({ name: "wallet", status: "error", message: message(err) });
}
// 3. Local project keys (the wallet itself is check 2). Empty is normal.
if (wanted("projects"))
try {
const keystore = loadKeyStore();
const projectCount = Object.keys(keystore?.projects ?? {}).length;
checks.push({
name: "projects",
status: "ok",
value: { project_count: projectCount },
...(projectCount === 0 && {
hint: walletConfigured
? "No projects yet — run 'run402 projects provision' to create one (wallet is already set up)."
: "No projects yet — run 'run402 init' to set up the wallet first, then 'run402 projects provision'.",
}),
});
}
catch (err) {
checks.push({ name: "projects", status: "error", message: message(err) });
}
// 4. API reachability (read-only, unauthenticated).
if (wanted("api_reachable"))
try {
const t0 = Date.now();
await r.service.status();
const elapsed = Date.now() - t0;
checks.push({ name: "api_reachable", status: "ok", ...(verbose && { value: { elapsed_ms: elapsed } }) });
}
catch (err) {
checks.push({
name: "api_reachable",
status: "error",
message: message(err),
hint: "Check the RUN402_API_BASE env var and your network connection.",
});
}
// 5. Tier. `status` is a FIXED vocabulary — ok | inactive | frozen |
// past_due | dormant | purged | missing | unknown | error — never a tier
// name; the name and raw lifecycle ride in value.
if (wanted("tier"))
try {
const tier = (await r.tier.status());
const tierName = typeof tier?.tier === "string" && tier.tier.length > 0 ? tier.tier : null;
const lifecycle = typeof tier?.organization_lifecycle_state === "string" && tier.organization_lifecycle_state.length > 0
? tier.organization_lifecycle_state
: null;
const active = tier?.active === true;
// A wallet whose own org holds no tier can still ship into another org's
// projects, so "missing" blocks only when there is nowhere to ship.
const reachableProjects = Array.isArray(tier?.projects) ? tier.projects.length : 0;
let status;
if (lifecycle !== null && lifecycle !== "active") {
status = TIER_LIFECYCLE_STATUSES.has(lifecycle) ? lifecycle : "inactive";
}
else if (tierName === null) {
status = "missing";
}
else if (!active) {
status = "inactive";
}
else if (lifecycle === null) {
status = "unknown";
}
else {
status = "ok";
}
const value = {
tier: tierName,
lifecycle,
active,
organization_lifecycle_state: lifecycle,
lease_expires_at: tier?.lease_perpetual === true ? null : tier?.lease_expires_at ?? null,
reachable_projects: reachableProjects,
};
if (status === "ok") {
checks.push({ name: "tier", status, value });
}
else if (status === "missing" && reachableProjects > 0) {
checks.push({
name: "tier",
status,
severity: "advisory",
value,
message: `this wallet's own organization holds no tier, but it can reach ${reachableProjects} project(s) owned by another organization (membership or grant) — deploys to those are unaffected`,
hint: "Run 'run402 tier set prototype' only if you want to provision projects under this wallet's own organization.",
});
}
else {
checks.push({
name: "tier",
status,
value,
message: status === "unknown"
? "tier resolved, but the organization lifecycle could not be determined"
: status === "missing"
? "no tier on this wallet's organization and no reachable project — nothing can be provisioned or deployed from here"
: status === "inactive"
? `tier '${tierName ?? "(none)"}' is not active${lifecycle && lifecycle !== "active" ? ` (organization lifecycle '${lifecycle}')` : ""}`
: `organization lifecycle is '${lifecycle}' — the control plane is gated until the tier is reactivated`,
hint: status === "unknown"
? "Tier resolved, but organization lifecycle could not be determined. Check `run402 tier status` before assuming the account is healthy."
: "Run 'run402 tier set prototype' to set, renew, or reactivate the tier.",
});
}
}
catch (err) {
checks.push({ name: "tier", status: "error", message: describeCheckFailure("tier status check", err) });
}
// 6. Account health, runtime staleness, and recovery posture all ride ONE
// `GET /agent/v1/me/status` read, skipped entirely when none is wanted.
if (wanted("account_health") || wanted("runtime_staleness") || wanted("retiring_routes") || wanted("recovery_posture"))
try {
const status = (await r.me.status());
const gaps = [];
if (status.contact.email_status !== "verified") {
const ch = status.email_verification?.last_challenge;
if (ch && ch.hint) {
const attemptsLine = ch.attempt_count > 0
? ` (${ch.attempt_count}/${ch.attempt_count + ch.remaining_attempts} attempts used, ${ch.remaining_attempts} remaining)`
: "";
gaps.push(`contact email not verified${attemptsLine}: ${ch.hint}`);
}
else {
gaps.push(`contact email not verified (${status.contact.email_status}) — run 'run402 agent contact --email ...' then reply to the challenge`);
}
}
if (status.contact.passkey_status !== "verified") {
gaps.push("contact passkey not bound — run 'run402 login' (or 'run402 agent passkey enroll') after email verification");
}
const reach = status.reachability;
if (reach && reach.reachable === false) {
const skipped = reach.skipped_last_90d > 0
? ` (${reach.skipped_last_90d} notification(s) already skipped in the last 90 days)`
: "";
gaps.push(`no verified notification recipient — mandatory recovery/security notifications currently reach nobody${skipped}; run 'run402 agent contact --email ...' then reply to the challenge`);
}
if (Array.isArray(status.skipped_notifications) && status.skipped_notifications.length > 0) {
gaps.push(`${status.skipped_notifications.length} notification(s) skipped due to missing verified recipient`);
}
if (Array.isArray(status.critical_items) && status.critical_items.length > 0) {
for (const item of status.critical_items)
gaps.push(`${item.kind}: ${item.detail}`);
}
if (wanted("account_health")) {
if (gaps.length > 0) {
checks.push({
name: "account_health",
status: "warning",
value: { gaps },
hint: "Address the above gaps; they're what 'run402 notifications' is designed to surface.",
});
}
else {
checks.push({ name: "account_health", status: "ok" });
}
}
// 6b. Function runtime staleness: a redeploy with unchanged source does
// not refresh the platform wrapper; refreshing is opt-in.
if (wanted("runtime_staleness")) {
const runtime = status.runtime;
if (runtime && typeof runtime.stale_function_count === "number") {
if (runtime.stale_function_count > 0) {
checks.push({
name: "runtime_staleness",
status: "warning",
value: { stale_function_count: runtime.stale_function_count, stale_functions: runtime.stale_functions ?? [] },
hint: `${runtime.stale_function_count} function(s) are running an older platform runtime. Run 'run402 functions rebuild --all' to refresh (re-bundles from your stored source; no source change).`,
});
}
else {
checks.push({ name: "runtime_staleness", status: "ok", value: { stale_function_count: 0 } });
}
}
else {
checks.push({
name: "runtime_staleness",
status: "skipped",
...(verbose && { hint: "account status has no 'runtime' block; requires v1.69+ gateway." }),
});
}
}
// 6b'. Retiring routes: calls this principal's projects made to a route
// being retired (surface-retirement), with the successor and the fix.
if (wanted("retiring_routes")) {
const retiring = status.retiring_routes;
if (!Array.isArray(retiring)) {
checks.push({
name: "retiring_routes",
status: "skipped",
...(verbose && { hint: "account status has no 'retiring_routes' block; requires a newer gateway." }),
});
}
else if (retiring.length === 0) {
checks.push({ name: "retiring_routes", status: "ok", value: { callers: 0 } });
}
else {
const fixes = retiring.map((r) => {
const kinds = Array.isArray(r.caller_kinds) ? r.caller_kinds : [];
const fns = Array.isArray(r.function_names) ? r.function_names : [];
const fix = kinds.includes("function") || fns.length > 0
? `run402 functions rebuild --all --project ${String(r.project_id)}`
: kinds.some((k) => k === "cli" || k === "sdk" || k === "mcp" || k === "sandbox")
? "npm i -g run402@latest"
: `call ${String(r.successor)} instead`;
return `${String(r.project_id)}: ${String(r.calls_7d)} call(s) in 7 days to ${String(r.legacy)} (${kinds.join(", ") || "unknown caller"}${fns.length ? `: ${fns.join(", ")}` : ""}); fix: ${fix}`;
});
checks.push({
name: "retiring_routes",
status: "warning",
value: { callers: retiring.length, routes: retiring, fixes },
hint: `Your projects still call routes that are being retired and will answer 410 once retired. ${fixes.join(" · ")}`,
});
}
}
// 6c. Org recovery posture: evidence levels, never guarantees.
if (wanted("recovery_posture")) {
const posture = status.recovery_posture;
if (!Array.isArray(posture)) {
checks.push({
name: "recovery_posture",
status: "skipped",
...(verbose && { hint: "account status has no 'recovery_posture' block; requires a vault-recovery-custody gateway." }),
});
}
else if (posture.length === 0) {
checks.push({ name: "recovery_posture", status: "ok", value: { orgs: [] } });
}
else {
const postureGaps = [];
for (const org of posture) {
const label = `org ${org.org_id} (${org.vault_count} vault${org.vault_count === 1 ? "" : "s"})`;
if (org.control_plane_configured === false) {
postureGaps.push(`${label}: no human owner with a working control-plane login — if this org's agent machine dies, nobody can sign in to recover it. Invite a backup human (run402 orgs invite create ${org.org_id} --email <their-email> --role owner) and have them complete login at console.run402.com.`);
}
if (org.source_backup_configured === false) {
postureGaps.push(`${label}: no human member holds a working source-access key — vault history has no member-side decryption backup. Have a member complete source enrollment at console.run402.com/account → Source access.`);
}
if (org.custody_legacy_present === true) {
postureGaps.push(`${label}: a member key is still on single-credential legacy custody (one passkey, no recovery code — losing that one credential loses source access). Re-enroll at console.run402.com/account to move to wrapper custody with a recovery code.`);
}
}
checks.push(postureGaps.length > 0
? {
name: "recovery_posture",
status: "warning",
value: { orgs: posture, gaps: postureGaps },
hint: "These are the org's disaster-recovery backstops — the same facts arrive as org_recovery_posture_degraded/_recovered feed events. After enrolling, export the recovery bundle (run402 repos recovery-bundle) and store it separately from the code.",
}
: { name: "recovery_posture", status: "ok", value: { orgs: posture } });
}
}
}
catch (err) {
// An unreachable account status is a soft skip for all three checks it feeds.
if (wanted("account_health"))
checks.push({
name: "account_health",
status: "skipped",
message: describeCheckFailure("account status check", err),
...(verbose && { hint: "GET /agent/v1/me/status not reachable." }),
});
if (wanted("runtime_staleness"))
checks.push({ name: "runtime_staleness", status: "skipped", message: describeCheckFailure("account status check", err) });
if (wanted("retiring_routes"))
checks.push({ name: "retiring_routes", status: "skipped", message: describeCheckFailure("account status check", err) });
if (wanted("recovery_posture"))
checks.push({ name: "recovery_posture", status: "skipped", message: describeCheckFailure("account status check", err) });
}
// 7. The vault: policy, whether THIS machine can produce the capture a
// `required` policy demands, open override journals, mirror currency, and
// where the keystore lives. Read-only and best-effort in every branch.
if (wanted("vault")) {
const daemonInfo = opts.daemonStatus ? await opts.daemonStatus().catch(() => ({ running: false })) : undefined;
const target = await resolveVaultTarget(r.repos, { repoDir: cwd, explicitProjectId: opts.project ?? undefined, ...(opts.warn ? { warn: opts.warn } : {}) });
const projectId = target.project_id ?? null;
const repoId = target.repo_id ?? null;
if (!projectId && !repoId) {
checks.push({
name: "vault",
status: "skipped",
...(verbose && { hint: "no active project — run 'run402 projects use <project_id>' to check its vault." }),
});
}
else {
try {
const gv = (await r.repos.status({
...(repoId ? { repo_id: repoId } : { project_id: projectId }),
repo_dir: cwd,
}));
const value = {
project_id: gv.project_id ?? projectId,
repo_id: gv.repo_id,
vault: gv.vault === null ? null : "allocated",
storage_profile: gv.vault?.storage_profile ?? null,
byo_destination: gv.vault?.byo_destination ?? null,
vault_policy: gv.vault_policy,
keystore_root: gv.keystore.root,
can_sign: gv.keystore.can_sign,
holds_repo_key: gv.keystore.holds_repo_key,
pending_overrides: gv.pending_overrides,
pins: gv.pins,
remote: gv.remote,
// null/<=1: the single-principal terminal-loss statement is honest;
// >=2: a second covering recipient is proven.
covering_recipients: gv.covering_recipients ?? null,
...(daemonInfo !== undefined ? { daemon: daemonInfo } : {}),
// This machine's standing on the chain-verified writer set; the
// read-only terminal state outranks it.
writer: gv.vault === null
? null
: gv.vault.read_only_terminal
? "read_only_vault"
: gv.vault.writer_set?.writers.some((w) => w.writer_key_id === gv.keystore.identity_fingerprint)
? "active"
: gv.vault.pending_writers?.some((p) => p.writer_key_id === gv.keystore.identity_fingerprint)
? "pending"
: "not_admitted",
};
const gaps = [];
if (value.writer === "read_only_vault") {
gaps.push("this vault has lost its last writer (D228 read-only terminal) — it still serves reads, but no push can be admitted until a new writer is admitted through a recovery path");
}
else if (value.writer === "pending") {
gaps.push("this machine's key is an eligible writer candidate but not yet admitted — run 'run402 repos access sync' if you already hold writer standing on this vault, or ask a current writer to run any vault operation");
}
else if (value.writer === "not_admitted") {
gaps.push("this machine's key is not an active writer on this vault — a push from here is refused VAULT_WRITER_NOT_ADMITTED; ask a current writer to admit you (org membership at role developer+ and a published signing key make you eligible)");
}
if (gv.vault_policy === "required" && !gv.keystore.holds_repo_key) {
gaps.push("vault_policy is 'required' but this machine holds no key for the vault — a deploy from here is refused with VAULT_CLIENT_UPGRADE_REQUIRED. " +
"Run 'run402 repos create --project <project_id>' (idempotent; resolves to the existing repo), or 'run402 repos policy grandfathered --reason <why>' to un-gate the project.");
}
else if (gv.vault_policy === "required" && !gv.keystore.can_sign) {
gaps.push("vault_policy is 'required' and this keystore is read-only (no signing key) — it can verify but cannot publish the capture a deploy needs");
}
if (gv.pending_overrides > 0) {
gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 repos capture' to drain them`);
}
// `matches` is tri-state: only `false` is a mismatch.
if (gv.remote && gv.remote.matches === false) {
gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${value.project_id} (${gv.remote.url})`);
}
for (const w of gv.warnings ?? [])
gaps.push(`${w.kind}: ${w.message}`);
// Mirror currency, alongside (never in place of) the deploy gaps; only
// STALE becomes a gap.
if (gv.vault !== null && value.repo_id) {
try {
const mirrorStatus = (await r.repos.mirrorStatus({ repo_id: value.repo_id, is_byo: value.storage_profile === "byo" }));
value.vault_mirror = {
configured: mirrorStatus.configured,
destination: mirrorStatus.destination,
mirrored_generation: mirrorStatus.mirrored_generation,
newest_generation: mirrorStatus.newest_generation,
is_current: mirrorStatus.is_current,
last_success_at: mirrorStatus.last_success_at,
finding: mirrorStatus.finding,
validity_not_freshness: mirrorStatus.validity_not_freshness,
keystore_still_required: mirrorStatus.keystore_still_required,
};
if (mirrorStatus.is_current === false) {
gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
}
}
catch {
// A mirror read failing is never a doctor failure.
}
}
const byoDisclosure = value.storage_profile === "byo" ? ` Storage: byo (${value.byo_destination ?? "(unknown)"}) — ${VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT}` : "";
checks.push({
name: "vault",
status: gaps.length > 0 ? "warning" : "ok",
value: gaps.length > 0 ? { ...value, gaps } : value,
hint: (gv.vault === null
? `No vault for this project (that is a normal shape). Allocate one with 'run402 repos create --project <project_id>'. Keystore: ${gv.keystore.root}`
: gv.durability_statement
? `Back up ${gv.keystore.root} anyway — ${gv.durability_statement} (covering_recipients: ${gv.covering_recipients})`
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`) + byoDisclosure,
});
}
catch (err) {
// A gateway without vaults, an unreachable API, or an unreadable
// project is not a local health problem.
checks.push({ name: "vault", status: "skipped", message: describeCheckFailure("vault status check", err) });
}
}
}
// 8. Source scan, scoped to the selected application. Error findings block
// deploy; an explicit arbitrary scan directory is advisory only.
if (!opts.noScan && wanted("source_scan")) {
const scanDirOverride = opts.scanDir ?? null;
try {
const scope = await resolveApplicationScope({ ...(opts.cwd ? { cwd: opts.cwd } : {}), dir: opts.dir ?? undefined, manifest: opts.manifest ?? undefined });
if (!scanDirOverride && !scope.selected) {
checks.push({ name: "source_scan", status: "skipped", value: { scope: "unscoped", app_root: scope.app_root }, message: "No application selected. Run doctor --dir <app> or --manifest <path>; sibling applications are not deployment blockers." });
}
else {
const scanRoot = scanDirOverride ?? resolveScanRoot(scope.app_root);
const scanContext = { scope: scanDirOverride ? "explicit_scan_directory" : "application", app_root: scope.app_root, manifest_path: scope.manifest_path };
let findings;
if (scanDirOverride)
findings = scanSourceTree(scanRoot, { cwd: scope.app_root });
else {
const selected = await loadApplicationScanInput(scope.manifest_path);
scanContext.build_outputs = selected.build_deferred ? "deferred_until_build" : "not_deferred";
findings = scanDeploymentSources(selected.spec, scope.app_root).findings;
}
const errorFindings = findings.filter((f) => f.severity === SCAN_SEVERITY.ERROR);
const warnFindings = findings.filter((f) => f.severity === SCAN_SEVERITY.WARN);
if (findings.length === 0) {
checks.push({ name: "source_scan", status: "ok", value: { ...scanContext, scan_root: scanRoot, file_count_with_findings: 0 } });
}
else {
checks.push({
name: "source_scan",
status: errorFindings.length > 0 ? "error" : "warning",
...(scanDirOverride ? { severity: "advisory" } : {}),
value: {
...scanContext,
scan_root: scanRoot,
findings: errorFindings.length + warnFindings.length,
errors: errorFindings.length,
warnings: warnFindings.length,
details: findings,
},
hint: errorFindings.length > 0
? scanDirOverride ? "Findings are from the explicit arbitrary scan directory; they do not establish that an application deploy will be refused." : "Fix the findings in this application. The same scoped source scan gates up and deploy."
: "Source scan emitted warnings (non-blocking). Review and address when convenient.",
});
}
}
}
catch (err) {
checks.push({
name: "source_scan",
status: "error",
value: { code: err?.code ?? "APPLICATION_SCAN_FAILED", details: err?.details ?? null },
message: message(err),
});
}
}
return buildDoctorReport(checks);
}
//# sourceMappingURL=doctor.js.map