Skip to content

Commit aaa52f4

Browse files
committed
fix(core): add global config fallback and fix banner test colors
- Add resolveContext() that checks local .dwf/ first, falls back to ~/.dwf/ - All commands (doctor, add, remove, list, explain, compile, watch) now use shared resolver - Show "Running in global mode (~/.dwf)" when using global config - Consistent error message when no config found - Fix banner test for gray gradient colors (252→240)
1 parent be4913d commit aaa52f4

13 files changed

Lines changed: 239 additions & 78 deletions

File tree

packages/cli/src/commands/add.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { convert } from '../core/converter.js';
1313
import { isAssetType, parseAssetFrontmatter } from '../core/assets.js';
1414
import { fileExists } from '../utils/fs.js';
1515
import { readConfig } from '../core/parser.js';
16+
import { resolveContext } from '../core/resolve-context.js';
1617
import {
1718
selectPrompt,
1819
multiselectPrompt,
@@ -915,14 +916,20 @@ export async function runAdd(ruleArg: string | undefined, options: AddOptions):
915916
return;
916917
}
917918

918-
const cwd = process.cwd();
919+
const resolved = await resolveContext(process.cwd());
919920

920-
if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
921-
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
921+
if (!resolved) {
922+
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
922923
process.exitCode = 1;
923924
return;
924925
}
925926

927+
const cwd = resolved.configRoot;
928+
929+
if (resolved.globalMode) {
930+
ui.info('Adding to global config (~/.dwf)');
931+
}
932+
926933
if (!ruleArg) {
927934
if (!isInteractiveSession()) {
928935
ui.error('No rule specified', 'Usage: devw add <category>/<rule>');

packages/cli/src/commands/compile.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mkdir, writeFile, readFile, symlink, unlink } from 'node:fs/promises';
2-
import { join, dirname, basename } from 'node:path';
2+
import { join, dirname } from 'node:path';
33
import { homedir } from 'node:os';
44
import type { Command } from 'commander';
55
import pc from 'picocolors';
@@ -19,6 +19,7 @@ import { cleanStaleFiles } from '../core/scope-filename.js';
1919
import { detectLegacyFiles, migrateLegacyFiles } from '../core/cleanup.js';
2020
import { buildCanonicalOutputs, writeCanonical } from '../core/canonical.js';
2121
import { fileExists } from '../utils/fs.js';
22+
import { resolveContext } from '../core/resolve-context.js';
2223
import * as ui from '../utils/ui.js';
2324
import { ICONS } from '../utils/ui.js';
2425
import { renderTable } from '../utils/table.js';
@@ -95,6 +96,7 @@ interface CompileContext {
9596
configRoot: string;
9697
outputRoot: string;
9798
globalMode: boolean;
99+
dwfDir: string;
98100
}
99101

100102
function toCompileSummaryRows(result: CompileResult): string[][] {
@@ -124,34 +126,25 @@ function toCompileSummaryRows(result: CompileResult): string[][] {
124126
}
125127

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

136-
const inGlobalConfigDir = basename(cwd) === '.dwf';
137-
const globalConfigPath = join(cwd, 'config.yml');
138-
if (inGlobalConfigDir && await fileExists(globalConfigPath)) {
139-
return {
140-
configRoot: cwd,
141-
outputRoot: homedir(),
142-
globalMode: true,
143-
};
144-
}
145-
146-
throw new Error('.dwf/config.yml not found. Run devw init to initialize the project');
134+
return {
135+
configRoot: resolved.configRoot,
136+
outputRoot: resolved.outputRoot,
137+
globalMode: resolved.globalMode,
138+
dwfDir: resolved.dwfDir,
139+
};
147140
}
148141

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

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

368361
if (options.verbose) {
369-
const config = context.globalMode ? await readConfigFromDwfDir(context.configRoot) : await readConfig(context.configRoot);
362+
const config = context.globalMode ? await readConfigFromDwfDir(context.dwfDir) : await readConfig(context.configRoot);
370363
const projectRules = await readRules(context.configRoot);
371364
const globalRules = context.globalMode || config.global === false
372365
? []

packages/cli/src/commands/doctor.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { copilotBridge } from '../bridges/copilot.js';
1212
import type { Bridge, DirectoryBridge, ProjectConfig, PulledEntry, AssetEntry, Rule } from '../bridges/types.js';
1313
import { getBridgeOutputPaths, isDirectoryBridge } from '../bridges/types.js';
1414
import { fileExists } from '../utils/fs.js';
15+
import { resolveContext } from '../core/resolve-context.js';
1516
import { isValidScope } from '../core/schema.js';
1617
import { buildCanonicalOutputs } from '../core/canonical.js';
1718
import { detectLegacyFiles } from '../core/cleanup.js';
@@ -479,44 +480,64 @@ export async function runDoctor(): Promise<void> {
479480
const results: CheckResult[] = [];
480481
let hasFailed = false;
481482

483+
// Resolve context: local project or global ~/.dwf
484+
const resolved = await resolveContext(cwd);
485+
486+
if (!resolved) {
487+
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
488+
process.exitCode = 1;
489+
return;
490+
}
491+
492+
const effectiveCwd = resolved.configRoot;
493+
494+
if (resolved.globalMode) {
495+
ui.info('Running in global mode (~/.dwf)');
496+
ui.newline();
497+
}
498+
482499
// Check 1: .dwf/config.yml exists
483-
const configExistsResult = await checkConfigExists(cwd);
500+
const configExistsResult = await checkConfigExists(effectiveCwd);
484501
results.push(configExistsResult);
485502

486503
if (!configExistsResult.passed) {
487504
for (const r of results) {
488505
ui.check(r.passed, r.message, r.skipped);
489-
if (!r.passed) hasFailed = true;
506+
if (!r.passed) {
507+
hasFailed = true;
508+
}
490509
}
491510
printSummary(results, startTime);
492511
process.exitCode = 1;
493512
return;
494513
}
495514

496515
// Check 2: config.yml is valid
497-
const configValidResult = await checkConfigValid(cwd);
516+
const configValidResult = await checkConfigValid(effectiveCwd);
498517
results.push(configValidResult);
499518

500519
// Check 3: Rule files are valid YAML
501-
const rulesValidResult = await checkRulesValid(cwd);
520+
const rulesValidResult = await checkRulesValid(effectiveCwd);
502521
results.push(rulesValidResult);
503522

504523
if (!configValidResult.passed) {
505524
for (const r of results) {
506525
ui.check(r.passed, r.message, r.skipped);
507-
if (!r.passed) hasFailed = true;
526+
if (!r.passed) {
527+
hasFailed = true;
528+
}
508529
}
509530
printSummary(results, startTime);
510531
process.exitCode = 1;
511532
return;
512533
}
513534

514-
const config = await readConfig(cwd);
535+
const config = await readConfig(effectiveCwd);
515536

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

536557
// Check 7: Symlinks valid (conditional on mode)
537-
const symlinkResult = await checkSymlinks(cwd, config);
558+
const symlinkResult = await checkSymlinks(effectiveCwd, config);
538559
results.push(symlinkResult);
539560

540561
// Check 8: Pulled files exist
541-
const pulledResult = await checkPulledFilesExist(cwd, config.pulled);
562+
const pulledResult = await checkPulledFilesExist(effectiveCwd, config.pulled);
542563
results.push(pulledResult);
543564

544565
// Check 9: Asset files exist
545-
const assetResult = await checkAssetFilesExist(cwd, config.assets);
566+
const assetResult = await checkAssetFilesExist(effectiveCwd, config.assets);
546567
results.push(assetResult);
547568

548569
// Check 10: Hash sync (conditional on compiled files existing)
549-
const hashResult = await checkHashSync(cwd, rules);
570+
const hashResult = await checkHashSync(effectiveCwd, rules);
550571
results.push(hashResult);
551572

552573
// Check 11: Canonical output exists (skip if no rules)
553574
if (rules.length > 0) {
554-
const canonicalExistsResult = await checkCanonicalExists(cwd);
575+
const canonicalExistsResult = await checkCanonicalExists(effectiveCwd);
555576
results.push(canonicalExistsResult);
556577

557578
// Check 12: Canonical and native outputs are synchronized
558-
const canonicalSyncResult = await checkCanonicalSync(cwd, rules, config);
579+
const canonicalSyncResult = await checkCanonicalSync(effectiveCwd, rules, config);
559580
results.push(canonicalSyncResult);
560581
}
561582

562583
// Check 13: Legacy migration has no pending files
563-
const legacyResult = await checkLegacyMigration(cwd);
584+
const legacyResult = await checkLegacyMigration(effectiveCwd);
564585
results.push(legacyResult);
565586

566587
// Check 14: Native files have valid frontmatter for their editor
567-
const frontmatterResult = await checkNativeFrontmatter(cwd, config);
588+
const frontmatterResult = await checkNativeFrontmatter(effectiveCwd, config);
568589
results.push(frontmatterResult);
569590

570591
// Output
571592
for (const r of results) {
572593
ui.check(r.passed, r.message, r.skipped);
573-
if (!r.passed) hasFailed = true;
594+
if (!r.passed) {
595+
hasFailed = true;
596+
}
574597
}
575598

576599
printSummary(results, startTime);

packages/cli/src/commands/explain.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { join } from 'node:path';
21
import type { Command } from 'commander';
32
import pc from 'picocolors';
43
import { readConfig, readRules } from '../core/parser.js';
@@ -10,7 +9,7 @@ import { geminiBridge } from '../bridges/gemini.js';
109
import { windsurfBridge } from '../bridges/windsurf.js';
1110
import { copilotBridge } from '../bridges/copilot.js';
1211
import { filterRules, groupByScope } from '../core/helpers.js';
13-
import { fileExists } from '../utils/fs.js';
12+
import { resolveContext } from '../core/resolve-context.js';
1413
import * as ui from '../utils/ui.js';
1514
import { ICONS } from '../utils/ui.js';
1615

@@ -57,14 +56,15 @@ function formatSeparator(toolId: string): string {
5756
}
5857

5958
async function runExplain(options: ExplainOptions): Promise<void> {
60-
const cwd = process.cwd();
59+
const resolved = await resolveContext(process.cwd());
6160

62-
if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
63-
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
61+
if (!resolved) {
62+
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
6463
process.exitCode = 1;
6564
return;
6665
}
6766

67+
const cwd = resolved.configRoot;
6868
const config = await readConfig(cwd);
6969
const rules = await readRules(cwd);
7070

packages/cli/src/commands/list.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { join } from 'node:path';
21
import type { Command } from 'commander';
32
import pc from 'picocolors';
43
import { readConfig, readRules } from '../core/parser.js';
5-
import { fileExists } from '../utils/fs.js';
4+
import { resolveContext } from '../core/resolve-context.js';
65
import { claudeBridge } from '../bridges/claude.js';
76
import { cursorBridge } from '../bridges/cursor.js';
87
import { geminiBridge } from '../bridges/gemini.js';
@@ -16,18 +15,21 @@ import { ICONS } from '../utils/ui.js';
1615

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

19-
async function ensureConfig(cwd: string): Promise<boolean> {
20-
if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
21-
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
18+
async function ensureConfig(): Promise<string | null> {
19+
const resolved = await resolveContext(process.cwd());
20+
if (!resolved) {
21+
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
2222
process.exitCode = 1;
23-
return false;
23+
return null;
2424
}
25-
return true;
25+
return resolved.configRoot;
2626
}
2727

2828
async function listRules(): Promise<void> {
29-
const cwd = process.cwd();
30-
if (!(await ensureConfig(cwd))) return;
29+
const cwd = await ensureConfig();
30+
if (!cwd) {
31+
return;
32+
}
3133

3234
let rules;
3335
try {
@@ -67,8 +69,10 @@ async function listBlocks(): Promise<void> {
6769
}
6870

6971
async function listTools(): Promise<void> {
70-
const cwd = process.cwd();
71-
if (!(await ensureConfig(cwd))) return;
72+
const cwd = await ensureConfig();
73+
if (!cwd) {
74+
return;
75+
}
7276

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

121125
async function listAssets(typeFilter?: string): Promise<void> {
122-
const cwd = process.cwd();
123-
if (!(await ensureConfig(cwd))) return;
126+
const cwd = await ensureConfig();
127+
if (!cwd) {
128+
return;
129+
}
124130

125131
const config = await readConfig(cwd);
126132

packages/cli/src/commands/remove.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Command } from 'commander';
44
import { parse, stringify } from 'yaml';
55
import { readConfig } from '../core/parser.js';
66
import { fileExists } from '../utils/fs.js';
7+
import { resolveContext } from '../core/resolve-context.js';
78
import { isAssetType, removeAsset } from '../core/assets.js';
89
import { validateInput } from './add.js';
910
import { multiselectPrompt, confirmPrompt, introPrompt, outroPrompt, isInteractiveSession } from '../utils/prompt.js';
@@ -52,18 +53,24 @@ async function removeRule(cwd: string, path: string): Promise<boolean> {
5253
}
5354

5455
export async function runRemove(ruleArg: string | undefined): Promise<void> {
55-
const cwd = process.cwd();
56-
5756
if (isInteractiveSession()) {
5857
introPrompt('Remove rules or assets');
5958
}
6059

61-
if (!(await fileExists(join(cwd, '.dwf', 'config.yml')))) {
62-
ui.error('.dwf/config.yml not found', 'Run devw init to initialize the project');
60+
const resolved = await resolveContext(process.cwd());
61+
62+
if (!resolved) {
63+
ui.error('No devw configuration found.', 'Run "devw init" to set up a project or global configuration.');
6364
process.exitCode = 1;
6465
return;
6566
}
6667

68+
const cwd = resolved.configRoot;
69+
70+
if (resolved.globalMode) {
71+
ui.info('Removing from global config (~/.dwf)');
72+
}
73+
6774
const config = await readConfig(cwd);
6875

6976
if (!ruleArg) {

0 commit comments

Comments
 (0)