11import { mkdir , writeFile , readFile , symlink , unlink } from 'node:fs/promises' ;
2- import { join , dirname } from 'node:path' ;
2+ import { join , dirname , basename } from 'node:path' ;
3+ import { homedir } from 'node:os' ;
34import type { Command } from 'commander' ;
45import chalk from 'chalk' ;
5- import { readConfig , readRules } from '../core/parser.js' ;
6+ import { readConfig , readConfigFromDwfDir , readRules } from '../core/parser.js' ;
7+ import { mergeRules } from '../core/merge.js' ;
68import { computeRulesHash , writeHash } from '../core/hash.js' ;
79import { deployAssets } from '../core/assets.js' ;
8- import type { Bridge , DirectoryBridge } from '../bridges/types.js' ;
10+ import type { Bridge , DirectoryBridge , Rule } from '../bridges/types.js' ;
911import { isDirectoryBridge , getBridgeOutputPaths } from '../bridges/types.js' ;
1012import { claudeBridge } from '../bridges/claude.js' ;
1113import { cursorBridge } from '../bridges/cursor.js' ;
@@ -46,6 +48,9 @@ export interface MigrationResult {
4648export interface CompileResult {
4749 results : BridgeResult [ ] ;
4850 activeRuleCount : number ;
51+ globalRuleCount : number ;
52+ projectRuleCount : number ;
53+ overriddenRuleIds : string [ ] ;
4954 canonicalFileCount : number ;
5055 canonicalError ?: string ;
5156 assetPaths : string [ ] ;
@@ -72,7 +77,7 @@ function extractFilenameFromPath(relativePath: string): string {
7277}
7378
7479async function handleDirectoryBridgeCleanup (
75- cwd : string ,
80+ outputRoot : string ,
7681 bridge : DirectoryBridge ,
7782 writtenFilenames : Set < string > ,
7883 write : boolean ,
@@ -81,16 +86,51 @@ async function handleDirectoryBridgeCleanup(
8186 return [ ] ;
8287 }
8388
84- const outputDir = join ( cwd , bridge . outputDir ) ;
89+ const outputDir = join ( outputRoot , bridge . outputDir ) ;
8590 return cleanStaleFiles ( outputDir , bridge . filePrefix , bridge . fileExtension , writtenFilenames ) ;
8691}
8792
93+ interface CompileContext {
94+ configRoot : string ;
95+ outputRoot : string ;
96+ globalMode : boolean ;
97+ }
98+
99+ async function resolveCompileContext ( cwd : string ) : Promise < CompileContext > {
100+ const projectConfigPath = join ( cwd , '.dwf' , 'config.yml' ) ;
101+ if ( await fileExists ( projectConfigPath ) ) {
102+ return {
103+ configRoot : cwd ,
104+ outputRoot : cwd ,
105+ globalMode : false ,
106+ } ;
107+ }
108+
109+ const inGlobalConfigDir = basename ( cwd ) === '.dwf' ;
110+ const globalConfigPath = join ( cwd , 'config.yml' ) ;
111+ if ( inGlobalConfigDir && await fileExists ( globalConfigPath ) ) {
112+ return {
113+ configRoot : cwd ,
114+ outputRoot : homedir ( ) ,
115+ globalMode : true ,
116+ } ;
117+ }
118+
119+ throw new Error ( '.dwf/config.yml not found. Run devw init to initialize the project' ) ;
120+ }
121+
88122export async function executePipeline ( options : PipelineOptions ) : Promise < CompileResult > {
89123 const { cwd, tool, write = true } = options ;
90124 const startTime = performance . now ( ) ;
125+ const context = await resolveCompileContext ( cwd ) ;
91126
92- const config = await readConfig ( cwd ) ;
93- const rules = await readRules ( cwd ) ;
127+ const config = context . globalMode ? await readConfigFromDwfDir ( context . configRoot ) : await readConfig ( context . configRoot ) ;
128+ const projectRules = await readRules ( context . configRoot ) ;
129+ const globalRules = context . globalMode || config . global === false
130+ ? [ ]
131+ : await readRules ( context . configRoot , join ( homedir ( ) , '.dwf' , 'rules' ) ) ;
132+ const rules = mergeRules ( globalRules , projectRules ) ;
133+ const overriddenRuleIds = getOverriddenRuleIds ( globalRules , projectRules ) ;
94134
95135 let toolIds = config . tools ;
96136 if ( tool ) {
@@ -103,9 +143,9 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
103143 // Legacy migration — run ONCE before writing new files
104144 const migration : MigrationResult = { actions : [ ] } ;
105145 if ( write ) {
106- const legacyFiles = await detectLegacyFiles ( cwd ) ;
146+ const legacyFiles = await detectLegacyFiles ( context . outputRoot ) ;
107147 if ( legacyFiles . length > 0 ) {
108- const actions = await migrateLegacyFiles ( cwd , legacyFiles ) ;
148+ const actions = await migrateLegacyFiles ( context . outputRoot , legacyFiles ) ;
109149 migration . actions = actions ;
110150 }
111151 }
@@ -125,7 +165,7 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
125165 // DirectoryBridge flow: multi-file output with stale cleanup
126166 if ( activeRules . length === 0 && write ) {
127167 // No active rules → clean all dwf- files from the output dir
128- const deleted = await handleDirectoryBridgeCleanup ( cwd , bridge , new Set ( ) , write ) ;
168+ const deleted = await handleDirectoryBridgeCleanup ( context . outputRoot , bridge , new Set ( ) , write ) ;
129169 if ( deleted . length > 0 ) {
130170 staleResults . push ( { bridgeId : bridge . id , deleted } ) ;
131171 }
@@ -143,11 +183,11 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
143183 continue ;
144184 }
145185
146- const absolutePath = join ( cwd , relativePath ) ;
186+ const absolutePath = join ( context . outputRoot , relativePath ) ;
147187 await mkdir ( dirname ( absolutePath ) , { recursive : true } ) ;
148188
149189 if ( config . mode === 'link' ) {
150- const cachePath = join ( cwd , '.dwf' , '.cache' , relativePath ) ;
190+ const cachePath = join ( context . outputRoot , '.dwf' , '.cache' , relativePath ) ;
151191 await mkdir ( dirname ( cachePath ) , { recursive : true } ) ;
152192 await writeFile ( cachePath , content , 'utf-8' ) ;
153193
@@ -163,15 +203,15 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
163203 }
164204
165205 // Stale file cleanup for DirectoryBridge
166- const deleted = await handleDirectoryBridgeCleanup ( cwd , bridge , writtenFilenames , write ) ;
206+ const deleted = await handleDirectoryBridgeCleanup ( context . outputRoot , bridge , writtenFilenames , write ) ;
167207 if ( deleted . length > 0 ) {
168208 staleResults . push ( { bridgeId : bridge . id , deleted } ) ;
169209 }
170210 } else {
171211 // MarkerBridge flow: merge content between markers in target file
172212 if ( activeRules . length === 0 && write ) {
173213 for ( const relativePath of getBridgeOutputPaths ( bridge ) ) {
174- const absolutePath = join ( cwd , relativePath ) ;
214+ const absolutePath = join ( context . outputRoot , relativePath ) ;
175215 if ( ! ( await fileExists ( absolutePath ) ) ) {
176216 continue ;
177217 }
@@ -192,7 +232,7 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
192232
193233 for ( const [ relativePath , rawContent ] of outputs ) {
194234 let content = rawContent ;
195- const absoluteCheck = join ( cwd , relativePath ) ;
235+ const absoluteCheck = join ( context . outputRoot , relativePath ) ;
196236 let existing : string | null = null ;
197237 try {
198238 existing = await readFile ( absoluteCheck , 'utf-8' ) ;
@@ -206,11 +246,11 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
206246 continue ;
207247 }
208248
209- const absolutePath = join ( cwd , relativePath ) ;
249+ const absolutePath = join ( context . outputRoot , relativePath ) ;
210250 await mkdir ( dirname ( absolutePath ) , { recursive : true } ) ;
211251
212252 if ( config . mode === 'link' ) {
213- const cachePath = join ( cwd , '.dwf' , '.cache' , relativePath ) ;
253+ const cachePath = join ( context . outputRoot , '.dwf' , '.cache' , relativePath ) ;
214254 await mkdir ( dirname ( cachePath ) , { recursive : true } ) ;
215255 await writeFile ( cachePath , content , 'utf-8' ) ;
216256
@@ -245,7 +285,7 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
245285 let canonicalError : string | undefined ;
246286 if ( write ) {
247287 try {
248- canonicalPaths = await writeCanonical ( cwd , canonicalOutputs ) ;
288+ canonicalPaths = await writeCanonical ( context . outputRoot , canonicalOutputs ) ;
249289 for ( const relativePath of canonicalPaths ) {
250290 results . push ( { bridgeId : 'canonical' , outputPath : relativePath , success : true } ) ;
251291 }
@@ -270,16 +310,19 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
270310 let assetPaths : string [ ] = [ ] ;
271311 if ( write ) {
272312 const hash = computeRulesHash ( activeRules ) ;
273- await writeHash ( cwd , hash ) ;
313+ await writeHash ( context . outputRoot , hash ) ;
274314
275- const assetResult = await deployAssets ( cwd , config ) ;
315+ const assetResult = await deployAssets ( context . outputRoot , config ) ;
276316 assetPaths = assetResult . deployed ;
277317 }
278318
279319 const elapsedMs = performance . now ( ) - startTime ;
280320 return {
281321 results,
282322 activeRuleCount : activeRules . length ,
323+ globalRuleCount : globalRules . length ,
324+ projectRuleCount : projectRules . length ,
325+ overriddenRuleIds,
283326 canonicalFileCount : canonicalPaths . length ,
284327 canonicalError,
285328 assetPaths,
@@ -292,19 +335,31 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
292335export async function runCompile ( options : CompileOptions ) : Promise < void > {
293336 const cwd = process . cwd ( ) ;
294337
295- if ( ! ( await fileExists ( join ( cwd , '.dwf' , 'config.yml' ) ) ) ) {
296- ui . error ( '.dwf/config.yml not found' , 'Run devw init to initialize the project' ) ;
297- process . exitCode = 1 ;
298- return ;
299- }
300-
301338 try {
339+ const context = await resolveCompileContext ( cwd ) ;
340+
302341 if ( options . verbose ) {
303- const config = await readConfig ( cwd ) ;
304- const rules = await readRules ( cwd ) ;
342+ const config = context . globalMode ? await readConfigFromDwfDir ( context . configRoot ) : await readConfig ( context . configRoot ) ;
343+ const projectRules = await readRules ( context . configRoot ) ;
344+ const globalRules = context . globalMode || config . global === false
345+ ? [ ]
346+ : await readRules ( context . configRoot , join ( homedir ( ) , '.dwf' , 'rules' ) ) ;
347+ const mergedRules = mergeRules ( globalRules , projectRules ) ;
348+ const overriddenRuleIds = getOverriddenRuleIds ( globalRules , projectRules ) ;
349+
305350 ui . keyValue ( 'Project:' , chalk . bold ( config . project . name ) ) ;
351+ ui . keyValue ( 'Scope:' , context . globalMode ? 'global (~/.dwf)' : 'project (.dwf)' ) ;
306352 ui . keyValue ( 'Mode:' , config . mode ) ;
307- ui . keyValue ( 'Rules:' , String ( rules . length ) ) ;
353+ ui . keyValue ( 'Project rules:' , String ( projectRules . length ) ) ;
354+ if ( config . global === false ) {
355+ ui . keyValue ( 'Global rules:' , 'disabled by config' ) ;
356+ } else {
357+ ui . keyValue ( 'Global rules:' , String ( globalRules . length ) ) ;
358+ }
359+ ui . keyValue ( 'Merged rules:' , String ( mergedRules . length ) ) ;
360+ if ( overriddenRuleIds . length > 0 ) {
361+ ui . keyValue ( 'Project overrides:' , String ( overriddenRuleIds . length ) ) ;
362+ }
308363 const toolIds = options . tool ? [ options . tool ] : config . tools ;
309364 ui . keyValue ( 'Tools:' , chalk . cyan ( toolIds . join ( ', ' ) ) ) ;
310365 ui . newline ( ) ;
@@ -359,6 +414,9 @@ export async function runCompile(options: CompileOptions): Promise<void> {
359414 ui . newline ( ) ;
360415 ui . success ( `Compiled ${ String ( result . activeRuleCount ) } rules ${ ICONS . arrow } ${ String ( allPaths . length ) } file${ allPaths . length !== 1 ? 's' : '' } ${ ui . timing ( result . elapsedMs ) } ` ) ;
361416 ui . info ( `Canonical files: ${ String ( result . canonicalFileCount ) } ` ) ;
417+ if ( options . verbose && result . overriddenRuleIds . length > 0 ) {
418+ ui . info ( `Project overrides (${ String ( result . overriddenRuleIds . length ) } ): ${ result . overriddenRuleIds . join ( ', ' ) } ` ) ;
419+ }
362420 ui . newline ( ) ;
363421
364422 if ( options . verbose ) {
@@ -393,6 +451,22 @@ export async function runCompileFromAdd(): Promise<void> {
393451 await runCompile ( { } ) ;
394452}
395453
454+ function getOverriddenRuleIds ( globalRules : Rule [ ] , projectRules : Rule [ ] ) : string [ ] {
455+ const globalIds = new Set < string > ( globalRules . map ( ( rule ) => rule . id ) ) ;
456+ const orderedOverrides : string [ ] = [ ] ;
457+ const seen = new Set < string > ( ) ;
458+
459+ for ( const rule of projectRules ) {
460+ if ( ! globalIds . has ( rule . id ) || seen . has ( rule . id ) ) {
461+ continue ;
462+ }
463+ seen . add ( rule . id ) ;
464+ orderedOverrides . push ( rule . id ) ;
465+ }
466+
467+ return orderedOverrides ;
468+ }
469+
396470export function registerCompileCommand ( program : Command ) : void {
397471 program
398472 . command ( 'compile' )
0 commit comments