Files
store/sync.sune

27 lines
14 KiB
JSON

[
{
"id": "csx7463",
"name": "Github Sync",
"pinned": false,
"avatar": "",
"url": "",
"updatedAt": 1756912356042,
"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 <div class=\"space-y-4\">\n <div>\n <label for=\"ghPathInput\" class=\"block mb-1 text-sm font-medium text-gray-700\">GitHub Sync Path</label>\n <div class=\"flex items-center gap-2\">\n <input type=\"text\" id=\"ghPathInput\" placeholder=\"gh://owner/repo/path/to/your.sune\" class=\"flex-1 w-full px-3 py-2 bg-white border-gray-300 rounded-md shadow-sm sm:text-sm focus:border-indigo-500 focus:ring-indigo-500\">\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 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700\" title=\"Check Status\">\n <i data-lucide=\"refresh-cw\" class=\"w-5 h-5\"></i>\n </button>\n </div>\n <p class=\"mt-1 text-xs text-gray-500\">Path to a `.sune` or `.json` file for the active sune.</p>\n </div>\n\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-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\" disabled>\n <i data-lucide=\"upload-cloud\" class=\"w-5 h-5 mr-2\"></i>\n <span>Sync to GitHub</span>\n </button>\n </div>\n\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 // This script is designed to re-initialize every time its containing sune is rendered.\n // It is tied to the lifecycle of the active sune's custom HTML.\n \n const suneContainer = document.getElementById('githubSyncSune');\n if (!suneContainer) {\n console.error(\"Sune 'GitHub Sync' container not found.\");\n return;\n }\n // Prevent re-initialization if the script is somehow run multiple times on the same DOM element.\n if (suneContainer.dataset.initialized) return;\n suneContainer.dataset.initialized = 'true';\n\n const ghPathInput = suneContainer.querySelector('#ghPathInput');\n const checkStatusBtn = suneContainer.querySelector('#checkStatusBtn');\n const syncBtn = suneContainer.querySelector('#syncBtn');\n const logArea = suneContainer.querySelector('#logArea');\n\n // At the time of execution, window.SUNE refers to the global sune manager.\n // window.SUNE.active provides the object for the currently active sune.\n const activeSune = window.SUNE.active;\n if (!activeSune) {\n logArea.textContent = 'Error: No active sune found.';\n return;\n }\n const activeSuneId = activeSune.id;\n const CACHE_KEY = `sune_github_sync_path_${activeSuneId}`;\n\n let worker = null;\n const state = {\n isBusy: false,\n currentSha: null,\n pathInfo: null,\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, operation = 'check') => {\n state.isBusy = busy;\n ghPathInput.disabled = busy;\n checkStatusBtn.disabled = busy;\n syncBtn.disabled = true; // Always disable sync button when any operation is busy.\n\n const restoreSyncButton = () => {\n // Only enable the sync button if a valid path has been checked.\n if (!state.isBusy && state.pathInfo && state.currentSha !== undefined) {\n syncBtn.disabled = false;\n }\n };\n \n if (busy) {\n if (operation === 'sync') {\n syncBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 mr-2 animate-spin\"></i><span>Syncing...</span>`;\n } else { // 'check'\n checkStatusBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 animate-spin\"></i>`;\n }\n } else {\n checkStatusBtn.innerHTML = `<i data-lucide=\"refresh-cw\" class=\"w-5 h-5\"></i>`;\n if (state.currentSha) {\n syncBtn.innerHTML = `<i data-lucide=\"upload-cloud\" class=\"w-5 h-5 mr-2\"></i><span>Update on GitHub</span>`;\n } else {\n syncBtn.innerHTML = `<i data-lucide=\"upload-cloud\" class=\"w-5 h-5 mr-2\"></i><span>Create on GitHub</span>`;\n }\n restoreSyncButton();\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 = {\n 'Authorization': \\`token \\${token}\\`,\n 'Accept': 'application/vnd.github.v3+json',\n };\n if (body) headers['Content-Type'] = 'application/json';\n const options = { method, headers, body: body ? JSON.stringify(body) : null };\n\n try {\n const response = await fetch(GITHUB_API + path, options);\n const data = response.status === 204 || response.status === 201 ? {} : await response.json();\n return { ok: response.ok, status: response.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 \n if (!pat) {\n self.postMessage({ type: 'log', message: 'ERROR: GitHub PAT not found. Set it in Account Settings.' });\n self.postMessage({ type: 'error', reason: 'No PAT' });\n return;\n }\n\n const { owner, repo, path } = pathInfo;\n const contentsPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\`;\n\n if (type === 'check') {\n self.postMessage({ type: 'log', message: \\`Checking file: \\${path} in \\${owner}/\\${repo}\\` });\n const res = await apiCall('GET', contentsPath, pat);\n\n if (res.status === 404) {\n self.postMessage({ type: 'log', message: 'File not found. It will be created on first sync.' });\n const repoRes = await apiCall('GET', \\`/repos/\\${owner}/\\${repo}\\`, pat);\n if (repoRes.status === 404) {\n self.postMessage({ type: 'log', message: 'ERROR: Repository not found. Please create it on GitHub first.' });\n self.postMessage({ type: 'error', reason: 'Repo not found' });\n } else {\n self.postMessage({ type: 'status', exists: false, sha: null });\n }\n } else if (res.ok) {\n self.postMessage({ type: 'log', message: \\`File found. SHA: \\${res.data.sha}\\` });\n self.postMessage({ type: 'status', exists: true, sha: res.data.sha });\n } else {\n self.postMessage({ type: 'log', message: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error || 'Failed to check file status.'}\\` });\n self.postMessage({ type: 'error', reason: res.data?.message });\n }\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 \n self.postMessage({ type: 'log', message: \\`Committing to \\${owner}/\\${repo}/\\${path}...\\` });\n const res = await apiCall('PUT', contentsPath, 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}\\` });\n self.postMessage({ type: 'sync_complete', newSha });\n } else {\n self.postMessage({ type: 'log', message: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error || 'Failed to sync file.'}\\` });\n self.postMessage({ type: 'error', reason: res.data?.message });\n }\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); // Clean up the blob URL to prevent memory leaks\n return newWorker;\n };\n\n const handleWorkerMessage = (e) => {\n const { type, message, exists, sha, newSha } = e.data;\n\n if (type === 'log') {\n log(message);\n } else if (type === 'status') {\n state.currentSha = sha;\n setBusy(false, 'check');\n } else if (type === 'sync_complete') {\n state.currentSha = newSha;\n log('Ready for next sync.');\n setBusy(false, 'sync');\n } else if (type === 'error') {\n setBusy(false, e.data.operation || 'check');\n syncBtn.disabled = true;\n }\n };\n \n const parseGhPath = (path) => {\n const regex = /^gh:\\/\\/([a-zA-Z0-9\\-\\._]+)\\/([a-zA-Z0-9\\-\\._]+)\\/(.+\\.(?:sune|json))$/;\n const match = path.trim().match(regex);\n if (!match) return null;\n return { owner: match[1], repo: match[2], path: match[3] };\n };\n\n checkStatusBtn.addEventListener('click', () => {\n const pathInfo = parseGhPath(ghPathInput.value);\n if (!pathInfo) {\n log('ERROR: Invalid GitHub path format. Use gh://owner/repo/path/to/file.sune');\n return;\n }\n const pat = window.USER && window.USER.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found. Please set it in Account Settings.');\n return;\n }\n setBusy(true, 'check');\n logArea.textContent = '';\n log('Starting check...');\n state.pathInfo = pathInfo;\n worker.postMessage({ type: 'check', pat, pathInfo });\n });\n\n syncBtn.addEventListener('click', () => {\n if (state.isBusy) return;\n if (!state.pathInfo) {\n log('ERROR: Please check status before syncing by clicking the refresh button.');\n return;\n }\n const pat = window.USER && window.USER.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found. Please set it in Account Settings.');\n return;\n }\n setBusy(true, 'sync');\n log('Preparing sune data for sync...');\n \n try {\n const currentActiveSune = window.SUNE.active;\n if (!currentActiveSune || currentActiveSune.id !== activeSuneId) {\n log('ERROR: Active sune has changed. This tool will reload on the next sune change.');\n setBusy(false, 'sync');\n return;\n }\n // Create a clean, serializable object. Matches export format: an array of sunes.\n const suneData = [JSON.parse(JSON.stringify(currentActiveSune))];\n const suneString = JSON.stringify(suneData, null, 2);\n // Robust base64 encoding for Unicode characters\n const suneContentB64 = btoa(unescape(encodeURIComponent(suneString)));\n\n log(`Sune \"${currentActiveSune.name}\" data prepared. Syncing...`);\n worker.postMessage({\n type: 'sync',\n pat,\n pathInfo: state.pathInfo,\n suneContentB64,\n suneName: currentActiveSune.name,\n sha: state.currentSha,\n });\n } catch (error) {\n log(`FATAL: Failed to prepare sune data. ${error.message}`);\n setBusy(false, 'sync');\n }\n });\n\n ghPathInput.addEventListener('input', () => {\n localStorage.setItem(CACHE_KEY, ghPathInput.value);\n // Invalidate status when path changes\n syncBtn.disabled = true;\n state.currentSha = null;\n state.pathInfo = null;\n });\n\n // --- INITIALIZATION ---\n const init = () => {\n if (!window.SUNE || !window.USER) {\n log('ERROR: Core Sune environment not found.');\n suneContainer.classList.add('hidden');\n return;\n }\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n const savedPath = localStorage.getItem(CACHE_KEY);\n ghPathInput.value = savedPath || '';\n \n logArea.textContent = ''; // Clear previous logs\n log(`Ready. This tool will sync the active sune: \"${activeSune.name}\".`);\n if (savedPath) {\n log('Path loaded from cache. Click refresh to check status.');\n } else {\n log('Enter a GitHub sync path to begin.');\n }\n if (window.lucide) lucide.createIcons();\n };\n\n init();\n})();\n</script>\n",
"extension_html": ""
}
}
]