@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * aeon dev - Start development server with hot reload
3
+ *
4
+ * Features:
5
+ * - Hot module reload
6
+ * - 'use aeon' directive processing
7
+ * - Real-time sync preview
8
+ * - AST ↔ Source bidirectional editing
9
+ * - Error overlay
10
+ */
11
+
12
+ import { watch } from 'fs';
13
+ import { readFile, stat } from 'fs/promises';
14
+ import { resolve, join, relative } from 'path';
15
+
16
+ interface DevOptions {
17
+ port: number;
18
+ config?: string;
19
+ }
20
+
21
+ interface AeonConfig {
22
+ pagesDir: string;
23
+ componentsDir?: string;
24
+ runtime: 'bun' | 'cloudflare';
25
+ port?: number;
26
+ aeon?: {
27
+ sync?: { mode: string };
28
+ presence?: { enabled: boolean };
29
+ versioning?: { enabled: boolean };
30
+ offline?: { enabled: boolean };
31
+ };
32
+ }
33
+
34
+ export async function dev(options: DevOptions): Promise<void> {
35
+ const cwd = process.cwd();
36
+ const configPath = options.config || 'aeon.config.ts';
37
+
38
+ console.log('\n🚀 Starting Aeon Flux development server...\n');
39
+
40
+ // Load config
41
+ const config = await loadConfig(resolve(cwd, configPath));
42
+ const port = options.port || config.port || 3000;
43
+
44
+ // Resolve directories
45
+ const pagesDir = resolve(cwd, config.pagesDir || './pages');
46
+ const componentsDir = resolve(cwd, config.componentsDir || './components');
47
+
48
+ console.log(`📁 Pages: ${relative(cwd, pagesDir)}`);
49
+ console.log(`📁 Components: ${relative(cwd, componentsDir)}`);
50
+ console.log(`🌐 Port: ${port}`);
51
+ console.log('');
52
+
53
+ // Scan for routes
54
+ const routes = await scanRoutes(pagesDir);
55
+ console.log(`📄 Found ${routes.length} route(s):`);
56
+ for (const route of routes) {
57
+ const aeonBadge = route.isAeon ? ' [aeon]' : '';
58
+ console.log(` ${route.pattern}${aeonBadge}`);
59
+ }
60
+ console.log('');
61
+
62
+ // Start file watcher
63
+ const watcher = startWatcher(
64
+ pagesDir,
65
+ componentsDir,
66
+ async (event, filename) => {
67
+ console.log(`\n♻️ ${event}: ${filename}`);
68
+ // Trigger HMR
69
+ broadcastHMR(filename);
70
+ },
71
+ );
72
+
73
+ // Start server
74
+ const server = Bun.serve({
75
+ port,
76
+ async fetch(req) {
77
+ const url = new URL(req.url);
78
+
79
+ // HMR WebSocket
80
+ if (url.pathname === '/__aeon_hmr') {
81
+ const upgraded = server.upgrade(req);
82
+ if (!upgraded) {
83
+ return new Response('WebSocket upgrade failed', { status: 400 });
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ // Dev overlay script
89
+ if (url.pathname === '/__aeon_dev.js') {
90
+ return new Response(DEV_OVERLAY_SCRIPT, {
91
+ headers: { 'Content-Type': 'application/javascript' },
92
+ });
93
+ }
94
+
95
+ // Match route
96
+ const match = matchRoute(url.pathname, routes);
97
+ if (!match) {
98
+ return new Response('Not found', { status: 404 });
99
+ }
100
+
101
+ // Read and process page
102
+ try {
103
+ const content = await readFile(match.filePath, 'utf-8');
104
+ const html = await renderPage(content, match, config);
105
+
106
+ // Inject dev overlay
107
+ const devHtml = injectDevOverlay(html, port);
108
+
109
+ return new Response(devHtml, {
110
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
111
+ });
112
+ } catch (err) {
113
+ console.error('Render error:', err);
114
+ return new Response(renderErrorPage(err as Error), {
115
+ status: 500,
116
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
117
+ });
118
+ }
119
+ },
120
+ websocket: {
121
+ open(ws) {
122
+ hmrClients.add(ws);
123
+ console.log(' HMR client connected');
124
+ },
125
+ close(ws) {
126
+ hmrClients.delete(ws);
127
+ },
128
+ message(_ws, _message) {
129
+ // Handle client messages if needed
130
+ },
131
+ },
132
+ });
133
+
134
+ console.log(`\n✨ Ready at http://localhost:${port}\n`);
135
+ console.log(' Press Ctrl+C to stop\n');
136
+
137
+ // Handle graceful shutdown
138
+ process.on('SIGINT', () => {
139
+ console.log('\n\n👋 Shutting down...\n');
140
+ watcher.close();
141
+ server.stop();
142
+ process.exit(0);
143
+ });
144
+ }
145
+
146
+ // HMR clients
147
+ const hmrClients = new Set<unknown>();
148
+
149
+ function broadcastHMR(filename: string): void {
150
+ const message = JSON.stringify({ type: 'reload', filename });
151
+ for (const client of hmrClients) {
152
+ try {
153
+ (client as { send: (msg: string) => void }).send(message);
154
+ } catch {
155
+ hmrClients.delete(client);
156
+ }
157
+ }
158
+ }
159
+
160
+ async function loadConfig(configPath: string): Promise<AeonConfig> {
161
+ try {
162
+ const module = await import(configPath);
163
+ return module.default || module;
164
+ } catch {
165
+ console.log('⚠️ No config found, using defaults');
166
+ return {
167
+ pagesDir: './pages',
168
+ runtime: 'bun',
169
+ };
170
+ }
171
+ }
172
+
173
+ interface RouteInfo {
174
+ pattern: string;
175
+ filePath: string;
176
+ isAeon: boolean;
177
+ layout?: string;
178
+ }
179
+
180
+ async function scanRoutes(pagesDir: string): Promise<RouteInfo[]> {
181
+ const routes: RouteInfo[] = [];
182
+
183
+ async function scan(dir: string, routePath: string): Promise<void> {
184
+ const { readdir } = await import('fs/promises');
185
+ const entries = await readdir(dir, { withFileTypes: true });
186
+
187
+ for (const entry of entries) {
188
+ const fullPath = join(dir, entry.name);
189
+
190
+ if (entry.isDirectory()) {
191
+ let segment = entry.name;
192
+ // Convert Next.js dynamic routes to Aeon format
193
+ if (segment.startsWith('[') && segment.endsWith(']')) {
194
+ if (segment.startsWith('[...')) {
195
+ // Catch-all: [...slug] -> *slug
196
+ segment = '*' + segment.slice(4, -1);
197
+ } else if (segment.startsWith('[[...')) {
198
+ // Optional catch-all: [[...slug]] -> **slug
199
+ segment = '**' + segment.slice(5, -2);
200
+ } else {
201
+ // Dynamic: [id] -> :id
202
+ segment = ':' + segment.slice(1, -1);
203
+ }
204
+ }
205
+ await scan(fullPath, `${routePath}/${segment}`);
206
+ } else if (entry.isFile()) {
207
+ const isPage =
208
+ entry.name === 'page.tsx' ||
209
+ entry.name === 'page.ts' ||
210
+ entry.name === 'page.jsx' ||
211
+ entry.name === 'page.js' ||
212
+ (entry.name.endsWith('.tsx') && !entry.name.startsWith('_'));
213
+
214
+ if (isPage) {
215
+ const content = await readFile(fullPath, 'utf-8');
216
+ const isAeon =
217
+ content.includes("'use aeon'") || content.includes('"use aeon"');
218
+
219
+ // Check for layout
220
+ const layoutPath = join(dir, 'layout.tsx');
221
+ let layout: string | undefined;
222
+ try {
223
+ await stat(layoutPath);
224
+ layout = layoutPath;
225
+ } catch {
226
+ // No layout
227
+ }
228
+
229
+ routes.push({
230
+ pattern: routePath || '/',
231
+ filePath: fullPath,
232
+ isAeon,
233
+ layout,
234
+ });
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ await scan(pagesDir, '');
241
+ return routes.sort((a, b) => {
242
+ // Static routes first
243
+ const aStatic = !a.pattern.includes(':') && !a.pattern.includes('*');
244
+ const bStatic = !b.pattern.includes(':') && !b.pattern.includes('*');
245
+ if (aStatic && !bStatic) return -1;
246
+ if (!aStatic && bStatic) return 1;
247
+ return a.pattern.localeCompare(b.pattern);
248
+ });
249
+ }
250
+
251
+ function matchRoute(
252
+ path: string,
253
+ routes: RouteInfo[],
254
+ ): (RouteInfo & { params: Record<string, string> }) | null {
255
+ for (const route of routes) {
256
+ const params = matchPattern(path, route.pattern);
257
+ if (params !== null) {
258
+ return { ...route, params };
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ function matchPattern(
265
+ path: string,
266
+ pattern: string,
267
+ ): Record<string, string> | null {
268
+ const pathParts = path.split('/').filter(Boolean);
269
+ const patternParts = pattern.split('/').filter(Boolean);
270
+ const params: Record<string, string> = {};
271
+
272
+ let pi = 0;
273
+ let pati = 0;
274
+
275
+ while (pi < pathParts.length && pati < patternParts.length) {
276
+ const pathPart = pathParts[pi];
277
+ const patternPart = patternParts[pati];
278
+
279
+ if (patternPart.startsWith('**')) {
280
+ // Optional catch-all
281
+ const paramName = patternPart.slice(2);
282
+ params[paramName] = pathParts.slice(pi).join('/');
283
+ return params;
284
+ } else if (patternPart.startsWith('*')) {
285
+ // Catch-all
286
+ const paramName = patternPart.slice(1);
287
+ params[paramName] = pathParts.slice(pi).join('/');
288
+ return params;
289
+ } else if (patternPart.startsWith(':')) {
290
+ // Dynamic segment
291
+ const paramName = patternPart.slice(1);
292
+ params[paramName] = pathPart;
293
+ pi++;
294
+ pati++;
295
+ } else if (pathPart === patternPart) {
296
+ pi++;
297
+ pati++;
298
+ } else {
299
+ return null;
300
+ }
301
+ }
302
+
303
+ // Handle optional catch-all at end
304
+ if (pati < patternParts.length && patternParts[pati].startsWith('**')) {
305
+ const paramName = patternParts[pati].slice(2);
306
+ params[paramName] = '';
307
+ return params;
308
+ }
309
+
310
+ if (pi === pathParts.length && pati === patternParts.length) {
311
+ return params;
312
+ }
313
+
314
+ return null;
315
+ }
316
+
317
+ async function renderPage(
318
+ content: string,
319
+ match: RouteInfo & { params: Record<string, string> },
320
+ _config: AeonConfig,
321
+ ): Promise<string> {
322
+ // For dev mode, we do a simple transform
323
+ // In production, this would use the WASM runtime
324
+
325
+ // Check for 'use aeon' directive
326
+ const isAeon = match.isAeon;
327
+
328
+ // Extract the default export component
329
+ const componentMatch = content.match(/export\s+default\s+function\s+(\w+)/);
330
+ const componentName = componentMatch?.[1] || 'Page';
331
+
332
+ // Simple dev rendering - in production this uses React SSR
333
+ const html = `<!DOCTYPE html>
334
+ <html lang="en">
335
+ <head>
336
+ <meta charset="UTF-8">
337
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
338
+ <title>${componentName} - Aeon Flux</title>
339
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
340
+ <script type="importmap">
341
+ {
342
+ "imports": {
343
+ "react": "https://esm.sh/react@18",
344
+ "react-dom": "https://esm.sh/react-dom@18",
345
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
346
+ "@affectively/aeon-pages/react": "/__aeon_runtime.js"
347
+ }
348
+ }
349
+ </script>
350
+ </head>
351
+ <body>
352
+ <div id="root"></div>
353
+ ${
354
+ isAeon
355
+ ? `
356
+ <script type="module">
357
+ // Aeon Flux runtime
358
+ window.__AEON_PAGE__ = {
359
+ route: "${match.pattern}",
360
+ params: ${JSON.stringify(match.params)},
361
+ isAeon: true,
362
+ };
363
+ </script>
364
+ `
365
+ : ''
366
+ }
367
+ <script type="module">
368
+ // Dev mode hydration
369
+ import React from 'react';
370
+ import { createRoot } from 'react-dom/client';
371
+
372
+ // Simple placeholder for dev mode
373
+ const App = () => React.createElement('div', {
374
+ style: { padding: '2rem', fontFamily: 'system-ui' }
375
+ }, [
376
+ React.createElement('h1', { key: 'h1' }, '${componentName}'),
377
+ React.createElement('p', { key: 'p' }, 'Route: ${match.pattern}'),
378
+ ${isAeon ? `React.createElement('p', { key: 'aeon', style: { color: 'green' } }, '✓ Aeon Flux enabled'),` : ''}
379
+ React.createElement('p', { key: 'note', style: { color: '#666', marginTop: '1rem' } },
380
+ 'This is dev mode. Full rendering coming soon.')
381
+ ]);
382
+
383
+ const root = createRoot(document.getElementById('root'));
384
+ root.render(React.createElement(App));
385
+ </script>
386
+ </body>
387
+ </html>`;
388
+
389
+ return html;
390
+ }
391
+
392
+ function injectDevOverlay(html: string, port: number): string {
393
+ const script = `<script>
394
+ // Aeon Flux HMR
395
+ const ws = new WebSocket('ws://localhost:${port}/__aeon_hmr');
396
+ ws.onmessage = (e) => {
397
+ const msg = JSON.parse(e.data);
398
+ if (msg.type === 'reload') {
399
+ console.log('[Aeon Flux] Reloading:', msg.filename);
400
+ location.reload();
401
+ }
402
+ };
403
+ ws.onclose = () => {
404
+ console.log('[Aeon Flux] Server disconnected, reconnecting...');
405
+ setTimeout(() => location.reload(), 1000);
406
+ };
407
+ </script>`;
408
+
409
+ return html.replace('</body>', `${script}</body>`);
410
+ }
411
+
412
+ function renderErrorPage(error: Error): string {
413
+ return `<!DOCTYPE html>
414
+ <html>
415
+ <head>
416
+ <title>Error - Aeon Flux</title>
417
+ <style>
418
+ body { font-family: system-ui; padding: 2rem; background: #1a1a1a; color: #fff; }
419
+ pre { background: #2d2d2d; padding: 1rem; border-radius: 8px; overflow: auto; }
420
+ h1 { color: #ff6b6b; }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <h1>⚠️ Error</h1>
425
+ <pre>${escapeHtml(error.message)}\n\n${escapeHtml(error.stack || '')}</pre>
426
+ </body>
427
+ </html>`;
428
+ }
429
+
430
+ function escapeHtml(str: string): string {
431
+ return str
432
+ .replace(/&/g, '&amp;')
433
+ .replace(/</g, '&lt;')
434
+ .replace(/>/g, '&gt;')
435
+ .replace(/"/g, '&quot;');
436
+ }
437
+
438
+ function startWatcher(
439
+ pagesDir: string,
440
+ componentsDir: string,
441
+ callback: (event: string, filename: string) => void,
442
+ ): { close: () => void } {
443
+ const watchers: ReturnType<typeof watch>[] = [];
444
+
445
+ try {
446
+ watchers.push(
447
+ watch(pagesDir, { recursive: true }, (event, filename) => {
448
+ if (filename) callback(event, `pages/${filename}`);
449
+ }),
450
+ );
451
+ } catch {
452
+ console.log('⚠️ Could not watch pages directory');
453
+ }
454
+
455
+ try {
456
+ watchers.push(
457
+ watch(componentsDir, { recursive: true }, (event, filename) => {
458
+ if (filename) callback(event, `components/${filename}`);
459
+ }),
460
+ );
461
+ } catch {
462
+ // Components dir may not exist
463
+ }
464
+
465
+ return {
466
+ close: () => watchers.forEach((w) => w.close()),
467
+ };
468
+ }
469
+
470
+ const DEV_OVERLAY_SCRIPT = `
471
+ // Aeon Flux Dev Overlay
472
+ console.log('[Aeon Flux] Dev mode active');
473
+ `;