@brainervirus/workit-core 0.8.7 → 0.8.8
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/package.json +1 -1
- package/scripts/doctor-check.ts +16 -0
- package/scripts/install-cursor-plugin.sh +16 -0
- package/src/core/doctor.ts +239 -18
package/package.json
CHANGED
package/scripts/doctor-check.ts
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
// return nonzero when a required registration/config check fails. `installer:
|
|
4
4
|
// true` limits the enforced checks to the ones the installer just guaranteed, so
|
|
5
5
|
// a dev/stub checkout without built assets does not fail a post-install run.
|
|
6
|
+
// CA-02: `cursor --stale` is the installer's self-heal pre-check — it exits 2
|
|
7
|
+
// when the doctor's stale_install check fails and 0 otherwise. A
|
|
8
|
+
// registry-unreachable comparison warns as registry_unreachable and is NOT
|
|
9
|
+
// stale (CA-04): no false stale_install and no install failure. Other check
|
|
10
|
+
// failures are left to the post-install gate.
|
|
6
11
|
import { runDoctor } from "../src/core/doctor";
|
|
7
12
|
|
|
8
13
|
const host = process.argv[2] === "cursor" ? "cursor" : "opencode";
|
|
14
|
+
const staleOnly = process.argv.includes("--stale");
|
|
9
15
|
const report = runDoctor({ host, installer: true });
|
|
10
16
|
|
|
17
|
+
if (staleOnly) {
|
|
18
|
+
const stale = report.checks.find((c) => c.id === "stale_install");
|
|
19
|
+
if (stale?.status === "fail") {
|
|
20
|
+
process.stderr.write(`STALE: ${stale.detail}\n`);
|
|
21
|
+
if (stale.fix) process.stderr.write(` fix: ${stale.fix}\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
if (report.exitCode !== 0) {
|
|
12
28
|
for (const check of report.checks) {
|
|
13
29
|
if (check.status === "fail") {
|
|
@@ -43,6 +43,22 @@ else
|
|
|
43
43
|
ROOT="$LOCAL_ROOT"
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# CA-02: self-heal pre-check. The doctor's stale_install check runs against the
|
|
47
|
+
# INSTALLED plugin dir (version/selectors vs the current runtime). Exit 2 means
|
|
48
|
+
# stale: the refresh below syncs the plugin dir from ROOT and the registration
|
|
49
|
+
# pass rewrites the workit MCP/hook entries to the canonical current selector.
|
|
50
|
+
# The check reads only workit-owned files, so a healthy install is byte-untouched;
|
|
51
|
+
# a registry-unreachable comparison reports registry_unreachable, never stale
|
|
52
|
+
# (CA-04) — no false stale_install and no install failure.
|
|
53
|
+
if bun "$ROOT/packages/workit-core/scripts/doctor-check.ts" cursor --stale; then
|
|
54
|
+
:
|
|
55
|
+
elif [ $? -eq 2 ]; then
|
|
56
|
+
echo "workit: stale Cursor plugin install detected — self-healing (refresh + canonical re-registration)" >&2
|
|
57
|
+
else
|
|
58
|
+
echo "FATAL: pre-install doctor probe failed" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
46
62
|
chmod +x "$ROOT/packages/workit-core/scripts/sync-runtime.sh" "$ROOT/packages/workit-core/scripts/"*.sh
|
|
47
63
|
# Prefer syncing from this ROOT (dev or freshly cloned share)
|
|
48
64
|
export WORKFLOW_TOOLKIT_DEV="$ROOT"
|
package/src/core/doctor.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Shared offline doctor (DG-07/DG-08, CA-09). One host-neutral engine checks the
|
|
2
2
|
// installed Workit surfaces — pins, versions, assets, launchers, runtimes,
|
|
3
3
|
// utilities, registrations, config, workspace match, credential metadata, and
|
|
4
|
-
// log writability — with no network access
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// log writability — with no network access except the optional registry probe
|
|
5
|
+
// behind the stale-install comparison (CA-04), which fails open. Never reads
|
|
6
|
+
// credential values: only existence, mode, and a placeholder flag are evaluated;
|
|
7
|
+
// token bytes never enter the report or any log event.
|
|
7
8
|
import { spawnSync } from "node:child_process";
|
|
8
9
|
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
10
|
import os from "node:os";
|
|
@@ -11,7 +12,13 @@ import path from "node:path";
|
|
|
11
12
|
import { SUPPORT_MATRIX } from "./support-matrix";
|
|
12
13
|
import { EVENT } from "./boundary";
|
|
13
14
|
import { getDiagnosticLogger, isConfigObject } from "./config";
|
|
14
|
-
import {
|
|
15
|
+
import { packageRoot } from "./package-root";
|
|
16
|
+
import {
|
|
17
|
+
CURSOR_RUNTIME_PACKAGE,
|
|
18
|
+
cursorHooksEntry,
|
|
19
|
+
cursorMcpServerEntry,
|
|
20
|
+
isWorkitPlugin,
|
|
21
|
+
} from "./registration";
|
|
15
22
|
import { resolveWorkspaceFrom } from "./workspaces";
|
|
16
23
|
import { validateCursorSkills } from "./skill-manifests";
|
|
17
24
|
|
|
@@ -28,6 +35,8 @@ export type DoctorCheckId =
|
|
|
28
35
|
| "launcher"
|
|
29
36
|
| "utility"
|
|
30
37
|
| "stale_pin"
|
|
38
|
+
| "stale_install"
|
|
39
|
+
| "registry_unreachable"
|
|
31
40
|
| "duplicate_registration"
|
|
32
41
|
| "malformed_config"
|
|
33
42
|
| "workspace_mismatch"
|
|
@@ -46,12 +55,18 @@ export type DoctorCheck = {
|
|
|
46
55
|
|
|
47
56
|
export type DoctorFix = { id: DoctorCheckId; fix: string };
|
|
48
57
|
|
|
49
|
-
export type DoctorSummary = {
|
|
58
|
+
export type DoctorSummary = {
|
|
59
|
+
passed: number;
|
|
60
|
+
warned: number;
|
|
61
|
+
failed: number;
|
|
62
|
+
total: number;
|
|
63
|
+
};
|
|
50
64
|
|
|
51
65
|
export type DoctorReport = {
|
|
52
66
|
ok: boolean;
|
|
53
67
|
exitCode: number;
|
|
54
|
-
|
|
68
|
+
/** False when the doctor consulted the npm registry (local-dist version probe). */
|
|
69
|
+
offline: boolean;
|
|
55
70
|
host: DoctorHost;
|
|
56
71
|
checked_at: string;
|
|
57
72
|
summary: DoctorSummary;
|
|
@@ -534,7 +549,11 @@ const checkLauncher = (res: Resolved): DoctorCheck => {
|
|
|
534
549
|
detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks",
|
|
535
550
|
};
|
|
536
551
|
}
|
|
537
|
-
return {
|
|
552
|
+
return {
|
|
553
|
+
id: "launcher",
|
|
554
|
+
status: "pass",
|
|
555
|
+
detail: `${res.host} launcher/hook entries present`,
|
|
556
|
+
};
|
|
538
557
|
};
|
|
539
558
|
|
|
540
559
|
const checkUtility = (res: Resolved): DoctorCheck => {
|
|
@@ -555,7 +574,11 @@ const checkUtility = (res: Resolved): DoctorCheck => {
|
|
|
555
574
|
fix: "Install util-linux (flock)",
|
|
556
575
|
};
|
|
557
576
|
}
|
|
558
|
-
return {
|
|
577
|
+
return {
|
|
578
|
+
id: "utility",
|
|
579
|
+
status: "pass",
|
|
580
|
+
detail: "git (and flock where required) on PATH",
|
|
581
|
+
};
|
|
559
582
|
};
|
|
560
583
|
|
|
561
584
|
const staleEntry = (entry: string): "ok" | "stale" | "missing-file" => {
|
|
@@ -579,7 +602,11 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
|
|
|
579
602
|
};
|
|
580
603
|
}
|
|
581
604
|
if (!existsSync(res.opencodeConfig)) {
|
|
582
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
id: "stale_pin",
|
|
607
|
+
status: "pass",
|
|
608
|
+
detail: "no opencode config — not registered",
|
|
609
|
+
};
|
|
583
610
|
}
|
|
584
611
|
const cfg = readJson(res.opencodeConfig);
|
|
585
612
|
const entries = pluginEntries(cfg);
|
|
@@ -611,6 +638,169 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
|
|
|
611
638
|
return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
|
|
612
639
|
};
|
|
613
640
|
|
|
641
|
+
// Cursor plugin stale-install detection (CA-01/CA-04). Compares the installed
|
|
642
|
+
// `~/.cursor/plugins/local/workit` against the current runtime source of truth
|
|
643
|
+
// (`CURSOR_RUNTIME_PACKAGE` + the canonical entry builders from registration.ts,
|
|
644
|
+
// D-02) and surfaces a structured `stale_install` finding with the exact repair
|
|
645
|
+
// step. A stale install means the plugin's sessionStart hook and MCP entry run
|
|
646
|
+
// an ancient runtime that no longer auto-registers the workflow features.
|
|
647
|
+
//
|
|
648
|
+
// Three independent staleness signals are read from the installed plugin dir:
|
|
649
|
+
// 1. Legacy selectors (offline evidence, checked first): the plugin's own
|
|
650
|
+
// mcp.json (`--package=...workit-cursor@<exact>` pins other than the
|
|
651
|
+
// canonical `@latest`) and hooks-cursor.json sessionStart command.
|
|
652
|
+
// 2. Installed version behind the current source release (offline): the dev
|
|
653
|
+
// checkout's workit-cursor version (or the doctor's own package version)
|
|
654
|
+
// is the release train the doctor ships with.
|
|
655
|
+
// 3. Local-dist installs behind the published runtime (needs the registry):
|
|
656
|
+
// the probe is the sole network read in the doctor. A SUCCEEDED probe with
|
|
657
|
+
// the installed version genuinely behind the published runtime is a hard
|
|
658
|
+
// `stale_install` fail (the installer self-heals on it); an unreachable
|
|
659
|
+
// registry fails open (CA-04) as a `registry_unreachable` warning, never a
|
|
660
|
+
// false `stale_install` and never a hard doctor failure. Canonical
|
|
661
|
+
// `@latest` installs skip the probe entirely: the selector resolves fresh
|
|
662
|
+
// at launch, so the installed package.json version is metadata, not a
|
|
663
|
+
// freshness signal.
|
|
664
|
+
// Test seams (no spawns on the canonical path): WORKIT_DOCTOR_STALE_REGISTRY_VERSION
|
|
665
|
+
// short-circuits the probe with the resolved latest version; the optional
|
|
666
|
+
// WORKIT_DOCTOR_STALE_REGISTRY_CMD replaces the `npm view` binary (a
|
|
667
|
+
// nonexistent path deterministically exercises the fail-open path).
|
|
668
|
+
const pluginMcpSelectors = (res: Resolved): string[] | null => {
|
|
669
|
+
const p = path.join(res.cursorPluginDir, "mcp.json");
|
|
670
|
+
if (!existsSync(p)) return null;
|
|
671
|
+
const cfg = readJson(p);
|
|
672
|
+
const server = cfg?.mcpServers?.workit;
|
|
673
|
+
if (!server || typeof server !== "object" || Array.isArray(server)) return null;
|
|
674
|
+
const args = server.args;
|
|
675
|
+
if (!Array.isArray(args)) return null;
|
|
676
|
+
return args.map(String).filter((a) => a.startsWith("--package="));
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const registryLatestVersion = (res: Resolved): string | null => {
|
|
680
|
+
const seam = res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
|
|
681
|
+
if (typeof seam === "string" && seam) {
|
|
682
|
+
return /^\d+(?:\.\d+){1,2}$/.test(seam) ? seam : null;
|
|
683
|
+
}
|
|
684
|
+
const cmd =
|
|
685
|
+
res.env.WORKIT_DOCTOR_STALE_REGISTRY_CMD ?? (commandOnPath("npm", res.env) ? "npm" : null);
|
|
686
|
+
if (!cmd) return null;
|
|
687
|
+
try {
|
|
688
|
+
const r = spawnSync(cmd, ["view", CURSOR_RUNTIME_PACKAGE, "version"], {
|
|
689
|
+
encoding: "utf8",
|
|
690
|
+
timeout: 20_000,
|
|
691
|
+
env: res.env,
|
|
692
|
+
});
|
|
693
|
+
const v = (r.stdout ?? "").trim().split(/\s+/)[0];
|
|
694
|
+
return r.status === 0 && /^\d+(?:\.\d+){1,2}$/.test(v) ? v : null;
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const installedPluginVersion = (res: Resolved): string | null => {
|
|
701
|
+
const pkg = readJson(path.join(res.cursorPluginDir, "package.json"));
|
|
702
|
+
return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const checkStaleInstall = (res: Resolved): DoctorCheck & { registryProbed?: boolean } => {
|
|
706
|
+
if (res.host !== "cursor" && res.host !== "cli") {
|
|
707
|
+
return {
|
|
708
|
+
id: "stale_install",
|
|
709
|
+
status: "pass",
|
|
710
|
+
detail: "cursor plugin not inspected on the opencode host",
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
|
|
714
|
+
const mcpSelectors = pluginMcpSelectors(res);
|
|
715
|
+
const legacyPin = (mcpSelectors ?? []).find(
|
|
716
|
+
(s) => s !== canonicalMcp && s.includes("@brainervirus/workit-cursor@"),
|
|
717
|
+
);
|
|
718
|
+
if (legacyPin) {
|
|
719
|
+
return {
|
|
720
|
+
id: "stale_install",
|
|
721
|
+
status: "fail",
|
|
722
|
+
detail: `stale_install: plugin mcp.json pins a legacy selector ${legacyPin} (canonical: ${canonicalMcp})`,
|
|
723
|
+
fix: "Re-run install-cursor-plugin.sh — it rewrites the workit MCP entry to the canonical @latest selector",
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const hooksFile = path.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
|
|
727
|
+
const hookCmd =
|
|
728
|
+
(readJson(hooksFile)?.hooks?.sessionStart?.[0]?.command as string | undefined) ?? null;
|
|
729
|
+
const canonicalHook = canonicalCursorHook;
|
|
730
|
+
const staleHook =
|
|
731
|
+
hookCmd !== null &&
|
|
732
|
+
hookCmd !== canonicalHook &&
|
|
733
|
+
!hookCmd.startsWith("node ") &&
|
|
734
|
+
hookCmd.includes("@brainervirus/workit-cursor@");
|
|
735
|
+
if (staleHook) {
|
|
736
|
+
return {
|
|
737
|
+
id: "stale_install",
|
|
738
|
+
status: "fail",
|
|
739
|
+
detail: `stale_install: sessionStart hook runs a legacy selector (canonical: ${canonicalHook})`,
|
|
740
|
+
fix: "Re-run install-cursor-plugin.sh — it rewrites the sessionStart hook to the canonical @latest selector",
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const installed = installedPluginVersion(res);
|
|
744
|
+
const source = res.dev
|
|
745
|
+
? ((readJson(path.join(res.dev, "packages/workit-cursor/package.json"))?.version as
|
|
746
|
+
| string
|
|
747
|
+
| undefined) ?? null)
|
|
748
|
+
: ((readJson(path.join(packageRoot(), "package.json"))?.version as string | undefined) ?? null);
|
|
749
|
+
// A local-dist install (node entry) runs the installed dir's own dist, so its
|
|
750
|
+
// version is comparable against the current runtime (and the published one
|
|
751
|
+
// below). Canonical @latest installs resolve fresh at launch — the installed
|
|
752
|
+
// package.json version is metadata, not a freshness signal (CA-04), so they
|
|
753
|
+
// skip both version comparisons and never fail stale_install on metadata.
|
|
754
|
+
const localDist = mcpSelectors === null && hookCmd !== null && hookCmd.startsWith("node ");
|
|
755
|
+
if (localDist && installed !== null && source !== null && !semverAtLeast(installed, source)) {
|
|
756
|
+
return {
|
|
757
|
+
id: "stale_install",
|
|
758
|
+
status: "fail",
|
|
759
|
+
detail: `stale_install: installed workit-cursor ${installed} is behind the current runtime ${source}`,
|
|
760
|
+
fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory and rewrites the workit MCP/hook entries",
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (installed !== null && localDist) {
|
|
764
|
+
const expected = registryLatestVersion(res);
|
|
765
|
+
if (expected === null) {
|
|
766
|
+
return {
|
|
767
|
+
id: "registry_unreachable",
|
|
768
|
+
status: "warn",
|
|
769
|
+
registryProbed: true,
|
|
770
|
+
detail:
|
|
771
|
+
"registry_unreachable: cannot compare installed workit-cursor against the published runtime",
|
|
772
|
+
fix: "Retry when the npm registry is reachable, or re-run install-cursor-plugin.sh to refresh the install",
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (!semverAtLeast(installed, expected)) {
|
|
776
|
+
// The probe SUCCEEDED and the installed version is genuinely behind the
|
|
777
|
+
// published runtime: report a hard fail so `doctor-check.ts cursor --stale`
|
|
778
|
+
// exits 2 and the installer self-heals the local-dist install.
|
|
779
|
+
return {
|
|
780
|
+
id: "stale_install",
|
|
781
|
+
status: "fail",
|
|
782
|
+
registryProbed: true,
|
|
783
|
+
detail: `stale_install: local-dist workit-cursor ${installed} is behind the published runtime ${expected}`,
|
|
784
|
+
fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory with the current build",
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
id: "stale_install",
|
|
789
|
+
status: "pass",
|
|
790
|
+
registryProbed: true,
|
|
791
|
+
detail: `installed local-dist workit-cursor ${installed} matches the published runtime ${expected}`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
id: "stale_install",
|
|
796
|
+
status: "pass",
|
|
797
|
+
detail:
|
|
798
|
+
installed === null
|
|
799
|
+
? "installed workit-cursor selectors are canonical"
|
|
800
|
+
: `installed workit-cursor ${installed} is metadata on the canonical @latest install (fresh at launch)`,
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
|
|
614
804
|
const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
|
|
615
805
|
const problems: string[] = [];
|
|
616
806
|
const opencodeHost = res.host !== "cursor";
|
|
@@ -659,7 +849,11 @@ const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
|
|
|
659
849
|
}
|
|
660
850
|
}
|
|
661
851
|
if (problems.length === 0) {
|
|
662
|
-
return {
|
|
852
|
+
return {
|
|
853
|
+
id: "duplicate_registration",
|
|
854
|
+
status: "pass",
|
|
855
|
+
detail: "no duplicate registrations",
|
|
856
|
+
};
|
|
663
857
|
}
|
|
664
858
|
return {
|
|
665
859
|
id: "duplicate_registration",
|
|
@@ -682,7 +876,11 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
|
|
|
682
876
|
if (cursorHost && existsSync(res.cursorMcp)) files.push(res.cursorMcp);
|
|
683
877
|
const bad = files.filter((p) => !parsesAsConfigObject(p));
|
|
684
878
|
if (bad.length === 0)
|
|
685
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
id: "malformed_config",
|
|
881
|
+
status: "pass",
|
|
882
|
+
detail: "config files parse",
|
|
883
|
+
};
|
|
686
884
|
return {
|
|
687
885
|
id: "malformed_config",
|
|
688
886
|
status: "fail",
|
|
@@ -694,10 +892,18 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
|
|
|
694
892
|
const checkWorkspaceMismatch = (res: Resolved): DoctorCheck => {
|
|
695
893
|
const file = path.join(res.configDir, "workspaces.json");
|
|
696
894
|
if (!existsSync(file))
|
|
697
|
-
return {
|
|
895
|
+
return {
|
|
896
|
+
id: "workspace_mismatch",
|
|
897
|
+
status: "pass",
|
|
898
|
+
detail: "no workspaces configured",
|
|
899
|
+
};
|
|
698
900
|
const ws = readJson(file);
|
|
699
901
|
if (!Array.isArray(ws?.workspaces)) {
|
|
700
|
-
return {
|
|
902
|
+
return {
|
|
903
|
+
id: "workspace_mismatch",
|
|
904
|
+
status: "pass",
|
|
905
|
+
detail: "no workspaces configured",
|
|
906
|
+
};
|
|
701
907
|
}
|
|
702
908
|
const match = resolveWorkspaceFrom(res.cwd, res.configDir);
|
|
703
909
|
if (match)
|
|
@@ -748,7 +954,11 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
|
|
|
748
954
|
}
|
|
749
955
|
|
|
750
956
|
if (tokenPaths.length === 0) {
|
|
751
|
-
return {
|
|
957
|
+
return {
|
|
958
|
+
id: "credential_metadata",
|
|
959
|
+
status: "pass",
|
|
960
|
+
detail: "no credentials configured",
|
|
961
|
+
};
|
|
752
962
|
}
|
|
753
963
|
const problems: string[] = [];
|
|
754
964
|
for (const raw of tokenPaths) {
|
|
@@ -786,7 +996,11 @@ const checkLogWritable = (res: Resolved): DoctorCheck => {
|
|
|
786
996
|
try {
|
|
787
997
|
mkdirSync(logsDir, { recursive: true, mode: 0o700 });
|
|
788
998
|
writeFileSync(probe, '{"probe":true}\n', { mode: 0o600 });
|
|
789
|
-
return {
|
|
999
|
+
return {
|
|
1000
|
+
id: "log_writable",
|
|
1001
|
+
status: "pass",
|
|
1002
|
+
detail: "log directory writable",
|
|
1003
|
+
};
|
|
790
1004
|
} catch (err) {
|
|
791
1005
|
return {
|
|
792
1006
|
id: "log_writable",
|
|
@@ -810,6 +1024,7 @@ const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
|
|
|
810
1024
|
checkLauncher,
|
|
811
1025
|
checkUtility,
|
|
812
1026
|
checkStalePin,
|
|
1027
|
+
checkStaleInstall,
|
|
813
1028
|
checkDuplicateRegistration,
|
|
814
1029
|
checkMalformedConfig,
|
|
815
1030
|
checkWorkspaceMismatch,
|
|
@@ -827,6 +1042,7 @@ const INSTALLER_REQUIRED = new Set<DoctorCheckId>([
|
|
|
827
1042
|
"launcher",
|
|
828
1043
|
"utility",
|
|
829
1044
|
"stale_pin",
|
|
1045
|
+
"stale_install",
|
|
830
1046
|
"duplicate_registration",
|
|
831
1047
|
"malformed_config",
|
|
832
1048
|
]);
|
|
@@ -837,7 +1053,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
837
1053
|
const checks = res.installer
|
|
838
1054
|
? raw.map((c) =>
|
|
839
1055
|
c.status === "fail" && !INSTALLER_REQUIRED.has(c.id)
|
|
840
|
-
? {
|
|
1056
|
+
? {
|
|
1057
|
+
...c,
|
|
1058
|
+
status: "warn" as const,
|
|
1059
|
+
detail: `${c.detail} (not enforced by installer)`,
|
|
1060
|
+
}
|
|
841
1061
|
: c,
|
|
842
1062
|
)
|
|
843
1063
|
: raw;
|
|
@@ -846,10 +1066,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
846
1066
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
847
1067
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
848
1068
|
const exitCode = failed > 0 ? 1 : 0;
|
|
1069
|
+
const offline = !raw.some((c) => "registryProbed" in c && c.registryProbed);
|
|
849
1070
|
const report: DoctorReport = {
|
|
850
1071
|
ok: failed === 0,
|
|
851
1072
|
exitCode,
|
|
852
|
-
offline
|
|
1073
|
+
offline,
|
|
853
1074
|
host: res.host,
|
|
854
1075
|
checked_at: new Date().toISOString(),
|
|
855
1076
|
summary: { passed, warned, failed, total: checks.length },
|
|
@@ -862,7 +1083,7 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
862
1083
|
getDiagnosticLogger()?.info(EVENT.doctor, {
|
|
863
1084
|
host: res.host,
|
|
864
1085
|
exit_code: exitCode,
|
|
865
|
-
offline
|
|
1086
|
+
offline,
|
|
866
1087
|
failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
|
|
867
1088
|
total: checks.length,
|
|
868
1089
|
});
|