/** * Admin > System > Packages * * Full package lifecycle: list, install (upload + registry), enable/disable, * settings, export, delete. Uses /api/v1/admin/packages endpoints. */ const { html } = window; const { useState, useEffect, useCallback } = hooks; const TYPE_OPTIONS = ['all', 'surface', 'extension', 'full', 'workflow']; const CORE_IDS = new Set(['admin']); function typeBadge(type) { const cls = type === 'surface' ? 'badge-active' : type === 'extension' ? 'badge-pending' : ''; return html`${type || '?'}`; } function tierBadge(tier) { const cls = tier === 'browser' ? 'badge-active' : tier === 'starlark' ? 'badge-pending' : ''; return html`${tier || 'unknown'}`; } function sourceBadge(source) { if (!source || source === 'extension') return null; const cls = source === 'core' ? 'badge-active' : source === 'registry' ? 'badge-pending' : ''; return html`${source}`; } function statusBadge(status) { if (status === 'dormant') return html`dormant`; if (status === 'pending_review') return html`review`; if (status === 'suspended') return html`suspended`; return null; } export default function PackagesSection() { const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(true); const [typeFilter, setTypeFilter] = useState('all'); const [expandedId, setExpandedId] = useState(null); const [pkgSettings, setPkgSettings] = useState(null); const [settingsDraft, setSettingsDraft] = useState({}); const [registryOpen, setRegistryOpen] = useState(false); const [registryPkgs, setRegistryPkgs] = useState([]); const [registryLoading, setRegistryLoading] = useState(false); const [installing, setInstalling] = useState(false); const [permsId, setPermsId] = useState(null); // v0.5.0: permissions drawer const [perms, setPerms] = useState([]); const BASE = window.__BASE__ || ''; // ── Load packages ─────────────────────────── const load = useCallback(async () => { try { const opts = typeFilter !== 'all' ? { type: typeFilter } : undefined; const data = await sw.api.admin.packages.list(opts); setPackages(data || []); } catch (e) { sw.toast(e.message, 'error'); } finally { setLoading(false); } }, [typeFilter]); useEffect(() => { setLoading(true); load(); }, [typeFilter]); // ── Actions ───────────────────────────────── async function toggleEnabled(pkg) { if (CORE_IDS.has(pkg.id)) return; if (pkg.status === 'dormant') { sw.toast('This package requires chat — it cannot be enabled yet', 'error'); return; } try { if (pkg.enabled) { await sw.api.admin.packages.disable(pkg.id); sw.toast(`${pkg.title || pkg.id} disabled`, 'success'); } else { await sw.api.admin.packages.enable(pkg.id); sw.toast(`${pkg.title || pkg.id} enabled`, 'success'); } load(); } catch (e) { sw.toast(e.message, 'error'); } } async function deletePkg(pkg) { if (pkg.source === 'core') return; const ok = await sw.confirm(`Delete "${pkg.title || pkg.id}"? This cannot be undone.`, true); if (!ok) return; try { await sw.api.admin.packages.del(pkg.id); sw.toast('Package deleted', 'success'); if (expandedId === pkg.id) { setExpandedId(null); setPkgSettings(null); } load(); } catch (e) { sw.toast(e.message, 'error'); } } function uploadPackage() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.pkg,.surface,.zip'; input.onchange = async () => { const file = input.files[0]; if (!file) return; setInstalling(true); try { await sw.api.admin.packages.install(file); sw.toast('Package installed', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } finally { setInstalling(false); } }; input.click(); } function exportPkg(id) { window.open(`${BASE}/api/v1/admin/packages/${id}/export`, '_blank'); } // ── Settings drawer ───────────────────────── async function toggleSettings(pkgId) { if (expandedId === pkgId) { setExpandedId(null); setPkgSettings(null); setSettingsDraft({}); return; } try { const data = await sw.api.admin.packages.settings(pkgId); const settings = data?.data || {}; setPkgSettings(settings); setSettingsDraft({ ...settings }); setExpandedId(pkgId); } catch (e) { sw.toast(e.message, 'error'); } } async function saveSettings(pkgId) { try { await sw.api.admin.packages.updateSettings(pkgId, settingsDraft); sw.toast('Settings saved', 'success'); setPkgSettings({ ...settingsDraft }); } catch (e) { sw.toast(e.message, 'error'); } } // ── Permissions drawer (v0.5.0) ──────────── async function togglePerms(pkgId) { if (permsId === pkgId) { setPermsId(null); setPerms([]); return; } try { const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); setPermsId(pkgId); // Close settings if open if (expandedId === pkgId) { setExpandedId(null); setPkgSettings(null); } } catch (e) { sw.toast(e.message, 'error'); } } async function grantPerm(pkgId, perm) { try { await sw.api.admin.packages.grantPerm(pkgId, perm); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); load(); } catch (e) { sw.toast(e.message, 'error'); } } async function revokePerm(pkgId, perm) { try { await sw.api.admin.packages.revokePerm(pkgId, perm); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); load(); } catch (e) { sw.toast(e.message, 'error'); } } async function grantAllPerms(pkgId) { try { await sw.api.admin.packages.grantAllPerms(pkgId); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); sw.toast('All permissions granted', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } } // ── Registry ──────────────────────────────── async function toggleRegistry() { if (registryOpen) { setRegistryOpen(false); return; } setRegistryOpen(true); setRegistryLoading(true); try { const data = await sw.api.admin.packages.registry(); setRegistryPkgs(data?.data || []); } catch (e) { sw.toast('Registry unavailable: ' + e.message, 'error'); setRegistryPkgs([]); } finally { setRegistryLoading(false); } } async function installFromRegistry(url) { try { await sw.api.admin.packages.registryInstall(url); sw.toast('Package installed from registry', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } } // ── Render ─────────────────────────────────── if (loading) return html`
Loading\u2026
`; const enabled = packages.filter(p => p.enabled).length; const dormant = packages.filter(p => p.status === 'dormant').length; const hasManifestSettings = (pkg) => pkg.manifest?.settings || pkg.package_settings; const hasDeclaredPerms = (pkg) => pkg.manifest?.permissions && pkg.manifest.permissions.length > 0; return html`
${/* ── Stat cards ─────────────── */``}
${packages.length}
Total
${enabled}
Enabled
${dormant > 0 && html`
${dormant}
Dormant
`}
${/* ── Action bar ─────────────── */``}
${TYPE_OPTIONS.map(t => html` `)}
${/* ── Package list ────────────── */``}
${packages.length === 0 && html`
No packages found
`} ${packages.map(pkg => html`
${pkg.title || pkg.id} v${pkg.version || '?'} ${' '}${typeBadge(pkg.type)} ${' '}${tierBadge(pkg.tier)} ${sourceBadge(pkg.source)} ${statusBadge(pkg.status)} ${pkg.is_system && html`system`}
${hasManifestSettings(pkg) && html` `} ${hasDeclaredPerms(pkg) && html` `} ${pkg.source !== 'core' && !pkg.is_system && html` `}
${/* ── Inline settings drawer ── */``} ${expandedId === pkg.id && pkgSettings != null && html`
${Object.keys(pkgSettings).length === 0 ? html`No configurable settings` : html` ${Object.entries(settingsDraft).map(([k, v]) => html`
setSettingsDraft(prev => ({ ...prev, [k]: e.target.value }))} style="flex:1;padding:4px 8px;background:var(--bg-1,var(--bg));color:var(--text);border:1px solid var(--border);border-radius:4px;font-size:12px;" />
`)} ` }
`} ${/* ── Inline permissions drawer (v0.5.0) ── */``} ${permsId === pkg.id && html`
${perms.length === 0 ? html`No permissions declared` : html`
${perms.map(p => html`
${p.permission} ${p.granted && p.granted_by && html` by ${p.granted_by} `}
`)}
${perms.some(p => !p.granted) && html` `} ` }
`}
`)}
${/* ── Registry panel ──────────── */``} ${registryOpen && html`

Package Registry

${registryLoading ? html`
Loading registry\u2026
` : registryPkgs.length === 0 ? html`
No packages available in registry
` : html`
${registryPkgs.map(rp => html`
${rp.title || rp.id} v${rp.version || '?'} ${rp.description && html`
${rp.description}
`}
${typeBadge(rp.type)}
`)}
` }
`}
`; }