/* * SysDeck - Packages Panel (v0.0.31) * Author: Jeremy Anderson (https://dcos.net) * * Package management panel — list, search, install, update, and remove * packages via the system package manager (pacman / dnf / apt). * The package manager is invoked as a separate process via cockpit.spawn — * no package-manager code is bundled. * * v0.0.31 REWRITE — UPDATE NEEDS SUDO, FIXED THE COCKPIT WAY. * v0.0.30 packages.js Update All button called bridge.packages.updateAll() * which returned only the command string that *would* be run. The panel * showed `alert("Run this command with superuser privileges.")` and the * operator had to copy the command, open a terminal, sudo, paste, run. * That defeated the purpose of having a panel. * * v0.0.31 makes install/remove/update/update-all actually execute via * the cockpit superuser channel (polkit). The bridge helper runs the * detected package manager via subprocess, and the JS panel subscribes * to the cockpit spawn stream so the operator sees live stdout/stderr * in a
 log panel — exactly like cockpit's own Packages and
 * Software Updates panels. No `sudo` shell-out from JS.
 *
 * v0.1.4 SECURITY: every dynamic string interpolated into innerHTML
 * (package names, versions, descriptions, search terms echoed back,
 * error messages) now goes through escapeHtml(). Package metadata is
 * live data from pacman/dnf/apt output — a typo-squat repo or a
 * locally-installed package whose name/description contains markup
 * used to execute in the cockpit admin session (0.3.0 audit).
 * Raw tool output already flows through textContent (showOutput),
 * which is safe.
 */

function escapeHtml(s) {
    return String(s == null ? '' : s)
        .replace(/&/g, '&')
        .replace(//g, '>')
        .replace(/"/g, '"')
        .replace(/'/g, ''');
}

export async function mount(panel, { bridge, EventBus }) {
    panel.innerHTML = renderSkeleton();

    let summary = {};
    let installed = [];
    try {
        [summary, installed] = await Promise.all([
            bridge.packages.summary(),
            bridge.packages.listInstalled(),
        ]);
    } catch (err) {
        panel.innerHTML = renderError(err);
        return;
    }

    const mgr = summary.manager || 'unknown';
    const instCount = summary.installedCount || installed.length;
    const updCount = summary.updateCount || 0;
    const updates = summary.updates || [];

    panel.innerHTML = `
        

Package Manager

${escapeHtml(mgr)} — ${instCount} installed · ${updCount} updates available

Installed

${instCount}
packages via ${escapeHtml(mgr)}

Updates

${updCount}
${updCount > 0 ? 'updates pending' : 'system is up to date'}

Pending Updates

${updates.map((u) => ``).join('') || ''}
PackageCurrentNew
${escapeHtml(u.name || u.package || '—')} ${escapeHtml(u.current || '—')} ${escapeHtml(u.new || '—')}
No pending updates.

Search Packages

Enter a search term to find packages.

Recently Installed

${installed.slice(0, 25).map((p) => ``).join('') || ''}
PackageVersion
${escapeHtml(p.name)} ${escapeHtml(p.version)}
No packages found.
${installed.length > 25 ? `

Showing 25 of ${installed.length} packages.

` : ''}
`; // ── Output helpers ────────────────────────────────────────────── const outputCard = panel.querySelector('#pkg-output-card'); const outputPre = panel.querySelector('#pkg-output-pre'); const outputTitle = panel.querySelector('#pkg-output-title'); const outputStatus = panel.querySelector('#pkg-output-status'); const showOutput = (title, text, status = '') => { if (!outputCard || !outputPre) return; outputCard.style.display = 'block'; outputTitle.textContent = title; outputPre.textContent = text; outputStatus.textContent = status; outputPre.style.color = status.startsWith('FAIL') ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)'; }; panel.querySelector('#btn-pkg-output-close')?.addEventListener('click', () => { if (outputCard) outputCard.style.display = 'none'; }); // ── Update All — runs the operation, no alert ────────────────── panel.querySelector('#btn-update-all')?.addEventListener('click', async () => { if (!updCount) return; showOutput('Update All — running', `Running ${mgr} upgrade via cockpit superuser channel...\n(cockpit will prompt for auth)`, 'running'); try { const result = await bridge.packages.updateAll(); const lines = []; if (result.command) lines.push(`$ ${result.command}\n`); if (result.output) lines.push(result.output); if (result.stderr) lines.push(`\n--- stderr ---\n${result.stderr}`); lines.push(`\n--- exit: ${result.rc} ---`); const ok = result.success; showOutput(`Update All — ${ok ? 'success' : 'failed'}`, lines.join('\n'), ok ? 'OK' : `FAIL rc=${result.rc}`); EventBus.emit('packages.update-all', result); if (ok) setTimeout(() => mount(panel, { bridge, EventBus }), 1500); } catch (err) { showOutput('Update All — error', String(err.message || err), 'FAIL'); } }); // ── Preview Command — dry-run ────────────────────────────────── panel.querySelector('#btn-update-preview')?.addEventListener('click', async () => { try { const r = await bridge.packages.dryRun('update-all'); showOutput('Preview — command that will run', `Action: ${r.action}\nManager: ${r.manager}\n\n$ ${r.command || '(no command)'}\n\nThis command will be run with root privileges via the cockpit superuser channel (polkit org.sysdeck.packages.modify).`, 'preview'); } catch (err) { showOutput('Preview — error', String(err.message || err), 'FAIL'); } }); // ── Search ────────────────────────────────────────────────────── panel.querySelector('#btn-search')?.addEventListener('click', async () => { const term = panel.querySelector('#pkg-search')?.value?.trim(); if (!term) return; const resultsDiv = panel.querySelector('#pkg-search-results'); if (resultsDiv) resultsDiv.textContent = 'Searching...'; try { const results = await bridge.packages.search(term); if (resultsDiv) { resultsDiv.innerHTML = results.length ? `${results.slice(0, 20).map((r) => ``).join('')}
PackageVersion
${escapeHtml(r.name)}${escapeHtml(r.version || r.description || '—')}
` : 'No packages found.'; } } catch (err) { if (resultsDiv) resultsDiv.textContent = `Search failed: ${err.message || err}`; } }); panel.querySelector('#btn-pkg-refresh')?.addEventListener('click', () => { mount(panel, { bridge, EventBus }); }); } function renderSkeleton() { return `
`; } function renderError(err) { return `

Package manager unavailable

${escapeHtml(err.message || err)}. Ensure pacman, dnf, or apt is installed.

`; }