@@ -106,6 +106,93 @@ const localPlugin = {
106106 } ;
107107 } ,
108108 } ,
109+ "no-cross-boundary-imports" : {
110+ meta : {
111+ type : "problem" ,
112+ docs : {
113+ description : "Enforce folder boundaries to prevent architectural violations" ,
114+ } ,
115+ messages : {
116+ browserToNode :
117+ "browser/ cannot import from node/. Move shared code to common/ or use IPC." ,
118+ nodeToDesktop :
119+ "node/ cannot import from desktop/. Move shared code to common/ or use dependency injection." ,
120+ nodeToCli :
121+ "node/ cannot import from cli/. Move shared code to common/." ,
122+ cliToBrowser :
123+ "cli/ cannot import from browser/. Move shared code to common/." ,
124+ desktopToBrowser :
125+ "desktop/ cannot import from browser/. Move shared code to common/." ,
126+ } ,
127+ } ,
128+ create ( context ) {
129+ return {
130+ ImportDeclaration ( node ) {
131+ // Allow type-only imports (for DI patterns)
132+ if ( node . importKind === "type" ) {
133+ return ;
134+ }
135+
136+ const sourceFile = context . filename ;
137+ const importPath = node . source . value ;
138+
139+ // Extract folder from source file (browser, node, desktop, cli, common)
140+ const sourceFolderMatch = sourceFile . match ( / \/ s r c \/ ( b r o w s e r | n o d e | d e s k t o p | c l i | c o m m o n ) \/ / ) ;
141+ if ( ! sourceFolderMatch ) return ;
142+ const sourceFolder = sourceFolderMatch [ 1 ] ;
143+
144+ // Extract folder from import target
145+ // Handle relative imports (e.g., '../node/...')
146+ let targetFolder = null ;
147+ if ( importPath . startsWith ( "../" ) ) {
148+ const targetMatch = importPath . match ( / \. \. \/ ( b r o w s e r | n o d e | d e s k t o p | c l i | c o m m o n ) \/ / ) ;
149+ if ( targetMatch ) {
150+ targetFolder = targetMatch [ 1 ] ;
151+ }
152+ } else if ( importPath . startsWith ( "@/" ) ) {
153+ // Handle alias imports (e.g., '@/node/...')
154+ const targetMatch = importPath . match ( / @ \/ ( b r o w s e r | n o d e | d e s k t o p | c l i | c o m m o n ) \/ / ) ;
155+ if ( targetMatch ) {
156+ targetFolder = targetMatch [ 1 ] ;
157+ }
158+ }
159+
160+ if ( ! targetFolder ) return ;
161+
162+ // Allow imports from common
163+ if ( targetFolder === "common" ) return ;
164+
165+ // Check for violations
166+ if ( sourceFolder === "browser" && targetFolder === "node" ) {
167+ context . report ( {
168+ node,
169+ messageId : "browserToNode" ,
170+ } ) ;
171+ } else if ( sourceFolder === "node" && targetFolder === "desktop" ) {
172+ context . report ( {
173+ node,
174+ messageId : "nodeToDesktop" ,
175+ } ) ;
176+ } else if ( sourceFolder === "node" && targetFolder === "cli" ) {
177+ context . report ( {
178+ node,
179+ messageId : "nodeToCli" ,
180+ } ) ;
181+ } else if ( sourceFolder === "cli" && targetFolder === "browser" ) {
182+ context . report ( {
183+ node,
184+ messageId : "cliToBrowser" ,
185+ } ) ;
186+ } else if ( sourceFolder === "desktop" && targetFolder === "browser" ) {
187+ context . report ( {
188+ node,
189+ messageId : "desktopToBrowser" ,
190+ } ) ;
191+ }
192+ } ,
193+ } ;
194+ } ,
195+ } ,
109196 } ,
110197} ;
111198
@@ -265,6 +352,7 @@ export default defineConfig([
265352 // Safe Node.js patterns
266353 "local/no-unsafe-child-process" : "error" ,
267354 "local/no-sync-fs-methods" : "error" ,
355+ "local/no-cross-boundary-imports" : "error" ,
268356
269357 // Allow console for this app (it's a dev tool)
270358 "no-console" : "off" ,
@@ -410,6 +498,8 @@ export default defineConfig([
410498 // This file is only used by Node.js code (cli/debug) but lives in common/
411499 // TODO: Consider moving to node/utils/
412500 "src/common/utils/providers/ensureProvidersConfig.ts" ,
501+ // Telemetry uses defensive process checks for test environments
502+ "src/common/telemetry/**" ,
413503 ] ,
414504 rules : {
415505 "no-restricted-globals" : [
0 commit comments