@oh-my-pi/pi-mom 0.1.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.
- package/CHANGELOG.md +338 -0
- package/README.md +517 -0
- package/dev.sh +30 -0
- package/docker.sh +95 -0
- package/docs/artifacts-server.md +475 -0
- package/docs/events.md +307 -0
- package/docs/new.md +977 -0
- package/docs/sandbox.md +153 -0
- package/docs/slack-bot-minimal-guide.md +399 -0
- package/docs/v86.md +319 -0
- package/package.json +45 -0
- package/scripts/migrate-timestamps.ts +121 -0
- package/src/agent.ts +887 -0
- package/src/context.ts +666 -0
- package/src/download.ts +117 -0
- package/src/events.ts +385 -0
- package/src/log.ts +271 -0
- package/src/main.ts +334 -0
- package/src/sandbox.ts +238 -0
- package/src/slack.ts +635 -0
- package/src/store.ts +253 -0
- package/src/tools/attach.ts +47 -0
- package/src/tools/bash.ts +99 -0
- package/src/tools/edit.ts +165 -0
- package/src/tools/index.ts +19 -0
- package/src/tools/read.ts +165 -0
- package/src/tools/truncate.ts +236 -0
- package/src/tools/write.ts +45 -0
- package/tsconfig.build.json +9 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# Artifacts Server
|
|
2
|
+
|
|
3
|
+
Share HTML files, visualizations, and interactive demos publicly via Cloudflare Tunnel with live reload support.
|
|
4
|
+
|
|
5
|
+
## What is it?
|
|
6
|
+
|
|
7
|
+
The artifacts server lets Mom create HTML/JS/CSS files that you can instantly view in a browser, with WebSocket-based live reload for development. Perfect for dashboards, visualizations, prototypes, and interactive demos.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### 1. Install Dependencies
|
|
12
|
+
|
|
13
|
+
**Node.js packages:**
|
|
14
|
+
```bash
|
|
15
|
+
cd /workspace/artifacts
|
|
16
|
+
npm init -y
|
|
17
|
+
npm install express ws chokidar
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Cloudflared (Cloudflare Tunnel):**
|
|
21
|
+
```bash
|
|
22
|
+
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
|
|
23
|
+
mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
|
|
24
|
+
chmod +x /usr/local/bin/cloudflared
|
|
25
|
+
cloudflared --version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Create Server
|
|
29
|
+
|
|
30
|
+
Save this as `/workspace/artifacts/server.js`:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
#!/usr/bin/env node
|
|
34
|
+
|
|
35
|
+
const express = require('express');
|
|
36
|
+
const { WebSocketServer } = require('ws');
|
|
37
|
+
const chokidar = require('chokidar');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const http = require('http');
|
|
41
|
+
|
|
42
|
+
const PORT = 8080;
|
|
43
|
+
const FILES_DIR = path.join(__dirname, 'files');
|
|
44
|
+
|
|
45
|
+
// Ensure files directory exists
|
|
46
|
+
if (!fs.existsSync(FILES_DIR)) {
|
|
47
|
+
fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const app = express();
|
|
51
|
+
const server = http.createServer(app);
|
|
52
|
+
const wss = new WebSocketServer({ server, clientTracking: true });
|
|
53
|
+
|
|
54
|
+
// Track connected WebSocket clients
|
|
55
|
+
const clients = new Set();
|
|
56
|
+
|
|
57
|
+
// WebSocket connection handler with error handling
|
|
58
|
+
wss.on('connection', (ws) => {
|
|
59
|
+
console.log('WebSocket client connected');
|
|
60
|
+
clients.add(ws);
|
|
61
|
+
|
|
62
|
+
ws.on('error', (err) => {
|
|
63
|
+
console.error('WebSocket client error:', err.message);
|
|
64
|
+
clients.delete(ws);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ws.on('close', () => {
|
|
68
|
+
console.log('WebSocket client disconnected');
|
|
69
|
+
clients.delete(ws);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
wss.on('error', (err) => {
|
|
74
|
+
console.error('WebSocket server error:', err.message);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Watch for file changes
|
|
78
|
+
const watcher = chokidar.watch(FILES_DIR, {
|
|
79
|
+
persistent: true,
|
|
80
|
+
ignoreInitial: true,
|
|
81
|
+
depth: 99, // Watch all subdirectory levels
|
|
82
|
+
ignorePermissionErrors: true,
|
|
83
|
+
awaitWriteFinish: {
|
|
84
|
+
stabilityThreshold: 100,
|
|
85
|
+
pollInterval: 50
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
watcher.on('all', (event, filepath) => {
|
|
90
|
+
console.log(`File ${event}: ${filepath}`);
|
|
91
|
+
|
|
92
|
+
// If a new directory is created, explicitly watch it
|
|
93
|
+
// This ensures newly created artifact folders are monitored without restart
|
|
94
|
+
if (event === 'addDir') {
|
|
95
|
+
watcher.add(filepath);
|
|
96
|
+
console.log(`Now watching directory: ${filepath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const relativePath = path.relative(FILES_DIR, filepath);
|
|
100
|
+
const message = JSON.stringify({
|
|
101
|
+
type: 'reload',
|
|
102
|
+
file: relativePath
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
clients.forEach(client => {
|
|
106
|
+
if (client.readyState === 1) {
|
|
107
|
+
try {
|
|
108
|
+
client.send(message);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Error sending to client:', err.message);
|
|
111
|
+
clients.delete(client);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
clients.delete(client);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
watcher.on('error', (err) => {
|
|
120
|
+
console.error('File watcher error:', err.message);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Cache-busting headers
|
|
124
|
+
app.use((req, res, next) => {
|
|
125
|
+
res.set({
|
|
126
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
127
|
+
'Pragma': 'no-cache',
|
|
128
|
+
'Expires': '0',
|
|
129
|
+
'Surrogate-Control': 'no-store'
|
|
130
|
+
});
|
|
131
|
+
next();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Inject live reload script for HTML files with ?ws=true
|
|
135
|
+
app.use((req, res, next) => {
|
|
136
|
+
if (!req.path.endsWith('.html') || req.query.ws !== 'true') {
|
|
137
|
+
return next();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const filePath = path.join(FILES_DIR, req.path);
|
|
141
|
+
|
|
142
|
+
// Security: Prevent path traversal attacks
|
|
143
|
+
const resolvedPath = path.resolve(filePath);
|
|
144
|
+
const resolvedBase = path.resolve(FILES_DIR);
|
|
145
|
+
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
146
|
+
return res.status(403).send('Forbidden: Path traversal detected');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fs.readFile(filePath, 'utf8', (err, data) => {
|
|
150
|
+
if (err) {
|
|
151
|
+
return next();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const liveReloadScript = `
|
|
155
|
+
<script>
|
|
156
|
+
(function() {
|
|
157
|
+
const errorDiv = document.createElement('div');
|
|
158
|
+
errorDiv.style.cssText = 'position:fixed;bottom:10px;left:10px;background:rgba(0,150,0,0.9);color:white;padding:15px;border-radius:8px;font-family:monospace;font-size:12px;max-width:90%;z-index:9999;word-break:break-all';
|
|
159
|
+
errorDiv.textContent = 'Live reload: connecting...';
|
|
160
|
+
document.body.appendChild(errorDiv);
|
|
161
|
+
|
|
162
|
+
function showStatus(msg, isError) {
|
|
163
|
+
errorDiv.textContent = msg;
|
|
164
|
+
errorDiv.style.background = isError ? 'rgba(255,0,0,0.9)' : 'rgba(0,150,0,0.9)';
|
|
165
|
+
if (!isError) setTimeout(() => errorDiv.style.display = 'none', 3000);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
170
|
+
const wsUrl = protocol + window.location.host;
|
|
171
|
+
const ws = new WebSocket(wsUrl);
|
|
172
|
+
|
|
173
|
+
ws.onopen = () => showStatus('Live reload connected!', false);
|
|
174
|
+
ws.onmessage = (e) => {
|
|
175
|
+
const msg = JSON.parse(e.data);
|
|
176
|
+
if (msg.type === 'reload') {
|
|
177
|
+
showStatus('File changed, reloading...', false);
|
|
178
|
+
setTimeout(() => window.location.reload(), 500);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
ws.onerror = () => showStatus('Connection failed', true);
|
|
182
|
+
ws.onclose = (e) => showStatus('Disconnected: ' + e.code, true);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
showStatus('Error: ' + err.message, true);
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
</script>`;
|
|
188
|
+
|
|
189
|
+
if (data.includes('</body>')) {
|
|
190
|
+
data = data.replace('</body>', liveReloadScript + '</body>');
|
|
191
|
+
} else {
|
|
192
|
+
data = data + liveReloadScript;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
res.type('html').send(data);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Serve static files
|
|
200
|
+
app.use(express.static(FILES_DIR));
|
|
201
|
+
|
|
202
|
+
// Error handling
|
|
203
|
+
app.use((err, req, res, next) => {
|
|
204
|
+
console.error('Express error:', err.message);
|
|
205
|
+
res.status(500).send('Internal server error');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
server.on('error', (err) => {
|
|
209
|
+
if (err.code === 'EADDRINUSE') {
|
|
210
|
+
console.error(`Port ${PORT} is already in use`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
} else {
|
|
213
|
+
console.error('Server error:', err.message);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Global error handlers
|
|
218
|
+
process.on('uncaughtException', (err) => {
|
|
219
|
+
console.error('Uncaught exception:', err);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
process.on('unhandledRejection', (reason) => {
|
|
223
|
+
console.error('Unhandled rejection:', reason);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Graceful shutdown
|
|
227
|
+
process.on('SIGTERM', () => {
|
|
228
|
+
console.log('SIGTERM received, closing gracefully');
|
|
229
|
+
watcher.close();
|
|
230
|
+
server.close(() => process.exit(0));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
process.on('SIGINT', () => {
|
|
234
|
+
console.log('SIGINT received, closing gracefully');
|
|
235
|
+
watcher.close();
|
|
236
|
+
server.close(() => process.exit(0));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Start server
|
|
240
|
+
server.listen(PORT, () => {
|
|
241
|
+
console.log(`Artifacts server running on http://localhost:${PORT}`);
|
|
242
|
+
console.log(`Serving files from: ${FILES_DIR}`);
|
|
243
|
+
console.log(`Add ?ws=true to any URL for live reload`);
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Make executable:
|
|
248
|
+
```bash
|
|
249
|
+
chmod +x /workspace/artifacts/server.js
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 3. Create Startup Script
|
|
253
|
+
|
|
254
|
+
Save this as `/workspace/artifacts/start-server.sh`:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
#!/bin/sh
|
|
258
|
+
set -e
|
|
259
|
+
|
|
260
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
261
|
+
cd "$SCRIPT_DIR"
|
|
262
|
+
|
|
263
|
+
echo "Starting artifacts server..."
|
|
264
|
+
|
|
265
|
+
# Start Node.js server in background
|
|
266
|
+
node server.js > /tmp/server.log 2>&1 &
|
|
267
|
+
NODE_PID=$!
|
|
268
|
+
|
|
269
|
+
# Wait for server to be ready
|
|
270
|
+
sleep 2
|
|
271
|
+
|
|
272
|
+
# Start cloudflare tunnel
|
|
273
|
+
echo "Starting Cloudflare Tunnel..."
|
|
274
|
+
cloudflared tunnel --url http://localhost:8080 2>&1 | tee /tmp/cloudflared.log &
|
|
275
|
+
TUNNEL_PID=$!
|
|
276
|
+
|
|
277
|
+
# Wait for tunnel to establish
|
|
278
|
+
sleep 5
|
|
279
|
+
|
|
280
|
+
# Extract and display public URL
|
|
281
|
+
PUBLIC_URL=$(grep -o 'https://.*\.trycloudflare\.com' /tmp/cloudflared.log | head -1)
|
|
282
|
+
|
|
283
|
+
if [ -n "$PUBLIC_URL" ]; then
|
|
284
|
+
echo ""
|
|
285
|
+
echo "=========================================="
|
|
286
|
+
echo "Artifacts server is running!"
|
|
287
|
+
echo "=========================================="
|
|
288
|
+
echo "Public URL: $PUBLIC_URL"
|
|
289
|
+
echo "Files directory: $SCRIPT_DIR/files/"
|
|
290
|
+
echo ""
|
|
291
|
+
echo "Add ?ws=true to any URL for live reload"
|
|
292
|
+
echo "Example: $PUBLIC_URL/test.html?ws=true"
|
|
293
|
+
echo "=========================================="
|
|
294
|
+
echo ""
|
|
295
|
+
|
|
296
|
+
echo "$PUBLIC_URL" > /tmp/artifacts-url.txt
|
|
297
|
+
else
|
|
298
|
+
echo "Warning: Could not extract public URL"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# Keep script running
|
|
302
|
+
cleanup() {
|
|
303
|
+
echo "Shutting down..."
|
|
304
|
+
kill $NODE_PID 2>/dev/null || true
|
|
305
|
+
kill $TUNNEL_PID 2>/dev/null || true
|
|
306
|
+
exit 0
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
trap cleanup INT TERM
|
|
310
|
+
wait $NODE_PID $TUNNEL_PID
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Make executable:
|
|
314
|
+
```bash
|
|
315
|
+
chmod +x /workspace/artifacts/start-server.sh
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Directory Structure
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
/workspace/artifacts/
|
|
322
|
+
├── server.js # Node.js server
|
|
323
|
+
├── start-server.sh # Startup script
|
|
324
|
+
├── package.json # Dependencies
|
|
325
|
+
├── node_modules/ # Installed packages
|
|
326
|
+
└── files/ # PUT YOUR ARTIFACTS HERE
|
|
327
|
+
├── 2025-12-14-demo/
|
|
328
|
+
│ ├── index.html
|
|
329
|
+
│ ├── style.css
|
|
330
|
+
│ └── logo.png
|
|
331
|
+
├── 2025-12-15-chart/
|
|
332
|
+
│ └── index.html
|
|
333
|
+
└── test.html (standalone OK)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Usage
|
|
337
|
+
|
|
338
|
+
### Starting the Server
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
cd /workspace/artifacts
|
|
342
|
+
./start-server.sh
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
This will:
|
|
346
|
+
1. Start Node.js server on localhost:8080
|
|
347
|
+
2. Create Cloudflare Tunnel with public URL
|
|
348
|
+
3. Print the URL (e.g., `https://random-words-123.trycloudflare.com`)
|
|
349
|
+
4. Save URL to `/tmp/artifacts-url.txt`
|
|
350
|
+
|
|
351
|
+
**Note:** URL changes every time you restart (free Cloudflare Tunnel limitation).
|
|
352
|
+
|
|
353
|
+
### Creating Artifacts
|
|
354
|
+
|
|
355
|
+
**Folder organization:**
|
|
356
|
+
- Create one subfolder per artifact: `$(date +%Y-%m-%d)-description/`
|
|
357
|
+
- Put main file as `index.html` for clean URLs
|
|
358
|
+
- Include images, CSS, JS, data in same folder
|
|
359
|
+
- CDN resources (Tailwind, Three.js, etc.) work fine
|
|
360
|
+
|
|
361
|
+
**Example:**
|
|
362
|
+
```bash
|
|
363
|
+
mkdir -p /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard
|
|
364
|
+
cat > /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard/index.html << 'EOF'
|
|
365
|
+
<!DOCTYPE html>
|
|
366
|
+
<html>
|
|
367
|
+
<head>
|
|
368
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
369
|
+
</head>
|
|
370
|
+
<body class="bg-gray-900 text-white p-8">
|
|
371
|
+
<h1 class="text-4xl font-bold">My Dashboard</h1>
|
|
372
|
+
<img src="logo.png" alt="Logo">
|
|
373
|
+
</body>
|
|
374
|
+
</html>
|
|
375
|
+
EOF
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Access:**
|
|
379
|
+
- **IMPORTANT:** Always use full `index.html` path for live reload to work
|
|
380
|
+
- Development (live reload): `https://your-url.trycloudflare.com/2025-12-14-dashboard/index.html?ws=true`
|
|
381
|
+
- Share (static): `https://your-url.trycloudflare.com/2025-12-14-dashboard/index.html`
|
|
382
|
+
|
|
383
|
+
**Note:** Folder URLs (`/folder/`) won't inject WebSocket script, must use `/folder/index.html`
|
|
384
|
+
|
|
385
|
+
### Live Reload
|
|
386
|
+
|
|
387
|
+
When viewing with `?ws=true`:
|
|
388
|
+
1. You'll see a green box at bottom-left: "Live reload connected!"
|
|
389
|
+
2. Edit any file in the artifact folder
|
|
390
|
+
3. Page auto-reloads within 1 second
|
|
391
|
+
4. Perfect for iterating on designs
|
|
392
|
+
|
|
393
|
+
**Remove `?ws=true` when sharing** - no WebSocket overhead for viewers.
|
|
394
|
+
|
|
395
|
+
## How It Works
|
|
396
|
+
|
|
397
|
+
**Architecture:**
|
|
398
|
+
- Node.js server (Express) serves static files from `/workspace/artifacts/files/`
|
|
399
|
+
- Chokidar file watcher monitors for changes (including new directories)
|
|
400
|
+
- WebSocket broadcasts reload messages to connected clients
|
|
401
|
+
- Cloudflare Tunnel exposes localhost to internet with public HTTPS URL
|
|
402
|
+
- Client-side script auto-reloads browser when file changes detected
|
|
403
|
+
|
|
404
|
+
**Security:**
|
|
405
|
+
- Path traversal protection prevents access outside `files/` directory
|
|
406
|
+
- Only files in `/workspace/artifacts/files/` are served
|
|
407
|
+
- Cache-busting headers prevent stale content
|
|
408
|
+
|
|
409
|
+
**File Watching:**
|
|
410
|
+
- Automatically detects new artifact folders created after server start
|
|
411
|
+
- Watches all subdirectories recursively (depth: 99)
|
|
412
|
+
- No server restart needed when creating new projects
|
|
413
|
+
|
|
414
|
+
## Troubleshooting
|
|
415
|
+
|
|
416
|
+
**502 Bad Gateway:**
|
|
417
|
+
- Node server crashed. Check logs: `cat /tmp/server.log`
|
|
418
|
+
- Restart: `cd /workspace/artifacts && node server.js &`
|
|
419
|
+
|
|
420
|
+
**WebSocket not connecting:**
|
|
421
|
+
- Check browser console for errors
|
|
422
|
+
- Ensure `?ws=true` is in URL
|
|
423
|
+
- Red/yellow box at bottom-left shows connection errors
|
|
424
|
+
- Use full `index.html` path, not folder URL
|
|
425
|
+
|
|
426
|
+
**Files not updating:**
|
|
427
|
+
- Check file watcher logs: `tail /tmp/server.log`
|
|
428
|
+
- Ensure files are in `/workspace/artifacts/files/`
|
|
429
|
+
- Should see "File change:" messages in logs
|
|
430
|
+
|
|
431
|
+
**Port already in use:**
|
|
432
|
+
- Kill existing server: `pkill node`
|
|
433
|
+
- Wait 2 seconds, restart
|
|
434
|
+
|
|
435
|
+
**Browser caching issues:**
|
|
436
|
+
- Server sends no-cache headers
|
|
437
|
+
- Hard refresh: Ctrl+Shift+R
|
|
438
|
+
- Add version parameter: `?ws=true&v=2`
|
|
439
|
+
|
|
440
|
+
## Example Session
|
|
441
|
+
|
|
442
|
+
**You:** "Create a Three.js spinning cube demo with Tailwind UI"
|
|
443
|
+
|
|
444
|
+
**Mom creates:**
|
|
445
|
+
```
|
|
446
|
+
/workspace/artifacts/files/2025-12-14-threejs-cube/
|
|
447
|
+
├── index.html (Three.js from CDN, Tailwind from CDN)
|
|
448
|
+
└── screenshot.png
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Access:** `https://concepts-rome-123.trycloudflare.com/2025-12-14-threejs-cube/index.html?ws=true`
|
|
452
|
+
|
|
453
|
+
**You:** "Make the cube purple and add a grid"
|
|
454
|
+
|
|
455
|
+
**Mom:** Edits `index.html`
|
|
456
|
+
|
|
457
|
+
**Result:** Your browser auto-reloads, showing purple cube with grid (within 1 second)
|
|
458
|
+
|
|
459
|
+
## Technical Notes
|
|
460
|
+
|
|
461
|
+
**Why not Node.js fs.watch?**
|
|
462
|
+
- `fs.watch` with `recursive: true` only works on macOS/Windows
|
|
463
|
+
- On Linux (Docker), it doesn't support recursive watching
|
|
464
|
+
- Chokidar is the most reliable cross-platform solution
|
|
465
|
+
- We explicitly add new directories when detected to ensure monitoring
|
|
466
|
+
|
|
467
|
+
**WebSocket vs Server-Sent Events:**
|
|
468
|
+
- WebSocket works reliably through Cloudflare Tunnel
|
|
469
|
+
- All connected clients reload when ANY file changes (simple approach)
|
|
470
|
+
- For production, you'd filter by current page path
|
|
471
|
+
|
|
472
|
+
**Cloudflare Tunnel Free Tier:**
|
|
473
|
+
- Random subdomain changes on each restart
|
|
474
|
+
- No persistent URLs without paid account
|
|
475
|
+
- WebSocket support is reliable despite being free tier
|