-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexplain.ts
More file actions
166 lines (140 loc) · 5.68 KB
/
Copy pathexplain.ts
File metadata and controls
166 lines (140 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import type { Command } from 'commander';
import pc from 'picocolors';
import { readConfig, readRules } from '../core/parser.js';
import type { Bridge, Rule } from '../bridges/types.js';
import { isMarkerBridge, isDirectoryBridge, getBridgeOutputPaths } from '../bridges/types.js';
import { claudeBridge } from '../bridges/claude.js';
import { cursorBridge } from '../bridges/cursor.js';
import { geminiBridge } from '../bridges/gemini.js';
import { windsurfBridge } from '../bridges/windsurf.js';
import { copilotBridge } from '../bridges/copilot.js';
import { filterRules, groupByScope } from '../core/helpers.js';
import { resolveContext } from '../core/resolve-context.js';
import * as ui from '../utils/ui.js';
import { ICONS } from '../utils/ui.js';
const WINDSURF_CHAR_LIMIT = 6000;
export interface ExplainOptions {
tool?: string;
}
const BRIDGES: Bridge[] = [claudeBridge, cursorBridge, geminiBridge, windsurfBridge, copilotBridge];
function getBridge(id: string): Bridge | undefined {
return BRIDGES.find((b) => b.id === id);
}
function getModeLabel(bridge: Bridge): string {
if (isMarkerBridge(bridge)) {
return 'markers (BEGIN/END)';
}
return 'multi-file (one per scope)';
}
function getExcludedRules(rules: Rule[]): Array<{ id: string; reason: string }> {
const excluded: Array<{ id: string; reason: string }> = [];
for (const rule of rules) {
if (rule.severity === 'info') {
excluded.push({ id: rule.id, reason: `severity: info ${ICONS.arrow} excluded from output` });
} else if (!rule.enabled) {
excluded.push({ id: rule.id, reason: 'enabled: false' });
}
}
return excluded;
}
function formatSeparator(toolId: string): string {
const label = ` ${toolId} `;
const lineWidth = 40;
const prefix = `${ICONS.separator}${ICONS.separator}`;
const remaining = lineWidth - prefix.length - label.length;
const suffix = ICONS.separator.repeat(Math.max(0, remaining));
return pc.dim(`${prefix}${label}${suffix}`);
}
export async function runExplain(options: ExplainOptions): Promise<void> {
const resolved = await resolveContext(process.cwd());
if (!resolved) {
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
process.exitCode = 1;
return;
}
const cwd = resolved.configRoot;
const config = await readConfig(cwd);
const rules = await readRules(cwd);
let toolIds = config.tools;
if (options.tool) {
if (!config.tools.includes(options.tool)) {
ui.error(`Tool "${options.tool}" is not configured in .dwf/config.yml`, `Configured tools: ${config.tools.join(', ')}`);
process.exitCode = 1;
return;
}
toolIds = [options.tool];
}
for (const toolId of toolIds) {
const bridge = getBridge(toolId);
if (!bridge) {
continue;
}
console.log(` ${formatSeparator(toolId)}`);
ui.newline();
if (isDirectoryBridge(bridge)) {
// DirectoryBridge: show output directory and file listing
const outputPattern = `${bridge.outputDir}/${bridge.filePrefix}*${bridge.fileExtension}`;
ui.keyValue('Output:', outputPattern);
ui.keyValue('Mode:', getModeLabel(bridge));
const included = filterRules(rules);
const grouped = groupByScope(included);
ui.keyValue('Rules:', `${String(included.length)} included`);
// Show files that would be generated (one per scope)
ui.newline();
ui.keyValue('Files:', `${String(grouped.size)} scope${grouped.size !== 1 ? 's' : ''}`);
const outputs = bridge.compile(rules, config);
for (const [filePath] of outputs) {
console.log(` ${' '.repeat(10)}${filePath}`);
}
// Show scope breakdown
for (const [scope, scopeRules] of grouped) {
console.log(` ${' '.repeat(10)} ${scope}: ${String(scopeRules.length)} rule${scopeRules.length !== 1 ? 's' : ''}`);
}
} else {
// MarkerBridge: show single output file
const bridgePaths = getBridgeOutputPaths(bridge);
const outputPath = bridgePaths[0] ?? toolId;
ui.keyValue('Output:', outputPath);
ui.keyValue('Mode:', getModeLabel(bridge));
const included = filterRules(rules);
const grouped = groupByScope(included);
ui.keyValue('Rules:', `${String(included.length)} included`);
for (const [scope, scopeRules] of grouped) {
console.log(` ${' '.repeat(10)}${scope}: ${String(scopeRules.length)}`);
}
}
const excluded = getExcludedRules(rules);
ui.newline();
ui.keyValue('Excluded:', String(excluded.length));
if (excluded.length > 0) {
for (const entry of excluded) {
const label = rules.find((r) => r.id === entry.id)?.severity === 'info' ? 'info' : 'disabled';
console.log(` ${' '.repeat(10)}[${label}] ${entry.id}`);
}
}
if (bridge.id === 'windsurf') {
const outputs = bridge.compile(rules, config);
let maxPerFile = 0;
for (const [, val] of outputs) {
if (val.length > maxPerFile) {
maxPerFile = val.length;
}
}
const formatted = `${String(maxPerFile).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} / ${String(WINDSURF_CHAR_LIMIT).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} chars (per file)`;
ui.newline();
if (maxPerFile > WINDSURF_CHAR_LIMIT) {
ui.warn(`Max file size: ${formatted} (Windsurf limit)`);
} else {
ui.keyValue('Size:', `${formatted} (Windsurf limit)`);
}
}
ui.newline();
}
}
export function registerExplainCommand(program: Command): void {
program
.command('explain')
.description('Show what each configured editor will receive and why')
.option('--tool <tool>', 'Explain only a specific tool')
.action((options: ExplainOptions) => runExplain(options));
}