Files
store/sync.sune

27 lines
14 KiB
JSON

[
{
"id": "csx7463",
"name": "Github Sync",
"pinned": false,
"avatar": "",
"url": "",
"updatedAt": 1756858158522,
"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 bg-gray-50 border rounded-lg m-2\">\n <div class=\"space-y-4\">\n <div>\n <label for=\"ghPathInput\" class=\"block text-sm font-medium text-gray-700 mb-1\">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 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 bg-white\">\n <button id=\"checkStatusBtn\" class=\"inline-flex items-center justify-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-800 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=\"h-5 w-5\"></i>\n </button>\n </div>\n <p class=\"mt-1 text-xs text-gray-500\">Path to a `.sune` or `.json` file on GitHub for the active sune.</p>\n </div>\n\n <div>\n <button id=\"syncBtn\" class=\"w-full inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 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=\"h-5 w-5 mr-2\"></i>\n <span>Sync to GitHub</span>\n </button>\n </div>\n\n <div>\n <label for=\"logArea\" class=\"block text-sm font-medium text-gray-700 mb-1\">Logs</label>\n <pre id=\"logArea\" class=\"w-full h-48 p-3 rounded-md border border-gray-300 bg-gray-100 overflow-auto font-mono text-xs text-gray-600 whitespace-pre-wrap\"></pre>\n </div>\n </div>\n</div>\n\n<script>\n(() => {\n // Ensure this script runs only once and is self-contained.\n if (window.sune_github_sync_initialized) return;\n window.sune_github_sync_initialized = true;\n\n const suneContainer = document.getElementById('githubSyncSune');\n if (!suneContainer) return;\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 // SUNE.id is the ID of this sune, not the active one.\n // So we'll use a dynamic key based on the *active* sune's ID.\n const getCacheKey = (suneId) => `sune_github_sync_path_${suneId}`;\n\n let worker;\n const state = {\n isBusy: false,\n currentSha: null,\n pathInfo: null,\n lastCheckedSuneId: null,\n };\n\n const log = (message) => {\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 ghPathInput.disabled = busy;\n if (busy) {\n syncBtn.disabled = true;\n checkStatusBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"h-5 w-5 animate-spin\"></i>`;\n } else {\n checkStatusBtn.innerHTML = `<i data-lucide=\"refresh-cw\" class=\"h-5 w-5\"></i>`;\n }\n 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) {\n headers['Content-Type'] = 'application/json';\n }\n\n const options = { method, headers };\n if (body) {\n options.body = JSON.stringify(body);\n }\n\n try {\n const response = await fetch(GITHUB_API + path, options);\n if (method === 'HEAD') {\n return { ok: response.ok, status: response.status, headers: response.headers };\n }\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, error: error.message };\n }\n }\n \n self.onmessage = async (e) => {\n const { type, pat, pathInfo, suneContentB64, sha } = 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 for 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 // Check if repo exists\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.data?.message || '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 configuration\\` : \\`Sync: Create sune configuration\\`;\n const body = {\n message: commitMessage,\n content: suneContentB64,\n };\n if (sha) {\n body.sha = sha;\n }\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.data?.message || '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 return new Worker(URL.createObjectURL(blob));\n };\n\n const handleWorkerMessage = (e) => {\n const { type, message, exists, sha, newSha, reason } = e.data;\n\n if (type === 'log') {\n log(message);\n } else if (type === 'status') {\n state.currentSha = sha;\n syncBtn.disabled = false;\n syncBtn.querySelector('span').textContent = exists ? 'Update on GitHub' : 'Create on GitHub';\n setBusy(false);\n } else if (type === 'sync_complete') {\n state.currentSha = newSha;\n syncBtn.querySelector('span').textContent = 'Update on GitHub';\n log('Ready for next sync.');\n setBusy(false);\n } else if (type === 'error') {\n setBusy(false);\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 {\n owner: match[1],\n repo: match[2],\n path: match[3],\n };\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\n const pat = window.USER && window.USER.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found. Please set it via the Account Settings menu.');\n return;\n }\n \n setBusy(true);\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.pathInfo) {\n log('ERROR: Please check status before syncing.');\n return;\n }\n\n const pat = window.USER && window.USER.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found. Please set it via the Account Settings menu.');\n return;\n }\n\n setBusy(true);\n log('Preparing sune data for sync...');\n\n // Create a clean, serializable object from the active sune proxy\n const activeSune = window.SUNE.active;\n if (!activeSune) {\n log('ERROR: No active sune found.');\n setBusy(false);\n return;\n }\n \n // Match the export format which is an array containing the sune object\n const suneData = [JSON.parse(JSON.stringify(activeSune))];\n const suneString = JSON.stringify(suneData, null, 2);\n const suneContentB64 = btoa(unescape(encodeURIComponent(suneString)));\n\n log(`Sune \"${activeSune.name}\" data prepared.`);\n worker.postMessage({\n type: 'sync',\n pat,\n pathInfo: state.pathInfo,\n suneContentB64,\n sha: state.currentSha,\n });\n });\n\n const loadStateForActiveSune = () => {\n const activeSuneId = window.SUNE.id;\n if (state.lastCheckedSuneId === activeSuneId) {\n return; // No change\n }\n\n log(`Active sune changed to \"${window.SUNE.name}\". Loading its sync path.`);\n state.lastCheckedSuneId = activeSuneId;\n const cacheKey = getCacheKey(activeSuneId);\n const savedPath = localStorage.getItem(cacheKey);\n\n ghPathInput.value = savedPath || '';\n state.pathInfo = null;\n state.currentSha = null;\n syncBtn.disabled = true;\n syncBtn.querySelector('span').textContent = 'Sync to GitHub';\n logArea.textContent = 'Enter a sync path and click the refresh button to check its status.';\n };\n\n ghPathInput.addEventListener('input', () => {\n const activeSuneId = window.SUNE.id;\n if (activeSuneId) {\n const cacheKey = getCacheKey(activeSuneId);\n localStorage.setItem(cacheKey, ghPathInput.value);\n }\n // As user types, the checked state becomes invalid\n syncBtn.disabled = true;\n state.currentSha = null;\n });\n\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 // Check for active sune changes and update UI accordingly\n setInterval(loadStateForActiveSune, 1000);\n\n // Initial load\n loadStateForActiveSune();\n\n // Initial icon render\n if (window.lucide) {\n lucide.createIcons();\n }\n };\n\n init();\n})();\n</script>\n",
"extension_html": ""
}
}
]