@driftdev/cli 1.9.0 → 1.11.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/dist/drift.js CHANGED
@@ -573,8 +573,13 @@ async function extractSpecFromRef(ref, entry, cwd = process.cwd()) {
573
573
  }
574
574
 
575
575
  // src/utils/output.ts
576
+ function elapsed(startTime) {
577
+ if (process.env.SOURCE_DATE_EPOCH !== undefined)
578
+ return 0;
579
+ return Date.now() - startTime;
580
+ }
576
581
  function formatOutput(command, data, startTime, version, humanRenderer, next) {
577
- const duration = Date.now() - startTime;
582
+ const duration = elapsed(startTime);
578
583
  const envelope = {
579
584
  ok: true,
580
585
  data,
@@ -593,8 +598,8 @@ function formatOutput(command, data, startTime, version, humanRenderer, next) {
593
598
  }
594
599
  return envelope;
595
600
  }
596
- function formatError(command, error, startTime, version, suggestion) {
597
- const duration = Date.now() - startTime;
601
+ function formatError(command, error, startTime, version, suggestion, exitCode = 2) {
602
+ const duration = elapsed(startTime);
598
603
  if (shouldRenderHuman()) {
599
604
  process.stdout.write(`
600
605
  ${c.red("x")} ${error}
@@ -615,7 +620,7 @@ function formatError(command, error, startTime, version, suggestion) {
615
620
  process.stderr.write(`drift ${command} failed: ${error}
616
621
  `);
617
622
  }
618
- process.exitCode = 1;
623
+ process.exitCode = exitCode;
619
624
  }
620
625
  function formatWarning(message) {
621
626
  if (shouldRenderHuman()) {
@@ -710,7 +715,8 @@ function validateConfig(raw) {
710
715
  }
711
716
  if (errors.length > 0)
712
717
  return { ok: false, errors };
713
- return { ok: true, config: mergeDefaults(obj) };
718
+ const { $schema: _schema, ...rest } = obj;
719
+ return { ok: true, config: mergeDefaults(rest) };
714
720
  }
715
721
 
716
722
  // src/config/loader.ts
@@ -3321,9 +3327,9 @@ function renderNotFound(exportName, suggestions, startTime, version) {
3321
3327
  `));
3322
3328
  process.exitCode = 1;
3323
3329
  } else if (suggestions.length > 0) {
3324
- formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version);
3330
+ formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version, undefined, 1);
3325
3331
  } else {
3326
- formatError("get", `Export '${exportName}' not found`, startTime, version);
3332
+ formatError("get", `Export '${exportName}' not found`, startTime, version, undefined, 1);
3327
3333
  }
3328
3334
  }
3329
3335
 
@@ -3857,6 +3863,7 @@ function generateConfig(packages) {
3857
3863
  const worstCoverage = Math.min(...packages.map((p) => p.coverage));
3858
3864
  const threshold = Math.max(0, Math.floor(worstCoverage) - 5);
3859
3865
  return {
3866
+ $schema: "https://unpkg.com/@driftdev/cli/schemas/drift.config.schema.json",
3860
3867
  coverage: { min: threshold }
3861
3868
  };
3862
3869
  }
@@ -3945,6 +3952,26 @@ function renderLint(data, next) {
3945
3952
  `);
3946
3953
  }
3947
3954
 
3955
+ // src/utils/annotations.ts
3956
+ function escapeData(value) {
3957
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
3958
+ }
3959
+ function escapeProperty(value) {
3960
+ return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
3961
+ }
3962
+ function emitAnnotations(issues, level = "error") {
3963
+ for (const issue of issues) {
3964
+ const props = [];
3965
+ if (issue.filePath)
3966
+ props.push(`file=${escapeProperty(issue.filePath)}`);
3967
+ if (issue.line !== undefined)
3968
+ props.push(`line=${issue.line}`);
3969
+ props.push(`title=${escapeProperty(`drift: ${issue.export || "docs"}`)}`);
3970
+ process.stdout.write(`::${level} ${props.join(",")}::${escapeData(issue.issue)}
3971
+ `);
3972
+ }
3973
+ }
3974
+
3948
3975
  // src/utils/docs-corpus.ts
3949
3976
  import { readFileSync as readFileSync17, statSync as statSync2 } from "node:fs";
3950
3977
  import * as path24 from "node:path";
@@ -3985,7 +4012,7 @@ function readPackageName(cwd = process.cwd()) {
3985
4012
 
3986
4013
  // src/commands/lint.ts
3987
4014
  function registerLintCommand(program) {
3988
- program.command("lint [entry]").description("Cross-reference docs against the API surface for accuracy issues").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language (inferred from --spec/--abi/.clar; default typescript)").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document: path or URL (implies --lang openapi)").option("--docs <patterns...>", "Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)").action(async (entry, options) => {
4015
+ program.command("lint [entry]").description("Cross-reference docs against the API surface for accuracy issues").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language (inferred from --spec/--abi/.clar; default typescript)").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document: path or URL (implies --lang openapi)").option("--docs <patterns...>", "Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)").option("--annotations", "Emit GitHub Actions ::error annotations for findings").action(async (entry, options) => {
3989
4016
  const startTime = Date.now();
3990
4017
  const version = getVersion();
3991
4018
  try {
@@ -4086,6 +4113,8 @@ function registerLintCommand(program) {
4086
4113
  reason: `${issues.length} issue${issues.length === 1 ? "" : "s"} found`
4087
4114
  } : undefined;
4088
4115
  formatOutput("lint", data, startTime, version, renderLint, next);
4116
+ if (options.annotations && issues.length > 0)
4117
+ emitAnnotations(issues);
4089
4118
  if (issues.length > 0) {
4090
4119
  if (!shouldRenderHuman()) {
4091
4120
  process.stderr.write(`${issues.length} issue${issues.length === 1 ? "" : "s"} found
@@ -4385,6 +4414,51 @@ function registerMcpCommand(program) {
4385
4414
  cli.push("--min", String(args.min));
4386
4415
  return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4387
4416
  });
4417
+ server.registerTool("drift_lint", {
4418
+ title: "Lint docs accuracy",
4419
+ description: "Cross-reference docs against the real API surface: wrong parameter names/types, stale signatures, references to removed exports, prose drift in markdown docs. Returns issues with file/line locations. The primary fix loop: lint, fix what it reports, lint again until clean.",
4420
+ inputSchema: {
4421
+ ...truthShape,
4422
+ docs: z.array(z.string()).optional().describe("Markdown corpus for prose drift: glob patterns or directories (overrides repo-local defaults)")
4423
+ }
4424
+ }, async (args) => {
4425
+ const cli = ["lint"];
4426
+ if (args.entry)
4427
+ cli.push(args.entry);
4428
+ if (args.docs?.length)
4429
+ cli.push("--docs", ...args.docs);
4430
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4431
+ });
4432
+ server.registerTool("drift_coverage", {
4433
+ title: "Measure docs coverage",
4434
+ description: "Documentation coverage of an API surface: documented vs total exports, score, what is missing per rule. Use to find undocumented surface before writing docs.",
4435
+ inputSchema: {
4436
+ ...truthShape,
4437
+ min: z.number().optional().describe("Minimum coverage % (result fails below it)")
4438
+ }
4439
+ }, async (args) => {
4440
+ const cli = ["coverage"];
4441
+ if (args.entry)
4442
+ cli.push(args.entry);
4443
+ if (args.min !== undefined)
4444
+ cli.push("--min", String(args.min));
4445
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4446
+ });
4447
+ server.registerTool("drift_health", {
4448
+ title: "Docs health score",
4449
+ description: "Combined docs health score (coverage + accuracy) for an API surface, with the breakdown. The single number to gate on in CI.",
4450
+ inputSchema: {
4451
+ ...truthShape,
4452
+ min: z.number().optional().describe("Minimum health % (result fails below it)")
4453
+ }
4454
+ }, async (args) => {
4455
+ const cli = ["health"];
4456
+ if (args.entry)
4457
+ cli.push(args.entry);
4458
+ if (args.min !== undefined)
4459
+ cli.push("--min", String(args.min));
4460
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4461
+ });
4388
4462
  server.registerTool("drift_diff", {
4389
4463
  title: "Diff two API specs",
4390
4464
  description: "Diff two extracted API specs (TypeScript only today): spec files or git refs. Reports added/removed/changed exports. Useful for changelog and release-note verification.",
@@ -5243,5 +5317,5 @@ if (userArgs.length === 0 && !hasHelpOrVersion) {
5243
5317
  process.argv.splice(2, 0, "scan");
5244
5318
  }
5245
5319
  program.parseAsync().catch(() => {
5246
- process.exit(1);
5320
+ process.exit(2);
5247
5321
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftdev/cli",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Drift CLI - detect when your docs drift from your code",
5
5
  "keywords": [
6
6
  "typescript",
@@ -38,7 +38,8 @@
38
38
  "test": "bun run --cwd ../sdk build && bun run --cwd ../adapters/clarity build && bun run --cwd ../adapters/openapi build && for f in test/*.test.ts; do bun test \"$f\" || exit 1; done"
39
39
  },
40
40
  "files": [
41
- "dist"
41
+ "dist",
42
+ "schemas"
42
43
  ],
43
44
  "engines": {
44
45
  "node": ">=20"
@@ -46,10 +47,10 @@
46
47
  "dependencies": {
47
48
  "@driftdev/clarity-adapter": "^1.0.1",
48
49
  "@driftdev/openapi-adapter": "^1.0.1",
49
- "@driftdev/sdk": "^1.9.0",
50
+ "@driftdev/sdk": "^1.11.0",
50
51
  "@modelcontextprotocol/sdk": "^1.29.0",
51
- "@openpkg-ts/sdk": "^0.40.0",
52
- "@openpkg-ts/spec": "^0.37.0",
52
+ "@openpkg-ts/sdk": "^0.43.0",
53
+ "@openpkg-ts/spec": "^0.43.0",
53
54
  "chalk": "^5.4.1",
54
55
  "commander": "^14.0.0",
55
56
  "zod": "^4.2.1"
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@driftdev/cli/schemas/drift.config.schema.json",
4
+ "title": "Drift configuration",
5
+ "description": "Configuration for the drift CLI, loaded from drift.config.json or the \"drift\" key in package.json. JSON only — no code execution.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "Path or URL of this schema, for editor/agent validation"
11
+ },
12
+ "entry": {
13
+ "type": "string",
14
+ "description": "Entry point override (otherwise auto-detected)"
15
+ },
16
+ "include": {
17
+ "description": "Export include patterns",
18
+ "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }]
19
+ },
20
+ "exclude": {
21
+ "description": "Export exclude patterns",
22
+ "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }]
23
+ },
24
+ "coverage": {
25
+ "type": "object",
26
+ "description": "Coverage thresholds",
27
+ "properties": {
28
+ "min": {
29
+ "type": "number",
30
+ "minimum": 0,
31
+ "maximum": 100,
32
+ "description": "Minimum coverage % (exit 1 if below)"
33
+ },
34
+ "ratchet": {
35
+ "type": "boolean",
36
+ "description": "Ratchet: effective min = max(min, highest coverage ever recorded)"
37
+ }
38
+ },
39
+ "additionalProperties": false
40
+ },
41
+ "lint": {
42
+ "type": "boolean",
43
+ "default": true,
44
+ "description": "Enable lint checks"
45
+ },
46
+ "docs": {
47
+ "type": "object",
48
+ "description": "Markdown docs discovery",
49
+ "properties": {
50
+ "include": {
51
+ "type": "array",
52
+ "items": { "type": "string" },
53
+ "description": "Glob patterns for markdown docs to include"
54
+ },
55
+ "exclude": {
56
+ "type": "array",
57
+ "items": { "type": "string" },
58
+ "description": "Glob patterns for markdown docs to exclude"
59
+ },
60
+ "remote": {
61
+ "type": "array",
62
+ "description": "Remote repos to sync docs on breaking changes",
63
+ "items": {
64
+ "type": "object",
65
+ "properties": {
66
+ "repo": {
67
+ "type": "string",
68
+ "pattern": "^[^/]+/[^/]+$",
69
+ "description": "Target repo in \"owner/repo\" format"
70
+ },
71
+ "branch": {
72
+ "type": "string",
73
+ "description": "Target branch (defaults to repo's default branch)"
74
+ }
75
+ },
76
+ "required": ["repo"],
77
+ "additionalProperties": false
78
+ }
79
+ }
80
+ },
81
+ "additionalProperties": false
82
+ }
83
+ },
84
+ "additionalProperties": true
85
+ }