@stsepelin/checktrail 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +15 -5
- package/dist/src/cli.js +75 -3
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/onboarding.d.ts +50 -0
- package/dist/src/onboarding.js +295 -0
- package/dist/src/types.d.ts +1 -1
- package/dist/src/types.js +1 -1
- package/dist/src/typescript-arguments.d.ts +4 -0
- package/dist/src/typescript-arguments.js +10 -0
- package/dist/src/typescript-runner.d.ts +1 -0
- package/dist/src/typescript-runner.js +19 -0
- package/dist/src/typescript.js +3 -9
- package/dist/src/vue-tsc-runner.js +2 -1
- package/docs/ACCEPTANCE.md +23 -13
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/INSTALLATION.md +10 -6
- package/docs/LANGUAGES.md +14 -0
- package/docs/NATIVE-CI.md +4 -0
- package/docs/ONBOARDING.md +133 -0
- package/docs/PUBLIC-ADOPTION.md +116 -0
- package/docs/RELEASE.md +41 -9
- package/docs/SKILLS.md +123 -0
- package/docs/STATUS.md +26 -8
- package/docs/TYPESCRIPT.md +78 -0
- package/docs/measurements/public-adoption-alpha2.json +2034 -0
- package/docs/measurements/typescript-legacy-replay.json +39 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -4,18 +4,28 @@ Local code validation with a CLI, MCP tools, and evidence of what actually ran.
|
|
|
4
4
|
|
|
5
5
|
Formerly Repo Verifier. See the [rename guide](docs/RENAMING.md) for existing source checkouts.
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Published preview: 0.1.0-alpha.2; source candidate: 0.1.0-alpha.3.** Public source is available at
|
|
8
8
|
[stsepelin/checktrail](https://github.com/stsepelin/checktrail).
|
|
9
9
|
See [implementation status](docs/STATUS.md), the [plan](docs/PLAN.md) and the
|
|
10
10
|
[language matrix](docs/LANGUAGES.md) before relying on an adapter.
|
|
11
11
|
[Installation](docs/INSTALLATION.md) covers the CLI, Claude Code and Codex.
|
|
12
|
+
[Agent skills](docs/SKILLS.md) provides setup, validation and review workflows
|
|
13
|
+
installable with `npx skills add stsepelin/checktrail`.
|
|
14
|
+
[Project setup](docs/ONBOARDING.md) covers `init`, `doctor`
|
|
15
|
+
and MCP configuration generator.
|
|
12
16
|
[Release preparation](docs/RELEASE.md) records publication and verification gates. The
|
|
13
17
|
[milestone audit](docs/ACCEPTANCE.md) separates implemented profiles from open
|
|
14
18
|
acceptance work; [client checks](docs/CLIENTS.md) record actual application coverage.
|
|
19
|
+
[Public adoption](docs/PUBLIC-ADOPTION.md) records the published package on five
|
|
20
|
+
pinned libraries, including setup friction and compatibility gaps.
|
|
21
|
+
The alpha.3 candidate fixes the recorded [TypeScript 4.9.5 incompatibility](docs/TYPESCRIPT.md);
|
|
22
|
+
it has not been published.
|
|
15
23
|
|
|
16
|
-
The [
|
|
17
|
-
passed at `
|
|
18
|
-
|
|
24
|
+
The [hosted matrix](https://github.com/stsepelin/checktrail/actions/runs/35590670960)
|
|
25
|
+
passed at release commit `4ce8398` on Linux and macOS. The exact npm tarball and
|
|
26
|
+
fresh CLI/MCP installations were verified. The
|
|
27
|
+
[MCP Registry entry](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.stsepelin%2Fchecktrail/versions/0.1.0-alpha.2)
|
|
28
|
+
is active; execution remains disabled by default.
|
|
19
29
|
|
|
20
30
|
Checktrail discovers projects, plans registered checks, invokes native tools
|
|
21
31
|
when explicitly trusted, and reports results without turning skipped or empty
|
|
@@ -41,7 +51,7 @@ node dist/src/cli.js run --root examples/javascript --trust-project --detailed
|
|
|
41
51
|
`--trust-project`: tests, compiler plugins and project configuration can execute
|
|
42
52
|
code with your user privileges. This is not a sandbox.
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
Successful commands return JSON except help/version; input errors use stderr. Exit codes:
|
|
45
55
|
|
|
46
56
|
| Code | Meaning |
|
|
47
57
|
| ---- | --------------------------------------------------------------------------------------- |
|
package/dist/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { initialize, diagnose, mcpConfiguration, mcpClients, } from "./onboarding.js";
|
|
3
4
|
import { createReviewContext, projectReviewContext, receiveReview, projectReviewReceipt, } from "./review.js";
|
|
4
5
|
import { fetchPolicyPack } from "./fetch-pack.js";
|
|
5
6
|
import { externalReferencesSchema, loadExternalAdapters, } from "./external-adapter.js";
|
|
@@ -22,11 +23,15 @@ import { VERSION } from "./types.js";
|
|
|
22
23
|
import { validateContracts, projectContractReport } from "./contracts.js";
|
|
23
24
|
import { compareRuntimeInventories, projectRuntimeComparison, } from "./runtime-inventory.js";
|
|
24
25
|
async function main() {
|
|
25
|
-
const { values, positionals } = parseArgs({
|
|
26
|
+
const { values, positionals, tokens } = parseArgs({
|
|
27
|
+
tokens: true,
|
|
26
28
|
allowPositionals: true,
|
|
27
29
|
strict: true,
|
|
28
30
|
options: {
|
|
29
31
|
root: { type: "string", default: "." },
|
|
32
|
+
write: { type: "boolean", default: false },
|
|
33
|
+
check: { type: "string", multiple: true },
|
|
34
|
+
client: { type: "string" },
|
|
30
35
|
"trust-project": { type: "boolean", default: false },
|
|
31
36
|
"allow-execution": { type: "boolean", default: false },
|
|
32
37
|
detailed: { type: "boolean", default: false },
|
|
@@ -58,12 +63,29 @@ async function main() {
|
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
60
65
|
if (values.help || positionals.length === 0) {
|
|
61
|
-
process.stdout.write("checktrail <inspect|plan|run|serve|adapters|import-junit|export-sarif|create-baseline|compare-findings|compare-runtime|check-contracts|check-architecture|guidance|review-context|review-receipt|mutate|fetch-pack> [--root PATH] [--detailed] [--base REVISION] [--policy-overlay PATH] [--adapter PATH#sha256=DIGEST ...]\nRun: --trust-project [--timeout-ms 30000] [--allow-env NAME ...]\nServe: --allow-execution (optional; disabled by default) [--allow-env NAME ...]\nFetch-pack: --url HTTPS_URL --sha256 DIGEST --output RELATIVE_JSON_PATH\nExit: 0 passed/read-only success/completed advisory experiment, 1 failed checks, 2 incomplete/error\n");
|
|
66
|
+
process.stdout.write("checktrail <init|doctor|mcp-config|inspect|plan|run|serve|adapters|import-junit|export-sarif|create-baseline|compare-findings|compare-runtime|check-contracts|check-architecture|guidance|review-context|review-receipt|mutate|fetch-pack> [--root PATH] [--detailed] [--base REVISION] [--policy-overlay PATH] [--adapter PATH#sha256=DIGEST ...]\nInit: [--write] [--check PATH#CHECK_ID ...] (preview by default; preserves existing config)\nDoctor: [--detailed] [--policy-overlay PATH] [--allow-env NAME ...] [--adapter PATH#sha256=DIGEST ...] (no execution)\nMcp-config: --client codex|claude-code|claude-desktop|cursor|vscode (prints configuration only)\nRun: --trust-project [--timeout-ms 30000] [--allow-env NAME ...]\nServe: --allow-execution (optional; disabled by default) [--allow-env NAME ...]\nFetch-pack: --url HTTPS_URL --sha256 DIGEST --output RELATIVE_JSON_PATH\nExit: 0 passed/read-only success/completed advisory experiment, 1 failed checks, 2 incomplete/error\n");
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
if (positionals.length !== 1)
|
|
65
70
|
throw new Error("Expected exactly one command");
|
|
66
71
|
const command = positionals[0];
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
if (token.kind !== "option")
|
|
74
|
+
continue;
|
|
75
|
+
if (["write", "check"].includes(token.name) && command !== "init")
|
|
76
|
+
throw new Error(`--${token.name} applies only to init`);
|
|
77
|
+
if (token.name === "client" && command !== "mcp-config")
|
|
78
|
+
throw new Error("--client applies only to mcp-config");
|
|
79
|
+
const allowed = command === "init"
|
|
80
|
+
? ["root", "write", "check"]
|
|
81
|
+
: command === "doctor"
|
|
82
|
+
? ["root", "detailed", "policy-overlay", "allow-env", "adapter"]
|
|
83
|
+
: command === "mcp-config"
|
|
84
|
+
? ["root", "client"]
|
|
85
|
+
: undefined;
|
|
86
|
+
if (allowed && !allowed.includes(token.name))
|
|
87
|
+
throw new Error(`--${token.name} does not apply to ${command}`);
|
|
88
|
+
}
|
|
67
89
|
if (values["allow-review-source"] &&
|
|
68
90
|
(!values.detailed ||
|
|
69
91
|
!["review-context", "review-receipt", "serve"].includes(command)))
|
|
@@ -85,7 +107,15 @@ async function main() {
|
|
|
85
107
|
};
|
|
86
108
|
}));
|
|
87
109
|
if (externalAdapters.length &&
|
|
88
|
-
![
|
|
110
|
+
![
|
|
111
|
+
"inspect",
|
|
112
|
+
"plan",
|
|
113
|
+
"run",
|
|
114
|
+
"serve",
|
|
115
|
+
"adapters",
|
|
116
|
+
"guidance",
|
|
117
|
+
"doctor",
|
|
118
|
+
].includes(command))
|
|
89
119
|
throw new Error("External adapters apply only to inspection, planning, validation, serving and derived guidance");
|
|
90
120
|
if (externalAdapters.length && command === "guidance" && values.input)
|
|
91
121
|
throw new Error("External adapters require derived guidance, not an imported context");
|
|
@@ -103,7 +133,49 @@ async function main() {
|
|
|
103
133
|
]);
|
|
104
134
|
return;
|
|
105
135
|
}
|
|
136
|
+
if (command === "doctor") {
|
|
137
|
+
const result = await diagnose(values.root, {
|
|
138
|
+
detailed: values.detailed,
|
|
139
|
+
externalAdapters,
|
|
140
|
+
environment: inheritEnvironment(values["allow-env"] ?? []),
|
|
141
|
+
...(values["policy-overlay"]
|
|
142
|
+
? { policyOverlay: values["policy-overlay"] }
|
|
143
|
+
: {}),
|
|
144
|
+
});
|
|
145
|
+
print(result);
|
|
146
|
+
process.exitCode = result.status === "no-static-blockers" ? 0 : 2;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
106
149
|
const root = await realpath(values.root);
|
|
150
|
+
if (command === "init") {
|
|
151
|
+
const selections = new Map();
|
|
152
|
+
for (const value of values.check ?? []) {
|
|
153
|
+
const split = value.lastIndexOf("#");
|
|
154
|
+
if (split <= 0 || split === value.length - 1)
|
|
155
|
+
throw new Error("Check selection requires PATH#CHECK_ID");
|
|
156
|
+
const selectedPath = value.slice(0, split);
|
|
157
|
+
selections.set(selectedPath, [
|
|
158
|
+
...(selections.get(selectedPath) ?? []),
|
|
159
|
+
value.slice(split + 1),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
const result = await initialize(root, {
|
|
163
|
+
write: values.write,
|
|
164
|
+
selections: [...selections].map(([selectedPath, checks]) => ({
|
|
165
|
+
path: selectedPath,
|
|
166
|
+
checks,
|
|
167
|
+
})),
|
|
168
|
+
});
|
|
169
|
+
print(result);
|
|
170
|
+
process.exitCode = result.status === "needs-selection" ? 2 : 0;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (command === "mcp-config") {
|
|
174
|
+
if (!mcpClients.includes(values.client))
|
|
175
|
+
throw new Error(`mcp-config requires --client ${mcpClients.join("|")}`);
|
|
176
|
+
print(await mcpConfiguration(root, values.client));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
107
179
|
if (command === "fetch-pack") {
|
|
108
180
|
if (!values.url || !values.sha256 || !values.output)
|
|
109
181
|
throw new Error("fetch-pack requires --url HTTPS_URL --sha256 DIGEST --output RELATIVE_JSON_PATH");
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { validateContracts } from "./contracts.js";
|
|
|
3
3
|
export type { ContractBundle, ContractReport } from "./contract-schema.js";
|
|
4
4
|
export type { PlanOptions, ValidationOptions } from "./engine.js";
|
|
5
5
|
export { adapters } from "./adapters.js";
|
|
6
|
+
export { initialize, diagnose, mcpConfiguration, mcpClients, } from "./onboarding.js";
|
|
7
|
+
export type { InitOptions, InitResult, DoctorIssue, DoctorResult, McpClient, } from "./onboarding.js";
|
|
6
8
|
export type { PolicyPack, PackReference } from "./policy-pack.js";
|
|
7
9
|
export { fetchPolicyPack } from "./fetch-pack.js";
|
|
8
10
|
export type { FetchPackOptions, FetchedPack } from "./fetch-pack.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createPlan, validate, aggregate } from "./engine.js";
|
|
2
2
|
export { validateContracts } from "./contracts.js";
|
|
3
3
|
export { adapters } from "./adapters.js";
|
|
4
|
+
export { initialize, diagnose, mcpConfiguration, mcpClients, } from "./onboarding.js";
|
|
4
5
|
export { fetchPolicyPack } from "./fetch-pack.js";
|
|
5
6
|
export { projectPlan, projectReport } from "./output.js";
|
|
6
7
|
export { importJUnit } from "./junit.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Config } from "./config.js";
|
|
2
|
+
import { type PlanOptions } from "./engine.js";
|
|
3
|
+
export interface InitOptions {
|
|
4
|
+
write?: boolean;
|
|
5
|
+
selections?: {
|
|
6
|
+
path: string;
|
|
7
|
+
checks: string[];
|
|
8
|
+
}[];
|
|
9
|
+
}
|
|
10
|
+
export interface InitResult {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
engineVersion: string;
|
|
13
|
+
status: "preview" | "created" | "preserved" | "needs-selection";
|
|
14
|
+
configuration: Config | null;
|
|
15
|
+
unresolved: {
|
|
16
|
+
path: string;
|
|
17
|
+
adapter: string;
|
|
18
|
+
choices: string[];
|
|
19
|
+
}[];
|
|
20
|
+
executionEnabled: false;
|
|
21
|
+
}
|
|
22
|
+
export declare function initialize(root: string, options?: InitOptions): Promise<InitResult>;
|
|
23
|
+
export interface DoctorIssue {
|
|
24
|
+
code: "configuration-error" | "empty-plan" | "unselected-project" | "unavailable-check" | "missing-executable" | "package-metadata" | "unsupported-platform";
|
|
25
|
+
check?: string;
|
|
26
|
+
adapter?: string;
|
|
27
|
+
project?: string;
|
|
28
|
+
detail?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DoctorResult {
|
|
31
|
+
schemaVersion: 1;
|
|
32
|
+
engineVersion: string;
|
|
33
|
+
status: "no-static-blockers" | "attention-required";
|
|
34
|
+
validationPerformed: false;
|
|
35
|
+
projects: number;
|
|
36
|
+
checks: number;
|
|
37
|
+
issues: DoctorIssue[];
|
|
38
|
+
unverified: string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function diagnose(root: string, options?: Omit<PlanOptions, "base"> & {
|
|
41
|
+
detailed?: boolean;
|
|
42
|
+
}): Promise<DoctorResult>;
|
|
43
|
+
export declare const mcpClients: readonly ["codex", "claude-code", "claude-desktop", "cursor", "vscode"];
|
|
44
|
+
export type McpClient = (typeof mcpClients)[number];
|
|
45
|
+
export declare function mcpConfiguration(root: string, client: McpClient): Promise<{
|
|
46
|
+
client: McpClient;
|
|
47
|
+
format: "json" | "toml";
|
|
48
|
+
configuration: string;
|
|
49
|
+
executionEnabled: false;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, link, lstat, mkdtemp, realpath, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { adapters, checksFor } from "./adapters.js";
|
|
5
|
+
import { configSchema } from "./config.js";
|
|
6
|
+
import { createPlan } from "./engine.js";
|
|
7
|
+
import { inventory, readProjectFile } from "./inventory.js";
|
|
8
|
+
import { identifyTool } from "./tool-versions.js";
|
|
9
|
+
import { VERSION } from "./types.js";
|
|
10
|
+
async function configurationExists(root) {
|
|
11
|
+
try {
|
|
12
|
+
const entry = await lstat(path.join(root, "checktrail.json"));
|
|
13
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
14
|
+
throw new Error("checktrail.json must be a regular file, not a symlink or directory");
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === "ENOENT")
|
|
19
|
+
return false;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function initialize(root, options = {}) {
|
|
24
|
+
root = await realpath(root);
|
|
25
|
+
const exists = await configurationExists(root);
|
|
26
|
+
const { source, plan } = await createPlan(root);
|
|
27
|
+
const result = {
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
engineVersion: VERSION,
|
|
30
|
+
status: "preview",
|
|
31
|
+
configuration: null,
|
|
32
|
+
unresolved: [],
|
|
33
|
+
executionEnabled: false,
|
|
34
|
+
};
|
|
35
|
+
if (exists) {
|
|
36
|
+
if (options.selections?.length)
|
|
37
|
+
throw new Error("Existing configuration is preserved; edit its policy explicitly to change checks");
|
|
38
|
+
return { ...result, status: "preserved" };
|
|
39
|
+
}
|
|
40
|
+
const selected = new Map();
|
|
41
|
+
for (const selection of options.selections ?? []) {
|
|
42
|
+
if (selected.has(selection.path))
|
|
43
|
+
throw new Error("Duplicate selection path");
|
|
44
|
+
if (!selection.checks.length ||
|
|
45
|
+
new Set(selection.checks).size !== selection.checks.length)
|
|
46
|
+
throw new Error("Selections require nonempty, unique check IDs");
|
|
47
|
+
selected.set(selection.path, selection.checks);
|
|
48
|
+
}
|
|
49
|
+
for (const selectedPath of selected.keys())
|
|
50
|
+
if (!plan.projects.some((project) => project.path === selectedPath))
|
|
51
|
+
throw new Error("Selection does not match a discovered project path");
|
|
52
|
+
const grouped = new Map();
|
|
53
|
+
for (const project of plan.projects) {
|
|
54
|
+
const registered = adapters.find((adapter) => adapter.id === project.adapter).checks;
|
|
55
|
+
const requested = selected.get(project.path);
|
|
56
|
+
let ids = requested?.filter((id) => registered.includes(id));
|
|
57
|
+
if (!requested) {
|
|
58
|
+
ids = plan.checks
|
|
59
|
+
.filter((check) => check.project === project.path &&
|
|
60
|
+
check.adapter === project.adapter &&
|
|
61
|
+
registered.includes(check.id))
|
|
62
|
+
.map((check) => check.id);
|
|
63
|
+
if (project.adapter === "python")
|
|
64
|
+
ids = [];
|
|
65
|
+
if (project.adapter === "javascript") {
|
|
66
|
+
const manifest = JSON.parse(await readProjectFile(root, path.posix.join(project.path, "package.json")));
|
|
67
|
+
const runners = {
|
|
68
|
+
"node --test": "javascript.node-test",
|
|
69
|
+
"vitest run": "javascript.vitest",
|
|
70
|
+
jest: "javascript.jest",
|
|
71
|
+
"playwright test": "javascript.playwright",
|
|
72
|
+
};
|
|
73
|
+
const script = manifest?.scripts?.test;
|
|
74
|
+
ids =
|
|
75
|
+
typeof script === "string" && Object.hasOwn(runners, script)
|
|
76
|
+
? [runners[script]]
|
|
77
|
+
: [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!ids?.length ||
|
|
81
|
+
plan.checks.some((check) => check.project === project.path &&
|
|
82
|
+
check.adapter === project.adapter &&
|
|
83
|
+
check.kind === "unsupported")) {
|
|
84
|
+
result.unresolved.push({
|
|
85
|
+
path: project.path,
|
|
86
|
+
adapter: project.adapter,
|
|
87
|
+
choices: [...registered],
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const candidates = await checksFor(source, project, ids);
|
|
92
|
+
if (ids.some((id) => !candidates.some((check) => check.id === id)))
|
|
93
|
+
throw new Error("Selected check is inapplicable to the discovered project");
|
|
94
|
+
const checks = grouped.get(project.path) ?? new Set();
|
|
95
|
+
for (const id of ids)
|
|
96
|
+
checks.add(id);
|
|
97
|
+
grouped.set(project.path, checks);
|
|
98
|
+
}
|
|
99
|
+
for (const [selectedPath, ids] of selected)
|
|
100
|
+
if (ids.some((id) => !grouped.get(selectedPath)?.has(id)))
|
|
101
|
+
throw new Error("Selected check is unknown or inapplicable");
|
|
102
|
+
if (!plan.projects.length || result.unresolved.length)
|
|
103
|
+
return { ...result, status: "needs-selection" };
|
|
104
|
+
result.configuration = configSchema.parse({
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
projects: [...grouped].map(([projectPath, checks]) => ({
|
|
107
|
+
path: projectPath,
|
|
108
|
+
checks: [...checks].sort(),
|
|
109
|
+
})),
|
|
110
|
+
});
|
|
111
|
+
if (!options.write)
|
|
112
|
+
return result;
|
|
113
|
+
if ((await inventory(root)).fingerprint !== source.fingerprint)
|
|
114
|
+
throw new Error("Project changed during setup; review a fresh preview");
|
|
115
|
+
const temporary = await mkdtemp(path.join(root, ".checktrail-init-"));
|
|
116
|
+
try {
|
|
117
|
+
const staged = path.join(temporary, "checktrail.json");
|
|
118
|
+
await writeFile(staged, `${JSON.stringify(result.configuration, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
119
|
+
// An exclusive link publishes complete bytes without replacing a concurrent writer.
|
|
120
|
+
await link(staged, path.join(root, "checktrail.json"));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await rm(temporary, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
return { ...result, status: "created" };
|
|
126
|
+
}
|
|
127
|
+
async function executableAvailable(executable, cwd, searchPath) {
|
|
128
|
+
const candidates = executable.includes("/") || path.isAbsolute(executable)
|
|
129
|
+
? [path.resolve(cwd, executable)]
|
|
130
|
+
: searchPath
|
|
131
|
+
.split(path.delimiter)
|
|
132
|
+
.map((directory) => path.resolve(cwd, directory, executable));
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
try {
|
|
135
|
+
if (!(await stat(candidate)).isFile())
|
|
136
|
+
continue;
|
|
137
|
+
await access(candidate, constants.X_OK);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* Other PATH entries may provide the executable. */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
export async function diagnose(root, options = {}) {
|
|
147
|
+
const issues = [];
|
|
148
|
+
const result = {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
engineVersion: VERSION,
|
|
151
|
+
status: "attention-required",
|
|
152
|
+
validationPerformed: false,
|
|
153
|
+
projects: 0,
|
|
154
|
+
checks: 0,
|
|
155
|
+
issues,
|
|
156
|
+
unverified: [
|
|
157
|
+
"Tool versions and runtime compatibility",
|
|
158
|
+
"Importable modules and runtime services",
|
|
159
|
+
"Validation results; run with explicit project trust",
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
const add = (issue) => {
|
|
163
|
+
const summary = { ...issue };
|
|
164
|
+
delete summary.project;
|
|
165
|
+
delete summary.detail;
|
|
166
|
+
issues.push(options.detailed ? issue : summary);
|
|
167
|
+
};
|
|
168
|
+
if (process.platform === "win32")
|
|
169
|
+
add({ code: "unsupported-platform" });
|
|
170
|
+
try {
|
|
171
|
+
root = await realpath(root);
|
|
172
|
+
await configurationExists(root);
|
|
173
|
+
const { plan } = await createPlan(root, {
|
|
174
|
+
...(options.externalAdapters
|
|
175
|
+
? { externalAdapters: options.externalAdapters }
|
|
176
|
+
: {}),
|
|
177
|
+
...(options.policyOverlay
|
|
178
|
+
? { policyOverlay: options.policyOverlay }
|
|
179
|
+
: {}),
|
|
180
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
181
|
+
});
|
|
182
|
+
result.projects = plan.projects.length;
|
|
183
|
+
result.checks = plan.checks.length;
|
|
184
|
+
if (!plan.checks.length)
|
|
185
|
+
add({ code: "empty-plan" });
|
|
186
|
+
for (const project of plan.projects)
|
|
187
|
+
if (!plan.checks.some((check) => check.project === project.path && check.adapter === project.adapter))
|
|
188
|
+
add({
|
|
189
|
+
code: "unselected-project",
|
|
190
|
+
project: project.path,
|
|
191
|
+
adapter: project.adapter,
|
|
192
|
+
});
|
|
193
|
+
for (const check of plan.checks) {
|
|
194
|
+
const context = {
|
|
195
|
+
check: check.id,
|
|
196
|
+
adapter: check.adapter,
|
|
197
|
+
project: check.project,
|
|
198
|
+
};
|
|
199
|
+
if (check.unavailableReason)
|
|
200
|
+
add({
|
|
201
|
+
...context,
|
|
202
|
+
code: "unavailable-check",
|
|
203
|
+
detail: check.unavailableReason,
|
|
204
|
+
});
|
|
205
|
+
const commands = [...check.commands];
|
|
206
|
+
for (const tool of check.tools ?? []) {
|
|
207
|
+
if (tool.source === "version-command")
|
|
208
|
+
commands.push(tool.command);
|
|
209
|
+
else if (tool.source === "package-metadata") {
|
|
210
|
+
const identity = await identifyTool(root, tool, async () => {
|
|
211
|
+
throw new Error("Doctor must not execute version probes");
|
|
212
|
+
});
|
|
213
|
+
if (identity.status !== "identified")
|
|
214
|
+
add({
|
|
215
|
+
...context,
|
|
216
|
+
code: "package-metadata",
|
|
217
|
+
detail: `Missing or invalid package metadata: ${tool.name}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const wrapped = check.adapter === "jvm"
|
|
222
|
+
? ["java"]
|
|
223
|
+
: check.adapter === "dotnet"
|
|
224
|
+
? ["dotnet"]
|
|
225
|
+
: check.id === "infrastructure.actionlint"
|
|
226
|
+
? ["actionlint"]
|
|
227
|
+
: [];
|
|
228
|
+
for (const executable of wrapped)
|
|
229
|
+
commands.push({ executable, args: [], cwd: check.project });
|
|
230
|
+
const checked = new Set();
|
|
231
|
+
for (const command of commands) {
|
|
232
|
+
const searchPath = command.env?.PATH ??
|
|
233
|
+
(check.environment?.includes("PATH")
|
|
234
|
+
? options.environment?.PATH
|
|
235
|
+
: undefined) ??
|
|
236
|
+
process.env.PATH ??
|
|
237
|
+
"/usr/bin:/bin";
|
|
238
|
+
const cwd = path.resolve(root, command.cwd);
|
|
239
|
+
const key = JSON.stringify([command.executable, cwd, searchPath]);
|
|
240
|
+
if (checked.has(key))
|
|
241
|
+
continue;
|
|
242
|
+
checked.add(key);
|
|
243
|
+
if (!(await executableAvailable(command.executable, cwd, searchPath)))
|
|
244
|
+
add({
|
|
245
|
+
...context,
|
|
246
|
+
code: "missing-executable",
|
|
247
|
+
detail: `Executable unavailable or not executable: ${command.executable}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
add({
|
|
254
|
+
code: "configuration-error",
|
|
255
|
+
detail: error instanceof Error ? error.message : "Cannot inspect project",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
result.status = issues.length ? "attention-required" : "no-static-blockers";
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
export const mcpClients = [
|
|
262
|
+
"codex",
|
|
263
|
+
"claude-code",
|
|
264
|
+
"claude-desktop",
|
|
265
|
+
"cursor",
|
|
266
|
+
"vscode",
|
|
267
|
+
];
|
|
268
|
+
export async function mcpConfiguration(root, client) {
|
|
269
|
+
if (!mcpClients.includes(client))
|
|
270
|
+
throw new Error("Unknown MCP client");
|
|
271
|
+
root = await realpath(root);
|
|
272
|
+
if (!(await stat(root)).isDirectory())
|
|
273
|
+
throw new Error("MCP root must be a directory");
|
|
274
|
+
if (root.includes("${"))
|
|
275
|
+
throw new Error("MCP root contains client variable syntax; choose a path without ${");
|
|
276
|
+
const server = {
|
|
277
|
+
command: "npx",
|
|
278
|
+
args: [
|
|
279
|
+
"--yes",
|
|
280
|
+
"--ignore-scripts",
|
|
281
|
+
`@stsepelin/checktrail@${VERSION}`,
|
|
282
|
+
"serve",
|
|
283
|
+
"--root",
|
|
284
|
+
root,
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
client,
|
|
289
|
+
executionEnabled: false,
|
|
290
|
+
format: client === "codex" ? "toml" : "json",
|
|
291
|
+
configuration: client === "codex"
|
|
292
|
+
? `[mcp_servers.checktrail]\ncommand = "npx"\nargs = ${JSON.stringify(server.args)}\n`
|
|
293
|
+
: `${JSON.stringify(client === "vscode" ? { servers: { checktrail: { type: "stdio", ...server } } } : { mcpServers: { checktrail: server } }, null, 2)}\n`,
|
|
294
|
+
};
|
|
295
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExternalIdentity } from "./external-adapter.js";
|
|
2
2
|
import type { RuntimeInventory } from "./runtime-inventory.js";
|
|
3
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
3
|
+
export declare const VERSION = "0.1.0-alpha.3";
|
|
4
4
|
export declare const PARSERS: readonly ["vue-router-json", "nuxt-json", "exit", "empty", "node-events", "unittest", "go-scope-test", "go-scope-analysis", "golangci-json", "staticcheck-json", "go-json", "typescript-build-json", "tsc-files", "eslint-json", "vitest-json", "playwright-json", "jest-json", "pytest-json", "fastapi-json", "django-json", "laravel-json", "rust-json", "clang-json", "java-json", "dotnet-json", "actionlint-json", "external-json", "ruby-syntax", "silent-syntax", "ruff-json", "mypy-json", "phpstan-json", "phpunit-junit", "pint-json"];
|
|
5
5
|
export type Status = "passed" | "failed" | "unavailable" | "skipped" | "error" | "inconclusive";
|
|
6
6
|
export type Outcome = "passed" | "failed" | "incomplete";
|
package/dist/src/types.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function typecheckArguments(compiler, args) {
|
|
2
|
+
const probe = compiler.parseCommandLine(["--noCheck", "false"]);
|
|
3
|
+
if (probe.errors.length === 0 && probe.options.noCheck === false)
|
|
4
|
+
return [...args, "--noCheck", "false"];
|
|
5
|
+
if (probe.errors.length === 1 &&
|
|
6
|
+
probe.errors[0].code === 5023 &&
|
|
7
|
+
probe.options.noCheck === undefined)
|
|
8
|
+
return args;
|
|
9
|
+
throw new Error("Cannot establish the compiler's noCheck option support");
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { withinRoot } from "./inventory.js";
|
|
4
|
+
import { typecheckArguments } from "./typescript-arguments.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const [entry, root, ...args] = process.argv.slice(2);
|
|
7
|
+
if (!entry || !root)
|
|
8
|
+
throw new Error("Invalid TypeScript compiler arguments");
|
|
9
|
+
const tool = await withinRoot(root, path.relative(root, entry));
|
|
10
|
+
const api = await withinRoot(root, path.relative(root, path.resolve(path.dirname(tool), "../lib/typescript.js")));
|
|
11
|
+
const load = createRequire(tool);
|
|
12
|
+
const ts = load(api);
|
|
13
|
+
process.argv = [process.execPath, tool, ...typecheckArguments(ts, args)];
|
|
14
|
+
load(tool);
|
|
15
|
+
}
|
|
16
|
+
main().catch((error) => {
|
|
17
|
+
process.stderr.write(`${error instanceof Error ? error.message : "TypeScript checking failed"}\n`);
|
|
18
|
+
process.exitCode = 2;
|
|
19
|
+
});
|
package/dist/src/typescript.js
CHANGED
|
@@ -16,18 +16,12 @@ export async function typescriptCheck(source, project, vue = false) {
|
|
|
16
16
|
{
|
|
17
17
|
executable: process.execPath,
|
|
18
18
|
args: [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
compiler,
|
|
23
|
-
source.root,
|
|
24
|
-
]
|
|
25
|
-
: [compiler]),
|
|
19
|
+
fileURLToPath(new URL(vue ? "./vue-tsc-runner.js" : "./typescript-runner.js", import.meta.url)),
|
|
20
|
+
compiler,
|
|
21
|
+
source.root,
|
|
26
22
|
"--project",
|
|
27
23
|
"./tsconfig.json",
|
|
28
24
|
"--noEmit",
|
|
29
|
-
"--noCheck",
|
|
30
|
-
"false",
|
|
31
25
|
"--pretty",
|
|
32
26
|
"false",
|
|
33
27
|
"--incremental",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { typecheckArguments } from "./typescript-arguments.js";
|
|
3
4
|
import { withinRoot } from "./inventory.js";
|
|
4
5
|
async function main() {
|
|
5
6
|
const [entry, root, ...args] = process.argv.slice(2);
|
|
@@ -15,7 +16,7 @@ async function main() {
|
|
|
15
16
|
throw new Error("Vue template checking is disabled by skipTemplateCodegen");
|
|
16
17
|
const compiler = load(tool);
|
|
17
18
|
const tsc = await resolve("typescript/lib/tsc.js");
|
|
18
|
-
process.argv = [process.execPath, tool, ...args];
|
|
19
|
+
process.argv = [process.execPath, tool, ...typecheckArguments(ts, args)];
|
|
19
20
|
compiler.run(tsc);
|
|
20
21
|
}
|
|
21
22
|
main().catch((error) => {
|