@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.
@@ -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