← back to Claude Webdev Accelerator
preview: gated repo viewer at accelerator.agentabrams.com (read-only, .git/dotfiles excluded); add to pm2 fleet
a481cbac8ffdd1dd0d2338d457fe86bd731cbab6 · 2026-07-16 07:24:18 -0700 · Steve
Files touched
M ecosystem.config.jsA scripts/repo-viewer.js
Diff
commit a481cbac8ffdd1dd0d2338d457fe86bd731cbab6
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 16 07:24:18 2026 -0700
preview: gated repo viewer at accelerator.agentabrams.com (read-only, .git/dotfiles excluded); add to pm2 fleet
---
ecosystem.config.js | 17 ++++++++++++++++
scripts/repo-viewer.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 71 insertions(+)
diff --git a/ecosystem.config.js b/ecosystem.config.js
index 74c4e22..1ca55bb 100644
--- a/ecosystem.config.js
+++ b/ecosystem.config.js
@@ -31,4 +31,21 @@ for (const { slug, app, proxy } of APPS) {
autorestart: true, max_restarts: 20, min_uptime: '5s',
});
}
+
+// Accelerator repo viewer (serves this repo read-only) + its gate-proxy.
+apps.push({
+ name: 'prev-accelerator',
+ cwd: ACCEL,
+ script: 'scripts/repo-viewer.js',
+ env: { PORT: '3906', ROOT: ACCEL },
+ autorestart: true, max_restarts: 20, min_uptime: '5s',
+});
+apps.push({
+ name: 'gate-accelerator',
+ cwd: ACCEL,
+ script: 'scripts/gate-proxy.js',
+ env: { PORT: '4906', UPSTREAM: '127.0.0.1:3906', GATE_AUTH: GATE },
+ autorestart: true, max_restarts: 20, min_uptime: '5s',
+});
+
module.exports = { apps };
diff --git a/scripts/repo-viewer.js b/scripts/repo-viewer.js
new file mode 100644
index 0000000..ffe59b1
--- /dev/null
+++ b/scripts/repo-viewer.js
@@ -0,0 +1,54 @@
+// Zero-dep read-only repo viewer: directory listing + file view for a ROOT dir.
+// Safe: rejects path traversal and any segment starting with '.' (blocks .git, .env, .DS_Store).
+// Serves .html as html; text/code as styled <pre>; other types as octet-stream download.
+// Usage: PORT=3906 ROOT=/path/to/repo node repo-viewer.js (run plain; front with gate-proxy)
+const http = require('http'), fs = require('fs'), path = require('path');
+const PORT = parseInt(process.env.PORT || '3906', 10);
+const ROOT = path.resolve(process.env.ROOT || process.cwd());
+
+const TEXT = new Set(['.md', '.txt', '.js', '.mjs', '.cjs', '.json', '.sh', '.html', '.css', '.yml', '.yaml', '.gitignore', '.conf']);
+const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+const page = (title, body) => `<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>${esc(title)}</title>
+<style>body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#fafafa}
+header{padding:12px 20px;border-bottom:1px solid #e6e6e6;background:#fff;position:sticky;top:0}
+a{color:#0645ad;text-decoration:none}a:hover{text-decoration:underline}
+.wrap{padding:16px 20px;max-width:1000px}ul{list-style:none;padding:0;margin:0}
+li{padding:4px 0;border-bottom:1px solid #f0f0f0}.d{font-weight:600}
+pre{background:#fff;border:1px solid #e6e6e6;border-radius:8px;padding:16px;overflow:auto;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
+.crumb{color:#888;font-size:12px}</style>
+<header><b>claude-webdev-accelerator</b> <span class=crumb>${esc(title)}</span></header><div class=wrap>${body}</div>`;
+
+function safe(reqPath) {
+ const rel = decodeURIComponent(reqPath.replace(/^\/+/, '')).replace(/\/+$/, '');
+ if (rel.split('/').some(seg => seg.startsWith('.') || seg === '')) return rel === '' ? ROOT : null;
+ const abs = path.resolve(ROOT, rel);
+ if (abs !== ROOT && !abs.startsWith(ROOT + path.sep)) return null;
+ return abs;
+}
+
+http.createServer((req, res) => {
+ const u = new URL(req.url, 'http://x');
+ const abs = safe(u.pathname);
+ if (abs === null) { res.writeHead(403); return res.end('forbidden'); }
+ let st; try { st = fs.statSync(abs); } catch { res.writeHead(404); return res.end('not found'); }
+ const rel = path.relative(ROOT, abs) || '/';
+ if (st.isDirectory()) {
+ const up = rel === '/' ? '' : `<li><a href="/${esc(path.dirname(rel) === '.' ? '' : path.dirname(rel))}">../</a></li>`;
+ const items = fs.readdirSync(abs).filter(n => !n.startsWith('.')).sort((a, b) => {
+ const ad = fs.statSync(path.join(abs, a)).isDirectory(), bd = fs.statSync(path.join(abs, b)).isDirectory();
+ return ad === bd ? a.localeCompare(b) : (ad ? -1 : 1);
+ }).map(n => {
+ const d = fs.statSync(path.join(abs, n)).isDirectory();
+ const href = '/' + (rel === '/' ? '' : rel + '/') + encodeURIComponent(n);
+ return `<li${d ? ' class=d' : ''}><a href="${href}">${esc(n)}${d ? '/' : ''}</a></li>`;
+ }).join('');
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ return res.end(page(rel, `<ul>${up}${items}</ul>`));
+ }
+ const ext = path.extname(abs).toLowerCase() || path.basename(abs).toLowerCase();
+ const buf = fs.readFileSync(abs);
+ if (ext === '.html') { res.writeHead(200, { 'Content-Type': 'text/html' }); return res.end(buf); }
+ if (TEXT.has(ext)) { res.writeHead(200, { 'Content-Type': 'text/html' }); return res.end(page(rel, `<pre>${esc(buf.toString('utf8'))}</pre>`)); }
+ res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${path.basename(abs)}"` });
+ res.end(buf);
+}).listen(PORT, '127.0.0.1', function () { console.log(`[repo-viewer] :${PORT} serving ${ROOT}`); });
← cb4ca5c preview: pm2 ecosystem for the 5-app + 5-gate-proxy preview
·
back to Claude Webdev Accelerator
·
shopify: read-only catalog puller + normalize sample variant 1be34c6 →