|
| 1 | +<!doctype html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | + <meta charset="utf-8" /> |
| 5 | + <title>Browser IDE — Live GitHub Runner</title> |
| 6 | + <style> |
| 7 | + body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; display:flex; height:100vh; } |
| 8 | + #left { width: 50%; border-right: 1px solid #ddd; display:flex; flex-direction:column; } |
| 9 | + #right { width: 50%; display:flex; flex-direction:column; } |
| 10 | + header { padding:10px; background:#f6f6f6; border-bottom:1px solid #eaeaea; } |
| 11 | + textarea { flex:1; width:100%; padding:10px; box-sizing:border-box; font-family: monospace; font-size:13px; } |
| 12 | + #controls { padding:10px; display:flex; gap:8px; flex-wrap:wrap; } |
| 13 | + input, button, select { padding:6px; font-size:14px; } |
| 14 | + pre { margin:0; padding:10px; background:#111; color:#0f0; height:50vh; overflow:auto; font-family: monospace; font-size:12px; } |
| 15 | + </style> |
| 16 | +</head> |
| 17 | +<body> |
| 18 | + <div id="left"> |
| 19 | + <header> |
| 20 | + <strong>Browser IDE (minimal)</strong> |
| 21 | + <div id="controls"> |
| 22 | + <input id="token" placeholder="GitHub PAT" size="36" /> |
| 23 | + <button id="connectBtn">Connect WS</button> |
| 24 | + <select id="workspaces"></select> |
| 25 | + </div> |
| 26 | + <div id="repoControls"> |
| 27 | + <input id="owner" placeholder="owner" /> |
| 28 | + <input id="repo" placeholder="repo" /> |
| 29 | + <input id="branch" placeholder="branch (optional)" /> |
| 30 | + <button id="cloneBtn">Clone</button> |
| 31 | + </div> |
| 32 | + </header> |
| 33 | + <textarea id="editor" placeholder="Open or create a file, edit, then Save to workspace"></textarea> |
| 34 | + <div style="padding:8px;display:flex;gap:8px;"> |
| 35 | + <input id="filePath" placeholder="path/inside/repo.txt" style="flex:1"/> |
| 36 | + <button id="saveFileBtn">Save to workspace</button> |
| 37 | + <button id="commitBtn">Commit</button> |
| 38 | + <button id="pushBtn">Push</button> |
| 39 | + <button id="prBtn">Create PR</button> |
| 40 | + </div> |
| 41 | + </div> |
| 42 | + |
| 43 | + <div id="right"> |
| 44 | + <header><strong>Live Runner Console</strong></header> |
| 45 | + <pre id="console">disconnected</pre> |
| 46 | + </div> |
| 47 | + |
| 48 | +<script> |
| 49 | +(function(){ |
| 50 | + let ws; |
| 51 | + let token; |
| 52 | + let currentWorkspaceId = null; |
| 53 | + const consoleEl = document.getElementById('console'); |
| 54 | + const workspacesSelect = document.getElementById('workspaces'); |
| 55 | + |
| 56 | + function log(level, text){ |
| 57 | + const t = `[${new Date().toISOString()}] ${text}`; |
| 58 | + consoleEl.textContent += "\\n" + t; |
| 59 | + consoleEl.scrollTop = consoleEl.scrollHeight; |
| 60 | + } |
| 61 | + |
| 62 | + document.getElementById('connectBtn').addEventListener('click', () => { |
| 63 | + token = document.getElementById('token').value.trim(); |
| 64 | + if(!token) { alert('enter PAT'); return; } |
| 65 | + ws = new WebSocket(`ws://${location.host}`); |
| 66 | + ws.onopen = () => { |
| 67 | + consoleEl.textContent = ''; |
| 68 | + log('info','ws open; sending auth'); |
| 69 | + ws.send(JSON.stringify({ type: 'auth', token })); |
| 70 | + }; |
| 71 | + ws.onmessage = (ev) => { |
| 72 | + try { |
| 73 | + const m = JSON.parse(ev.data); |
| 74 | + if(m.type === 'log') { |
| 75 | + log(m.level, m.text); |
| 76 | + } else if(m.type === 'result') { |
| 77 | + log('result', JSON.stringify(m.data)); |
| 78 | + if(m.action === 'clone' && m.data.workspaceId){ |
| 79 | + const opt = document.createElement('option'); |
| 80 | + opt.value = m.data.workspaceId; |
| 81 | + opt.text = `${m.data.workspaceId} (${m.data.path})`; |
| 82 | + workspacesSelect.appendChild(opt); |
| 83 | + workspacesSelect.value = m.data.workspaceId; |
| 84 | + currentWorkspaceId = m.data.workspaceId; |
| 85 | + } |
| 86 | + } else if(m.type === 'error') { |
| 87 | + log('error', m.message); |
| 88 | + } else { |
| 89 | + log('info', JSON.stringify(m)); |
| 90 | + } |
| 91 | + } catch(e){ |
| 92 | + log('error', ev.data); |
| 93 | + } |
| 94 | + }; |
| 95 | + ws.onclose = () => log('info','ws closed'); |
| 96 | + ws.onerror = (e)=> log('error', 'ws error'); |
| 97 | + }); |
| 98 | + |
| 99 | + document.getElementById('cloneBtn').addEventListener('click', () => { |
| 100 | + const owner = document.getElementById('owner').value.trim(); |
| 101 | + const repo = document.getElementById('repo').value.trim(); |
| 102 | + const branch = document.getElementById('branch').value.trim(); |
| 103 | + if(!owner || !repo) { alert('owner & repo required'); return; } |
| 104 | + ws.send(JSON.stringify({ type:'run', action:'clone', payload: { owner, repo, branch } })); |
| 105 | + }); |
| 106 | + |
| 107 | + document.getElementById('saveFileBtn').addEventListener('click', () => { |
| 108 | + const filePath = document.getElementById('filePath').value.trim(); |
| 109 | + const content = document.getElementById('editor').value; |
| 110 | + const wsId = workspacesSelect.value; |
| 111 | + if(!wsId) { alert('select workspace'); return; } |
| 112 | + ws.send(JSON.stringify({ type:'run', action:'write', payload: { workspaceId: wsId, path: filePath, content } })); |
| 113 | + }); |
| 114 | + |
| 115 | + document.getElementById('commitBtn').addEventListener('click', () => { |
| 116 | + const wsId = workspacesSelect.value; |
| 117 | + if(!wsId) { alert('select workspace'); return; } |
| 118 | + const msg = prompt('Commit message', 'update from web IDE'); |
| 119 | + ws.send(JSON.stringify({ type:'run', action:'commit', payload: { workspaceId: wsId, message: msg } })); |
| 120 | + }); |
| 121 | + |
| 122 | + document.getElementById('pushBtn').addEventListener('click', () => { |
| 123 | + const wsId = workspacesSelect.value; |
| 124 | + const branch = prompt('Branch to push to (remote)', 'main'); |
| 125 | + if(!wsId) { alert('select workspace'); return; } |
| 126 | + ws.send(JSON.stringify({ type:'run', action:'push', payload: { workspaceId: wsId, branch } })); |
| 127 | + }); |
| 128 | + |
| 129 | + document.getElementById('prBtn').addEventListener('click', () => { |
| 130 | + const wsId = workspacesSelect.value; |
| 131 | + if(!wsId) { alert('select workspace'); return; } |
| 132 | + const head = prompt('Head branch (your branch)', 'feature-branch'); |
| 133 | + const base = prompt('Base branch', 'main'); |
| 134 | + const title = prompt('PR title', 'Automated PR from web IDE'); |
| 135 | + const body = prompt('PR body (optional)', ''); |
| 136 | + ws.send(JSON.stringify({ type:'run', action:'pr', payload: { workspaceId: wsId, headBranch: head, baseBranch: base, title, body } })); |
| 137 | + }); |
| 138 | + |
| 139 | +})(); |
| 140 | +</script> |
| 141 | +</body> |
| 142 | +</html> |
0 commit comments