Skip to content

Commit 0845596

Browse files
committed
feat(cli): two-surface architecture with --tools and enriched context
1 parent c4be45c commit 0845596

File tree

7 files changed

+165
-73
lines changed

7 files changed

+165
-73
lines changed

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @doccov/cli
22

3+
## 0.40.0
4+
5+
### Minor Changes
6+
7+
- Two-surface CLI: hide non-human commands from --help, default bare `drift` to scan, replace --capabilities with --tools, enrich context with per-issue detail and undocumented export locations
8+
39
## 0.39.0
410

511
### Minor Changes

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@driftdev/cli",
3-
"version": "0.39.0",
3+
"version": "0.40.0",
44
"description": "Drift CLI - Documentation coverage and drift detection for TypeScript",
55
"keywords": [
66
"typescript",

packages/cli/src/commands/context.ts

Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { execSync } from 'node:child_process';
22
import { readFileSync } from 'node:fs';
33
import * as path from 'node:path';
4-
import { computeDrift } from '@driftdev/sdk';
4+
import type { OpenPkgSpec } from '@openpkg-ts/spec';
5+
import { computeDrift, isFixableDrift } from '@driftdev/sdk';
56
import type { Command } from 'commander';
67
import { cachedExtract } from '../cache/cached-extract';
78
import { loadConfig } from '../config/loader';
89
import { renderContext } from '../formatters/context';
910
import {
1011
type ContextData,
1112
type PackageContext,
13+
type PackageIssue,
14+
type UndocumentedExport,
1215
renderContextMarkdown,
1316
writeContext,
1417
} from '../utils/context-writer';
@@ -26,6 +29,59 @@ function getCommitSha(): string | null {
2629
}
2730
}
2831

32+
function buildPackageContext(name: string, spec: OpenPkgSpec): PackageContext {
33+
const exports = spec.exports ?? [];
34+
let documented = 0;
35+
const undocumented: string[] = [];
36+
const undocumentedExports: UndocumentedExport[] = [];
37+
38+
for (const exp of exports) {
39+
if (exp.description?.trim()) {
40+
documented++;
41+
} else {
42+
undocumented.push(exp.name);
43+
undocumentedExports.push({
44+
name: exp.name,
45+
kind: exp.kind,
46+
filePath: exp.source?.file,
47+
line: exp.source?.line,
48+
});
49+
}
50+
}
51+
52+
const coverage = exports.length > 0 ? Math.round((documented / exports.length) * 100) : 100;
53+
54+
const driftResult = computeDrift(spec);
55+
const issues: PackageIssue[] = [];
56+
let lintIssues = 0;
57+
58+
for (const [exportName, drifts] of driftResult.exports) {
59+
lintIssues += drifts.length;
60+
const exp = exports.find((e) => e.name === exportName);
61+
for (const drift of drifts) {
62+
issues.push({
63+
export: exportName,
64+
type: drift.type,
65+
issue: drift.issue,
66+
filePath: drift.filePath ?? exp?.source?.file,
67+
line: drift.line ?? exp?.source?.line,
68+
fixable: isFixableDrift(drift),
69+
});
70+
}
71+
}
72+
73+
return {
74+
name,
75+
coverage,
76+
lintIssues,
77+
exports: exports.length,
78+
documented,
79+
undocumented,
80+
issues: issues.length > 0 ? issues : undefined,
81+
undocumentedExports: undocumentedExports.length > 0 ? undocumentedExports : undefined,
82+
};
83+
}
84+
2985
export function registerContextCommand(program: Command): void {
3086
program
3187
.command('context [entry]')
@@ -49,7 +105,6 @@ export function registerContextCommand(program: Command): void {
49105
const packages: PackageContext[] = [];
50106

51107
if (options.all || !entry) {
52-
// Multi-package: discover workspace packages
53108
const allPackages = discoverPackages(cwd);
54109
const pkgs =
55110
allPackages && allPackages.length > 0
@@ -62,26 +117,7 @@ export function registerContextCommand(program: Command): void {
62117
for (const pkg of pkgs) {
63118
try {
64119
const { spec } = await cachedExtract(pkg.entry);
65-
const exports = spec.exports ?? [];
66-
let documented = 0;
67-
const undocumented: string[] = [];
68-
for (const exp of exports) {
69-
if (exp.description?.trim()) documented++;
70-
else undocumented.push(exp.name);
71-
}
72-
const coverage =
73-
exports.length > 0 ? Math.round((documented / exports.length) * 100) : 100;
74-
const driftResult = computeDrift(spec);
75-
let lintIssues = 0;
76-
for (const [, drifts] of driftResult.exports) lintIssues += drifts.length;
77-
packages.push({
78-
name: pkg.name,
79-
coverage,
80-
lintIssues,
81-
exports: exports.length,
82-
documented,
83-
undocumented,
84-
});
120+
packages.push(buildPackageContext(pkg.name, spec));
85121
} catch {
86122
packages.push({
87123
name: pkg.name,
@@ -94,66 +130,24 @@ export function registerContextCommand(program: Command): void {
94130
}
95131
}
96132
} else {
97-
// Single package fallback
98133
const entryFile = config.entry ? path.resolve(cwd, config.entry) : detectEntry();
99134
const { spec } = await cachedExtract(entryFile);
100-
const exports = spec.exports ?? [];
101-
let documented = 0;
102-
const undocumented: string[] = [];
103-
for (const exp of exports) {
104-
if (exp.description?.trim()) documented++;
105-
else undocumented.push(exp.name);
106-
}
107-
const coverage =
108-
exports.length > 0 ? Math.round((documented / exports.length) * 100) : 100;
109-
const driftResult = computeDrift(spec);
110-
let lintIssues = 0;
111-
for (const [, drifts] of driftResult.exports) lintIssues += drifts.length;
112-
113135
const pkgJsonPath = path.resolve(cwd, 'package.json');
114136
let name = path.basename(cwd);
115137
try {
116138
name = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')).name ?? name;
117139
} catch {}
118-
packages.push({
119-
name,
120-
coverage,
121-
lintIssues,
122-
exports: exports.length,
123-
documented,
124-
undocumented,
125-
});
140+
packages.push(buildPackageContext(name, spec));
126141
}
127142
} else {
128-
// Single entry
129143
const entryFile = path.resolve(cwd, entry);
130144
const { spec } = await cachedExtract(entryFile);
131-
const exports = spec.exports ?? [];
132-
let documented = 0;
133-
const undocumented: string[] = [];
134-
for (const exp of exports) {
135-
if (exp.description?.trim()) documented++;
136-
else undocumented.push(exp.name);
137-
}
138-
const coverage =
139-
exports.length > 0 ? Math.round((documented / exports.length) * 100) : 100;
140-
const driftResult = computeDrift(spec);
141-
let lintIssues = 0;
142-
for (const [, drifts] of driftResult.exports) lintIssues += drifts.length;
143-
144145
const pkgJsonPath = path.resolve(cwd, 'package.json');
145146
let name = path.basename(cwd);
146147
try {
147148
name = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')).name ?? name;
148149
} catch {}
149-
packages.push({
150-
name,
151-
coverage,
152-
lintIssues,
153-
exports: exports.length,
154-
documented,
155-
undocumented,
156-
});
150+
packages.push(buildPackageContext(name, spec));
157151
}
158152

159153
const contextData: ContextData = { packages, history, config, commit: commit ?? null };

packages/cli/src/drift.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ const program = new Command();
3939

4040
program
4141
.name('drift')
42-
.description('drift — documentation quality primitives for TypeScript')
42+
.description('drift — documentation quality for TypeScript')
4343
.version(packageJson.version)
4444
.option('--json', 'Force JSON output (default when piped)')
4545
.option('--human', 'Force human-readable output (default in terminal)')
4646
.option('--config <path>', 'Path to drift config file')
4747
.option('--cwd <dir>', 'Run as if started in <dir>')
4848
.option('--no-cache', 'Bypass spec cache')
49+
.option('--tools', 'List all available tools for agent use (JSON)')
4950
.hook('preAction', (_thisCommand) => {
5051
const opts = program.opts();
5152
if (opts.cwd) {
@@ -95,20 +96,30 @@ registerContextCommand(program);
9596
// Cache management
9697
registerCacheCommand(program);
9798

98-
if (process.argv.includes('--capabilities')) {
99+
// Hide non-human commands from --help (still functional)
100+
const HUMAN_COMMANDS = new Set(['scan', 'ci', 'init']);
101+
for (const cmd of program.commands) {
102+
if (!HUMAN_COMMANDS.has(cmd.name())) {
103+
(cmd as any)._hidden = true;
104+
}
105+
}
106+
107+
if (process.argv.includes('--tools')) {
99108
const caps = extractCapabilities(program);
100109
process.stdout.write(`${JSON.stringify(caps, null, 2)}\n`);
101110
process.exit(0);
102111
}
103112

104-
// Smart default: bare `drift` runs init if no config, health otherwise
113+
// Smart default: bare `drift` runs init if no config, scan otherwise
105114
// Skip if user passed --help/-h/--version/-V (let commander handle those)
106115
const rawArgs = process.argv.slice(2);
107-
const hasHelpOrVersion = rawArgs.some((a) => ['-h', '--help', '-V', '--version'].includes(a));
116+
const hasHelpOrVersion = rawArgs.some((a) =>
117+
['-h', '--help', '-V', '--version', '--tools'].includes(a),
118+
);
108119
const userArgs = rawArgs.filter((a) => !a.startsWith('-'));
109120
if (userArgs.length === 0 && !hasHelpOrVersion) {
110121
const { configPath } = loadConfig();
111-
const subcommand = configPath ? 'health' : 'init';
122+
const subcommand = configPath ? 'scan' : 'init';
112123
process.argv.splice(2, 0, subcommand);
113124
}
114125

packages/cli/src/utils/capabilities.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface CommandInfo {
1111
description: string;
1212
flags: FlagInfo[];
1313
positional?: string;
14+
examples?: string[];
1415
}
1516

1617
interface EntityInfo {
@@ -21,6 +22,8 @@ interface EntityInfo {
2122

2223
export interface Capabilities {
2324
version: string;
25+
hint: string;
26+
humanCommands: string[];
2427
commands: CommandInfo[];
2528
globalFlags: FlagInfo[];
2629
entities: EntityInfo[];
@@ -40,6 +43,30 @@ function extractFlags(cmd: Command): FlagInfo[] {
4043
}));
4144
}
4245

46+
const COMMAND_EXAMPLES: Record<string, string[]> = {
47+
scan: ['drift scan --json', 'drift scan --all --json', 'drift scan --ci --json'],
48+
lint: ['drift lint --json', 'drift lint --all --json'],
49+
coverage: ['drift coverage --json', 'drift coverage --min 80 --json'],
50+
extract: ['drift extract --json'],
51+
list: ['drift list --json'],
52+
get: ['drift get createClient --json'],
53+
diff: ['drift diff --base main --json'],
54+
breaking: ['drift breaking --base main --json'],
55+
semver: ['drift semver --base main --json'],
56+
changelog: ['drift changelog --base main --json'],
57+
ci: ['drift ci --json', 'drift ci --all --json'],
58+
release: ['drift release --json'],
59+
context: ['drift context --json', 'drift context --all --json'],
60+
examples: ['drift examples --typecheck --json'],
61+
health: ['drift health --json'],
62+
config: ['drift config list --json', 'drift config get coverage.min --json'],
63+
init: ['drift init --json'],
64+
validate: ['drift validate spec.json --json'],
65+
filter: ['drift filter spec.json --kind function --json'],
66+
report: ['drift report --json'],
67+
cache: ['drift cache status', 'drift cache clear'],
68+
};
69+
4370
export function extractCapabilities(program: Command): Capabilities {
4471
const commands: CommandInfo[] = [];
4572

@@ -52,11 +79,14 @@ export function extractCapabilities(program: Command): Capabilities {
5279
...(positionalArgs.length > 0
5380
? { positional: positionalArgs.map((a) => a.name()).join(' ') }
5481
: {}),
82+
...(COMMAND_EXAMPLES[cmd.name()] ? { examples: COMMAND_EXAMPLES[cmd.name()] } : {}),
5583
});
5684
}
5785

5886
return {
5987
version: program.version() ?? '0.0.0',
88+
hint: "Run 'drift' for human output. Use these primitives with --json for agent workflows.",
89+
humanCommands: ['scan', 'ci', 'init'],
6090
commands,
6191
globalFlags: extractFlags(program),
6292
entities: [

packages/cli/src/utils/context-writer.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,31 @@ import type { DriftConfig } from '../config/drift-config';
44
import { getProjectDir } from '../config/global';
55
import type { HistoryEntry } from './history';
66

7+
export interface PackageIssue {
8+
export: string;
9+
type: string;
10+
issue: string;
11+
filePath?: string;
12+
line?: number;
13+
fixable: boolean;
14+
}
15+
16+
export interface UndocumentedExport {
17+
name: string;
18+
kind: string;
19+
filePath?: string;
20+
line?: number;
21+
}
22+
723
export interface PackageContext {
824
name: string;
925
coverage: number;
1026
lintIssues: number;
1127
exports: number;
1228
documented?: number;
1329
undocumented?: string[];
30+
issues?: PackageIssue[];
31+
undocumentedExports?: UndocumentedExport[];
1432
}
1533

1634
export interface ContextData {
@@ -48,6 +66,39 @@ export function renderContextMarkdown(data: ContextData): string {
4866
lines.push(`**Average coverage**: ${avgCoverage}% `);
4967
lines.push(`**Total lint issues**: ${totalIssues}`);
5068
lines.push('');
69+
70+
// Per-package issue details
71+
for (const pkg of data.packages) {
72+
if (pkg.issues && pkg.issues.length > 0) {
73+
lines.push(`### ${pkg.name} — Issues`);
74+
lines.push('');
75+
lines.push('| Export | Type | Location | Fixable |');
76+
lines.push('|--------|------|----------|---------|');
77+
for (const issue of pkg.issues) {
78+
const loc = issue.filePath
79+
? `${issue.filePath}${issue.line ? `:${issue.line}` : ''}`
80+
: '—';
81+
lines.push(
82+
`| ${issue.export} | ${issue.type} | ${loc} | ${issue.fixable ? 'yes' : 'no'} |`,
83+
);
84+
}
85+
lines.push('');
86+
}
87+
88+
if (pkg.undocumentedExports && pkg.undocumentedExports.length > 0) {
89+
lines.push(`### ${pkg.name} — Undocumented Exports`);
90+
lines.push('');
91+
lines.push('| Export | Kind | Location |');
92+
lines.push('|--------|------|----------|');
93+
for (const exp of pkg.undocumentedExports) {
94+
const loc = exp.filePath
95+
? `${exp.filePath}${exp.line ? `:${exp.line}` : ''}`
96+
: '—';
97+
lines.push(`| ${exp.name} | ${exp.kind} | ${loc} |`);
98+
}
99+
lines.push('');
100+
}
101+
}
51102
}
52103

53104
// Recent Activity

0 commit comments

Comments
 (0)