# Artifacts Server Share HTML files, visualizations, and interactive demos publicly via Cloudflare Tunnel with live reload support. ## What is it? 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. ## Installation ### 1. Install Dependencies **Node.js packages:** ```bash cd /workspace/artifacts npm init -y npm install express ws chokidar ``` **Cloudflared (Cloudflare Tunnel):** ```bash wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 mv cloudflared-linux-amd64 /usr/local/bin/cloudflared chmod +x /usr/local/bin/cloudflared cloudflared --version ``` ### 2. Create Server Save this as `/workspace/artifacts/server.js`: ```javascript #!/usr/bin/env node const express = require('express'); const { WebSocketServer } = require('ws'); const chokidar = require('chokidar'); const path = require('path'); const fs = require('fs'); const http = require('http'); const PORT = 8080; const FILES_DIR = path.join(__dirname, 'files'); // Ensure files directory exists if (!fs.existsSync(FILES_DIR)) { fs.mkdirSync(FILES_DIR, { recursive: true }); } const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server, clientTracking: true }); // Track connected WebSocket clients const clients = new Set(); // WebSocket connection handler with error handling wss.on('connection', (ws) => { console.log('WebSocket client connected'); clients.add(ws); ws.on('error', (err) => { console.error('WebSocket client error:', err.message); clients.delete(ws); }); ws.on('close', () => { console.log('WebSocket client disconnected'); clients.delete(ws); }); }); wss.on('error', (err) => { console.error('WebSocket server error:', err.message); }); // Watch for file changes const watcher = chokidar.watch(FILES_DIR, { persistent: true, ignoreInitial: true, depth: 99, // Watch all subdirectory levels ignorePermissionErrors: true, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 } }); watcher.on('all', (event, filepath) => { console.log(`File ${event}: ${filepath}`); // If a new directory is created, explicitly watch it // This ensures newly created artifact folders are monitored without restart if (event === 'addDir') { watcher.add(filepath); console.log(`Now watching directory: ${filepath}`); } const relativePath = path.relative(FILES_DIR, filepath); const message = JSON.stringify({ type: 'reload', file: relativePath }); clients.forEach(client => { if (client.readyState === 1) { try { client.send(message); } catch (err) { console.error('Error sending to client:', err.message); clients.delete(client); } } else { clients.delete(client); } }); }); watcher.on('error', (err) => { console.error('File watcher error:', err.message); }); // Cache-busting headers app.use((req, res, next) => { res.set({ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Surrogate-Control': 'no-store' }); next(); }); // Inject live reload script for HTML files with ?ws=true app.use((req, res, next) => { if (!req.path.endsWith('.html') || req.query.ws !== 'true') { return next(); } const filePath = path.join(FILES_DIR, req.path); // Security: Prevent path traversal attacks const resolvedPath = path.resolve(filePath); const resolvedBase = path.resolve(FILES_DIR); if (!resolvedPath.startsWith(resolvedBase)) { return res.status(403).send('Forbidden: Path traversal detected'); } fs.readFile(filePath, 'utf8', (err, data) => { if (err) { return next(); } const liveReloadScript = ` `; if (data.includes('
')) { data = data.replace('', liveReloadScript + ''); } else { data = data + liveReloadScript; } res.type('html').send(data); }); }); // Serve static files app.use(express.static(FILES_DIR)); // Error handling app.use((err, req, res, next) => { console.error('Express error:', err.message); res.status(500).send('Internal server error'); }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`Port ${PORT} is already in use`); process.exit(1); } else { console.error('Server error:', err.message); } }); // Global error handlers process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, closing gracefully'); watcher.close(); server.close(() => process.exit(0)); }); process.on('SIGINT', () => { console.log('SIGINT received, closing gracefully'); watcher.close(); server.close(() => process.exit(0)); }); // Start server server.listen(PORT, () => { console.log(`Artifacts server running on http://localhost:${PORT}`); console.log(`Serving files from: ${FILES_DIR}`); console.log(`Add ?ws=true to any URL for live reload`); }); ``` Make executable: ```bash chmod +x /workspace/artifacts/server.js ``` ### 3. Create Startup Script Save this as `/workspace/artifacts/start-server.sh`: ```bash #!/bin/sh set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" echo "Starting artifacts server..." # Start Node.js server in background node server.js > /tmp/server.log 2>&1 & NODE_PID=$! # Wait for server to be ready sleep 2 # Start cloudflare tunnel echo "Starting Cloudflare Tunnel..." cloudflared tunnel --url http://localhost:8080 2>&1 | tee /tmp/cloudflared.log & TUNNEL_PID=$! # Wait for tunnel to establish sleep 5 # Extract and display public URL PUBLIC_URL=$(grep -o 'https://.*\.trycloudflare\.com' /tmp/cloudflared.log | head -1) if [ -n "$PUBLIC_URL" ]; then echo "" echo "==========================================" echo "Artifacts server is running!" echo "==========================================" echo "Public URL: $PUBLIC_URL" echo "Files directory: $SCRIPT_DIR/files/" echo "" echo "Add ?ws=true to any URL for live reload" echo "Example: $PUBLIC_URL/test.html?ws=true" echo "==========================================" echo "" echo "$PUBLIC_URL" > /tmp/artifacts-url.txt else echo "Warning: Could not extract public URL" fi # Keep script running cleanup() { echo "Shutting down..." kill $NODE_PID 2>/dev/null || true kill $TUNNEL_PID 2>/dev/null || true exit 0 } trap cleanup INT TERM wait $NODE_PID $TUNNEL_PID ``` Make executable: ```bash chmod +x /workspace/artifacts/start-server.sh ``` ## Directory Structure ``` /workspace/artifacts/ ├── server.js # Node.js server ├── start-server.sh # Startup script ├── package.json # Dependencies ├── node_modules/ # Installed packages └── files/ # PUT YOUR ARTIFACTS HERE ├── 2025-12-14-demo/ │ ├── index.html │ ├── style.css │ └── logo.png ├── 2025-12-15-chart/ │ └── index.html └── test.html (standalone OK) ``` ## Usage ### Starting the Server ```bash cd /workspace/artifacts ./start-server.sh ``` This will: 1. Start Node.js server on localhost:8080 2. Create Cloudflare Tunnel with public URL 3. Print the URL (e.g., `https://random-words-123.trycloudflare.com`) 4. Save URL to `/tmp/artifacts-url.txt` **Note:** URL changes every time you restart (free Cloudflare Tunnel limitation). ### Creating Artifacts **Folder organization:** - Create one subfolder per artifact: `$(date +%Y-%m-%d)-description/` - Put main file as `index.html` for clean URLs - Include images, CSS, JS, data in same folder - CDN resources (Tailwind, Three.js, etc.) work fine **Example:** ```bash mkdir -p /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard cat > /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard/index.html << 'EOF'