@geonosis/doctor 2.1.0 → 2.2.0

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.
@@ -427,6 +427,14 @@ var presumptionsOf = async (entry) => {
427
427
  const loaded = await import(pathToFileURL(entry).href);
428
428
  const namespace = loaded.default?.meta?.name;
429
429
  return {
430
+ // #290: a rule that names a pattern whose floor is not shipped is a requirement with nothing to
431
+ // install behind it — the second consumer hand-writes the floor to satisfy the lint.
432
+ floors: Object.fromEntries(
433
+ Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
434
+ const floor = rule?.meta?.floor;
435
+ return typeof floor?.name === "string" && floor.pending === true ? [[name, { name: floor.name }]] : [];
436
+ })
437
+ ),
430
438
  namespace: typeof namespace === "string" ? namespace : "",
431
439
  presumed: Object.fromEntries(
432
440
  Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
@@ -1217,6 +1225,10 @@ var COMPOSABLE = [
1217
1225
  }))
1218
1226
  ];
1219
1227
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
1228
+ var QUOTE = "['\"`]";
1229
+ var imports = (body, name) => new RegExp(
1230
+ `(?:from|import|require)\\s*\\(?\\s*${QUOTE}${name.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}(?:/[^'"\`]*)?${QUOTE}`
1231
+ ).test(body);
1220
1232
  var composedHere = (root, workspaces) => {
1221
1233
  const declared = declaredAnywhere(workspaces);
1222
1234
  const wanted = COMPOSABLE.filter((one) => declared.has(one.name));
@@ -1230,7 +1242,7 @@ var composedHere = (root, workspaces) => {
1230
1242
  continue;
1231
1243
  }
1232
1244
  for (const one of wanted) {
1233
- if (!importing.has(one.name) && body.includes(one.name)) importing.set(one.name, path);
1245
+ if (!importing.has(one.name) && imports(body, one.name)) importing.set(one.name, path);
1234
1246
  }
1235
1247
  if (importing.size === wanted.length) break;
1236
1248
  }
@@ -1895,9 +1907,55 @@ var checkEnvelopes = ({ root }) => {
1895
1907
  });
1896
1908
  };
1897
1909
 
1910
+ // src/claude-plugin.ts
1911
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1912
+ import { homedir as homedir2 } from "os";
1913
+ import { join as join10 } from "path";
1914
+ var INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
1915
+ var PLUGIN_NAME = "geonosis";
1916
+ var installedPluginVersions = (home, name) => {
1917
+ const at = join10(home, INSTALLED_PLUGINS);
1918
+ if (!existsSync9(at)) return [];
1919
+ let record;
1920
+ try {
1921
+ record = JSON.parse(readFileSync10(at, "utf8"));
1922
+ } catch {
1923
+ return [];
1924
+ }
1925
+ return Object.entries(record.plugins ?? {}).filter(([key]) => (key.split("@")[0] ?? key) === name).flatMap(([, value]) => Array.isArray(value) ? value : []).flatMap((one) => typeof one.version === "string" ? [one.version] : []);
1926
+ };
1927
+ var checkClaudePlugin = ({
1928
+ home = homedir2(),
1929
+ name = PLUGIN_NAME,
1930
+ train
1931
+ }) => {
1932
+ if (train === void 0) return [];
1933
+ const installed = installedPluginVersions(home, name);
1934
+ if (installed.length === 0) {
1935
+ return [
1936
+ {
1937
+ check: "loaded",
1938
+ message: `this machine has no "${name}" plugin installed, so none of this release's hooks, agents or skills is loaded in any session here \u2014 a bump moved the packages and nothing moved them. Install it, or say out loud that this repo runs without the plugin`,
1939
+ subject: `${name} (Claude plugin)`,
1940
+ verdict: "WARN"
1941
+ }
1942
+ ];
1943
+ }
1944
+ const drifted = installed.filter((one) => one !== train);
1945
+ if (drifted.length === 0) return [];
1946
+ return [
1947
+ {
1948
+ check: "loaded",
1949
+ message: `the installed plugin is ${drifted.join(", ")} and the packages here are ${train} \u2014 the plugin carries the hooks, the agents and the skills, and it moves on \`claude plugin update\`, never on an install. Every session in this repo is running the older ones`,
1950
+ subject: `${name} (Claude plugin)`,
1951
+ verdict: "WARN"
1952
+ }
1953
+ ];
1954
+ };
1955
+
1898
1956
  // src/loaded.ts
1899
- import { readFileSync as readFileSync10 } from "fs";
1900
- import { join as join10, sep as sep3 } from "path";
1957
+ import { readFileSync as readFileSync11 } from "fs";
1958
+ import { join as join11, sep as sep3 } from "path";
1901
1959
  var SCOPE = "@geonosis/";
1902
1960
  var NOT_THE_SCOPE_DIRECTORY = "Deleting only the scope directory inside that node_modules is a repair no pnpm install undoes \u2014 the tree is then missing the package and every install stays green.";
1903
1961
  var BLOCKS = [
@@ -2050,7 +2108,7 @@ var shipperOf = (root, bin) => {
2050
2108
  const name = bin === "geonosis" ? "@geonosis/cli" : `@geonosis/${bin.replace(/^geonosis-/, "")}`;
2051
2109
  try {
2052
2110
  const manifest = JSON.parse(
2053
- readFileSync10(join10(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2111
+ readFileSync11(join11(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2054
2112
  );
2055
2113
  const declared = manifest.bin;
2056
2114
  return typeof declared === "object" && !Object.hasOwn(declared, bin) ? "" : name;
@@ -2098,6 +2156,7 @@ var gitHooks = (root) => {
2098
2156
  };
2099
2157
  var checkLoaded = async ({
2100
2158
  configs,
2159
+ home,
2101
2160
  root,
2102
2161
  workspaces
2103
2162
  }) => {
@@ -2116,12 +2175,23 @@ var checkLoaded = async ({
2116
2175
  for (const specifier of [...specifiers].toSorted()) {
2117
2176
  findings.push(await copiesOf({ root, specifier, workspaces }));
2118
2177
  }
2178
+ findings.push(...checkClaudePlugin({ home, train: await trainVersion(root, [...specifiers]) }));
2119
2179
  return findings;
2120
2180
  };
2181
+ var trainVersion = async (root, specifiers) => {
2182
+ for (const specifier of specifiers.toSorted()) {
2183
+ try {
2184
+ return await pluginVersionOf(resolveFrom(root, specifier));
2185
+ } catch {
2186
+ continue;
2187
+ }
2188
+ }
2189
+ return void 0;
2190
+ };
2121
2191
 
2122
2192
  // src/observability.ts
2123
- import { readFileSync as readFileSync11 } from "fs";
2124
- import { join as join11 } from "path";
2193
+ import { readFileSync as readFileSync12 } from "fs";
2194
+ import { join as join12 } from "path";
2125
2195
  var GEONOSIS_FILE2 = "geonosis.json";
2126
2196
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
2127
2197
  var DEFAULT_MAX_AGE_SECONDS = 3600;
@@ -2135,7 +2205,7 @@ var finding8 = (verdict, subject, message) => ({
2135
2205
  var readGeonosis2 = (root) => {
2136
2206
  let text;
2137
2207
  try {
2138
- text = readFileSync11(join11(root, GEONOSIS_FILE2), "utf8");
2208
+ text = readFileSync12(join12(root, GEONOSIS_FILE2), "utf8");
2139
2209
  } catch {
2140
2210
  return { present: false };
2141
2211
  }
@@ -2208,7 +2278,7 @@ var ageFinding = (config, root, now) => {
2208
2278
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
2209
2279
  let record;
2210
2280
  try {
2211
- record = JSON.parse(readFileSync11(join11(root, file), "utf8"));
2281
+ record = JSON.parse(readFileSync12(join12(root, file), "utf8"));
2212
2282
  } catch (error) {
2213
2283
  return finding8(
2214
2284
  "FAIL",
@@ -2285,8 +2355,8 @@ var checkObservability = async ({
2285
2355
  };
2286
2356
 
2287
2357
  // src/runner.ts
2288
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
2289
- import { join as join12 } from "path";
2358
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
2359
+ import { join as join13 } from "path";
2290
2360
  var TEST_FAILURES = "testFailures";
2291
2361
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
2292
2362
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
@@ -2312,17 +2382,17 @@ var keyOf = (entry) => stringOf(entry.key) === "" ? TEST_FAILURES : stringOf(ent
2312
2382
  var PROVES = /--prove\b/;
2313
2383
  var everythingRun2 = (root, workspaces) => {
2314
2384
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
2315
- const dir = join12(root, ".github/workflows");
2316
- const workflows = (existsSync9(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2385
+ const dir = join13(root, ".github/workflows");
2386
+ const workflows = (existsSync10(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2317
2387
  try {
2318
- return readFileSync12(join12(dir, name), "utf8");
2388
+ return readFileSync13(join13(dir, name), "utf8");
2319
2389
  } catch {
2320
2390
  return "";
2321
2391
  }
2322
2392
  });
2323
2393
  let tiers = {};
2324
2394
  try {
2325
- const config = JSON.parse(readFileSync12(join12(root, "geonosis.json"), "utf8"));
2395
+ const config = JSON.parse(readFileSync13(join13(root, "geonosis.json"), "utf8"));
2326
2396
  tiers = config.verify ?? {};
2327
2397
  } catch {
2328
2398
  }
@@ -2403,8 +2473,8 @@ var checkRunner = ({
2403
2473
  };
2404
2474
 
2405
2475
  // src/doctor.ts
2406
- import { homedir as homedir2 } from "os";
2407
- import { join as join14 } from "path";
2476
+ import { homedir as homedir3 } from "os";
2477
+ import { join as join15 } from "path";
2408
2478
  import { resolveOxlint } from "@geonosis/lint-parity";
2409
2479
 
2410
2480
  // src/engine.ts
@@ -2432,16 +2502,32 @@ var checkEngines = async ({
2432
2502
  workspaces
2433
2503
  }) => {
2434
2504
  const plugin = await presumptionsOf(entry).catch(() => void 0);
2435
- if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0) {
2505
+ if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0 && Object.keys(plugin.floors).length === 0) {
2436
2506
  return [];
2437
2507
  }
2508
+ const pending = (rule) => {
2509
+ const floor = plugin.floors[rule.slice(plugin.namespace.length + 1)];
2510
+ if (floor === void 0) return [];
2511
+ return [
2512
+ {
2513
+ check: "exercised",
2514
+ message: `it names a pattern the kit does not ship yet (${floor.name}) \u2014 the rule is the requirement and there is nothing to install behind it, so a repo enabling it hand-writes the floor to satisfy the lint. Wait for the floor, or keep the pattern of your own and know that is what it is`,
2515
+ subject: `${config.relative} \u2192 ${rule}`,
2516
+ verdict: "WARN"
2517
+ }
2518
+ ];
2519
+ };
2438
2520
  const governed = governedBy(config.dir, workspaces);
2439
2521
  const declared = new Set(governed.flatMap((one) => [...declaredIn3(one.manifest)]));
2440
2522
  const asked = governed.map(labelOf3).join(", ");
2441
2523
  return enabledHere2(config, plugin.namespace).flatMap((rule) => {
2524
+ const floorless = pending(rule);
2442
2525
  const presumes = plugin.presumed[rule.slice(plugin.namespace.length + 1)];
2443
- if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) return [];
2526
+ if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) {
2527
+ return floorless;
2528
+ }
2444
2529
  return [
2530
+ ...floorless,
2445
2531
  {
2446
2532
  check: "exercised",
2447
2533
  message: `it presumes ${presumes.engine}, and none of the manifests this config governs (${asked}) declares ${presumes.packages.join(" or ")} \u2014 enabled here it has no possible finding, which reads exactly like a clean tree. Drop it from this config, or say which package brings the engine`,
@@ -2453,8 +2539,8 @@ var checkEngines = async ({
2453
2539
  };
2454
2540
 
2455
2541
  // src/rails.ts
2456
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
2457
- import { join as join13 } from "path";
2542
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
2543
+ import { join as join14 } from "path";
2458
2544
  var GEONOSIS2 = "geonosis.json";
2459
2545
  var PROJECT = ".claude/settings.json";
2460
2546
  var LOCAL = ".claude/settings.local.json";
@@ -2480,10 +2566,10 @@ var networkIn = (parsed) => {
2480
2566
  };
2481
2567
  };
2482
2568
  var sourceAt = (path, label) => {
2483
- if (!existsSync10(path)) return { network: void 0, path: label, unreadable: false };
2569
+ if (!existsSync11(path)) return { network: void 0, path: label, unreadable: false };
2484
2570
  try {
2485
2571
  return {
2486
- network: networkIn(JSON.parse(readFileSync13(path, "utf8"))),
2572
+ network: networkIn(JSON.parse(readFileSync14(path, "utf8"))),
2487
2573
  path: label,
2488
2574
  unreadable: false
2489
2575
  };
@@ -2492,11 +2578,11 @@ var sourceAt = (path, label) => {
2492
2578
  }
2493
2579
  };
2494
2580
  var declaredEgress = (root) => {
2495
- const at = join13(root, GEONOSIS2);
2496
- if (!existsSync10(at)) return void 0;
2581
+ const at = join14(root, GEONOSIS2);
2582
+ if (!existsSync11(at)) return void 0;
2497
2583
  let parsed;
2498
2584
  try {
2499
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2585
+ parsed = JSON.parse(readFileSync14(at, "utf8"));
2500
2586
  } catch {
2501
2587
  return void 0;
2502
2588
  }
@@ -2508,8 +2594,8 @@ var declaredEgress = (root) => {
2508
2594
  };
2509
2595
  };
2510
2596
  var deniedEgress = (root) => {
2511
- const at = join13(root, RUN_RECORD);
2512
- if (!existsSync10(at)) {
2597
+ const at = join14(root, RUN_RECORD);
2598
+ if (!existsSync11(at)) {
2513
2599
  return [
2514
2600
  finding10(
2515
2601
  "deniedEgress",
@@ -2520,7 +2606,7 @@ var deniedEgress = (root) => {
2520
2606
  }
2521
2607
  let parsed;
2522
2608
  try {
2523
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2609
+ parsed = JSON.parse(readFileSync14(at, "utf8"));
2524
2610
  } catch (error) {
2525
2611
  return [
2526
2612
  finding10(
@@ -2561,8 +2647,8 @@ var checkRails = ({
2561
2647
  const sources = [
2562
2648
  sourceAt(managedSettings, managedSettings),
2563
2649
  sourceAt(userSettings, userSettings),
2564
- sourceAt(join13(root, PROJECT), PROJECT),
2565
- sourceAt(join13(root, LOCAL), LOCAL)
2650
+ sourceAt(join14(root, PROJECT), PROJECT),
2651
+ sourceAt(join14(root, LOCAL), LOCAL)
2566
2652
  ];
2567
2653
  const unreadable = sources.filter((one) => one.unreadable).map(
2568
2654
  (one) => finding10(
@@ -2733,6 +2819,7 @@ var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one
2733
2819
  });
2734
2820
  var runDoctor = async ({
2735
2821
  baseline,
2822
+ home = homedir3(),
2736
2823
  only,
2737
2824
  oxlint,
2738
2825
  root,
@@ -2750,7 +2837,7 @@ var runDoctor = async ({
2750
2837
  [
2751
2838
  "loaded",
2752
2839
  async () => [
2753
- ...await checkLoaded({ configs, root, workspaces }),
2840
+ ...await checkLoaded({ configs, home, root, workspaces }),
2754
2841
  ...await requiredOptionsOf(configs)
2755
2842
  ]
2756
2843
  ],
@@ -2784,7 +2871,7 @@ var runDoctor = async ({
2784
2871
  ["observability", () => checkObservability({ now: Date.now(), root })],
2785
2872
  ["drift", () => checkDrift({ root, workspaces })],
2786
2873
  ["deployed", () => checkDeployed({ root })],
2787
- ["rails", () => checkRails({ root, userSettings: join14(homedir2(), USER_SETTINGS) })]
2874
+ ["rails", () => checkRails({ root, userSettings: join15(home, USER_SETTINGS) })]
2788
2875
  ];
2789
2876
  const collected = [];
2790
2877
  const ran = [];
@@ -2878,6 +2965,10 @@ export {
2878
2965
  ENVELOPES_DIR,
2879
2966
  NO_ENVELOPES,
2880
2967
  checkEnvelopes,
2968
+ INSTALLED_PLUGINS,
2969
+ PLUGIN_NAME,
2970
+ installedPluginVersions,
2971
+ checkClaudePlugin,
2881
2972
  SCOPE,
2882
2973
  satisfies,
2883
2974
  declaredFor,
@@ -4,15 +4,15 @@ import {
4
4
  formatDoctor,
5
5
  formatJson,
6
6
  runDoctor
7
- } from "./chunk-R2R2OYH3.js";
7
+ } from "./chunk-DR5P4QYC.js";
8
8
 
9
9
  // src/doctor-cli.ts
10
10
  import { fstatSync, statSync } from "fs";
11
- import { resolve } from "path";
11
+ import { resolve as resolve2 } from "path";
12
12
 
13
13
  // src/envelope-write.ts
14
14
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
15
- import { dirname, join } from "path";
15
+ import { dirname, join, resolve } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
  var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
18
18
  var UnbalancedEnvelope = class extends Error {
@@ -23,6 +23,14 @@ var UnbalancedEnvelope = class extends Error {
23
23
  };
24
24
  var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
25
25
  var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
26
+ var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
27
+ var refuseForbiddenRoot = (root) => {
28
+ const forbidden = process.env[FORBIDDEN_ROOT];
29
+ if (forbidden === void 0 || resolve(forbidden) !== resolve(root)) return;
30
+ throw new UnbalancedEnvelope(
31
+ `${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
32
+ );
33
+ };
26
34
  var writeEnvelope = ({
27
35
  envelope,
28
36
  next,
@@ -46,6 +54,7 @@ var writeEnvelope = ({
46
54
  if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
47
55
  throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
48
56
  }
57
+ refuseForbiddenRoot(root);
49
58
  const at = envelopePath(root, envelope.tool);
50
59
  mkdirSync(dirname(at), { recursive: true });
51
60
  writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
@@ -225,8 +234,8 @@ var parseDoctorArgs = (argv, cwd) => {
225
234
  ...baseline === void 0 ? {} : { baseline },
226
235
  json,
227
236
  ...only === void 0 || only.length === 0 ? {} : { only },
228
- ...read["--oxlint"] === void 0 ? {} : { oxlint: resolve(cwd, read["--oxlint"]) },
229
- root: resolve(cwd, read["--root"] ?? cwd),
237
+ ...read["--oxlint"] === void 0 ? {} : { oxlint: resolve2(cwd, read["--oxlint"]) },
238
+ root: resolve2(cwd, read["--root"] ?? cwd),
230
239
  strict
231
240
  };
232
241
  };
package/dist/index.d.ts CHANGED
@@ -66,6 +66,8 @@ type RatchetConfig = {
66
66
  counters: Record<string, unknown>[];
67
67
  };
68
68
  type DoctorOptions = {
69
+ /** Where `~/.claude` lives — the plugin registry and the user settings the doctor reads; the home directory unless a caller says otherwise. */
70
+ home?: string;
69
71
  /**
70
72
  * Present only when a baseline comparison was asked for, because making one is the one part of
71
73
  * this tool that runs git — and the release check runs the doctor inside repos other sessions
@@ -138,7 +140,7 @@ declare const discoverConfigs: (root: string) => DiscoveredConfig[];
138
140
  declare const discoverWorkspaces: (root: string) => Workspace[];
139
141
  declare const readRatchet: (root: string) => RatchetConfig | undefined;
140
142
 
141
- declare const runDoctor: ({ baseline, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
143
+ declare const runDoctor: ({ baseline, home, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
142
144
 
143
145
  /** The file `create-geonosis --domains` writes, held against the scaffold's own name by a test. */
144
146
  declare const COMPOSITION_ROOT = "src/platform.ts";
@@ -265,6 +267,39 @@ declare const checkGroup: ({ groups, root, workspaces, }: {
265
267
  workspaces: Workspace[];
266
268
  }) => Finding[];
267
269
 
270
+ /**
271
+ * Where Claude Code records what it has installed.
272
+ *
273
+ * MEASURED 2026-09-02 (`~/.claude/plugins/installed_plugins.json`, `version: 2`): a map keyed
274
+ * `<plugin>@<marketplace>`, each value a list of installations carrying `version`, `installPath`
275
+ * and the commit it came from. This is the file `claude plugin list` answers from.
276
+ */
277
+ declare const INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
278
+ /** The plugin this kit publishes. Its own name, the way `FIXED_GROUP` is its own group. */
279
+ declare const PLUGIN_NAME = "geonosis";
280
+ /**
281
+ * Every version of one plugin this machine has installed, in the order the record lists them.
282
+ *
283
+ * A name can be installed from more than one marketplace and at more than one scope, so this
284
+ * answers with a list rather than picking one: a session loads what it loads, and a doctor that
285
+ * chose for the reader would be reporting about an installation nobody has.
286
+ */
287
+ declare const installedPluginVersions: (home: string, name: string) => string[];
288
+ /**
289
+ * #133: a consumer's npm bump does not move the CLAUDE PLUGIN.
290
+ *
291
+ * The hooks, the agents and the skills live in the plugin, and it updates on `claude plugin update`
292
+ * and never on an install — so a repo can be on this release's packages while every session in it
293
+ * runs the hooks of the one before, and a refusal added this release cannot fire. That is the
294
+ * declared-versus-loaded question one surface out, and it is this check's to ask.
295
+ */
296
+ declare const checkClaudePlugin: ({ home, name, train, }: {
297
+ home?: string;
298
+ name?: string;
299
+ /** The version the package train is on here, or nothing when this tree declares none. */
300
+ train: string | undefined;
301
+ }) => Finding[];
302
+
268
303
  /** The scope this tool asks about. A config's other plugins are somebody else's release to check. */
269
304
  declare const SCOPE = "@geonosis/";
270
305
  /**
@@ -286,8 +321,9 @@ declare const declaredFor: ({ dir, specifier, workspaces, }: {
286
321
  at: string;
287
322
  spec: string;
288
323
  } | undefined;
289
- declare const checkLoaded: ({ configs, root, workspaces, }: {
324
+ declare const checkLoaded: ({ configs, home, root, workspaces, }: {
290
325
  configs: DiscoveredConfig[];
326
+ home?: string;
291
327
  root: string;
292
328
  workspaces: Workspace[];
293
329
  }) => Promise<Finding[]>;
@@ -370,4 +406,4 @@ declare const checkRunner: ({ ratchet, root, workspaces, }: {
370
406
  workspaces: Workspace[];
371
407
  }) => Finding[];
372
408
 
373
- export { CHECKS, COMPOSITION_ROOT, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, ENVELOPES_DIR, FIXED_GROUP, FLOOR_PACKAGES, type Finding, GEONOSIS_FILE, type HeldTogether, KIT_GROUP, type LastEventRecord, MANIFEST_FILE, type Manifest, NOT_WRITTEN, NO_ENVELOPES, type ObservabilityConfig, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkDeployed, checkDrift, checkEnvelopes, checkExercised, checkGroup, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, declaredGroupsOf, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
409
+ export { CHECKS, COMPOSITION_ROOT, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, ENVELOPES_DIR, FIXED_GROUP, FLOOR_PACKAGES, type Finding, GEONOSIS_FILE, type HeldTogether, INSTALLED_PLUGINS, KIT_GROUP, type LastEventRecord, MANIFEST_FILE, type Manifest, NOT_WRITTEN, NO_ENVELOPES, type ObservabilityConfig, PLUGIN_NAME, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkClaudePlugin, checkDeployed, checkDrift, checkEnvelopes, checkExercised, checkGroup, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, declaredGroupsOf, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, installedPluginVersions, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
package/dist/index.js CHANGED
@@ -8,14 +8,17 @@ import {
8
8
  FIXED_GROUP,
9
9
  FLOOR_PACKAGES,
10
10
  GEONOSIS_FILE,
11
+ INSTALLED_PLUGINS,
11
12
  KIT_GROUP,
12
13
  MANIFEST_FILE,
13
14
  NOT_WRITTEN,
14
15
  NO_ENVELOPES,
16
+ PLUGIN_NAME,
15
17
  RATCHET_FILE,
16
18
  READERS,
17
19
  SCOPE,
18
20
  checkBaseline,
21
+ checkClaudePlugin,
19
22
  checkDeployed,
20
23
  checkDrift,
21
24
  checkEnvelopes,
@@ -33,6 +36,7 @@ import {
33
36
  enabledRulesOf,
34
37
  formatDoctor,
35
38
  formatJson,
39
+ installedPluginVersions,
36
40
  packageDirOf,
37
41
  pluginVersionOf,
38
42
  readConfig,
@@ -43,7 +47,7 @@ import {
43
47
  resolveFrom,
44
48
  runDoctor,
45
49
  satisfies
46
- } from "./chunk-R2R2OYH3.js";
50
+ } from "./chunk-DR5P4QYC.js";
47
51
  export {
48
52
  CHECKS,
49
53
  COMPOSITION_ROOT,
@@ -54,14 +58,17 @@ export {
54
58
  FIXED_GROUP,
55
59
  FLOOR_PACKAGES,
56
60
  GEONOSIS_FILE,
61
+ INSTALLED_PLUGINS,
57
62
  KIT_GROUP,
58
63
  MANIFEST_FILE,
59
64
  NOT_WRITTEN,
60
65
  NO_ENVELOPES,
66
+ PLUGIN_NAME,
61
67
  RATCHET_FILE,
62
68
  READERS,
63
69
  SCOPE,
64
70
  checkBaseline,
71
+ checkClaudePlugin,
65
72
  checkDeployed,
66
73
  checkDrift,
67
74
  checkEnvelopes,
@@ -79,6 +86,7 @@ export {
79
86
  enabledRulesOf,
80
87
  formatDoctor,
81
88
  formatJson,
89
+ installedPluginVersions,
82
90
  packageDirOf,
83
91
  pluginVersionOf,
84
92
  readConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/doctor",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "The adoption doctor — declared ≠ loaded, enabled ≠ exercised, a baseline that grew, a runner whose exit code is the only verdict.",
6
6
  "keywords": [
@@ -35,10 +35,10 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@geonosis/lint-parity": "2.1.0"
38
+ "@geonosis/lint-parity": "2.2.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@geonosis/ratchet": "2.1.0"
41
+ "@geonosis/ratchet": "2.2.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "oxlint": ">=1.77"