@driftdev/cli 1.0.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +10 -8
  2. package/dist/drift.js +128 -56
  3. package/package.json +13 -15
package/README.md CHANGED
@@ -44,8 +44,8 @@ Entry auto-detects from `package.json` (`types`, `exports`, `main`, `module`, `b
44
44
 
45
45
  | Command | Description |
46
46
  |---------|-------------|
47
- | `drift scan [entry]` | Coverage + lint + prose drift + health in one pass |
48
- | `drift health [entry]` | Documentation health score (default command) |
47
+ | `drift scan [entry]` | Coverage + lint + prose drift + health in one pass (default command) |
48
+ | `drift health [entry]` | Documentation health score |
49
49
  | `drift ci` | CI checks on changed packages with PR comments |
50
50
 
51
51
  ### Analysis
@@ -61,8 +61,8 @@ Entry auto-detects from `package.json` (`types`, `exports`, `main`, `module`, `b
61
61
  | Command | Description |
62
62
  |---------|-------------|
63
63
  | `drift extract [entry]` | Extract full API spec as JSON |
64
- | `drift list [entry]` | List all exports with kinds |
65
- | `drift get <name> [entry]` | Inspect single export detail + types |
64
+ | `drift list [searchOrEntry]` | List all exports with kinds |
65
+ | `drift get <name>` | Inspect single export detail + types (entry auto-detected; `drift get <entry> <name>` to override) |
66
66
 
67
67
  ### Spec Operations
68
68
 
@@ -112,8 +112,10 @@ drift --tools
112
112
 
113
113
  Run coverage + lint + prose drift + health in one pass.
114
114
 
115
+ Default command — bare `drift` runs this.
116
+
115
117
  ```bash
116
- drift scan # single package
118
+ drift scan # single package (bare `drift` does the same)
117
119
  drift scan --min 80 # fail if health below 80%
118
120
  drift scan --all # all workspace packages
119
121
  drift scan --all --private # include private packages
@@ -121,7 +123,7 @@ drift scan --all --private # include private packages
121
123
 
122
124
  ## lint
123
125
 
124
- Cross-reference JSDoc against code signatures. Detects 15 drift types across 4 categories (structural, semantic, example, prose). Prose detection scans markdown files for broken import references.
126
+ Cross-reference JSDoc against code signatures. Detects 16 drift types across 4 categories (structural, semantic, example, prose). Prose detection scans markdown files for broken import references and method calls that don't exist on any exported type.
125
127
 
126
128
  ```bash
127
129
  drift lint # single package
@@ -144,7 +146,7 @@ drift coverage --all # all workspace packages
144
146
  Weighted health score: completeness (coverage) + accuracy (lint).
145
147
 
146
148
  ```bash
147
- drift health # default command (bare `drift`)
149
+ drift health
148
150
  drift health --min 80
149
151
  drift health --all
150
152
  ```
@@ -201,7 +203,7 @@ All commands return structured JSON when piped or with `--json`:
201
203
  {
202
204
  "ok": true,
203
205
  "data": { "score": 88, "documented": 243, "total": 275 },
204
- "meta": { "command": "coverage", "duration": 1234, "version": "0.38.0" }
206
+ "meta": { "command": "coverage", "duration": 1234, "version": "1.4.0" }
205
207
  }
206
208
  ```
207
209
 
package/dist/drift.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -1161,26 +1165,6 @@ function registerBreakingCommand(program) {
1161
1165
  });
1162
1166
  }
1163
1167
 
1164
- // src/commands/commands.ts
1165
- var GROUPS = {
1166
- Composed: ["scan", "ci", "health"],
1167
- Analysis: ["coverage", "lint", "examples"],
1168
- Extraction: ["extract", "list", "get"],
1169
- Comparison: ["diff", "breaking", "semver", "changelog"],
1170
- Setup: ["init", "config", "context"],
1171
- Plumbing: ["validate", "filter", "cache", "report", "release"]
1172
- };
1173
- function registerCommandsCommand(program) {
1174
- program.command("commands").description("List all available commands grouped by category").action(() => {
1175
- const maxGroup = Math.max(...Object.keys(GROUPS).map((g) => g.length));
1176
- for (const [group, cmds] of Object.entries(GROUPS)) {
1177
- const pad2 = " ".repeat(maxGroup - group.length);
1178
- process.stdout.write(` ${group}${pad2} ${cmds.join(", ")}
1179
- `);
1180
- }
1181
- });
1182
- }
1183
-
1184
1168
  // src/formatters/cache.ts
1185
1169
  function renderCacheStatus(data) {
1186
1170
  const lines = [];
@@ -1974,6 +1958,26 @@ function registerCiCommand(program) {
1974
1958
  });
1975
1959
  }
1976
1960
 
1961
+ // src/commands/commands.ts
1962
+ var GROUPS = {
1963
+ Composed: ["scan", "ci", "health"],
1964
+ Analysis: ["coverage", "lint", "examples"],
1965
+ Extraction: ["extract", "list", "get"],
1966
+ Comparison: ["diff", "breaking", "semver", "changelog"],
1967
+ Setup: ["init", "config", "context"],
1968
+ Plumbing: ["validate", "filter", "cache", "report", "release"]
1969
+ };
1970
+ function registerCommandsCommand(program) {
1971
+ program.command("commands").description("List all available commands grouped by category").action(() => {
1972
+ const maxGroup = Math.max(...Object.keys(GROUPS).map((g) => g.length));
1973
+ for (const [group, cmds] of Object.entries(GROUPS)) {
1974
+ const pad2 = " ".repeat(maxGroup - group.length);
1975
+ process.stdout.write(` ${group}${pad2} ${cmds.join(", ")}
1976
+ `);
1977
+ }
1978
+ });
1979
+ }
1980
+
1977
1981
  // src/commands/config.ts
1978
1982
  init_global();
1979
1983
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
@@ -3162,7 +3166,7 @@ function getPackageInfo(cwd) {
3162
3166
  }
3163
3167
  }
3164
3168
  function registerHealthCommand(program) {
3165
- program.command("health [entry]").description("Show documentation health score (default command)").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
3169
+ program.command("health [entry]").description("Show documentation health score").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
3166
3170
  const startTime = Date.now();
3167
3171
  const version = getVersion();
3168
3172
  try {
@@ -4249,6 +4253,8 @@ function registerReportCommand(program) {
4249
4253
  // src/commands/scan.ts
4250
4254
  import { existsSync as existsSync16, readFileSync as readFileSync20 } from "node:fs";
4251
4255
  import * as path27 from "node:path";
4256
+ import { fromSource } from "@driftdev/clarity-adapter";
4257
+ import { fromDocument } from "@driftdev/openapi-adapter";
4252
4258
  import {
4253
4259
  buildExportRegistry as buildExportRegistry2,
4254
4260
  computeDrift as computeDrift8,
@@ -4327,11 +4333,59 @@ function getPackageInfo2(cwd) {
4327
4333
  return {};
4328
4334
  }
4329
4335
  }
4336
+ async function loadSpec2(entryFile, lang, abiPath, specPath) {
4337
+ if (lang === "openapi") {
4338
+ if (!specPath)
4339
+ throw new Error("--spec is required when --lang openapi");
4340
+ if (!existsSync16(specPath))
4341
+ throw new Error(`Spec file not found: ${specPath}`);
4342
+ const document = readFileSync20(specPath, "utf-8");
4343
+ const name = path27.basename(specPath, path27.extname(specPath));
4344
+ const apiSpec = fromDocument(document);
4345
+ if (!apiSpec.meta.name || apiSpec.meta.name === "openapi")
4346
+ apiSpec.meta.name = name;
4347
+ return { apiSpec, packageName: apiSpec.meta.name, packageVersion: apiSpec.meta.version };
4348
+ }
4349
+ if (lang === "clarity") {
4350
+ if (!abiPath)
4351
+ throw new Error("--abi is required when --lang clarity");
4352
+ if (!existsSync16(entryFile))
4353
+ throw new Error(`Source file not found: ${entryFile}`);
4354
+ if (!existsSync16(abiPath))
4355
+ throw new Error(`ABI file not found: ${abiPath}`);
4356
+ const source = readFileSync20(entryFile, "utf-8");
4357
+ const abi = JSON.parse(readFileSync20(abiPath, "utf-8"));
4358
+ const name = path27.basename(entryFile, path27.extname(entryFile));
4359
+ const pkg2 = getPackageInfo2(process.cwd());
4360
+ const apiSpec = fromSource(source, abi, { name, version: pkg2.version });
4361
+ return { apiSpec, packageName: pkg2.name ?? name, packageVersion: pkg2.version };
4362
+ }
4363
+ const { spec } = await cachedExtract(entryFile);
4364
+ const pkg = getPackageInfo2(process.cwd());
4365
+ return { apiSpec: spec, packageName: pkg.name, packageVersion: pkg.version };
4366
+ }
4330
4367
  function registerScanCommand(program) {
4331
- program.command("scan [entry]").description("Run coverage + lint + prose drift in one pass").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
4368
+ program.command("scan [entry]").description("Run coverage + lint + prose drift in one pass").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--lang <language>", "Source language", "typescript").option("--abi <path>", "ABI JSON file (required for --lang clarity)").option("--spec <path>", "OpenAPI document (required for --lang openapi)").action(async (entry, options) => {
4332
4369
  const startTime = Date.now();
4333
4370
  const version = getVersion();
4334
4371
  try {
4372
+ const lang = options.lang ?? "typescript";
4373
+ if (lang !== "typescript" && lang !== "clarity" && lang !== "openapi") {
4374
+ formatError("scan", `Unknown language: ${lang}`, startTime, version);
4375
+ return;
4376
+ }
4377
+ if (lang !== "typescript" && options.all) {
4378
+ formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4379
+ return;
4380
+ }
4381
+ if (lang === "clarity" && !options.abi) {
4382
+ formatError("scan", "--abi is required when --lang clarity", startTime, version);
4383
+ return;
4384
+ }
4385
+ if (lang === "openapi" && !options.spec) {
4386
+ formatError("scan", "--spec is required when --lang openapi", startTime, version);
4387
+ return;
4388
+ }
4335
4389
  if (options.all) {
4336
4390
  const allPackages = discoverPackages(process.cwd());
4337
4391
  if (!allPackages || allPackages.length === 0) {
@@ -4346,16 +4400,16 @@ function registerScanCommand(program) {
4346
4400
  }
4347
4401
  const rows = [];
4348
4402
  let anyFail = false;
4349
- for (const pkg2 of packages) {
4350
- const { spec: spec2 } = await cachedExtract(pkg2.entry);
4351
- const exps = spec2.exports ?? [];
4403
+ for (const pkg of packages) {
4404
+ const { spec } = await cachedExtract(pkg.entry);
4405
+ const exps = spec.exports ?? [];
4352
4406
  let documented2 = 0;
4353
4407
  for (const e of exps) {
4354
4408
  if (e.description?.trim())
4355
4409
  documented2++;
4356
4410
  }
4357
4411
  const coverage = exps.length > 0 ? Math.round(documented2 / exps.length * 100) : 100;
4358
- const driftResult2 = computeDrift8(spec2);
4412
+ const driftResult2 = computeDrift8(spec);
4359
4413
  const issues2 = [];
4360
4414
  for (const [exportName, drifts] of driftResult2.exports) {
4361
4415
  for (const d of drifts)
@@ -4366,7 +4420,7 @@ function registerScanCommand(program) {
4366
4420
  if (min2 !== undefined && h2.health < min2)
4367
4421
  anyFail = true;
4368
4422
  rows.push({
4369
- name: pkg2.name,
4423
+ name: pkg.name,
4370
4424
  exports: exps.length,
4371
4425
  coverage,
4372
4426
  lintIssues: issues2.length,
@@ -4385,9 +4439,13 @@ function registerScanCommand(program) {
4385
4439
  return;
4386
4440
  }
4387
4441
  const { config } = loadConfig();
4388
- const entryFile = entry ? path27.resolve(process.cwd(), entry) : config.entry ? path27.resolve(process.cwd(), config.entry) : detectEntry();
4389
- const { spec } = await cachedExtract(entryFile);
4390
- const exports = spec.exports ?? [];
4442
+ const specPath = options.spec ? path27.resolve(process.cwd(), options.spec) : undefined;
4443
+ const entryFile = lang === "openapi" ? specPath : entry ? path27.resolve(process.cwd(), entry) : lang === "clarity" ? (() => {
4444
+ throw new Error("Entry file required for --lang clarity");
4445
+ })() : config.entry ? path27.resolve(process.cwd(), config.entry) : detectEntry();
4446
+ const abiPath = options.abi ? path27.resolve(process.cwd(), options.abi) : undefined;
4447
+ const { apiSpec, packageName, packageVersion } = await loadSpec2(entryFile, lang, abiPath, specPath);
4448
+ const exports = apiSpec.exports ?? [];
4391
4449
  const total = exports.length;
4392
4450
  let documented = 0;
4393
4451
  for (const exp of exports) {
@@ -4395,7 +4453,7 @@ function registerScanCommand(program) {
4395
4453
  documented++;
4396
4454
  }
4397
4455
  const coverageScore = total > 0 ? Math.round(documented / total * 100) : 100;
4398
- const driftResult = computeDrift8(spec);
4456
+ const driftResult = computeDrift8(apiSpec);
4399
4457
  const issues = [];
4400
4458
  for (const [exportName, drifts] of driftResult.exports) {
4401
4459
  for (const drift of drifts) {
@@ -4408,30 +4466,35 @@ function registerScanCommand(program) {
4408
4466
  });
4409
4467
  }
4410
4468
  }
4411
- try {
4412
- const pkgJsonPath = path27.resolve(process.cwd(), "package.json");
4413
- const pkgJson = JSON.parse(readFileSync20(pkgJsonPath, "utf-8"));
4414
- const packageName = pkgJson.name;
4415
- if (packageName) {
4416
- const registry = buildExportRegistry2(spec);
4417
- const markdownFiles = discoverMarkdownFiles2(process.cwd(), config.docs);
4418
- const proseDrifts = detectProseDrift2({ packageName, markdownFiles, registry });
4419
- for (const drift of proseDrifts) {
4420
- issues.push({
4421
- export: drift.target ?? "",
4422
- issue: drift.issue,
4423
- ...drift.suggestion ? { location: drift.suggestion } : {},
4424
- filePath: drift.filePath,
4425
- line: drift.line
4469
+ if (lang === "typescript") {
4470
+ try {
4471
+ const pkgJsonPath = path27.resolve(process.cwd(), "package.json");
4472
+ const pkgJson = JSON.parse(readFileSync20(pkgJsonPath, "utf-8"));
4473
+ const pkgName = pkgJson.name;
4474
+ if (pkgName) {
4475
+ const registry = buildExportRegistry2(apiSpec);
4476
+ const markdownFiles = discoverMarkdownFiles2(process.cwd(), config.docs);
4477
+ const proseDrifts = detectProseDrift2({
4478
+ packageName: pkgName,
4479
+ markdownFiles,
4480
+ registry
4426
4481
  });
4482
+ for (const drift of proseDrifts) {
4483
+ issues.push({
4484
+ export: drift.target ?? "",
4485
+ issue: drift.issue,
4486
+ ...drift.suggestion ? { location: drift.suggestion } : {},
4487
+ filePath: drift.filePath,
4488
+ line: drift.line
4489
+ });
4490
+ }
4427
4491
  }
4492
+ } catch (err) {
4493
+ formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
4428
4494
  }
4429
- } catch (err) {
4430
- formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
4431
4495
  }
4432
4496
  const healthIssues = issues.map((i) => ({ export: i.export, issue: i.issue }));
4433
4497
  const h = computeHealth(total, documented, healthIssues);
4434
- const pkg = getPackageInfo2(process.cwd());
4435
4498
  let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
4436
4499
  if (min !== undefined && config.coverage?.ratchet) {
4437
4500
  const ratchet = computeRatchetMin(min);
@@ -4443,8 +4506,8 @@ function registerScanCommand(program) {
4443
4506
  lint: { issues, count: issues.length },
4444
4507
  health: h.health,
4445
4508
  pass,
4446
- packageName: pkg.name,
4447
- packageVersion: pkg.version
4509
+ packageName,
4510
+ packageVersion
4448
4511
  };
4449
4512
  let next;
4450
4513
  if (issues.length > 0) {
@@ -4645,11 +4708,20 @@ function extractCapabilities(program) {
4645
4708
  }
4646
4709
  ],
4647
4710
  workflows: {
4648
- "detect-drift": { steps: ["extract", "lint"], description: "Find stale JSDoc and prose drift" },
4711
+ "detect-drift": {
4712
+ steps: ["extract", "lint"],
4713
+ description: "Find stale JSDoc and prose drift"
4714
+ },
4649
4715
  "full-scan": { steps: ["scan"], description: "Coverage + lint + prose in one pass" },
4650
- "detect-and-enrich": { steps: ["scan", "context"], description: "Scan and generate agent context" },
4716
+ "detect-and-enrich": {
4717
+ steps: ["scan", "context"],
4718
+ description: "Scan and generate agent context"
4719
+ },
4651
4720
  "ci-pipeline": { steps: ["ci"], description: "Run CI checks on changed packages" },
4652
- "pre-release": { steps: ["scan", "breaking", "release"], description: "Full pre-release quality gate" }
4721
+ "pre-release": {
4722
+ steps: ["scan", "breaking", "release"],
4723
+ description: "Full pre-release quality gate"
4724
+ }
4653
4725
  }
4654
4726
  };
4655
4727
  }
@@ -4659,7 +4731,7 @@ var __filename2 = fileURLToPath2(import.meta.url);
4659
4731
  var __dirname3 = path29.dirname(__filename2);
4660
4732
  var packageJson = JSON.parse(readFileSync22(path29.join(__dirname3, "../package.json"), "utf-8"));
4661
4733
  var program = new Command;
4662
- program.name("drift").description("drift — documentation quality for TypeScript").version(packageJson.version).option("--json", "Force JSON output (default when piped)").option("--human", "Force human-readable output (default in terminal)").option("--config <path>", "Path to drift config file").option("--cwd <dir>", "Run as if started in <dir>").option("--no-cache", "Bypass spec cache").option("--tools", "List all available tools for agent use (JSON)").hook("preAction", (_thisCommand) => {
4734
+ program.name("drift").description("drift — documentation quality for TypeScript, Clarity, and OpenAPI").version(packageJson.version).option("--json", "Force JSON output (default when piped)").option("--human", "Force human-readable output (default in terminal)").option("--config <path>", "Path to drift config file").option("--cwd <dir>", "Run as if started in <dir>").option("--no-cache", "Bypass spec cache").option("--tools", "List all available tools for agent use (JSON)").hook("preAction", (_thisCommand) => {
4663
4735
  const opts = program.opts();
4664
4736
  if (opts.cwd) {
4665
4737
  process.chdir(path29.resolve(opts.cwd));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftdev/cli",
3
- "version": "1.0.0",
3
+ "version": "1.4.0",
4
4
  "description": "Drift CLI - Documentation coverage and drift detection for TypeScript",
5
5
  "keywords": [
6
6
  "typescript",
@@ -19,17 +19,9 @@
19
19
  "license": "MIT",
20
20
  "author": "Ryan Waits",
21
21
  "type": "module",
22
- "main": "./dist/index.js",
23
- "types": "./dist/index.d.ts",
24
22
  "bin": {
25
23
  "drift": "./dist/drift.js"
26
24
  },
27
- "exports": {
28
- ".": {
29
- "import": "./dist/index.js",
30
- "types": "./dist/index.d.ts"
31
- }
32
- },
33
25
  "scripts": {
34
26
  "build": "bunup",
35
27
  "dev": "bunup --watch",
@@ -37,22 +29,28 @@
37
29
  "lint": "biome check src/",
38
30
  "lint:fix": "biome check --write src/",
39
31
  "format": "biome format --write src/",
40
- "test": "for f in test/*.test.ts; do bun test \"$f\" || exit 1; done"
32
+ "prepublishOnly": "bun run build",
33
+ "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"
41
34
  },
42
35
  "files": [
43
36
  "dist"
44
37
  ],
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
45
41
  "dependencies": {
46
- "@driftdev/sdk": "^1.0.0",
47
- "@openpkg-ts/sdk": "^0.37.0",
42
+ "@driftdev/clarity-adapter": "^1.0.1",
43
+ "@driftdev/openapi-adapter": "^1.0.0",
44
+ "@driftdev/sdk": "^1.4.0",
45
+ "@openpkg-ts/sdk": "^0.38.0",
48
46
  "@openpkg-ts/spec": "^0.37.0",
49
47
  "chalk": "^5.4.1",
50
48
  "commander": "^14.0.0"
51
49
  },
52
50
  "devDependencies": {
53
- "@types/bun": "latest",
54
- "@types/node": "^20.0.0",
55
- "bunup": "latest"
51
+ "@types/bun": "^1.3.14",
52
+ "@types/node": "^24.0.0",
53
+ "bunup": "0.16.26"
56
54
  },
57
55
  "publishConfig": {
58
56
  "access": "public"