Snippets: add built-in templates (collapsed "Templates" section in the snippets view and the terminal snippet panel): authorize an SSH key from the keychain for a user, install the QEMU guest agent, set/remove an APT cache server (apt-cacher-ng), system update; parameters are asked for before running
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+83
-2
@@ -658,7 +658,7 @@ function viewSnippets(page) {
|
||||
page.append(toolbar(T('Search snippets …'), [{ label: T('New snippet'), icon: 'plus', cls: 'primary', run: () => editSnippet() }]));
|
||||
const c = h('<div class="content"></div>'); page.append(c);
|
||||
const list = S.vault.snippets.filter((s) => matches(s.label, s.command));
|
||||
if (!list.length) return c.append(emptyState('code', T('No snippets'), T('Save frequently used commands and send them to a terminal with one click.'), T('New snippet'), () => editSnippet()));
|
||||
if (!list.length) { c.append(emptyState('code', T('No snippets'), T('Save frequently used commands and send them to a terminal with one click.'), T('New snippet'), () => editSnippet())); c.append(templateList()); return; }
|
||||
const l = h('<div class="list"></div>');
|
||||
for (const s of list) {
|
||||
const card = h(`<div class="card"><div class="avatar" style="background:var(--icon-bg);color:var(--green)">${ICONS.code}</div><div class="meta"><div class="title">${esc(s.label)}</div><div class="sub mono">${esc(s.command.split('\n')[0])}${s.command.includes('\n') ? ' …' : ''}</div></div><div class="actions"><button data-a="run" title="${T('Run in active terminal')}">${ICONS.play}</button><button data-a="copy" title="${T('Copy')}">${ICONS.copy}</button><button data-a="del" title="${T('Delete')}">${ICONS.trash}</button></div></div>`);
|
||||
@@ -671,7 +671,7 @@ function viewSnippets(page) {
|
||||
};
|
||||
l.append(card);
|
||||
}
|
||||
c.append(l);
|
||||
c.append(l, templateList());
|
||||
}
|
||||
function editSnippet(s = {}) {
|
||||
const form = openDrawer(s.id ? T('Edit snippet') : T('New snippet'), [
|
||||
@@ -685,6 +685,86 @@ function editSnippet(s = {}) {
|
||||
reload();
|
||||
});
|
||||
}
|
||||
// Eingebaute Vorlagen (nicht im Vault). Parameter werden vor dem Ausführen abgefragt; build() liefert eine Zeile.
|
||||
// $S = sudo, außer man ist bereits root.
|
||||
const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
||||
const SUDO = 'S=$([ "$(id -u)" = 0 ] || echo sudo);';
|
||||
const SNIPPET_TEMPLATES = [
|
||||
{
|
||||
label: () => T('Authorize SSH key for a user'),
|
||||
desc: () => T('Adds a public key from the keychain to ~/.ssh/authorized_keys of the user.'),
|
||||
params: () => [
|
||||
S.vault.keys.some((k) => k.publicKey)
|
||||
? field(T('Key'), 'key', '', { type: 'select', options: S.vault.keys.filter((k) => k.publicKey).map((k) => [k.id, k.label]) })
|
||||
: field('Public Key', 'pub', '', { type: 'textarea', placeholder: 'ssh-ed25519 AAAA…' }),
|
||||
field(T('User'), 'user', 'root'),
|
||||
],
|
||||
build: (v) => {
|
||||
const pub = (v.key ? S.vault.keys.find((k) => k.id === v.key)?.publicKey : v.pub || '').trim();
|
||||
if (!pub) throw new Error(T('Public key is missing.'));
|
||||
if (!v.user.trim()) throw new Error(T('User is missing.'));
|
||||
return `${SUDO} u=${shq(v.user.trim())}; k=${shq(pub)}; d="$(getent passwd "$u" | cut -d: -f6)/.ssh"; `
|
||||
+ `$S install -d -m 700 -o "$u" -g "$(id -gn "$u")" "$d" && { $S grep -qxF "$k" "$d/authorized_keys" 2>/dev/null || echo "$k" | $S tee -a "$d/authorized_keys" >/dev/null; } `
|
||||
+ `&& $S chown "$u": "$d/authorized_keys" && $S chmod 600 "$d/authorized_keys" && echo "OK: $d/authorized_keys"`;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => T('Install QEMU guest agent'),
|
||||
desc: () => T('Installs and starts qemu-guest-agent (apt, dnf, yum, pacman, zypper or apk).'),
|
||||
build: () => `${SUDO} if command -v apt-get >/dev/null; then $S apt-get update && $S apt-get install -y qemu-guest-agent; `
|
||||
+ 'elif command -v dnf >/dev/null; then $S dnf install -y qemu-guest-agent; elif command -v yum >/dev/null; then $S yum install -y qemu-guest-agent; '
|
||||
+ 'elif command -v pacman >/dev/null; then $S pacman -S --needed --noconfirm qemu-guest-agent; elif command -v zypper >/dev/null; then $S zypper -n install qemu-guest-agent; '
|
||||
+ 'elif command -v apk >/dev/null; then $S apk add qemu-guest-agent && $S rc-update add qemu-guest-agent && $S rc-service qemu-guest-agent start; fi; '
|
||||
+ 'command -v systemctl >/dev/null && { $S systemctl enable --now qemu-guest-agent 2>/dev/null || $S systemctl start qemu-guest-agent; }; '
|
||||
+ 'command -v systemctl >/dev/null && systemctl is-active qemu-guest-agent',
|
||||
},
|
||||
{
|
||||
label: () => T('Use APT cache server'),
|
||||
desc: () => T('Routes APT downloads through a cache proxy such as apt-cacher-ng (/etc/apt/apt.conf.d/00proxy).'),
|
||||
params: () => [field(T('Proxy URL'), 'url', 'http://', { placeholder: 'http://192.168.1.10:3142' })],
|
||||
build: (v) => {
|
||||
const url = v.url.trim();
|
||||
if (!/^https?:\/\/[^\s/]+/.test(url)) throw new Error(T('Invalid URL.'));
|
||||
return `${SUDO} echo ${shq(`Acquire::http::Proxy "${url}";`)} | $S tee /etc/apt/apt.conf.d/00proxy && $S apt-get update`;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => T('Remove APT cache server'),
|
||||
desc: () => T('Removes the APT proxy setting again.'),
|
||||
build: () => `${SUDO} $S rm -f /etc/apt/apt.conf.d/00proxy && $S apt-get update`,
|
||||
},
|
||||
{
|
||||
label: () => T('System update'),
|
||||
desc: () => T('Updates all packages with the system package manager.'),
|
||||
build: () => `${SUDO} if command -v apt-get >/dev/null; then $S apt-get update && $S apt-get -y full-upgrade; elif command -v dnf >/dev/null; then $S dnf -y upgrade; `
|
||||
+ 'elif command -v pacman >/dev/null; then $S pacman -Syu --noconfirm; elif command -v zypper >/dev/null; then $S zypper -n update; elif command -v apk >/dev/null; then $S apk upgrade --update; fi',
|
||||
},
|
||||
];
|
||||
|
||||
// Fragt die Parameter einer Vorlage ab und schickt den Befehl ins Terminal
|
||||
async function runTemplate(t, session) {
|
||||
const params = t.params ? t.params() : [];
|
||||
let v = {};
|
||||
if (params.length) {
|
||||
const body = `<p style="margin-top:0;color:var(--muted)">${esc(t.desc())}</p>${params.map((el) => el.outerHTML).join('')}`;
|
||||
v = await modal({ title: t.label(), body, buttons: [{ label: T('Cancel'), value: null, cls: 'ghost' }, { label: T('Run'), value: 'form', cls: 'primary' }] });
|
||||
if (!v) return;
|
||||
}
|
||||
let command;
|
||||
try { command = t.build(v); } catch (e) { return toast(e.message, 'error'); }
|
||||
runSnippet({ command, autoRun: true }, session);
|
||||
}
|
||||
|
||||
function templateList(session) {
|
||||
const d = h(`<details class="snip-tpl"><summary>${T('Templates')}</summary><div class="tpl-items"></div></details>`);
|
||||
for (const t of SNIPPET_TEMPLATES) {
|
||||
const it = h(`<div class="it"><div>${esc(t.label())}</div><div class="hint">${esc(t.desc())}</div></div>`);
|
||||
it.onclick = () => runTemplate(t, session);
|
||||
$('.tpl-items', d).append(it);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function runSnippet(s, session) {
|
||||
const t = session || S.tabs.find((x) => x.id === S.active && x.kind === 'ssh') || [...S.tabs].reverse().find((x) => x.kind === 'ssh');
|
||||
if (!t) return toast(T('No open terminal'), 'error');
|
||||
@@ -1504,6 +1584,7 @@ class TerminalSession {
|
||||
it.onclick = () => runSnippet(s, this);
|
||||
box.append(it);
|
||||
});
|
||||
box.append(templateList(this));
|
||||
}
|
||||
|
||||
toggleFind(open) {
|
||||
|
||||
Reference in New Issue
Block a user