mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
1 line
18 KiB
JSON
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":1757532729808,"settings":{"model":"openai/gpt-5","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"This extension is automatically included in extension.html of every sune. In order to use it you need to put your GitHub token in account settings, then give the file path, inside a repo (your own repo) youd like the sune to be placed in (like in the url above). Then hit save, hit refresh icon -> upload and your sune will be uploaded to your repo.","html":"<div id=\"githubSyncSune\" class=\"p-4 mx-4 mb-4 bg-gray-50/50 border rounded-lg\">\n<!-- SuneBuilderGPT: GitHub Sync v2.2.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(() => {\nconst HIDE_UI = true\n\nconst 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\nif (!el.root || el.root.dataset.init) return;\nel.root.dataset.init = '1';\n\nif (!window.SUNE?.active || !window.USER) return el.log && (el.log.textContent = 'ERROR: Core environment not found.');\n\nconst 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\nconst log = m => el.log && !HIDE_UI && (el.log.textContent += `[${new Date().toLocaleTimeString()}] ${m}\\n`, el.log.scrollTop = el.log.scrollHeight);\nconst hasPat = () => !!window.USER?.PAT;\nconst parseGhPath = p => (p || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\nconst lucideRender = n => window.lucide?.createIcons({ nodes: n });\nconst formatTimeDiff = ms => {\n const s = Math.abs(Math.floor(ms / 1000)), m = Math.floor(s / 60), h = Math.floor(m / 60), d = Math.floor(h / 24);\n const p = [];\n if(d > 0) p.push(`${d}d`);\n if(h % 24 > 0) p.push(`${h % 24}h`);\n if(m % 60 > 0) p.push(`${m % 60}m`);\n if(s % 60 > 0 && p.length < 2) p.push(`${s % 60}s`);\n return p.slice(0, 2).join(' ') || '0s';\n};\n\nconst 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\nconst setBusy = (isBusy) => {\n state.busy = isBusy;\n el.mainBtn?.querySelector('svg')?.classList.toggle('animate-spin', isBusy);\n};\n\nconst 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 cPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}?ref=\\${encodeURIComponent(branch)}\\`;\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 \n let remoteUpdatedAt = null;\n try {\n const suneArr = JSON.parse(decodeURIComponent(escape(atob(res.data.content))));\n if (suneArr?.[0]?.updatedAt) remoteUpdatedAt = suneArr[0].updatedAt;\n } catch (e) { return self.postMessage({ type: 'error', reason: \\`Failed to parse remote sune: \\${e.message}\\` }); }\n\n return self.postMessage({ type: 'status', exists: true, sha: res.data.sha, remoteUpdatedAt, 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\nconst 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\nconst 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 window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\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\nconst performDownload = (isAuto = false) => {\n if (!state.pathInfo) return log('ERROR: Sync path not set.');\n hidePopover();\n if (!isAuto && !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(isAuto ? 'Auto-downloading newer version...' : 'Force download initiated...');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\n};\n\nconst 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, remoteUpdatedAt, discoveredBranch } = e.data;\n if (discoveredBranch && state.pathInfo && !state.pathInfo.branch) {\n state.pathInfo.branch = discoveredBranch;\n log(`Discovered default branch: @${discoveredBranch}`);\n el.info.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${state.pathInfo.owner}/${state.pathInfo.repo}@${state.pathInfo.branch}/${state.pathInfo.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 \n const remoteUpdate = remoteUpdatedAt || 0;\n const localUpdate = window.SUNE.active.updatedAt;\n if (!remoteUpdate) {\n log('⚠️ Remote sune missing \"updatedAt\". Cannot compare versions. Manual sync recommended.');\n updateStatusBadge('desynced');\n return;\n }\n\n const isOutOfSync = Math.abs(remoteUpdate - localUpdate) > 10000;\n\n if (isOutOfSync) {\n const diff = remoteUpdate - localUpdate;\n log(`Desynced by ${formatTimeDiff(diff)}.`);\n log(`> Remote: ${new Date(remoteUpdate).toLocaleString()}`);\n log(`> Local: ${new Date(localUpdate).toLocaleString()}`);\n if (diff > 0) {\n log('Remote is newer. Initiating auto-download...');\n performDownload(true);\n return;\n }\n log('Local is newer. Ready to upload.');\n updateStatusBadge('desynced');\n } else { \n log('✅ Sune is in sync.'); \n updateStatusBadge('synced'); \n }\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]);\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 state.remoteSha = e.data.newSha;\n log('✅ Sync successful!'); setBusy(false); updateStatusBadge('synced');\n }\n};\n\nconst 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\nconst 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\nconst 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\nconst 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(false));\n};\n\nconst 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};\nconst hidePopover = () => state.popover?.classList.add('hidden');\n\nfunction init() {\n if (HIDE_UI) el.root.style.display = 'none';\n log(`GitHub Sync v2.2.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 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\n startCheck();\n lucideRender();\n};\ninit();\n})();\n</script>\n","extension_html":"","hide_composer":false,"presence_penalty":"","max_tokens":""},"storage":{}}] |