@driftdev/cli 1.4.0 → 1.5.1

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 +604 -197
  2. package/package.json +12 -5
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,17 @@ 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
3001
+ function col(text, width) {
3002
+ return padRight(text, Math.max(width, text.length + 2));
3003
+ }
2864
3004
  function renderGet(data) {
2865
3005
  const lines = [""];
2866
3006
  const exp = data.export;
3007
+ const schema = typeof exp.schema === "object" && exp.schema !== null ? exp.schema : undefined;
2867
3008
  lines.push(indent(`${c.bold(exp.name)}${" ".repeat(Math.max(2, 50 - exp.name.length))}${c.gray(exp.kind)}`));
2868
3009
  if (exp.deprecated) {
2869
3010
  lines.push(indent(c.yellow("deprecated")));
@@ -2876,24 +3017,24 @@ function renderGet(data) {
2876
3017
  lines.push(indent(` ${exp.signature}`));
2877
3018
  lines.push("");
2878
3019
  }
2879
- const params = exp.parameters ?? extractParams(exp.schema);
3020
+ const params = exp.parameters ?? extractParams(schema);
2880
3021
  if (params.length > 0) {
2881
3022
  lines.push(indent(c.gray(" PARAMETERS")));
2882
3023
  for (const p of params) {
2883
3024
  const req = p.required ? "required" : "optional";
2884
3025
  const type = p.type ?? "unknown";
2885
3026
  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}`));
3027
+ lines.push(indent(` ${col(p.name ?? "", 16)}${col(type, 24)}${c.gray(req)}${desc}`));
2887
3028
  }
2888
3029
  lines.push("");
2889
3030
  }
2890
- const returns = exp.returns ?? extractReturns(exp.schema);
3031
+ const returns = exp.returns ?? extractReturns(schema);
2891
3032
  if (returns) {
2892
3033
  lines.push(indent(c.gray(" RETURNS")));
2893
3034
  lines.push(indent(` ${returns.type ?? "void"}`));
2894
3035
  lines.push("");
2895
3036
  }
2896
- const members = exp.members ?? extractMembers(exp.schema);
3037
+ const members = exp.members ?? extractMembers(schema);
2897
3038
  if (members.length > 0) {
2898
3039
  const shown = members.slice(0, 50);
2899
3040
  const remaining = members.length - shown.length;
@@ -2901,7 +3042,7 @@ function renderGet(data) {
2901
3042
  for (const m of shown) {
2902
3043
  const req = m.required ? "required" : "optional";
2903
3044
  const type = m.type ?? "";
2904
- lines.push(indent(` ${padRight(m.name, 20)}${padRight(type, 20)}${c.gray(req)}`));
3045
+ lines.push(indent(` ${col(m.name ?? "", 20)}${col(type, 20)}${c.gray(req)}`));
2905
3046
  }
2906
3047
  if (remaining > 0) {
2907
3048
  lines.push(indent(c.gray(` ... ${remaining} more`)));
@@ -2966,6 +3107,8 @@ function extractPropsFromSchema(schema) {
2966
3107
  function formatType(schema) {
2967
3108
  if (!schema)
2968
3109
  return "unknown";
3110
+ if (typeof schema === "string")
3111
+ return schema;
2969
3112
  if (schema.$ref)
2970
3113
  return schema.$ref.replace("#/types/", "");
2971
3114
  if (schema.type === "array" && schema.items)
@@ -3037,14 +3180,72 @@ function looksLikeFilePath(s) {
3037
3180
 
3038
3181
  // src/commands/get.ts
3039
3182
  function registerGetCommand(program) {
3040
- program.command("get <nameOrEntry> [name]").description("Get detailed spec for a single export").action(async (nameOrEntry, name) => {
3183
+ 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
3184
  const startTime = Date.now();
3042
3185
  const version = getVersion();
3043
3186
  try {
3187
+ const lang = resolveLang({
3188
+ entry: name ? nameOrEntry : undefined,
3189
+ lang: options.lang,
3190
+ spec: options.spec,
3191
+ abi: options.abi
3192
+ });
3193
+ if (lang !== "typescript") {
3194
+ const entryArg = name ? nameOrEntry : undefined;
3195
+ const exportName2 = name ?? nameOrEntry;
3196
+ const { apiSpec } = await resolveTruth({
3197
+ entry: entryArg,
3198
+ lang,
3199
+ spec: options.spec,
3200
+ abi: options.abi
3201
+ });
3202
+ const allExports = apiSpec.exports ?? [];
3203
+ const exp = allExports.find((e) => e.name === exportName2 || e.id === exportName2);
3204
+ if (!exp) {
3205
+ const suggestions = fuzzyTop(exportName2, allExports);
3206
+ renderNotFound(exportName2, suggestions, startTime, version);
3207
+ return;
3208
+ }
3209
+ const sig = exp.signatures?.[0];
3210
+ const data = {
3211
+ export: {
3212
+ name: exp.name,
3213
+ kind: exp.kind,
3214
+ ...exp.description ? { description: exp.description } : {},
3215
+ ...exp.deprecated ? { deprecated: true } : {},
3216
+ ...sig?.parameters ? {
3217
+ parameters: sig.parameters.map((p) => ({
3218
+ name: p.name,
3219
+ type: schemaTypeString(p.schema),
3220
+ required: p.required,
3221
+ ...p.description ? { description: p.description } : {},
3222
+ schema: p.schema
3223
+ }))
3224
+ } : {},
3225
+ ...sig?.returns ? {
3226
+ returns: {
3227
+ type: schemaTypeString(sig.returns.schema),
3228
+ ...sig.returns.description ? { description: sig.returns.description } : {},
3229
+ schema: sig.returns.schema
3230
+ }
3231
+ } : {},
3232
+ ...exp.members ? {
3233
+ members: exp.members.map((m) => ({
3234
+ name: m.name,
3235
+ ...m.description ? { description: m.description } : {}
3236
+ }))
3237
+ } : {},
3238
+ ...exp.schema ? { schema: exp.schema } : {},
3239
+ ...exp.flags ? { flags: exp.flags } : {}
3240
+ }
3241
+ };
3242
+ formatOutput("get", data, startTime, version, renderGet);
3243
+ return;
3244
+ }
3044
3245
  let entryFile;
3045
3246
  let exportName;
3046
3247
  if (name) {
3047
- entryFile = path20.resolve(process.cwd(), nameOrEntry);
3248
+ entryFile = path21.resolve(process.cwd(), nameOrEntry);
3048
3249
  exportName = name;
3049
3250
  } else {
3050
3251
  entryFile = detectEntry();
@@ -3054,39 +3255,70 @@ function registerGetCommand(program) {
3054
3255
  if (!result.export) {
3055
3256
  const listResult = await listExports({ entryFile });
3056
3257
  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
- }
3258
+ renderNotFound(exportName, suggestions, startTime, version);
3078
3259
  return;
3079
3260
  }
3080
- formatOutput("get", { export: result.export, types: result.types }, startTime, version, renderGet);
3261
+ const types = Object.fromEntries((result.types ?? []).map((t) => [
3262
+ t.name,
3263
+ t.schema ?? {}
3264
+ ]));
3265
+ formatOutput("get", { export: result.export, types }, startTime, version, renderGet);
3081
3266
  } catch (err) {
3082
3267
  formatError("get", err instanceof Error ? err.message : String(err), startTime, version);
3083
3268
  }
3084
3269
  });
3085
3270
  }
3271
+ function schemaTypeString(schema, depth = 0) {
3272
+ if (schema === undefined || schema === null)
3273
+ return;
3274
+ if (typeof schema === "string")
3275
+ return schema;
3276
+ if (typeof schema !== "object" || depth > 3)
3277
+ return;
3278
+ const s = schema;
3279
+ if (typeof s.$ref === "string")
3280
+ return s.$ref.split("/").pop();
3281
+ const composite = s.oneOf ?? s.anyOf;
3282
+ if (Array.isArray(composite)) {
3283
+ const arms = composite.map((arm) => schemaTypeString(arm, depth + 1) ?? "unknown").filter((v, i, a) => a.indexOf(v) === i);
3284
+ return arms.join(" | ");
3285
+ }
3286
+ if (s.type === "array") {
3287
+ const item = schemaTypeString(s.items, depth + 1);
3288
+ return item ? `${item}[]` : "array";
3289
+ }
3290
+ if (typeof s.title === "string" && (s.type === "object" || s.type === undefined))
3291
+ return s.title;
3292
+ if (typeof s.type === "string")
3293
+ return s.type;
3294
+ return;
3295
+ }
3296
+ function renderNotFound(exportName, suggestions, startTime, version) {
3297
+ if (suggestions.length > 0 && shouldRenderHuman()) {
3298
+ const lines = [
3299
+ "",
3300
+ indent(`${c.red("x")} Export "${exportName}" not found.`),
3301
+ "",
3302
+ indent(c.gray("Similar:"))
3303
+ ];
3304
+ for (const s of suggestions) {
3305
+ lines.push(indent(` ${s}`));
3306
+ }
3307
+ lines.push("");
3308
+ lines.push(indent(`drift get ${suggestions[0]}`));
3309
+ lines.push("");
3310
+ process.stdout.write(lines.join(`
3311
+ `));
3312
+ process.exitCode = 1;
3313
+ } else if (suggestions.length > 0) {
3314
+ formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version);
3315
+ } else {
3316
+ formatError("get", `Export '${exportName}' not found`, startTime, version);
3317
+ }
3318
+ }
3086
3319
 
3087
3320
  // src/commands/health.ts
3088
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "node:fs";
3089
- import * as path21 from "node:path";
3321
+ import * as path22 from "node:path";
3090
3322
  import { computeDrift as computeDrift3 } from "@driftdev/sdk";
3091
3323
 
3092
3324
  // src/formatters/health.ts
@@ -3153,23 +3385,21 @@ function computeHealth(totalExports, documented, issues) {
3153
3385
  }
3154
3386
 
3155
3387
  // 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
3388
  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) => {
3389
+ 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
3390
  const startTime = Date.now();
3171
3391
  const version = getVersion();
3172
3392
  try {
3393
+ const lang = resolveLang({
3394
+ entry,
3395
+ lang: options.lang,
3396
+ spec: options.spec,
3397
+ abi: options.abi
3398
+ });
3399
+ if (lang !== "typescript" && options.all) {
3400
+ formatError("health", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
3401
+ return;
3402
+ }
3173
3403
  if (options.all) {
3174
3404
  const allPackages = discoverPackages(process.cwd());
3175
3405
  if (!allPackages || allPackages.length === 0) {
@@ -3185,8 +3415,8 @@ function registerHealthCommand(program) {
3185
3415
  const rows = [];
3186
3416
  let totalDoc = 0;
3187
3417
  let totalAll = 0;
3188
- for (const pkg2 of packages) {
3189
- const { spec: spec2 } = await cachedExtract(pkg2.entry);
3418
+ for (const pkg of packages) {
3419
+ const { spec: spec2 } = await cachedExtract(pkg.entry);
3190
3420
  const exps = spec2.exports ?? [];
3191
3421
  let doc = 0;
3192
3422
  for (const e of exps) {
@@ -3194,7 +3424,7 @@ function registerHealthCommand(program) {
3194
3424
  doc++;
3195
3425
  }
3196
3426
  const score = exps.length > 0 ? Math.round(doc / exps.length * 100) : 100;
3197
- rows.push({ name: pkg2.name, exports: exps.length, score });
3427
+ rows.push({ name: pkg.name, exports: exps.length, score });
3198
3428
  totalDoc += doc;
3199
3429
  totalAll += exps.length;
3200
3430
  }
@@ -3208,8 +3438,15 @@ function registerHealthCommand(program) {
3208
3438
  return;
3209
3439
  }
3210
3440
  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);
3441
+ let entryFile = entry ? path22.resolve(process.cwd(), entry) : undefined;
3442
+ if (lang === "typescript" && !entryFile) {
3443
+ entryFile = config.entry ? path22.resolve(process.cwd(), config.entry) : detectEntry();
3444
+ }
3445
+ const {
3446
+ apiSpec: spec,
3447
+ packageName,
3448
+ packageVersion
3449
+ } = await resolveTruth({ entry: entryFile, lang, spec: options.spec, abi: options.abi });
3213
3450
  const exports = spec.exports ?? [];
3214
3451
  const total = exports.length;
3215
3452
  let documented = 0;
@@ -3226,11 +3463,10 @@ function registerHealthCommand(program) {
3226
3463
  }
3227
3464
  }
3228
3465
  const health = computeHealth(total, documented, issues);
3229
- const pkg = getPackageInfo(process.cwd());
3230
3466
  const data = {
3231
3467
  ...health,
3232
- packageName: pkg.name,
3233
- packageVersion: pkg.version
3468
+ packageName,
3469
+ packageVersion
3234
3470
  };
3235
3471
  let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
3236
3472
  if (min !== undefined && config.coverage?.ratchet) {
@@ -3267,7 +3503,7 @@ function registerHealthCommand(program) {
3267
3503
  // src/commands/init.ts
3268
3504
  init_global();
3269
3505
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
3270
- import * as path22 from "node:path";
3506
+ import * as path23 from "node:path";
3271
3507
  import { extract as extract6 } from "@openpkg-ts/sdk";
3272
3508
  import { normalize as normalize7 } from "@openpkg-ts/spec";
3273
3509
 
@@ -3573,10 +3809,10 @@ ${detailLine}`);
3573
3809
 
3574
3810
  // src/commands/init.ts
3575
3811
  async function scanPackage(cwd, pkgDir) {
3576
- const absDir = path22.join(cwd, pkgDir);
3812
+ const absDir = path23.join(cwd, pkgDir);
3577
3813
  if (!existsSync13(absDir))
3578
3814
  return null;
3579
- const pkgPath = path22.join(absDir, "package.json");
3815
+ const pkgPath = path23.join(absDir, "package.json");
3580
3816
  let name = pkgDir;
3581
3817
  if (existsSync13(pkgPath)) {
3582
3818
  try {
@@ -3598,7 +3834,7 @@ async function scanPackage(cwd, pkgDir) {
3598
3834
  }
3599
3835
  const coverage = total > 0 ? Math.round(documented / total * 100) : 100;
3600
3836
  const health = Math.round(coverage * 0.5 + 100 * 0.5);
3601
- return { name, entry: path22.relative(cwd, entryFile), exports: total, coverage, health };
3837
+ return { name, entry: path23.relative(cwd, entryFile), exports: total, coverage, health };
3602
3838
  } catch {
3603
3839
  return null;
3604
3840
  }
@@ -3634,7 +3870,7 @@ function registerInitCommand(program) {
3634
3870
  return;
3635
3871
  }
3636
3872
  const config = generateConfig(packages);
3637
- const configPath = opts.project ? path22.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3873
+ const configPath = opts.project ? path23.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
3638
3874
  if (!opts.project) {
3639
3875
  const globalDir = getGlobalDir();
3640
3876
  if (!existsSync13(globalDir))
@@ -3659,7 +3895,7 @@ function registerInitCommand(program) {
3659
3895
 
3660
3896
  // src/commands/lint.ts
3661
3897
  import { readFileSync as readFileSync17 } from "node:fs";
3662
- import * as path23 from "node:path";
3898
+ import * as path24 from "node:path";
3663
3899
  import {
3664
3900
  buildExportRegistry,
3665
3901
  computeDrift as computeDrift4,
@@ -3703,10 +3939,20 @@ function renderLint(data, next) {
3703
3939
 
3704
3940
  // src/commands/lint.ts
3705
3941
  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) => {
3942
+ 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
3943
  const startTime = Date.now();
3708
3944
  const version = getVersion();
3709
3945
  try {
3946
+ const lang = resolveLang({
3947
+ entry,
3948
+ lang: options.lang,
3949
+ spec: options.spec,
3950
+ abi: options.abi
3951
+ });
3952
+ if (lang !== "typescript" && options.all) {
3953
+ formatError("lint", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
3954
+ return;
3955
+ }
3710
3956
  if (options.all) {
3711
3957
  const allPackages = discoverPackages(process.cwd());
3712
3958
  if (!allPackages || allPackages.length === 0) {
@@ -3745,8 +3991,16 @@ function registerLintCommand(program) {
3745
3991
  formatOutput("lint", { issues: [], count: 0 }, startTime, version, renderLint);
3746
3992
  return;
3747
3993
  }
3748
- const entryFile = entry ? path23.resolve(process.cwd(), entry) : config.entry ? path23.resolve(process.cwd(), config.entry) : detectEntry();
3749
- const { spec } = await cachedExtract(entryFile);
3994
+ let entryFile = entry ? path24.resolve(process.cwd(), entry) : undefined;
3995
+ if (lang === "typescript" && !entryFile) {
3996
+ entryFile = config.entry ? path24.resolve(process.cwd(), config.entry) : detectEntry();
3997
+ }
3998
+ const { apiSpec: spec } = await resolveTruth({
3999
+ entry: entryFile,
4000
+ lang,
4001
+ spec: options.spec,
4002
+ abi: options.abi
4003
+ });
3750
4004
  const driftResult = computeDrift4(spec);
3751
4005
  const issues = [];
3752
4006
  for (const [exportName, drifts] of driftResult.exports) {
@@ -3760,27 +4014,28 @@ function registerLintCommand(program) {
3760
4014
  });
3761
4015
  }
3762
4016
  }
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
- });
4017
+ if (lang === "typescript")
4018
+ try {
4019
+ const pkgJsonPath = path24.resolve(process.cwd(), "package.json");
4020
+ const pkgJson = JSON.parse(readFileSync17(pkgJsonPath, "utf-8"));
4021
+ const packageName = pkgJson.name;
4022
+ if (packageName) {
4023
+ const registry = buildExportRegistry(spec);
4024
+ const markdownFiles = discoverMarkdownFiles(process.cwd(), config.docs);
4025
+ const proseDrifts = detectProseDrift({ packageName, markdownFiles, registry });
4026
+ for (const drift of proseDrifts) {
4027
+ issues.push({
4028
+ export: drift.target ?? "",
4029
+ issue: drift.issue,
4030
+ ...drift.suggestion ? { location: drift.suggestion } : {},
4031
+ filePath: drift.filePath,
4032
+ line: drift.line
4033
+ });
4034
+ }
3779
4035
  }
4036
+ } catch (err) {
4037
+ formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
3780
4038
  }
3781
- } catch (err) {
3782
- formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
3783
- }
3784
4039
  const data = { issues, count: issues.length };
3785
4040
  const next = issues.length > 0 ? {
3786
4041
  suggested: "drift-fix skill",
@@ -3804,7 +4059,7 @@ function registerLintCommand(program) {
3804
4059
  }
3805
4060
 
3806
4061
  // src/commands/list.ts
3807
- import * as path24 from "node:path";
4062
+ import * as path25 from "node:path";
3808
4063
  import { computeDrift as computeDrift5 } from "@driftdev/sdk";
3809
4064
  import { listExports as listExports2 } from "@openpkg-ts/sdk";
3810
4065
 
@@ -3861,10 +4116,20 @@ function renderList(data) {
3861
4116
 
3862
4117
  // src/commands/list.ts
3863
4118
  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) => {
4119
+ 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
4120
  const startTime = Date.now();
3866
4121
  const version = getVersion();
3867
4122
  try {
4123
+ const lang = resolveLang({
4124
+ entry: searchOrEntry,
4125
+ lang: options.lang,
4126
+ spec: options.spec,
4127
+ abi: options.abi
4128
+ });
4129
+ if (lang !== "typescript" && options.all) {
4130
+ formatError("list", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4131
+ return;
4132
+ }
3868
4133
  if (options.all) {
3869
4134
  const packages = discoverPackages(process.cwd());
3870
4135
  if (!packages || packages.length === 0) {
@@ -3884,23 +4149,50 @@ function registerListCommand(program) {
3884
4149
  formatOutput("list", { packages: rows, filter: filter2 }, startTime, version, renderBatchList);
3885
4150
  return;
3886
4151
  }
3887
- let entryFile;
3888
4152
  let searchTerm;
3889
- if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
3890
- entryFile = path24.resolve(process.cwd(), searchOrEntry);
3891
- } else if (searchOrEntry) {
3892
- entryFile = detectEntry();
3893
- searchTerm = searchOrEntry;
4153
+ let exports;
4154
+ let driftedNames;
4155
+ if (lang !== "typescript") {
4156
+ const entryArg = lang === "clarity" ? searchOrEntry : undefined;
4157
+ if (lang === "openapi")
4158
+ searchTerm = searchOrEntry;
4159
+ const { apiSpec } = await resolveTruth({
4160
+ entry: entryArg,
4161
+ lang,
4162
+ spec: options.spec,
4163
+ abi: options.abi
4164
+ });
4165
+ exports = (apiSpec.exports ?? []).map((e) => ({
4166
+ name: e.name,
4167
+ kind: e.kind,
4168
+ description: e.description,
4169
+ ...e.deprecated ? { deprecated: true } : {}
4170
+ }));
4171
+ if (options.drifted) {
4172
+ driftedNames = new Set(computeDrift5(apiSpec).exports.keys());
4173
+ }
3894
4174
  } else {
3895
- entryFile = detectEntry();
4175
+ let entryFile;
4176
+ if (searchOrEntry && looksLikeFilePath(searchOrEntry)) {
4177
+ entryFile = path25.resolve(process.cwd(), searchOrEntry);
4178
+ } else if (searchOrEntry) {
4179
+ entryFile = detectEntry();
4180
+ searchTerm = searchOrEntry;
4181
+ } else {
4182
+ entryFile = detectEntry();
4183
+ }
4184
+ const result = await listExports2({ entryFile });
4185
+ exports = result.exports.map((e) => ({
4186
+ name: e.name,
4187
+ kind: e.kind,
4188
+ description: e.description,
4189
+ ...e.deprecated ? { deprecated: true } : {}
4190
+ }));
4191
+ if (options.drifted) {
4192
+ const { spec } = await cachedExtract(entryFile);
4193
+ driftedNames = new Set(computeDrift5(spec).exports.keys());
4194
+ }
3896
4195
  }
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
4196
  if (options.kind) {
3905
4197
  const kinds = new Set(options.kind.split(",").map((k) => k.trim().toLowerCase()));
3906
4198
  exports = exports.filter((e) => kinds.has(e.kind));
@@ -3908,11 +4200,9 @@ function registerListCommand(program) {
3908
4200
  if (options.undocumented) {
3909
4201
  exports = exports.filter((e) => !e.description || e.description.trim().length === 0);
3910
4202
  }
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));
4203
+ if (driftedNames) {
4204
+ const names = driftedNames;
4205
+ exports = exports.filter((e) => names.has(e.name));
3916
4206
  }
3917
4207
  if (searchTerm) {
3918
4208
  const matches = fuzzySearch(searchTerm, exports);
@@ -3937,10 +4227,166 @@ function registerListCommand(program) {
3937
4227
  });
3938
4228
  }
3939
4229
 
4230
+ // src/commands/mcp.ts
4231
+ import { spawn } from "node:child_process";
4232
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4233
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4234
+ import { z } from "zod";
4235
+ var truthShape = {
4236
+ cwd: z.string().optional().describe("Directory to run in (project root; where package.json/config live)"),
4237
+ entry: z.string().optional().describe("Entry file: TypeScript entry or Clarity .clar source. Omit for OpenAPI specs."),
4238
+ lang: z.enum(["typescript", "clarity", "openapi"]).optional().describe("Source language override; inferred from spec/abi/.clar extension otherwise"),
4239
+ spec: z.string().optional().describe("OpenAPI 3.x JSON document — local path or https URL. Implies lang=openapi."),
4240
+ abi: z.string().optional().describe("Clarity ABI JSON path (required for Clarity sources)")
4241
+ };
4242
+ function truthFlags(args) {
4243
+ const out = [];
4244
+ if (args.lang)
4245
+ out.push("--lang", args.lang);
4246
+ if (args.spec)
4247
+ out.push("--spec", args.spec);
4248
+ if (args.abi)
4249
+ out.push("--abi", args.abi);
4250
+ return out;
4251
+ }
4252
+ function runDrift(cliArgs, cwd) {
4253
+ return new Promise((resolve18) => {
4254
+ const child = spawn(process.execPath, [process.argv[1], ...cliArgs, "--json"], {
4255
+ cwd: cwd ?? process.cwd(),
4256
+ env: { ...process.env, NO_COLOR: "1" },
4257
+ stdio: ["ignore", "pipe", "pipe"]
4258
+ });
4259
+ let stdout = "";
4260
+ let stderr = "";
4261
+ child.stdout.on("data", (d) => {
4262
+ stdout += d;
4263
+ });
4264
+ child.stderr.on("data", (d) => {
4265
+ stderr += d;
4266
+ });
4267
+ child.on("close", (code) => {
4268
+ const text = stdout.trim() || stderr.trim() || `drift exited with code ${code}`;
4269
+ let ok = code === 0;
4270
+ try {
4271
+ ok = JSON.parse(stdout).ok === true;
4272
+ } catch {}
4273
+ resolve18({ text, ok });
4274
+ });
4275
+ child.on("error", (err) => resolve18({ text: `Failed to run drift: ${err.message}`, ok: false }));
4276
+ });
4277
+ }
4278
+ function toResult({ text, ok }) {
4279
+ return { content: [{ type: "text", text }], isError: !ok };
4280
+ }
4281
+ function registerMcpCommand(program) {
4282
+ program.command("mcp").description("Run an MCP stdio server exposing drift tools to agents").action(async () => {
4283
+ const server = new McpServer({ name: "drift", version: getVersion() });
4284
+ server.registerTool("drift_extract", {
4285
+ title: "Extract API spec",
4286
+ 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.",
4287
+ inputSchema: truthShape
4288
+ }, async (args) => toResult(await runDrift(["extract", ...args.entry ? [args.entry] : [], ...truthFlags(args)], args.cwd)));
4289
+ server.registerTool("drift_list", {
4290
+ title: "List exports/operations",
4291
+ 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.",
4292
+ inputSchema: {
4293
+ ...truthShape,
4294
+ search: z.string().optional().describe("Fuzzy search term"),
4295
+ kind: z.string().optional().describe("Filter by kind (comma-separated)"),
4296
+ undocumented: z.boolean().optional().describe("Only items missing docs"),
4297
+ drifted: z.boolean().optional().describe("Only items whose docs drifted")
4298
+ }
4299
+ }, async (args) => {
4300
+ const cli = ["list"];
4301
+ if (args.entry)
4302
+ cli.push(args.entry);
4303
+ else if (args.search)
4304
+ cli.push(args.search);
4305
+ if (args.kind)
4306
+ cli.push("--kind", args.kind);
4307
+ if (args.undocumented)
4308
+ cli.push("--undocumented");
4309
+ if (args.drifted)
4310
+ cli.push("--drifted");
4311
+ cli.push("--full");
4312
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4313
+ });
4314
+ server.registerTool("drift_get", {
4315
+ title: "Get one export/operation",
4316
+ 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.",
4317
+ inputSchema: {
4318
+ ...truthShape,
4319
+ name: z.string().describe("Export/operation name (e.g. candidateInfo, transfer, createClient)")
4320
+ }
4321
+ }, async (args) => {
4322
+ const cli = ["get"];
4323
+ if (args.entry)
4324
+ cli.push(args.entry, args.name);
4325
+ else
4326
+ cli.push(args.name);
4327
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4328
+ });
4329
+ server.registerTool("drift_scan", {
4330
+ title: "Scan docs health",
4331
+ 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.",
4332
+ inputSchema: {
4333
+ ...truthShape,
4334
+ min: z.number().optional().describe("Minimum health threshold (result.pass=false below it)")
4335
+ }
4336
+ }, async (args) => {
4337
+ const cli = ["scan"];
4338
+ if (args.entry)
4339
+ cli.push(args.entry);
4340
+ if (args.min !== undefined)
4341
+ cli.push("--min", String(args.min));
4342
+ return toResult(await runDrift([...cli, ...truthFlags(args)], args.cwd));
4343
+ });
4344
+ server.registerTool("drift_diff", {
4345
+ title: "Diff two API specs",
4346
+ 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.",
4347
+ inputSchema: {
4348
+ cwd: truthShape.cwd,
4349
+ old: z.string().optional().describe("Old spec file path"),
4350
+ new: z.string().optional().describe("New spec file path"),
4351
+ base: z.string().optional().describe("Git ref for old spec (alternative to files)"),
4352
+ head: z.string().optional().describe("Git ref for new spec (default: working tree)"),
4353
+ entry: z.string().optional().describe("Entry file for git ref extraction")
4354
+ }
4355
+ }, async (args) => toResult(await runDrift(diffArgs("diff", args), args.cwd)));
4356
+ server.registerTool("drift_breaking", {
4357
+ title: "Detect breaking changes",
4358
+ description: "Detect breaking API changes between two specs or git refs (TypeScript only today). Use to check whether docs claiming compatibility are still true.",
4359
+ inputSchema: {
4360
+ cwd: truthShape.cwd,
4361
+ old: z.string().optional().describe("Old spec file path"),
4362
+ new: z.string().optional().describe("New spec file path"),
4363
+ base: z.string().optional().describe("Git ref for old spec (alternative to files)"),
4364
+ head: z.string().optional().describe("Git ref for new spec (default: working tree)"),
4365
+ entry: z.string().optional().describe("Entry file for git ref extraction")
4366
+ }
4367
+ }, async (args) => toResult(await runDrift(diffArgs("breaking", args), args.cwd)));
4368
+ await server.connect(new StdioServerTransport);
4369
+ });
4370
+ }
4371
+ function diffArgs(command, args) {
4372
+ const cli = [command];
4373
+ if (args.old)
4374
+ cli.push(args.old);
4375
+ if (args.new)
4376
+ cli.push(args.new);
4377
+ if (args.base)
4378
+ cli.push("--base", args.base);
4379
+ if (args.head)
4380
+ cli.push("--head", args.head);
4381
+ if (args.entry)
4382
+ cli.push("--entry", args.entry);
4383
+ return cli;
4384
+ }
4385
+
3940
4386
  // src/commands/release.ts
3941
4387
  import { execSync as execSync4 } from "node:child_process";
3942
4388
  import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
3943
- import * as path25 from "node:path";
4389
+ import * as path26 from "node:path";
3944
4390
  import { computeDrift as computeDrift6 } from "@driftdev/sdk";
3945
4391
 
3946
4392
  // src/formatters/release.ts
@@ -3988,7 +4434,7 @@ function getLastTag() {
3988
4434
  }
3989
4435
  }
3990
4436
  function getPackageVersion(cwd) {
3991
- const pkgPath = path25.join(cwd, "package.json");
4437
+ const pkgPath = path26.join(cwd, "package.json");
3992
4438
  if (!existsSync14(pkgPath))
3993
4439
  return null;
3994
4440
  try {
@@ -4004,7 +4450,7 @@ function registerReleaseCommand(program) {
4004
4450
  const cwd = process.cwd();
4005
4451
  try {
4006
4452
  const { config } = loadConfig();
4007
- const entryFile = entry ? path25.resolve(cwd, entry) : config.entry ? path25.resolve(cwd, config.entry) : detectEntry();
4453
+ const entryFile = entry ? path26.resolve(cwd, entry) : config.entry ? path26.resolve(cwd, config.entry) : detectEntry();
4008
4454
  const { spec } = await cachedExtract(entryFile);
4009
4455
  const exports = spec.exports ?? [];
4010
4456
  const total = exports.length;
@@ -4129,7 +4575,7 @@ function renderReport(data) {
4129
4575
  // src/utils/scan-packages.ts
4130
4576
  import { execSync as execSync5 } from "node:child_process";
4131
4577
  import { existsSync as existsSync15, readFileSync as readFileSync19 } from "node:fs";
4132
- import * as path26 from "node:path";
4578
+ import * as path27 from "node:path";
4133
4579
  import { computeDrift as computeDrift7 } from "@driftdev/sdk";
4134
4580
  function detectPackageDirs2(cwd) {
4135
4581
  const workspaces = detectWorkspaces(cwd);
@@ -4148,11 +4594,11 @@ async function scanAllPackages(cwd) {
4148
4594
  const packageDirs = detectPackageDirs2(cwd);
4149
4595
  const results = [];
4150
4596
  for (const dir of packageDirs) {
4151
- const absDir = dir === "." ? cwd : path26.join(cwd, dir);
4597
+ const absDir = dir === "." ? cwd : path27.join(cwd, dir);
4152
4598
  if (!existsSync15(absDir))
4153
4599
  continue;
4154
4600
  let name = dir;
4155
- const pkgPath = path26.join(absDir, "package.json");
4601
+ const pkgPath = path27.join(absDir, "package.json");
4156
4602
  if (existsSync15(pkgPath)) {
4157
4603
  try {
4158
4604
  const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
@@ -4251,10 +4697,8 @@ function registerReportCommand(program) {
4251
4697
  }
4252
4698
 
4253
4699
  // 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";
4700
+ import { readFileSync as readFileSync20 } from "node:fs";
4701
+ import * as path28 from "node:path";
4258
4702
  import {
4259
4703
  buildExportRegistry as buildExportRegistry2,
4260
4704
  computeDrift as computeDrift8,
@@ -4321,59 +4765,17 @@ function renderBatchScan(data) {
4321
4765
  }
4322
4766
 
4323
4767
  // 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
4768
  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) => {
4769
+ 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
4770
  const startTime = Date.now();
4370
4771
  const version = getVersion();
4371
4772
  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
- }
4773
+ const lang = resolveLang({
4774
+ entry,
4775
+ lang: options.lang,
4776
+ spec: options.spec,
4777
+ abi: options.abi
4778
+ });
4377
4779
  if (lang !== "typescript" && options.all) {
4378
4780
  formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
4379
4781
  return;
@@ -4439,12 +4841,16 @@ function registerScanCommand(program) {
4439
4841
  return;
4440
4842
  }
4441
4843
  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);
4844
+ let entryFile = entry ? path28.resolve(process.cwd(), entry) : undefined;
4845
+ if (lang === "typescript" && !entryFile) {
4846
+ entryFile = config.entry ? path28.resolve(process.cwd(), config.entry) : detectEntry();
4847
+ }
4848
+ const { apiSpec, packageName, packageVersion } = await resolveTruth({
4849
+ entry: entryFile,
4850
+ lang,
4851
+ spec: options.spec,
4852
+ abi: options.abi
4853
+ });
4448
4854
  const exports = apiSpec.exports ?? [];
4449
4855
  const total = exports.length;
4450
4856
  let documented = 0;
@@ -4468,7 +4874,7 @@ function registerScanCommand(program) {
4468
4874
  }
4469
4875
  if (lang === "typescript") {
4470
4876
  try {
4471
- const pkgJsonPath = path27.resolve(process.cwd(), "package.json");
4877
+ const pkgJsonPath = path28.resolve(process.cwd(), "package.json");
4472
4878
  const pkgJson = JSON.parse(readFileSync20(pkgJsonPath, "utf-8"));
4473
4879
  const pkgName = pkgJson.name;
4474
4880
  if (pkgName) {
@@ -4576,7 +4982,7 @@ function registerSemverCommand(program) {
4576
4982
 
4577
4983
  // src/commands/validate.ts
4578
4984
  import { readFileSync as readFileSync21 } from "node:fs";
4579
- import * as path28 from "node:path";
4985
+ import * as path29 from "node:path";
4580
4986
  import { validateSpec } from "@openpkg-ts/spec";
4581
4987
 
4582
4988
  // src/formatters/validate.ts
@@ -4602,7 +5008,7 @@ function registerValidateCommand(program) {
4602
5008
  const startTime = Date.now();
4603
5009
  const version = getVersion();
4604
5010
  try {
4605
- const filePath = path28.resolve(process.cwd(), file);
5011
+ const filePath = path29.resolve(process.cwd(), file);
4606
5012
  const content = readFileSync21(filePath, "utf-8");
4607
5013
  const spec = JSON.parse(content);
4608
5014
  const result = validateSpec(spec);
@@ -4728,13 +5134,13 @@ function extractCapabilities(program) {
4728
5134
 
4729
5135
  // src/drift.ts
4730
5136
  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"));
5137
+ var __dirname3 = path30.dirname(__filename2);
5138
+ var packageJson = JSON.parse(readFileSync22(path30.join(__dirname3, "../package.json"), "utf-8"));
4733
5139
  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) => {
5140
+ 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
5141
  const opts = program.opts();
4736
5142
  if (opts.cwd) {
4737
- process.chdir(path29.resolve(opts.cwd));
5143
+ process.chdir(path30.resolve(opts.cwd));
4738
5144
  }
4739
5145
  setOutputMode({ json: opts.json, human: opts.human });
4740
5146
  setConfigPath(opts.config);
@@ -4763,6 +5169,7 @@ registerConfigCommand(program);
4763
5169
  registerContextCommand(program);
4764
5170
  registerCacheCommand(program);
4765
5171
  registerCommandsCommand(program);
5172
+ registerMcpCommand(program);
4766
5173
  var HUMAN_COMMANDS = new Set(["scan", "ci", "init", "commands"]);
4767
5174
  for (const cmd of program.commands) {
4768
5175
  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.1",
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": {
@@ -40,12 +45,14 @@
40
45
  },
41
46
  "dependencies": {
42
47
  "@driftdev/clarity-adapter": "^1.0.1",
43
- "@driftdev/openapi-adapter": "^1.0.0",
48
+ "@driftdev/openapi-adapter": "^1.0.1",
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",