Files
store/sync.sune

27 lines
18 KiB
JSON

[
{
"id": "csx7463",
"name": "Github Sync",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@master/sync.sune",
"updatedAt": 1756927870194,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.95,
"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.3.8 -->\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 <!-- Content dynamically set by script -->\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(() => {\n const suneContainer = document.getElementById('githubSyncSune');\n if (!suneContainer || suneContainer.dataset.initialized) return;\n suneContainer.dataset.initialized = 'true';\n\n const infoText = suneContainer.querySelector('#infoText');\n const syncBtn = suneContainer.querySelector('#syncBtn');\n const logArea = suneContainer.querySelector('#logArea');\n const mainSyncButton = document.getElementById('syncSune');\n\n if (!window.SUNE || !window.USER) {\n logArea.textContent = 'ERROR: Core Sune environment not found.';\n return;\n }\n\n const activeSune = window.SUNE.active;\n if (!activeSune) {\n logArea.textContent = 'Error: No active sune found.';\n return;\n }\n\n let worker = null;\n let syncTimeout = null;\n const state = {\n isBusy: false,\n remoteSha: null,\n pathInfo: null,\n syncAction: 'none',\n forceSync: false,\n };\n\n const log = (message) => {\n if (!logArea) return;\n const timestamp = new Date().toLocaleTimeString();\n logArea.textContent += `[${timestamp}] ${message}\\n`;\n logArea.scrollTop = logArea.scrollHeight;\n };\n\n const clearSyncTimeout = () => {\n if (syncTimeout) {\n clearTimeout(syncTimeout);\n syncTimeout = null;\n }\n };\n\n const setBusy = (busy) => {\n state.isBusy = busy;\n const icon = mainSyncButton?.querySelector('svg');\n if (icon) {\n if (busy) {\n icon.classList.add('animate-spin');\n } else {\n icon.classList.remove('animate-spin');\n }\n }\n if (!busy) {\n clearSyncTimeout();\n }\n };\n\n const updateSyncButtonUI = (action, sha) => {\n state.syncAction = action;\n if (action !== 'synced') {\n state.remoteSha = sha;\n }\n switch (action) {\n case 'upload':\n syncBtn.disabled = state.isBusy;\n syncBtn.innerHTML = `<i data-lucide=\"upload-cloud\" class=\"w-5 h-5 mr-2\"></i><span>${sha ? 'Upload Changes' : 'Create on GitHub'}</span>`;\n break;\n case 'synced':\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"check-circle-2\" class=\"w-5 h-5 mr-2\"></i><span>In Sync</span>`;\n break;\n default: // 'none' or error\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"help-circle\" class=\"w-5 h-5 mr-2\"></i><span>Configure Sync Path</span>`;\n break;\n }\n if (window.lucide) window.lucide.createIcons();\n };\n\n const cleanup = () => {\n clearSyncTimeout();\n if (worker) {\n worker.terminate();\n worker = null;\n }\n if (mainSyncButton && mainSyncButton.__syncClickListener) {\n mainSyncButton.removeEventListener('click', mainSyncButton.__syncClickListener);\n delete mainSyncButton.__syncClickListener;\n }\n suneContainer.dataset.initialized = ''; // Allow re-init on next open\n };\n\n const createWorker = () => {\n const workerCode = `\n const GITHUB_API = 'https://api.github.com';\n\n async function apiCall(method, path, token, body = null) {\n const headers = { 'Authorization': \\`token \\${token}\\`, 'Accept': 'application/vnd.github.v3+json' };\n if (body) headers['Content-Type'] = 'application/json';\n const options = { method, headers, body: body ? JSON.stringify(body) : null };\n try {\n const res = await fetch(GITHUB_API + path, options);\n const data = res.status === 204 ? {} : await res.json().catch(() => ({}));\n return { ok: res.ok, status: res.status, data };\n } catch (error) {\n return { ok: false, status: 0, error: error.message };\n }\n }\n\n self.onmessage = async (e) => {\n const { type, pat, pathInfo, suneContentB64, sha, suneName } = e.data;\n if (!pathInfo) return self.postMessage({ type: 'error', reason: 'pathInfo is missing.' });\n const { owner, repo, branch, path } = pathInfo;\n\n if (!pat) {\n return self.postMessage({ type: 'error', reason: 'GitHub PAT not found.' });\n }\n \n const refParam = branch ? \\`?ref=\\${encodeURIComponent(branch)}\\` : '';\n const contentsPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${refParam}\\`;\n \n if (type === 'check') {\n const contentRes = await apiCall('GET', contentsPath, pat);\n\n if (contentRes.status === 404) {\n return self.postMessage({ type: 'status_report', exists: false });\n }\n if (!contentRes.ok) {\n self.postMessage({ type: 'log', message: \\`Error (\\${contentRes.status}) fetching file: \\${contentRes.data?.message || contentRes.error}\\` });\n return self.postMessage({ type: 'error', reason: 'Content fetch failed' });\n }\n \n const commitSha = branch || 'HEAD';\n const commitsPath = \\`/repos/\\${owner}/\\${repo}/commits?sha=\\${commitSha}&path=\\${encodeURIComponent(path)}&page=1&per_page=1\\`;\n const commitRes = await apiCall('GET', commitsPath, pat);\n const commitDate = (commitRes.ok && commitRes.data.length > 0) ? commitRes.data[0].commit.committer.date : null;\n\n return self.postMessage({ type: 'status_report', exists: true, sha: contentRes.data.sha, commitDate });\n }\n \n if (type === 'fetch_content') {\n const res = await apiCall('GET', contentsPath, pat);\n if (res.ok) return self.postMessage({ type: 'content_fetched', contentB64: res.data.content, newSha: res.data.sha });\n return self.postMessage({ type: 'error', reason: 'Fetch failed' });\n }\n\n if (type === 'sync') {\n const commitMessage = sha ? \\`Sync: Update sune '\\${suneName}'\\` : \\`Sync: Create sune '\\${suneName}'\\`;\n const body = { message: commitMessage, content: suneContentB64 };\n if (sha) body.sha = sha;\n if (branch) body.branch = branch;\n \n self.postMessage({ type: 'log', message: \\`Committing to \\${owner}/\\${repo}...\\`});\n const res = await apiCall('PUT', \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\`, pat, body);\n\n if (res.ok) {\n const newSha = res.data.content.sha;\n self.postMessage({ type: 'log', message: \\`✅ Sync successful! New SHA: \\${newSha.substring(0,7)}\\` });\n return self.postMessage({ type: 'sync_complete', newSha });\n }\n self.postMessage({ type: 'log', message: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error || 'Sync failed.'}\\` });\n return self.postMessage({ type: 'error', reason: res.data?.message || 'Sync failed' });\n }\n };\n `;\n const blob = new Blob([workerCode], { type: 'application/javascript' });\n return new Worker(URL.createObjectURL(blob));\n };\n \n const startWorkerTask = (message, timeout = 20000) => {\n if (!worker) {\n log('ERROR: Worker is not initialized.');\n setBusy(false);\n return;\n }\n clearSyncTimeout();\n worker.postMessage(message);\n syncTimeout = setTimeout(() => {\n log('ERROR: Sync operation timed out. Terminating task.');\n setBusy(false);\n cleanup();\n init(); // Re-initialize for the next attempt\n log('Sync has been reset. Please try again.');\n }, timeout);\n };\n\n const performUpload = () => {\n if (!state.pathInfo) return;\n const pat = window.USER?.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found in Account Settings.');\n return;\n }\n\n setBusy(true);\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 mr-2 animate-spin\"></i><span>Uploading...</span>`;\n if (window.lucide) window.lucide.createIcons();\n log('Preparing sune data for upload...');\n \n try {\n const suneToSync = JSON.parse(JSON.stringify(window.SUNE.active));\n const suneJson = JSON.stringify([suneToSync], null, 2);\n const suneContentB64 = btoa(unescape(encodeURIComponent(suneJson)));\n startWorkerTask({ type: 'sync', pat, pathInfo: state.pathInfo, suneContentB64, suneName: window.SUNE.active.name, sha: state.remoteSha });\n } catch (error) {\n log(`FATAL: Failed to prepare sune data. ${error.message}`);\n setBusy(false);\n }\n };\n \n const handleWorkerMessage = (e) => {\n clearSyncTimeout();\n const { type } = e.data;\n if (type === 'log') return log(e.data.message);\n \n if (type === 'error') {\n log(`Worker error: ${e.data.reason || 'Unknown'}`);\n setBusy(false);\n updateSyncButtonUI('upload', state.remoteSha);\n return;\n }\n\n if (type === 'status_report') {\n const { exists, sha, commitDate } = e.data;\n const forceSync = state.forceSync;\n state.forceSync = false; // Reset force flag\n\n if (!exists) {\n log('File not found on GitHub. Ready to create.');\n setBusy(false);\n updateSyncButtonUI('upload', null);\n if (forceSync) {\n log('Force sync: creating file on GitHub...');\n performUpload();\n }\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = commitDate ? new Date(commitDate).getTime() : 0;\n \n if (forceSync) {\n log('Force sync: uploading local version...');\n setBusy(false);\n updateSyncButtonUI('upload', sha);\n performUpload();\n } else if (remoteUpdate > localUpdate + 5000) { // 5s buffer\n log('Remote is newer. Auto-downloading...');\n setBusy(true);\n startWorkerTask({ type: 'fetch_content', 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 updateSyncButtonUI('upload', sha);\n } else {\n log('✅ Sune is in sync.');\n setBusy(false);\n updateSyncButtonUI('synced', sha);\n }\n }\n } else if (type === 'content_fetched') {\n try {\n const decodedJson = decodeURIComponent(escape(atob(e.data.contentB64)));\n const remoteSuneArr = JSON.parse(decodedJson);\n if (!Array.isArray(remoteSuneArr) || !remoteSuneArr[0]?.id) throw new Error(\"Invalid sune format in remote file.\");\n \n const remoteSune = remoteSuneArr[0];\n const currentSune = window.SUNE.active;\n \n Object.assign(currentSune, { ...remoteSune, settings: { ...currentSune.settings, ...remoteSune.settings } });\n currentSune.updatedAt = Date.now();\n window.SUNE.save();\n\n log('✅ Sune updated from GitHub. Re-opening settings...');\n updateSyncButtonUI('synced', e.data.newSha);\n\n if (window.closeSettings) window.closeSettings();\n setTimeout(() => {\n if (window.openSettings) window.openSettings();\n if (window.renderSidebar) window.renderSidebar();\n if (window.reflectActiveSune) window.reflectActiveSune();\n }, 100);\n } catch (err) {\n log(`ERROR processing download: ${err.message}`);\n } finally {\n setBusy(false);\n }\n } else if (type === 'sync_complete') {\n window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\n setBusy(false);\n updateSyncButtonUI('synced', e.data.newSha);\n }\n };\n\n const startCheck = (isManual = false) => {\n const pathInfo = parseGhPath(window.SUNE.active.url);\n if (!pathInfo) return;\n state.pathInfo = pathInfo;\n\n if (!window.USER?.PAT) {\n log('ERROR: GitHub PAT not found in Account Settings.');\n return;\n }\n\n setBusy(true);\n if (isManual) logArea.textContent = '';\n log('Checking remote status...');\n startWorkerTask({ type: 'check', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const parseGhPath = (path) => {\n const match = (path || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\n return match ? { owner: match[1], repo: match[2], branch: match[3], path: match[4] } : null;\n };\n\n const handleMainSyncClick = (e) => {\n e.preventDefault();\n e.stopPropagation();\n if (state.isBusy) {\n log('Sync is already in progress.');\n return;\n }\n if (state.pathInfo) {\n log('Manual force sync triggered...');\n state.forceSync = true;\n startCheck(true);\n }\n };\n \n const init = () => {\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n logArea.textContent = ''; \n log(`GitHub Sync v1.3.8 ready for \"${activeSune.name}\".`);\n \n const pathInfo = parseGhPath(activeSune.url);\n if (pathInfo) {\n const branchText = pathInfo.branch ? `@<span class=\"font-semibold\">${pathInfo.branch}</span>` : '';\n infoText.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${pathInfo.owner}/${pathInfo.repo}${branchText}/${pathInfo.path}</span>`;\n updateSyncButtonUI('none', null);\n startCheck(false);\n } else {\n infoText.innerHTML = 'Set a `gh://owner/repo@branch/path.sune` URL in Sune settings (click ✺) to enable sync.';\n updateSyncButtonUI('none', null);\n }\n\n syncBtn.addEventListener('click', performUpload);\n \n if (mainSyncButton) {\n if(mainSyncButton.__syncClickListener) {\n mainSyncButton.removeEventListener('click', mainSyncButton.__syncClickListener);\n }\n mainSyncButton.__syncClickListener = handleMainSyncClick;\n mainSyncButton.addEventListener('click', mainSyncButton.__syncClickListener);\n }\n\n if (window.lucide) window.lucide.createIcons();\n\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.removedNodes) {\n for (const removedNode of mutation.removedNodes) {\n if (removedNode === suneContainer || removedNode.contains(suneContainer)) {\n observer.disconnect();\n cleanup();\n return;\n }\n }\n }\n }\n });\n if (suneContainer.parentElement) {\n observer.observe(suneContainer.parentElement, { childList: true });\n }\n };\n\n init();\n})();\n</script>\n",
"extension_html": ""
}
}
]