@geonosis/doctor 2.0.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.
- package/README.md +9 -7
- package/dist/{chunk-SMCRXSH5.js → chunk-DR5P4QYC.js} +672 -490
- package/dist/doctor-cli.js +127 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +9 -1
- package/package.json +5 -2
package/dist/doctor-cli.js
CHANGED
|
@@ -1,12 +1,108 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CHECKS,
|
|
3
|
+
ENVELOPES_DIR,
|
|
3
4
|
formatDoctor,
|
|
4
5
|
formatJson,
|
|
5
6
|
runDoctor
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DR5P4QYC.js";
|
|
8
|
+
|
|
9
|
+
// src/doctor-cli.ts
|
|
10
|
+
import { fstatSync, statSync } from "fs";
|
|
11
|
+
import { resolve as resolve2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/envelope-write.ts
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
+
import { dirname, join, resolve } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
|
|
18
|
+
var UnbalancedEnvelope = class extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "UnbalancedEnvelope";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
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
|
+
};
|
|
34
|
+
var writeEnvelope = ({
|
|
35
|
+
envelope,
|
|
36
|
+
next,
|
|
37
|
+
root
|
|
38
|
+
}) => {
|
|
39
|
+
if (envelope.tool.trim() === "") {
|
|
40
|
+
throw new UnbalancedEnvelope(
|
|
41
|
+
`an envelope with no tool name cannot be filed or reported against. Next: ${next}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (envelope.version.trim() === "") {
|
|
45
|
+
throw new UnbalancedEnvelope(
|
|
46
|
+
`${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!isCount(envelope.considered) || !isCount(envelope.read)) {
|
|
50
|
+
throw new UnbalancedEnvelope(
|
|
51
|
+
`${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
|
|
55
|
+
throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
|
|
56
|
+
}
|
|
57
|
+
refuseForbiddenRoot(root);
|
|
58
|
+
const at = envelopePath(root, envelope.tool);
|
|
59
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
60
|
+
writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
61
|
+
`);
|
|
62
|
+
return at;
|
|
63
|
+
};
|
|
64
|
+
var UNKNOWN = "unknown";
|
|
65
|
+
var versionIn = (dir) => {
|
|
66
|
+
try {
|
|
67
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
68
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
69
|
+
} catch {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var versionOf = (moduleUrl) => {
|
|
74
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
75
|
+
for (; ; ) {
|
|
76
|
+
const found = versionIn(dir);
|
|
77
|
+
if (found !== void 0) return found;
|
|
78
|
+
const up = dirname(dir);
|
|
79
|
+
if (up === dir) return UNKNOWN;
|
|
80
|
+
dir = up;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var DOCTOR_TOOL = "doctor";
|
|
84
|
+
var DOCTOR_NEXT = "geonosis-doctor --json and count the checks it printed \u2014 this run considered every question the tool has, and a check nobody asked for is excused by name rather than left out of the denominator";
|
|
85
|
+
var doctorEnvelope = ({
|
|
86
|
+
durationMs,
|
|
87
|
+
report
|
|
88
|
+
}) => {
|
|
89
|
+
const ran = new Set(report.ran);
|
|
90
|
+
return {
|
|
91
|
+
considered: CHECKS.length,
|
|
92
|
+
durationMs,
|
|
93
|
+
excused: CHECKS.filter((check) => !ran.has(check)).map((check) => ({
|
|
94
|
+
path: check,
|
|
95
|
+
reason: "not asked for by --only, so this run says nothing about it"
|
|
96
|
+
})),
|
|
97
|
+
findings: report.findings,
|
|
98
|
+
read: report.ran.length,
|
|
99
|
+
refused: [],
|
|
100
|
+
tool: DOCTOR_TOOL,
|
|
101
|
+
version: versionOf(import.meta.url)
|
|
102
|
+
};
|
|
103
|
+
};
|
|
7
104
|
|
|
8
105
|
// src/doctor-cli.ts
|
|
9
|
-
import { resolve } from "path";
|
|
10
106
|
var ABOUT = {
|
|
11
107
|
baseline: `The ratchet's numbers at HEAD against another ref. A ratchet lowers its own baseline
|
|
12
108
|
when a number shrinks, so nothing inside one checkout can see a branch raise one back
|
|
@@ -94,6 +190,10 @@ plugin-route-namespaced on its package root, while the shipped corpus says acme
|
|
|
94
190
|
|
|
95
191
|
--print-config-shape every file this reads, and the keys it reads out of each
|
|
96
192
|
|
|
193
|
+
Every run writes .geonosis/envelopes/doctor.json: the ten checks CONSIDERED against the ones this
|
|
194
|
+
run read, with the rest excused by name, so a --only run can never read as a verdict about the whole
|
|
195
|
+
tree. The envelope check SKIPs that one file \u2014 this run is the one writing it.
|
|
196
|
+
|
|
97
197
|
Exits 1 when any line is a FAIL or an UNJUDGED \u2014 a question this could not ask is not a pass \u2014 and
|
|
98
198
|
2 when the run could not be made at all. --strict promotes every WARN to a FAIL. --json prints the
|
|
99
199
|
whole report for a CI step to read.`;
|
|
@@ -134,8 +234,8 @@ var parseDoctorArgs = (argv, cwd) => {
|
|
|
134
234
|
...baseline === void 0 ? {} : { baseline },
|
|
135
235
|
json,
|
|
136
236
|
...only === void 0 || only.length === 0 ? {} : { only },
|
|
137
|
-
...read["--oxlint"] === void 0 ? {} : { oxlint:
|
|
138
|
-
root:
|
|
237
|
+
...read["--oxlint"] === void 0 ? {} : { oxlint: resolve2(cwd, read["--oxlint"]) },
|
|
238
|
+
root: resolve2(cwd, read["--root"] ?? cwd),
|
|
139
239
|
strict
|
|
140
240
|
};
|
|
141
241
|
};
|
|
@@ -186,6 +286,15 @@ geonosis.json \u2192 "rails.egress"
|
|
|
186
286
|
.geonosis/rails-run.json
|
|
187
287
|
deniedEgress object[] what a run reached for and was refused. A GATE AT ZERO (D-025): one
|
|
188
288
|
entry fails, and it is never a counter to hold flat`;
|
|
289
|
+
var sameFile = (fd, path) => {
|
|
290
|
+
try {
|
|
291
|
+
const open = fstatSync(fd);
|
|
292
|
+
const there = statSync(path);
|
|
293
|
+
return open.dev === there.dev && open.ino === there.ino;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
189
298
|
var main = async () => {
|
|
190
299
|
const argv = process.argv.slice(2);
|
|
191
300
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -199,7 +308,21 @@ var main = async () => {
|
|
|
199
308
|
return 0;
|
|
200
309
|
}
|
|
201
310
|
const { json, ...options } = parseDoctorArgs(argv, process.cwd());
|
|
311
|
+
const started = Date.now();
|
|
202
312
|
const report = await runDoctor(options);
|
|
313
|
+
const at = envelopePath(options.root, "doctor");
|
|
314
|
+
if (sameFile(process.stdout.fd, at)) {
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
`geonosis-doctor: stdout is ${ENVELOPES_DIR}/doctor.json, so no envelope was written \u2014 it would have overwritten the report redirected there. Send --json somewhere else and the envelope is written after every run.
|
|
317
|
+
`
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
writeEnvelope({
|
|
321
|
+
envelope: doctorEnvelope({ durationMs: Date.now() - started, report }),
|
|
322
|
+
next: DOCTOR_NEXT,
|
|
323
|
+
root: options.root
|
|
324
|
+
});
|
|
325
|
+
}
|
|
203
326
|
process.stdout.write(json ? formatJson(report) : formatDoctor(report));
|
|
204
327
|
return report.ok ? 0 : 1;
|
|
205
328
|
};
|
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
|
|
@@ -83,6 +85,8 @@ type DoctorOptions = {
|
|
|
83
85
|
type DoctorReport = {
|
|
84
86
|
counts: Record<Verdict, number>;
|
|
85
87
|
findings: Finding[];
|
|
88
|
+
/** The checks this run asked. `--only` makes it a subset, and the envelope's denominator needs it. */
|
|
89
|
+
ran: CheckName[];
|
|
86
90
|
/**
|
|
87
91
|
* False as soon as one line is a FAIL or an UNJUDGED — after `--strict` has promoted the
|
|
88
92
|
* warnings. "We could not tell" is not a pass; it is the quietest way there is of reading green.
|
|
@@ -136,7 +140,7 @@ declare const discoverConfigs: (root: string) => DiscoveredConfig[];
|
|
|
136
140
|
declare const discoverWorkspaces: (root: string) => Workspace[];
|
|
137
141
|
declare const readRatchet: (root: string) => RatchetConfig | undefined;
|
|
138
142
|
|
|
139
|
-
declare const runDoctor: ({ baseline, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
|
|
143
|
+
declare const runDoctor: ({ baseline, home, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
|
|
140
144
|
|
|
141
145
|
/** The file `create-geonosis --domains` writes, held against the scaffold's own name by a test. */
|
|
142
146
|
declare const COMPOSITION_ROOT = "src/platform.ts";
|
|
@@ -189,7 +193,7 @@ declare const checkDrift: ({ readers, root, userSettings, workspaces, }: {
|
|
|
189
193
|
* duplicated here on purpose: it is one comparison over three numbers, and it is the check.
|
|
190
194
|
*/
|
|
191
195
|
declare const ENVELOPES_DIR = ".geonosis/envelopes";
|
|
192
|
-
declare const NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
|
|
196
|
+
declare const NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one (this run\u2019s own doctor.json is not one of them: it is written after these checks, and balanced at write time)";
|
|
193
197
|
declare const checkEnvelopes: ({ root }: {
|
|
194
198
|
root: string;
|
|
195
199
|
}) => Finding[];
|
|
@@ -263,6 +267,39 @@ declare const checkGroup: ({ groups, root, workspaces, }: {
|
|
|
263
267
|
workspaces: Workspace[];
|
|
264
268
|
}) => Finding[];
|
|
265
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
|
+
|
|
266
303
|
/** The scope this tool asks about. A config's other plugins are somebody else's release to check. */
|
|
267
304
|
declare const SCOPE = "@geonosis/";
|
|
268
305
|
/**
|
|
@@ -284,8 +321,9 @@ declare const declaredFor: ({ dir, specifier, workspaces, }: {
|
|
|
284
321
|
at: string;
|
|
285
322
|
spec: string;
|
|
286
323
|
} | undefined;
|
|
287
|
-
declare const checkLoaded: ({ configs, root, workspaces, }: {
|
|
324
|
+
declare const checkLoaded: ({ configs, home, root, workspaces, }: {
|
|
288
325
|
configs: DiscoveredConfig[];
|
|
326
|
+
home?: string;
|
|
289
327
|
root: string;
|
|
290
328
|
workspaces: Workspace[];
|
|
291
329
|
}) => Promise<Finding[]>;
|
|
@@ -368,4 +406,4 @@ declare const checkRunner: ({ ratchet, root, workspaces, }: {
|
|
|
368
406
|
workspaces: Workspace[];
|
|
369
407
|
}) => Finding[];
|
|
370
408
|
|
|
371
|
-
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-
|
|
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.
|
|
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,7 +35,10 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@geonosis/lint-parity": "2.
|
|
38
|
+
"@geonosis/lint-parity": "2.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@geonosis/ratchet": "2.2.0"
|
|
39
42
|
},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"oxlint": ">=1.77"
|