import { getScripts, saveScripts, getSecrets, saveSecrets, garbageCollectScriptStorage, } from './utils/storage.js'; import { parseMeta, getBoilerplate } from './utils/parser.js'; import { isUserScriptsAvailable } from './utils/userScripts.js'; import { renderIcons, icon } from './utils/icons.js'; import { VERSION } from './version.js'; // Application State const state = { tab: 'list', // 'list' | 'editor' | 'secrets' editingId: null, scripts: [], secrets: {}, revealedSecrets: new Set(), userScriptsReady: true, search: '', }; const $ = sel => document.querySelector(sel); const app = $('#app'); const syncScripts = refreshRequires => chrome.runtime.sendMessage({ type: 'SYNC_SCRIPTS', refreshRequires }); // Toast feedback helper let toastTimeout; const showToast = (msg, isErr = false) => { const el = $('#toast'); if (!el) return; el.textContent = msg; el.className = `fixed bottom-8 left-1/2 -translate-x-1/2 text-xs px-3 py-1.5 rounded shadow-lg transition-all z-50 font-medium ${ isErr ? 'bg-red-600 text-white' : 'bg-emerald-600 text-white' }`; clearTimeout(toastTimeout); toastTimeout = setTimeout(() => el.classList.add('hidden'), 2200); }; // Initialize & Load const init = async () => { state.userScriptsReady = await isUserScriptsAvailable(); await syncScripts(false); const [scripts, secrets] = await Promise.all([getScripts(), getSecrets()]); await garbageCollectScriptStorage(scripts.map(s => s.id)); state.scripts = scripts.map(s => ({ ...s, version: parseMeta(s.code || '').version, })); state.secrets = secrets; render(); }; // Actions const setTab = (tab, editingId = null) => { state.tab = tab; state.editingId = editingId; render(); }; const toggleScript = async id => { const s = state.scripts.find(x => x.id === id); if (!s) return; s.enabled = !s.enabled; await saveScripts(state.scripts); await syncScripts(false); render(); showToast(`Script ${s.enabled ? 'enabled' : 'disabled'}`); }; const deleteScript = async id => { if (!confirm('Delete this user script?')) return; state.scripts = state.scripts.filter(s => s.id !== id); await saveScripts(state.scripts); await Promise.all([syncScripts(false), garbageCollectScriptStorage(state.scripts.map(s => s.id))]); render(); showToast('Script deleted'); }; const saveCurrentScript = async () => { const textarea = $('#editor-code'); if (!textarea) return; const code = textarea.value.trim(); if (!code) return showToast('Script cannot be empty', true); const meta = parseMeta(code); const existing = state.scripts.find(s => s.id === state.editingId); const scriptObj = { id: state.editingId || `script_${Date.now()}`, name: meta.name, version: meta.version, description: meta.description, matches: meta.matches, requires: meta.requires, requireCache: existing?.requireCache || {}, storageToken: existing?.storageToken, runAt: $('#run-at-select')?.value || meta.runAt || 'document_idle', code, enabled: existing ? existing.enabled : true, updatedAt: Date.now(), }; state.scripts = state.editingId ? state.scripts.map(s => (s.id === state.editingId ? scriptObj : s)) : [scriptObj, ...state.scripts]; await saveScripts(state.scripts); const result = await syncScripts(true); state.scripts = await getScripts(); setTab('list'); if (!result.success) showToast(`Saved, but not synced: ${result.errors[0]}`, true); else if (result.warnings.length) showToast('Saved using cached dependencies'); else showToast('Script saved & synced!'); }; const addSecret = async (key, val) => { const k = key.trim().toUpperCase().replace(/[^A-Z0-9_]/g, '_'); const v = val.trim(); if (!k) return showToast('Enter variable name', true); state.secrets[k] = v; await saveSecrets(state.secrets); await syncScripts(false); render(); showToast(`Saved secret: ${k}`); }; const removeSecret = async k => { delete state.secrets[k]; state.revealedSecrets.delete(k); await saveSecrets(state.secrets); await syncScripts(false); render(); showToast(`Removed secret: ${k}`); }; // UI Templates const renderHeader = () => `
`; const renderBanner = () => state.userScriptsReady ? '' : `
${icon('AlertTriangle', 'w-4 h-4 text-amber-600 shrink-0')} Enable "Allow User Scripts" in extension details to run scripts.
`; const renderScriptList = () => { const filtered = state.scripts.filter(s => !state.search || s.name.toLowerCase().includes(state.search.toLowerCase()) || s.matches?.some(m => m.toLowerCase().includes(state.search.toLowerCase())) ); return `
${filtered.length ? filtered.map(s => `
${s.name} ${s.version ? `v${s.version}` : ''}
${s.description ? `

${s.description}

` : ''}
${(s.matches || ['*://*/*']).slice(0, 3).map(m => ` ${m} `).join('')} ${(s.matches?.length > 3) ? `+${s.matches.length - 3}` : ''}
`).join('') : `
${icon('FileCode', 'w-10 h-10 text-slate-300 mb-2')}

No user scripts found

Create a new OpenScript to get started.

`}
`; }; const renderEditor = () => { const script = state.scripts.find(s => s.id === state.editingId); const code = script ? script.code : getBoilerplate(); const runAt = script?.runAt || 'document_idle'; return `
`; }; const renderSecrets = () => { const keys = Object.keys(state.secrets); return `

${icon('Key', 'w-3.5 h-3.5 text-sky-600')} Synced Secrets / Environment Variables

Secrets are saved to chrome.storage.sync and available across all devices.

${keys.length ? keys.map(k => { const isRevealed = state.revealedSecrets.has(k); const val = state.secrets[k]; return `
${k} = ${isRevealed ? val : '••••••••••••'}
`; }).join('') : `
${icon('Key', 'w-8 h-8 text-slate-300 mb-2')}

No secrets configured

Add API tokens or credentials (like GH_PAT) here to securely access them in your user scripts.

`}

Code usage in scripts:

const token = OpenScript.env.GH_PAT;
// or: const token = env.GH_PAT;
const cache = await OpenScript.storage.get('cache');
`; }; // Main Render const render = () => { let content = ''; if (state.tab === 'list') content = renderScriptList(); else if (state.tab === 'editor') content = renderEditor(); else if (state.tab === 'secrets') content = renderSecrets(); app.innerHTML = ` ${renderHeader()} ${renderBanner()} ${content} `; renderIcons(); bindEvents(); }; // Event Bindings const bindEvents = () => { // Navigation $('#nav-brand')?.addEventListener('click', () => setTab('list')); $('#nav-list')?.addEventListener('click', () => setTab('list')); $('#nav-secrets')?.addEventListener('click', () => setTab('secrets')); $('#nav-new')?.addEventListener('click', () => setTab('editor', null)); $('#btn-empty-new')?.addEventListener('click', () => setTab('editor', null)); // Banner settings button $('#btn-open-settings')?.addEventListener('click', async () => { const extUrl = `chrome://extensions/?id=${chrome.runtime.id}`; try { await chrome.tabs.create({ url: extUrl }); } catch { await navigator.clipboard.writeText(extUrl); showToast('Copied extension URL to clipboard!'); } }); // Search const searchInput = $('#script-search'); if (searchInput) { searchInput.addEventListener('input', e => { state.search = e.target.value; // Re-render only script list body or re-render render(); const el = $('#script-search'); if (el) { el.focus(); el.selectionStart = el.selectionEnd = el.value.length; } }); } // Script List Actions (delegated) app.querySelectorAll('[data-action="toggle"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); toggleScript(btn.dataset.id); }); }); app.querySelectorAll('[data-action="edit"]').forEach(btn => { btn.addEventListener('click', () => setTab('editor', btn.dataset.id)); }); app.querySelectorAll('[data-action="delete"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); deleteScript(btn.dataset.id); }); }); // Editor Actions $('#btn-save-script')?.addEventListener('click', saveCurrentScript); $('#btn-cancel-edit')?.addEventListener('click', () => setTab('list')); $('#btn-reset-boilerplate')?.addEventListener('click', () => { const el = $('#editor-code'); if (el && confirm('Reset code to default boilerplate?')) el.value = getBoilerplate(); }); // Tab key indent in editor const textarea = $('#editor-code'); if (textarea) { textarea.addEventListener('keydown', e => { if (e.key === 'Tab') { e.preventDefault(); const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 2; } }); } // Secrets Actions $('#btn-add-secret')?.addEventListener('click', () => { const k = $('#new-secret-key')?.value || ''; const v = $('#new-secret-val')?.value || ''; addSecret(k, v); }); app.querySelectorAll('[data-action="toggle-secret"]').forEach(btn => { btn.addEventListener('click', () => { const k = btn.dataset.key; if (state.revealedSecrets.has(k)) state.revealedSecrets.delete(k); else state.revealedSecrets.add(k); render(); }); }); app.querySelectorAll('[data-action="copy-secret"]').forEach(btn => { btn.addEventListener('click', () => { const val = state.secrets[btn.dataset.key] || ''; navigator.clipboard.writeText(val); showToast('Copied to clipboard'); }); }); app.querySelectorAll('[data-action="delete-secret"]').forEach(btn => { btn.addEventListener('click', () => removeSecret(btn.dataset.key)); }); }; // Start init(); // Auto re-check when returning to popup after toggling setting window.addEventListener('focus', async () => { const ready = await isUserScriptsAvailable(); if (ready !== state.userScriptsReady) { state.userScriptsReady = ready; if (ready) await syncScripts(false); render(); } });