Files
store/sync.sune

27 lines
15 KiB
JSON

[
{
"id": "csx7463",
"name": "Github Sync",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@master/sync.sune",
"updatedAt": 1756926366975,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.96,
"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 m-2 bg-gray-50 border rounded-lg\">\n <!-- Sune version v1.3.3 -->\n <div class=\"space-y-4\">\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 class=\"flex items-center gap-3\">\n <button id=\"syncBtn\" class=\"inline-flex items-center justify-center flex-1 w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md 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\">\n <!-- Content dynamically set by script -->\n </button>\n <button id=\"checkStatusBtn\" class=\"inline-flex items-center justify-center p-2 text-sm font-medium text-white bg-gray-800 border border-transparent rounded-md shadow-sm shrink-0 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700 disabled:opacity-50\" title=\"Manual Status Check\">\n <i data-lucide=\"refresh-cw\" class=\"w-5 h-5\"></i>\n </button>\n </div>\n <div>\n <label for=\"logArea\" class=\"block mb-1 text-sm font-medium text-gray-700\">Logs</label>\n <pre id=\"logArea\" class=\"w-full h-48 p-3 overflow-auto font-mono text-xs text-gray-600 bg-gray-100 border border-gray-300 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 checkStatusBtn = suneContainer.querySelector('#checkStatusBtn');\n const syncBtn = suneContainer.querySelector('#syncBtn');\n const logArea = suneContainer.querySelector('#logArea');\n\n if (!window.SUNE || !window.USER) {\n logArea.textContent = 'ERROR: Core Sune environment not found.';\n suneContainer.classList.add('hidden');\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 const state = {\n isBusy: false,\n currentSha: null,\n pathInfo: null,\n syncAction: 'none' // 'none', 'upload', 'download'\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 setBusy = (busy) => {\n state.isBusy = busy;\n checkStatusBtn.disabled = busy;\n if (busy) {\n syncBtn.disabled = true;\n checkStatusBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 animate-spin\"></i>`;\n } else {\n checkStatusBtn.innerHTML = `<i data-lucide=\"refresh-cw\" class=\"w-5 h-5\"></i>`;\n }\n if (window.lucide) lucide.createIcons();\n };\n\n const updateSyncButton = (action, sha) => {\n state.syncAction = action;\n state.currentSha = sha;\n switch (action) {\n case 'upload':\n syncBtn.disabled = false;\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'\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) lucide.createIcons();\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 || res.status === 201 ? {} : await res.json();\n return { ok: res.ok, status: res.status, data };\n } catch (error) {\n return { ok: false, error: error.message };\n }\n }\n\n self.onmessage = async (e) => {\n const { type, pat, pathInfo, suneContentB64, sha, suneName, isManual } = e.data;\n const { owner, repo, branch, path } = pathInfo;\n\n if (!pat) {\n self.postMessage({ type: 'log', message: 'ERROR: GitHub PAT not found.' });\n return self.postMessage({ type: 'error', reason: 'No PAT' });\n }\n \n const refParam = branch ? \\`?ref=\\${encodeURIComponent(branch)}\\` : '';\n const contentsPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${refParam}\\`;\n \n if (type === 'check') {\n // ** BUG FIX **: Two-step check for correctness.\n // Step 1: Get the file content metadata. This gives us the definitive blob SHA required for updates.\n const contentRes = await apiCall('GET', contentsPath, pat);\n\n if (contentRes.status === 404) {\n return self.postMessage({ type: 'status_report', exists: false, isManual });\n }\n if (!contentRes.ok) {\n self.postMessage({ type: 'log', message: \\`Error (\\${contentRes.status}) fetching file details: \\${contentRes.data?.message || contentRes.error}\\` });\n return self.postMessage({ type: 'error', reason: 'Content fetch failed' });\n }\n const fileSha = contentRes.data.sha;\n\n // Step 2: Get the latest commit date for that file to compare timestamps.\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 \n const commitDate = (commitRes.ok && commitRes.data.length > 0) ? commitRes.data[0].commit.committer.date : null;\n\n // Step 3: Report back with the correct blob SHA and the commit date.\n return self.postMessage({ type: 'status_report', exists: true, sha: fileSha, commitDate, isManual });\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; // This sha is now guaranteed to be the correct blob sha.\n if (branch) body.branch = branch;\n \n self.postMessage({ type: 'log', message: \\`Committing to \\${owner}/\\${repo}\\${branch ? '@' + branch : ''}...\\`});\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 const workerUrl = URL.createObjectURL(blob);\n const newWorker = new Worker(workerUrl);\n URL.revokeObjectURL(workerUrl);\n return newWorker;\n };\n\n const uploadChanges = () => {\n if (state.isBusy || state.syncAction !== 'upload') return;\n const pat = window.USER && window.USER.PAT;\n if (!pat) return log('ERROR: GitHub PAT not found in Account Settings.');\n\n setBusy(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) lucide.createIcons();\n log('Preparing sune data for upload...');\n \n try {\n const suneData = [JSON.parse(JSON.stringify(window.SUNE.active))];\n const suneContentB64 = btoa(unescape(encodeURIComponent(JSON.stringify(suneData, null, 2))));\n worker.postMessage({ type: 'sync', pat, pathInfo: state.pathInfo, suneContentB64, suneName: window.SUNE.active.name, sha: state.currentSha });\n } catch (error) {\n log(`FATAL: Failed to prepare sune data. ${error.message}`);\n setBusy(false);\n }\n };\n \n const handleWorkerMessage = (e) => {\n const { type, message } = e.data;\n if (type === 'log') return log(message); \n if (type === 'error') return setBusy(false);\n\n if (type === 'status_report') {\n const { exists, sha, commitDate } = e.data;\n setBusy(false);\n if (!exists) {\n log('File not found on GitHub. Ready to create.');\n updateSyncButton('upload', null);\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = commitDate ? new Date(commitDate).getTime() : 0;\n \n if (remoteUpdate > localUpdate) {\n log('Remote is newer. Auto-downloading...');\n setBusy(true);\n worker.postMessage({ type: 'fetch_content', pat: window.USER.PAT, pathInfo: state.pathInfo });\n } else if (localUpdate > remoteUpdate + 2000) {\n log('Local is newer. Ready to upload.');\n updateSyncButton('upload', sha);\n } else {\n log('✅ Sune is in sync.');\n updateSyncButton('synced', sha);\n }\n }\n } else if (type === 'content_fetched') {\n try {\n const remoteSune = JSON.parse(decodeURIComponent(escape(atob(e.data.contentB64))))[0];\n if(!remoteSune || !remoteSune.id) throw new Error(\"Invalid sune format in remote file.\");\n \n Object.assign(window.SUNE.active, {\n ...remoteSune,\n settings: { ...window.SUNE.active.settings, ...remoteSune.settings },\n });\n window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\n\n log('✅ Sune updated from GitHub.');\n updateSyncButton('synced', e.data.newSha);\n if (window.renderSidebar) window.renderSidebar();\n if (window.reflectActiveSune) window.reflectActiveSune();\n\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 updateSyncButton('synced', e.data.newSha);\n }\n };\n\n const checkStatus = (isManual = false) => {\n const pathInfo = parseGhPath(window.SUNE.active.url);\n if (!pathInfo) return;\n\n state.pathInfo = pathInfo;\n const pat = window.USER && window.USER.PAT;\n if (!pat) return log('ERROR: GitHub PAT not found in Account Settings.');\n\n setBusy(true);\n if (isManual) logArea.textContent = '';\n log(`Checking status...`);\n worker.postMessage({ type: 'check', pat, pathInfo, isManual });\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 init = () => {\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n logArea.textContent = ''; \n log(`GitHub Sync v1.3.3 ready for \"${activeSune.name}\".`);\n \n const pathInfo = parseGhPath(activeSune.url);\n if (pathInfo) {\n const branchText = pathInfo.branch ? `@<span class=\"font-bold\">${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 checkStatusBtn.disabled = false;\n checkStatus(false);\n } else {\n infoText.textContent = 'Set a `gh://owner/repo@branch/path.sune` URL in Sune settings (click ✺) to enable sync.';\n updateSyncButton('none', null);\n checkStatusBtn.disabled = true;\n }\n\n checkStatusBtn.addEventListener('click', () => checkStatus(true));\n syncBtn.addEventListener('click', uploadChanges);\n\n if (window.lucide) lucide.createIcons();\n };\n\n init();\n})();\n</script>\n",
"extension_html": ""
}
}
]