Files
store/sync.sune

1 line
18 KiB
JSON

[{"id":"csx7463","name":"Github Sync","pinned":false,"avatar":"data:image/webp;base64,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","url":"gh://sune-org/store/sync.sune","updatedAt":1757207242376,"settings":{"model":"openai/gpt-5","temperature":"","top_p":"","top_k":"","frequency_penalty":"","presence_penalty":"","repetition_penalty":"","min_p":"","top_a":"","max_tokens":"","verbosity":"","reasoning_effort":"default","system_prompt":"To use this simply put this in the extension.html of your sune:\n\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />","html":"<div id=\"githubSyncSune\" class=\"p-4 mx-4 mb-4 bg-gray-50/50 border rounded-lg\">\n <!-- SuneBuilderGPT: GitHub Sync v1.9.0 -->\n <div class=\"space-y-3\">\n <div>\n <p id=\"infoText\" class=\"p-3 text-sm text-center text-gray-600 border border-dashed rounded-md break-all\"></p>\n </div>\n <div>\n <label for=\"logArea\" class=\"block mb-1 text-xs font-medium text-gray-500\">Sync Logs</label>\n <pre id=\"logArea\" class=\"w-full h-48 p-2.5 overflow-auto font-mono text-xs text-gray-600 bg-white border border-gray-200 rounded-md whitespace-pre-wrap\"></pre>\n </div>\n </div>\n</div>\n\n<script>\n'use strict';\n(() => {\n const HIDE_UI = true; // Set true to hide the sync UI panel below the sune settings.\n\n const el = {\n root: document.getElementById('githubSyncSune'),\n info: document.getElementById('infoText'),\n log: document.getElementById('logArea'),\n mainBtn: document.getElementById('syncSune'),\n suneModal: document.getElementById('suneModal'),\n };\n\n if (!el.root || el.root.dataset.init) return;\n el.root.dataset.init = '1';\n\n if (!window.SUNE?.active || !window.USER) return el.log && (el.log.textContent = 'ERROR: Core environment not found.');\n\n const state = {\n busy: false,\n remoteSha: undefined,\n pathInfo: null,\n worker: null,\n timeout: null,\n popover: null,\n badge: document.createElement('span'),\n listeners: new Map()\n };\n\n const log = m => el.log && !HIDE_UI && (el.log.textContent += `[${new Date().toLocaleTimeString()}] ${m}\\n`, el.log.scrollTop = el.log.scrollHeight);\n const hasPat = () => !!window.USER?.PAT;\n const parseGhPath = p => (p || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\n const lucideRender = n => window.lucide?.createIcons({ nodes: n });\n\n const updateStatusBadge = (status = 'idle') => {\n if (!state.badge) return;\n const b = state.badge;\n b.className = 'absolute top-0 right-0 block h-2.5 w-2.5 rounded-full ring-2 ring-white';\n b.classList.toggle('hidden', status === 'hidden');\n switch (status) {\n case 'checking': b.classList.add('bg-blue-500', 'animate-pulse'); break;\n case 'synced': b.classList.add('bg-green-500'); break;\n case 'desynced': case 'error': b.classList.add('bg-red-500'); break;\n case 'idle': b.classList.add('bg-gray-400'); break;\n }\n };\n \n const setBusy = (isBusy) => {\n state.busy = isBusy;\n el.mainBtn?.querySelector('svg')?.classList.toggle('animate-spin', isBusy);\n };\n\n const createWorker = () => {\n const c = `\n const GITHUB_API = 'https://api.github.com';\n async function gh(method, path, token, body = null) {\n const h = { 'Accept': 'application/vnd.github.v3+json', ...(token && {'Authorization': \\`token \\${token}\\`}), ...(body && {'Content-Type': 'application/json'}) };\n try {\n const r = await fetch(GITHUB_API + path, { method, headers: h, body: body ? JSON.stringify(body) : null });\n const d = r.status === 204 ? {} : await r.json().catch(() => ({}));\n return { ok: r.ok, status: r.status, data: d };\n } catch (e) { return { ok: false, status: 0, error: e.message }; }\n }\n self.onmessage = async ({ data: { type, pat, pathInfo, contentB64, sha, name } }) => {\n let { owner, repo, branch, path } = pathInfo;\n if (type === 'check') {\n if (!branch) {\n const repoRes = await gh('GET', \\`/repos/\\${owner}/\\${repo}\\`, pat);\n if (repoRes.ok) branch = repoRes.data.default_branch;\n else return self.postMessage({ type: 'error', reason: \\`Repo lookup failed: \\${repoRes.data?.message || 'Error'}\\` });\n }\n const ref = \\`?ref=\\${encodeURIComponent(branch)}\\`;\n const cPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${ref}\\`;\n const res = await gh('GET', cPath, pat);\n if (res.status === 404) return self.postMessage({ type: 'status', exists: false, discoveredBranch: branch });\n if ((res.status === 403 || res.status === 401) && !pat) return self.postMessage({ type: 'error', reason: 'Private repo or rate limit. PAT required.' });\n if (!res.ok) return self.postMessage({ type: 'error', reason: \\`API Error (\\${res.status}): \\${res.data?.message || res.error}\\` });\n const comPath = \\`/repos/\\${owner}/\\${repo}/commits?sha=\\${branch}&path=\\${encodeURIComponent(path)}&per_page=1\\`;\n const comRes = await gh('GET', comPath, pat);\n const date = comRes.ok && comRes.data.length > 0 ? comRes.data[0].commit.committer.date : null;\n return self.postMessage({ type: 'status', exists: true, sha: res.data.sha, date, discoveredBranch: branch });\n }\n const cPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\`;\n if (type === 'fetch') {\n const res = await gh('GET', \\`\\${cPath}?ref=\\${encodeURIComponent(branch)}\\`, pat);\n return res.ok ? self.postMessage({ type: 'fetched', contentB64: res.data.content, newSha: res.data.sha })\n : self.postMessage({ type: 'error', reason: \\`Download failed (\\${res.status}): \\${res.data?.message || res.error}\\` });\n }\n if (type === 'sync') {\n if (!pat) return self.postMessage({ type: 'error', reason: 'PAT required to upload.' });\n const msg = sha ? \\`Sync: Update sune '\\${name}'\\` : \\`Sync: Create sune '\\${name}'\\`;\n const res = await gh('PUT', cPath, pat, { message: msg, content: contentB64, sha, ...(branch && { branch }) });\n return res.ok ? self.postMessage({ type: 'synced', newSha: res.data.content.sha })\n : self.postMessage({ type: 'error', reason: res.data?.message || 'Sync failed' });\n }\n };`;\n return new Worker(URL.createObjectURL(new Blob([c], { type: 'application/javascript' })));\n };\n \n const startWorker = (m, t = 20000) => {\n if (!state.worker) return log('ERROR: Worker not running.');\n clearTimeout(state.timeout);\n state.worker.postMessage(m);\n state.timeout = setTimeout(() => {\n log('ERROR: Operation timed out.');\n setBusy(false);\n updateStatusBadge('error');\n cleanup();\n init();\n }, t);\n };\n\n const performUpload = () => {\n if (!state.pathInfo || !hasPat()) return log('ERROR: Sync path not set or PAT missing.');\n hidePopover();\n if (!confirm(\"This will overwrite the remote file on GitHub with your local version. Continue?\")) return;\n setBusy(true); updateStatusBadge('checking');\n log('Preparing sune for upload...');\n try {\n const contentB64 = btoa(unescape(encodeURIComponent(JSON.stringify([window.SUNE.active]))));\n startWorker({ type: 'sync', pat: window.USER.PAT, pathInfo: state.pathInfo, contentB64, name: window.SUNE.active.name, sha: state.remoteSha });\n } catch (e) {\n log(`FATAL: Failed to prepare data. ${e.message}`);\n setBusy(false); updateStatusBadge('error');\n }\n };\n \n const performDownload = () => {\n if (!state.pathInfo) return log('ERROR: Sync path not set.');\n hidePopover();\n if (!confirm(\"This will overwrite your local version with the file from GitHub. All local changes will be lost. Continue?\")) return;\n setBusy(true); updateStatusBadge('checking');\n log('Force download initiated...');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const handleMsg = (e) => {\n clearTimeout(state.timeout);\n const { type, reason } = e.data;\n if (type === 'error') { log(`Worker error: ${reason || 'Unknown'}`); setBusy(false); updateStatusBadge('error'); return; }\n if (type === 'status') {\n const { exists, sha, date, discoveredBranch } = e.data;\n if (discoveredBranch && state.pathInfo && !state.pathInfo.branch) {\n state.pathInfo.branch = discoveredBranch;\n log(`Discovered default branch: @${discoveredBranch}`);\n const { owner, repo, branch, path } = state.pathInfo;\n el.info.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${owner}/${repo}@${branch}/${path}</span>`;\n }\n state.remoteSha = exists ? sha : null;\n const upBtn = state.popover?.querySelector('#suneSyncUploadBtn'), downBtn = state.popover?.querySelector('#suneSyncDownloadBtn');\n if (upBtn) upBtn.disabled = false;\n if (downBtn) downBtn.disabled = !exists;\n setBusy(false);\n if (!exists) { log('File not on GitHub. Out of sync.'); updateStatusBadge('desynced'); return; }\n const remoteUpdate = date ? new Date(date).getTime() : 0, localUpdate = window.SUNE.active.updatedAt;\n const isOutOfSync = Math.abs(remoteUpdate - localUpdate) > 10000;\n if (isOutOfSync) {\n log(`Sune is out of sync. ${remoteUpdate > localUpdate ? 'Remote' : 'Local'} is newer.`);\n updateStatusBadge('desynced');\n } else { log('✅ Sune is in sync.'); updateStatusBadge('synced'); }\n } else if (type === 'fetched') {\n try {\n const suneArr = JSON.parse(decodeURIComponent(escape(atob(e.data.contentB64))));\n if (!suneArr?.[0]?.id) throw new Error(\"Invalid sune format.\");\n Object.assign(window.SUNE.active, suneArr[0], { updatedAt: Date.now() });\n window.SUNE.save(); log('✅ Sune updated from GitHub.');\n state.remoteSha = e.data.newSha;\n ['closeSettings', 'renderSidebar', 'reflectActiveSune'].forEach(fn => window[fn]?.());\n } catch (err) { log(`ERROR processing download: ${err.message}`); updateStatusBadge('error');}\n finally { setBusy(false); updateStatusBadge('synced'); }\n } else if (type === 'synced') {\n window.SUNE.active.updatedAt = Date.now(); window.SUNE.save();\n state.remoteSha = e.data.newSha;\n log('✅ Sync successful!'); setBusy(false); updateStatusBadge('synced');\n }\n };\n\n const startCheck = () => {\n if (state.busy) return;\n const pathArr = parseGhPath(window.SUNE.active.url);\n if (!pathArr) { updateStatusBadge('idle'); return; }\n const [, owner, repo, branch, path] = pathArr;\n state.pathInfo = { owner, repo, branch, path };\n if (!HIDE_UI) el.info.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${owner}/${repo}${branch?`@${branch}`:''}/${path}</span>`;\n setBusy(true); updateStatusBadge('checking');\n log(`Checking remote status... ${branch ? '' : '(finding default branch)'}`);\n startWorker({ type: 'check', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const addListener = (el, ev, h, o) => {\n if (!el) return;\n el.addEventListener(ev, h, o);\n const hs = state.listeners.get(el) || {}; hs[ev] = h; state.listeners.set(el, hs);\n };\n\n const cleanup = () => {\n clearTimeout(state.timeout);\n state.worker?.terminate();\n state.popover?.remove();\n state.listeners.forEach((h, el) => Object.keys(h).forEach(e => el.removeEventListener(e, h[e])));\n state.listeners.clear();\n el.root.dataset.init = '';\n };\n\n const createPopover = () => {\n const p = document.createElement('div');\n p.id = 'suneSyncActionPopover';\n p.className = 'menu-card hidden';\n p.innerHTML = `<button id=\"suneSyncUploadBtn\" class=\"menu-item\" disabled><i data-lucide=\"upload-cloud\" class=\"h-4 w-4\"></i><span>Upload to GitHub</span></button>\n <button id=\"suneSyncDownloadBtn\" class=\"menu-item\" disabled><i data-lucide=\"download-cloud\" class=\"h-4 w-4\"></i><span>Download from GitHub</span></button>`;\n document.body.appendChild(p); state.popover = p; lucideRender([p]);\n addListener(p.querySelector('#suneSyncUploadBtn'), 'click', performUpload);\n addListener(p.querySelector('#suneSyncDownloadBtn'), 'click', performDownload);\n };\n\n const showPopover = btn => {\n if (!state.popover || state.busy) return;\n const r = btn.getBoundingClientRect();\n state.popover.style.top = `${r.bottom + 4}px`;\n state.popover.style.left = `${Math.min(window.innerWidth - 248, Math.max(8, r.left - 120))}px`;\n state.popover.classList.remove('hidden');\n };\n const hidePopover = () => state.popover?.classList.add('hidden');\n\n function init() {\n if (HIDE_UI) el.root.style.display = 'none';\n log(`GitHub Sync v1.9.0 ready for \"${window.SUNE.active.name}\".`);\n \n state.worker = createWorker();\n state.worker.onmessage = handleMsg;\n createPopover();\n \n addListener(el.mainBtn, 'click', e => { e.preventDefault(); e.stopPropagation(); showPopover(el.mainBtn); });\n addListener(document.body, 'click', e => !state.popover?.classList.contains('hidden') && !state.popover?.contains(e.target) && !el.mainBtn?.contains(e.target) && hidePopover(), true);\n\n if (el.mainBtn) {\n el.mainBtn.style.position = 'relative';\n if (!document.getElementById('suneSyncStatusBadge')) el.mainBtn.appendChild(state.badge);\n }\n\n const modalObserver = new MutationObserver(() => {\n const isHidden = el.suneModal.classList.contains('hidden');\n state.badge.classList.toggle('hidden', isHidden);\n if (!isHidden && !state.busy) startCheck();\n });\n if(el.suneModal) modalObserver.observe(el.suneModal, { attributes: true, attributeFilter: ['class'] });\n\n new MutationObserver((_, o) => { if (!document.body.contains(el.root)) { cleanup(); o.disconnect(); modalObserver.disconnect(); } }).observe(document.body, { childList: true, subtree: true });\n lucideRender();\n };\n init();\n})();\n</script>\n","extension_html":"","hide_composer":false},"storage":{}}]