Created
August 8, 2025 19:51
-
-
Save minanagehsalalma/c5894be53240fd05fe34d1d94704162a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (async () => { | |
| const sleep = (ms) => new Promise(r => setTimeout(r, ms)); | |
| // ---------- Deep selectors (shadow roots + same-origin iframes) ---------- | |
| const deepAll = (sel) => { | |
| const out = []; | |
| const visit = (root) => { | |
| if (!root) return; | |
| out.push(...root.querySelectorAll(sel)); | |
| for (const el of root.querySelectorAll('*')) if (el.shadowRoot) visit(el.shadowRoot); | |
| }; | |
| visit(document); | |
| for (const f of document.querySelectorAll('iframe')) { | |
| try { if (f.contentDocument) visit(f.contentDocument); } catch {} | |
| } | |
| return out; | |
| }; | |
| // ---------- Best-way reader: CodeMirror API, else Monaco, else DOM ------- | |
| const readFromCodeMirrorAPI = () => { | |
| for (const el of deepAll('.cm-editor')) { | |
| const view = el.cmView || el.view; | |
| if (view?.state?.doc) { | |
| try { return view.state.doc.toString(); } catch {} | |
| } | |
| } | |
| return ''; | |
| }; | |
| const readFromMonacoAPI = () => { | |
| try { | |
| const models = window.monaco?.editor?.getModels?.() || []; | |
| if (!models.length) return ''; | |
| // Choose the model with the largest text (usually the active one) | |
| const m = models.reduce((a,b)=> (a?.getValue().length||0) > (b?.getValue().length||0) ? a : b); | |
| return m?.getValue() || ''; | |
| } catch { return ''; } | |
| }; | |
| const readVisibleText = () => { | |
| // fallback DOM text | |
| const monaco = deepAll('.view-lines')[0]; | |
| if (monaco) { | |
| return [...monaco.querySelectorAll('.view-line')].map(el => (el.textContent||'').replace(/\u00A0/g,' ')).join('\n'); | |
| } | |
| const cm = deepAll('.cm-content')[0]; | |
| if (cm) { | |
| return [...cm.querySelectorAll('.cm-line')].map(el => el.textContent||'').join('\n'); | |
| } | |
| let best = ''; | |
| deepAll('pre, code').forEach(n => { const t = (n.innerText||n.textContent||''); if (t.length>best.length) best = t; }); | |
| return best; | |
| }; | |
| // Scroller helper (for virtualization forcing; API path usually avoids this) | |
| const getScroller = () => | |
| deepAll('.monaco-scrollable-element')[0]?.parentElement || | |
| deepAll('.cm-scroller')[0] || | |
| deepAll('.overflow-guard, [class*="scroll"]')[0]; | |
| // Smarter merge that avoids duplicated “file starts over” issues | |
| function smartMerge(acc, chunk) { | |
| if (!acc) return chunk || ''; | |
| if (!chunk) return acc; | |
| acc = acc.replace(/\u00A0/g,' '); | |
| chunk = chunk.replace(/\u00A0/g,' '); | |
| if (chunk.includes(acc)) return chunk; // chunk is superset | |
| if (acc.includes(chunk)) return acc; // chunk adds nothing | |
| // Try to find any suffix of acc inside chunk (not just at start) | |
| const window = acc.slice(-4000); | |
| for (let len = Math.min(2000, window.length, chunk.length); len >= 200; len -= 50) { | |
| const needle = window.slice(-len); | |
| const idx = chunk.indexOf(needle); | |
| if (idx !== -1) return acc + chunk.slice(idx + len); | |
| } | |
| // If chunk looks like it restarted from top and is large, prefer the longer | |
| if (chunk.slice(0, 1000) === acc.slice(0, 1000) && chunk.length > acc.length * 0.9) { | |
| return chunk; | |
| } | |
| // Last resort: append with a newline (better than interleaving) | |
| return acc + (acc.endsWith('\n') ? '' : '\n') + chunk; | |
| } | |
| // Read entire open file, preferring APIs; scroll only if needed | |
| async function readWholeFile() { | |
| let txt = readFromCodeMirrorAPI() || readFromMonacoAPI(); | |
| if (txt && txt.length > 0) return txt; | |
| // DOM fallback with scrolling | |
| const scroller = getScroller(); | |
| if (!scroller) return readVisibleText(); | |
| scroller.scrollTop = 0; await sleep(120); | |
| let acc = '', lastLen = -1, guard = 0; | |
| const step = Math.max(40, Math.floor(scroller.clientHeight * 0.7)); | |
| const endBy = performance.now() + 15000; | |
| while (performance.now() < endBy && guard++ < 1000) { | |
| const chunk = readVisibleText(); | |
| acc = smartMerge(acc, chunk); | |
| if (acc.length === lastLen && scroller.scrollTop + scroller.clientHeight + 2 >= scroller.scrollHeight) break; | |
| lastLen = acc.length; | |
| const next = Math.min(scroller.scrollTop + step, scroller.scrollHeight); | |
| scroller.scrollTo(0, next); | |
| await sleep(70); | |
| } | |
| // One last capture at bottom and then back to top to fill head | |
| acc = smartMerge(acc, readVisibleText()); | |
| scroller.scrollTop = 0; await sleep(120); | |
| acc = smartMerge(readVisibleText(), acc); | |
| return acc.trimEnd(); | |
| } | |
| // Optional post-process: if file is JSON and looks like two JSONs glued, | |
| // trim to the first well-balanced root object. | |
| function fixJSONIfNeeded(path, text) { | |
| if (!/\.json$/i.test(path)) return text; | |
| // quick thumbs rule: if it parses, keep; else try to trim to first balanced {} | |
| try { JSON.parse(text); return text; } catch {} | |
| let depth = 0, inStr = false, esc = false, end = -1; | |
| for (let i = 0; i < text.length; i++) { | |
| const ch = text[i]; | |
| if (inStr) { | |
| if (esc) esc = false; | |
| else if (ch === '\\') esc = true; | |
| else if (ch === '"') inStr = false; | |
| } else { | |
| if (ch === '"') inStr = true; | |
| else if (ch === '{') depth++; | |
| else if (ch === '}') { depth--; if (depth === 0) { end = i + 1; break; } } | |
| } | |
| } | |
| if (end > 0) { | |
| const trimmed = text.slice(0, end); | |
| try { JSON.parse(trimmed); return trimmed; } catch {} | |
| } | |
| return text; // give up | |
| } | |
| // ----------------- File tree crawler (don’t skip folders) ----------------- | |
| const panel = document.querySelector('[id*="content-files"]') || document.body; | |
| const rowSel = '[role="button"].group'; | |
| const nameFromRow = r => r.querySelector('span.flex-1')?.textContent?.trim() || r.innerText.trim(); | |
| const isDir = r => !!r.querySelector('[title="Reference directory in chat"]') || | |
| /rotate/.test(r.querySelector('svg')?.getAttribute('class') || ''); | |
| const levelOf = r => { | |
| const ind = r.querySelector('[style*="margin-left"]'); | |
| const px = ind ? parseInt((ind.style.marginLeft || '0'), 10) : 0; | |
| return Math.round(px / 16); | |
| }; | |
| async function expandEverything() { | |
| let seenCount = -1, tries = 0; | |
| while (tries++ < 20) { | |
| // open all closed dirs currently visible | |
| for (const r of panel.querySelectorAll(rowSel)) { | |
| if (!isDir(r)) continue; | |
| const svg = r.querySelector('svg'); | |
| const open = /rotate-90/.test(svg?.getAttribute('class') || ''); | |
| if (!open) { r.click(); await sleep(120); } | |
| } | |
| // scroll down a "page" | |
| const before = panel.scrollTop; | |
| panel.scrollTop += Math.max(100, panel.clientHeight - 60); | |
| await sleep(180); | |
| const count = panel.querySelectorAll(rowSel).length; | |
| if (count === seenCount && panel.scrollTop === before) break; // stable | |
| seenCount = count; | |
| } | |
| // sweep back to top once more | |
| panel.scrollTop = 0; await sleep(200); | |
| for (const r of panel.querySelectorAll(rowSel)) { | |
| if (isDir(r)) { | |
| const svg = r.querySelector('svg'); const open = /rotate-90/.test(svg?.getAttribute('class') || ''); | |
| if (!open) { r.click(); await sleep(120); } | |
| } | |
| } | |
| } | |
| function listFiles() { | |
| const rows = [...panel.querySelectorAll(rowSel)]; | |
| const files = []; const stack = []; | |
| for (const row of rows) { | |
| const lvl = levelOf(row); | |
| while (stack.length && stack[stack.length - 1].level >= lvl) stack.pop(); | |
| const name = nameFromRow(row); | |
| if (isDir(row)) { stack.push({ name, level: lvl }); continue; } | |
| const path = [...stack.map(s => s.name), name].join('/'); | |
| files.push({ row, name, path }); | |
| } | |
| return files; | |
| } | |
| // ----------------------------- RUN --------------------------------------- | |
| await expandEverything(); | |
| const files = listFiles(); | |
| console.log('Files discovered:', files.length, files.map(f => f.path)); | |
| const outputs = {}; | |
| for (const f of files) { | |
| f.row.click(); await sleep(250); | |
| let txt = await readWholeFile(); | |
| txt = fixJSONIfNeeded(f.path, txt); | |
| outputs[f.path] = txt || ''; | |
| console.log('✔', f.path, outputs[f.path].length, 'chars'); | |
| } | |
| // ----------------------- Download as ZIP ---------------------------------- | |
| const JSZip = (await import('https://cdn.jsdelivr.net/npm/[email protected]/+esm')).default; | |
| const zip = new JSZip(); | |
| Object.entries(outputs).forEach(([p, c]) => zip.file(p, c ?? '')); | |
| const blob = await zip.generateAsync({ type: 'blob' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = (document.title.replace(/\s+/g, '_') || 'project') + '.zip'; | |
| document.body.append(a); a.click(); a.remove(); | |
| const suspicious = Object.entries(outputs).filter(([,c]) => !c || c.length < 50).map(([p]) => p); | |
| if (suspicious.length) console.warn('Potentially incomplete:', suspicious); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment