mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
28 lines
19 KiB
JSON
28 lines
19 KiB
JSON
[
|
|
{
|
|
"id": "csx7463",
|
|
"name": "Github Sync",
|
|
"pinned": false,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/sync.sune",
|
|
"updatedAt": 1757051191095,
|
|
"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.6.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 forceSync: false,\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) return;\n badge.textContent = text;\n 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 state.remoteSha = (action !== 'synced') ? sha : state.remoteSha;\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(method, path, token, body = null) {\n const h = { 'Accept': 'application/vnd.github.v3+json' };\n if (token) h['Authorization'] = \\`token \\${token}\\`;\n if (body) h['Content-Type'] = 'application/json';\n const opts = { method, headers: h, body: body ? JSON.stringify(body) : null };\n try {\n const r = await fetch(GITHUB_API + path, opts);\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 (e) => {\n const { type, pat, pathInfo, contentB64, sha, name } = e.data;\n const { owner, repo, branch, path } = pathInfo;\n const ref = branch ? \\`?ref=\\${encodeURIComponent(branch)}\\` : '';\n const cPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${ref}\\`;\n \n if (type === 'check') {\n const res = await gh('GET', cPath, pat);\n if (res.status === 404) return self.postMessage({ type: 'status', exists: false });\n if ((res.status === 403 || res.status === 401) && !pat) return self.postMessage({ type: 'error', reason: 'Private repo or rate limit hit. A GitHub PAT is required.' });\n if (!res.ok) return self.postMessage({ type: 'error', reason: \\`API Error (\\${res.status}): \\${res.data?.message || res.error}\\` });\n \n const commitSha = branch || 'HEAD';\n const comPath = \\`/repos/\\${owner}/\\${repo}/commits?sha=\\${commitSha}&path=\\${encodeURIComponent(path)}&page=1&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 });\n }\n \n if (type === 'fetch') {\n const res = await gh('GET', cPath, pat);\n if (res.ok) return self.postMessage({ type: 'fetched', contentB64: res.data.content, newSha: res.data.sha });\n return self.postMessage({ type: 'error', reason: \\`Download failed (\\${res.status}): \\${res.data?.message || res.error}\\` });\n }\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 body = { message: msg, content: contentB64, sha, ...(branch && { branch }) };\n self.postMessage({ type: 'log', msg: \\`Committing to \\${owner}/\\${repo}...\\`});\n const res = await gh('PUT', cPath, pat, body);\n if (res.ok) {\n const newSha = res.data.content.sha;\n self.postMessage({ type: 'log', msg: \\`✅ Sync successful! New SHA: \\${newSha.substring(0,7)}\\` });\n return self.postMessage({ type: 'synced', newSha });\n }\n self.postMessage({ type: 'log', msg: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error}\\` });\n return self.postMessage({ type: 'error', reason: res.data?.message || 'Sync failed' });\n }\n };`;\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 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 return setBusy(false), updateBtnUI('upload', state.remoteSha);\n }\n\n if (type === 'status') {\n const { exists, sha, date } = e.data;\n const force = state.forceSync;\n state.forceSync = false;\n\n if (!exists) {\n log('File not found on GitHub. Ready to create.');\n setBusy(false);\n updateBtnUI('upload', null);\n if (force && hasPat()) {\n log('Force sync: creating file...');\n performUpload();\n } else if (force) {\n log('Force sync failed: PAT required to create file.');\n }\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = date ? new Date(date).getTime() : 0;\n \n if (force && hasPat()) {\n log('Force sync: uploading local version...');\n setBusy(false);\n updateBtnUI('upload', sha);\n performUpload();\n } else if (force) {\n log('Force sync failed: PAT required to upload.');\n setBusy(false);\n } else if (remoteUpdate > localUpdate + 5000) {\n log('Remote is newer. Auto-downloading...');\n setBusy(true, 'Down');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\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) => {\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 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.6.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 const branchText = branch ? `@<span class=\"font-semibold\">${branch}</span>` : '';\n el.info.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${owner}/${repo}${branchText}/${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 // --- Event Listeners ---\n addListener(el.btn, 'click', performUpload);\n \n const handleMainClick = (e) => {\n e.preventDefault(); e.stopPropagation();\n if (state.busy) return log('Sync is busy.');\n if (state.pathInfo) {\n log('Manual force-sync triggered...');\n if(!HIDE_UI && el.log) el.log.textContent = '';\n state.forceSync = true;\n startCheck(true);\n }\n };\n addListener(el.mainBtn, 'click', handleMainClick);\n\n // --- Popover & Long-press ---\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 lucideRender([popover]);\n\n const showPopover = (btn) => {\n if (!popover || state.busy) 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();\n hidePopover();\n if (!confirm(\"Overwrite local changes with the version from GitHub? This cannot be undone.\")) return;\n log('Force download initiated...');\n setBusy(true, 'Down');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\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 // --- Main Badge & Cleanup Observer ---\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 min-w-[1rem] px-1 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": {}
|
|
}
|
|
] |