mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
28 lines
17 KiB
JSON
28 lines
17 KiB
JSON
[
|
|
{
|
|
"id": "csx7463",
|
|
"name": "Github Sync",
|
|
"pinned": false,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/sync.sune",
|
|
"updatedAt": 1757100401030,
|
|
"settings": {
|
|
"model": "openai/gpt-5",
|
|
"temperature": 1,
|
|
"top_p": 0.97,
|
|
"top_k": 0,
|
|
"frequency_penalty": 0,
|
|
"presence_penalty": 0,
|
|
"repetition_penalty": 1,
|
|
"min_p": 0,
|
|
"top_a": 0,
|
|
"max_tokens": 0,
|
|
"verbosity": "",
|
|
"reasoning_effort": "default",
|
|
"system_prompt": "",
|
|
"html": "<div id=\"githubSyncSune\" class=\"p-4 mx-4 mb-4 bg-gray-50/50 border rounded-lg\">\n <!-- Sune version v1.7.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 <button id=\"syncBtn\" class=\"inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors\">\n </button>\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-32 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.\n\n const el = {\n root: document.getElementById('githubSyncSune'),\n info: document.getElementById('infoText'),\n btn: document.getElementById('syncBtn'),\n log: document.getElementById('logArea'),\n mainBtn: document.getElementById('syncSune')\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 Sune environment not found.');\n\n const state = {\n busy: false,\n remoteSha: null,\n pathInfo: null,\n forceAction: null, // 'push' or 'pull'\n worker: null,\n timeout: null,\n popover: null,\n pressTimer: null,\n badgeTimer: null,\n badgeCounter: 0,\n listeners: new Map()\n };\n\n const log = (msg) => el.log && !HIDE_UI && (el.log.textContent += `[${new Date().toLocaleTimeString()}] ${msg}\\n`, el.log.scrollTop = el.log.scrollHeight);\n const hasPat = () => !!window.USER?.PAT;\n const parseGhPath = (p) => (p || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\n const lucideRender = (nodes) => window.lucide?.createIcons({ nodes });\n\n const updateBadge = (text) => {\n const badge = document.getElementById('suneSyncBadge');\n if (badge) badge.textContent = text, badge.classList.toggle('hidden', !text);\n };\n\n const stopBadgeTimer = () => {\n if (state.badgeTimer) clearInterval(state.badgeTimer);\n state.badgeTimer = null;\n updateBadge(null);\n };\n\n const startBadgeTimer = (prefix) => {\n stopBadgeTimer();\n state.badgeCounter = 0;\n const update = () => updateBadge(`${prefix} ${++state.badgeCounter}s`);\n update();\n state.badgeTimer = setInterval(update, 1000);\n };\n\n const setBusy = (busy, opPrefix = '') => {\n state.busy = busy;\n el.mainBtn?.querySelector('svg')?.classList.toggle('animate-spin', busy);\n if (busy && opPrefix) startBadgeTimer(opPrefix);\n else if (!busy) stopBadgeTimer();\n };\n\n const updateBtnUI = (action, sha) => {\n if (action !== 'synced') state.remoteSha = sha;\n if (HIDE_UI) return;\n let icon, text, disabled = false;\n switch (action) {\n case 'upload':\n icon = 'upload-cloud';\n disabled = state.busy || !hasPat();\n text = !hasPat() ? 'Upload (PAT Required)' : (sha ? 'Upload Changes' : 'Create on GitHub');\n break;\n case 'synced':\n icon = 'check-circle-2';\n disabled = true;\n text = 'In Sync';\n break;\n default:\n icon = 'help-circle';\n disabled = true;\n text = 'Configure Sync Path';\n }\n el.btn.disabled = disabled;\n el.btn.innerHTML = `<i data-lucide=\"${icon}\" class=\"w-5 h-5 mr-2\"></i><span>${text}</span>`;\n lucideRender([el.btn]);\n };\n\n const createWorker = () => {\n const code = `\n const GITHUB_API = 'https://api.github.com';\n async function gh(m,p,t,b=null){const h={'Accept':'application/vnd.github.v3+json'};if(t)h['Authorization']=\\`token \\${t}\\`;if(b)h['Content-Type']='application/json';const o={method:m,headers:h,body:b?JSON.stringify(b):null};try{const r=await fetch(GITHUB_API+p,o);const d=r.status===204?{}:await r.json().catch(()=>({}));return{ok:r.ok,status:r.status,data:d}}catch(e){return{ok:!1,status:0,error:e.message}}}\n self.onmessage=async e=>{const{type:y,pat:p,pathInfo:i,contentB64:c,sha:s,name:n}=e.data,{owner:o,repo:r,branch:b,path:a}=i,f=b?\\`?ref=\\${encodeURIComponent(b)}\\`:'',h=\\`/repos/\\${o}/\\${r}/contents/\\${a}\\${f}\\`;if(y==='check'){const t=await gh('GET',h,p);if(t.status===404)return self.postMessage({type:'status',exists:!1});if((t.status===403||t.status===401)&&!p)return self.postMessage({type:'error',reason:'Private repo/rate limit. PAT required.'});if(!t.ok)return self.postMessage({type:'error',reason:\\`API Error (\\${t.status}): \\${t.data?.message||t.error}\\`});const e=b||'HEAD',s=\\`/repos/\\${o}/\\${r}/commits?sha=\\${e}&path=\\${encodeURIComponent(a)}&page=1&per_page=1\\`,c=await gh('GET',s,p),u=c.ok&&c.data.length>0?c.data[0].commit.committer.date:null;return self.postMessage({type:'status',exists:!0,sha:t.data.sha,date:u})}if(y==='fetch'){const t=await gh('GET',h,p);return t.ok?self.postMessage({type:'fetched',contentB64:t.data.content,newSha:t.data.sha}):self.postMessage({type:'error',reason:\\`Download failed (\\${t.status}): \\${t.data?.message||t.error}\\`})}if(y==='sync'){if(!p)return self.postMessage({type:'error',reason:'PAT required to upload.'});const t=s?\\`Sync: Update sune '\\${n}'\\`:\\`Sync: Create sune '\\${n}'\\`,d={message:t,content:c,sha:s,...(b&&{branch:b})};self.postMessage({type:'log',msg:\\`Committing to \\${o}/\\${r}...\\`});const a=await gh('PUT',h,p,d);if(a.ok){const t=a.data.content.sha;self.postMessage({type:'log',msg:\\`✅ Sync successful! New SHA: \\${t.substring(0,7)}\\`});return self.postMessage({type:'synced',newSha:t})}self.postMessage({type:'log',msg:\\`ERROR (\\${a.status}): \\${a.data?.message||a.error}\\`});return self.postMessage({type:'error',reason:a.data?.message||'Sync failed'})}};`;\n return new Worker(URL.createObjectURL(new Blob([code], { type: 'application/javascript' })));\n };\n \n const startWorker = (msg, timeout = 20000) => {\n if (!state.worker) return log('ERROR: Worker not running.');\n clearTimeout(state.timeout);\n state.worker.postMessage(msg);\n state.timeout = setTimeout(() => {\n log('ERROR: Operation timed out.');\n setBusy(false);\n cleanup();\n init();\n }, timeout);\n };\n\n const performUpload = () => {\n if (!state.pathInfo) return log('ERROR: Sync path not set.');\n if (!hasPat()) return log('ERROR: GitHub PAT required to upload.');\n setBusy(true, 'Up');\n if (!HIDE_UI) {\n el.btn.disabled = true;\n el.btn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 mr-2 animate-spin\"></i><span>Uploading...</span>`;\n lucideRender([el.btn]);\n }\n log('Preparing sune for upload...');\n try {\n const suneJson = JSON.stringify([window.SUNE.active], null, 2);\n const contentB64 = btoa(unescape(encodeURIComponent(suneJson)));\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);\n }\n };\n \n const performDownload = () => {\n if (!state.pathInfo) return log('ERROR: Sync path not set.');\n log('Force download initiated...');\n setBusy(true, 'Down');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const handleMsg = (e) => {\n clearTimeout(state.timeout);\n const { type, msg, reason } = e.data;\n if (type === 'log') return log(msg);\n if (type === 'error') {\n log(`Worker error: ${reason || 'Unknown'}`);\n setBusy(false);\n updateBtnUI('upload', state.remoteSha);\n return;\n }\n\n if (type === 'status') {\n const { exists, sha, date } = e.data;\n state.remoteSha = sha;\n const action = state.forceAction;\n state.forceAction = null;\n\n if (action === 'push') {\n log('Force sync: pushing local version...');\n return performUpload();\n }\n if (action === 'pull') {\n return performDownload();\n }\n \n if (!exists) {\n log('File not found on GitHub. Ready to create.');\n setBusy(false);\n updateBtnUI('upload', null);\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = date ? new Date(date).getTime() : 0;\n \n if (remoteUpdate > localUpdate + 5000) {\n log('Remote is newer. Auto-downloading...');\n performDownload();\n } else if (localUpdate > remoteUpdate + 5000) {\n log('Local is newer. Ready to upload.');\n setBusy(false);\n updateBtnUI('upload', sha);\n } else {\n log('✅ Sune is in sync.');\n setBusy(false);\n updateBtnUI('synced', sha);\n }\n }\n } else if (type === 'fetched') {\n try {\n const json = decodeURIComponent(escape(atob(e.data.contentB64)));\n const suneArr = JSON.parse(json);\n if (!suneArr?.[0]?.id) throw new Error(\"Invalid sune format.\");\n \n Object.assign(window.SUNE.active, suneArr[0], { updatedAt: Date.now() });\n window.SUNE.save();\n\n log('✅ Sune updated from GitHub.');\n updateBtnUI('synced', e.data.newSha);\n ['closeSettings', 'renderSidebar', 'reflectActiveSune'].forEach(fn => window[fn]?.());\n } catch (err) { log(`ERROR processing download: ${err.message}`); }\n finally { setBusy(false); }\n } else if (type === 'synced') {\n window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\n setBusy(false);\n updateBtnUI('synced', e.data.newSha);\n }\n };\n\n const startCheck = (manual = false, action = null) => {\n state.pathInfo = parseGhPath(window.SUNE.active.url)?.slice(1, 5);\n if (!state.pathInfo) return;\n state.pathInfo = { owner: state.pathInfo[0], repo: state.pathInfo[1], branch: state.pathInfo[2], path: state.pathInfo[3] };\n state.forceAction = action;\n setBusy(true, 'Chk');\n if (manual) log('Checking remote status...');\n startWorker({ type: 'check', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const cleanup = () => {\n clearTimeout(state.timeout);\n clearTimeout(state.pressTimer);\n stopBadgeTimer();\n state.worker?.terminate();\n state.popover?.remove();\n state.listeners.forEach((handler, el) => {\n Object.keys(handler).forEach(evt => el.removeEventListener(evt, handler[evt]));\n });\n state.listeners.clear();\n el.root.dataset.init = '';\n };\n\n const addListener = (element, event, handler) => {\n if (!element) return;\n element.addEventListener(event, handler);\n const handlers = state.listeners.get(element) || {};\n handlers[event] = handler;\n state.listeners.set(element, handlers);\n };\n \n function init() {\n if (HIDE_UI) el.root.style.display = 'none';\n state.worker = createWorker();\n state.worker.onmessage = handleMsg;\n log(`GitHub Sync v1.7.0 ready for \"${window.SUNE.active.name}\".`);\n \n const pathInfoArr = parseGhPath(window.SUNE.active.url);\n if (pathInfoArr) {\n const [, owner, repo, branch, path] = pathInfoArr;\n if (!HIDE_UI) {\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 ? `@<span class=\"font-semibold\">${branch}</span>` : ''}/${path}</span>`;\n }\n updateBtnUI('none', null);\n startCheck(false);\n } else {\n if (!HIDE_UI) el.info.innerHTML = 'Set a `gh://owner/repo@branch/path.sune` URL in Sune settings (click ✺) to enable sync.';\n updateBtnUI('none', null);\n }\n\n addListener(el.btn, 'click', performUpload);\n \n const handleForcePush = (e) => {\n e.preventDefault(); e.stopPropagation();\n if (state.busy) return log('Sync is busy.');\n if (state.pathInfo) {\n if(!HIDE_UI && el.log) el.log.textContent = '';\n startCheck(true, 'push');\n }\n };\n addListener(el.mainBtn, 'click', handleForcePush);\n\n const popover = document.createElement('div');\n popover.id = 'suneSyncActionPopover';\n popover.className = 'menu-card hidden';\n popover.innerHTML = `<button class=\"menu-item\"><i data-lucide=\"download-cloud\" class=\"h-4 w-4\"></i><span>Overwrite local with Remote</span></button>`;\n document.body.appendChild(popover);\n state.popover = popover;\n\n const showPopover = (btn) => {\n if (!popover || state.busy || !state.pathInfo) return;\n const r = btn.getBoundingClientRect();\n popover.style.top = `${r.bottom + 4}px`;\n popover.style.left = `${Math.min(window.innerWidth - 248, Math.max(8, r.left - 120))}px`;\n popover.classList.remove('hidden');\n };\n const hidePopover = () => popover.classList.add('hidden');\n \n addListener(popover.querySelector('button'), 'click', e => {\n e.stopPropagation(); hidePopover();\n if (state.busy) return log('Sync is busy.');\n if (confirm(\"Overwrite local changes with the version from GitHub? This cannot be undone.\")) {\n performDownload();\n }\n });\n\n const startPress = (e) => { e.preventDefault(); state.pressTimer = setTimeout(() => showPopover(el.mainBtn), 500); };\n const cancelPress = () => clearTimeout(state.pressTimer);\n addListener(el.mainBtn, 'touchstart', startPress);\n addListener(el.mainBtn, 'touchend', cancelPress);\n addListener(el.mainBtn, 'touchmove', cancelPress);\n addListener(el.mainBtn, 'contextmenu', (e) => { e.preventDefault(); showPopover(el.mainBtn); });\n addListener(document.body, 'click', (e) => !popover.classList.contains('hidden') && !popover.contains(e.target) && !el.mainBtn.contains(e.target) && hidePopover(), true);\n\n if (el.mainBtn) {\n el.mainBtn.style.position = 'relative';\n const badge = document.createElement('span');\n badge.id = 'suneSyncBadge';\n badge.className = 'hidden absolute -top-1 -right-1 h-4 px-1.5 rounded-full bg-indigo-600 text-white text-[10px] font-bold leading-4 flex items-center justify-center';\n el.mainBtn.appendChild(badge);\n }\n new MutationObserver((_, obs) => {\n if (!document.contains(el.root)) {\n cleanup();\n obs.disconnect();\n }\n }).observe(document.body, { childList: true, subtree: true });\n \n lucideRender();\n };\n\n init();\n})();\n</script>\n",
|
|
"extension_html": ""
|
|
},
|
|
"storage": {}
|
|
}
|
|
] |