@haven_ai/connect 0.0.0-dev.202609181713.990ba70 → 0.0.0-dev.202609181927.cd4620a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -4
- package/dist/cli.cjs +164 -64
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +164 -64
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +164 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +164 -64
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -87,13 +87,39 @@ every credential directory on the machine and classifies each one:
|
|
|
87
87
|
| `retired` | Tombstoned (see below); key material removed. |
|
|
88
88
|
| `orphaned` | No usable identity and no tombstone. |
|
|
89
89
|
|
|
90
|
-
The exit code is non-zero if **any** wired agent fails **any** check
|
|
90
|
+
The exit code is non-zero if **any** wired agent fails **any** check
|
|
91
|
+
(`level: failed` — see the next section; an advisory does not count) — not
|
|
91
92
|
only the one the report's main section describes. In `--json`, the same
|
|
92
93
|
information is on `agents[]`, each entry carrying `slug`, `agentId`,
|
|
93
94
|
`directory`, `classification` and its own `checks[]`; the flat `checks` array
|
|
94
95
|
is retained and still describes one agent, so a single-agent install reads as
|
|
95
96
|
it always did.
|
|
96
97
|
|
|
98
|
+
### Three verdicts, not two (#3121)
|
|
99
|
+
|
|
100
|
+
Every check carries a `level` — `ok`, `advisory` or `failed` — and the report
|
|
101
|
+
carries the rolled-up `level` over the flat checks and every wired agent's
|
|
102
|
+
checks. Only `failed` reaches the exit code; an advisory is printed with a `!`
|
|
103
|
+
marker, gets its own summary line ("No failures. N advisory finding(s)") and
|
|
104
|
+
exits 0. `ok` on a check and on the report is kept for `--json` consumers and
|
|
105
|
+
means "nothing is broken": it is `true` for `ok` and `advisory`, `false` only
|
|
106
|
+
for `failed`, so `report.ok` is always the exit code's predicate. The doctor
|
|
107
|
+
report stays `version: 1`; `level` is additive.
|
|
108
|
+
|
|
109
|
+
| Level | Meaning | Example |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `ok` | Nothing to say. | The installed signer matches the connector's pin. |
|
|
112
|
+
| `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered). `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
|
|
113
|
+
| `failed` | A real failure with one repair action. Exit 1. | A stale or empty runtime directory; a live key in a directory the runtime's config demonstrably does not use. |
|
|
114
|
+
|
|
115
|
+
What stays blocking is live spend authority: a directory whose stored key
|
|
116
|
+
still authenticates and whose classification is not `wired` fails the run, for
|
|
117
|
+
every non-wired classification, whenever the runtime config could be read. The
|
|
118
|
+
classification itself does not change on Claude Code — only the severity of
|
|
119
|
+
the check that reads it — so `agents[]` reads as before. A runtime string the
|
|
120
|
+
connector does not recognise (`--runtime codex-clii`) is not "no config": it
|
|
121
|
+
fails `runtime_config` naming the allowed values, and demotes nothing.
|
|
122
|
+
|
|
97
123
|
One check is worth calling out: **`identity_match`** compares the agent the
|
|
98
124
|
stored API key actually authenticates as against the `delegate_address` in
|
|
99
125
|
that directory's `signer.json`. A mismatch means the runtime would quote as
|
|
@@ -496,9 +522,9 @@ the agent credential files, the pinned signer runtime install (and, since
|
|
|
496
522
|
see the last section of this file), the hosted MCP
|
|
497
523
|
(authorized `tools/list`), and starts the local signer for a real stdio
|
|
498
524
|
handshake — reporting its advertised compat versions. Every failing check
|
|
499
|
-
prints one concrete repair action; the exit code is non-zero on any failure
|
|
500
|
-
|
|
501
|
-
printed.
|
|
525
|
+
prints one concrete repair action; the exit code is non-zero on any failure
|
|
526
|
+
and zero on an advisory (see "Three verdicts, not two" above). Add `--json`
|
|
527
|
+
for a machine-readable report. No secret material is ever printed.
|
|
502
528
|
|
|
503
529
|
`--doctor` also probes every OTHER agent credential directory it did not
|
|
504
530
|
select (#1688). A re-run of setup mints a NEW agent and, unless it ran with
|
package/dist/cli.cjs
CHANGED
|
@@ -533,6 +533,16 @@ async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
|
533
533
|
await restrictPermissions(path$1, 384, warn);
|
|
534
534
|
return path$1;
|
|
535
535
|
}
|
|
536
|
+
async function readConnectOutcomeRuntime(directory) {
|
|
537
|
+
try {
|
|
538
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, CONNECT_OUTCOME_FILENAME), "utf8"));
|
|
539
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
540
|
+
const runtime = parsed.runtime;
|
|
541
|
+
return typeof runtime === "string" && runtime.length > 0 ? runtime : null;
|
|
542
|
+
} catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
536
546
|
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
|
|
537
547
|
var init_storage = __esm({
|
|
538
548
|
"src/storage.ts"() {
|
|
@@ -557,9 +567,9 @@ var init_runtime_manifest = __esm({
|
|
|
557
567
|
mcpPackage: "@haven_ai/mcp",
|
|
558
568
|
mcpVersion: mcp.MCP_VERSION,
|
|
559
569
|
sdkPackage: "@haven_ai/sdk",
|
|
560
|
-
sdkVersion: "0.0.0-dev.
|
|
570
|
+
sdkVersion: "0.0.0-dev.202609181927.cd4620a",
|
|
561
571
|
signerPackage: "@haven_ai/signer",
|
|
562
|
-
signerVersion: "0.0.0-dev.
|
|
572
|
+
signerVersion: "0.0.0-dev.202609181927.cd4620a",
|
|
563
573
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
564
574
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
565
575
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -3517,9 +3527,32 @@ var init_rekey_restart = __esm({
|
|
|
3517
3527
|
var doctor_exports = {};
|
|
3518
3528
|
__export(doctor_exports, {
|
|
3519
3529
|
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
3530
|
+
rollUpLevel: () => rollUpLevel,
|
|
3520
3531
|
runDoctor: () => runDoctor,
|
|
3521
3532
|
runRepair: () => runRepair
|
|
3522
3533
|
});
|
|
3534
|
+
function finalizeCheck(check) {
|
|
3535
|
+
return { ...check, ok: check.level !== "failed" };
|
|
3536
|
+
}
|
|
3537
|
+
function rollUpLevel(checks) {
|
|
3538
|
+
if (checks.some((check) => check.level === "failed")) return "failed";
|
|
3539
|
+
if (checks.some((check) => check.level === "advisory")) return "advisory";
|
|
3540
|
+
return "ok";
|
|
3541
|
+
}
|
|
3542
|
+
function runtimeFlagFor(runtime) {
|
|
3543
|
+
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
3544
|
+
}
|
|
3545
|
+
async function resolveDoctorRuntime(input, directory) {
|
|
3546
|
+
const explicit = input.runtime.trim();
|
|
3547
|
+
if (explicit) return { runtime: explicit, origin: "flag" };
|
|
3548
|
+
if (directory) {
|
|
3549
|
+
const recorded = await readConnectOutcomeRuntime(directory);
|
|
3550
|
+
if (recorded !== null && normalizeRuntimeName(recorded)) {
|
|
3551
|
+
return { runtime: recorded, origin: "record" };
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return { runtime: "", origin: "unknown" };
|
|
3555
|
+
}
|
|
3523
3556
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
3524
3557
|
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
3525
3558
|
let entries = [];
|
|
@@ -3580,13 +3613,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
|
|
|
3580
3613
|
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3581
3614
|
}
|
|
3582
3615
|
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3616
|
+
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
3617
|
+
}
|
|
3618
|
+
function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
|
|
3583
3619
|
const label = "Pending re-key";
|
|
3584
3620
|
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3585
3621
|
if (status.state === "unreadable") {
|
|
3586
3622
|
return {
|
|
3587
3623
|
id: "rekey_pending",
|
|
3588
3624
|
label,
|
|
3589
|
-
|
|
3625
|
+
level: "failed",
|
|
3590
3626
|
detail: `A re-key was started here but ${status.path} does not parse, so neither the address it generated nor when it started can be read. The file still holds what was a private key.`,
|
|
3591
3627
|
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3592
3628
|
};
|
|
@@ -3598,9 +3634,9 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3598
3634
|
return {
|
|
3599
3635
|
id: "rekey_pending",
|
|
3600
3636
|
label,
|
|
3601
|
-
|
|
3637
|
+
level: "failed",
|
|
3602
3638
|
detail: `A re-key started ${started} has COMPLETED on Haven \u2014 the agent's signing address is already ${address}, the one this machine generated \u2014 but the local half was never finished, so the credential files here still hold the old key. Parked at ${status.path}.` + (status.state === "expired" ? " The local file is also past its 24h TTL, which --rekey-finish refuses, so the finish command below will not accept it any more." : ""),
|
|
3603
|
-
repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you
|
|
3639
|
+
repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you>${runtimeFlagFor(runtime)}`
|
|
3604
3640
|
};
|
|
3605
3641
|
}
|
|
3606
3642
|
const wedgeNote = "Haven is NOT yet on this address, so the re-key did not complete. This machine cannot tell whether the on-chain revoke on the agent page already ran: if it did not, closing this costs nothing; if it did, the agent's old delegations are revoked, no new ones were issued, and only an owner re-grant restores its spend authority (#1868). Check the agent page before assuming the harmless case.";
|
|
@@ -3608,7 +3644,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3608
3644
|
return {
|
|
3609
3645
|
id: "rekey_pending",
|
|
3610
3646
|
label,
|
|
3611
|
-
|
|
3647
|
+
level: "failed",
|
|
3612
3648
|
detail: `A re-key started ${started} EXPIRED ${status.expiresAt ?? ""} without being finished. Its address was ${address}; the private half it generated is still on disk at ${status.path}. ` + wedgeNote,
|
|
3613
3649
|
repair: `Either delete ${status.path} to drop the parked key, or start over: ${RERUN} --rekey${nameFlag}. Connect never deletes it for you \u2014 an expired TTL is a refusal to USE the key, not a licence to destroy key material you may still be mid-flow on.`
|
|
3614
3650
|
};
|
|
@@ -3616,7 +3652,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3616
3652
|
return {
|
|
3617
3653
|
id: "rekey_pending",
|
|
3618
3654
|
label,
|
|
3619
|
-
|
|
3655
|
+
level: "ok",
|
|
3620
3656
|
detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
|
|
3621
3657
|
};
|
|
3622
3658
|
}
|
|
@@ -3644,13 +3680,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
|
3644
3680
|
if (shell) facts.push(shell);
|
|
3645
3681
|
if (facts.length === 0) return void 0;
|
|
3646
3682
|
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3647
|
-
return {
|
|
3683
|
+
return finalizeCheck({
|
|
3648
3684
|
id: "runtime_spec_override",
|
|
3649
3685
|
label: "Runtime spec override",
|
|
3650
|
-
|
|
3686
|
+
level: "failed",
|
|
3651
3687
|
detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
|
|
3652
3688
|
repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>. If the override is intentional, this finding is the record of it.`
|
|
3653
|
-
};
|
|
3689
|
+
});
|
|
3654
3690
|
}
|
|
3655
3691
|
async function readMcpSidecarOverride(directory) {
|
|
3656
3692
|
try {
|
|
@@ -3677,6 +3713,13 @@ function describeAccountAddressKey(identity, signerFile) {
|
|
|
3677
3713
|
return "no account address stored";
|
|
3678
3714
|
}
|
|
3679
3715
|
async function checksForAgent(entry, input, deps) {
|
|
3716
|
+
const verdicts = await verdictsForAgent(entry, input, deps);
|
|
3717
|
+
return {
|
|
3718
|
+
checks: verdicts.checks.map(finalizeCheck),
|
|
3719
|
+
...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
async function verdictsForAgent(entry, input, deps) {
|
|
3680
3723
|
const { directory, identity, sidecar } = entry;
|
|
3681
3724
|
const checks = [];
|
|
3682
3725
|
let signerCapabilities;
|
|
@@ -3691,7 +3734,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3691
3734
|
checks.push({
|
|
3692
3735
|
id: "credentials",
|
|
3693
3736
|
label: "Agent credentials",
|
|
3694
|
-
ok:
|
|
3737
|
+
level: credentialsOk ? "ok" : "failed",
|
|
3695
3738
|
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"}; ${describeAccountAddressKey(identity, signerFile)})` : "identity.json or signer.json is missing or unparseable.",
|
|
3696
3739
|
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
3697
3740
|
});
|
|
@@ -3699,9 +3742,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3699
3742
|
checks.push({
|
|
3700
3743
|
id: "signer_runtime",
|
|
3701
3744
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3702
|
-
|
|
3745
|
+
level: "failed",
|
|
3703
3746
|
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3704
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
3747
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3705
3748
|
});
|
|
3706
3749
|
} else if (sidecar.runtime_spec_override) {
|
|
3707
3750
|
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3711,9 +3754,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3711
3754
|
checks.push({
|
|
3712
3755
|
id: "signer_runtime",
|
|
3713
3756
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3714
|
-
ok:
|
|
3757
|
+
level: matches ? "ok" : "failed",
|
|
3715
3758
|
detail: matches ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory} (override install \u2014 see runtime_spec_override)` : `Override runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
3716
|
-
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
3759
|
+
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
|
|
3717
3760
|
});
|
|
3718
3761
|
} else {
|
|
3719
3762
|
const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3725,9 +3768,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3725
3768
|
checks.push({
|
|
3726
3769
|
id: "signer_runtime",
|
|
3727
3770
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3728
|
-
ok,
|
|
3771
|
+
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
3729
3772
|
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : intact ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion} \u2014 intact, but outdated.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
3730
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
3773
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3731
3774
|
});
|
|
3732
3775
|
}
|
|
3733
3776
|
const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
|
|
@@ -3738,7 +3781,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3738
3781
|
checks.push({
|
|
3739
3782
|
id: "hosted_mcp",
|
|
3740
3783
|
label: "Hosted Haven MCP",
|
|
3741
|
-
|
|
3784
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3742
3785
|
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3743
3786
|
...probe.status === "ok" ? {} : {
|
|
3744
3787
|
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
@@ -3748,7 +3791,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3748
3791
|
checks.push({
|
|
3749
3792
|
id: "hosted_mcp",
|
|
3750
3793
|
label: "Hosted Haven MCP",
|
|
3751
|
-
|
|
3794
|
+
level: "failed",
|
|
3752
3795
|
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3753
3796
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3754
3797
|
});
|
|
@@ -3766,15 +3809,15 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3766
3809
|
checks.push({
|
|
3767
3810
|
id: "identity_match",
|
|
3768
3811
|
label: "Hosted identity matches the local signing key",
|
|
3769
|
-
|
|
3812
|
+
level: "failed",
|
|
3770
3813
|
detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
|
|
3771
|
-
repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor
|
|
3814
|
+
repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor${runtimeFlagFor(input.runtime)}`
|
|
3772
3815
|
});
|
|
3773
3816
|
} else if (!localDelegate) {
|
|
3774
3817
|
checks.push({
|
|
3775
3818
|
id: "identity_match",
|
|
3776
3819
|
label: "Hosted identity matches the local signing key",
|
|
3777
|
-
|
|
3820
|
+
level: "failed",
|
|
3778
3821
|
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3779
3822
|
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3780
3823
|
});
|
|
@@ -3783,7 +3826,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3783
3826
|
checks.push({
|
|
3784
3827
|
id: "identity_match",
|
|
3785
3828
|
label: "Hosted identity matches the local signing key",
|
|
3786
|
-
ok:
|
|
3829
|
+
level: same ? "ok" : "failed",
|
|
3787
3830
|
detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
|
|
3788
3831
|
...same ? {} : {
|
|
3789
3832
|
repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
|
|
@@ -3801,7 +3844,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3801
3844
|
checks.push({
|
|
3802
3845
|
id: "signer_process",
|
|
3803
3846
|
label: "Signer stdio handshake",
|
|
3804
|
-
|
|
3847
|
+
level: "failed",
|
|
3805
3848
|
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3806
3849
|
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
3807
3850
|
});
|
|
@@ -3818,18 +3861,18 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3818
3861
|
checks.push({
|
|
3819
3862
|
id: "signer_process",
|
|
3820
3863
|
label: "Signer stdio handshake",
|
|
3821
|
-
|
|
3864
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3822
3865
|
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
3823
|
-
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
3866
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3824
3867
|
});
|
|
3825
3868
|
}
|
|
3826
3869
|
} else {
|
|
3827
3870
|
checks.push({
|
|
3828
3871
|
id: "signer_process",
|
|
3829
3872
|
label: "Signer stdio handshake",
|
|
3830
|
-
|
|
3873
|
+
level: "failed",
|
|
3831
3874
|
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3832
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
3875
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3833
3876
|
});
|
|
3834
3877
|
}
|
|
3835
3878
|
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
@@ -3839,7 +3882,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3839
3882
|
const checks = [];
|
|
3840
3883
|
let signerCapabilities;
|
|
3841
3884
|
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3842
|
-
const
|
|
3885
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
3886
|
+
const runtime = resolution.runtime;
|
|
3887
|
+
const input2 = { ...input, runtime };
|
|
3888
|
+
const normalizedRuntime = normalizeRuntimeName(input2.runtime);
|
|
3889
|
+
const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
|
|
3843
3890
|
let configText = null;
|
|
3844
3891
|
if (configPath !== null) {
|
|
3845
3892
|
try {
|
|
@@ -3876,7 +3923,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3876
3923
|
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
3877
3924
|
// a retired directory that also holds a parked key stays `retired`.
|
|
3878
3925
|
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
3879
|
-
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0,
|
|
3926
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
|
|
3880
3927
|
...rekeyPending ? { rekeyPending } : {}
|
|
3881
3928
|
});
|
|
3882
3929
|
continue;
|
|
@@ -3891,11 +3938,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3891
3938
|
...rekeyPending ? { rekeyPending } : {}
|
|
3892
3939
|
};
|
|
3893
3940
|
if (wired) {
|
|
3894
|
-
const result = await checksForAgent({ directory: dir, identity, sidecar },
|
|
3941
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
|
|
3895
3942
|
entry.checks = result.checks;
|
|
3896
3943
|
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
3897
3944
|
} else if (rekeyPending) {
|
|
3898
|
-
entry.checks = [rekeyPendingCheck(rekeyPending, void 0,
|
|
3945
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
|
|
3899
3946
|
}
|
|
3900
3947
|
inventory.push(entry);
|
|
3901
3948
|
}
|
|
@@ -3910,7 +3957,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3910
3957
|
checks.push({
|
|
3911
3958
|
id: "credentials",
|
|
3912
3959
|
label: "Agent credentials",
|
|
3913
|
-
|
|
3960
|
+
level: "failed",
|
|
3914
3961
|
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3915
3962
|
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3916
3963
|
});
|
|
@@ -3920,7 +3967,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3920
3967
|
if (!primaryChecksById.has("credentials")) {
|
|
3921
3968
|
const result = await checksForAgent(
|
|
3922
3969
|
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
3923
|
-
|
|
3970
|
+
input2,
|
|
3924
3971
|
deps
|
|
3925
3972
|
);
|
|
3926
3973
|
signerCapabilities = result.signerCapabilities;
|
|
@@ -3931,20 +3978,37 @@ async function runDoctor(input, deps = {}) {
|
|
|
3931
3978
|
if (check) checks.push(check);
|
|
3932
3979
|
}
|
|
3933
3980
|
}
|
|
3934
|
-
|
|
3981
|
+
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
3982
|
+
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
3935
3983
|
checks.push({
|
|
3936
3984
|
id: "runtime_config",
|
|
3937
3985
|
label: "Runtime MCP config",
|
|
3938
|
-
|
|
3939
|
-
detail: `Runtime '${
|
|
3986
|
+
level: "failed",
|
|
3987
|
+
detail: `Runtime '${input2.runtime}' is not one the connector recognises. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
|
|
3988
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
3989
|
+
});
|
|
3990
|
+
} else if (configPath === null && input2.runtime === "") {
|
|
3991
|
+
checks.push({
|
|
3992
|
+
id: "runtime_config",
|
|
3993
|
+
label: "Runtime MCP config",
|
|
3994
|
+
level: "failed",
|
|
3995
|
+
detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? input.credentialsDir ?? "~/.haven/agents"} carries no resolvable ${CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
|
|
3996
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
3997
|
+
});
|
|
3998
|
+
} else if (configPath === null) {
|
|
3999
|
+
checks.push({
|
|
4000
|
+
id: "runtime_config",
|
|
4001
|
+
label: "Runtime MCP config",
|
|
4002
|
+
level: "ok",
|
|
4003
|
+
detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.`
|
|
3940
4004
|
});
|
|
3941
4005
|
} else if (configText === null) {
|
|
3942
4006
|
checks.push({
|
|
3943
4007
|
id: "runtime_config",
|
|
3944
4008
|
label: "Runtime MCP config",
|
|
3945
|
-
|
|
4009
|
+
level: "failed",
|
|
3946
4010
|
detail: `No runtime config at ${configPath}.`,
|
|
3947
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4011
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
|
|
3948
4012
|
});
|
|
3949
4013
|
} else {
|
|
3950
4014
|
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
@@ -3956,9 +4020,9 @@ async function runDoctor(input, deps = {}) {
|
|
|
3956
4020
|
checks.push({
|
|
3957
4021
|
id: "runtime_config",
|
|
3958
4022
|
label: "Runtime MCP config",
|
|
3959
|
-
ok,
|
|
4023
|
+
level: ok ? "ok" : "failed",
|
|
3960
4024
|
detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
3961
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4025
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
|
|
3962
4026
|
});
|
|
3963
4027
|
}
|
|
3964
4028
|
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
@@ -4005,13 +4069,17 @@ async function runDoctor(input, deps = {}) {
|
|
|
4005
4069
|
);
|
|
4006
4070
|
}
|
|
4007
4071
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
4072
|
+
const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
|
|
4073
|
+
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
4008
4074
|
checks.push({
|
|
4009
4075
|
id: "superseded_agents",
|
|
4010
4076
|
label: "Superseded agent credentials",
|
|
4011
|
-
|
|
4012
|
-
detail:
|
|
4013
|
-
...
|
|
4077
|
+
level: supersededLevel,
|
|
4078
|
+
detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
4079
|
+
...supersededLevel === "failed" ? {
|
|
4014
4080
|
repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
4081
|
+
} : supersededLevel === "advisory" ? {
|
|
4082
|
+
repair: `Check ${supersededLive.join(", ")} on the Haven agent page: revoke the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
4015
4083
|
} : {}
|
|
4016
4084
|
});
|
|
4017
4085
|
}
|
|
@@ -4022,7 +4090,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
4022
4090
|
checks.push({
|
|
4023
4091
|
id: "rekey_pending_elsewhere",
|
|
4024
4092
|
label: "Parked re-keys in other credential directories",
|
|
4025
|
-
|
|
4093
|
+
level: abandoned.length === 0 ? "ok" : "failed",
|
|
4026
4094
|
detail: abandoned.length > 0 ? `ABANDONED re-key key material outside the agent this report describes: ${abandoned.map(describe).join(", ")}. Each holds a private key that was generated for a re-key nobody finished.` : `${parkedElsewhere.length} other director(y/ies) hold an open pending re-key: ${parkedElsewhere.map(describe).join(", ")}.`,
|
|
4027
4095
|
...abandoned.length > 0 ? {
|
|
4028
4096
|
repair: "Check the Haven agent page for each before deleting: if its on-chain revoke already ran, the agent has no spend authority until you re-grant it (#1868), and that is not visible from this machine. Connect never deletes key material for you."
|
|
@@ -4031,20 +4099,23 @@ async function runDoctor(input, deps = {}) {
|
|
|
4031
4099
|
}
|
|
4032
4100
|
const signerProcess = primaryChecksById.get("signer_process");
|
|
4033
4101
|
if (signerProcess) checks.push(signerProcess);
|
|
4034
|
-
const restart = restartRequiredForRuntime(
|
|
4102
|
+
const restart = restartRequiredForRuntime(input2.runtime, deps.env);
|
|
4035
4103
|
checks.push({
|
|
4036
4104
|
id: "restart",
|
|
4037
4105
|
label: "Runtime restart",
|
|
4038
|
-
|
|
4039
|
-
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
|
|
4106
|
+
level: "ok",
|
|
4107
|
+
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : input2.runtime === "" ? "Runtime is unknown \u2014 whether a restart is needed cannot be determined. Re-run the doctor naming the runtime for a definitive answer." : "No restart requirement known for this runtime."
|
|
4040
4108
|
});
|
|
4041
|
-
const
|
|
4109
|
+
const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
|
|
4110
|
+
const finalChecks = checks.map(finalizeCheck);
|
|
4111
|
+
const level = rollUpLevel([...finalChecks, ...wiredChecks]);
|
|
4042
4112
|
return {
|
|
4043
4113
|
version: 1,
|
|
4044
|
-
ok:
|
|
4045
|
-
|
|
4114
|
+
ok: level !== "failed",
|
|
4115
|
+
level,
|
|
4116
|
+
runtime: input2.runtime,
|
|
4046
4117
|
credentialDirectory: primaryDirectory,
|
|
4047
|
-
checks,
|
|
4118
|
+
checks: finalChecks,
|
|
4048
4119
|
agents: inventory,
|
|
4049
4120
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
4050
4121
|
};
|
|
@@ -4062,6 +4133,21 @@ async function runRepair(input, deps = {}) {
|
|
|
4062
4133
|
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
4063
4134
|
};
|
|
4064
4135
|
}
|
|
4136
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4137
|
+
if (resolution.origin === "record") {
|
|
4138
|
+
messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${path.join(directory, CONNECT_OUTCOME_FILENAME)}.`);
|
|
4139
|
+
}
|
|
4140
|
+
const runtime = resolution.runtime;
|
|
4141
|
+
const input2 = { ...input, runtime };
|
|
4142
|
+
if (runtime === "") {
|
|
4143
|
+
return {
|
|
4144
|
+
ok: false,
|
|
4145
|
+
messages: [
|
|
4146
|
+
"Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
|
|
4147
|
+
`Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4148
|
+
]
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4065
4151
|
let identity;
|
|
4066
4152
|
try {
|
|
4067
4153
|
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
@@ -4071,7 +4157,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4071
4157
|
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
4072
4158
|
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
4073
4159
|
}
|
|
4074
|
-
const configPath = runtimeConfigPathFor(
|
|
4160
|
+
const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
|
|
4075
4161
|
if (configPath) {
|
|
4076
4162
|
try {
|
|
4077
4163
|
const existing = await promises.readFile(configPath, "utf8");
|
|
@@ -4098,7 +4184,10 @@ async function runRepair(input, deps = {}) {
|
|
|
4098
4184
|
const names = serverNamesFor(serverName);
|
|
4099
4185
|
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
4100
4186
|
const configResult = await writeRuntimeConfig({
|
|
4101
|
-
|
|
4187
|
+
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
4188
|
+
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
4189
|
+
// — a repair that reported success while writing nothing.
|
|
4190
|
+
runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
|
|
4102
4191
|
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
4103
4192
|
apiKey: identity.api_key,
|
|
4104
4193
|
identityPath: path.join(directory, "identity.json"),
|
|
@@ -4429,7 +4518,7 @@ async function pathExists2(path) {
|
|
|
4429
4518
|
init_unwire();
|
|
4430
4519
|
init_local_mcp_runtime();
|
|
4431
4520
|
init_runtime_manifest();
|
|
4432
|
-
var CONNECTOR_VERSION = "0.0.0-dev.
|
|
4521
|
+
var CONNECTOR_VERSION = "0.0.0-dev.202609181927.cd4620a";
|
|
4433
4522
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4434
4523
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4435
4524
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -5327,7 +5416,7 @@ function helpText() {
|
|
|
5327
5416
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
5328
5417
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
5329
5418
|
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
5330
|
-
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on
|
|
5419
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
|
|
5331
5420
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
5332
5421
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
5333
5422
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
@@ -5393,6 +5482,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
|
|
|
5393
5482
|
}
|
|
5394
5483
|
return 1;
|
|
5395
5484
|
}
|
|
5485
|
+
function levelMarker(level) {
|
|
5486
|
+
return level === "ok" ? "\u2713" : level === "advisory" ? "!" : "\u2717";
|
|
5487
|
+
}
|
|
5488
|
+
function advisoryCount(report) {
|
|
5489
|
+
const others = report.agents.filter((agent) => agent.classification === "wired" && agent.directory !== report.credentialDirectory).flatMap((agent) => agent.checks);
|
|
5490
|
+
return [...report.checks, ...others].filter((check) => check.level === "advisory").length;
|
|
5491
|
+
}
|
|
5396
5492
|
async function runCli(argv, io = {
|
|
5397
5493
|
stdout: (message) => process.stdout.write(message),
|
|
5398
5494
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -5599,7 +5695,7 @@ async function runCli(argv, io = {
|
|
|
5599
5695
|
`);
|
|
5600
5696
|
} else {
|
|
5601
5697
|
for (const check of report.checks) {
|
|
5602
|
-
io.stdout(redactSecrets(`${check.
|
|
5698
|
+
io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5603
5699
|
`));
|
|
5604
5700
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5605
5701
|
`));
|
|
@@ -5609,21 +5705,25 @@ async function runCli(argv, io = {
|
|
|
5609
5705
|
io.stdout("\nOther agents on this machine:\n");
|
|
5610
5706
|
for (const agent of otherAgents) {
|
|
5611
5707
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
5612
|
-
const failed = agent.checks.filter((check) =>
|
|
5613
|
-
const
|
|
5614
|
-
|
|
5708
|
+
const failed = agent.checks.filter((check) => check.level === "failed");
|
|
5709
|
+
const advised = agent.checks.filter((check) => check.level === "advisory");
|
|
5710
|
+
const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
|
|
5711
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
|
|
5615
5712
|
`));
|
|
5616
|
-
for (const check of failed) {
|
|
5617
|
-
io.stdout(redactSecrets(`
|
|
5713
|
+
for (const check of [...failed, ...advised]) {
|
|
5714
|
+
io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5618
5715
|
`));
|
|
5619
5716
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5620
5717
|
`));
|
|
5621
5718
|
}
|
|
5622
5719
|
}
|
|
5623
5720
|
}
|
|
5624
|
-
io.stdout(
|
|
5721
|
+
io.stdout(
|
|
5722
|
+
report.level === "failed" ? "One or more checks FAILED \u2014 see repairs above.\n" : report.level === "advisory" ? `No failures. ${advisoryCount(report)} advisory finding(s) \u2014 see the ! line(s) above.
|
|
5723
|
+
` : "All checks passed.\n"
|
|
5724
|
+
);
|
|
5625
5725
|
}
|
|
5626
|
-
return report.
|
|
5726
|
+
return report.level === "failed" ? 1 : 0;
|
|
5627
5727
|
} catch (err) {
|
|
5628
5728
|
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5629
5729
|
code: "doctor_failed",
|