@driftdev/cli 1.4.0 → 1.5.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 (2) hide show
  1. package/dist/drift.js +594 -197
  2. package/package.json +11 -4
package/dist/drift.js CHANGED
@@ -52,7 +52,7 @@ var init_global = () => {};
52
52
 
53
53
  // src/drift.ts
54
54
  import { readFileSync as readFileSync22 } from "node:fs";
55
- import * as path29 from "node:path";
55
+ import * as path30 from "node:path";
56
56
  import { fileURLToPath as fileURLToPath2 } from "node:url";
57
57
  import { Command } from "commander";
58
58
 
@@ -1094,7 +1094,7 @@ function getVersion() {
1094
1094
  } catch {
1095
1095
  cached = "0.0.0";
1096
1096
  }
1097
- return cached;
1097
+ return cached ?? "0.0.0";
1098
1098
  }
1099
1099
 
1100
1100
  // src/commands/breaking.ts
@@ -2259,7 +2259,7 @@ function registerContextCommand(program) {
2259
2259
  }
2260
2260
 
2261
2261
  // src/commands/coverage.ts
2262
- import * as path15 from "node:path";
2262
+ import * as path16 from "node:path";
2263
2263
 
2264
2264
  // src/formatters/coverage.ts
2265
2265
  function renderCoverage(data) {
@@ -2286,12 +2286,114 @@ function renderCoverage(data) {
2286
2286
  `);
2287
2287
  }
2288
2288
 
2289
+ // src/utils/load-spec.ts
2290
+ import { existsSync as existsSync12, readFileSync as readFileSync13 } from "node:fs";
2291
+ import * as path15 from "node:path";
2292
+ import { fromSource } from "@driftdev/clarity-adapter";
2293
+ import { fromDocument } from "@driftdev/openapi-adapter";
2294
+ function getPackageInfo(cwd) {
2295
+ const pkgPath = path15.join(cwd, "package.json");
2296
+ if (!existsSync12(pkgPath))
2297
+ return {};
2298
+ try {
2299
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
2300
+ return { name: pkg.name, version: pkg.version };
2301
+ } catch (err) {
2302
+ formatWarning(`Could not parse package.json${err instanceof Error ? `: ${err.message}` : ""}`);
2303
+ return {};
2304
+ }
2305
+ }
2306
+ function resolveLang(opts) {
2307
+ if (opts.lang) {
2308
+ if (opts.lang !== "typescript" && opts.lang !== "clarity" && opts.lang !== "openapi") {
2309
+ throw new Error(`Unknown language: ${opts.lang}`);
2310
+ }
2311
+ return opts.lang;
2312
+ }
2313
+ if (opts.spec)
2314
+ return "openapi";
2315
+ if (opts.abi || opts.entry?.endsWith(".clar"))
2316
+ return "clarity";
2317
+ return "typescript";
2318
+ }
2319
+ function isUrl(value) {
2320
+ return /^https?:\/\//.test(value);
2321
+ }
2322
+ async function readSpecDocument(specPath) {
2323
+ if (isUrl(specPath)) {
2324
+ const res = await fetch(specPath, { signal: AbortSignal.timeout(30000) });
2325
+ if (!res.ok)
2326
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText} (${specPath})`);
2327
+ return res.text();
2328
+ }
2329
+ if (!existsSync12(specPath))
2330
+ throw new Error(`Spec file not found: ${specPath}`);
2331
+ return readFileSync13(specPath, "utf-8");
2332
+ }
2333
+ async function resolveTruth(opts) {
2334
+ const lang = resolveLang(opts);
2335
+ if (lang === "openapi") {
2336
+ if (!opts.spec)
2337
+ throw new Error("--spec is required when --lang openapi");
2338
+ const specPath = isUrl(opts.spec) ? opts.spec : path15.resolve(process.cwd(), opts.spec);
2339
+ const document = await readSpecDocument(specPath);
2340
+ const name = isUrl(specPath) ? new URL(specPath).pathname.split("/").pop()?.replace(/\.[^.]*$/, "") || "openapi" : path15.basename(specPath, path15.extname(specPath));
2341
+ const apiSpec = fromDocument(document);
2342
+ if (!apiSpec.meta.name || apiSpec.meta.name === "openapi")
2343
+ apiSpec.meta.name = name;
2344
+ return {
2345
+ apiSpec,
2346
+ packageName: apiSpec.meta.name,
2347
+ packageVersion: apiSpec.meta.version,
2348
+ lang
2349
+ };
2350
+ }
2351
+ if (lang === "clarity") {
2352
+ if (!opts.abi)
2353
+ throw new Error("--abi is required when --lang clarity");
2354
+ if (!opts.entry)
2355
+ throw new Error("Entry file required for --lang clarity");
2356
+ const entryFile = path15.resolve(process.cwd(), opts.entry);
2357
+ const abiPath = path15.resolve(process.cwd(), opts.abi);
2358
+ if (!existsSync12(entryFile))
2359
+ throw new Error(`Source file not found: ${entryFile}`);
2360
+ if (!existsSync12(abiPath))
2361
+ throw new Error(`ABI file not found: ${abiPath}`);
2362
+ const source = readFileSync13(entryFile, "utf-8");
2363
+ const abi = JSON.parse(readFileSync13(abiPath, "utf-8"));
2364
+ const name = path15.basename(entryFile, path15.extname(entryFile));
2365
+ const pkg2 = getPackageInfo(process.cwd());
2366
+ const apiSpec = fromSource(source, abi, { name, version: pkg2.version });
2367
+ return { apiSpec, packageName: pkg2.name ?? name, packageVersion: pkg2.version, lang };
2368
+ }
2369
+ if (!opts.entry)
2370
+ throw new Error("Entry file required");
2371
+ const { spec } = await cachedExtract(opts.entry);
2372
+ const pkg = getPackageInfo(process.cwd());
2373
+ return {
2374
+ apiSpec: spec,
2375
+ packageName: pkg.name,
2376
+ packageVersion: pkg.version,
2377
+ lang
2378
+ };
2379
+ }
2380
+
2289
2381
  // src/commands/coverage.ts
2290
2382
  function registerCoverageCommand(program) {
2291
- program.command("coverage [entry]").description("Measure documentation coverage for a TypeScript entry file").option("--min <n>", "Minimum coverage threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
2383
+ program.command("coverage [entry]").description("Measure documentation coverage").option("--min <n>", "Minimum coverage 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 (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)").action(async (entry, options) => {
2292
2384
  const startTime = Date.now();
2293
2385
  const version = getVersion();
2294
2386
  try {
2387
+ const lang = resolveLang({
2388
+ entry,
2389
+ lang: options.lang,
2390
+ spec: options.spec,
2391
+ abi: options.abi
2392
+ });
2393
+ if (lang !== "typescript" && options.all) {
2394
+ formatError("coverage", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
2395
+ return;
2396
+ }
2295
2397
  if (options.all) {
2296
2398
  const allPackages = discoverPackages(process.cwd());
2297
2399
  if (!allPackages || allPackages.length === 0) {
@@ -2333,8 +2435,16 @@ function registerCoverageCommand(program) {
2333
2435
  return;
2334
2436
  }
2335
2437
  const { config } = loadConfig();
2336
- const entryFile = entry ? path15.resolve(process.cwd(), entry) : config.entry ? path15.resolve(process.cwd(), config.entry) : detectEntry();
2337
- const { spec } = await cachedExtract(entryFile);
2438
+ let entryFile = entry ? path16.resolve(process.cwd(), entry) : undefined;
2439
+ if (lang === "typescript" && !entryFile) {
2440
+ entryFile = config.entry ? path16.resolve(process.cwd(), config.entry) : detectEntry();
2441
+ }
2442
+ const { apiSpec: spec } = await resolveTruth({
2443
+ entry: entryFile,
2444
+ lang,
2445
+ spec: options.spec,
2446
+ abi: options.abi
2447
+ });
2338
2448
  const exports = spec.exports ?? [];
2339
2449
  const total = exports.length;
2340
2450
  const undocumented = [];
@@ -2382,7 +2492,7 @@ function registerCoverageCommand(program) {
2382
2492
  }
2383
2493
 
2384
2494
  // src/commands/diff.ts
2385
- import * as path16 from "node:path";
2495
+ import * as path17 from "node:path";
2386
2496
  import { extract as extract5 } from "@openpkg-ts/sdk";
2387
2497
  import { categorizeBreakingChanges as categorizeBreakingChanges4, diffSpec as diffSpec4, normalize as normalize5 } from "@openpkg-ts/spec";
2388
2498
 
@@ -2456,7 +2566,7 @@ function registerDiffCommand(program) {
2456
2566
  let totalAdded = 0;
2457
2567
  let totalChanged = 0;
2458
2568
  for (const pkg of packages) {
2459
- const relEntry = path16.relative(cwd, pkg.entry);
2569
+ const relEntry = path17.relative(cwd, pkg.entry);
2460
2570
  const oldSpec2 = await extractSpecFromRef(options.base, relEntry, cwd);
2461
2571
  const newSpec2 = options.head ? await extractSpecFromRef(options.head, relEntry, cwd) : normalize5((await extract5({ entryFile: pkg.entry })).spec);
2462
2572
  const diff2 = diffSpec4(oldSpec2, newSpec2);
@@ -2526,8 +2636,8 @@ function registerDiffCommand(program) {
2526
2636
  }
2527
2637
 
2528
2638
  // src/commands/examples.ts
2529
- import { readFileSync as readFileSync13 } from "node:fs";
2530
- import * as path17 from "node:path";
2639
+ import { readFileSync as readFileSync14 } from "node:fs";
2640
+ import * as path18 from "node:path";
2531
2641
  import { validateExamples } from "@driftdev/sdk";
2532
2642
 
2533
2643
  // src/formatters/examples.ts
@@ -2606,16 +2716,16 @@ function renderExamples(data) {
2606
2716
 
2607
2717
  // src/commands/examples.ts
2608
2718
  function findPackagePath(entryFile) {
2609
- let dir = path17.dirname(entryFile);
2610
- while (dir !== path17.dirname(dir)) {
2719
+ let dir = path18.dirname(entryFile);
2720
+ while (dir !== path18.dirname(dir)) {
2611
2721
  try {
2612
- readFileSync13(path17.join(dir, "package.json"), "utf-8");
2722
+ readFileSync14(path18.join(dir, "package.json"), "utf-8");
2613
2723
  return dir;
2614
2724
  } catch {
2615
- dir = path17.dirname(dir);
2725
+ dir = path18.dirname(dir);
2616
2726
  }
2617
2727
  }
2618
- return path17.dirname(entryFile);
2728
+ return path18.dirname(entryFile);
2619
2729
  }
2620
2730
  function registerExamplesCommand(program) {
2621
2731
  program.command("examples [entry]").description("Validate @example blocks on exports").option("--typecheck", "Type-check examples with TypeScript").option("--run", "Execute examples at runtime (implies --typecheck)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").option("--min <n>", "Minimum presence threshold (exit 1 if below)").action(async (entry, options) => {
@@ -2683,7 +2793,7 @@ function registerExamplesCommand(program) {
2683
2793
  return;
2684
2794
  }
2685
2795
  const { config } = loadConfig();
2686
- const entryFile = entry ? path17.resolve(process.cwd(), entry) : config.entry ? path17.resolve(process.cwd(), config.entry) : detectEntry();
2796
+ const entryFile = entry ? path18.resolve(process.cwd(), entry) : config.entry ? path18.resolve(process.cwd(), config.entry) : detectEntry();
2687
2797
  const { spec } = await cachedExtract(entryFile);
2688
2798
  const exports = spec.exports ?? [];
2689
2799
  const packagePath = findPackagePath(entryFile);
@@ -2718,7 +2828,7 @@ function registerExamplesCommand(program) {
2718
2828
  }
2719
2829
 
2720
2830
  // src/commands/extract.ts
2721
- import * as path18 from "node:path";
2831
+ import * as path19 from "node:path";
2722
2832
  import { Drift } from "@driftdev/sdk";
2723
2833
  import { normalize as normalize6 } from "@openpkg-ts/spec";
2724
2834
 
@@ -2735,10 +2845,37 @@ function renderExtract(data) {
2735
2845
 
2736
2846
  // src/commands/extract.ts
2737
2847
  function registerExtractCommand(program) {
2738
- program.command("extract [entry]").description("Extract OpenPkg spec from TypeScript entry file").option("-o, --output <file>", "Write JSON to file instead of stdout").option("--only <patterns>", "Include exports matching glob (comma-separated)").option("--ignore <patterns>", "Exclude exports matching glob (comma-separated)").option("--max-depth <n>", "Max type resolution depth", "10").option("--all", "Extract from all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
2848
+ program.command("extract [entry]").description("Extract API spec from a source of truth (TypeScript, Clarity, OpenAPI)").option("-o, --output <file>", "Write JSON to file instead of stdout").option("--only <patterns>", "Include exports matching glob (comma-separated)").option("--ignore <patterns>", "Exclude exports matching glob (comma-separated)").option("--max-depth <n>", "Max type resolution depth", "10").option("--all", "Extract from 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)").action(async (entry, options) => {
2739
2849
  const startTime = Date.now();
2740
2850
  const version = getVersion();
2741
2851
  try {
2852
+ const lang = resolveLang({
2853
+ entry,
2854
+ lang: options.lang,
2855
+ spec: options.spec,
2856
+ abi: options.abi
2857
+ });
2858
+ if (lang !== "typescript") {
2859
+ if (options.all || options.only || options.ignore) {
2860
+ formatError("extract", `--all/--only/--ignore not yet supported for ${lang}`, startTime, version);
2861
+ return;
2862
+ }
2863
+ const { apiSpec } = await resolveTruth({
2864
+ entry,
2865
+ lang,
2866
+ spec: options.spec,
2867
+ abi: options.abi
2868
+ });
2869
+ if (options.output) {
2870
+ const { writeFileSync: writeFileSync5 } = await import("node:fs");
2871
+ writeFileSync5(options.output, JSON.stringify(apiSpec, null, 2));
2872
+ process.stderr.write(`drift extract: wrote ${options.output}
2873
+ `);
2874
+ } else {
2875
+ formatOutput("extract", apiSpec, startTime, version, renderExtract);
2876
+ }
2877
+ return;
2878
+ }
2742
2879
  if (options.all) {
2743
2880
  const allPackages = discoverPackages(process.cwd());
2744
2881
  if (!allPackages || allPackages.length === 0) {
@@ -2759,7 +2896,7 @@ function registerExtractCommand(program) {
2759
2896
  formatOutput("extract", { packages: specs, ...skipped.length > 0 ? { skipped } : {} }, startTime, version);
2760
2897
  return;
2761
2898
  }
2762
- const entryFile = entry ? path18.resolve(process.cwd(), entry) : detectEntry();
2899
+ const entryFile = entry ? path19.resolve(process.cwd(), entry) : detectEntry();
2763
2900
  const hasFilters = !!(options.only || options.ignore);
2764
2901
  let spec;
2765
2902
  if (hasFilters) {
@@ -2798,8 +2935,8 @@ function registerExtractCommand(program) {
2798
2935
  }
2799
2936
 
2800
2937
  // src/commands/filter.ts
2801
- import { readFileSync as readFileSync14 } from "node:fs";
2802
- import * as path19 from "node:path";
2938
+ import { readFileSync as readFileSync15 } from "node:fs";
2939
+ import * as path20 from "node:path";
2803
2940
  import { filterSpec } from "@openpkg-ts/sdk";
2804
2941
 
2805
2942
  // src/formatters/filter.ts
@@ -2830,8 +2967,8 @@ function registerFilterCommand(program) {
2830
2967
  const startTime = Date.now();
2831
2968
  const version = getVersion();
2832
2969
  try {
2833
- const filePath = path19.resolve(process.cwd(), file);
2834
- const content = readFileSync14(filePath, "utf-8");
2970
+ const filePath = path20.resolve(process.cwd(), file);
2971
+ const content = readFileSync15(filePath, "utf-8");
2835
2972
  const spec = JSON.parse(content);
2836
2973
  const criteria = {};
2837
2974
  if (options.kind) {
@@ -2857,13 +2994,14 @@ function registerFilterCommand(program) {
2857
2994
  }
2858
2995
 
2859
2996
  // src/commands/get.ts
2860
- import * as path20 from "node:path";
2997
+ import * as path21 from "node:path";
2861
2998
  import { getExport, listExports } from "@openpkg-ts/sdk";
2862
2999
 
2863
3000
  // src/formatters/get.ts
2864
3001
  function renderGet(data) {
2865
3002
  const lines = [""];
2866
3003
  const exp = data.export;
3004
+ const schema = typeof exp.schema === "object" && exp.schema !== null ? exp.schema : undefined;
2867
3005
  lines.push(indent(`${c.bold(exp.name)}${" ".repeat(Math.max(2, 50 - exp.name.length))}${c.gray(exp.kind)}`));
2868
3006
  if (exp.deprecated) {
2869
3007
  lines.push(indent(c.yellow("deprecated")));
@@ -2876,24 +3014,24 @@ function renderGet(data) {
2876
3014
  lines.push(indent(` ${exp.signature}`));
2877
3015
  lines.push("");
2878
3016
  }
2879
- const params = exp.parameters ?? extractParams(exp.schema);
3017
+ const params = exp.parameters ?? extractParams(schema);
2880
3018
  if (params.length > 0) {
2881
3019
  lines.push(indent(c.gray(" PARAMETERS")));
2882
3020
  for (const p of params) {
2883
3021
  const req = p.required ? "required" : "optional";
2884
3022
  const type = p.type ?? "unknown";
2885
3023
  const desc = p.description ? ` ${c.dim(JSON.stringify(p.description))}` : "";
2886
- lines.push(indent(` ${padRight(p.name, 16)}${padRight(type, 24)}${c.gray(req)}${desc}`));
3024
+ lines.push(indent(` ${padRight(p.name ?? "", 16)}${padRight(type, 24)}${c.gray(req)}${desc}`));
2887
3025
  }
2888
3026
  lines.push("");
2889
3027
  }
2890
- const returns = exp.returns ?? extractReturns(exp.schema);
3028
+ const returns = exp.returns ?? extractReturns(schema);
2891
3029
  if (returns) {
2892
3030
  lines.push(indent(c.gray(" RETURNS")));
2893
3031
  lines.push(indent(` ${returns.type ?? "void"}`));
2894
3032
  lines.push("");
2895
3033
  }
2896
- const members = exp.members ?? extractMembers(exp.schema);
3034
+ const members = exp.members ?? extractMembers(schema);
2897
3035
  if (members.length > 0) {
2898
3036
  const shown = members.slice(0, 50);
2899
3037
  const remaining = members.length - shown.length;
@@ -2901,7 +3039,7 @@ function renderGet(data) {
2901
3039
  for (const m of shown) {
2902
3040
  const req = m.required ? "required" : "optional";
2903
3041
  const type = m.type ?? "";
2904
- lines.push(indent(` ${padRight(m.name, 20)}${padRight(type, 20)}${c.gray(req)}`));
3042
+ lines.push(indent(` ${padRight(m.name ?? "", 20)}${padRight(type, 20)}${c.gray(req)}`));
2905
3043
  }
2906
3044
  if (remaining > 0) {
2907
3045
  lines.push(indent(c.gray(` ... ${remaining} more`)));
@@ -2966,6 +3104,8 @@ function extractPropsFromSchema(schema) {
2966
3104
  function formatType(schema) {
2967
3105
  if (!schema)
2968
3106
  return "unknown";
3107
+ if (typeof schema === "string")
3108
+ return schema;
2969
3109
  if (schema.$ref)
2970
3110
  return schema.$ref.replace("#/types/", "");
2971
3111
  if (schema.type === "array" && schema.items)
@@ -3037,14 +3177,72 @@ function looksLikeFilePath(s) {
3037
3177
 
3038
3178
  // src/commands/get.ts
3039
3179
  function registerGetCommand(program) {
3040
- program.command("get <nameOrEntry> [name]").description("Get detailed spec for a single export").action(async (nameOrEntry, name) => {
3180
+ program.command("get <nameOrEntry> [name]").description("Get detailed spec for a single export").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)").action(async (nameOrEntry, name, options = {}) => {
3041
3181
  const startTime = Date.now();
3042
3182
  const version = getVersion();
3043
3183
  try {
3184
+ const lang = resolveLang({
3185
+ entry: name ? nameOrEntry : undefined,
3186
+ lang: options.lang,
3187
+ spec: options.spec,
3188
+ abi: options.abi
3189
+ });
3190
+ if (lang !== "typescript") {
3191
+ const entryArg = name ? nameOrEntry : undefined;
3192
+ const exportName2 = name ?? nameOrEntry;
3193
+ const { apiSpec } = await resolveTruth({
3194
+ entry: entryArg,
3195
+ lang,
3196
+ spec: options.spec,
3197
+ abi: options.abi
3198
+ });
3199
+ const allExports = apiSpec.exports ?? [];
3200
+ const exp = allExports.find((e) => e.name === exportName2 || e.id === exportName2);
3201
+ if (!exp) {
3202
+ const suggestions = fuzzyTop(exportName2, allExports);
3203
+ renderNotFound(exportName2, suggestions, startTime, version);
3204
+ return;
3205
+ }
3206
+ const sig = exp.signatures?.[0];
3207
+ const data = {
3208
+ export: {
3209
+ name: exp.name,
3210
+ kind: exp.kind,
3211
+ ...exp.description ? { description: exp.description } : {},
3212
+ ...exp.deprecated ? { deprecated: true } : {},
3213
+ ...sig?.parameters ? {
3214
+ parameters: sig.parameters.map((p) => ({
3215
+ name: p.name,
3216
+ type: schemaTypeString(p.schema),
3217
+ required: p.required,
3218
+ ...p.description ? { description: p.description } : {},
3219
+ schema: p.schema
3220
+ }))
3221
+ } : {},
3222
+ ...sig?.returns ? {
3223
+ returns: {
3224
+ type: schemaTypeString(sig.returns.schema),
3225
+ ...sig.returns.description ? { description: sig.returns.description } : {},
3226
+ schema: sig.returns.schema
3227
+ }
3228
+ } : {},
3229
+ ...exp.members ? {
3230
+ members: exp.members.map((m) => ({
3231
+ name: m.name,
3232
+ ...m.description ? { description: m.description } : {}
3233
+ }))
3234
+ } : {},
3235
+ ...exp.schema ? { schema: exp.schema } : {},
3236
+ ...exp.flags ? { flags: exp.flags } : {}
3237
+ }
3238
+ };
3239
+ formatOutput("get", data, startTime, version, renderGet);
3240
+ return;
3241
+ }
3044
3242
  let entryFile;
3045
3243
  let exportName;
3046
3244
  if (name) {
3047
- entryFile = path20.resolve(process.cwd(), nameOrEntry);
3245
+ entryFile = path21.resolve(process.cwd(), nameOrEntry);
3048
3246
  exportName = name;
3049
3247
  } else {
3050
3248
  entryFile = detectEntry();
@@ -3054,39 +3252,63 @@ function registerGetCommand(program) {
3054
3252
  if (!result.export) {
3055
3253
  const listResult = await listExports({ entryFile });
3056
3254
  const suggestions = fuzzyTop(exportName, listResult.exports);
3057
- if (suggestions.length > 0 && shouldRenderHuman()) {
3058
- const lines = [
3059
- "",
3060
- indent(`${c.red("x")} Export "${exportName}" not found.`),
3061
- "",
3062
- indent(c.gray("Similar:"))
3063
- ];
3064
- for (const s of suggestions) {
3065
- lines.push(indent(` ${s}`));
3066
- }
3067
- lines.push("");
3068
- lines.push(indent(`drift get ${suggestions[0]}`));
3069
- lines.push("");
3070
- process.stdout.write(lines.join(`
3071
- `));
3072
- process.exitCode = 1;
3073
- } else if (suggestions.length > 0) {
3074
- formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version);
3075
- } else {
3076
- formatError("get", `Export '${exportName}' not found`, startTime, version);
3077
- }
3255
+ renderNotFound(exportName, suggestions, startTime, version);
3078
3256
  return;
3079
3257
  }
3080
- formatOutput("get", { export: result.export, types: result.types }, startTime, version, renderGet);
3258
+ const types = Object.fromEntries((result.types ?? []).map((t) => [
3259
+ t.name,
3260
+ t.schema ?? {}
3261
+ ]));
3262
+ formatOutput("get", { export: result.export, types }, startTime, version, renderGet);
3081
3263
  } catch (err) {
3082
3264
  formatError("get", err instanceof Error ? err.message : String(err), startTime, version);
3083
3265
  }
3084
3266
  });
3085
3267
  }
3268
+ function schemaTypeString(schema) {
3269
+ if (schema === undefined || schema === null)
3270
+ return;
3271
+ if (typeof schema === "string")
3272
+ return schema;
3273
+ if (typeof schema === "object") {
3274
+ const s = schema;
3275
+ if (typeof s.$ref === "string")
3276
+ return s.$ref.split("/").pop();
3277
+ if (typeof s.type === "string")
3278
+ return s.type;
3279
+ if (Array.isArray(s.oneOf))
3280
+ return "oneOf";
3281
+ if (Array.isArray(s.anyOf))
3282
+ return "anyOf";
3283
+ }
3284
+ return;
3285
+ }
3286
+ function renderNotFound(exportName, suggestions, startTime, version) {
3287
+ if (suggestions.length > 0 && shouldRenderHuman()) {
3288
+ const lines = [
3289
+ "",
3290
+ indent(`${c.red("x")} Export "${exportName}" not found.`),
3291
+ "",
3292
+ indent(c.gray("Similar:"))
3293
+ ];
3294
+ for (const s of suggestions) {
3295
+ lines.push(indent(` ${s}`));
3296
+ }
3297
+ lines.push("");
3298
+ lines.push(indent(`drift get ${suggestions[0]}`));
3299
+ lines.push("");
3300
+ process.stdout.write(lines.join(`
3301
+ `));
3302
+ process.exitCode = 1;
3303
+ } else if (suggestions.length > 0) {
3304
+ formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version);
3305
+ } else {
3306
+ formatError("get", `Export '${exportName}' not found`, startTime, version);
3307
+ }
3308
+ }
3086
3309
 
3087
3310
  // src/commands/health.ts
3088
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "node:fs";
3089
- import * as path21 from "node:path";
3311
+ import * as path22 from "node:path";
3090
3312
  import { computeDrift as computeDrift3 } from "@driftdev/sdk";
3091
3313
 
3092
3314
  // src/formatters/health.ts
@@ -3153,23 +3375,21 @@ function computeHealth(totalExports, documented, issues) {
3153
3375
  }
3154
3376
 
3155
3377
  // src/commands/health.ts
3156
- function getPackageInfo(cwd) {
3157
- const pkgPath = path21.join(cwd, "package.json");
3158
- if (!existsSync12(pkgPath))
3159
- return {};
3160
- try {
3161
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3162
- return { name: pkg.name, version: pkg.version };
3163
- } catch (err) {
3164
- formatWarning(`Could not parse package.json${err instanceof Error ? `: ${err.message}` : ""}`);
3165
- return {};
3166
- }
3167
- }
3168
3378
  function registerHealthCommand(program) {
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) => {
3379
+ 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").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)").action(async (entry, options) => {
3170
3380
  const startTime = Date.now();
3171
3381
  const version = getVersion();
3172
3382
  try {
3383
+ const lang = resolveLang({
3384
+ entry,
3385
+ lang: options.lang,
3386
+ spec: options.spec,
3387
+ abi: options.abi
3388
+ });
3389
+ if (lang !== "typescript" && options.all) {
3390
+ formatError("health", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
3391
+ return;
3392
+ }
3173
3393
  if (options.all) {
3174
3394
  const allPackages = discoverPackages(process.cwd());
3175
3395
  if (!allPackages || allPackages.length === 0) {
@@ -3185,8 +3405,8 @@ function registerHealthCommand(program) {
3185
3405
  const rows = [];
3186
3406
  let totalDoc = 0;
3187
3407
  let totalAll = 0;
3188
- for (const pkg2 of packages) {
3189
- const { spec: spec2 } = await cachedExtract(pkg2.entry);
3408
+ for (const pkg of packages) {
3409
+ const { spec: spec2 } = await cachedExtract(pkg.entry);
3190
3410
  const exps = spec2.exports ?? [];
3191
3411
  let doc = 0;
3192
3412
  for (const e of exps) {
@@ -3194,7 +3414,7 @@ function registerHealthCommand(program) {
3194
3414
  doc++;
3195
3415
  }
3196
3416
  const score = exps.length > 0 ? Math.round(doc / exps.length * 100) : 100;
3197
- rows.push({ name: pkg2.name, exports: exps.length, score });
3417
+ rows.push({ name: pkg.name, exports: exps.length, score });
3198
3418
  totalDoc += doc;
3199
3419
  totalAll += exps.length;
3200
3420
  }
@@ -3208,8 +3428,15 @@ function registerHealthCommand(program) {
3208
3428
  return;
3209
3429
  }
3210
3430
  const { config } = loadConfig();
3211
- const entryFile = entry ? path21.resolve(process.cwd(), entry) : config.entry ? path21.resolve(process.cwd(), config.entry) : detectEntry();
3212
- const { spec } = await cachedExtract(entryFile);
3431
+ let entryFile = entry ? path22.resolve(process.cwd(), entry) : undefined;
3432
+ if (lang === "typescript" && !entryFile) {
3433
+ entryFile = config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
3434
+ }
3435
+ const {
3436
+ apiSpec: spec,
3437
+ packageName,
3438
+ packageVersion
3439
+ } = await resolveTruth({ entry: entryFile, lang, spec: options.spec, abi: options.abi });
3213
3440
  const exports = spec.exports ?? [];
3214
3441
  const total = exports.length;
3215
3442
  let documented = 0;
@@ -3226,11 +3453,10 @@ function registerHealthCommand(program) {
3226
3453
  }
3227
3454
  }
3228
3455
  const health = computeHealth(total, documented, issues);
3229
- const pkg = getPackageInfo(process.cwd());
3230
3456
  const data = {
3231
3457
  ...health,
3232
- packageName: pkg.name,
3233
- packageVersion: pkg.version
3458
+ packageName,
3459
+ packageVersion
3234
3460
  };
3235
3461
  let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
3236
3462
  if (min !== undefined && config.coverage?.ratchet) {
@@ -3267,7 +3493,7 @@ function registerHealthCommand(program) {
3267
3493
  // src/commands/init.ts
3268
3494
  init_global();
3269
3495
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
3270
- import * as path22 from "node:path";
3496
+ import * as path23 from "node:path";
3271
3497
  import { extract as extract6 } from "@openpkg-ts/sdk";
3272
3498
  import { normalize as normalize7 } from "@openpkg-ts/spec";
3273
3499
 
@@ -3573,10 +3799,10 @@ ${detailLine}`);
3573
3799
 
3574
3800
  // src/commands/init.ts
3575
3801
  async function scanPackage(cwd, pkgDir) {
3576
- const absDir = path22.join(cwd, pkgDir);
3802
+ const absDir = path23.join(cwd, pkgDir);
3577
3803
  if (!existsSync13(absDir))
3578
3804
  return null;
3579
- const pkgPath = path22.join(absDir, "package.json");
3805
+ const pkgPath = path23.join(absDir, "package.json");
3580
3806
  let name = pkgDir;
3581
3807
  if (existsSync13(pkgPath)) {
3582
3808
  try {
@@ -3598,7 +3824,7 @@ async function scanPackage(cwd, pkgDir) {
3598
3824
  }
3599
3825
  const coverage = total > 0 ? Math.round(documented / total * 100) : 100;
3600
3826
  const health = Math.round(coverage * 0.5 + 100 * 0.5);
3601
- return { name, entry: path22.relative(cwd, entryFile), exports: total, coverage, health };
3827
+ return { name, entry: path23.relative(cwd, entryFile), exports: total, coverage, health };
3602
3828
  } catch {
3603
3829
  return null;
3604
3830
  }
@@ -3634,7 +3860,7 @@ function registerInitCommand(program) {
3634
3860
  return;
3635
3861
  }
3636
3862
  const config = generateConfig(packages);
3637
- const configPath = opts.project ? path22.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3863
+ const configPath = opts.project ? path23.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3638
3864
  if (!opts.project) {
3639
3865
  const globalDir = getGlobalDir();
3640
3866
  if (!existsSync13(globalDir))
@@ -3659,7 +3885,7 @@ function registerInitCommand(program) {
3659
3885
 
3660
3886
  // src/commands/lint.ts
3661
3887
  import { readFileSync as readFileSync17 } from "node:fs";
3662
- import * as path23 from "node:path";
3888
+ import * as path24 from "node:path";
3663
3889
  import {
3664
3890
  buildExportRegistry,
3665
3891
  computeDrift as computeDrift4,
@@ -3703,10 +3929,20 @@ function renderLint(data, next) {
3703
3929
 
3704
3930
  // src/commands/lint.ts
3705
3931
  function registerLintCommand(program) {
3706
- program.command("lint [entry]").description("Cross-reference JSDoc against code for accuracy issues").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
3932
+ 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)").action(async (entry, options) => {
3707
3933
  const startTime = Date.now();
3708
3934
  const version = getVersion();
3709
3935
  try {
3936
+ const lang = resolveLang({
3937
+ entry,
3938
+ lang: options.lang,
3939
+ spec: options.spec,
3940
+ abi: options.abi
3941
+ });
3942
+ if (lang !== "typescript" && options.all) {
3943
+ formatError("lint", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
3944
+ return;
3945
+ }
3710
3946
  if (options.all) {
3711
3947
  const allPackages = discoverPackages(process.cwd());
3712
3948
  if (!allPackages || allPackages.length === 0) {
@@ -3745,8 +3981,16 @@ function registerLintCommand(program) {
3745
3981
  formatOutput("lint", { issues: [], count: 0 }, startTime, version, renderLint);
3746
3982
  return;
3747
3983
  }
3748
- const entryFile = entry ? path23.resolve(process.cwd(), entry) : config.entry ? path23.resolve(process.cwd(), config.entry) : detectEntry();
3749
- const { spec } = await cachedExtract(entryFile);
3984
+ let entryFile = entry ? path24.resolve(process.cwd(), entry) : undefined;
3985
+ if (lang === "typescript" && !entryFile) {
3986
+ entryFile = config.entry ? path24.resolve(process.cwd(), config.entry) : detectEntry();
3987
+ }
3988
+ const { apiSpec: spec } = await resolveTruth({
3989
+ entry: entryFile,
3990
+ lang,
3991
+ spec: options.spec,
3992
+ abi: options.abi
3993
+ });
3750
3994
  const driftResult = computeDrift4(spec);
3751
3995
  const issues = [];
3752
3996
  for (const [exportName, drifts] of driftResult.exports) {
@@ -3760,27 +4004,28 @@ function registerLintCommand(program) {
3760
4004
  });
3761
4005
  }
3762
4006
  }
3763
- try {
3764
- const pkgJsonPath = path23.resolve(process.cwd(), "package.json");
3765
- const pkgJson = JSON.parse(readFileSync17(pkgJsonPath, "utf-8"));
3766
- const packageName = pkgJson.name;
3767
- if (packageName) {
3768
- const registry = buildExportRegistry(spec);
3769
- const markdownFiles = discoverMarkdownFiles(process.cwd(), config.docs);
3770
- const proseDrifts = detectProseDrift({ packageName, markdownFiles, registry });
3771
- for (const drift of proseDrifts) {
3772
- issues.push({
3773
- export: drift.target ?? "",
3774
- issue: drift.issue,
3775
- ...drift.suggestion ? { location: drift.suggestion } : {},
3776
- filePath: drift.filePath,
3777
- line: drift.line
3778
- });
4007
+ if (lang === "typescript")
4008
+ try {
4009
+ const pkgJsonPath = path24.resolve(process.cwd(), "package.json");
4010
+ const pkgJson = JSON.parse(readFileSync17(pkgJsonPath, "utf-8"));
4011
+ const packageName = pkgJson.name;
4012
+ if (packageName) {
4013
+ const registry = buildExportRegistry(spec);
4014
+ const markdownFiles = discoverMarkdownFiles(process.cwd(), config.docs);
4015
+ const proseDrifts = detectProseDrift({ packageName, markdownFiles, registry });
4016
+ for (const drift of proseDrifts) {
4017
+ issues.push({
4018
+ export: drift.target ?? "",
4019
+ issue: drift.issue,
4020
+ ...drift.suggestion ? { location: drift.suggestion } : {},
4021
+ filePath: drift.filePath,
4022
+ line: drift.line
4023
+ });
4024
+ }
3779
4025
  }
4026
+ } catch (err) {
4027
+ formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
3780
4028
  }
3781
- } catch (err) {
3782
- formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
3783
- }
3784
4029
  const data = { issues, count: issues.length };
3785
4030
  const next = issues.length > 0 ? {
3786
4031
  suggested: "drift-fix skill",
@@ -3804,7 +4049,7 @@ function registerLintCommand(program) {
3804
4049
  }
3805
4050
 
3806
4051
  // src/commands/list.ts
3807
- import * as path24 from "node:path";
4052
+ import * as path25 from "node:path";
3808
4053
  import { computeDrift as computeDrift5 } from "@driftdev/sdk";
3809
4054
  import { listExports as listExports2 } from "@openpkg-ts/sdk";
3810
4055
 
@@ -3861,10 +4106,20 @@ function renderList(data) {
3861
4106
 
3862
4107
  // src/commands/list.ts
3863
4108
  function registerListCommand(program) {
3864
- program.command("list [searchOrEntry]").description("List exports (positional arg = search term or entry file)").option("--kind <kinds>", "Filter by kind (comma-separated)").option("--undocumented", "Only exports missing JSDoc").option("--drifted", "Only exports with stale JSDoc").option("--full", "Show full list (no truncation)").option("--all", "Run across all workspace packages").action(async (searchOrEntry, options) => {
4109
+ program.command("list [searchOrEntry]").description("List exports (positional arg = search term or entry file)").option("--kind <kinds>", "Filter by kind (comma-separated)").option("--undocumented", "Only exports missing docs").option("--drifted", "Only exports with stale docs").option("--full", "Show full list (no truncation)").option("--all", "Run across all workspace packages").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)").action(async (searchOrEntry, options) => {
3865
4110
  const startTime = Date.now();
3866
4111
  const version = getVersion();
3867
4112
  try {
4113
+ const lang = resolveLang({
4114
+ entry: searchOrEntry,
4115
+ lang: options.lang,
4116
+ spec: options.spec,
4117
+ abi: options.abi
4118
+ });
4119
+ if (lang !== "typescript" && options.all) {
4120
+ formatError("list", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4121
+ return;
4122
+ }
3868
4123
  if (options.all) {
3869
4124
  const packages = discoverPackages(process.cwd());
3870
4125
  if (!packages || packages.length === 0) {
@@ -3884,23 +4139,50 @@ function registerListCommand(program) {
3884
4139
  formatOutput("list", { packages: rows, filter: filter2 }, startTime, version, renderBatchList);
3885
4140
  return;
3886
4141
  }
3887
- let entryFile;
3888
4142
  let searchTerm;
3889
- if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
3890
- entryFile = path24.resolve(process.cwd(), searchOrEntry);
3891
- } else if (searchOrEntry) {
3892
- entryFile = detectEntry();
3893
- searchTerm = searchOrEntry;
4143
+ let exports;
4144
+ let driftedNames;
4145
+ if (lang !== "typescript") {
4146
+ const entryArg = lang === "clarity" ? searchOrEntry : undefined;
4147
+ if (lang === "openapi")
4148
+ searchTerm = searchOrEntry;
4149
+ const { apiSpec } = await resolveTruth({
4150
+ entry: entryArg,
4151
+ lang,
4152
+ spec: options.spec,
4153
+ abi: options.abi
4154
+ });
4155
+ exports = (apiSpec.exports ?? []).map((e) => ({
4156
+ name: e.name,
4157
+ kind: e.kind,
4158
+ description: e.description,
4159
+ ...e.deprecated ? { deprecated: true } : {}
4160
+ }));
4161
+ if (options.drifted) {
4162
+ driftedNames = new Set(computeDrift5(apiSpec).exports.keys());
4163
+ }
3894
4164
  } else {
3895
- entryFile = detectEntry();
4165
+ let entryFile;
4166
+ if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
4167
+ entryFile = path25.resolve(process.cwd(), searchOrEntry);
4168
+ } else if (searchOrEntry) {
4169
+ entryFile = detectEntry();
4170
+ searchTerm = searchOrEntry;
4171
+ } else {
4172
+ entryFile = detectEntry();
4173
+ }
4174
+ const result = await listExports2({ entryFile });
4175
+ exports = result.exports.map((e) => ({
4176
+ name: e.name,
4177
+ kind: e.kind,
4178
+ description: e.description,
4179
+ ...e.deprecated ? { deprecated: true } : {}
4180
+ }));
4181
+ if (options.drifted) {
4182
+ const { spec } = await cachedExtract(entryFile);
4183
+ driftedNames = new Set(computeDrift5(spec).exports.keys());
4184
+ }
3896
4185
  }
3897
- const result = await listExports2({ entryFile });
3898
- let exports = result.exports.map((e) => ({
3899
- name: e.name,
3900
- kind: e.kind,
3901
- description: e.description,
3902
- ...e.deprecated ? { deprecated: true } : {}
3903
- }));
3904
4186
  if (options.kind) {
3905
4187
  const kinds = new Set(options.kind.split(",").map((k) => k.trim().toLowerCase()));
3906
4188
  exports = exports.filter((e) => kinds.has(e.kind));
@@ -3908,11 +4190,9 @@ function registerListCommand(program) {
3908
4190
  if (options.undocumented) {
3909
4191
  exports = exports.filter((e) => !e.description || e.description.trim().length === 0);
3910
4192
  }
3911
- if (options.drifted) {
3912
- const { spec } = await cachedExtract(entryFile);
3913
- const driftResult = computeDrift5(spec);
3914
- const driftedNames = new Set(driftResult.exports.keys());
3915
- exports = exports.filter((e) => driftedNames.has(e.name));
4193
+ if (driftedNames) {
4194
+ const names = driftedNames;
4195
+ exports = exports.filter((e) => names.has(e.name));
3916
4196
  }
3917
4197
  if (searchTerm) {
3918
4198
  const matches = fuzzySearch(searchTerm, exports);
@@ -3937,10 +4217,166 @@ function registerListCommand(program) {
3937
4217
  });
3938
4218
  }
3939
4219
 
4220
+ // src/commands/mcp.ts
4221
+ import { spawn } from "node:child_process";
4222
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4223
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4224
+ import { z } from "zod";
4225
+ var truthShape = {
4226
+ cwd: z.string().optional().describe("Directory to run in (project root; where package.json/config live)"),
4227
+ entry: z.string().optional().describe("Entry file: TypeScript entry or Clarity .clar source. Omit for OpenAPI specs."),
4228
+ lang: z.enum(["typescript", "clarity", "openapi"]).optional().describe("Source language override; inferred from spec/abi/.clar extension otherwise"),
4229
+ spec: z.string().optional().describe("OpenAPI 3.x JSON document — local path or https URL. Implies lang=openapi."),
4230
+ abi: z.string().optional().describe("Clarity ABI JSON path (required for Clarity sources)")
4231
+ };
4232
+ function truthFlags(args) {
4233
+ const out = [];
4234
+ if (args.lang)
4235
+ out.push("--lang", args.lang);
4236
+ if (args.spec)
4237
+ out.push("--spec", args.spec);
4238
+ if (args.abi)
4239
+ out.push("--abi", args.abi);
4240
+ return out;
4241
+ }
4242
+ function runDrift(cliArgs, cwd) {
4243
+ return new Promise((resolve18) => {
4244
+ const child = spawn(process.execPath, [process.argv[1], ...cliArgs, "--json"], {
4245
+ cwd: cwd ?? process.cwd(),
4246
+ env: { ...process.env, NO_COLOR: "1" },
4247
+ stdio: ["ignore", "pipe", "pipe"]
4248
+ });
4249
+ let stdout = "";
4250
+ let stderr = "";
4251
+ child.stdout.on("data", (d) => {
4252
+ stdout += d;
4253
+ });
4254
+ child.stderr.on("data", (d) => {
4255
+ stderr += d;
4256
+ });
4257
+ child.on("close", (code) => {
4258
+ const text = stdout.trim() || stderr.trim() || `drift exited with code ${code}`;
4259
+ let ok = code === 0;
4260
+ try {
4261
+ ok = JSON.parse(stdout).ok === true;
4262
+ } catch {}
4263
+ resolve18({ text, ok });
4264
+ });
4265
+ child.on("error", (err) => resolve18({ text: `Failed to run drift: ${err.message}`, ok: false }));
4266
+ });
4267
+ }
4268
+ function toResult({ text, ok }) {
4269
+ return { content: [{ type: "text", text }], isError: !ok };
4270
+ }
4271
+ function registerMcpCommand(program) {
4272
+ program.command("mcp").description("Run an MCP stdio server exposing drift tools to agents").action(async () => {
4273
+ const server = new McpServer({ name: "drift", version: getVersion() });
4274
+ server.registerTool("drift_extract", {
4275
+ title: "Extract API spec",
4276
+ description: "Extract the full machine-readable API spec from a source of truth: a TypeScript package (entry auto-detected from cwd), an OpenAPI 3.x document (spec path/URL), or a Clarity contract (entry .clar + abi). Returns every export with signatures, types, and docs. This is ground truth — use it instead of assuming what an API looks like.",
4277
+ inputSchema: truthShape
4278
+ }, async (args) => toResult(await runDrift(["extract", ...args.entry ? [args.entry] : [], ...truthFlags(args)], args.cwd)));
4279
+ server.registerTool("drift_list", {
4280
+ title: "List exports/operations",
4281
+ description: "List every export (TypeScript), operation (OpenAPI), or function (Clarity) in an API surface: name, kind, one-line description, deprecated flag. Cheap way to check what exists before drilling in with drift_get. Supports filtering to undocumented or drifted items.",
4282
+ inputSchema: {
4283
+ ...truthShape,
4284
+ search: z.string().optional().describe("Fuzzy search term"),
4285
+ kind: z.string().optional().describe("Filter by kind (comma-separated)"),
4286
+ undocumented: z.boolean().optional().describe("Only items missing docs"),
4287
+ drifted: z.boolean().optional().describe("Only items whose docs drifted")
4288
+ }
4289
+ }, async (args) => {
4290
+ const cli = ["list"];
4291
+ if (args.entry)
4292
+ cli.push(args.entry);
4293
+ else if (args.search)
4294
+ cli.push(args.search);
4295
+ if (args.kind)
4296
+ cli.push("--kind", args.kind);
4297
+ if (args.undocumented)
4298
+ cli.push("--undocumented");
4299
+ if (args.drifted)
4300
+ cli.push("--drifted");
4301
+ cli.push("--full");
4302
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4303
+ });
4304
+ server.registerTool("drift_get", {
4305
+ title: "Get one export/operation",
4306
+ description: "Get the authoritative definition of a single export, endpoint operation, or contract function by name: parameters with types/required/descriptions, return shape, deprecation, referenced types. Use this to verify every claim a docs page makes — one drift_get per claim, never from memory. Unknown names return fuzzy suggestions.",
4307
+ inputSchema: {
4308
+ ...truthShape,
4309
+ name: z.string().describe("Export/operation name (e.g. candidateInfo, transfer, createClient)")
4310
+ }
4311
+ }, async (args) => {
4312
+ const cli = ["get"];
4313
+ if (args.entry)
4314
+ cli.push(args.entry, args.name);
4315
+ else
4316
+ cli.push(args.name);
4317
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4318
+ });
4319
+ server.registerTool("drift_scan", {
4320
+ title: "Scan docs health",
4321
+ description: "Full docs-drift scan of an API surface: coverage score, drift issues with file/line locations, health score. The one-shot summary — use drift_list/drift_get for targeted follow-up.",
4322
+ inputSchema: {
4323
+ ...truthShape,
4324
+ min: z.number().optional().describe("Minimum health threshold (result.pass=false below it)")
4325
+ }
4326
+ }, async (args) => {
4327
+ const cli = ["scan"];
4328
+ if (args.entry)
4329
+ cli.push(args.entry);
4330
+ if (args.min !== undefined)
4331
+ cli.push("--min", String(args.min));
4332
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4333
+ });
4334
+ server.registerTool("drift_diff", {
4335
+ title: "Diff two API specs",
4336
+ 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.",
4337
+ inputSchema: {
4338
+ cwd: truthShape.cwd,
4339
+ old: z.string().optional().describe("Old spec file path"),
4340
+ new: z.string().optional().describe("New spec file path"),
4341
+ base: z.string().optional().describe("Git ref for old spec (alternative to files)"),
4342
+ head: z.string().optional().describe("Git ref for new spec (default: working tree)"),
4343
+ entry: z.string().optional().describe("Entry file for git ref extraction")
4344
+ }
4345
+ }, async (args) => toResult(await runDrift(diffArgs("diff", args), args.cwd)));
4346
+ server.registerTool("drift_breaking", {
4347
+ title: "Detect breaking changes",
4348
+ description: "Detect breaking API changes between two specs or git refs (TypeScript only today). Use to check whether docs claiming compatibility are still true.",
4349
+ inputSchema: {
4350
+ cwd: truthShape.cwd,
4351
+ old: z.string().optional().describe("Old spec file path"),
4352
+ new: z.string().optional().describe("New spec file path"),
4353
+ base: z.string().optional().describe("Git ref for old spec (alternative to files)"),
4354
+ head: z.string().optional().describe("Git ref for new spec (default: working tree)"),
4355
+ entry: z.string().optional().describe("Entry file for git ref extraction")
4356
+ }
4357
+ }, async (args) => toResult(await runDrift(diffArgs("breaking", args), args.cwd)));
4358
+ await server.connect(new StdioServerTransport);
4359
+ });
4360
+ }
4361
+ function diffArgs(command, args) {
4362
+ const cli = [command];
4363
+ if (args.old)
4364
+ cli.push(args.old);
4365
+ if (args.new)
4366
+ cli.push(args.new);
4367
+ if (args.base)
4368
+ cli.push("--base", args.base);
4369
+ if (args.head)
4370
+ cli.push("--head", args.head);
4371
+ if (args.entry)
4372
+ cli.push("--entry", args.entry);
4373
+ return cli;
4374
+ }
4375
+
3940
4376
  // src/commands/release.ts
3941
4377
  import { execSync as execSync4 } from "node:child_process";
3942
4378
  import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
3943
- import * as path25 from "node:path";
4379
+ import * as path26 from "node:path";
3944
4380
  import { computeDrift as computeDrift6 } from "@driftdev/sdk";
3945
4381
 
3946
4382
  // src/formatters/release.ts
@@ -3988,7 +4424,7 @@ function getLastTag() {
3988
4424
  }
3989
4425
  }
3990
4426
  function getPackageVersion(cwd) {
3991
- const pkgPath = path25.join(cwd, "package.json");
4427
+ const pkgPath = path26.join(cwd, "package.json");
3992
4428
  if (!existsSync14(pkgPath))
3993
4429
  return null;
3994
4430
  try {
@@ -4004,7 +4440,7 @@ function registerReleaseCommand(program) {
4004
4440
  const cwd = process.cwd();
4005
4441
  try {
4006
4442
  const { config } = loadConfig();
4007
- const entryFile = entry ? path25.resolve(cwd, entry) : config.entry ? path25.resolve(cwd, config.entry) : detectEntry();
4443
+ const entryFile = entry ? path26.resolve(cwd, entry) : config.entry ? path26.resolve(cwd, config.entry) : detectEntry();
4008
4444
  const { spec } = await cachedExtract(entryFile);
4009
4445
  const exports = spec.exports ?? [];
4010
4446
  const total = exports.length;
@@ -4129,7 +4565,7 @@ function renderReport(data) {
4129
4565
  // src/utils/scan-packages.ts
4130
4566
  import { execSync as execSync5 } from "node:child_process";
4131
4567
  import { existsSync as existsSync15, readFileSync as readFileSync19 } from "node:fs";
4132
- import * as path26 from "node:path";
4568
+ import * as path27 from "node:path";
4133
4569
  import { computeDrift as computeDrift7 } from "@driftdev/sdk";
4134
4570
  function detectPackageDirs2(cwd) {
4135
4571
  const workspaces = detectWorkspaces(cwd);
@@ -4148,11 +4584,11 @@ async function scanAllPackages(cwd) {
4148
4584
  const packageDirs = detectPackageDirs2(cwd);
4149
4585
  const results = [];
4150
4586
  for (const dir of packageDirs) {
4151
- const absDir = dir === "." ? cwd : path26.join(cwd, dir);
4587
+ const absDir = dir === "." ? cwd : path27.join(cwd, dir);
4152
4588
  if (!existsSync15(absDir))
4153
4589
  continue;
4154
4590
  let name = dir;
4155
- const pkgPath = path26.join(absDir, "package.json");
4591
+ const pkgPath = path27.join(absDir, "package.json");
4156
4592
  if (existsSync15(pkgPath)) {
4157
4593
  try {
4158
4594
  const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
@@ -4251,10 +4687,8 @@ function registerReportCommand(program) {
4251
4687
  }
4252
4688
 
4253
4689
  // src/commands/scan.ts
4254
- import { existsSync as existsSync16, readFileSync as readFileSync20 } from "node:fs";
4255
- import * as path27 from "node:path";
4256
- import { fromSource } from "@driftdev/clarity-adapter";
4257
- import { fromDocument } from "@driftdev/openapi-adapter";
4690
+ import { readFileSync as readFileSync20 } from "node:fs";
4691
+ import * as path28 from "node:path";
4258
4692
  import {
4259
4693
  buildExportRegistry as buildExportRegistry2,
4260
4694
  computeDrift as computeDrift8,
@@ -4321,59 +4755,17 @@ function renderBatchScan(data) {
4321
4755
  }
4322
4756
 
4323
4757
  // src/commands/scan.ts
4324
- function getPackageInfo2(cwd) {
4325
- const pkgPath = path27.join(cwd, "package.json");
4326
- if (!existsSync16(pkgPath))
4327
- return {};
4328
- try {
4329
- const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
4330
- return { name: pkg.name, version: pkg.version };
4331
- } catch (err) {
4332
- formatWarning(`Could not parse package.json${err instanceof Error ? `: ${err.message}` : ""}`);
4333
- return {};
4334
- }
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
- }
4367
4758
  function registerScanCommand(program) {
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) => {
4759
+ 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 (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)").action(async (entry, options) => {
4369
4760
  const startTime = Date.now();
4370
4761
  const version = getVersion();
4371
4762
  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
- }
4763
+ const lang = resolveLang({
4764
+ entry,
4765
+ lang: options.lang,
4766
+ spec: options.spec,
4767
+ abi: options.abi
4768
+ });
4377
4769
  if (lang !== "typescript" && options.all) {
4378
4770
  formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4379
4771
  return;
@@ -4439,12 +4831,16 @@ function registerScanCommand(program) {
4439
4831
  return;
4440
4832
  }
4441
4833
  const { config } = loadConfig();
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);
4834
+ let entryFile = entry ? path28.resolve(process.cwd(), entry) : undefined;
4835
+ if (lang === "typescript" && !entryFile) {
4836
+ entryFile = config.entry ? path28.resolve(process.cwd(), config.entry) : detectEntry();
4837
+ }
4838
+ const { apiSpec, packageName, packageVersion } = await resolveTruth({
4839
+ entry: entryFile,
4840
+ lang,
4841
+ spec: options.spec,
4842
+ abi: options.abi
4843
+ });
4448
4844
  const exports = apiSpec.exports ?? [];
4449
4845
  const total = exports.length;
4450
4846
  let documented = 0;
@@ -4468,7 +4864,7 @@ function registerScanCommand(program) {
4468
4864
  }
4469
4865
  if (lang === "typescript") {
4470
4866
  try {
4471
- const pkgJsonPath = path27.resolve(process.cwd(), "package.json");
4867
+ const pkgJsonPath = path28.resolve(process.cwd(), "package.json");
4472
4868
  const pkgJson = JSON.parse(readFileSync20(pkgJsonPath, "utf-8"));
4473
4869
  const pkgName = pkgJson.name;
4474
4870
  if (pkgName) {
@@ -4576,7 +4972,7 @@ function registerSemverCommand(program) {
4576
4972
 
4577
4973
  // src/commands/validate.ts
4578
4974
  import { readFileSync as readFileSync21 } from "node:fs";
4579
- import * as path28 from "node:path";
4975
+ import * as path29 from "node:path";
4580
4976
  import { validateSpec } from "@openpkg-ts/spec";
4581
4977
 
4582
4978
  // src/formatters/validate.ts
@@ -4602,7 +4998,7 @@ function registerValidateCommand(program) {
4602
4998
  const startTime = Date.now();
4603
4999
  const version = getVersion();
4604
5000
  try {
4605
- const filePath = path28.resolve(process.cwd(), file);
5001
+ const filePath = path29.resolve(process.cwd(), file);
4606
5002
  const content = readFileSync21(filePath, "utf-8");
4607
5003
  const spec = JSON.parse(content);
4608
5004
  const result = validateSpec(spec);
@@ -4728,13 +5124,13 @@ function extractCapabilities(program) {
4728
5124
 
4729
5125
  // src/drift.ts
4730
5126
  var __filename2 = fileURLToPath2(import.meta.url);
4731
- var __dirname3 = path29.dirname(__filename2);
4732
- var packageJson = JSON.parse(readFileSync22(path29.join(__dirname3, "../package.json"), "utf-8"));
5127
+ var __dirname3 = path30.dirname(__filename2);
5128
+ var packageJson = JSON.parse(readFileSync22(path30.join(__dirname3, "../package.json"), "utf-8"));
4733
5129
  var program = new Command;
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) => {
5130
+ program.name("drift").description("drift — detect when your docs drift from your code").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) => {
4735
5131
  const opts = program.opts();
4736
5132
  if (opts.cwd) {
4737
- process.chdir(path29.resolve(opts.cwd));
5133
+ process.chdir(path30.resolve(opts.cwd));
4738
5134
  }
4739
5135
  setOutputMode({ json: opts.json, human: opts.human });
4740
5136
  setConfigPath(opts.config);
@@ -4763,6 +5159,7 @@ registerConfigCommand(program);
4763
5159
  registerContextCommand(program);
4764
5160
  registerCacheCommand(program);
4765
5161
  registerCommandsCommand(program);
5162
+ registerMcpCommand(program);
4766
5163
  var HUMAN_COMMANDS = new Set(["scan", "ci", "init", "commands"]);
4767
5164
  for (const cmd of program.commands) {
4768
5165
  if (!HUMAN_COMMANDS.has(cmd.name())) {
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@driftdev/cli",
3
- "version": "1.4.0",
4
- "description": "Drift CLI - Documentation coverage and drift detection for TypeScript",
3
+ "version": "1.5.0",
4
+ "description": "Drift CLI - detect when your docs drift from your code",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "cli",
8
8
  "documentation",
9
9
  "drift",
10
10
  "docs-coverage",
11
- "drift-detection"
11
+ "drift-detection",
12
+ "openapi",
13
+ "rest",
14
+ "api",
15
+ "agent",
16
+ "mcp"
12
17
  ],
13
18
  "homepage": "https://github.com/ryanwaits/drift#readme",
14
19
  "repository": {
@@ -42,10 +47,12 @@
42
47
  "@driftdev/clarity-adapter": "^1.0.1",
43
48
  "@driftdev/openapi-adapter": "^1.0.0",
44
49
  "@driftdev/sdk": "^1.4.0",
50
+ "@modelcontextprotocol/sdk": "^1.29.0",
45
51
  "@openpkg-ts/sdk": "^0.38.0",
46
52
  "@openpkg-ts/spec": "^0.37.0",
47
53
  "chalk": "^5.4.1",
48
- "commander": "^14.0.0"
54
+ "commander": "^14.0.0",
55
+ "zod": "^4.2.1"
49
56
  },
50
57
  "devDependencies": {
51
58
  "@types/bun": "^1.3.14",