@driftdev/cli 1.3.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.
- package/README.md +10 -8
- package/dist/drift.js +674 -205
- package/package.json +23 -19
package/dist/drift.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
4
8
|
var __export = (target, all) => {
|
|
5
9
|
for (var name in all)
|
|
6
10
|
__defProp(target, name, {
|
|
7
11
|
get: all[name],
|
|
8
12
|
enumerable: true,
|
|
9
13
|
configurable: true,
|
|
10
|
-
set: (
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
11
15
|
});
|
|
12
16
|
};
|
|
13
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -48,7 +52,7 @@ var init_global = () => {};
|
|
|
48
52
|
|
|
49
53
|
// src/drift.ts
|
|
50
54
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
51
|
-
import * as
|
|
55
|
+
import * as path30 from "node:path";
|
|
52
56
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
53
57
|
import { Command } from "commander";
|
|
54
58
|
|
|
@@ -1090,7 +1094,7 @@ function getVersion() {
|
|
|
1090
1094
|
} catch {
|
|
1091
1095
|
cached = "0.0.0";
|
|
1092
1096
|
}
|
|
1093
|
-
return cached;
|
|
1097
|
+
return cached ?? "0.0.0";
|
|
1094
1098
|
}
|
|
1095
1099
|
|
|
1096
1100
|
// src/commands/breaking.ts
|
|
@@ -1161,26 +1165,6 @@ function registerBreakingCommand(program) {
|
|
|
1161
1165
|
});
|
|
1162
1166
|
}
|
|
1163
1167
|
|
|
1164
|
-
// src/commands/commands.ts
|
|
1165
|
-
var GROUPS = {
|
|
1166
|
-
Composed: ["scan", "ci", "health"],
|
|
1167
|
-
Analysis: ["coverage", "lint", "examples"],
|
|
1168
|
-
Extraction: ["extract", "list", "get"],
|
|
1169
|
-
Comparison: ["diff", "breaking", "semver", "changelog"],
|
|
1170
|
-
Setup: ["init", "config", "context"],
|
|
1171
|
-
Plumbing: ["validate", "filter", "cache", "report", "release"]
|
|
1172
|
-
};
|
|
1173
|
-
function registerCommandsCommand(program) {
|
|
1174
|
-
program.command("commands").description("List all available commands grouped by category").action(() => {
|
|
1175
|
-
const maxGroup = Math.max(...Object.keys(GROUPS).map((g) => g.length));
|
|
1176
|
-
for (const [group, cmds] of Object.entries(GROUPS)) {
|
|
1177
|
-
const pad2 = " ".repeat(maxGroup - group.length);
|
|
1178
|
-
process.stdout.write(` ${group}${pad2} ${cmds.join(", ")}
|
|
1179
|
-
`);
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
1168
|
// src/formatters/cache.ts
|
|
1185
1169
|
function renderCacheStatus(data) {
|
|
1186
1170
|
const lines = [];
|
|
@@ -1974,6 +1958,26 @@ function registerCiCommand(program) {
|
|
|
1974
1958
|
});
|
|
1975
1959
|
}
|
|
1976
1960
|
|
|
1961
|
+
// src/commands/commands.ts
|
|
1962
|
+
var GROUPS = {
|
|
1963
|
+
Composed: ["scan", "ci", "health"],
|
|
1964
|
+
Analysis: ["coverage", "lint", "examples"],
|
|
1965
|
+
Extraction: ["extract", "list", "get"],
|
|
1966
|
+
Comparison: ["diff", "breaking", "semver", "changelog"],
|
|
1967
|
+
Setup: ["init", "config", "context"],
|
|
1968
|
+
Plumbing: ["validate", "filter", "cache", "report", "release"]
|
|
1969
|
+
};
|
|
1970
|
+
function registerCommandsCommand(program) {
|
|
1971
|
+
program.command("commands").description("List all available commands grouped by category").action(() => {
|
|
1972
|
+
const maxGroup = Math.max(...Object.keys(GROUPS).map((g) => g.length));
|
|
1973
|
+
for (const [group, cmds] of Object.entries(GROUPS)) {
|
|
1974
|
+
const pad2 = " ".repeat(maxGroup - group.length);
|
|
1975
|
+
process.stdout.write(` ${group}${pad2} ${cmds.join(", ")}
|
|
1976
|
+
`);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1977
1981
|
// src/commands/config.ts
|
|
1978
1982
|
init_global();
|
|
1979
1983
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -2255,7 +2259,7 @@ function registerContextCommand(program) {
|
|
|
2255
2259
|
}
|
|
2256
2260
|
|
|
2257
2261
|
// src/commands/coverage.ts
|
|
2258
|
-
import * as
|
|
2262
|
+
import * as path16 from "node:path";
|
|
2259
2263
|
|
|
2260
2264
|
// src/formatters/coverage.ts
|
|
2261
2265
|
function renderCoverage(data) {
|
|
@@ -2282,12 +2286,114 @@ function renderCoverage(data) {
|
|
|
2282
2286
|
`);
|
|
2283
2287
|
}
|
|
2284
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
|
+
|
|
2285
2381
|
// src/commands/coverage.ts
|
|
2286
2382
|
function registerCoverageCommand(program) {
|
|
2287
|
-
program.command("coverage [entry]").description("Measure documentation coverage
|
|
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) => {
|
|
2288
2384
|
const startTime = Date.now();
|
|
2289
2385
|
const version = getVersion();
|
|
2290
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
|
+
}
|
|
2291
2397
|
if (options.all) {
|
|
2292
2398
|
const allPackages = discoverPackages(process.cwd());
|
|
2293
2399
|
if (!allPackages || allPackages.length === 0) {
|
|
@@ -2329,8 +2435,16 @@ function registerCoverageCommand(program) {
|
|
|
2329
2435
|
return;
|
|
2330
2436
|
}
|
|
2331
2437
|
const { config } = loadConfig();
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
+
});
|
|
2334
2448
|
const exports = spec.exports ?? [];
|
|
2335
2449
|
const total = exports.length;
|
|
2336
2450
|
const undocumented = [];
|
|
@@ -2378,7 +2492,7 @@ function registerCoverageCommand(program) {
|
|
|
2378
2492
|
}
|
|
2379
2493
|
|
|
2380
2494
|
// src/commands/diff.ts
|
|
2381
|
-
import * as
|
|
2495
|
+
import * as path17 from "node:path";
|
|
2382
2496
|
import { extract as extract5 } from "@openpkg-ts/sdk";
|
|
2383
2497
|
import { categorizeBreakingChanges as categorizeBreakingChanges4, diffSpec as diffSpec4, normalize as normalize5 } from "@openpkg-ts/spec";
|
|
2384
2498
|
|
|
@@ -2452,7 +2566,7 @@ function registerDiffCommand(program) {
|
|
|
2452
2566
|
let totalAdded = 0;
|
|
2453
2567
|
let totalChanged = 0;
|
|
2454
2568
|
for (const pkg of packages) {
|
|
2455
|
-
const relEntry =
|
|
2569
|
+
const relEntry = path17.relative(cwd, pkg.entry);
|
|
2456
2570
|
const oldSpec2 = await extractSpecFromRef(options.base, relEntry, cwd);
|
|
2457
2571
|
const newSpec2 = options.head ? await extractSpecFromRef(options.head, relEntry, cwd) : normalize5((await extract5({ entryFile: pkg.entry })).spec);
|
|
2458
2572
|
const diff2 = diffSpec4(oldSpec2, newSpec2);
|
|
@@ -2522,8 +2636,8 @@ function registerDiffCommand(program) {
|
|
|
2522
2636
|
}
|
|
2523
2637
|
|
|
2524
2638
|
// src/commands/examples.ts
|
|
2525
|
-
import { readFileSync as
|
|
2526
|
-
import * as
|
|
2639
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
2640
|
+
import * as path18 from "node:path";
|
|
2527
2641
|
import { validateExamples } from "@driftdev/sdk";
|
|
2528
2642
|
|
|
2529
2643
|
// src/formatters/examples.ts
|
|
@@ -2602,16 +2716,16 @@ function renderExamples(data) {
|
|
|
2602
2716
|
|
|
2603
2717
|
// src/commands/examples.ts
|
|
2604
2718
|
function findPackagePath(entryFile) {
|
|
2605
|
-
let dir =
|
|
2606
|
-
while (dir !==
|
|
2719
|
+
let dir = path18.dirname(entryFile);
|
|
2720
|
+
while (dir !== path18.dirname(dir)) {
|
|
2607
2721
|
try {
|
|
2608
|
-
|
|
2722
|
+
readFileSync14(path18.join(dir, "package.json"), "utf-8");
|
|
2609
2723
|
return dir;
|
|
2610
2724
|
} catch {
|
|
2611
|
-
dir =
|
|
2725
|
+
dir = path18.dirname(dir);
|
|
2612
2726
|
}
|
|
2613
2727
|
}
|
|
2614
|
-
return
|
|
2728
|
+
return path18.dirname(entryFile);
|
|
2615
2729
|
}
|
|
2616
2730
|
function registerExamplesCommand(program) {
|
|
2617
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) => {
|
|
@@ -2679,7 +2793,7 @@ function registerExamplesCommand(program) {
|
|
|
2679
2793
|
return;
|
|
2680
2794
|
}
|
|
2681
2795
|
const { config } = loadConfig();
|
|
2682
|
-
const entryFile = entry ?
|
|
2796
|
+
const entryFile = entry ? path18.resolve(process.cwd(), entry) : config.entry ? path18.resolve(process.cwd(), config.entry) : detectEntry();
|
|
2683
2797
|
const { spec } = await cachedExtract(entryFile);
|
|
2684
2798
|
const exports = spec.exports ?? [];
|
|
2685
2799
|
const packagePath = findPackagePath(entryFile);
|
|
@@ -2714,7 +2828,7 @@ function registerExamplesCommand(program) {
|
|
|
2714
2828
|
}
|
|
2715
2829
|
|
|
2716
2830
|
// src/commands/extract.ts
|
|
2717
|
-
import * as
|
|
2831
|
+
import * as path19 from "node:path";
|
|
2718
2832
|
import { Drift } from "@driftdev/sdk";
|
|
2719
2833
|
import { normalize as normalize6 } from "@openpkg-ts/spec";
|
|
2720
2834
|
|
|
@@ -2731,10 +2845,37 @@ function renderExtract(data) {
|
|
|
2731
2845
|
|
|
2732
2846
|
// src/commands/extract.ts
|
|
2733
2847
|
function registerExtractCommand(program) {
|
|
2734
|
-
program.command("extract [entry]").description("Extract
|
|
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) => {
|
|
2735
2849
|
const startTime = Date.now();
|
|
2736
2850
|
const version = getVersion();
|
|
2737
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
|
+
}
|
|
2738
2879
|
if (options.all) {
|
|
2739
2880
|
const allPackages = discoverPackages(process.cwd());
|
|
2740
2881
|
if (!allPackages || allPackages.length === 0) {
|
|
@@ -2755,7 +2896,7 @@ function registerExtractCommand(program) {
|
|
|
2755
2896
|
formatOutput("extract", { packages: specs, ...skipped.length > 0 ? { skipped } : {} }, startTime, version);
|
|
2756
2897
|
return;
|
|
2757
2898
|
}
|
|
2758
|
-
const entryFile = entry ?
|
|
2899
|
+
const entryFile = entry ? path19.resolve(process.cwd(), entry) : detectEntry();
|
|
2759
2900
|
const hasFilters = !!(options.only || options.ignore);
|
|
2760
2901
|
let spec;
|
|
2761
2902
|
if (hasFilters) {
|
|
@@ -2794,8 +2935,8 @@ function registerExtractCommand(program) {
|
|
|
2794
2935
|
}
|
|
2795
2936
|
|
|
2796
2937
|
// src/commands/filter.ts
|
|
2797
|
-
import { readFileSync as
|
|
2798
|
-
import * as
|
|
2938
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
2939
|
+
import * as path20 from "node:path";
|
|
2799
2940
|
import { filterSpec } from "@openpkg-ts/sdk";
|
|
2800
2941
|
|
|
2801
2942
|
// src/formatters/filter.ts
|
|
@@ -2826,8 +2967,8 @@ function registerFilterCommand(program) {
|
|
|
2826
2967
|
const startTime = Date.now();
|
|
2827
2968
|
const version = getVersion();
|
|
2828
2969
|
try {
|
|
2829
|
-
const filePath =
|
|
2830
|
-
const content =
|
|
2970
|
+
const filePath = path20.resolve(process.cwd(), file);
|
|
2971
|
+
const content = readFileSync15(filePath, "utf-8");
|
|
2831
2972
|
const spec = JSON.parse(content);
|
|
2832
2973
|
const criteria = {};
|
|
2833
2974
|
if (options.kind) {
|
|
@@ -2853,13 +2994,14 @@ function registerFilterCommand(program) {
|
|
|
2853
2994
|
}
|
|
2854
2995
|
|
|
2855
2996
|
// src/commands/get.ts
|
|
2856
|
-
import * as
|
|
2997
|
+
import * as path21 from "node:path";
|
|
2857
2998
|
import { getExport, listExports } from "@openpkg-ts/sdk";
|
|
2858
2999
|
|
|
2859
3000
|
// src/formatters/get.ts
|
|
2860
3001
|
function renderGet(data) {
|
|
2861
3002
|
const lines = [""];
|
|
2862
3003
|
const exp = data.export;
|
|
3004
|
+
const schema = typeof exp.schema === "object" && exp.schema !== null ? exp.schema : undefined;
|
|
2863
3005
|
lines.push(indent(`${c.bold(exp.name)}${" ".repeat(Math.max(2, 50 - exp.name.length))}${c.gray(exp.kind)}`));
|
|
2864
3006
|
if (exp.deprecated) {
|
|
2865
3007
|
lines.push(indent(c.yellow("deprecated")));
|
|
@@ -2872,24 +3014,24 @@ function renderGet(data) {
|
|
|
2872
3014
|
lines.push(indent(` ${exp.signature}`));
|
|
2873
3015
|
lines.push("");
|
|
2874
3016
|
}
|
|
2875
|
-
const params = exp.parameters ?? extractParams(
|
|
3017
|
+
const params = exp.parameters ?? extractParams(schema);
|
|
2876
3018
|
if (params.length > 0) {
|
|
2877
3019
|
lines.push(indent(c.gray(" PARAMETERS")));
|
|
2878
3020
|
for (const p of params) {
|
|
2879
3021
|
const req = p.required ? "required" : "optional";
|
|
2880
3022
|
const type = p.type ?? "unknown";
|
|
2881
3023
|
const desc = p.description ? ` ${c.dim(JSON.stringify(p.description))}` : "";
|
|
2882
|
-
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}`));
|
|
2883
3025
|
}
|
|
2884
3026
|
lines.push("");
|
|
2885
3027
|
}
|
|
2886
|
-
const returns = exp.returns ?? extractReturns(
|
|
3028
|
+
const returns = exp.returns ?? extractReturns(schema);
|
|
2887
3029
|
if (returns) {
|
|
2888
3030
|
lines.push(indent(c.gray(" RETURNS")));
|
|
2889
3031
|
lines.push(indent(` ${returns.type ?? "void"}`));
|
|
2890
3032
|
lines.push("");
|
|
2891
3033
|
}
|
|
2892
|
-
const members = exp.members ?? extractMembers(
|
|
3034
|
+
const members = exp.members ?? extractMembers(schema);
|
|
2893
3035
|
if (members.length > 0) {
|
|
2894
3036
|
const shown = members.slice(0, 50);
|
|
2895
3037
|
const remaining = members.length - shown.length;
|
|
@@ -2897,7 +3039,7 @@ function renderGet(data) {
|
|
|
2897
3039
|
for (const m of shown) {
|
|
2898
3040
|
const req = m.required ? "required" : "optional";
|
|
2899
3041
|
const type = m.type ?? "";
|
|
2900
|
-
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)}`));
|
|
2901
3043
|
}
|
|
2902
3044
|
if (remaining > 0) {
|
|
2903
3045
|
lines.push(indent(c.gray(` ... ${remaining} more`)));
|
|
@@ -2962,6 +3104,8 @@ function extractPropsFromSchema(schema) {
|
|
|
2962
3104
|
function formatType(schema) {
|
|
2963
3105
|
if (!schema)
|
|
2964
3106
|
return "unknown";
|
|
3107
|
+
if (typeof schema === "string")
|
|
3108
|
+
return schema;
|
|
2965
3109
|
if (schema.$ref)
|
|
2966
3110
|
return schema.$ref.replace("#/types/", "");
|
|
2967
3111
|
if (schema.type === "array" && schema.items)
|
|
@@ -3033,14 +3177,72 @@ function looksLikeFilePath(s) {
|
|
|
3033
3177
|
|
|
3034
3178
|
// src/commands/get.ts
|
|
3035
3179
|
function registerGetCommand(program) {
|
|
3036
|
-
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 = {}) => {
|
|
3037
3181
|
const startTime = Date.now();
|
|
3038
3182
|
const version = getVersion();
|
|
3039
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
|
+
}
|
|
3040
3242
|
let entryFile;
|
|
3041
3243
|
let exportName;
|
|
3042
3244
|
if (name) {
|
|
3043
|
-
entryFile =
|
|
3245
|
+
entryFile = path21.resolve(process.cwd(), nameOrEntry);
|
|
3044
3246
|
exportName = name;
|
|
3045
3247
|
} else {
|
|
3046
3248
|
entryFile = detectEntry();
|
|
@@ -3050,39 +3252,63 @@ function registerGetCommand(program) {
|
|
|
3050
3252
|
if (!result.export) {
|
|
3051
3253
|
const listResult = await listExports({ entryFile });
|
|
3052
3254
|
const suggestions = fuzzyTop(exportName, listResult.exports);
|
|
3053
|
-
|
|
3054
|
-
const lines = [
|
|
3055
|
-
"",
|
|
3056
|
-
indent(`${c.red("x")} Export "${exportName}" not found.`),
|
|
3057
|
-
"",
|
|
3058
|
-
indent(c.gray("Similar:"))
|
|
3059
|
-
];
|
|
3060
|
-
for (const s of suggestions) {
|
|
3061
|
-
lines.push(indent(` ${s}`));
|
|
3062
|
-
}
|
|
3063
|
-
lines.push("");
|
|
3064
|
-
lines.push(indent(`drift get ${suggestions[0]}`));
|
|
3065
|
-
lines.push("");
|
|
3066
|
-
process.stdout.write(lines.join(`
|
|
3067
|
-
`));
|
|
3068
|
-
process.exitCode = 1;
|
|
3069
|
-
} else if (suggestions.length > 0) {
|
|
3070
|
-
formatError("get", `Export '${exportName}' not found. Similar: ${suggestions.join(", ")}`, startTime, version);
|
|
3071
|
-
} else {
|
|
3072
|
-
formatError("get", `Export '${exportName}' not found`, startTime, version);
|
|
3073
|
-
}
|
|
3255
|
+
renderNotFound(exportName, suggestions, startTime, version);
|
|
3074
3256
|
return;
|
|
3075
3257
|
}
|
|
3076
|
-
|
|
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);
|
|
3077
3263
|
} catch (err) {
|
|
3078
3264
|
formatError("get", err instanceof Error ? err.message : String(err), startTime, version);
|
|
3079
3265
|
}
|
|
3080
3266
|
});
|
|
3081
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
|
+
}
|
|
3082
3309
|
|
|
3083
3310
|
// src/commands/health.ts
|
|
3084
|
-
import
|
|
3085
|
-
import * as path21 from "node:path";
|
|
3311
|
+
import * as path22 from "node:path";
|
|
3086
3312
|
import { computeDrift as computeDrift3 } from "@driftdev/sdk";
|
|
3087
3313
|
|
|
3088
3314
|
// src/formatters/health.ts
|
|
@@ -3149,23 +3375,21 @@ function computeHealth(totalExports, documented, issues) {
|
|
|
3149
3375
|
}
|
|
3150
3376
|
|
|
3151
3377
|
// src/commands/health.ts
|
|
3152
|
-
function getPackageInfo(cwd) {
|
|
3153
|
-
const pkgPath = path21.join(cwd, "package.json");
|
|
3154
|
-
if (!existsSync12(pkgPath))
|
|
3155
|
-
return {};
|
|
3156
|
-
try {
|
|
3157
|
-
const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
|
|
3158
|
-
return { name: pkg.name, version: pkg.version };
|
|
3159
|
-
} catch (err) {
|
|
3160
|
-
formatWarning(`Could not parse package.json${err instanceof Error ? `: ${err.message}` : ""}`);
|
|
3161
|
-
return {};
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
3378
|
function registerHealthCommand(program) {
|
|
3165
|
-
program.command("health [entry]").description("Show documentation health score
|
|
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) => {
|
|
3166
3380
|
const startTime = Date.now();
|
|
3167
3381
|
const version = getVersion();
|
|
3168
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
|
+
}
|
|
3169
3393
|
if (options.all) {
|
|
3170
3394
|
const allPackages = discoverPackages(process.cwd());
|
|
3171
3395
|
if (!allPackages || allPackages.length === 0) {
|
|
@@ -3181,8 +3405,8 @@ function registerHealthCommand(program) {
|
|
|
3181
3405
|
const rows = [];
|
|
3182
3406
|
let totalDoc = 0;
|
|
3183
3407
|
let totalAll = 0;
|
|
3184
|
-
for (const
|
|
3185
|
-
const { spec: spec2 } = await cachedExtract(
|
|
3408
|
+
for (const pkg of packages) {
|
|
3409
|
+
const { spec: spec2 } = await cachedExtract(pkg.entry);
|
|
3186
3410
|
const exps = spec2.exports ?? [];
|
|
3187
3411
|
let doc = 0;
|
|
3188
3412
|
for (const e of exps) {
|
|
@@ -3190,7 +3414,7 @@ function registerHealthCommand(program) {
|
|
|
3190
3414
|
doc++;
|
|
3191
3415
|
}
|
|
3192
3416
|
const score = exps.length > 0 ? Math.round(doc / exps.length * 100) : 100;
|
|
3193
|
-
rows.push({ name:
|
|
3417
|
+
rows.push({ name: pkg.name, exports: exps.length, score });
|
|
3194
3418
|
totalDoc += doc;
|
|
3195
3419
|
totalAll += exps.length;
|
|
3196
3420
|
}
|
|
@@ -3204,8 +3428,15 @@ function registerHealthCommand(program) {
|
|
|
3204
3428
|
return;
|
|
3205
3429
|
}
|
|
3206
3430
|
const { config } = loadConfig();
|
|
3207
|
-
|
|
3208
|
-
|
|
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 });
|
|
3209
3440
|
const exports = spec.exports ?? [];
|
|
3210
3441
|
const total = exports.length;
|
|
3211
3442
|
let documented = 0;
|
|
@@ -3222,11 +3453,10 @@ function registerHealthCommand(program) {
|
|
|
3222
3453
|
}
|
|
3223
3454
|
}
|
|
3224
3455
|
const health = computeHealth(total, documented, issues);
|
|
3225
|
-
const pkg = getPackageInfo(process.cwd());
|
|
3226
3456
|
const data = {
|
|
3227
3457
|
...health,
|
|
3228
|
-
packageName
|
|
3229
|
-
packageVersion
|
|
3458
|
+
packageName,
|
|
3459
|
+
packageVersion
|
|
3230
3460
|
};
|
|
3231
3461
|
let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
|
|
3232
3462
|
if (min !== undefined && config.coverage?.ratchet) {
|
|
@@ -3263,7 +3493,7 @@ function registerHealthCommand(program) {
|
|
|
3263
3493
|
// src/commands/init.ts
|
|
3264
3494
|
init_global();
|
|
3265
3495
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3266
|
-
import * as
|
|
3496
|
+
import * as path23 from "node:path";
|
|
3267
3497
|
import { extract as extract6 } from "@openpkg-ts/sdk";
|
|
3268
3498
|
import { normalize as normalize7 } from "@openpkg-ts/spec";
|
|
3269
3499
|
|
|
@@ -3569,10 +3799,10 @@ ${detailLine}`);
|
|
|
3569
3799
|
|
|
3570
3800
|
// src/commands/init.ts
|
|
3571
3801
|
async function scanPackage(cwd, pkgDir) {
|
|
3572
|
-
const absDir =
|
|
3802
|
+
const absDir = path23.join(cwd, pkgDir);
|
|
3573
3803
|
if (!existsSync13(absDir))
|
|
3574
3804
|
return null;
|
|
3575
|
-
const pkgPath =
|
|
3805
|
+
const pkgPath = path23.join(absDir, "package.json");
|
|
3576
3806
|
let name = pkgDir;
|
|
3577
3807
|
if (existsSync13(pkgPath)) {
|
|
3578
3808
|
try {
|
|
@@ -3594,7 +3824,7 @@ async function scanPackage(cwd, pkgDir) {
|
|
|
3594
3824
|
}
|
|
3595
3825
|
const coverage = total > 0 ? Math.round(documented / total * 100) : 100;
|
|
3596
3826
|
const health = Math.round(coverage * 0.5 + 100 * 0.5);
|
|
3597
|
-
return { name, entry:
|
|
3827
|
+
return { name, entry: path23.relative(cwd, entryFile), exports: total, coverage, health };
|
|
3598
3828
|
} catch {
|
|
3599
3829
|
return null;
|
|
3600
3830
|
}
|
|
@@ -3630,7 +3860,7 @@ function registerInitCommand(program) {
|
|
|
3630
3860
|
return;
|
|
3631
3861
|
}
|
|
3632
3862
|
const config = generateConfig(packages);
|
|
3633
|
-
const configPath = opts.project ?
|
|
3863
|
+
const configPath = opts.project ? path23.resolve(cwd, "drift.config.json") : getGlobalConfigPath();
|
|
3634
3864
|
if (!opts.project) {
|
|
3635
3865
|
const globalDir = getGlobalDir();
|
|
3636
3866
|
if (!existsSync13(globalDir))
|
|
@@ -3655,7 +3885,7 @@ function registerInitCommand(program) {
|
|
|
3655
3885
|
|
|
3656
3886
|
// src/commands/lint.ts
|
|
3657
3887
|
import { readFileSync as readFileSync17 } from "node:fs";
|
|
3658
|
-
import * as
|
|
3888
|
+
import * as path24 from "node:path";
|
|
3659
3889
|
import {
|
|
3660
3890
|
buildExportRegistry,
|
|
3661
3891
|
computeDrift as computeDrift4,
|
|
@@ -3699,10 +3929,20 @@ function renderLint(data, next) {
|
|
|
3699
3929
|
|
|
3700
3930
|
// src/commands/lint.ts
|
|
3701
3931
|
function registerLintCommand(program) {
|
|
3702
|
-
program.command("lint [entry]").description("Cross-reference
|
|
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) => {
|
|
3703
3933
|
const startTime = Date.now();
|
|
3704
3934
|
const version = getVersion();
|
|
3705
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
|
+
}
|
|
3706
3946
|
if (options.all) {
|
|
3707
3947
|
const allPackages = discoverPackages(process.cwd());
|
|
3708
3948
|
if (!allPackages || allPackages.length === 0) {
|
|
@@ -3741,8 +3981,16 @@ function registerLintCommand(program) {
|
|
|
3741
3981
|
formatOutput("lint", { issues: [], count: 0 }, startTime, version, renderLint);
|
|
3742
3982
|
return;
|
|
3743
3983
|
}
|
|
3744
|
-
|
|
3745
|
-
|
|
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
|
+
});
|
|
3746
3994
|
const driftResult = computeDrift4(spec);
|
|
3747
3995
|
const issues = [];
|
|
3748
3996
|
for (const [exportName, drifts] of driftResult.exports) {
|
|
@@ -3756,27 +4004,28 @@ function registerLintCommand(program) {
|
|
|
3756
4004
|
});
|
|
3757
4005
|
}
|
|
3758
4006
|
}
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
+
}
|
|
3775
4025
|
}
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
3776
4028
|
}
|
|
3777
|
-
} catch (err) {
|
|
3778
|
-
formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
3779
|
-
}
|
|
3780
4029
|
const data = { issues, count: issues.length };
|
|
3781
4030
|
const next = issues.length > 0 ? {
|
|
3782
4031
|
suggested: "drift-fix skill",
|
|
@@ -3800,7 +4049,7 @@ function registerLintCommand(program) {
|
|
|
3800
4049
|
}
|
|
3801
4050
|
|
|
3802
4051
|
// src/commands/list.ts
|
|
3803
|
-
import * as
|
|
4052
|
+
import * as path25 from "node:path";
|
|
3804
4053
|
import { computeDrift as computeDrift5 } from "@driftdev/sdk";
|
|
3805
4054
|
import { listExports as listExports2 } from "@openpkg-ts/sdk";
|
|
3806
4055
|
|
|
@@ -3857,10 +4106,20 @@ function renderList(data) {
|
|
|
3857
4106
|
|
|
3858
4107
|
// src/commands/list.ts
|
|
3859
4108
|
function registerListCommand(program) {
|
|
3860
|
-
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
|
|
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) => {
|
|
3861
4110
|
const startTime = Date.now();
|
|
3862
4111
|
const version = getVersion();
|
|
3863
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
|
+
}
|
|
3864
4123
|
if (options.all) {
|
|
3865
4124
|
const packages = discoverPackages(process.cwd());
|
|
3866
4125
|
if (!packages || packages.length === 0) {
|
|
@@ -3880,23 +4139,50 @@ function registerListCommand(program) {
|
|
|
3880
4139
|
formatOutput("list", { packages: rows, filter: filter2 }, startTime, version, renderBatchList);
|
|
3881
4140
|
return;
|
|
3882
4141
|
}
|
|
3883
|
-
let entryFile;
|
|
3884
4142
|
let searchTerm;
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
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
|
+
}
|
|
3890
4164
|
} else {
|
|
3891
|
-
entryFile
|
|
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
|
+
}
|
|
3892
4185
|
}
|
|
3893
|
-
const result = await listExports2({ entryFile });
|
|
3894
|
-
let exports = result.exports.map((e) => ({
|
|
3895
|
-
name: e.name,
|
|
3896
|
-
kind: e.kind,
|
|
3897
|
-
description: e.description,
|
|
3898
|
-
...e.deprecated ? { deprecated: true } : {}
|
|
3899
|
-
}));
|
|
3900
4186
|
if (options.kind) {
|
|
3901
4187
|
const kinds = new Set(options.kind.split(",").map((k) => k.trim().toLowerCase()));
|
|
3902
4188
|
exports = exports.filter((e) => kinds.has(e.kind));
|
|
@@ -3904,11 +4190,9 @@ function registerListCommand(program) {
|
|
|
3904
4190
|
if (options.undocumented) {
|
|
3905
4191
|
exports = exports.filter((e) => !e.description || e.description.trim().length === 0);
|
|
3906
4192
|
}
|
|
3907
|
-
if (
|
|
3908
|
-
const
|
|
3909
|
-
|
|
3910
|
-
const driftedNames = new Set(driftResult.exports.keys());
|
|
3911
|
-
exports = exports.filter((e) => driftedNames.has(e.name));
|
|
4193
|
+
if (driftedNames) {
|
|
4194
|
+
const names = driftedNames;
|
|
4195
|
+
exports = exports.filter((e) => names.has(e.name));
|
|
3912
4196
|
}
|
|
3913
4197
|
if (searchTerm) {
|
|
3914
4198
|
const matches = fuzzySearch(searchTerm, exports);
|
|
@@ -3933,10 +4217,166 @@ function registerListCommand(program) {
|
|
|
3933
4217
|
});
|
|
3934
4218
|
}
|
|
3935
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
|
+
|
|
3936
4376
|
// src/commands/release.ts
|
|
3937
4377
|
import { execSync as execSync4 } from "node:child_process";
|
|
3938
4378
|
import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
|
|
3939
|
-
import * as
|
|
4379
|
+
import * as path26 from "node:path";
|
|
3940
4380
|
import { computeDrift as computeDrift6 } from "@driftdev/sdk";
|
|
3941
4381
|
|
|
3942
4382
|
// src/formatters/release.ts
|
|
@@ -3984,7 +4424,7 @@ function getLastTag() {
|
|
|
3984
4424
|
}
|
|
3985
4425
|
}
|
|
3986
4426
|
function getPackageVersion(cwd) {
|
|
3987
|
-
const pkgPath =
|
|
4427
|
+
const pkgPath = path26.join(cwd, "package.json");
|
|
3988
4428
|
if (!existsSync14(pkgPath))
|
|
3989
4429
|
return null;
|
|
3990
4430
|
try {
|
|
@@ -4000,7 +4440,7 @@ function registerReleaseCommand(program) {
|
|
|
4000
4440
|
const cwd = process.cwd();
|
|
4001
4441
|
try {
|
|
4002
4442
|
const { config } = loadConfig();
|
|
4003
|
-
const entryFile = entry ?
|
|
4443
|
+
const entryFile = entry ? path26.resolve(cwd, entry) : config.entry ? path26.resolve(cwd, config.entry) : detectEntry();
|
|
4004
4444
|
const { spec } = await cachedExtract(entryFile);
|
|
4005
4445
|
const exports = spec.exports ?? [];
|
|
4006
4446
|
const total = exports.length;
|
|
@@ -4125,7 +4565,7 @@ function renderReport(data) {
|
|
|
4125
4565
|
// src/utils/scan-packages.ts
|
|
4126
4566
|
import { execSync as execSync5 } from "node:child_process";
|
|
4127
4567
|
import { existsSync as existsSync15, readFileSync as readFileSync19 } from "node:fs";
|
|
4128
|
-
import * as
|
|
4568
|
+
import * as path27 from "node:path";
|
|
4129
4569
|
import { computeDrift as computeDrift7 } from "@driftdev/sdk";
|
|
4130
4570
|
function detectPackageDirs2(cwd) {
|
|
4131
4571
|
const workspaces = detectWorkspaces(cwd);
|
|
@@ -4144,11 +4584,11 @@ async function scanAllPackages(cwd) {
|
|
|
4144
4584
|
const packageDirs = detectPackageDirs2(cwd);
|
|
4145
4585
|
const results = [];
|
|
4146
4586
|
for (const dir of packageDirs) {
|
|
4147
|
-
const absDir = dir === "." ? cwd :
|
|
4587
|
+
const absDir = dir === "." ? cwd : path27.join(cwd, dir);
|
|
4148
4588
|
if (!existsSync15(absDir))
|
|
4149
4589
|
continue;
|
|
4150
4590
|
let name = dir;
|
|
4151
|
-
const pkgPath =
|
|
4591
|
+
const pkgPath = path27.join(absDir, "package.json");
|
|
4152
4592
|
if (existsSync15(pkgPath)) {
|
|
4153
4593
|
try {
|
|
4154
4594
|
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
@@ -4247,8 +4687,8 @@ function registerReportCommand(program) {
|
|
|
4247
4687
|
}
|
|
4248
4688
|
|
|
4249
4689
|
// src/commands/scan.ts
|
|
4250
|
-
import {
|
|
4251
|
-
import * as
|
|
4690
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
4691
|
+
import * as path28 from "node:path";
|
|
4252
4692
|
import {
|
|
4253
4693
|
buildExportRegistry as buildExportRegistry2,
|
|
4254
4694
|
computeDrift as computeDrift8,
|
|
@@ -4315,23 +4755,29 @@ function renderBatchScan(data) {
|
|
|
4315
4755
|
}
|
|
4316
4756
|
|
|
4317
4757
|
// src/commands/scan.ts
|
|
4318
|
-
function getPackageInfo2(cwd) {
|
|
4319
|
-
const pkgPath = path27.join(cwd, "package.json");
|
|
4320
|
-
if (!existsSync16(pkgPath))
|
|
4321
|
-
return {};
|
|
4322
|
-
try {
|
|
4323
|
-
const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));
|
|
4324
|
-
return { name: pkg.name, version: pkg.version };
|
|
4325
|
-
} catch (err) {
|
|
4326
|
-
formatWarning(`Could not parse package.json${err instanceof Error ? `: ${err.message}` : ""}`);
|
|
4327
|
-
return {};
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
4758
|
function registerScanCommand(program) {
|
|
4331
|
-
program.command("scan [entry]").description("Run coverage + lint + prose drift in one pass").option("--min <n>", "Minimum health threshold (exit 1 if below)").option("--all", "Run across all workspace packages").option("--private", "Include private packages in --all mode").action(async (entry, options) => {
|
|
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) => {
|
|
4332
4760
|
const startTime = Date.now();
|
|
4333
4761
|
const version = getVersion();
|
|
4334
4762
|
try {
|
|
4763
|
+
const lang = resolveLang({
|
|
4764
|
+
entry,
|
|
4765
|
+
lang: options.lang,
|
|
4766
|
+
spec: options.spec,
|
|
4767
|
+
abi: options.abi
|
|
4768
|
+
});
|
|
4769
|
+
if (lang !== "typescript" && options.all) {
|
|
4770
|
+
formatError("scan", `Batch mode (--all) not yet supported for ${lang}`, startTime, version);
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
if (lang === "clarity" && !options.abi) {
|
|
4774
|
+
formatError("scan", "--abi is required when --lang clarity", startTime, version);
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
if (lang === "openapi" && !options.spec) {
|
|
4778
|
+
formatError("scan", "--spec is required when --lang openapi", startTime, version);
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4335
4781
|
if (options.all) {
|
|
4336
4782
|
const allPackages = discoverPackages(process.cwd());
|
|
4337
4783
|
if (!allPackages || allPackages.length === 0) {
|
|
@@ -4346,16 +4792,16 @@ function registerScanCommand(program) {
|
|
|
4346
4792
|
}
|
|
4347
4793
|
const rows = [];
|
|
4348
4794
|
let anyFail = false;
|
|
4349
|
-
for (const
|
|
4350
|
-
const { spec
|
|
4351
|
-
const exps =
|
|
4795
|
+
for (const pkg of packages) {
|
|
4796
|
+
const { spec } = await cachedExtract(pkg.entry);
|
|
4797
|
+
const exps = spec.exports ?? [];
|
|
4352
4798
|
let documented2 = 0;
|
|
4353
4799
|
for (const e of exps) {
|
|
4354
4800
|
if (e.description?.trim())
|
|
4355
4801
|
documented2++;
|
|
4356
4802
|
}
|
|
4357
4803
|
const coverage = exps.length > 0 ? Math.round(documented2 / exps.length * 100) : 100;
|
|
4358
|
-
const driftResult2 = computeDrift8(
|
|
4804
|
+
const driftResult2 = computeDrift8(spec);
|
|
4359
4805
|
const issues2 = [];
|
|
4360
4806
|
for (const [exportName, drifts] of driftResult2.exports) {
|
|
4361
4807
|
for (const d of drifts)
|
|
@@ -4366,7 +4812,7 @@ function registerScanCommand(program) {
|
|
|
4366
4812
|
if (min2 !== undefined && h2.health < min2)
|
|
4367
4813
|
anyFail = true;
|
|
4368
4814
|
rows.push({
|
|
4369
|
-
name:
|
|
4815
|
+
name: pkg.name,
|
|
4370
4816
|
exports: exps.length,
|
|
4371
4817
|
coverage,
|
|
4372
4818
|
lintIssues: issues2.length,
|
|
@@ -4385,9 +4831,17 @@ function registerScanCommand(program) {
|
|
|
4385
4831
|
return;
|
|
4386
4832
|
}
|
|
4387
4833
|
const { config } = loadConfig();
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
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
|
+
});
|
|
4844
|
+
const exports = apiSpec.exports ?? [];
|
|
4391
4845
|
const total = exports.length;
|
|
4392
4846
|
let documented = 0;
|
|
4393
4847
|
for (const exp of exports) {
|
|
@@ -4395,7 +4849,7 @@ function registerScanCommand(program) {
|
|
|
4395
4849
|
documented++;
|
|
4396
4850
|
}
|
|
4397
4851
|
const coverageScore = total > 0 ? Math.round(documented / total * 100) : 100;
|
|
4398
|
-
const driftResult = computeDrift8(
|
|
4852
|
+
const driftResult = computeDrift8(apiSpec);
|
|
4399
4853
|
const issues = [];
|
|
4400
4854
|
for (const [exportName, drifts] of driftResult.exports) {
|
|
4401
4855
|
for (const drift of drifts) {
|
|
@@ -4408,30 +4862,35 @@ function registerScanCommand(program) {
|
|
|
4408
4862
|
});
|
|
4409
4863
|
}
|
|
4410
4864
|
}
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
...drift.suggestion ? { location: drift.suggestion } : {},
|
|
4424
|
-
filePath: drift.filePath,
|
|
4425
|
-
line: drift.line
|
|
4865
|
+
if (lang === "typescript") {
|
|
4866
|
+
try {
|
|
4867
|
+
const pkgJsonPath = path28.resolve(process.cwd(), "package.json");
|
|
4868
|
+
const pkgJson = JSON.parse(readFileSync20(pkgJsonPath, "utf-8"));
|
|
4869
|
+
const pkgName = pkgJson.name;
|
|
4870
|
+
if (pkgName) {
|
|
4871
|
+
const registry = buildExportRegistry2(apiSpec);
|
|
4872
|
+
const markdownFiles = discoverMarkdownFiles2(process.cwd(), config.docs);
|
|
4873
|
+
const proseDrifts = detectProseDrift2({
|
|
4874
|
+
packageName: pkgName,
|
|
4875
|
+
markdownFiles,
|
|
4876
|
+
registry
|
|
4426
4877
|
});
|
|
4878
|
+
for (const drift of proseDrifts) {
|
|
4879
|
+
issues.push({
|
|
4880
|
+
export: drift.target ?? "",
|
|
4881
|
+
issue: drift.issue,
|
|
4882
|
+
...drift.suggestion ? { location: drift.suggestion } : {},
|
|
4883
|
+
filePath: drift.filePath,
|
|
4884
|
+
line: drift.line
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4427
4887
|
}
|
|
4888
|
+
} catch (err) {
|
|
4889
|
+
formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
4428
4890
|
}
|
|
4429
|
-
} catch (err) {
|
|
4430
|
-
formatWarning(`Prose drift skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
4431
4891
|
}
|
|
4432
4892
|
const healthIssues = issues.map((i) => ({ export: i.export, issue: i.issue }));
|
|
4433
4893
|
const h = computeHealth(total, documented, healthIssues);
|
|
4434
|
-
const pkg = getPackageInfo2(process.cwd());
|
|
4435
4894
|
let min = options.min ? parseInt(options.min, 10) : config.coverage?.min;
|
|
4436
4895
|
if (min !== undefined && config.coverage?.ratchet) {
|
|
4437
4896
|
const ratchet = computeRatchetMin(min);
|
|
@@ -4443,8 +4902,8 @@ function registerScanCommand(program) {
|
|
|
4443
4902
|
lint: { issues, count: issues.length },
|
|
4444
4903
|
health: h.health,
|
|
4445
4904
|
pass,
|
|
4446
|
-
packageName
|
|
4447
|
-
packageVersion
|
|
4905
|
+
packageName,
|
|
4906
|
+
packageVersion
|
|
4448
4907
|
};
|
|
4449
4908
|
let next;
|
|
4450
4909
|
if (issues.length > 0) {
|
|
@@ -4513,7 +4972,7 @@ function registerSemverCommand(program) {
|
|
|
4513
4972
|
|
|
4514
4973
|
// src/commands/validate.ts
|
|
4515
4974
|
import { readFileSync as readFileSync21 } from "node:fs";
|
|
4516
|
-
import * as
|
|
4975
|
+
import * as path29 from "node:path";
|
|
4517
4976
|
import { validateSpec } from "@openpkg-ts/spec";
|
|
4518
4977
|
|
|
4519
4978
|
// src/formatters/validate.ts
|
|
@@ -4539,7 +4998,7 @@ function registerValidateCommand(program) {
|
|
|
4539
4998
|
const startTime = Date.now();
|
|
4540
4999
|
const version = getVersion();
|
|
4541
5000
|
try {
|
|
4542
|
-
const filePath =
|
|
5001
|
+
const filePath = path29.resolve(process.cwd(), file);
|
|
4543
5002
|
const content = readFileSync21(filePath, "utf-8");
|
|
4544
5003
|
const spec = JSON.parse(content);
|
|
4545
5004
|
const result = validateSpec(spec);
|
|
@@ -4645,24 +5104,33 @@ function extractCapabilities(program) {
|
|
|
4645
5104
|
}
|
|
4646
5105
|
],
|
|
4647
5106
|
workflows: {
|
|
4648
|
-
"detect-drift": {
|
|
5107
|
+
"detect-drift": {
|
|
5108
|
+
steps: ["extract", "lint"],
|
|
5109
|
+
description: "Find stale JSDoc and prose drift"
|
|
5110
|
+
},
|
|
4649
5111
|
"full-scan": { steps: ["scan"], description: "Coverage + lint + prose in one pass" },
|
|
4650
|
-
"detect-and-enrich": {
|
|
5112
|
+
"detect-and-enrich": {
|
|
5113
|
+
steps: ["scan", "context"],
|
|
5114
|
+
description: "Scan and generate agent context"
|
|
5115
|
+
},
|
|
4651
5116
|
"ci-pipeline": { steps: ["ci"], description: "Run CI checks on changed packages" },
|
|
4652
|
-
"pre-release": {
|
|
5117
|
+
"pre-release": {
|
|
5118
|
+
steps: ["scan", "breaking", "release"],
|
|
5119
|
+
description: "Full pre-release quality gate"
|
|
5120
|
+
}
|
|
4653
5121
|
}
|
|
4654
5122
|
};
|
|
4655
5123
|
}
|
|
4656
5124
|
|
|
4657
5125
|
// src/drift.ts
|
|
4658
5126
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4659
|
-
var __dirname3 =
|
|
4660
|
-
var packageJson = JSON.parse(readFileSync22(
|
|
5127
|
+
var __dirname3 = path30.dirname(__filename2);
|
|
5128
|
+
var packageJson = JSON.parse(readFileSync22(path30.join(__dirname3, "../package.json"), "utf-8"));
|
|
4661
5129
|
var program = new Command;
|
|
4662
|
-
program.name("drift").description("drift —
|
|
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) => {
|
|
4663
5131
|
const opts = program.opts();
|
|
4664
5132
|
if (opts.cwd) {
|
|
4665
|
-
process.chdir(
|
|
5133
|
+
process.chdir(path30.resolve(opts.cwd));
|
|
4666
5134
|
}
|
|
4667
5135
|
setOutputMode({ json: opts.json, human: opts.human });
|
|
4668
5136
|
setConfigPath(opts.config);
|
|
@@ -4691,6 +5159,7 @@ registerConfigCommand(program);
|
|
|
4691
5159
|
registerContextCommand(program);
|
|
4692
5160
|
registerCacheCommand(program);
|
|
4693
5161
|
registerCommandsCommand(program);
|
|
5162
|
+
registerMcpCommand(program);
|
|
4694
5163
|
var HUMAN_COMMANDS = new Set(["scan", "ci", "init", "commands"]);
|
|
4695
5164
|
for (const cmd of program.commands) {
|
|
4696
5165
|
if (!HUMAN_COMMANDS.has(cmd.name())) {
|