Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { convert } from '../core/converter.js';
import { isAssetType, parseAssetFrontmatter } from '../core/assets.js';
import { fileExists } from '../utils/fs.js';
import { readConfig } from '../core/parser.js';
import { resolveContext } from '../core/resolve-context.js';
import {
selectPrompt,
multiselectPrompt,
Expand Down Expand Up @@ -915,14 +916,20 @@ export async function runAdd(ruleArg: string | undefined, options: AddOptions):
return;
}

const cwd = process.cwd();
const resolved = await resolveContext(process.cwd());

if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
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;

if (resolved.globalMode) {
ui.info('Adding to global config (~/.dwf)');
}

if (!ruleArg) {
if (!isInteractiveSession()) {
ui.error('No rule specified', 'Usage: devw add <category>/<rule>');
Expand Down
35 changes: 14 additions & 21 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdir, writeFile, readFile, symlink, unlink } from 'node:fs/promises';
import { join, dirname, basename } from 'node:path';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';
import type { Command } from 'commander';
import pc from 'picocolors';
Expand All @@ -19,6 +19,7 @@ import { cleanStaleFiles } from '../core/scope-filename.js';
import { detectLegacyFiles, migrateLegacyFiles } from '../core/cleanup.js';
import { buildCanonicalOutputs, writeCanonical } from '../core/canonical.js';
import { fileExists } from '../utils/fs.js';
import { resolveContext } from '../core/resolve-context.js';
import * as ui from '../utils/ui.js';
import { ICONS } from '../utils/ui.js';
import { renderTable } from '../utils/table.js';
Expand Down Expand Up @@ -95,6 +96,7 @@ interface CompileContext {
configRoot: string;
outputRoot: string;
globalMode: boolean;
dwfDir: string;
}

function toCompileSummaryRows(result: CompileResult): string[][] {
Expand Down Expand Up @@ -124,34 +126,25 @@ function toCompileSummaryRows(result: CompileResult): string[][] {
}

async function resolveCompileContext(cwd: string): Promise<CompileContext> {
const projectConfigPath = join(cwd, '.dwf', 'config.yml');
if (await fileExists(projectConfigPath)) {
return {
configRoot: cwd,
outputRoot: cwd,
globalMode: false,
};
const resolved = await resolveContext(cwd);
if (!resolved) {
throw new Error('No devw configuration found.\nRun "devw init" to set up a project or global configuration.');
}

const inGlobalConfigDir = basename(cwd) === '.dwf';
const globalConfigPath = join(cwd, 'config.yml');
if (inGlobalConfigDir && await fileExists(globalConfigPath)) {
return {
configRoot: cwd,
outputRoot: homedir(),
globalMode: true,
};
}

throw new Error('.dwf/config.yml not found. Run devw init to initialize the project');
return {
configRoot: resolved.configRoot,
outputRoot: resolved.outputRoot,
globalMode: resolved.globalMode,
dwfDir: resolved.dwfDir,
};
}

export async function executePipeline(options: PipelineOptions): Promise<CompileResult> {
const { cwd, tool, write = true } = options;
const startTime = performance.now();
const context = await resolveCompileContext(cwd);

const config = context.globalMode ? await readConfigFromDwfDir(context.configRoot) : await readConfig(context.configRoot);
const config = context.globalMode ? await readConfigFromDwfDir(context.dwfDir) : await readConfig(context.configRoot);
const projectRules = await readRules(context.configRoot);
const globalRules = context.globalMode || config.global === false
? []
Expand Down Expand Up @@ -366,7 +359,7 @@ export async function runCompile(options: CompileOptions): Promise<void> {
const context = await resolveCompileContext(cwd);

if (options.verbose) {
const config = context.globalMode ? await readConfigFromDwfDir(context.configRoot) : await readConfig(context.configRoot);
const config = context.globalMode ? await readConfigFromDwfDir(context.dwfDir) : await readConfig(context.configRoot);
const projectRules = await readRules(context.configRoot);
const globalRules = context.globalMode || config.global === false
? []
Expand Down
55 changes: 39 additions & 16 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { copilotBridge } from '../bridges/copilot.js';
import type { Bridge, DirectoryBridge, ProjectConfig, PulledEntry, AssetEntry, Rule } from '../bridges/types.js';
import { getBridgeOutputPaths, isDirectoryBridge } from '../bridges/types.js';
import { fileExists } from '../utils/fs.js';
import { resolveContext } from '../core/resolve-context.js';
import { isValidScope } from '../core/schema.js';
import { buildCanonicalOutputs } from '../core/canonical.js';
import { detectLegacyFiles } from '../core/cleanup.js';
Expand Down Expand Up @@ -479,44 +480,64 @@ export async function runDoctor(): Promise<void> {
const results: CheckResult[] = [];
let hasFailed = false;

// Resolve context: local project or global ~/.dwf
const resolved = await resolveContext(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 effectiveCwd = resolved.configRoot;

if (resolved.globalMode) {
ui.info('Running in global mode (~/.dwf)');
ui.newline();
}

// Check 1: .dwf/config.yml exists
const configExistsResult = await checkConfigExists(cwd);
const configExistsResult = await checkConfigExists(effectiveCwd);
results.push(configExistsResult);

if (!configExistsResult.passed) {
for (const r of results) {
ui.check(r.passed, r.message, r.skipped);
if (!r.passed) hasFailed = true;
if (!r.passed) {
hasFailed = true;
}
}
printSummary(results, startTime);
process.exitCode = 1;
return;
}

// Check 2: config.yml is valid
const configValidResult = await checkConfigValid(cwd);
const configValidResult = await checkConfigValid(effectiveCwd);
results.push(configValidResult);

// Check 3: Rule files are valid YAML
const rulesValidResult = await checkRulesValid(cwd);
const rulesValidResult = await checkRulesValid(effectiveCwd);
results.push(rulesValidResult);

if (!configValidResult.passed) {
for (const r of results) {
ui.check(r.passed, r.message, r.skipped);
if (!r.passed) hasFailed = true;
if (!r.passed) {
hasFailed = true;
}
}
printSummary(results, startTime);
process.exitCode = 1;
return;
}

const config = await readConfig(cwd);
const config = await readConfig(effectiveCwd);

// Load rules for remaining checks
let rules: Rule[] = [];
try {
rules = await readRules(cwd);
rules = await readRules(effectiveCwd);
} catch {
// readRules may fail if rules dir is missing; that's ok
}
Expand All @@ -534,43 +555,45 @@ export async function runDoctor(): Promise<void> {
results.push(bridgeResult);

// Check 7: Symlinks valid (conditional on mode)
const symlinkResult = await checkSymlinks(cwd, config);
const symlinkResult = await checkSymlinks(effectiveCwd, config);
results.push(symlinkResult);

// Check 8: Pulled files exist
const pulledResult = await checkPulledFilesExist(cwd, config.pulled);
const pulledResult = await checkPulledFilesExist(effectiveCwd, config.pulled);
results.push(pulledResult);

// Check 9: Asset files exist
const assetResult = await checkAssetFilesExist(cwd, config.assets);
const assetResult = await checkAssetFilesExist(effectiveCwd, config.assets);
results.push(assetResult);

// Check 10: Hash sync (conditional on compiled files existing)
const hashResult = await checkHashSync(cwd, rules);
const hashResult = await checkHashSync(effectiveCwd, rules);
results.push(hashResult);

// Check 11: Canonical output exists (skip if no rules)
if (rules.length > 0) {
const canonicalExistsResult = await checkCanonicalExists(cwd);
const canonicalExistsResult = await checkCanonicalExists(effectiveCwd);
results.push(canonicalExistsResult);

// Check 12: Canonical and native outputs are synchronized
const canonicalSyncResult = await checkCanonicalSync(cwd, rules, config);
const canonicalSyncResult = await checkCanonicalSync(effectiveCwd, rules, config);
results.push(canonicalSyncResult);
}

// Check 13: Legacy migration has no pending files
const legacyResult = await checkLegacyMigration(cwd);
const legacyResult = await checkLegacyMigration(effectiveCwd);
results.push(legacyResult);

// Check 14: Native files have valid frontmatter for their editor
const frontmatterResult = await checkNativeFrontmatter(cwd, config);
const frontmatterResult = await checkNativeFrontmatter(effectiveCwd, config);
results.push(frontmatterResult);

// Output
for (const r of results) {
ui.check(r.passed, r.message, r.skipped);
if (!r.passed) hasFailed = true;
if (!r.passed) {
hasFailed = true;
}
}

printSummary(results, startTime);
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/commands/explain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { join } from 'node:path';
import type { Command } from 'commander';
import pc from 'picocolors';
import { readConfig, readRules } from '../core/parser.js';
Expand All @@ -10,7 +9,7 @@ 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 { fileExists } from '../utils/fs.js';
import { resolveContext } from '../core/resolve-context.js';
import * as ui from '../utils/ui.js';
import { ICONS } from '../utils/ui.js';

Expand Down Expand Up @@ -57,14 +56,15 @@ function formatSeparator(toolId: string): string {
}

async function runExplain(options: ExplainOptions): Promise<void> {
const cwd = process.cwd();
const resolved = await resolveContext(process.cwd());

if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
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);

Expand Down
32 changes: 19 additions & 13 deletions packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { join } from 'node:path';
import type { Command } from 'commander';
import pc from 'picocolors';
import { readConfig, readRules } from '../core/parser.js';
import { fileExists } from '../utils/fs.js';
import { resolveContext } from '../core/resolve-context.js';
import { claudeBridge } from '../bridges/claude.js';
import { cursorBridge } from '../bridges/cursor.js';
import { geminiBridge } from '../bridges/gemini.js';
Expand All @@ -16,18 +15,21 @@ import { ICONS } from '../utils/ui.js';

const BRIDGES: Bridge[] = [claudeBridge, cursorBridge, geminiBridge, windsurfBridge, copilotBridge];

async function ensureConfig(cwd: string): Promise<boolean> {
if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
async function ensureConfig(): Promise<string | null> {
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 false;
return null;
}
return true;
return resolved.configRoot;
}

async function listRules(): Promise<void> {
const cwd = process.cwd();
if (!(await ensureConfig(cwd))) return;
const cwd = await ensureConfig();
if (!cwd) {
return;
}

let rules;
try {
Expand Down Expand Up @@ -67,8 +69,10 @@ async function listBlocks(): Promise<void> {
}

async function listTools(): Promise<void> {
const cwd = process.cwd();
if (!(await ensureConfig(cwd))) return;
const cwd = await ensureConfig();
if (!cwd) {
return;
}

const config = await readConfig(cwd);
let activeScopeCount = 0;
Expand Down Expand Up @@ -119,8 +123,10 @@ function getAssetOutputHint(type: string, name: string): string {
}

async function listAssets(typeFilter?: string): Promise<void> {
const cwd = process.cwd();
if (!(await ensureConfig(cwd))) return;
const cwd = await ensureConfig();
if (!cwd) {
return;
}

const config = await readConfig(cwd);

Expand Down
15 changes: 11 additions & 4 deletions packages/cli/src/commands/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Command } from 'commander';
import { parse, stringify } from 'yaml';
import { readConfig } from '../core/parser.js';
import { fileExists } from '../utils/fs.js';
import { resolveContext } from '../core/resolve-context.js';
import { isAssetType, removeAsset } from '../core/assets.js';
import { validateInput } from './add.js';
import { multiselectPrompt, confirmPrompt, introPrompt, outroPrompt, isInteractiveSession } from '../utils/prompt.js';
Expand Down Expand Up @@ -52,18 +53,24 @@ async function removeRule(cwd: string, path: string): Promise<boolean> {
}

export async function runRemove(ruleArg: string | undefined): Promise<void> {
const cwd = process.cwd();

if (isInteractiveSession()) {
introPrompt('Remove rules or assets');
}

if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
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;

if (resolved.globalMode) {
ui.info('Removing from global config (~/.dwf)');
}

const config = await readConfig(cwd);

if (!ruleArg) {
Expand Down
Loading
Loading