Skip to content

Commit af8fc2f

Browse files
authored
Merge pull request #59 from gpolanco/fix/v2-simplify
fix(core): remove canonical layer and simplify CLI output
2 parents 1fa7cb9 + 53f79c8 commit af8fc2f

11 files changed

Lines changed: 103 additions & 478 deletions

File tree

packages/cli/src/commands/compile.ts

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { windsurfBridge } from '../bridges/windsurf.js';
1616
import { copilotBridge } from '../bridges/copilot.js';
1717
import { mergeMarkedContent, removeMarkedBlock } from '../core/markers.js';
1818
import { cleanStaleFiles } from '../core/scope-filename.js';
19-
import { detectLegacyFiles, migrateLegacyFiles } from '../core/cleanup.js';
20-
import { buildCanonicalOutputs, writeCanonical } from '../core/canonical.js';
19+
import { detectLegacyFiles, migrateLegacyFiles, detectCanonicalDir, removeCanonicalDir } from '../core/cleanup.js';
2120
import { fileExists } from '../utils/fs.js';
2221
import { resolveContext } from '../core/resolve-context.js';
2322
import * as ui from '../utils/ui.js';
@@ -53,8 +52,6 @@ export interface CompileResult {
5352
globalRuleCount: number;
5453
projectRuleCount: number;
5554
overriddenRuleIds: string[];
56-
canonicalFileCount: number;
57-
canonicalError?: string;
5855
assetPaths: string[];
5956
elapsedMs: number;
6057
staleResults: StaleFileResult[];
@@ -168,6 +165,15 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
168165
const actions = await migrateLegacyFiles(context.outputRoot, legacyFiles);
169166
migration.actions = actions;
170167
}
168+
169+
// Clean up old canonical directory if it exists
170+
const canonicalDir = await detectCanonicalDir(context.outputRoot);
171+
if (canonicalDir) {
172+
const removed = await removeCanonicalDir(context.outputRoot);
173+
if (removed) {
174+
migration.actions.push('Removed legacy .agents/rules/devw/ canonical directory');
175+
}
176+
}
171177
}
172178

173179
const activeRules = rules.filter((r) => r.enabled);
@@ -298,35 +304,6 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
298304
}
299305
}
300306

301-
// Canonical output intentionally always runs, even when --tool filters bridges.
302-
// This keeps `.agents/rules/devw` as the source-of-truth for doctor checks and distribution.
303-
const canonicalOutputs = buildCanonicalOutputs(rules);
304-
let canonicalPaths: string[] = [];
305-
let canonicalError: string | undefined;
306-
if (write) {
307-
try {
308-
canonicalPaths = await writeCanonical(context.outputRoot, canonicalOutputs);
309-
for (const relativePath of canonicalPaths) {
310-
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: true });
311-
}
312-
} catch (err) {
313-
canonicalError = err instanceof Error ? err.message : String(err);
314-
const errorPaths = [...canonicalOutputs.keys()];
315-
if (errorPaths.length > 0) {
316-
for (const relativePath of errorPaths) {
317-
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: false, error: canonicalError });
318-
}
319-
} else {
320-
results.push({ bridgeId: 'canonical', outputPath: '.agents/rules/devw', success: false, error: canonicalError });
321-
}
322-
}
323-
} else {
324-
for (const [relativePath, content] of canonicalOutputs) {
325-
canonicalPaths.push(relativePath);
326-
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: true, content });
327-
}
328-
}
329-
330307
let assetPaths: string[] = [];
331308
if (write) {
332309
const hash = computeRulesHash(activeRules);
@@ -343,8 +320,6 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
343320
globalRuleCount: globalRules.length,
344321
projectRuleCount: projectRules.length,
345322
overriddenRuleIds,
346-
canonicalFileCount: canonicalPaths.length,
347-
canonicalError,
348323
assetPaths,
349324
elapsedMs,
350325
staleResults,
@@ -403,22 +378,13 @@ export async function runCompile(options: CompileOptions): Promise<void> {
403378
const fileCount = result.results.filter((r) => r.success).length;
404379
ui.newline();
405380
ui.info(
406-
`Would generate ${String(fileCount)} file${fileCount !== 1 ? 's' : ''} (${String(result.canonicalFileCount)} canonical) from ${String(result.activeRuleCount)} rules`,
381+
`Would generate ${String(fileCount)} file${fileCount !== 1 ? 's' : ''} from ${String(result.activeRuleCount)} rules`,
407382
);
408383
return;
409384
}
410385

411386
const result = await executePipeline({ cwd, tool: options.tool });
412387

413-
if (options.tool) {
414-
ui.info('Note: canonical output is always refreshed in .agents/rules/devw');
415-
}
416-
417-
if (result.canonicalError) {
418-
ui.warn(`Canonical write failed: ${result.canonicalError}`);
419-
ui.warn('Tool-specific outputs were still written');
420-
}
421-
422388
const summaryTable = renderTable(
423389
['bridge', 'generated', 'failed'],
424390
toCompileSummaryRows(result),
@@ -438,8 +404,11 @@ export async function runCompile(options: CompileOptions): Promise<void> {
438404
const allPaths = [...writtenPaths, ...result.assetPaths];
439405

440406
ui.newline();
407+
if (result.activeRuleCount === 0) {
408+
ui.warn('No rules found. Run "devw add" to install rules from the registry.');
409+
return;
410+
}
441411
ui.success(`Compiled ${String(result.activeRuleCount)} rules ${ICONS.arrow} ${String(allPaths.length)} file${allPaths.length !== 1 ? 's' : ''} ${ui.timing(result.elapsedMs)}`);
442-
ui.info(`Canonical files: ${String(result.canonicalFileCount)}`);
443412
ui.log(summaryTable);
444413
if (options.verbose && result.overriddenRuleIds.length > 0) {
445414
ui.info(`Project overrides (${String(result.overriddenRuleIds.length)}): ${result.overriddenRuleIds.join(', ')}`);

packages/cli/src/commands/doctor.ts

Lines changed: 2 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { lstat, readFile, readdir } from 'node:fs/promises';
2-
import { basename, join, relative } from 'node:path';
2+
import { join, relative } from 'node:path';
33
import type { Command } from 'commander';
44
import { parse } from 'yaml';
55
import { readConfig, readRules } from '../core/parser.js';
@@ -14,7 +14,6 @@ import { getBridgeOutputPaths, isDirectoryBridge } from '../bridges/types.js';
1414
import { fileExists } from '../utils/fs.js';
1515
import { resolveContext } from '../core/resolve-context.js';
1616
import { isValidScope } from '../core/schema.js';
17-
import { buildCanonicalOutputs } from '../core/canonical.js';
1817
import { detectLegacyFiles } from '../core/cleanup.js';
1918
import * as ui from '../utils/ui.js';
2019

@@ -268,12 +267,6 @@ export async function checkHashSync(cwd: string, rules: Rule[]): Promise<CheckRe
268267
};
269268
}
270269

271-
function normalizeComparableContent(content: string): string {
272-
const frontmatterPattern = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
273-
const withoutFrontmatter = content.replace(frontmatterPattern, '');
274-
return withoutFrontmatter.replaceAll('\r\n', '\n').trimEnd();
275-
}
276-
277270
function extractFrontmatter(content: string): string | null {
278271
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
279272
if (!match) {
@@ -282,112 +275,6 @@ function extractFrontmatter(content: string): string | null {
282275
return match[1] ?? null;
283276
}
284277

285-
export async function checkCanonicalExists(cwd: string): Promise<CheckResult> {
286-
const canonicalDir = join(cwd, '.agents', 'rules', 'devw');
287-
288-
let entries: string[];
289-
try {
290-
entries = await readdir(canonicalDir);
291-
} catch {
292-
return {
293-
passed: false,
294-
message: '.agents/rules/devw not found — run "devw compile"',
295-
};
296-
}
297-
298-
const canonicalFiles = entries.filter((entry) => entry.startsWith('dwf-') && entry.endsWith('.md'));
299-
if (canonicalFiles.length === 0) {
300-
return {
301-
passed: false,
302-
message: '.agents/rules/devw has no canonical files — run "devw compile"',
303-
};
304-
}
305-
306-
return {
307-
passed: true,
308-
message: `Canonical files exist (${String(canonicalFiles.length)} file${canonicalFiles.length === 1 ? '' : 's'})`,
309-
};
310-
}
311-
312-
export async function checkCanonicalSync(cwd: string, rules: Rule[], config: ProjectConfig): Promise<CheckResult> {
313-
const directoryBridges = getConfiguredDirectoryBridges(config);
314-
315-
if (directoryBridges.length === 0) {
316-
return {
317-
passed: true,
318-
message: 'Canonical sync skipped (no directory tools configured)',
319-
skipped: true,
320-
};
321-
}
322-
323-
const canonicalOutputs = buildCanonicalOutputs(rules);
324-
if (canonicalOutputs.size === 0) {
325-
return {
326-
passed: true,
327-
message: 'Canonical sync skipped (no active scope outputs)',
328-
skipped: true,
329-
};
330-
}
331-
332-
const mismatches: string[] = [];
333-
let compared = 0;
334-
335-
for (const bridge of directoryBridges) {
336-
const expectedNativeFiles = new Set<string>();
337-
338-
for (const [canonicalPath, canonicalContent] of canonicalOutputs) {
339-
const canonicalFilename = basename(canonicalPath);
340-
const scopeName = canonicalFilename.slice('dwf-'.length, canonicalFilename.length - '.md'.length);
341-
const nativeFilename = `${bridge.filePrefix}${scopeName}${bridge.fileExtension}`;
342-
expectedNativeFiles.add(nativeFilename);
343-
344-
const nativePath = join(cwd, bridge.outputDir, nativeFilename);
345-
if (!(await fileExists(nativePath))) {
346-
mismatches.push(`${bridge.id}: missing ${nativeFilename}`);
347-
continue;
348-
}
349-
350-
const nativeRaw = await readFile(nativePath, 'utf-8');
351-
const normalizedNative = normalizeComparableContent(nativeRaw);
352-
const normalizedCanonical = normalizeComparableContent(canonicalContent);
353-
354-
compared += 1;
355-
if (normalizedNative !== normalizedCanonical) {
356-
mismatches.push(`${bridge.id}: modified ${nativeFilename}`);
357-
}
358-
}
359-
360-
const bridgeDir = join(cwd, bridge.outputDir);
361-
let entries: string[] = [];
362-
try {
363-
entries = await readdir(bridgeDir);
364-
} catch {
365-
entries = [];
366-
}
367-
368-
for (const entry of entries) {
369-
if (!entry.startsWith(bridge.filePrefix) || !entry.endsWith(bridge.fileExtension)) {
370-
continue;
371-
}
372-
if (!expectedNativeFiles.has(entry)) {
373-
mismatches.push(`${bridge.id}: unexpected ${entry}`);
374-
}
375-
}
376-
}
377-
378-
if (mismatches.length > 0) {
379-
return {
380-
passed: false,
381-
message: `Canonical/native mismatch: ${mismatches.join(', ')}`,
382-
};
383-
}
384-
385-
return {
386-
passed: true,
387-
message: `Canonical and native files are in sync (${String(compared)} files compared)`,
388-
};
389-
}
390-
391278
export async function checkLegacyMigration(cwd: string): Promise<CheckResult> {
392279
const legacyFiles = await detectLegacyFiles(cwd);
393280
if (legacyFiles.length === 0) {
@@ -570,17 +457,7 @@ export async function runDoctor(): Promise<void> {
570457
const hashResult = await checkHashSync(effectiveCwd, rules);
571458
results.push(hashResult);
572459

573-
// Check 11: Canonical output exists (skip if no rules)
574-
if (rules.length > 0) {
575-
const canonicalExistsResult = await checkCanonicalExists(effectiveCwd);
576-
results.push(canonicalExistsResult);
577-
578-
// Check 12: Canonical and native outputs are synchronized
579-
const canonicalSyncResult = await checkCanonicalSync(effectiveCwd, rules, config);
580-
results.push(canonicalSyncResult);
581-
}
582-
583-
// Check 13: Legacy migration has no pending files
460+
// Check 11: Legacy migration has no pending files
584461
const legacyResult = await checkLegacyMigration(effectiveCwd);
585462
results.push(legacyResult);
586463

packages/cli/src/commands/init.ts

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { join, basename } from 'node:path';
33
import { homedir } from 'node:os';
44
import type { Command } from 'commander';
55
import { stringify } from 'yaml';
6-
import pc from 'picocolors';
76
import { detectTools, SUPPORTED_TOOLS } from '../utils/detect-tools.js';
87
import * as ui from '../utils/ui.js';
98
import type { ToolId } from '../utils/detect-tools.js';
109
import { fileExists } from '../utils/fs.js';
1110
import {
1211
selectPrompt,
1312
multiselectPrompt,
13+
confirmPrompt,
1414
introPrompt,
15-
notePrompt,
1615
outroPrompt,
1716
spinnerTask,
1817
isInteractiveSession,
@@ -165,11 +164,17 @@ export async function runInit(options: InitOptions): Promise<void> {
165164

166165
if (await fileExists(dwfDir)) {
167166
const locationHint = scope === 'global'
168-
? '~/.dwf/ already exists in your home directory'
169-
: '.dwf/ already exists in this directory';
170-
ui.error(locationHint, 'Remove it first or run from a different directory');
171-
process.exitCode = 1;
172-
return;
167+
? '~/.dwf/ already exists.'
168+
: '.dwf/ already exists in this directory.';
169+
ui.warn(locationHint);
170+
const overwrite = await confirmPrompt({
171+
message: 'Overwrite config? (rules will be preserved)',
172+
defaultValue: false,
173+
});
174+
if (!overwrite) {
175+
outroPrompt('Init cancelled.');
176+
return;
177+
}
173178
}
174179

175180
const projectName = scope === 'global' ? 'global' : basename(cwd);
@@ -210,41 +215,15 @@ export async function runInit(options: InitOptions): Promise<void> {
210215
},
211216
});
212217

213-
// Ensure canonical global output dir exists for global mode.
214-
if (scope === 'global') {
215-
await spinnerTask({
216-
label: 'Preparing canonical global output',
217-
task: async () => {
218-
await mkdir(join(rootDir, '.agents', 'rules', 'devw'), { recursive: true });
219-
},
220-
});
221-
} else {
218+
if (scope !== 'global') {
222219
await appendToGitignore(cwd);
223220
}
224221

225222
// Success summary
223+
const dwfPath = scope === 'global' ? '~/.dwf/' : '.dwf/';
226224
ui.newline();
227-
ui.header('dev-workflows');
228-
ui.newline();
229-
ui.success(`Initialized ${scope === 'global' ? '~/.dwf/' : '.dwf/'} successfully`);
230-
ui.newline();
231-
ui.keyValue('Project:', pc.bold(projectName));
232-
ui.keyValue('Scope:', scope);
233-
ui.keyValue('Tools:', pc.cyan(tools.join(', ')));
234-
ui.keyValue('Mode:', mode);
235-
ui.newline();
236-
ui.header("What's next");
237-
ui.newline();
238-
console.log(` 1. Browse available rules ${pc.cyan('devw add --list')}`);
239-
console.log(` 2. Add a rule ${pc.cyan('devw add <category>/<rule>')}`);
240-
console.log(` 3. Or write your own rules in ${pc.cyan(scope === 'global' ? '~/.dwf/rules/' : '.dwf/rules/')}`);
241-
console.log(` 4. When ready, compile ${pc.cyan('devw compile')}`);
242-
243-
notePrompt(
244-
`Project: ${projectName}\nScope: ${scope}\nTools: ${tools.join(', ')}\nMode: ${mode}`,
245-
'Initialized',
246-
);
247-
outroPrompt(`Ready: ${scope === 'global' ? '~/.dwf/' : '.dwf/'}`);
225+
ui.success(`Initialized ${dwfPath}${tools.join(', ')} (${mode} mode)`);
226+
outroPrompt('Run "devw add" to browse and install rules.');
248227

249228
if (options.preset) {
250229
ui.newline();

0 commit comments

Comments
 (0)