Compare commits

...
Author SHA1 Message Date
Codex b6b45fc11a fix(plugins): complete manager locale parity and local CLI assertions 2026-10-26 03:15:24 +00:00
Codex d601d3d238 feat(plugins): add installed package management to settings 2026-10-26 02:59:36 +00:00
Codex e20f2a9986 fix(plugins): disable incompatible plugins before lazy contributions 2026-10-26 02:54:41 +00:00
Netcatty-Botandgithub-actions[bot] 0256e7471d Update Nix release metadata for v1.1.89 (#3931)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-26 09:45:44 +08:00
github-actions[bot]andgithub-actions[bot] a4b1009c2a chore: update star history charts [skip ci] (#3932)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-26 00:10:10 +00:00
陈大猫 54b6f85b09 fix(fonts): include SFMono Nerd Font in the terminal font picker (#3930)
Add `sfmono` to KNOWN_MONOSPACE_FONTS so glued typographic family names like "SFMono Nerd Font" appear in the Settings → Terminal font picker. Matching logic unchanged.

Fixes #3929
2026-10-25 22:46:09 +08:00
23 changed files with 500 additions and 40 deletions

No files matched your search

+18
View File
@@ -1235,6 +1235,24 @@ Highlight the focused split pane:
'deepLink.jms.invalid': 'Invalid JumpServer link',
'deepLink.jms.unsupported': 'Unsupported JumpServer protocol: {{protocol}}',
'settings.tab.plugins': 'Plugins',
"settings.plugins.manager.installed": "Installed plugins",
"settings.plugins.manager.import": "Import .ncpkg",
"settings.plugins.manager.preview": "Developer preview. Imported packages remain disabled until you review and enable them.",
"settings.plugins.manager.empty": "Import a local plugin package to get started.",
"settings.plugins.manager.enabled": "Enabled",
"settings.plugins.manager.disabled": "Disabled",
"settings.plugins.manager.enable": "Enable",
"settings.plugins.manager.disable": "Disable",
"settings.plugins.manager.remove": "Uninstall",
"settings.plugins.manager.removeNote": "Uninstall removes package code and local KV data. User-owned settings, secrets and sync sidecars are retained.",
"settings.plugins.manager.permissions": "Declared permissions",
"settings.plugins.manager.permissionNote": "Read from the installed manifest. Declarations are not grants; sensitive operations still require host approval.",
"settings.plugins.manager.noPermissions": "No privileged permissions declared.",
"settings.plugins.manager.required": "Required",
"settings.plugins.manager.optional": "Optional",
"settings.plugins.manager.browser": "Browser runtime: privileged access goes through host permission brokers.",
"settings.plugins.manager.advanced": "Advanced runtime: full Node filesystem and network authority. Broker permissions cannot restrict this access. Development preview only.",
"settings.plugins.manager.data": "Local KV stays on this device. Only non-secret settings explicitly marked for sync participate in encrypted cloud sidecars. Packages and permission grants do not sync.",
'settings.plugins.title': 'Plugin settings',
'settings.plugins.description': 'Native, validated settings contributed by development plugins.',
'settings.plugins.loading': 'Loading plugin settings…',
+18
View File
@@ -1235,6 +1235,24 @@ Resalta el panel dividido enfocado:
'deepLink.jms.invalid': 'Enlace JumpServer inválido',
'deepLink.jms.unsupported': 'Protocolo JumpServer no compatible: {{protocol}}',
'settings.tab.plugins': 'Plugins',
"settings.plugins.manager.installed": "Plugins instalados",
"settings.plugins.manager.import": "Importar .ncpkg",
"settings.plugins.manager.preview": "Vista previa para desarrolladores. Los paquetes importados permanecen desactivados hasta que los revises y actives.",
"settings.plugins.manager.empty": "Importa un paquete local para empezar.",
"settings.plugins.manager.enabled": "Activado",
"settings.plugins.manager.disabled": "Desactivado",
"settings.plugins.manager.enable": "Activar",
"settings.plugins.manager.disable": "Desactivar",
"settings.plugins.manager.remove": "Desinstalar",
"settings.plugins.manager.removeNote": "La desinstalación elimina el código y los datos KV locales. Se conservan los ajustes del usuario, los secretos y los datos auxiliares de sincronización.",
"settings.plugins.manager.permissions": "Permisos declarados",
"settings.plugins.manager.permissionNote": "Obtenidos del manifiesto instalado. Las declaraciones no son autorizaciones; las operaciones sensibles requieren aprobación del anfitrión.",
"settings.plugins.manager.noPermissions": "No se han declarado permisos privilegiados.",
"settings.plugins.manager.required": "Obligatorio",
"settings.plugins.manager.optional": "Opcional",
"settings.plugins.manager.browser": "Entorno de navegador: el acceso privilegiado pasa por los controles de permisos del anfitrión.",
"settings.plugins.manager.advanced": "Entorno avanzado: acceso completo de Node al sistema de archivos y a la red. Los permisos del intermediario no restringen este acceso. Solo para desarrollo.",
"settings.plugins.manager.data": "Los datos KV permanecen en este dispositivo. Solo los ajustes no secretos marcados para sincronización participan en los datos auxiliares cifrados. Los paquetes y las autorizaciones no se sincronizan.",
'settings.plugins.title': 'Configuración de plugins',
'settings.plugins.description': 'Ajustes nativos y validados que aportan los plugins de desarrollo.',
'settings.plugins.loading': 'Cargando ajustes del plugin…',
+18
View File
@@ -1130,6 +1130,24 @@ export const ruCoreMessages: Messages = {
'deepLink.jms.unsupported': 'Неподдерживаемый протокол JumpServer: {{protocol}}',
'settings.tab.plugins': 'Плагины',
"settings.plugins.manager.installed": "Установленные плагины",
"settings.plugins.manager.import": "Импорт .ncpkg",
"settings.plugins.manager.preview": "Предварительная версия для разработчиков. Импортированные пакеты отключены до проверки и включения.",
"settings.plugins.manager.empty": "Импортируйте локальный пакет плагина.",
"settings.plugins.manager.enabled": "Включён",
"settings.plugins.manager.disabled": "Отключён",
"settings.plugins.manager.enable": "Включить",
"settings.plugins.manager.disable": "Отключить",
"settings.plugins.manager.remove": "Удалить",
"settings.plugins.manager.removeNote": "Удаление очищает код пакета и локальные данные KV. Настройки пользователя, секреты и данные синхронизации сохраняются.",
"settings.plugins.manager.permissions": "Заявленные разрешения",
"settings.plugins.manager.permissionNote": "Из установленного манифеста. Объявление не является разрешением; чувствительные операции требуют одобрения приложения.",
"settings.plugins.manager.noPermissions": "Привилегированные разрешения не заявлены.",
"settings.plugins.manager.required": "Обязательно",
"settings.plugins.manager.optional": "Необязательно",
"settings.plugins.manager.browser": "Браузерная среда: привилегированный доступ проходит через проверку разрешений приложения.",
"settings.plugins.manager.advanced": "Расширенная среда: полный доступ Node к файловой системе и сети. Разрешения посредника не ограничивают этот доступ. Только для разработки.",
"settings.plugins.manager.data": "Данные KV остаются на устройстве. Синхронизируются только явно отмеченные несекретные настройки в зашифрованных данных. Пакеты и разрешения не синхронизируются.",
'settings.plugins.title': 'Настройки плагинов',
'settings.plugins.description': 'Проверенные нативные настройки от плагинов разработки.',
'settings.plugins.loading': 'Загрузка настроек плагинов…',
+18
View File
@@ -1150,6 +1150,24 @@ Vaults 标签保持扁平,不使用这块背景。
'qs.localShells': '本地 Shell',
'qs.default': '默认',
'settings.tab.plugins': '插件',
"settings.plugins.manager.installed": "已安装插件",
"settings.plugins.manager.import": "导入 .ncpkg",
"settings.plugins.manager.preview": "开发者预览。导入的插件保持停用,请检查后再启用。",
"settings.plugins.manager.empty": "导入本地插件包以开始使用。",
"settings.plugins.manager.enabled": "已启用",
"settings.plugins.manager.disabled": "已停用",
"settings.plugins.manager.enable": "启用",
"settings.plugins.manager.disable": "停用",
"settings.plugins.manager.remove": "卸载",
"settings.plugins.manager.removeNote": "卸载会移除插件代码和本地 KV 数据,保留用户设置、秘密存储和同步 sidecar。",
"settings.plugins.manager.permissions": "声明的权限",
"settings.plugins.manager.permissionNote": "来自已安装的 manifest。声明不等于授权,敏感操作仍须通过宿主批准。",
"settings.plugins.manager.noPermissions": "未声明特权权限。",
"settings.plugins.manager.required": "必需",
"settings.plugins.manager.optional": "可选",
"settings.plugins.manager.browser": "浏览器运行时:特权访问通过宿主权限代理。",
"settings.plugins.manager.advanced": "高级运行时:拥有完整 Node 文件系统和网络能力,代理权限无法限制这些访问。仅供开发预览。",
"settings.plugins.manager.data": "本地 KV 仅保留在此设备。只有明确标记为同步的非秘密设置进入加密云 sidecar。插件包和权限授权不参与同步。",
'settings.plugins.title': '插件设置',
'settings.plugins.description': '由开发插件提供、经 Netcatty 原生组件验证的设置。',
'settings.plugins.loading': '正在加载插件设置…',
+18
View File
@@ -1153,6 +1153,24 @@ Vaults 分頁維持扁平,不使用這塊背景。
'qs.default': '預設',
'settings.tab.plugins': '外掛程式',
"settings.plugins.manager.installed": "已安裝插件",
"settings.plugins.manager.import": "導入 .ncpkg",
"settings.plugins.manager.preview": "開發者預覽。導入的插件保持停用,請檢查後再啟用。",
"settings.plugins.manager.empty": "導入本地插件包以開始使用。",
"settings.plugins.manager.enabled": "已啟用",
"settings.plugins.manager.disabled": "已停用",
"settings.plugins.manager.enable": "啟用",
"settings.plugins.manager.disable": "停用",
"settings.plugins.manager.remove": "卸載",
"settings.plugins.manager.removeNote": "卸載會移除插件代碼和本地 KV 數據,保留用戶設置、秘密存儲和同步 sidecar。",
"settings.plugins.manager.permissions": "聲明的權限",
"settings.plugins.manager.permissionNote": "來自已安裝的 manifest。聲明不等於授權,敏感操作仍須通過宿主批准。",
"settings.plugins.manager.noPermissions": "未聲明特權權限。",
"settings.plugins.manager.required": "必需",
"settings.plugins.manager.optional": "可選",
"settings.plugins.manager.browser": "瀏覽器運行時:特權訪問通過宿主權限代理。",
"settings.plugins.manager.advanced": "高級運行時:擁有完整 Node 文件系統和網絡能力,代理權限無法限制這些訪問。僅供開發預覽。",
"settings.plugins.manager.data": "本地 KV 僅保留在此設備。只有明確標記為同步的非秘密設置進入加密雲 sidecar。插件包和權限授權不參與同步。",
'settings.plugins.title': '外掛程式設定',
'settings.plugins.description': '由開發外掛程式提供、經 Netcatty 原生元件驗證的設定。',
'settings.plugins.loading': '正在載入外掛程式設定…',
+56
View File
@@ -0,0 +1,56 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
export function usePluginManager() {
const [plugins, setPlugins] = useState<NetcattyInstalledPlugin[]>([]);
const [available, setAvailable] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const generation = useRef(0);
const mutation = useRef(false);
const refresh = useCallback(async () => {
const current = ++generation.current;
const bridge = netcattyBridge.get();
const status = await bridge?.getPluginRuntimeStatus?.();
const list = status?.available ? await bridge?.listPlugins?.() ?? [] : [];
if (current !== generation.current) return;
setAvailable(status?.available === true);
setPlugins(list);
}, []);
useEffect(() => {
const update = () => { void refresh().catch((cause) => setError(String(cause))); };
update();
const unsubscribe = netcattyBridge.get()?.onPluginContributionsChanged?.(update);
const refreshGeneration = generation;
return () => { refreshGeneration.current++; unsubscribe?.(); };
}, [refresh]);
const run = useCallback(async (action: (bridge: NetcattyBridge) => Promise<unknown>) => {
if (mutation.current) return;
mutation.current = true;
setBusy(true);
setError(null);
try { await action(netcattyBridge.require()); }
catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
finally {
try { await refresh(); } catch (cause) { setError(String(cause)); }
mutation.current = false;
setBusy(false);
}
}, [refresh]);
return {
plugins, available, busy, error,
importPackage: () => run(async (bridge) => {
if (!bridge.selectFile || !bridge.installPluginPackage) throw new Error('Plugin import unavailable');
const path = await bridge.selectFile('Import plugin package', undefined, [{ name: 'Netcatty plugin', extensions: ['ncpkg'] }]);
if (path) await bridge.installPluginPackage(path, { enable: false });
}),
setEnabled: (id: string, enabled: boolean) => run(async (bridge) => {
if (!bridge.setPluginEnabled) throw new Error('Plugin management unavailable');
return bridge.setPluginEnabled(id, enabled);
}),
uninstall: (id: string) => run(async (bridge) => {
if (!bridge.uninstallPlugin) throw new Error('Plugin management unavailable');
return bridge.uninstallPlugin(id);
}),
};
}
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '../../../application/i18n/I18nProvider';
import { PluginPackageDetails } from './PluginPackageManager';
test('package detail renders manifest resources as text and explains advanced authority', () => {
const plugin = {
id: 'com.example.synthetic', enabled: false, activeVersion: '1.0.0',
archiveSha256: 'a'.repeat(64),
manifest: {
manifestVersion: 1, id: 'com.example.synthetic', name: 'synthetic', version: '1.0.0', publisher: 'example',
engines: { netcatty: '>=1.0.0 <2.0.0', api: '0.1.0-internal' },
main: { node: 'index.js' },
permissions: { required: [{ permission: 'network', resources: ['https://example.com'], reason: '<script>attack()</script>' }] },
},
runtime: { status: 'stopped', kind: null, lastError: null, quarantinedAt: null },
} as NetcattyInstalledPlugin;
const html = renderToStaticMarkup(<I18nProvider locale="en"><PluginPackageDetails plugin={plugin} /></I18nProvider>);
assert.match(html, /full Node filesystem and network authority/);
assert.match(html, /https:\/\/example.com/);
assert.match(html, /&lt;script&gt;/);
assert.doesNotMatch(html, /<script>/);
assert.match(html, /Declarations are not grants/);
assert.match(html, /aaaaaaaaaaaaaaaa/);
});
@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { Package, Upload } from 'lucide-react';
import { usePluginManager } from '../../../application/state/usePluginManager';
import { useI18n } from '../../../application/i18n/I18nProvider';
import { Button } from '../../ui/button';
export function PluginPackageDetails({ plugin }: { plugin: NetcattyInstalledPlugin }) {
const { t } = useI18n();
const manifest = plugin.manifest;
if (!manifest) return null;
const permissions = [
...(manifest.permissions?.required ?? []).map((value) => ({ value, required: true })),
...(manifest.permissions?.optional ?? []).map((value) => ({ value, required: false })),
];
const advanced = !manifest.main.browser || Boolean(manifest.companionExecutables?.length)
|| manifest.contributes?.providers?.some((p) => p.kind === 'terminal.interceptor.input' || p.kind === 'terminal.interceptor.output');
return <div className="space-y-4 border-t border-border/60 pt-4 text-sm">
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-xs">
<dt className="text-muted-foreground">ID</dt><dd className="break-all font-mono">{plugin.id}</dd>
<dt className="text-muted-foreground">Netcatty</dt><dd>{manifest.engines.netcatty}</dd>
<dt className="text-muted-foreground">API</dt><dd>{manifest.engines.api}</dd>
<dt className="text-muted-foreground">SHA-256</dt><dd className="break-all font-mono">{plugin.archiveSha256 ?? '-'}</dd>
</dl>
<p className={advanced ? 'text-destructive' : 'text-muted-foreground'}>{t(advanced ? 'settings.plugins.manager.advanced' : 'settings.plugins.manager.browser')}</p>
<div className="space-y-2">
<h4 className="font-medium">{t('settings.plugins.manager.permissions')}</h4>
<p className="text-xs text-muted-foreground">{t('settings.plugins.manager.permissionNote')}</p>
{permissions.length === 0 && <p className="text-xs text-muted-foreground">{t('settings.plugins.manager.noPermissions')}</p>}
{permissions.map(({ value, required }, index) => <div key={index} className="rounded-md bg-muted/40 p-3">
<div className="flex justify-between gap-3"><code className="break-all text-xs">{typeof value === 'string' ? value : value.permission}</code><span className="text-xs text-muted-foreground">{t(required ? 'settings.plugins.manager.required' : 'settings.plugins.manager.optional')}</span></div>
{typeof value !== 'string' && <><p className="mt-1 whitespace-pre-wrap break-all text-xs">{value.resources.join('\n')}</p>{value.reason && <p className="mt-1 text-xs text-muted-foreground">{value.reason}</p>}</>}
</div>)}
</div>
<p className="text-xs text-muted-foreground">{t('settings.plugins.manager.data')}</p>
</div>;
}
export function PluginPackageManager() {
const { t, resolvedLocale } = useI18n();
const manager = usePluginManager();
const [selected, setSelected] = useState<string | null>(null);
const [removing, setRemoving] = useState<string | null>(null);
if (!manager.available) return null;
return <section className="space-y-4" aria-label={t('settings.plugins.manager.installed')}>
<div className="flex items-center justify-between gap-3">
<h3 className="font-medium">{t('settings.plugins.manager.installed')} <span className="text-muted-foreground">{manager.plugins.length}</span></h3>
<Button variant="outline" size="sm" disabled={manager.busy} onClick={() => void manager.importPackage()}><Upload size={14} className="mr-2" />{t('settings.plugins.manager.import')}</Button>
</div>
<p className="text-xs text-muted-foreground">{t('settings.plugins.manager.preview')}</p>
{manager.error && <p role="alert" className="text-sm text-destructive">{manager.error}</p>}
{!manager.plugins.length && <div className="rounded-xl border border-dashed border-border p-8 text-center text-sm text-muted-foreground">{t('settings.plugins.manager.empty')}</div>}
{manager.plugins.map((plugin) => <article key={plugin.id} className="space-y-4 rounded-xl border border-border/70 bg-background p-4">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-muted p-2"><Package size={20} /></div>
<button type="button" className="min-w-0 flex-1 text-left" aria-expanded={selected === plugin.id} onClick={() => setSelected(selected === plugin.id ? null : plugin.id)}>
<div className="truncate text-sm font-medium">{typeof plugin.manifest?.displayName === 'string' ? plugin.manifest.displayName : plugin.manifest?.displayName?.[resolvedLocale] ?? plugin.manifest?.displayName?.en ?? plugin.manifest?.name ?? plugin.id}</div>
<div className="text-xs text-muted-foreground">{plugin.activeVersion} · {t(plugin.enabled ? 'settings.plugins.manager.enabled' : 'settings.plugins.manager.disabled')}</div>
</button>
<Button variant="outline" size="sm" disabled={manager.busy} onClick={() => void manager.setEnabled(plugin.id, !plugin.enabled)}>{t(plugin.enabled ? 'settings.plugins.manager.disable' : 'settings.plugins.manager.enable')}</Button>
</div>
{plugin.runtime.lastError && <p role="status" className="break-words text-xs text-destructive">{plugin.runtime.lastError}</p>}
{selected === plugin.id && <>
<PluginPackageDetails plugin={plugin} />
{removing === plugin.id ? <div className="space-y-2 rounded-lg border border-destructive/30 p-3">
<p className="text-xs">{t('settings.plugins.manager.removeNote')}</p>
<Button variant="destructive" size="sm" disabled={manager.busy} onClick={() => { void manager.uninstall(plugin.id); setRemoving(null); }}>{t('settings.plugins.manager.remove')}</Button>
<Button variant="ghost" size="sm" onClick={() => setRemoving(null)}>{t('common.cancel')}</Button>
</div> : <Button variant="ghost" size="sm" disabled={manager.busy} onClick={() => setRemoving(plugin.id)}>{t('settings.plugins.manager.remove')}</Button>}
</>}
</article>)}
</section>;
}
@@ -1,3 +1,4 @@
import { PluginPackageManager } from './PluginPackageManager';
import { FolderOpen } from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';
@@ -307,6 +308,7 @@ export default function SettingsPluginsTab() {
<h2 className="text-xl font-semibold">{t('settings.plugins.title')}</h2>
<p className="mt-1 text-sm text-muted-foreground">{t('settings.plugins.description')}</p>
</div>
<PluginPackageManager />
{contextualScopes.size > 0 && (
<section className="grid gap-3 rounded-lg border border-border/70 bg-muted/10 p-4 sm:grid-cols-2" aria-label={t('settings.plugins.scopeTargets')}>
{[...contextualScopes].map((kind) => (
+9 -12
View File
@@ -31,20 +31,17 @@
<text x="85" y="85.0" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<text x="43" y="271.5" fill="#e6edf3" font-size="17" font-family="Handlee, cursive" font-weight="bold" transform="rotate(-90, 43, 271.5)">GitHub Stars</text>
<text x="90.0" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jan 2026</text>
<text x="223.6" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="296.2" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Apr 2026</text>
<text x="366.6" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="439.2" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jun 2026</text>
<text x="509.5" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="582.2" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Aug 2026</text>
<text x="654.8" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="725.2" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Oct 2026</text>
<text x="220.4" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="291.4" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Apr 2026</text>
<text x="430.9" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jun 2026</text>
<text x="570.5" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Aug 2026</text>
<text x="710.1" y="488" fill="#e6edf3" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Oct 2026</text>
<text x="50%" y="525" fill="#e6edf3" font-size="17" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Date</text>
<line x1="90" y1="80" x2="90" y2="463" stroke="#30363d" stroke-width="2.5" filter="url(#xkcdify)"/>
<line x1="90" y1="463" x2="765" y2="463" stroke="#30363d" stroke-width="2.5" filter="url(#xkcdify)"/>
<path d="M 90.0,462.2 C 90.4,462.1 92.0,461.8 92.3,461.7 C 93.1,461.6 93.9,461.4 94.7,461.2 C 95.5,461.1 96.2,460.8 97.0,460.8 C 97.8,460.7 98.6,460.7 99.4,460.7 C 100.2,460.7 100.9,460.6 101.7,460.6 C 102.5,460.5 103.3,460.5 104.1,460.4 C 104.8,460.4 105.2,460.3 106.4,460.3 C 107.6,460.2 109.9,460.1 111.1,460.1 C 112.3,460.1 112.7,460.0 113.4,460.0 C 114.2,460.0 115.0,460.0 115.8,459.9 C 116.6,459.9 117.3,459.9 118.1,459.9 C 118.9,459.9 119.7,459.9 120.5,459.8 C 121.2,459.8 122.0,459.7 122.8,459.7 C 123.6,459.6 124.4,459.6 125.2,459.4 C 125.9,459.3 126.7,459.0 127.5,458.7 C 128.3,458.3 129.1,457.6 129.8,457.3 C 130.6,457.1 131.4,457.2 132.2,457.1 C 133.0,457.1 133.8,457.1 134.5,457.0 C 135.3,457.0 135.7,457.0 136.9,456.9 C 138.0,456.9 140.0,456.9 141.6,456.9 C 143.1,456.8 145.1,456.8 146.2,456.8 C 147.4,456.7 147.8,456.7 148.6,456.7 C 149.4,456.6 150.2,456.6 150.9,456.5 C 151.7,456.5 152.5,456.5 153.3,456.4 C 154.1,456.4 154.8,456.4 155.6,456.4 C 156.4,456.4 156.8,456.4 158.0,456.3 C 159.1,456.3 160.7,456.2 162.7,456.2 C 164.6,456.2 167.7,456.2 169.7,456.2 C 171.6,456.1 172.0,456.2 174.4,456.1 C 176.7,456.1 181.8,455.9 183.8,455.9 C 185.7,455.8 184.9,455.9 186.1,455.8 C 187.3,455.8 188.8,455.8 190.8,455.8 C 192.7,455.8 193.9,455.7 197.8,455.7 C 201.7,455.6 211.1,455.5 214.2,455.5 C 217.3,455.5 215.8,455.5 216.6,455.4 C 217.3,455.4 217.7,456.1 218.9,455.4 C 220.1,454.7 222.4,454.1 223.6,451.3 C 224.8,448.4 225.2,440.7 225.9,438.3 C 226.7,435.9 227.5,437.3 228.3,436.9 C 229.1,436.5 229.8,436.3 230.6,436.0 C 231.4,435.8 232.2,435.6 233.0,435.4 C 233.8,435.3 234.5,435.3 235.3,435.2 C 236.1,435.1 236.9,434.9 237.7,434.8 C 238.4,434.8 239.2,434.8 240.0,434.8 C 240.8,434.7 241.6,434.5 242.3,434.4 C 243.1,434.3 243.9,434.3 244.7,434.2 C 245.5,434.1 246.2,433.7 247.0,433.6 C 247.8,433.5 248.6,433.5 249.4,433.5 C 250.2,433.4 250.5,433.2 251.7,433.1 C 252.9,433.0 255.2,433.0 256.4,432.9 C 257.6,432.9 258.0,432.8 258.8,432.7 C 259.5,432.7 260.3,432.7 261.1,432.6 C 261.9,432.5 262.7,432.2 263.4,432.1 C 264.2,431.9 265.0,432.6 265.8,431.7 C 266.6,430.8 267.3,427.6 268.1,426.6 C 268.9,425.5 269.7,425.7 270.5,425.5 C 271.2,425.3 272.0,425.5 272.8,425.4 C 273.6,425.3 274.4,424.9 275.2,424.6 C 275.9,424.4 276.7,424.2 277.5,423.7 C 278.3,423.2 279.1,422.2 279.8,421.6 C 280.6,421.0 281.4,420.4 282.2,420.0 C 283.0,419.5 283.8,419.2 284.5,419.0 C 285.3,418.7 286.1,418.6 286.9,418.5 C 287.7,418.3 288.4,418.4 289.2,418.2 C 290.0,418.1 290.8,417.9 291.6,417.8 C 292.3,417.7 293.1,417.8 293.9,417.7 C 294.7,417.6 295.5,417.6 296.2,417.5 C 297.0,417.4 297.8,417.4 298.6,417.0 C 299.4,416.6 300.2,415.5 300.9,415.0 C 301.7,414.5 302.5,414.3 303.3,414.0 C 304.1,413.6 304.8,414.3 305.6,412.8 C 306.4,411.2 307.2,406.4 308.0,404.6 C 308.8,402.7 309.5,402.4 310.3,401.7 C 311.1,400.9 311.9,400.4 312.7,399.9 C 313.4,399.3 314.2,398.8 315.0,398.4 C 315.8,398.0 316.6,397.6 317.3,397.3 C 318.1,397.1 318.9,397.0 319.7,396.8 C 320.5,396.6 321.2,396.3 322.0,396.0 C 322.8,395.7 323.6,395.2 324.4,395.0 C 325.2,394.7 325.9,394.8 326.7,394.5 C 327.5,394.2 328.3,393.8 329.1,393.5 C 329.8,393.1 330.6,392.9 331.4,392.6 C 332.2,392.4 333.0,392.2 333.8,392.0 C 334.5,391.9 335.3,391.8 336.1,391.7 C 336.9,391.6 337.7,391.4 338.4,391.2 C 339.2,391.0 340.0,390.9 340.8,390.7 C 341.6,390.5 342.3,390.3 343.1,390.0 C 343.9,389.6 344.7,389.0 345.5,388.8 C 346.2,388.5 347.0,388.4 347.8,388.2 C 348.6,388.1 349.4,387.9 350.2,387.8 C 350.9,387.7 351.7,387.6 352.5,387.5 C 353.3,387.4 354.1,387.2 354.8,387.2 C 355.6,387.1 356.4,387.1 357.2,387.0 C 358.0,386.9 358.8,386.7 359.5,386.4 C 360.3,386.1 361.1,385.6 361.9,385.3 C 362.7,385.0 363.4,384.6 364.2,384.4 C 365.0,384.2 365.8,384.1 366.6,384.0 C 367.3,383.9 368.1,383.8 368.9,383.6 C 369.7,383.3 370.5,382.8 371.2,382.6 C 372.0,382.4 372.8,382.4 373.6,382.3 C 374.4,382.2 375.2,382.0 375.9,381.8 C 376.7,381.6 377.5,381.4 378.3,381.3 C 379.1,381.1 379.8,380.8 380.6,380.7 C 381.4,380.5 382.2,380.4 383.0,380.2 C 383.8,379.9 384.5,379.6 385.3,379.3 C 386.1,379.1 386.9,379.0 387.7,378.7 C 388.4,378.5 389.2,378.2 390.0,377.9 C 390.8,377.6 391.6,377.4 392.3,377.1 C 393.1,376.8 393.9,376.8 394.7,376.4 C 395.5,376.0 396.2,375.1 397.0,374.7 C 397.8,374.3 398.6,374.3 399.4,374.1 C 400.2,374.0 400.9,374.0 401.7,373.9 C 402.5,373.8 403.3,373.7 404.1,373.5 C 404.8,373.4 405.6,373.6 406.4,373.3 C 407.2,372.9 408.0,371.8 408.8,371.4 C 409.5,371.0 410.3,371.1 411.1,370.8 C 411.9,370.6 412.7,370.1 413.4,369.9 C 414.2,369.7 415.0,369.8 415.8,369.6 C 416.6,369.4 417.3,368.9 418.1,368.7 C 418.9,368.5 419.7,368.5 420.5,368.4 C 421.2,368.3 422.0,368.3 422.8,368.1 C 423.6,367.9 424.4,367.6 425.2,367.1 C 425.9,366.6 426.7,365.5 427.5,365.0 C 428.3,364.5 429.1,364.2 429.8,363.9 C 430.6,363.5 431.4,363.4 432.2,362.7 C 433.0,361.9 433.8,360.2 434.5,359.4 C 435.3,358.6 436.1,358.3 436.9,357.8 C 437.7,357.3 438.4,357.2 439.2,356.5 C 440.0,355.8 440.8,354.2 441.6,353.5 C 442.3,352.8 443.1,352.7 443.9,352.3 C 444.7,351.9 445.5,351.5 446.2,351.1 C 447.0,3Line truncated
<path d="M 90.0,462.2 C 90.4,462.1 92.0,461.8 92.3,461.7 C 93.1,461.6 93.9,461.4 94.7,461.2 C 95.5,461.1 96.2,460.8 97.0,460.8 C 97.8,460.7 98.6,460.7 99.4,460.7 C 100.2,460.7 100.9,460.6 101.7,460.6 C 102.5,460.5 103.3,460.5 104.1,460.4 C 104.8,460.4 105.2,460.3 106.4,460.3 C 107.6,460.2 109.9,460.1 111.1,460.1 C 112.3,460.1 112.7,460.0 113.4,460.0 C 114.2,460.0 115.0,460.0 115.8,459.9 C 116.6,459.9 117.3,459.9 118.1,459.9 C 118.9,459.9 119.7,459.9 120.5,459.8 C 121.2,459.8 122.0,459.7 122.8,459.7 C 123.6,459.6 124.4,459.6 125.2,459.4 C 125.9,459.3 126.7,459.0 127.5,458.7 C 128.3,458.3 129.1,457.6 129.8,457.3 C 130.6,457.1 131.4,457.2 132.2,457.1 C 133.0,457.1 133.8,457.1 134.5,457.0 C 135.3,457.0 135.7,457.0 136.9,456.9 C 138.0,456.9 140.0,456.9 141.6,456.9 C 143.1,456.8 145.1,456.8 146.2,456.8 C 147.4,456.7 147.8,456.7 148.6,456.7 C 149.4,456.6 150.2,456.6 150.9,456.5 C 151.7,456.5 152.5,456.5 153.3,456.4 C 154.1,456.4 154.8,456.4 155.6,456.4 C 156.4,456.4 156.8,456.4 158.0,456.3 C 159.1,456.3 160.7,456.2 162.7,456.2 C 164.6,456.2 167.7,456.2 169.7,456.2 C 171.6,456.1 172.0,456.2 174.4,456.1 C 176.7,456.1 181.8,455.9 183.8,455.9 C 185.7,455.8 184.9,455.9 186.1,455.8 C 187.3,455.8 188.8,455.8 190.8,455.8 C 192.7,455.8 193.9,455.7 197.8,455.7 C 201.7,455.6 211.1,455.5 214.2,455.5 C 217.3,455.5 215.8,455.5 216.6,455.4 C 217.3,455.4 217.7,456.1 218.9,455.4 C 220.1,454.7 222.4,454.1 223.6,451.3 C 224.8,448.4 225.2,440.7 225.9,438.3 C 226.7,435.9 227.5,437.3 228.3,436.9 C 229.1,436.5 229.8,436.3 230.6,436.0 C 231.4,435.8 232.2,435.6 233.0,435.4 C 233.8,435.3 234.5,435.3 235.3,435.2 C 236.1,435.1 236.9,434.9 237.7,434.8 C 238.4,434.8 239.2,434.8 240.0,434.8 C 240.8,434.7 241.6,434.5 242.3,434.4 C 243.1,434.3 243.9,434.3 244.7,434.2 C 245.5,434.1 246.2,433.7 247.0,433.6 C 247.8,433.5 248.6,433.5 249.4,433.5 C 250.2,433.4 250.5,433.2 251.7,433.1 C 252.9,433.0 255.2,433.0 256.4,432.9 C 257.6,432.9 258.0,432.8 258.8,432.7 C 259.5,432.7 260.3,432.7 261.1,432.6 C 261.9,432.5 262.7,432.2 263.4,432.1 C 264.2,431.9 265.0,432.6 265.8,431.7 C 266.6,430.8 267.3,427.6 268.1,426.6 C 268.9,425.5 269.7,425.7 270.5,425.5 C 271.2,425.3 272.0,425.5 272.8,425.4 C 273.6,425.3 274.4,424.9 275.2,424.6 C 275.9,424.4 276.7,424.2 277.5,423.7 C 278.3,423.2 279.1,422.2 279.8,421.6 C 280.6,421.0 281.4,420.4 282.2,420.0 C 283.0,419.5 283.8,419.2 284.5,419.0 C 285.3,418.7 286.1,418.6 286.9,418.5 C 287.7,418.3 288.4,418.4 289.2,418.2 C 290.0,418.1 290.8,417.9 291.6,417.8 C 292.3,417.7 293.1,417.8 293.9,417.7 C 294.7,417.6 295.5,417.6 296.2,417.5 C 297.0,417.4 297.8,417.4 298.6,417.0 C 299.4,416.6 300.2,415.5 300.9,415.0 C 301.7,414.5 302.5,414.3 303.3,414.0 C 304.1,413.6 304.8,414.3 305.6,412.8 C 306.4,411.2 307.2,406.4 308.0,404.6 C 308.8,402.7 309.5,402.4 310.3,401.7 C 311.1,400.9 311.9,400.4 312.7,399.9 C 313.4,399.3 314.2,398.8 315.0,398.4 C 315.8,398.0 316.6,397.6 317.3,397.3 C 318.1,397.1 318.9,397.0 319.7,396.8 C 320.5,396.6 321.2,396.3 322.0,396.0 C 322.8,395.7 323.6,395.2 324.4,395.0 C 325.2,394.7 325.9,394.8 326.7,394.5 C 327.5,394.2 328.3,393.8 329.1,393.5 C 329.8,393.1 330.6,392.9 331.4,392.6 C 332.2,392.4 333.0,392.2 333.8,392.0 C 334.5,391.9 335.3,391.8 336.1,391.7 C 336.9,391.6 337.7,391.4 338.4,391.2 C 339.2,391.0 340.0,390.9 340.8,390.7 C 341.6,390.5 342.3,390.3 343.1,390.0 C 343.9,389.6 344.7,389.0 345.5,388.8 C 346.2,388.5 347.0,388.4 347.8,388.2 C 348.6,388.1 349.4,387.9 350.2,387.8 C 350.9,387.7 351.7,387.6 352.5,387.5 C 353.3,387.4 354.1,387.2 354.8,387.2 C 355.6,387.1 356.4,387.1 357.2,387.0 C 358.0,386.9 358.8,386.7 359.5,386.4 C 360.3,386.1 361.1,385.6 361.9,385.3 C 362.7,385.0 363.4,384.6 364.2,384.4 C 365.0,384.2 365.8,384.1 366.6,384.0 C 367.3,383.9 368.1,383.8 368.9,383.6 C 369.7,383.3 370.5,382.8 371.2,382.6 C 372.0,382.4 372.8,382.4 373.6,382.3 C 374.4,382.2 375.2,382.0 375.9,381.8 C 376.7,381.6 377.5,381.4 378.3,381.3 C 379.1,381.1 379.8,380.8 380.6,380.7 C 381.4,380.5 382.2,380.4 383.0,380.2 C 383.8,379.9 384.5,379.6 385.3,379.3 C 386.1,379.1 386.9,379.0 387.7,378.7 C 388.4,378.5 389.2,378.2 390.0,377.9 C 390.8,377.6 391.6,377.4 392.3,377.1 C 393.1,376.8 393.9,376.8 394.7,376.4 C 395.5,376.0 396.2,375.1 397.0,374.7 C 397.8,374.3 398.6,374.3 399.4,374.1 C 400.2,374.0 400.9,374.0 401.7,373.9 C 402.5,373.8 403.3,373.7 404.1,373.5 C 404.8,373.4 405.6,373.6 406.4,373.3 C 407.2,372.9 408.0,371.8 408.8,371.4 C 409.5,371.0 410.3,371.1 411.1,370.8 C 411.9,370.6 412.7,370.1 413.4,369.9 C 414.2,369.7 415.0,369.8 415.8,369.6 C 416.6,369.4 417.3,368.9 418.1,368.7 C 418.9,368.5 419.7,368.5 420.5,368.4 C 421.2,368.3 422.0,368.3 422.8,368.1 C 423.6,367.9 424.4,367.6 425.2,367.1 C 425.9,366.6 426.7,365.5 427.5,365.0 C 428.3,364.5 429.1,364.2 429.8,363.9 C 430.6,363.5 431.4,363.4 432.2,362.7 C 433.0,361.9 433.8,360.2 434.5,359.4 C 435.3,358.6 436.1,358.3 436.9,357.8 C 437.7,357.3 438.4,357.2 439.2,356.5 C 440.0,355.8 440.8,354.2 441.6,353.5 C 442.3,352.8 443.1,352.7 443.9,352.3 C 444.7,351.9 445.5,351.5 446.2,351.1 C 447.0,3Line truncated
<circle cx="765.0" cy="99.0" r="5" fill="#dd4528" stroke="#0d1117" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6652</text>
<path d="M 90.0,462.2 C 90.4,462.1 91.9,461.8 92.3,461.7 C 93.1,461.6 93.8,461.4 94.6,461.2 C 95.3,461.1 96.1,460.8 96.9,460.8 C 97.6,460.7 98.4,460.7 99.2,460.7 C 99.9,460.7 100.7,460.6 101.4,460.6 C 102.2,460.5 103.0,460.5 103.7,460.4 C 104.5,460.4 104.9,460.3 106.0,460.3 C 107.2,460.2 109.4,460.1 110.6,460.1 C 111.7,460.1 112.1,460.0 112.9,460.0 C 113.6,460.0 114.4,460.0 115.2,459.9 C 115.9,459.9 116.7,459.9 117.5,459.9 C 118.2,459.9 119.0,459.9 119.7,459.8 C 120.5,459.8 121.3,459.7 122.0,459.7 C 122.8,459.6 123.6,459.6 124.3,459.4 C 125.1,459.3 125.8,459.0 126.6,458.7 C 127.4,458.3 128.1,457.6 128.9,457.3 C 129.7,457.1 130.4,457.2 131.2,457.1 C 131.9,457.1 132.7,457.1 133.5,457.0 C 134.2,457.0 134.6,457.0 135.8,456.9 C 136.9,456.9 138.8,456.9 140.3,456.9 C 141.9,456.8 143.8,456.8 144.9,456.8 C 146.1,456.7 146.4,456.7 147.2,456.7 C 148.0,456.6 148.7,456.6 149.5,456.5 C 150.3,456.5 151.0,456.5 151.8,456.4 C 152.5,456.4 153.3,456.4 154.1,456.4 C 154.8,456.4 155.2,456.4 156.4,456.3 C 157.5,456.3 159.0,456.2 160.9,456.2 C 162.8,456.2 165.9,456.2 167.8,456.2 C 169.7,456.1 170.1,456.2 172.4,456.1 C 174.7,456.1 179.6,455.9 181.5,455.9 C 183.4,455.8 182.7,455.9 183.8,455.8 C 185.0,455.8 186.5,455.8 188.4,455.8 C 190.3,455.8 191.4,455.7 195.3,455.7 C 199.1,455.6 208.2,455.5 211.3,455.5 C 214.3,455.5 212.8,455.5 213.6,455.4 C 214.3,455.4 214.7,456.1 215.8,455.4 C 217.0,454.7 219.3,454.1 220.4,451.3 C 221.6,448.4 221.9,440.7 222.7,438.3 C 223.5,435.9 224.2,437.3 225.0,436.9 C 225.8,436.5 226.5,436.3 227.3,436.0 C 228.1,435.8 228.8,435.6 229.6,435.4 C 230.3,435.3 231.1,435.3 231.9,435.2 C 232.6,435.1 233.4,434.9 234.2,434.8 C 234.9,434.8 235.7,434.8 236.4,434.8 C 237.2,434.7 238.0,434.5 238.7,434.4 C 239.5,434.3 240.3,434.3 241.0,434.2 C 241.8,434.1 242.5,433.7 243.3,433.6 C 244.1,433.5 244.8,433.5 245.6,433.5 C 246.4,433.4 246.7,433.2 247.9,433.1 C 249.0,433.0 251.3,433.0 252.5,432.9 C 253.6,432.9 254.0,432.8 254.7,432.7 C 255.5,432.7 256.3,432.7 257.0,432.6 C 257.8,432.5 258.6,432.2 259.3,432.1 C 260.1,431.9 260.8,432.6 261.6,431.7 C 262.4,430.8 263.1,427.6 263.9,426.6 C 264.7,425.5 265.4,425.7 266.2,425.5 C 266.9,425.3 267.7,425.5 268.5,425.4 C 269.2,425.3 270.0,424.9 270.8,424.6 C 271.5,424.4 272.3,424.2 273.1,423.7 C 273.8,423.2 274.6,422.2 275.3,421.6 C 276.1,421.0 276.9,420.5 277.6,420.0 C 278.4,419.6 279.2,419.3 279.9,419.0 C 280.7,418.8 281.4,418.6 282.2,418.5 C 283.0,418.4 283.7,418.4 284.5,418.3 C 285.3,418.2 286.0,418.0 286.8,417.9 C 287.5,417.8 288.3,417.8 289.1,417.8 C 289.8,417.7 290.6,417.7 291.4,417.5 C 292.1,417.4 292.9,417.5 293.6,417.0 C 294.4,416.6 295.2,415.6 295.9,415.1 C 296.7,414.6 297.5,414.4 298.2,414.0 C 299.0,413.7 299.7,414.4 300.5,412.8 C 301.3,411.3 302.0,406.5 302.8,404.6 C 303.6,402.8 304.3,402.5 305.1,401.7 C 305.8,400.9 306.6,400.4 307.4,399.9 C 308.1,399.3 308.9,398.8 309.7,398.4 C 310.4,398.0 311.2,397.6 311.9,397.3 C 312.7,397.1 313.5,397.0 314.2,396.8 C 315.0,396.6 315.8,396.3 316.5,396.0 C 317.3,395.7 318.1,395.2 318.8,395.0 C 319.6,394.7 320.3,394.8 321.1,394.5 C 321.9,394.2 322.6,393.8 323.4,393.5 C 324.2,393.1 324.9,392.9 325.7,392.6 C 326.4,392.4 327.2,392.2 328.0,392.0 C 328.7,391.9 329.5,391.8 330.3,391.7 C 331.0,391.6 331.8,391.4 332.5,391.2 C 333.3,391.0 334.1,390.9 334.8,390.7 C 335.6,390.5 336.4,390.3 337.1,390.0 C 337.9,389.6 338.6,389.0 339.4,388.8 C 340.2,388.5 340.9,388.4 341.7,388.2 C 342.5,388.1 343.2,387.9 344.0,387.8 C 344.7,387.7 345.5,387.6 346.3,387.5 C 347.0,387.4 347.8,387.2 348.6,387.2 C 349.3,387.1 350.1,387.1 350.8,387.0 C 351.6,386.9 352.4,386.7 353.1,386.4 C 353.9,386.1 354.7,385.6 355.4,385.3 C 356.2,385.0 356.9,384.6 357.7,384.4 C 358.5,384.2 359.2,384.1 360.0,384.0 C 360.8,383.9 361.5,383.8 362.3,383.6 C 363.1,383.3 363.8,382.8 364.6,382.6 C 365.3,382.4 366.1,382.4 366.9,382.3 C 367.6,382.2 368.4,382.0 369.2,381.8 C 369.9,381.6 370.7,381.4 371.4,381.3 C 372.2,381.1 373.0,380.8 373.7,380.7 C 374.5,380.5 375.3,380.4 376.0,380.2 C 376.8,379.9 377.5,379.6 378.3,379.3 C 379.1,379.1 379.8,379.0 380.6,378.7 C 381.4,378.5 382.1,378.2 382.9,377.9 C 383.6,377.6 384.4,377.4 385.2,377.1 C 385.9,376.8 386.7,376.8 387.5,376.4 C 388.2,376.0 389.0,375.1 389.7,374.7 C 390.5,374.3 391.3,374.3 392.0,374.1 C 392.8,374.0 393.6,374.0 394.3,373.9 C 395.1,373.8 395.8,373.7 396.6,373.5 C 397.4,373.4 398.1,373.6 398.9,373.3 C 399.7,372.9 400.4,371.8 401.2,371.4 C 401.9,371.0 402.7,371.1 403.5,370.8 C 404.2,370.6 405.0,370.1 405.8,369.9 C 406.5,369.7 407.3,369.8 408.1,369.6 C 408.8,369.4 409.6,368.9 410.3,368.7 C 411.1,368.5 411.9,368.5 412.6,368.4 C 413.4,368.3 414.2,368.3 414.9,368.1 C 415.7,367.9 416.4,367.6 417.2,367.1 C 418.0,366.6 418.7,365.5 419.5,365.0 C 420.3,364.5 421.0,364.2 421.8,363.9 C 422.5,363.5 423.3,363.4 424.1,362.7 C 424.8,361.9 425.6,360.2 426.4,359.4 C 427.1,358.6 427.9,358.3 428.6,357.8 C 429.4,357.4 430.2,357.3 430.9,356.6 C 431.7,355.9 432.5,354.2 433.2,353.5 C 434.0,352.8 434.7,352.7 435.5,352.3 C 436.3,351.9 437.0,351.6 437.8,351.1 C 438.6,35Line truncated
<path d="M 90.0,462.2 C 90.4,462.1 91.9,461.8 92.3,461.7 C 93.1,461.6 93.8,461.4 94.6,461.2 C 95.3,461.1 96.1,460.8 96.9,460.8 C 97.6,460.7 98.4,460.7 99.2,460.7 C 99.9,460.7 100.7,460.6 101.4,460.6 C 102.2,460.5 103.0,460.5 103.7,460.4 C 104.5,460.4 104.9,460.3 106.0,460.3 C 107.2,460.2 109.4,460.1 110.6,460.1 C 111.7,460.1 112.1,460.0 112.9,460.0 C 113.6,460.0 114.4,460.0 115.2,459.9 C 115.9,459.9 116.7,459.9 117.5,459.9 C 118.2,459.9 119.0,459.9 119.7,459.8 C 120.5,459.8 121.3,459.7 122.0,459.7 C 122.8,459.6 123.6,459.6 124.3,459.4 C 125.1,459.3 125.8,459.0 126.6,458.7 C 127.4,458.3 128.1,457.6 128.9,457.3 C 129.7,457.1 130.4,457.2 131.2,457.1 C 131.9,457.1 132.7,457.1 133.5,457.0 C 134.2,457.0 134.6,457.0 135.8,456.9 C 136.9,456.9 138.8,456.9 140.3,456.9 C 141.9,456.8 143.8,456.8 144.9,456.8 C 146.1,456.7 146.4,456.7 147.2,456.7 C 148.0,456.6 148.7,456.6 149.5,456.5 C 150.3,456.5 151.0,456.5 151.8,456.4 C 152.5,456.4 153.3,456.4 154.1,456.4 C 154.8,456.4 155.2,456.4 156.4,456.3 C 157.5,456.3 159.0,456.2 160.9,456.2 C 162.8,456.2 165.9,456.2 167.8,456.2 C 169.7,456.1 170.1,456.2 172.4,456.1 C 174.7,456.1 179.6,455.9 181.5,455.9 C 183.4,455.8 182.7,455.9 183.8,455.8 C 185.0,455.8 186.5,455.8 188.4,455.8 C 190.3,455.8 191.4,455.7 195.3,455.7 C 199.1,455.6 208.2,455.5 211.3,455.5 C 214.3,455.5 212.8,455.5 213.6,455.4 C 214.3,455.4 214.7,456.1 215.8,455.4 C 217.0,454.7 219.3,454.1 220.4,451.3 C 221.6,448.4 221.9,440.7 222.7,438.3 C 223.5,435.9 224.2,437.3 225.0,436.9 C 225.8,436.5 226.5,436.3 227.3,436.0 C 228.1,435.8 228.8,435.6 229.6,435.4 C 230.3,435.3 231.1,435.3 231.9,435.2 C 232.6,435.1 233.4,434.9 234.2,434.8 C 234.9,434.8 235.7,434.8 236.4,434.8 C 237.2,434.7 238.0,434.5 238.7,434.4 C 239.5,434.3 240.3,434.3 241.0,434.2 C 241.8,434.1 242.5,433.7 243.3,433.6 C 244.1,433.5 244.8,433.5 245.6,433.5 C 246.4,433.4 246.7,433.2 247.9,433.1 C 249.0,433.0 251.3,433.0 252.5,432.9 C 253.6,432.9 254.0,432.8 254.7,432.7 C 255.5,432.7 256.3,432.7 257.0,432.6 C 257.8,432.5 258.6,432.2 259.3,432.1 C 260.1,431.9 260.8,432.6 261.6,431.7 C 262.4,430.8 263.1,427.6 263.9,426.6 C 264.7,425.5 265.4,425.7 266.2,425.5 C 266.9,425.3 267.7,425.5 268.5,425.4 C 269.2,425.3 270.0,424.9 270.8,424.6 C 271.5,424.4 272.3,424.2 273.1,423.7 C 273.8,423.2 274.6,422.2 275.3,421.6 C 276.1,421.0 276.9,420.5 277.6,420.0 C 278.4,419.6 279.2,419.3 279.9,419.0 C 280.7,418.8 281.4,418.6 282.2,418.5 C 283.0,418.4 283.7,418.4 284.5,418.3 C 285.3,418.2 286.0,418.0 286.8,417.9 C 287.5,417.8 288.3,417.8 289.1,417.8 C 289.8,417.7 290.6,417.7 291.4,417.5 C 292.1,417.4 292.9,417.5 293.6,417.0 C 294.4,416.6 295.2,415.6 295.9,415.1 C 296.7,414.6 297.5,414.4 298.2,414.0 C 299.0,413.7 299.7,414.4 300.5,412.8 C 301.3,411.3 302.0,406.5 302.8,404.6 C 303.6,402.8 304.3,402.5 305.1,401.7 C 305.8,400.9 306.6,400.4 307.4,399.9 C 308.1,399.3 308.9,398.8 309.7,398.4 C 310.4,398.0 311.2,397.6 311.9,397.3 C 312.7,397.1 313.5,397.0 314.2,396.8 C 315.0,396.6 315.8,396.3 316.5,396.0 C 317.3,395.7 318.1,395.2 318.8,395.0 C 319.6,394.7 320.3,394.8 321.1,394.5 C 321.9,394.2 322.6,393.8 323.4,393.5 C 324.2,393.1 324.9,392.9 325.7,392.6 C 326.4,392.4 327.2,392.2 328.0,392.0 C 328.7,391.9 329.5,391.8 330.3,391.7 C 331.0,391.6 331.8,391.4 332.5,391.2 C 333.3,391.0 334.1,390.9 334.8,390.7 C 335.6,390.5 336.4,390.3 337.1,390.0 C 337.9,389.6 338.6,389.0 339.4,388.8 C 340.2,388.5 340.9,388.4 341.7,388.2 C 342.5,388.1 343.2,387.9 344.0,387.8 C 344.7,387.7 345.5,387.6 346.3,387.5 C 347.0,387.4 347.8,387.2 348.6,387.2 C 349.3,387.1 350.1,387.1 350.8,387.0 C 351.6,386.9 352.4,386.7 353.1,386.4 C 353.9,386.1 354.7,385.6 355.4,385.3 C 356.2,385.0 356.9,384.6 357.7,384.4 C 358.5,384.2 359.2,384.1 360.0,384.0 C 360.8,383.9 361.5,383.8 362.3,383.6 C 363.1,383.3 363.8,382.8 364.6,382.6 C 365.3,382.4 366.1,382.4 366.9,382.3 C 367.6,382.2 368.4,382.0 369.2,381.8 C 369.9,381.6 370.7,381.4 371.4,381.3 C 372.2,381.1 373.0,380.8 373.7,380.7 C 374.5,380.5 375.3,380.4 376.0,380.2 C 376.8,379.9 377.5,379.6 378.3,379.3 C 379.1,379.1 379.8,379.0 380.6,378.7 C 381.4,378.5 382.1,378.2 382.9,377.9 C 383.6,377.6 384.4,377.4 385.2,377.1 C 385.9,376.8 386.7,376.8 387.5,376.4 C 388.2,376.0 389.0,375.1 389.7,374.7 C 390.5,374.3 391.3,374.3 392.0,374.1 C 392.8,374.0 393.6,374.0 394.3,373.9 C 395.1,373.8 395.8,373.7 396.6,373.5 C 397.4,373.4 398.1,373.6 398.9,373.3 C 399.7,372.9 400.4,371.8 401.2,371.4 C 401.9,371.0 402.7,371.1 403.5,370.8 C 404.2,370.6 405.0,370.1 405.8,369.9 C 406.5,369.7 407.3,369.8 408.1,369.6 C 408.8,369.4 409.6,368.9 410.3,368.7 C 411.1,368.5 411.9,368.5 412.6,368.4 C 413.4,368.3 414.2,368.3 414.9,368.1 C 415.7,367.9 416.4,367.6 417.2,367.1 C 418.0,366.6 418.7,365.5 419.5,365.0 C 420.3,364.5 421.0,364.2 421.8,363.9 C 422.5,363.5 423.3,363.4 424.1,362.7 C 424.8,361.9 425.6,360.2 426.4,359.4 C 427.1,358.6 427.9,358.3 428.6,357.8 C 429.4,357.4 430.2,357.3 430.9,356.6 C 431.7,355.9 432.5,354.2 433.2,353.5 C 434.0,352.8 434.7,352.7 435.5,352.3 C 436.3,351.9 437.0,351.6 437.8,351.1 C 438.6,35Line truncated
<circle cx="765.0" cy="90.8" r="5" fill="#dd4528" stroke="#0d1117" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6802</text>
<a href="https://github.com/carsteneu/mystarhistory" target="_blank" rel="noopener"><text x="763" y="527" font-size="13" font-family="Handlee, cursive" fill="#fff" text-anchor="end">Made with carsteneu/mystarhistory</text></a>
</svg>

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

@@ -31,20 +31,17 @@
<text x="85" y="85.0" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">7K</text>
<text x="43" y="271.5" fill="#000" font-size="17" font-family="Handlee, cursive" font-weight="bold" transform="rotate(-90, 43, 271.5)">GitHub Stars</text>
<text x="90.0" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jan 2026</text>
<text x="223.6" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="296.2" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Apr 2026</text>
<text x="366.6" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">May 2026</text>
<text x="439.2" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jun 2026</text>
<text x="509.5" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jul 2026</text>
<text x="582.2" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Aug 2026</text>
<text x="654.8" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Sep 2026</text>
<text x="725.2" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Oct 2026</text>
<text x="220.4" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Mar 2026</text>
<text x="291.4" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Apr 2026</text>
<text x="430.9" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Jun 2026</text>
<text x="570.5" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Aug 2026</text>
<text x="710.1" y="488" fill="#000" font-size="16" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Oct 2026</text>
<text x="50%" y="525" fill="#000" font-size="17" font-family="Handlee, cursive" font-weight="bold" text-anchor="middle">Date</text>
<line x1="90" y1="80" x2="90" y2="463" stroke="#222" stroke-width="2.5" filter="url(#xkcdify)"/>
<line x1="90" y1="463" x2="765" y2="463" stroke="#222" stroke-width="2.5" filter="url(#xkcdify)"/>
<path d="M 90.0,462.2 C 90.4,462.1 92.0,461.8 92.3,461.7 C 93.1,461.6 93.9,461.4 94.7,461.2 C 95.5,461.1 96.2,460.8 97.0,460.8 C 97.8,460.7 98.6,460.7 99.4,460.7 C 100.2,460.7 100.9,460.6 101.7,460.6 C 102.5,460.5 103.3,460.5 104.1,460.4 C 104.8,460.4 105.2,460.3 106.4,460.3 C 107.6,460.2 109.9,460.1 111.1,460.1 C 112.3,460.1 112.7,460.0 113.4,460.0 C 114.2,460.0 115.0,460.0 115.8,459.9 C 116.6,459.9 117.3,459.9 118.1,459.9 C 118.9,459.9 119.7,459.9 120.5,459.8 C 121.2,459.8 122.0,459.7 122.8,459.7 C 123.6,459.6 124.4,459.6 125.2,459.4 C 125.9,459.3 126.7,459.0 127.5,458.7 C 128.3,458.3 129.1,457.6 129.8,457.3 C 130.6,457.1 131.4,457.2 132.2,457.1 C 133.0,457.1 133.8,457.1 134.5,457.0 C 135.3,457.0 135.7,457.0 136.9,456.9 C 138.0,456.9 140.0,456.9 141.6,456.9 C 143.1,456.8 145.1,456.8 146.2,456.8 C 147.4,456.7 147.8,456.7 148.6,456.7 C 149.4,456.6 150.2,456.6 150.9,456.5 C 151.7,456.5 152.5,456.5 153.3,456.4 C 154.1,456.4 154.8,456.4 155.6,456.4 C 156.4,456.4 156.8,456.4 158.0,456.3 C 159.1,456.3 160.7,456.2 162.7,456.2 C 164.6,456.2 167.7,456.2 169.7,456.2 C 171.6,456.1 172.0,456.2 174.4,456.1 C 176.7,456.1 181.8,455.9 183.8,455.9 C 185.7,455.8 184.9,455.9 186.1,455.8 C 187.3,455.8 188.8,455.8 190.8,455.8 C 192.7,455.8 193.9,455.7 197.8,455.7 C 201.7,455.6 211.1,455.5 214.2,455.5 C 217.3,455.5 215.8,455.5 216.6,455.4 C 217.3,455.4 217.7,456.1 218.9,455.4 C 220.1,454.7 222.4,454.1 223.6,451.3 C 224.8,448.4 225.2,440.7 225.9,438.3 C 226.7,435.9 227.5,437.3 228.3,436.9 C 229.1,436.5 229.8,436.3 230.6,436.0 C 231.4,435.8 232.2,435.6 233.0,435.4 C 233.8,435.3 234.5,435.3 235.3,435.2 C 236.1,435.1 236.9,434.9 237.7,434.8 C 238.4,434.8 239.2,434.8 240.0,434.8 C 240.8,434.7 241.6,434.5 242.3,434.4 C 243.1,434.3 243.9,434.3 244.7,434.2 C 245.5,434.1 246.2,433.7 247.0,433.6 C 247.8,433.5 248.6,433.5 249.4,433.5 C 250.2,433.4 250.5,433.2 251.7,433.1 C 252.9,433.0 255.2,433.0 256.4,432.9 C 257.6,432.9 258.0,432.8 258.8,432.7 C 259.5,432.7 260.3,432.7 261.1,432.6 C 261.9,432.5 262.7,432.2 263.4,432.1 C 264.2,431.9 265.0,432.6 265.8,431.7 C 266.6,430.8 267.3,427.6 268.1,426.6 C 268.9,425.5 269.7,425.7 270.5,425.5 C 271.2,425.3 272.0,425.5 272.8,425.4 C 273.6,425.3 274.4,424.9 275.2,424.6 C 275.9,424.4 276.7,424.2 277.5,423.7 C 278.3,423.2 279.1,422.2 279.8,421.6 C 280.6,421.0 281.4,420.4 282.2,420.0 C 283.0,419.5 283.8,419.2 284.5,419.0 C 285.3,418.7 286.1,418.6 286.9,418.5 C 287.7,418.3 288.4,418.4 289.2,418.2 C 290.0,418.1 290.8,417.9 291.6,417.8 C 292.3,417.7 293.1,417.8 293.9,417.7 C 294.7,417.6 295.5,417.6 296.2,417.5 C 297.0,417.4 297.8,417.4 298.6,417.0 C 299.4,416.6 300.2,415.5 300.9,415.0 C 301.7,414.5 302.5,414.3 303.3,414.0 C 304.1,413.6 304.8,414.3 305.6,412.8 C 306.4,411.2 307.2,406.4 308.0,404.6 C 308.8,402.7 309.5,402.4 310.3,401.7 C 311.1,400.9 311.9,400.4 312.7,399.9 C 313.4,399.3 314.2,398.8 315.0,398.4 C 315.8,398.0 316.6,397.6 317.3,397.3 C 318.1,397.1 318.9,397.0 319.7,396.8 C 320.5,396.6 321.2,396.3 322.0,396.0 C 322.8,395.7 323.6,395.2 324.4,395.0 C 325.2,394.7 325.9,394.8 326.7,394.5 C 327.5,394.2 328.3,393.8 329.1,393.5 C 329.8,393.1 330.6,392.9 331.4,392.6 C 332.2,392.4 333.0,392.2 333.8,392.0 C 334.5,391.9 335.3,391.8 336.1,391.7 C 336.9,391.6 337.7,391.4 338.4,391.2 C 339.2,391.0 340.0,390.9 340.8,390.7 C 341.6,390.5 342.3,390.3 343.1,390.0 C 343.9,389.6 344.7,389.0 345.5,388.8 C 346.2,388.5 347.0,388.4 347.8,388.2 C 348.6,388.1 349.4,387.9 350.2,387.8 C 350.9,387.7 351.7,387.6 352.5,387.5 C 353.3,387.4 354.1,387.2 354.8,387.2 C 355.6,387.1 356.4,387.1 357.2,387.0 C 358.0,386.9 358.8,386.7 359.5,386.4 C 360.3,386.1 361.1,385.6 361.9,385.3 C 362.7,385.0 363.4,384.6 364.2,384.4 C 365.0,384.2 365.8,384.1 366.6,384.0 C 367.3,383.9 368.1,383.8 368.9,383.6 C 369.7,383.3 370.5,382.8 371.2,382.6 C 372.0,382.4 372.8,382.4 373.6,382.3 C 374.4,382.2 375.2,382.0 375.9,381.8 C 376.7,381.6 377.5,381.4 378.3,381.3 C 379.1,381.1 379.8,380.8 380.6,380.7 C 381.4,380.5 382.2,380.4 383.0,380.2 C 383.8,379.9 384.5,379.6 385.3,379.3 C 386.1,379.1 386.9,379.0 387.7,378.7 C 388.4,378.5 389.2,378.2 390.0,377.9 C 390.8,377.6 391.6,377.4 392.3,377.1 C 393.1,376.8 393.9,376.8 394.7,376.4 C 395.5,376.0 396.2,375.1 397.0,374.7 C 397.8,374.3 398.6,374.3 399.4,374.1 C 400.2,374.0 400.9,374.0 401.7,373.9 C 402.5,373.8 403.3,373.7 404.1,373.5 C 404.8,373.4 405.6,373.6 406.4,373.3 C 407.2,372.9 408.0,371.8 408.8,371.4 C 409.5,371.0 410.3,371.1 411.1,370.8 C 411.9,370.6 412.7,370.1 413.4,369.9 C 414.2,369.7 415.0,369.8 415.8,369.6 C 416.6,369.4 417.3,368.9 418.1,368.7 C 418.9,368.5 419.7,368.5 420.5,368.4 C 421.2,368.3 422.0,368.3 422.8,368.1 C 423.6,367.9 424.4,367.6 425.2,367.1 C 425.9,366.6 426.7,365.5 427.5,365.0 C 428.3,364.5 429.1,364.2 429.8,363.9 C 430.6,363.5 431.4,363.4 432.2,362.7 C 433.0,361.9 433.8,360.2 434.5,359.4 C 435.3,358.6 436.1,358.3 436.9,357.8 C 437.7,357.3 438.4,357.2 439.2,356.5 C 440.0,355.8 440.8,354.2 441.6,353.5 C 442.3,352.8 443.1,352.7 443.9,352.3 C 444.7,351.9 445.5,351.5 446.2,351.1 C 447.0,3Line truncated
<path d="M 90.0,462.2 C 90.4,462.1 92.0,461.8 92.3,461.7 C 93.1,461.6 93.9,461.4 94.7,461.2 C 95.5,461.1 96.2,460.8 97.0,460.8 C 97.8,460.7 98.6,460.7 99.4,460.7 C 100.2,460.7 100.9,460.6 101.7,460.6 C 102.5,460.5 103.3,460.5 104.1,460.4 C 104.8,460.4 105.2,460.3 106.4,460.3 C 107.6,460.2 109.9,460.1 111.1,460.1 C 112.3,460.1 112.7,460.0 113.4,460.0 C 114.2,460.0 115.0,460.0 115.8,459.9 C 116.6,459.9 117.3,459.9 118.1,459.9 C 118.9,459.9 119.7,459.9 120.5,459.8 C 121.2,459.8 122.0,459.7 122.8,459.7 C 123.6,459.6 124.4,459.6 125.2,459.4 C 125.9,459.3 126.7,459.0 127.5,458.7 C 128.3,458.3 129.1,457.6 129.8,457.3 C 130.6,457.1 131.4,457.2 132.2,457.1 C 133.0,457.1 133.8,457.1 134.5,457.0 C 135.3,457.0 135.7,457.0 136.9,456.9 C 138.0,456.9 140.0,456.9 141.6,456.9 C 143.1,456.8 145.1,456.8 146.2,456.8 C 147.4,456.7 147.8,456.7 148.6,456.7 C 149.4,456.6 150.2,456.6 150.9,456.5 C 151.7,456.5 152.5,456.5 153.3,456.4 C 154.1,456.4 154.8,456.4 155.6,456.4 C 156.4,456.4 156.8,456.4 158.0,456.3 C 159.1,456.3 160.7,456.2 162.7,456.2 C 164.6,456.2 167.7,456.2 169.7,456.2 C 171.6,456.1 172.0,456.2 174.4,456.1 C 176.7,456.1 181.8,455.9 183.8,455.9 C 185.7,455.8 184.9,455.9 186.1,455.8 C 187.3,455.8 188.8,455.8 190.8,455.8 C 192.7,455.8 193.9,455.7 197.8,455.7 C 201.7,455.6 211.1,455.5 214.2,455.5 C 217.3,455.5 215.8,455.5 216.6,455.4 C 217.3,455.4 217.7,456.1 218.9,455.4 C 220.1,454.7 222.4,454.1 223.6,451.3 C 224.8,448.4 225.2,440.7 225.9,438.3 C 226.7,435.9 227.5,437.3 228.3,436.9 C 229.1,436.5 229.8,436.3 230.6,436.0 C 231.4,435.8 232.2,435.6 233.0,435.4 C 233.8,435.3 234.5,435.3 235.3,435.2 C 236.1,435.1 236.9,434.9 237.7,434.8 C 238.4,434.8 239.2,434.8 240.0,434.8 C 240.8,434.7 241.6,434.5 242.3,434.4 C 243.1,434.3 243.9,434.3 244.7,434.2 C 245.5,434.1 246.2,433.7 247.0,433.6 C 247.8,433.5 248.6,433.5 249.4,433.5 C 250.2,433.4 250.5,433.2 251.7,433.1 C 252.9,433.0 255.2,433.0 256.4,432.9 C 257.6,432.9 258.0,432.8 258.8,432.7 C 259.5,432.7 260.3,432.7 261.1,432.6 C 261.9,432.5 262.7,432.2 263.4,432.1 C 264.2,431.9 265.0,432.6 265.8,431.7 C 266.6,430.8 267.3,427.6 268.1,426.6 C 268.9,425.5 269.7,425.7 270.5,425.5 C 271.2,425.3 272.0,425.5 272.8,425.4 C 273.6,425.3 274.4,424.9 275.2,424.6 C 275.9,424.4 276.7,424.2 277.5,423.7 C 278.3,423.2 279.1,422.2 279.8,421.6 C 280.6,421.0 281.4,420.4 282.2,420.0 C 283.0,419.5 283.8,419.2 284.5,419.0 C 285.3,418.7 286.1,418.6 286.9,418.5 C 287.7,418.3 288.4,418.4 289.2,418.2 C 290.0,418.1 290.8,417.9 291.6,417.8 C 292.3,417.7 293.1,417.8 293.9,417.7 C 294.7,417.6 295.5,417.6 296.2,417.5 C 297.0,417.4 297.8,417.4 298.6,417.0 C 299.4,416.6 300.2,415.5 300.9,415.0 C 301.7,414.5 302.5,414.3 303.3,414.0 C 304.1,413.6 304.8,414.3 305.6,412.8 C 306.4,411.2 307.2,406.4 308.0,404.6 C 308.8,402.7 309.5,402.4 310.3,401.7 C 311.1,400.9 311.9,400.4 312.7,399.9 C 313.4,399.3 314.2,398.8 315.0,398.4 C 315.8,398.0 316.6,397.6 317.3,397.3 C 318.1,397.1 318.9,397.0 319.7,396.8 C 320.5,396.6 321.2,396.3 322.0,396.0 C 322.8,395.7 323.6,395.2 324.4,395.0 C 325.2,394.7 325.9,394.8 326.7,394.5 C 327.5,394.2 328.3,393.8 329.1,393.5 C 329.8,393.1 330.6,392.9 331.4,392.6 C 332.2,392.4 333.0,392.2 333.8,392.0 C 334.5,391.9 335.3,391.8 336.1,391.7 C 336.9,391.6 337.7,391.4 338.4,391.2 C 339.2,391.0 340.0,390.9 340.8,390.7 C 341.6,390.5 342.3,390.3 343.1,390.0 C 343.9,389.6 344.7,389.0 345.5,388.8 C 346.2,388.5 347.0,388.4 347.8,388.2 C 348.6,388.1 349.4,387.9 350.2,387.8 C 350.9,387.7 351.7,387.6 352.5,387.5 C 353.3,387.4 354.1,387.2 354.8,387.2 C 355.6,387.1 356.4,387.1 357.2,387.0 C 358.0,386.9 358.8,386.7 359.5,386.4 C 360.3,386.1 361.1,385.6 361.9,385.3 C 362.7,385.0 363.4,384.6 364.2,384.4 C 365.0,384.2 365.8,384.1 366.6,384.0 C 367.3,383.9 368.1,383.8 368.9,383.6 C 369.7,383.3 370.5,382.8 371.2,382.6 C 372.0,382.4 372.8,382.4 373.6,382.3 C 374.4,382.2 375.2,382.0 375.9,381.8 C 376.7,381.6 377.5,381.4 378.3,381.3 C 379.1,381.1 379.8,380.8 380.6,380.7 C 381.4,380.5 382.2,380.4 383.0,380.2 C 383.8,379.9 384.5,379.6 385.3,379.3 C 386.1,379.1 386.9,379.0 387.7,378.7 C 388.4,378.5 389.2,378.2 390.0,377.9 C 390.8,377.6 391.6,377.4 392.3,377.1 C 393.1,376.8 393.9,376.8 394.7,376.4 C 395.5,376.0 396.2,375.1 397.0,374.7 C 397.8,374.3 398.6,374.3 399.4,374.1 C 400.2,374.0 400.9,374.0 401.7,373.9 C 402.5,373.8 403.3,373.7 404.1,373.5 C 404.8,373.4 405.6,373.6 406.4,373.3 C 407.2,372.9 408.0,371.8 408.8,371.4 C 409.5,371.0 410.3,371.1 411.1,370.8 C 411.9,370.6 412.7,370.1 413.4,369.9 C 414.2,369.7 415.0,369.8 415.8,369.6 C 416.6,369.4 417.3,368.9 418.1,368.7 C 418.9,368.5 419.7,368.5 420.5,368.4 C 421.2,368.3 422.0,368.3 422.8,368.1 C 423.6,367.9 424.4,367.6 425.2,367.1 C 425.9,366.6 426.7,365.5 427.5,365.0 C 428.3,364.5 429.1,364.2 429.8,363.9 C 430.6,363.5 431.4,363.4 432.2,362.7 C 433.0,361.9 433.8,360.2 434.5,359.4 C 435.3,358.6 436.1,358.3 436.9,357.8 C 437.7,357.3 438.4,357.2 439.2,356.5 C 440.0,355.8 440.8,354.2 441.6,353.5 C 442.3,352.8 443.1,352.7 443.9,352.3 C 444.7,351.9 445.5,351.5 446.2,351.1 C 447.0,3Line truncated
<circle cx="765.0" cy="99.0" r="5" fill="#dd4528" stroke="#fff" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6652</text>
<path d="M 90.0,462.2 C 90.4,462.1 91.9,461.8 92.3,461.7 C 93.1,461.6 93.8,461.4 94.6,461.2 C 95.3,461.1 96.1,460.8 96.9,460.8 C 97.6,460.7 98.4,460.7 99.2,460.7 C 99.9,460.7 100.7,460.6 101.4,460.6 C 102.2,460.5 103.0,460.5 103.7,460.4 C 104.5,460.4 104.9,460.3 106.0,460.3 C 107.2,460.2 109.4,460.1 110.6,460.1 C 111.7,460.1 112.1,460.0 112.9,460.0 C 113.6,460.0 114.4,460.0 115.2,459.9 C 115.9,459.9 116.7,459.9 117.5,459.9 C 118.2,459.9 119.0,459.9 119.7,459.8 C 120.5,459.8 121.3,459.7 122.0,459.7 C 122.8,459.6 123.6,459.6 124.3,459.4 C 125.1,459.3 125.8,459.0 126.6,458.7 C 127.4,458.3 128.1,457.6 128.9,457.3 C 129.7,457.1 130.4,457.2 131.2,457.1 C 131.9,457.1 132.7,457.1 133.5,457.0 C 134.2,457.0 134.6,457.0 135.8,456.9 C 136.9,456.9 138.8,456.9 140.3,456.9 C 141.9,456.8 143.8,456.8 144.9,456.8 C 146.1,456.7 146.4,456.7 147.2,456.7 C 148.0,456.6 148.7,456.6 149.5,456.5 C 150.3,456.5 151.0,456.5 151.8,456.4 C 152.5,456.4 153.3,456.4 154.1,456.4 C 154.8,456.4 155.2,456.4 156.4,456.3 C 157.5,456.3 159.0,456.2 160.9,456.2 C 162.8,456.2 165.9,456.2 167.8,456.2 C 169.7,456.1 170.1,456.2 172.4,456.1 C 174.7,456.1 179.6,455.9 181.5,455.9 C 183.4,455.8 182.7,455.9 183.8,455.8 C 185.0,455.8 186.5,455.8 188.4,455.8 C 190.3,455.8 191.4,455.7 195.3,455.7 C 199.1,455.6 208.2,455.5 211.3,455.5 C 214.3,455.5 212.8,455.5 213.6,455.4 C 214.3,455.4 214.7,456.1 215.8,455.4 C 217.0,454.7 219.3,454.1 220.4,451.3 C 221.6,448.4 221.9,440.7 222.7,438.3 C 223.5,435.9 224.2,437.3 225.0,436.9 C 225.8,436.5 226.5,436.3 227.3,436.0 C 228.1,435.8 228.8,435.6 229.6,435.4 C 230.3,435.3 231.1,435.3 231.9,435.2 C 232.6,435.1 233.4,434.9 234.2,434.8 C 234.9,434.8 235.7,434.8 236.4,434.8 C 237.2,434.7 238.0,434.5 238.7,434.4 C 239.5,434.3 240.3,434.3 241.0,434.2 C 241.8,434.1 242.5,433.7 243.3,433.6 C 244.1,433.5 244.8,433.5 245.6,433.5 C 246.4,433.4 246.7,433.2 247.9,433.1 C 249.0,433.0 251.3,433.0 252.5,432.9 C 253.6,432.9 254.0,432.8 254.7,432.7 C 255.5,432.7 256.3,432.7 257.0,432.6 C 257.8,432.5 258.6,432.2 259.3,432.1 C 260.1,431.9 260.8,432.6 261.6,431.7 C 262.4,430.8 263.1,427.6 263.9,426.6 C 264.7,425.5 265.4,425.7 266.2,425.5 C 266.9,425.3 267.7,425.5 268.5,425.4 C 269.2,425.3 270.0,424.9 270.8,424.6 C 271.5,424.4 272.3,424.2 273.1,423.7 C 273.8,423.2 274.6,422.2 275.3,421.6 C 276.1,421.0 276.9,420.5 277.6,420.0 C 278.4,419.6 279.2,419.3 279.9,419.0 C 280.7,418.8 281.4,418.6 282.2,418.5 C 283.0,418.4 283.7,418.4 284.5,418.3 C 285.3,418.2 286.0,418.0 286.8,417.9 C 287.5,417.8 288.3,417.8 289.1,417.8 C 289.8,417.7 290.6,417.7 291.4,417.5 C 292.1,417.4 292.9,417.5 293.6,417.0 C 294.4,416.6 295.2,415.6 295.9,415.1 C 296.7,414.6 297.5,414.4 298.2,414.0 C 299.0,413.7 299.7,414.4 300.5,412.8 C 301.3,411.3 302.0,406.5 302.8,404.6 C 303.6,402.8 304.3,402.5 305.1,401.7 C 305.8,400.9 306.6,400.4 307.4,399.9 C 308.1,399.3 308.9,398.8 309.7,398.4 C 310.4,398.0 311.2,397.6 311.9,397.3 C 312.7,397.1 313.5,397.0 314.2,396.8 C 315.0,396.6 315.8,396.3 316.5,396.0 C 317.3,395.7 318.1,395.2 318.8,395.0 C 319.6,394.7 320.3,394.8 321.1,394.5 C 321.9,394.2 322.6,393.8 323.4,393.5 C 324.2,393.1 324.9,392.9 325.7,392.6 C 326.4,392.4 327.2,392.2 328.0,392.0 C 328.7,391.9 329.5,391.8 330.3,391.7 C 331.0,391.6 331.8,391.4 332.5,391.2 C 333.3,391.0 334.1,390.9 334.8,390.7 C 335.6,390.5 336.4,390.3 337.1,390.0 C 337.9,389.6 338.6,389.0 339.4,388.8 C 340.2,388.5 340.9,388.4 341.7,388.2 C 342.5,388.1 343.2,387.9 344.0,387.8 C 344.7,387.7 345.5,387.6 346.3,387.5 C 347.0,387.4 347.8,387.2 348.6,387.2 C 349.3,387.1 350.1,387.1 350.8,387.0 C 351.6,386.9 352.4,386.7 353.1,386.4 C 353.9,386.1 354.7,385.6 355.4,385.3 C 356.2,385.0 356.9,384.6 357.7,384.4 C 358.5,384.2 359.2,384.1 360.0,384.0 C 360.8,383.9 361.5,383.8 362.3,383.6 C 363.1,383.3 363.8,382.8 364.6,382.6 C 365.3,382.4 366.1,382.4 366.9,382.3 C 367.6,382.2 368.4,382.0 369.2,381.8 C 369.9,381.6 370.7,381.4 371.4,381.3 C 372.2,381.1 373.0,380.8 373.7,380.7 C 374.5,380.5 375.3,380.4 376.0,380.2 C 376.8,379.9 377.5,379.6 378.3,379.3 C 379.1,379.1 379.8,379.0 380.6,378.7 C 381.4,378.5 382.1,378.2 382.9,377.9 C 383.6,377.6 384.4,377.4 385.2,377.1 C 385.9,376.8 386.7,376.8 387.5,376.4 C 388.2,376.0 389.0,375.1 389.7,374.7 C 390.5,374.3 391.3,374.3 392.0,374.1 C 392.8,374.0 393.6,374.0 394.3,373.9 C 395.1,373.8 395.8,373.7 396.6,373.5 C 397.4,373.4 398.1,373.6 398.9,373.3 C 399.7,372.9 400.4,371.8 401.2,371.4 C 401.9,371.0 402.7,371.1 403.5,370.8 C 404.2,370.6 405.0,370.1 405.8,369.9 C 406.5,369.7 407.3,369.8 408.1,369.6 C 408.8,369.4 409.6,368.9 410.3,368.7 C 411.1,368.5 411.9,368.5 412.6,368.4 C 413.4,368.3 414.2,368.3 414.9,368.1 C 415.7,367.9 416.4,367.6 417.2,367.1 C 418.0,366.6 418.7,365.5 419.5,365.0 C 420.3,364.5 421.0,364.2 421.8,363.9 C 422.5,363.5 423.3,363.4 424.1,362.7 C 424.8,361.9 425.6,360.2 426.4,359.4 C 427.1,358.6 427.9,358.3 428.6,357.8 C 429.4,357.4 430.2,357.3 430.9,356.6 C 431.7,355.9 432.5,354.2 433.2,353.5 C 434.0,352.8 434.7,352.7 435.5,352.3 C 436.3,351.9 437.0,351.6 437.8,351.1 C 438.6,35Line truncated
<path d="M 90.0,462.2 C 90.4,462.1 91.9,461.8 92.3,461.7 C 93.1,461.6 93.8,461.4 94.6,461.2 C 95.3,461.1 96.1,460.8 96.9,460.8 C 97.6,460.7 98.4,460.7 99.2,460.7 C 99.9,460.7 100.7,460.6 101.4,460.6 C 102.2,460.5 103.0,460.5 103.7,460.4 C 104.5,460.4 104.9,460.3 106.0,460.3 C 107.2,460.2 109.4,460.1 110.6,460.1 C 111.7,460.1 112.1,460.0 112.9,460.0 C 113.6,460.0 114.4,460.0 115.2,459.9 C 115.9,459.9 116.7,459.9 117.5,459.9 C 118.2,459.9 119.0,459.9 119.7,459.8 C 120.5,459.8 121.3,459.7 122.0,459.7 C 122.8,459.6 123.6,459.6 124.3,459.4 C 125.1,459.3 125.8,459.0 126.6,458.7 C 127.4,458.3 128.1,457.6 128.9,457.3 C 129.7,457.1 130.4,457.2 131.2,457.1 C 131.9,457.1 132.7,457.1 133.5,457.0 C 134.2,457.0 134.6,457.0 135.8,456.9 C 136.9,456.9 138.8,456.9 140.3,456.9 C 141.9,456.8 143.8,456.8 144.9,456.8 C 146.1,456.7 146.4,456.7 147.2,456.7 C 148.0,456.6 148.7,456.6 149.5,456.5 C 150.3,456.5 151.0,456.5 151.8,456.4 C 152.5,456.4 153.3,456.4 154.1,456.4 C 154.8,456.4 155.2,456.4 156.4,456.3 C 157.5,456.3 159.0,456.2 160.9,456.2 C 162.8,456.2 165.9,456.2 167.8,456.2 C 169.7,456.1 170.1,456.2 172.4,456.1 C 174.7,456.1 179.6,455.9 181.5,455.9 C 183.4,455.8 182.7,455.9 183.8,455.8 C 185.0,455.8 186.5,455.8 188.4,455.8 C 190.3,455.8 191.4,455.7 195.3,455.7 C 199.1,455.6 208.2,455.5 211.3,455.5 C 214.3,455.5 212.8,455.5 213.6,455.4 C 214.3,455.4 214.7,456.1 215.8,455.4 C 217.0,454.7 219.3,454.1 220.4,451.3 C 221.6,448.4 221.9,440.7 222.7,438.3 C 223.5,435.9 224.2,437.3 225.0,436.9 C 225.8,436.5 226.5,436.3 227.3,436.0 C 228.1,435.8 228.8,435.6 229.6,435.4 C 230.3,435.3 231.1,435.3 231.9,435.2 C 232.6,435.1 233.4,434.9 234.2,434.8 C 234.9,434.8 235.7,434.8 236.4,434.8 C 237.2,434.7 238.0,434.5 238.7,434.4 C 239.5,434.3 240.3,434.3 241.0,434.2 C 241.8,434.1 242.5,433.7 243.3,433.6 C 244.1,433.5 244.8,433.5 245.6,433.5 C 246.4,433.4 246.7,433.2 247.9,433.1 C 249.0,433.0 251.3,433.0 252.5,432.9 C 253.6,432.9 254.0,432.8 254.7,432.7 C 255.5,432.7 256.3,432.7 257.0,432.6 C 257.8,432.5 258.6,432.2 259.3,432.1 C 260.1,431.9 260.8,432.6 261.6,431.7 C 262.4,430.8 263.1,427.6 263.9,426.6 C 264.7,425.5 265.4,425.7 266.2,425.5 C 266.9,425.3 267.7,425.5 268.5,425.4 C 269.2,425.3 270.0,424.9 270.8,424.6 C 271.5,424.4 272.3,424.2 273.1,423.7 C 273.8,423.2 274.6,422.2 275.3,421.6 C 276.1,421.0 276.9,420.5 277.6,420.0 C 278.4,419.6 279.2,419.3 279.9,419.0 C 280.7,418.8 281.4,418.6 282.2,418.5 C 283.0,418.4 283.7,418.4 284.5,418.3 C 285.3,418.2 286.0,418.0 286.8,417.9 C 287.5,417.8 288.3,417.8 289.1,417.8 C 289.8,417.7 290.6,417.7 291.4,417.5 C 292.1,417.4 292.9,417.5 293.6,417.0 C 294.4,416.6 295.2,415.6 295.9,415.1 C 296.7,414.6 297.5,414.4 298.2,414.0 C 299.0,413.7 299.7,414.4 300.5,412.8 C 301.3,411.3 302.0,406.5 302.8,404.6 C 303.6,402.8 304.3,402.5 305.1,401.7 C 305.8,400.9 306.6,400.4 307.4,399.9 C 308.1,399.3 308.9,398.8 309.7,398.4 C 310.4,398.0 311.2,397.6 311.9,397.3 C 312.7,397.1 313.5,397.0 314.2,396.8 C 315.0,396.6 315.8,396.3 316.5,396.0 C 317.3,395.7 318.1,395.2 318.8,395.0 C 319.6,394.7 320.3,394.8 321.1,394.5 C 321.9,394.2 322.6,393.8 323.4,393.5 C 324.2,393.1 324.9,392.9 325.7,392.6 C 326.4,392.4 327.2,392.2 328.0,392.0 C 328.7,391.9 329.5,391.8 330.3,391.7 C 331.0,391.6 331.8,391.4 332.5,391.2 C 333.3,391.0 334.1,390.9 334.8,390.7 C 335.6,390.5 336.4,390.3 337.1,390.0 C 337.9,389.6 338.6,389.0 339.4,388.8 C 340.2,388.5 340.9,388.4 341.7,388.2 C 342.5,388.1 343.2,387.9 344.0,387.8 C 344.7,387.7 345.5,387.6 346.3,387.5 C 347.0,387.4 347.8,387.2 348.6,387.2 C 349.3,387.1 350.1,387.1 350.8,387.0 C 351.6,386.9 352.4,386.7 353.1,386.4 C 353.9,386.1 354.7,385.6 355.4,385.3 C 356.2,385.0 356.9,384.6 357.7,384.4 C 358.5,384.2 359.2,384.1 360.0,384.0 C 360.8,383.9 361.5,383.8 362.3,383.6 C 363.1,383.3 363.8,382.8 364.6,382.6 C 365.3,382.4 366.1,382.4 366.9,382.3 C 367.6,382.2 368.4,382.0 369.2,381.8 C 369.9,381.6 370.7,381.4 371.4,381.3 C 372.2,381.1 373.0,380.8 373.7,380.7 C 374.5,380.5 375.3,380.4 376.0,380.2 C 376.8,379.9 377.5,379.6 378.3,379.3 C 379.1,379.1 379.8,379.0 380.6,378.7 C 381.4,378.5 382.1,378.2 382.9,377.9 C 383.6,377.6 384.4,377.4 385.2,377.1 C 385.9,376.8 386.7,376.8 387.5,376.4 C 388.2,376.0 389.0,375.1 389.7,374.7 C 390.5,374.3 391.3,374.3 392.0,374.1 C 392.8,374.0 393.6,374.0 394.3,373.9 C 395.1,373.8 395.8,373.7 396.6,373.5 C 397.4,373.4 398.1,373.6 398.9,373.3 C 399.7,372.9 400.4,371.8 401.2,371.4 C 401.9,371.0 402.7,371.1 403.5,370.8 C 404.2,370.6 405.0,370.1 405.8,369.9 C 406.5,369.7 407.3,369.8 408.1,369.6 C 408.8,369.4 409.6,368.9 410.3,368.7 C 411.1,368.5 411.9,368.5 412.6,368.4 C 413.4,368.3 414.2,368.3 414.9,368.1 C 415.7,367.9 416.4,367.6 417.2,367.1 C 418.0,366.6 418.7,365.5 419.5,365.0 C 420.3,364.5 421.0,364.2 421.8,363.9 C 422.5,363.5 423.3,363.4 424.1,362.7 C 424.8,361.9 425.6,360.2 426.4,359.4 C 427.1,358.6 427.9,358.3 428.6,357.8 C 429.4,357.4 430.2,357.3 430.9,356.6 C 431.7,355.9 432.5,354.2 433.2,353.5 C 434.0,352.8 434.7,352.7 435.5,352.3 C 436.3,351.9 437.0,351.6 437.8,351.1 C 438.6,35Line truncated
<circle cx="765.0" cy="90.8" r="5" fill="#dd4528" stroke="#fff" stroke-width="2" filter="url(#xkcdify)"/>
<text x="753.0" y="77.0" fill="#dd4528" font-size="18" font-family="Handlee, cursive" font-weight="bold" text-anchor="end">6802</text>
<a href="https://github.com/carsteneu/mystarhistory" target="_blank" rel="noopener"><text x="763" y="527" font-size="13" font-family="Handlee, cursive" fill="#000" text-anchor="end">Made with carsteneu/mystarhistory</text></a>
</svg>

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+67
View File
@@ -0,0 +1,67 @@
# Plugin platform implementation review
Baseline: main `0256e7471d66bbbca077c361ef379d7b11ce8465` (2026-10-26).
## Existing capabilities and gaps
| Area | Implemented source | Remaining boundary |
| --- | --- | --- |
| Contract, SDK, package CLI | `packages/plugin-contract/schema`, `packages/plugin-sdk`, `packages/plugin-cli` | Internal API `0.1.0-internal`; no public API stability promise |
| Runtime | `electron/plugins/browserPluginRuntime.cjs`, `utilityPluginRuntime.cjs`, `hostBootstrap.cjs` | Entire host remains development opt-in; advanced utility is ambient Node authority, not a fine-grained sandbox |
| Permissions | `permissionEngine.cjs`, `hostRpcRegistry.cjs`, `secureCapabilities.cjs` | Broker checks at privileged boundary; unsigned principal bound to package digest; verified publisher/distribution not implemented |
| UI contributions | `contributionService.cjs`, `components/plugins`, `SettingsPluginsTab.tsx` | Commands, menus, keybindings, settings and views exist; current Settings page is contributed settings, not a package manager/marketplace |
| Terminal | `terminalProviderService.cjs`, `terminalDataPipelineService.cjs` | Completion/link/visual providers and lifecycle; interceptors require privileged utility placement; not arbitrary terminal object access |
| Connection, authentication, importer | `extensionProviderService.cjs`, `application/state/pluginExtensionBridge.ts` | Existing operation-bound provider integration; not general vault/SFTP/clipboard CRUD |
| Sync | `pluginSyncSidecarService.cjs`, `application/pluginSyncSidecarBridge.ts` | Opt-in non-secret settings and account/CRDT sidecars; ordinary KV is local, package code/permissions/secrets are not cloud authority |
| Compatibility | `plugin-cli/src/compatibility.ts`, `packageStore.cjs`, `runtimeSupervisor.cjs` | Install/start checks existed; startup reconciliation and lazy re-enable checks missing |
| Local package | `packageStore.cjs`, CLI `pack`, first-party `examples/plugins/hello-netcatty` | Immutable snapshot, hash/integrity validation and safe extraction exist; no ordinary import management UI |
| Distribution | No public catalog implementation | Version/hash/pinned README, review workflow, manual update UI still needed |
Historical merged steps: #2276 contract/SDK; #2303 runtime; #2315 permission/security;
#2324 UI contributions; #2338 terminal providers; #2402 privileged terminal pipeline;
#2413 connection/auth/importers; #2713 sync; #2748 and #2752 sync follow-up fixes.
The repository AGENTS.md runtime section describes an earlier phase and must not
be mistaken for a complete current capability inventory. No repository `.agents/skills`
directory was present at this baseline.
## Small implementation slices
1. Reconcile host/API/features compatibility before contribution enumeration;
disable incompatible plugins and retain package/data, record the reason;
block lazy re-enable. Do not auto-enable after a host downgrade.
2. Reuse the existing Settings Plugins tab and management IPC for installed
packages, manifest-derived permissions, version/hash and lifecycle actions;
local import starts disabled for review. Keep native permission mediation.
3. Add a lightweight first-party catalog and pinned safe README detail renderer,
shared package verification, digest-bound manual update flow and author docs.
Do not introduce remote accounts, reviews, payments or a new repository.
4. Reproduce the actual Electron flow with synthetic data and the first-party
sample; transfer source/build instructions if GUI is unavailable here.
## Decisions and explicit release gates
User approved public source plus a clear license for catalog entries, and manual
confirmation of plugin updates. Private local packages remain separate.
Recommended public v1 eligibility: browser runtime only. Utility Node, native
companions and raw terminal interceptors retain the development preview path.
This excludes advanced provider implementations requiring Node/native binaries;
ordinary browser contributions, broker-backed networking and providers remain
possible. This policy needs product confirmation; no existing advanced path is
removed by this review.
Keep `NETCATTY_PLUGIN_DEV=1` until distribution trust, permission UI, compatibility
migration and native acceptance are reviewed. Passing tests is not proof of
security isolation. Do not describe utility processes as broker-constrained.
A future KV sync feature needs an explicit manifest opt-in schema, size limits,
namespace and schema-version migrations, deterministic conflict/deletion rules,
account switching semantics and secret exclusion. It is deliberately not silently
enabled in this iteration. Reuse existing settings sidecars for small preferences.
Catalog entries should bind ID, repository, license, author, version, commit,
artifact SHA-256 and URL, platform/engine ranges, permissions and README revision.
A hash mismatch must fail rather than fall back to latest. README must not execute
HTML or automatically fetch remote media. Directory PR checks are a bounded
review process, not a security certification or a guarantee that maintainers will
repair third-party plugins.
+36 -5
View File
@@ -151,6 +151,35 @@ class PackageStore {
.filter((value) => typeof value === "string" && value !== this.paths.database)
.map((directory) => mkdir(directory, { recursive: true, mode: 0o700 })));
await this.recover();
// Reconcile every installed manifest before lazy contributions are exposed.
// An application/API upgrade must not leave incompatible providers enabled.
for (const plugin of this.database.listPlugins()) {
if (!plugin.enabled) continue;
await this.assertCompatible(plugin).catch((error) => {
if (error.code !== "PLUGIN_INCOMPATIBLE") throw error;
});
}
}
async assertCompatible(plugin) {
const pluginCli = await loadPluginCli();
const result = pluginCli.checkPluginCompatibility(plugin.manifest, {
netcattyVersion: this.netcattyVersion,
apiVersion: this.apiVersion,
features: this.supportedFeatures,
});
if (result.compatible) return result;
const error = new Error(`Plugin is incompatible: ${result.errors.join("; ")}`);
error.code = "PLUGIN_INCOMPATIBLE";
const current = this.database.getActivePlugin(plugin.id);
if (current?.activeVersion === plugin.activeVersion) {
this.database.setEnabled(plugin.id, false);
this.database.setRuntimeState(plugin.id, "error", {
pluginVersion: plugin.activeVersion,
error: error.message,
});
}
throw error;
}
async install(archivePath, options = {}) {
@@ -187,7 +216,9 @@ class PackageStore {
const version = assertPluginStorageSegment(manifest.version, "version");
const targetDirectory = resolveInstalledVersionDirectory(this.paths, pluginId, version);
const previousPlugin = this.database.getActivePlugin(pluginId);
const enableAfterActivation = options.enable === true || previousPlugin?.enabled === true;
const enableAfterActivation = options.enable === false
? false
: options.enable === true || previousPlugin?.enabled === true;
let activationPrepared = false;
const prepareActivation = async (reason) => {
if (activationPrepared) return;
@@ -214,7 +245,7 @@ class PackageStore {
await rm(targetDirectory, { recursive: true, force: true });
}
if (existingValidation) {
if (previousPlugin?.activeVersion !== version) await prepareActivation("switch-active-version");
if (previousPlugin?.activeVersion !== version || options.enable === false) await prepareActivation("switch-active-version");
const existingPackage = path.join(targetDirectory, PACKAGE_DIRECTORY);
this.database.installVersion({
pluginId,
@@ -222,7 +253,7 @@ class PackageStore {
manifest: existingValidation.manifest,
archiveSha256: existing.archiveSha256,
packageRelativePath: path.relative(this.paths.packages, existingPackage),
}, { enable: enableAfterActivation });
}, { enable: enableAfterActivation, forceDisabled: options.enable === false });
return this.database.getActivePlugin(pluginId);
}
}
@@ -241,7 +272,7 @@ class PackageStore {
await writeDurableMetadata(path.join(stagingDirectory, INSTALL_METADATA_FILE), metadata);
await this.syncDirectory(stagingDirectory);
await mkdir(path.dirname(targetDirectory), { recursive: true, mode: 0o700 });
if (previousPlugin?.activeVersion !== version) await prepareActivation("switch-active-version");
if (previousPlugin?.activeVersion !== version || options.enable === false) await prepareActivation("switch-active-version");
await rename(stagingDirectory, targetDirectory);
await this.syncDirectory(path.dirname(targetDirectory));
const packageRelativePath = path.relative(
@@ -255,7 +286,7 @@ class PackageStore {
manifest,
archiveSha256: snapshot.sha256,
packageRelativePath,
}, { enable: enableAfterActivation });
}, { enable: enableAfterActivation, forceDisabled: options.enable === false });
} catch (error) {
this.verifiedArchives.delete(this.#versionKey(pluginId, version));
await rm(targetDirectory, { recursive: true, force: true });
+51
View File
@@ -370,3 +370,54 @@ test("failed package validation removes every extracted staging file", async (co
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
assert.deepEqual(fixture.database.listPlugins(), []);
});
test("host upgrade disables incompatible lazy plugins before contributions and retains data", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const archive = await createPackage(fixture.root, {
manifest: { engines: { netcatty: "<1.0.0", api: ">=0.1.0-internal <0.2.0" } },
});
const installed = await fixture.store.install(archive.archive, { enable: true });
fixture.database.setValue(installed.id, "synthetic", { retained: true });
fixture.store.netcattyVersion = "1.0.0";
await fixture.store.initialize();
const disabled = fixture.database.getActivePlugin(installed.id);
assert.equal(disabled.enabled, false);
assert.match(disabled.runtime.lastError, /incompatible.*Netcatty/);
assert.equal(disabled.activeVersion, installed.activeVersion);
assert.deepEqual(fixture.database.getValue(installed.id, "synthetic"), { retained: true });
assert.equal(fs.existsSync(fixture.store.resolvePackageRoot(disabled)), true);
await assert.rejects(fixture.store.assertCompatible(disabled), { code: "PLUGIN_INCOMPATIBLE" });
fixture.store.netcattyVersion = "0.0.0";
await fixture.store.initialize();
assert.equal(fixture.database.getActivePlugin(installed.id).enabled, false,
"returning to a compatible host must not silently re-enable code");
});
for (const target of [
{ apiVersion: "0.2.0" },
{ supportedFeatures: [] },
]) {
test(`startup checks API and required feature compatibility: ${JSON.stringify(target)}`, async (context) => {
const fixture = createStore(context, { supportedFeatures: ["example.feature"] });
await fixture.store.initialize();
const archive = await createPackage(fixture.root, {
manifest: { features: { required: ["example.feature"] } },
});
const installed = await fixture.store.install(archive.archive, { enable: true });
Object.assign(fixture.store, target);
await fixture.store.initialize();
assert.equal(fixture.database.getActivePlugin(installed.id).enabled, false);
});
}
test("explicit disabled import never auto-enables an update of an enabled plugin", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const first = await createPackage(fixture.root);
await fixture.store.install(first.archive, { enable: true });
const update = await createPackage(fixture.root, { manifest: { version: "1.1.0" } });
const installed = await fixture.store.install(update.archive, { enable: false });
assert.equal(installed.activeVersion, "1.1.0");
assert.equal(installed.enabled, false);
});
+1 -1
View File
@@ -646,7 +646,7 @@ function registerPluginBridge(ipcMain, options) {
handle(CHANNELS.list, async (activeManager) => activeManager.list());
handle(CHANNELS.install, async (activeManager, payload) => activeManager.install(
payload?.archivePath,
{ enable: payload?.enable === true },
{ enable: typeof payload?.enable === "boolean" ? payload.enable : undefined },
));
handle(CHANNELS.setEnabled, async (activeManager, payload) => activeManager.setEnabled(
payload?.pluginId,
+2 -1
View File
@@ -52,7 +52,7 @@ class PluginManager {
let plugin;
try {
plugin = await this.packageStore.install(archivePath, {
enable: options?.enable === true,
enable: options?.enable,
beforeActivate: async ({ pluginId, previousPlugin }) => {
if (!previousPlugin?.enabled) return;
this.database.setEnabled(pluginId, false);
@@ -107,6 +107,7 @@ class PluginManager {
await this.#ready();
if (enabled) {
const plugin = this.database.getActivePlugin(pluginId);
if (plugin) await this.packageStore.assertCompatible?.(plugin);
if (plugin?.runtime?.quarantinedAt != null) {
this.database.clearQuarantine(pluginId, plugin.activeVersion);
}
+23
View File
@@ -488,3 +488,26 @@ test("shutdown cancels an in-flight runtime mutation before waiting for its queu
"database:close",
]);
});
test("explicit enable checks compatibility before clearing quarantine or exposing lazy contributions", async () => {
const calls = [];
const plugin = { id: "com.example.incompatible", enabled: false, runtime: { quarantinedAt: 1 } };
const manager = new PluginManager({
database: {
getActivePlugin: () => plugin,
clearQuarantine: () => calls.push("clear"),
setEnabled: () => calls.push("enable"),
},
packageStore: {
async initialize() {},
async assertCompatible() { throw new Error("Plugin is incompatible"); },
},
runtimeSupervisor: {},
contributionService: {
async initialize() {},
onPluginEnabled: () => calls.push("contribute"),
},
});
await assert.rejects(manager.setEnabled(plugin.id, true), /incompatible/);
assert.deepEqual(calls, []);
});
+29 -3
View File
@@ -9,9 +9,9 @@ From the repository root:
```bash
npm run build:plugin-packages
npm exec -- netcatty-plugin validate examples/plugins/hello-netcatty
npm exec -- netcatty-plugin compatibility examples/plugins/hello-netcatty --netcatty 0.0.0
npm exec -- netcatty-plugin pack examples/plugins/hello-netcatty --out /tmp/hello-netcatty.ncpkg
node packages/plugin-cli/dist/cli.js validate examples/plugins/hello-netcatty
node packages/plugin-cli/dist/cli.js compatibility examples/plugins/hello-netcatty --netcatty 0.0.0
node packages/plugin-cli/dist/cli.js pack examples/plugins/hello-netcatty --out /tmp/hello-netcatty.ncpkg
```
After installation, change **Greeting** under **Settings → Plugins**, then run
@@ -21,3 +21,29 @@ sandboxed plugin runtime. Type `netc` at a shell prompt to exercise the bounded
completion Provider, and print `Hello from Netcatty` to exercise the declarative
decoration Provider. Neither Provider receives raw xterm objects or sensitive
terminal input/output streams.
## Import and inspect
Launch a development build with `NETCATTY_PLUGIN_DEV=1`, using a fresh Electron
user-data directory for demonstrations. In **Settings > Plugins**, choose
**Import .ncpkg**, select the generated archive, and open its details. Review the
manifest permissions, engine ranges and SHA-256, then choose **Enable**. Native
permission prompts still mediate activation and sensitive operations.
Importing an update is a manual action and leaves it disabled, including when
the previous version was enabled. Review the new package before enabling it.
Changing unsigned package contents changes its security principal; previous
persistent grants are not transferred to different bytes.
An incompatible application/API upgrade disables installed plugins before
publishing their contributions. The reason remains in the installed list, and
package files and local data remain available. Returning to a compatible host
requires explicit re-enable. Uninstall is different: package code and local KV
are deleted; user-owned settings, secret storage and sync sidecars are retained.
For a synthetic demonstration, use only this first-party example, change the
Greeting, invoke **Examples: Say Hello**, disable/re-enable, restart the app and
inspect the package again. Do not connect real hosts or a cloud account. The
existing greeting setting is local; declaring `sync: true` on a non-secret
setting opts it into the existing encrypted settings sidecar. Generic SDK KV
storage does not sync.
Vendored
+2 -1
View File
@@ -337,7 +337,8 @@ declare global {
id: string;
enabled: boolean;
activeVersion: string | null;
manifest: unknown;
manifest: import("@netcatty/plugin-contract").PluginManifest | null;
archiveSha256?: string | null;
runtime: {
status: string;
kind: 'browser' | 'utility' | null;
+16
View File
@@ -80,6 +80,22 @@ describe('queryLocalFonts deduplication', () => {
assert.ok(!names.includes('Microsoft YaHei UI'));
});
it('recognizes SFMono Nerd Font as a monospace terminal font (#3929)', async () => {
installMockWindow(async () => [
{ family: 'SFMono Nerd Font' },
{ family: 'SF Mono' },
{ family: 'Segoe UI' },
]);
const monoFonts = await getMonospaceFonts();
const names = monoFonts.map((f) => f.name);
// Glued typographic family (name ID 16) and the spaced family both belong.
assert.ok(names.includes('SFMono Nerd Font'));
assert.ok(names.includes('SF Mono'));
assert.ok(!names.includes('Segoe UI'));
});
it('a second sequential call also reuses the resolved promise (no second API call)', async () => {
let callCount = 0;
installMockWindow(async () => {
+3
View File
@@ -37,6 +37,9 @@ const KNOWN_MONOSPACE_FONTS = new Set([
'victor mono',
'ibm plex mono',
'sf mono',
// Nerd Font / patched builds often use the glued typographic family
// "SFMono Nerd Font" (name ID 16), which does not match "sf mono" (#3929).
'sfmono',
'operator mono',
'input mono',
'pragmata pro',
+3 -3
View File
@@ -1,14 +1,14 @@
{
version = "1.1.88";
version = "1.1.89";
sources = {
x86_64-linux = {
appImageArch = "x86_64";
hash = "sha256-8HpyVnwPu0nnSUFhe5Jx27l0nZaksFHmS24kBZN2+eI=";
hash = "sha256-Ne5TalZ3G9cHPl0xnQpPq/0GNRe1ErUg1DzE+/nnseM=";
};
aarch64-linux = {
appImageArch = "arm64";
hash = "sha256-NHhAkxOY1swYPHAIZK6dQCt+JbbDT7/NWSDjQRC9A4E=";
hash = "sha256-L1aiJ/CroFwLSLH5aDnakogxjP8k5gLt8NJC+IGfXIw=";
};
};
}
+2 -2
View File
@@ -581,11 +581,11 @@ test("example README commands use repository-root CLI paths", async () => {
assert.match(readme, /npm run build:plugin-packages/);
assert.match(
readme,
/npm exec -- netcatty-plugin validate examples\/plugins\/hello-netcatty/,
/node packages\/plugin-cli\/dist\/cli\.js validate examples\/plugins\/hello-netcatty/,
);
assert.match(
readme,
/npm exec -- netcatty-plugin compatibility examples\/plugins\/hello-netcatty --netcatty 0\.0\.0/,
/node packages\/plugin-cli\/dist\/cli\.js compatibility examples\/plugins\/hello-netcatty --netcatty 0\.0\.0/,
);
assert.doesNotMatch(readme, /npm exec --workspace @netcatty\/plugin-cli/);
});