mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: move snippets, keybinding actions and macros out of core into plugins
This commit is contained in:
1 parent
d9bba10612
commit
e0c30a4e6f
262 files changed
+9091
-10744
No files matched your search
@@ -195,5 +195,37 @@
|
||||
"shareFolderHint": "Shares every snippet in this folder. Snippets added later are not shared automatically.",
|
||||
"shareFolderSummary": "Shared {{count}} snippet(s) in this folder",
|
||||
"grantedBy": "Granted by",
|
||||
"expired": "Expired"
|
||||
"expired": "Expired",
|
||||
"variablesDialogTitle": "Run \"{{name}}\"",
|
||||
"variablesDialogDescription": "Fill in the values this snippet needs before running it.",
|
||||
"variablesPreviewLabel": "Preview",
|
||||
"variablesConfirmButton": "Run",
|
||||
"keybindings": {
|
||||
"runSnippet": "Run existing snippet",
|
||||
"snippetLabel": "Snippet",
|
||||
"selectSnippet": "Select a snippet",
|
||||
"snippetMissing": "snippet not found",
|
||||
"snippetRequired": "Select a snippet"
|
||||
},
|
||||
"paletteHintRun": "Run in the active terminal tab",
|
||||
"host": {
|
||||
"startupTitle": "Startup Snippet",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsDescription": "Quick actions appear as buttons in the Host Metrics toolbar for one-click command execution.",
|
||||
"noQuickActions": "No quick actions yet.",
|
||||
"buttonLabel": "Button label",
|
||||
"selectSnippet": "Select snippet...",
|
||||
"addQuickAction": "Add Action",
|
||||
"tabTitle": "Snippets",
|
||||
"startupDescription": "Runs in the terminal each time it connects to this host. Variables like $HOST are filled in; a snippet that asks for inputs is skipped.",
|
||||
"none": "None",
|
||||
"removeQuickAction": "Remove quick action"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executing {{name}}...",
|
||||
"succeeded": "{{name}} completed successfully",
|
||||
"failed": "{{name}} failed",
|
||||
"error": "Failed to execute {{name}}"
|
||||
},
|
||||
"paletteHintPaste": "Paste in the active terminal tab"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Begin \"{{name}}\"",
|
||||
"variablesDialogDescription": "Vul die waardes in wat hierdie brokkie benodig voordat dit uitgevoer word.",
|
||||
"variablesPreviewLabel": "Voorskou",
|
||||
"variablesConfirmButton": "Hardloop",
|
||||
"keybindings": {
|
||||
"runSnippet": "Voer bestaande brokkie uit",
|
||||
"snippetLabel": "Brokkie",
|
||||
"selectSnippet": "Kies 'n uittreksel",
|
||||
"snippetMissing": "brokkie nie gevind nie",
|
||||
"snippetRequired": "Kies 'n uittreksel"
|
||||
},
|
||||
"paletteHintRun": "Begin in die aktiewe terminaal-oortjie",
|
||||
"host": {
|
||||
"startupTitle": "Opstart-brokkie",
|
||||
"quickActionsTitle": "Vinnige Aksies",
|
||||
"quickActionsDescription": "Vinnige aksies verskyn as knoppies in die Host Metrics-nutsbalk vir die uitvoering van opdragte met een klik.",
|
||||
"noQuickActions": "Geen vinnige aksies nog nie.",
|
||||
"buttonLabel": "Knoppie-etiket",
|
||||
"selectSnippet": "Kies uittreksel...",
|
||||
"addQuickAction": "Voeg Aksie by"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Voer tans {{name}} uit ...",
|
||||
"succeeded": "{{name}} suksesvol voltooi",
|
||||
"failed": "{{name}} het misluk",
|
||||
"error": "Kon nie {{name}} uitvoer nie"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "تشغيل \"{{name}}\"",
|
||||
"variablesDialogDescription": "أدخل القيم التي يحتاجها هذا المقتطف البرمجي قبل تشغيله.",
|
||||
"variablesPreviewLabel": "معاينة",
|
||||
"variablesConfirmButton": "يجري",
|
||||
"keybindings": {
|
||||
"runSnippet": "تشغيل المقتطف البرمجي الحالي",
|
||||
"snippetLabel": "مقتطف",
|
||||
"selectSnippet": "حدد جزءًا من التعليمات البرمجية",
|
||||
"snippetMissing": "لم يتم العثور على المقتطف",
|
||||
"snippetRequired": "حدد جزءًا من التعليمات البرمجية"
|
||||
},
|
||||
"paletteHintRun": "قم بتشغيله في علامة تبويب الطرفية النشطة",
|
||||
"host": {
|
||||
"startupTitle": "مقتطف بدء التشغيل",
|
||||
"quickActionsTitle": "إجراءات سريعة",
|
||||
"quickActionsDescription": "تظهر الإجراءات السريعة كأزرار في شريط أدوات مقاييس المضيف لتنفيذ الأوامر بنقرة واحدة.",
|
||||
"noQuickActions": "لا توجد إجراءات سريعة بعد.",
|
||||
"buttonLabel": "تسمية الزر",
|
||||
"selectSnippet": "اختر مقتطفًا...",
|
||||
"addQuickAction": "إضافة إجراء"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "جارٍ تنفيذ {{name}}...",
|
||||
"succeeded": "تم إكمال {{name}} بنجاح",
|
||||
"failed": "فشل {{name}}",
|
||||
"error": "فشل تنفيذ {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Изпълни „{{name}}“",
|
||||
"variablesDialogDescription": "Попълнете стойностите, необходими на този фрагмент, преди да го изпълните.",
|
||||
"variablesPreviewLabel": "Преглед",
|
||||
"variablesConfirmButton": "Бягане",
|
||||
"keybindings": {
|
||||
"runSnippet": "Изпълнете съществуващия фрагмент",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Изберете фрагмент",
|
||||
"snippetMissing": "фрагмент не е намерен",
|
||||
"snippetRequired": "Изберете фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Изпълняване в активния раздел на терминала",
|
||||
"host": {
|
||||
"startupTitle": "Стартов фрагмент",
|
||||
"quickActionsTitle": "Бързи действия",
|
||||
"quickActionsDescription": "Бързите действия се показват като бутони в лентата с инструменти Host Metrics за изпълнение на команди с едно щракване.",
|
||||
"noQuickActions": "Все още няма бързи действия.",
|
||||
"buttonLabel": "Етикет на бутона",
|
||||
"selectSnippet": "Изберете фрагмент...",
|
||||
"addQuickAction": "Добавяне на действие"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Изпълнява се {{name}}...",
|
||||
"succeeded": "{{name}} завършено успешно",
|
||||
"failed": "{{name}} неуспешно",
|
||||
"error": "Неуспешно изпълнение на {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "দৌড়াও \"{{name}}\"",
|
||||
"variablesDialogDescription": "এই কোড স্নিপেটটি চালানোর আগে প্রয়োজনীয় মানগুলি পূরণ করুন।",
|
||||
"variablesPreviewLabel": "প্রিভিউ",
|
||||
"variablesConfirmButton": "দৌড়",
|
||||
"keybindings": {
|
||||
"runSnippet": "বিদ্যমান কোড চালান",
|
||||
"snippetLabel": "স্নিপেট",
|
||||
"selectSnippet": "একটি স্নিপেট নির্বাচন করুন",
|
||||
"snippetMissing": "স্নিপেটটি খুঁজে পাওয়া যায়নি",
|
||||
"snippetRequired": "একটি স্নিপেট নির্বাচন করুন"
|
||||
},
|
||||
"paletteHintRun": "সক্রিয় টার্মিনাল ট্যাবে চালান",
|
||||
"host": {
|
||||
"startupTitle": "স্টার্টআপ স্নিপেট",
|
||||
"quickActionsTitle": "দ্রুত পদক্ষেপ",
|
||||
"quickActionsDescription": "এক ক্লিকে কমান্ড কার্যকর করার জন্য কুইক অ্যাকশনগুলো হোস্ট মেট্রিক্স টুলবারে বাটন হিসেবে প্রদর্শিত হয়।",
|
||||
"noQuickActions": "এখনো কোনো দ্রুত পদক্ষেপ নেওয়া হয়নি।",
|
||||
"buttonLabel": "বোতাম লেবেল",
|
||||
"selectSnippet": "স্নিপেট নির্বাচন করুন...",
|
||||
"addQuickAction": "অ্যাকশন যোগ করুন"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} কার্যকর করা হচ্ছে ...",
|
||||
"succeeded": "{{name}} সফলভাবে সম্পন্ন হয়েছে",
|
||||
"failed": "{{name}} ব্যর্থ হয়েছে",
|
||||
"error": "{{name}} কার্যকর করতে ব্যর্থ হয়েছে"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executa \"{{name}}\"",
|
||||
"variablesDialogDescription": "Empleneu els valors que necessita aquest fragment abans d'executar-lo.",
|
||||
"variablesPreviewLabel": "Vista prèvia",
|
||||
"variablesConfirmButton": "Corre",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executa el fragment existent",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selecciona un fragment",
|
||||
"snippetMissing": "fragment no trobat",
|
||||
"snippetRequired": "Selecciona un fragment"
|
||||
},
|
||||
"paletteHintRun": "Executar a la pestanya activa del terminal",
|
||||
"host": {
|
||||
"startupTitle": "Fragment d'inici",
|
||||
"quickActionsTitle": "Accions ràpides",
|
||||
"quickActionsDescription": "Les accions ràpides apareixen com a botons a la barra d'eines de mètriques de l'amfitrió per executar ordres amb un sol clic.",
|
||||
"noQuickActions": "Encara no hi ha accions ràpides.",
|
||||
"buttonLabel": "Etiqueta del botó",
|
||||
"selectSnippet": "Selecciona un fragment...",
|
||||
"addQuickAction": "Afegeix una acció"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executant {{name}}...",
|
||||
"succeeded": "{{name}} s'ha completat correctament",
|
||||
"failed": "{{name}} ha fallat",
|
||||
"error": "No s'ha pogut executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Spustit „{{name}}“",
|
||||
"variablesDialogDescription": "Před spuštěním úryvku kódu vyplňte jeho potřebné hodnoty.",
|
||||
"variablesPreviewLabel": "Náhled",
|
||||
"variablesConfirmButton": "Běh",
|
||||
"keybindings": {
|
||||
"runSnippet": "Spustit existující úryvek kódu",
|
||||
"snippetLabel": "Úryvek",
|
||||
"selectSnippet": "Vyberte úryvek",
|
||||
"snippetMissing": "úryvek nenalezen",
|
||||
"snippetRequired": "Vyberte úryvek"
|
||||
},
|
||||
"paletteHintRun": "Spustit v aktivní kartě terminálu",
|
||||
"host": {
|
||||
"startupTitle": "Úryvek pro spuštění",
|
||||
"quickActionsTitle": "Rychlé akce",
|
||||
"quickActionsDescription": "Rychlé akce se zobrazují jako tlačítka v panelu nástrojů Metriky hostitele pro spuštění příkazů jedním kliknutím.",
|
||||
"noQuickActions": "Zatím žádné rychlé akce.",
|
||||
"buttonLabel": "Štítek tlačítka",
|
||||
"selectSnippet": "Vybrat úryvek...",
|
||||
"addQuickAction": "Přidat akci"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Spouštění {{name}}...",
|
||||
"succeeded": "{{name}} úspěšně dokončeno",
|
||||
"failed": "{{name}} selhalo",
|
||||
"error": "Nepodařilo se spustit {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kør \"{{name}}\"",
|
||||
"variablesDialogDescription": "Udfyld de værdier, som dette kodestykke skal bruge, før det køres.",
|
||||
"variablesPreviewLabel": "Forhåndsvisning",
|
||||
"variablesConfirmButton": "Løbe",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kør eksisterende kodestykke",
|
||||
"snippetLabel": "Uddrag",
|
||||
"selectSnippet": "Vælg et uddrag",
|
||||
"snippetMissing": "uddrag ikke fundet",
|
||||
"snippetRequired": "Vælg et uddrag"
|
||||
},
|
||||
"paletteHintRun": "Kør i den aktive terminalfane",
|
||||
"host": {
|
||||
"startupTitle": "Opstartskodestykke",
|
||||
"quickActionsTitle": "Hurtige handlinger",
|
||||
"quickActionsDescription": "Hurtige handlinger vises som knapper i værktøjslinjen Host Metrics til udførelse af kommandoer med et enkelt klik.",
|
||||
"noQuickActions": "Ingen hurtige handlinger endnu.",
|
||||
"buttonLabel": "Knapmærke",
|
||||
"selectSnippet": "Vælg uddrag...",
|
||||
"addQuickAction": "Tilføj handling"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Udfører {{name}}...",
|
||||
"succeeded": "{{name}} gennemført",
|
||||
"failed": "{{name}} mislykkedes",
|
||||
"error": "Kunne ikke udføre {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Run \"{{name}}\"",
|
||||
"variablesDialogDescription": "Füllen Sie die für diesen Codeabschnitt erforderlichen Werte aus, bevor Sie ihn ausführen.",
|
||||
"variablesPreviewLabel": "Vorschau",
|
||||
"variablesConfirmButton": "Laufen",
|
||||
"keybindings": {
|
||||
"runSnippet": "Vorhandenen Codeausschnitt ausführen",
|
||||
"snippetLabel": "Ausschnitt",
|
||||
"selectSnippet": "Wählen Sie einen Ausschnitt aus",
|
||||
"snippetMissing": "Ausschnitt nicht gefunden",
|
||||
"snippetRequired": "Wählen Sie einen Ausschnitt aus"
|
||||
},
|
||||
"paletteHintRun": "Im aktiven Terminal-Tab ausführen",
|
||||
"host": {
|
||||
"startupTitle": "Startup-Snippet",
|
||||
"quickActionsTitle": "Schnellaktionen",
|
||||
"quickActionsDescription": "Schnellaktionen erscheinen als Schaltflächen in der Host-Metriken-Symbolleiste zur Ausführung von Befehlen mit einem Klick.",
|
||||
"noQuickActions": "Noch keine Schnellaktionen.",
|
||||
"buttonLabel": "Schaltflächenbeschriftung",
|
||||
"selectSnippet": "Snippet auswählen...",
|
||||
"addQuickAction": "Aktion hinzufügen"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} wird ausgeführt...",
|
||||
"succeeded": "{{name}} erfolgreich abgeschlossen",
|
||||
"failed": "{{name}} fehlgeschlagen",
|
||||
"error": "Fehler beim Ausführen von {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Εκτέλεση \"{{name}}\"",
|
||||
"variablesDialogDescription": "Συμπληρώστε τις τιμές που χρειάζεται αυτό το τμήμα κώδικα πριν το εκτελέσετε.",
|
||||
"variablesPreviewLabel": "Πρεμιέρα",
|
||||
"variablesConfirmButton": "Τρέξιμο",
|
||||
"keybindings": {
|
||||
"runSnippet": "Εκτέλεση υπάρχοντος αποσπάσματος",
|
||||
"snippetLabel": "Απόσπασμα",
|
||||
"selectSnippet": "Επιλέξτε ένα απόσπασμα",
|
||||
"snippetMissing": "το απόσπασμα δεν βρέθηκε",
|
||||
"snippetRequired": "Επιλέξτε ένα απόσπασμα"
|
||||
},
|
||||
"paletteHintRun": "Εκτέλεση στην ενεργή καρτέλα τερματικού",
|
||||
"host": {
|
||||
"startupTitle": "Απόσπασμα εκκίνησης",
|
||||
"quickActionsTitle": "Γρήγορες ενέργειες",
|
||||
"quickActionsDescription": "Οι γρήγορες ενέργειες εμφανίζονται ως κουμπιά στη γραμμή εργαλείων Μετρήσεων κεντρικού υπολογιστή για εκτέλεση εντολών με ένα κλικ.",
|
||||
"noQuickActions": "Δεν υπάρχουν ακόμη γρήγορες ενέργειες.",
|
||||
"buttonLabel": "Ετικέτα κουμπιού",
|
||||
"selectSnippet": "Επιλογή αποσπάσματος...",
|
||||
"addQuickAction": "Προσθήκη ενέργειας"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Εκτέλεση {{name}}...",
|
||||
"succeeded": "{{name}} ολοκληρώθηκε με επιτυχία",
|
||||
"failed": "{{name}} απέτυχε",
|
||||
"error": "Αποτυχία εκτέλεσης {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Ejecutar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Rellena los valores que necesita este fragmento de código antes de ejecutarlo.",
|
||||
"variablesPreviewLabel": "Avance",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Ejecutar el fragmento existente",
|
||||
"snippetLabel": "Retazo",
|
||||
"selectSnippet": "Seleccione un fragmento",
|
||||
"snippetMissing": "Fragmento no encontrado",
|
||||
"snippetRequired": "Seleccione un fragmento"
|
||||
},
|
||||
"paletteHintRun": "Ejecutar en la pestaña de terminal activa",
|
||||
"host": {
|
||||
"startupTitle": "Fragmento de inicio",
|
||||
"quickActionsTitle": "Acciones rápidas",
|
||||
"quickActionsDescription": "Las acciones rápidas aparecen como botones en la barra de herramientas de Métricas del host para la ejecución de comandos con un solo clic.",
|
||||
"noQuickActions": "Aún no hay acciones rápidas.",
|
||||
"buttonLabel": "Etiqueta del botón",
|
||||
"selectSnippet": "Seleccionar fragmento...",
|
||||
"addQuickAction": "Añadir acción"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Ejecutando {{name}}...",
|
||||
"succeeded": "{{name}} completado correctamente",
|
||||
"failed": "{{name}} falló",
|
||||
"error": "Error al ejecutar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Suorita \"{{name}}\"",
|
||||
"variablesDialogDescription": "Täytä tämän koodinpätkän tarvitsemat arvot ennen sen suorittamista.",
|
||||
"variablesPreviewLabel": "Esikatselu",
|
||||
"variablesConfirmButton": "Juokse",
|
||||
"keybindings": {
|
||||
"runSnippet": "Suorita olemassa oleva koodinpätkä",
|
||||
"snippetLabel": "Katkelma",
|
||||
"selectSnippet": "Valitse katkelma",
|
||||
"snippetMissing": "katkelmaa ei löytynyt",
|
||||
"snippetRequired": "Valitse katkelma"
|
||||
},
|
||||
"paletteHintRun": "Suorita aktiivisessa päätevälilehdessä",
|
||||
"host": {
|
||||
"startupTitle": "Käynnistyskoodinpätkä",
|
||||
"quickActionsTitle": "Pikatoiminnot",
|
||||
"quickActionsDescription": "Pikatoiminnot näkyvät painikkeina isäntämetriikkatyökalurivillä komentojen suorittamista varten yhdellä napsautuksella.",
|
||||
"noQuickActions": "Ei vielä nopeita toimia.",
|
||||
"buttonLabel": "Painikkeen nimi",
|
||||
"selectSnippet": "Valitse katkelma...",
|
||||
"addQuickAction": "Lisää toiminto"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Suoritetaan {{name}}...",
|
||||
"succeeded": "{{name}} suoritettu onnistuneesti",
|
||||
"failed": "{{name}} epäonnistui",
|
||||
"error": "Suoritus epäonnistui {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Exécutez \"{{name}}\"",
|
||||
"variablesDialogDescription": "Renseignez les valeurs nécessaires à cet extrait de code avant de l'exécuter.",
|
||||
"variablesPreviewLabel": "Aperçu",
|
||||
"variablesConfirmButton": "Courir",
|
||||
"keybindings": {
|
||||
"runSnippet": "Exécuter l'extrait de code existant",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Sélectionnez un extrait",
|
||||
"snippetMissing": "extrait introuvable",
|
||||
"snippetRequired": "Sélectionnez un extrait"
|
||||
},
|
||||
"paletteHintRun": "Exécuter dans l'onglet du terminal actif",
|
||||
"host": {
|
||||
"startupTitle": "Extrait de démarrage",
|
||||
"quickActionsTitle": "Actions rapides",
|
||||
"quickActionsDescription": "Les actions rapides apparaissent sous forme de boutons dans la barre d'outils Métriques de l'hôte pour une exécution en un clic.",
|
||||
"noQuickActions": "Aucune action rapide pour le moment.",
|
||||
"buttonLabel": "Étiquette du bouton",
|
||||
"selectSnippet": "Sélectionner un snippet...",
|
||||
"addQuickAction": "Ajouter une action"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Exécution de {{name}}...",
|
||||
"succeeded": "{{name}} terminé avec succès",
|
||||
"failed": "Échec de {{name}}",
|
||||
"error": "Échec de l'exécution de {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "הפעל \"{{name}}\"",
|
||||
"variablesDialogDescription": "מלא את הערכים הדרושים לקטע זה לפני הפעלתו.",
|
||||
"variablesPreviewLabel": "תצוגה מקדימה",
|
||||
"variablesConfirmButton": "לָרוּץ",
|
||||
"keybindings": {
|
||||
"runSnippet": "הפעלת קטע קוד קיים",
|
||||
"snippetLabel": "קטע",
|
||||
"selectSnippet": "בחר קטע",
|
||||
"snippetMissing": "קטע לא נמצא",
|
||||
"snippetRequired": "בחר קטע"
|
||||
},
|
||||
"paletteHintRun": "הפעל בלשונית הטרמינל הפעילה",
|
||||
"host": {
|
||||
"startupTitle": "קטע הפעלה",
|
||||
"quickActionsTitle": "פעולות מהירות",
|
||||
"quickActionsDescription": "פעולות מהירות מופיעות ככפתורים בסרגל הכלים של Host Metrics לביצוע פקודה בלחיצה אחת.",
|
||||
"noQuickActions": "אין עדיין פעולות מהירות.",
|
||||
"buttonLabel": "תווית כפתור",
|
||||
"selectSnippet": "בחר קטע...",
|
||||
"addQuickAction": "הוסף פעולה"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "מבצע {{name}}...",
|
||||
"succeeded": "{{name}} הושלם בהצלחה",
|
||||
"failed": "{{name}} נכשל",
|
||||
"error": "נכשל בביצוע {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "रन \"{{name}}\"",
|
||||
"variablesDialogDescription": "इस कोड स्निपेट को चलाने से पहले इसमें आवश्यक मान भरें।",
|
||||
"variablesPreviewLabel": "पूर्व दर्शन",
|
||||
"variablesConfirmButton": "दौड़ना",
|
||||
"keybindings": {
|
||||
"runSnippet": "मौजूदा स्निपेट चलाएँ",
|
||||
"snippetLabel": "टुकड़ा",
|
||||
"selectSnippet": "एक अंश का चयन करें",
|
||||
"snippetMissing": "अंश नहीं मिला",
|
||||
"snippetRequired": "एक अंश का चयन करें"
|
||||
},
|
||||
"paletteHintRun": "सक्रिय टर्मिनल टैब में चलाएँ",
|
||||
"host": {
|
||||
"startupTitle": "स्टार्टअप स्निपेट",
|
||||
"quickActionsTitle": "त्वरित कार्रवाइयाँ",
|
||||
"quickActionsDescription": "त्वरित कार्रवाइयाँ एक-क्लिक कमांड निष्पादन के लिए होस्ट मेट्रिक्स टूलबार में बटन के रूप में दिखाई देती हैं।",
|
||||
"noQuickActions": "अभी तक कोई त्वरित कार्रवाई नहीं।",
|
||||
"buttonLabel": "बटन लेबल",
|
||||
"selectSnippet": "स्निपेट चुनें...",
|
||||
"addQuickAction": "कार्रवाई जोड़ें"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} निष्पादित हो रहा है...",
|
||||
"succeeded": "{{name}} सफलतापूर्वक पूर्ण हुआ",
|
||||
"failed": "{{name}} विफल रहा",
|
||||
"error": "{{name}} निष्पादित करने में विफल"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Futtassa a \"{{name}}\"",
|
||||
"variablesDialogDescription": "Töltse ki a kódrészlet futtatása előtt szükséges értékeket.",
|
||||
"variablesPreviewLabel": "Előnézet",
|
||||
"variablesConfirmButton": "Fut",
|
||||
"keybindings": {
|
||||
"runSnippet": "Meglévő kódrészlet futtatása",
|
||||
"snippetLabel": "Töredék",
|
||||
"selectSnippet": "Válasszon ki egy részletet",
|
||||
"snippetMissing": "részlet nem található",
|
||||
"snippetRequired": "Válasszon ki egy részletet"
|
||||
},
|
||||
"paletteHintRun": "Futtatás az aktív terminálfülön",
|
||||
"host": {
|
||||
"startupTitle": "Indítási kódrészlet",
|
||||
"quickActionsTitle": "Gyors műveletek",
|
||||
"quickActionsDescription": "A gyors műveletek gombokként jelennek meg a Host Metrics eszköztáron, így egyetlen kattintással végrehajthatók a parancsok.",
|
||||
"noQuickActions": "Még nincsenek gyors intézkedések.",
|
||||
"buttonLabel": "Gomb felirata",
|
||||
"selectSnippet": "Válasszon részletet...",
|
||||
"addQuickAction": "Művelet hozzáadása"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} végrehajtása ...",
|
||||
"succeeded": "{{name}} sikeresen befejeződött",
|
||||
"failed": "{{name}} sikertelen",
|
||||
"error": "Nem sikerült végrehajtani a {{name}} parancsot."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Jalankan \"{{name}}\"",
|
||||
"variablesDialogDescription": "Isi nilai-nilai yang dibutuhkan cuplikan kode ini sebelum menjalankannya.",
|
||||
"variablesPreviewLabel": "Pratinjau",
|
||||
"variablesConfirmButton": "Berlari",
|
||||
"keybindings": {
|
||||
"runSnippet": "Jalankan cuplikan kode yang sudah ada.",
|
||||
"snippetLabel": "Potongan",
|
||||
"selectSnippet": "Pilih cuplikan",
|
||||
"snippetMissing": "cuplikan tidak ditemukan",
|
||||
"snippetRequired": "Pilih cuplikan"
|
||||
},
|
||||
"paletteHintRun": "Jalankan di tab terminal aktif",
|
||||
"host": {
|
||||
"startupTitle": "Cuplikan Startup",
|
||||
"quickActionsTitle": "Tindakan Cepat",
|
||||
"quickActionsDescription": "Tindakan cepat muncul sebagai tombol di bilah alat Metrik Host untuk eksekusi perintah sekali klik.",
|
||||
"noQuickActions": "Belum ada tindakan cepat.",
|
||||
"buttonLabel": "Label tombol",
|
||||
"selectSnippet": "Pilih cuplikan...",
|
||||
"addQuickAction": "Tambahkan Tindakan"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Menjalankan {{name}}...",
|
||||
"succeeded": "{{name}} berhasil diselesaikan",
|
||||
"failed": "{{name}} gagal",
|
||||
"error": "Gagal mengeksekusi {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Esegui \"{{name}}\"",
|
||||
"variablesDialogDescription": "Inserisci i valori richiesti da questo frammento di codice prima di eseguirlo.",
|
||||
"variablesPreviewLabel": "Anteprima",
|
||||
"variablesConfirmButton": "Correre",
|
||||
"keybindings": {
|
||||
"runSnippet": "Esegui il frammento di codice esistente",
|
||||
"snippetLabel": "Frammento",
|
||||
"selectSnippet": "Seleziona un frammento",
|
||||
"snippetMissing": "frammento non trovato",
|
||||
"snippetRequired": "Seleziona un frammento"
|
||||
},
|
||||
"paletteHintRun": "Esegui nella scheda del terminale attiva",
|
||||
"host": {
|
||||
"startupTitle": "Frammento di avvio",
|
||||
"quickActionsTitle": "Azioni Rapide",
|
||||
"quickActionsDescription": "Le azioni rapide appaiono come pulsanti nella barra delle metriche host per l'esecuzione di comandi con un clic.",
|
||||
"noQuickActions": "Nessuna azione rapida ancora.",
|
||||
"buttonLabel": "Etichetta pulsante",
|
||||
"selectSnippet": "Seleziona snippet...",
|
||||
"addQuickAction": "Aggiungi Azione"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Esecuzione di {{name}}...",
|
||||
"succeeded": "{{name}} completato con successo",
|
||||
"failed": "{{name}} non riuscito",
|
||||
"error": "Impossibile eseguire {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "「{{name}} を実行」",
|
||||
"variablesDialogDescription": "このコードスニペットを実行する前に、必要な値を入力してください。",
|
||||
"variablesPreviewLabel": "プレビュー",
|
||||
"variablesConfirmButton": "走る",
|
||||
"keybindings": {
|
||||
"runSnippet": "既存のスニペットを実行する",
|
||||
"snippetLabel": "抜粋",
|
||||
"selectSnippet": "抜粋を選択してください",
|
||||
"snippetMissing": "スニペットが見つかりません",
|
||||
"snippetRequired": "抜粋を選択してください"
|
||||
},
|
||||
"paletteHintRun": "アクティブなターミナルタブで実行します",
|
||||
"host": {
|
||||
"startupTitle": "起動時スニペット",
|
||||
"quickActionsTitle": "クイックアクション",
|
||||
"quickActionsDescription": "クイックアクションはホストメトリクスツールバーにボタンとして表示され、ワンクリックでコマンドを実行できます。",
|
||||
"noQuickActions": "クイックアクションはまだありません",
|
||||
"buttonLabel": "ボタンラベル",
|
||||
"selectSnippet": "スニペットを選択...",
|
||||
"addQuickAction": "アクションを追加"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} を実行中...",
|
||||
"succeeded": "{{name}} が正常に完了しました",
|
||||
"failed": "{{name}} が失敗しました",
|
||||
"error": "{{name}} の実行に失敗しました"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "\"{{name}} \" 실행",
|
||||
"variablesDialogDescription": "이 코드 조각을 실행하기 전에 필요한 값을 입력하세요.",
|
||||
"variablesPreviewLabel": "시사",
|
||||
"variablesConfirmButton": "달리다",
|
||||
"keybindings": {
|
||||
"runSnippet": "기존 코드 조각을 실행하세요",
|
||||
"snippetLabel": "단편",
|
||||
"selectSnippet": "코드 조각을 선택하세요",
|
||||
"snippetMissing": "스니펫을 찾을 수 없습니다.",
|
||||
"snippetRequired": "코드 조각을 선택하세요"
|
||||
},
|
||||
"paletteHintRun": "현재 활성화된 터미널 탭에서 실행하세요",
|
||||
"host": {
|
||||
"startupTitle": "시작 스니펫",
|
||||
"quickActionsTitle": "빠른 작업",
|
||||
"quickActionsDescription": "빠른 작업은 한 번의 클릭으로 명령을 실행할 수 있도록 호스트 메트릭 도구 모음에 버튼으로 표시됩니다.",
|
||||
"noQuickActions": "아직 빠른 작업이 없습니다.",
|
||||
"buttonLabel": "버튼 레이블",
|
||||
"selectSnippet": "스니펫 선택...",
|
||||
"addQuickAction": "작업 추가"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} 실행 중...",
|
||||
"succeeded": "{{name}}이(가) 성공적으로 완료되었습니다.",
|
||||
"failed": "{{name}} 실패",
|
||||
"error": "{{name}} 실행에 실패했습니다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Uitvoeren \"{{name}}\"",
|
||||
"variablesDialogDescription": "Vul de waarden in die dit codefragment nodig heeft voordat je het uitvoert.",
|
||||
"variablesPreviewLabel": "Voorbeeld",
|
||||
"variablesConfirmButton": "Loop",
|
||||
"keybindings": {
|
||||
"runSnippet": "Voer het bestaande codefragment uit.",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selecteer een fragment",
|
||||
"snippetMissing": "fragment niet gevonden",
|
||||
"snippetRequired": "Selecteer een fragment"
|
||||
},
|
||||
"paletteHintRun": "Voer dit uit in het actieve terminaltabblad.",
|
||||
"host": {
|
||||
"startupTitle": "Opstartfragment",
|
||||
"quickActionsTitle": "Snelle acties",
|
||||
"quickActionsDescription": "Snelle acties verschijnen als knoppen in de Host Metrics-werkbalk, zodat u opdrachten met één klik kunt uitvoeren.",
|
||||
"noQuickActions": "Nog geen snelle acties ondernomen.",
|
||||
"buttonLabel": "Knoplabel",
|
||||
"selectSnippet": "Selecteer fragment...",
|
||||
"addQuickAction": "Actie toevoegen"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Het uitvoeren van {{name}}...",
|
||||
"succeeded": "{{name}} succesvol voltooid",
|
||||
"failed": "{{name}} mislukt",
|
||||
"error": "Het uitvoeren van {{name}} is mislukt."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kjør «{{name}}»",
|
||||
"variablesDialogDescription": "Fyll inn verdiene dette kodestykket trenger før du kjører det.",
|
||||
"variablesPreviewLabel": "Forhåndsvisning",
|
||||
"variablesConfirmButton": "Løp",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kjør eksisterende kodebit",
|
||||
"snippetLabel": "Utdrag",
|
||||
"selectSnippet": "Velg et utdrag",
|
||||
"snippetMissing": "utdraget ikke funnet",
|
||||
"snippetRequired": "Velg et utdrag"
|
||||
},
|
||||
"paletteHintRun": "Kjør i den aktive terminalfanen",
|
||||
"host": {
|
||||
"startupTitle": "Oppstartsutdrag",
|
||||
"quickActionsTitle": "Hurtighandlinger",
|
||||
"quickActionsDescription": "Hurtighandlinger vises som knapper i verktøylinjen for vertsmålinger for utførelse av kommandoer med ett klikk.",
|
||||
"noQuickActions": "Ingen hurtighandlinger ennå.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippet": "Velg utdrag...",
|
||||
"addQuickAction": "Legg til handling"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Utfører {{name}}...",
|
||||
"succeeded": "{{name}} fullført",
|
||||
"failed": "{{name}} mislyktes",
|
||||
"error": "Kunne ikke utføre {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Uruchom „{{name}}”",
|
||||
"variablesDialogDescription": "Przed uruchomieniem snippetu wprowadź wymagane wartości.",
|
||||
"variablesPreviewLabel": "Podgląd",
|
||||
"variablesConfirmButton": "Uruchom",
|
||||
"keybindings": {
|
||||
"runSnippet": "Uruchom istniejący snippet",
|
||||
"snippetLabel": "Snippet",
|
||||
"selectSnippet": "Wybierz snippet",
|
||||
"snippetMissing": "nie znaleziono snippetu",
|
||||
"snippetRequired": "Wybierz snippet"
|
||||
},
|
||||
"paletteHintRun": "Uruchom w aktywnej karcie terminala",
|
||||
"host": {
|
||||
"startupTitle": "Snippet startowy",
|
||||
"quickActionsTitle": "Szybkie akcje",
|
||||
"quickActionsDescription": "Szybkie akcje pojawiają się jako przyciski na pasku narzędzi Metryki hosta, umożliwiając wykonanie polecenia jednym kliknięciem.",
|
||||
"noQuickActions": "Brak szybkich akcji.",
|
||||
"buttonLabel": "Etykieta przycisku",
|
||||
"selectSnippet": "Wybierz snippet...",
|
||||
"addQuickAction": "Dodaj akcję"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Wykonywanie {{name}}...",
|
||||
"succeeded": "{{name}} ukończono pomyślnie",
|
||||
"failed": "{{name}} nie powiodło się",
|
||||
"error": "Nie udało się wykonać {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Preencha os valores necessários para este trecho de código antes de executá-lo.",
|
||||
"variablesPreviewLabel": "Pré-visualização",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executar trecho de código existente",
|
||||
"snippetLabel": "Trecho",
|
||||
"selectSnippet": "Selecione um trecho",
|
||||
"snippetMissing": "trecho não encontrado",
|
||||
"snippetRequired": "Selecione um trecho"
|
||||
},
|
||||
"paletteHintRun": "Execute na aba do terminal ativo",
|
||||
"host": {
|
||||
"startupTitle": "Trecho de startup",
|
||||
"quickActionsTitle": "Ações rápidas",
|
||||
"quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas do Host Metrics para execução de comandos com um único clique.",
|
||||
"noQuickActions": "Nenhuma ação imediata ainda.",
|
||||
"buttonLabel": "Rótulo do botão",
|
||||
"selectSnippet": "Selecione o trecho...",
|
||||
"addQuickAction": "Adicionar ação"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executando {{name}}...",
|
||||
"succeeded": "{{name}} concluído com sucesso",
|
||||
"failed": "{{name}} falhou",
|
||||
"error": "Falha ao executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Preencha os valores necessários para este excerto de código antes de o executar.",
|
||||
"variablesPreviewLabel": "Pré-visualização",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executar excerto de código existente",
|
||||
"snippetLabel": "Trecho",
|
||||
"selectSnippet": "Selecione um excerto",
|
||||
"snippetMissing": "trecho não encontrado",
|
||||
"snippetRequired": "Selecione um excerto"
|
||||
},
|
||||
"paletteHintRun": "Execute no separador do terminal ativo",
|
||||
"host": {
|
||||
"startupTitle": "Fragmento de inicialização",
|
||||
"quickActionsTitle": "Ações Rápidas",
|
||||
"quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas de Métricas do Host para execução de comandos com um clique.",
|
||||
"noQuickActions": "Ainda não existem ações rápidas.",
|
||||
"buttonLabel": "Texto do botão",
|
||||
"selectSnippet": "Selecionar snippet...",
|
||||
"addQuickAction": "Adicionar Ação"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "A executar {{name}}...",
|
||||
"succeeded": "{{name}} concluído com sucesso",
|
||||
"failed": "{{name}} falhou",
|
||||
"error": "Falha ao executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executați „{{name}}”",
|
||||
"variablesDialogDescription": "Completați valorile necesare acestui fragment de cod înainte de a-l rula.",
|
||||
"variablesPreviewLabel": "Previzualizare",
|
||||
"variablesConfirmButton": "Aleargă",
|
||||
"keybindings": {
|
||||
"runSnippet": "Execută fragmentul existent",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selectați un fragment",
|
||||
"snippetMissing": "fragmentul nu a fost găsit",
|
||||
"snippetRequired": "Selectați un fragment"
|
||||
},
|
||||
"paletteHintRun": "Execută în fila activă a terminalului",
|
||||
"host": {
|
||||
"startupTitle": "Fragment de pornire",
|
||||
"quickActionsTitle": "Acțiuni rapide",
|
||||
"quickActionsDescription": "Acțiunile rapide apar sub formă de butoane în bara de instrumente Host Metrics pentru executarea comenzilor cu un singur clic.",
|
||||
"noQuickActions": "Nicio acțiune rapidă încă.",
|
||||
"buttonLabel": "Etichetă buton",
|
||||
"selectSnippet": "Selectați fragmentul...",
|
||||
"addQuickAction": "Adăugați o acțiune"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Se execută {{name}}...",
|
||||
"succeeded": "{{name}} finalizat cu succes",
|
||||
"failed": "{{name}} a eșuat",
|
||||
"error": "Executarea {{name}} a eșuat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Запуск \"{{name}}\"",
|
||||
"variablesDialogDescription": "Перед запуском заполните необходимые для этого фрагмента значения.",
|
||||
"variablesPreviewLabel": "Предварительный просмотр",
|
||||
"variablesConfirmButton": "Бегать",
|
||||
"keybindings": {
|
||||
"runSnippet": "Запустить существующий фрагмент",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Выберите фрагмент",
|
||||
"snippetMissing": "фрагмент не найден",
|
||||
"snippetRequired": "Выберите фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Запустите в активной вкладке терминала.",
|
||||
"host": {
|
||||
"startupTitle": "Фрагмент запуска",
|
||||
"quickActionsTitle": "Быстрые действия",
|
||||
"quickActionsDescription": "Быстрые действия отображаются в виде кнопок на панели инструментов метрик хоста для выполнения команд в один клик.",
|
||||
"noQuickActions": "Пока нет быстрых действий.",
|
||||
"buttonLabel": "Надпись кнопки",
|
||||
"selectSnippet": "Выбрать сниппет...",
|
||||
"addQuickAction": "Добавить действие"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Выполняется {{name}}...",
|
||||
"succeeded": "{{name}} успешно завершено",
|
||||
"failed": "{{name}} не удалось",
|
||||
"error": "Не удалось выполнить {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Покрени „{{name}}“",
|
||||
"variablesDialogDescription": "Унесите вредности које су потребне овом фрагменту пре него што га покренете.",
|
||||
"variablesPreviewLabel": "Преглед",
|
||||
"variablesConfirmButton": "Трчи",
|
||||
"keybindings": {
|
||||
"runSnippet": "Покрени постојећи исечак кода",
|
||||
"snippetLabel": "Исечак",
|
||||
"selectSnippet": "Изаберите исечак",
|
||||
"snippetMissing": "фрагмент није пронађен",
|
||||
"snippetRequired": "Изаберите исечак"
|
||||
},
|
||||
"paletteHintRun": "Покрени у активној картици терминала",
|
||||
"host": {
|
||||
"startupTitle": "Исечак за покретање",
|
||||
"quickActionsTitle": "Брзе акције",
|
||||
"quickActionsDescription": "Брзе акције се појављују као дугмад у траци са алаткама Host Metrics за извршавање команди једним кликом.",
|
||||
"noQuickActions": "Још нема брзих акција.",
|
||||
"buttonLabel": "Ознака дугмета",
|
||||
"selectSnippet": "Изаберите исечак...",
|
||||
"addQuickAction": "Додај акцију"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Извршава се {{name}}...",
|
||||
"succeeded": "{{name}} успешно завршено",
|
||||
"failed": "{{name}} није успело",
|
||||
"error": "Није успело извршавање {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kör \"{{name}}\"",
|
||||
"variablesDialogDescription": "Fyll i de värden som detta kodavsnitt behöver innan du kör det.",
|
||||
"variablesPreviewLabel": "Förhandsvisning",
|
||||
"variablesConfirmButton": "Sikt",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kör befintligt kodavsnitt",
|
||||
"snippetLabel": "Utdrag",
|
||||
"selectSnippet": "Välj ett utdrag",
|
||||
"snippetMissing": "utdraget hittades inte",
|
||||
"snippetRequired": "Välj ett utdrag"
|
||||
},
|
||||
"paletteHintRun": "Kör i den aktiva terminalfliken",
|
||||
"host": {
|
||||
"startupTitle": "Startkodavsnitt",
|
||||
"quickActionsTitle": "Snabbåtgärder",
|
||||
"quickActionsDescription": "Snabbåtgärder visas som knappar i verktygsfältet Host Metrics för att köra kommandon med ett klick.",
|
||||
"noQuickActions": "Inga snabba åtgärder ännu.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippet": "Välj utdrag...",
|
||||
"addQuickAction": "Lägg till åtgärd"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Kör {{name}}...",
|
||||
"succeeded": "{{name}} slutfört",
|
||||
"failed": "{{name}} misslyckades",
|
||||
"error": "Misslyckades med att köra {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "วิ่ง \"{{name}}\"",
|
||||
"variablesDialogDescription": "กรอกค่าที่จำเป็นสำหรับโค้ดส่วนนี้ก่อนที่จะเรียกใช้งาน",
|
||||
"variablesPreviewLabel": "ตัวอย่าง",
|
||||
"variablesConfirmButton": "วิ่ง",
|
||||
"keybindings": {
|
||||
"runSnippet": "เรียกใช้โค้ดตัวอย่างที่มีอยู่",
|
||||
"snippetLabel": "ส่วนหนึ่ง",
|
||||
"selectSnippet": "เลือกส่วนย่อย",
|
||||
"snippetMissing": "ไม่พบส่วนย่อย",
|
||||
"snippetRequired": "เลือกส่วนย่อย"
|
||||
},
|
||||
"paletteHintRun": "เรียกใช้ในแท็บเทอร์มินัลที่ใช้งานอยู่",
|
||||
"host": {
|
||||
"startupTitle": "ตัวอย่างโค้ดสำหรับเริ่มต้น",
|
||||
"quickActionsTitle": "การดำเนินการด่วน",
|
||||
"quickActionsDescription": "การดำเนินการด่วนจะปรากฏเป็นปุ่มในแถบเครื่องมือ Host Metrics เพื่อให้สามารถเรียกใช้คำสั่งได้ด้วยการคลิกเพียงครั้งเดียว",
|
||||
"noQuickActions": "ยังไม่มีการดำเนินการใดๆ อย่างเร่งด่วนในขณะนี้",
|
||||
"buttonLabel": "ป้ายกำกับปุ่ม",
|
||||
"selectSnippet": "เลือกส่วนย่อย...",
|
||||
"addQuickAction": "เพิ่มการดำเนินการ"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "กำลังดำเนินการ {{name}}...",
|
||||
"succeeded": "{{name}} เสร็จสมบูรณ์เรียบร้อยแล้ว",
|
||||
"failed": "{{name}} ล้มเหลว",
|
||||
"error": "ไม่สามารถดำเนินการ {{name}} ได้"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Çalıştır \"{{name}}\"",
|
||||
"variablesDialogDescription": "Bu kod parçasını çalıştırmadan önce gerekli değerleri girin.",
|
||||
"variablesPreviewLabel": "Önizleme",
|
||||
"variablesConfirmButton": "Koşmak",
|
||||
"keybindings": {
|
||||
"runSnippet": "Mevcut kod parçasını çalıştır",
|
||||
"snippetLabel": "Alıntı",
|
||||
"selectSnippet": "Bir parça seçin",
|
||||
"snippetMissing": "Kod parçası bulunamadı",
|
||||
"snippetRequired": "Bir parça seçin"
|
||||
},
|
||||
"paletteHintRun": "Etkin terminal sekmesinde çalıştırın.",
|
||||
"host": {
|
||||
"startupTitle": "Başlangıç Betiği",
|
||||
"quickActionsTitle": "Hızlı Eylemler",
|
||||
"quickActionsDescription": "Hızlı eylemler, tek tıklamayla komut çalıştırmak için Ana Bilgisayar Metrikleri araç çubuğunda düğme olarak görünür.",
|
||||
"noQuickActions": "Henüz hızlı eylem yok.",
|
||||
"buttonLabel": "Düğme etiketi",
|
||||
"selectSnippet": "Parçacık seç...",
|
||||
"addQuickAction": "Eylem Ekle"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} çalıştırılıyor...",
|
||||
"succeeded": "{{name}} başarıyla tamamlandı",
|
||||
"failed": "{{name}} başarısız oldu",
|
||||
"error": "{{name}} çalıştırılamadı"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Виконати \"{{name}}\"",
|
||||
"variablesDialogDescription": "Заповніть значення, необхідні цьому фрагменту, перш ніж запускати його.",
|
||||
"variablesPreviewLabel": "Попередній перегляд",
|
||||
"variablesConfirmButton": "Бігти",
|
||||
"keybindings": {
|
||||
"runSnippet": "Запустити існуючий фрагмент коду",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Виберіть фрагмент",
|
||||
"snippetMissing": "фрагмент не знайдено",
|
||||
"snippetRequired": "Виберіть фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Запустити в активній вкладці терміналу",
|
||||
"host": {
|
||||
"startupTitle": "Фрагмент запуску",
|
||||
"quickActionsTitle": "Швидкі дії",
|
||||
"quickActionsDescription": "Швидкі дії відображаються як кнопки на панелі інструментів «Метрики хоста» для виконання команд одним клацанням миші.",
|
||||
"noQuickActions": "Поки що жодних швидких дій.",
|
||||
"buttonLabel": "Мітка кнопки",
|
||||
"selectSnippet": "Вибрати фрагмент...",
|
||||
"addQuickAction": "Додати дію"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Виконання {{name}}...",
|
||||
"succeeded": "{{name}} успішно завершено",
|
||||
"failed": "{{name}} не вдалося",
|
||||
"error": "Не вдалося виконати {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Chạy \"{{name}}\"",
|
||||
"variablesDialogDescription": "Hãy điền đầy đủ các giá trị cần thiết cho đoạn mã này trước khi chạy.",
|
||||
"variablesPreviewLabel": "Xem trước",
|
||||
"variablesConfirmButton": "Chạy",
|
||||
"keybindings": {
|
||||
"runSnippet": "Chạy đoạn mã hiện có",
|
||||
"snippetLabel": "Đoạn trích",
|
||||
"selectSnippet": "Chọn một đoạn trích",
|
||||
"snippetMissing": "Không tìm thấy đoạn mã.",
|
||||
"snippetRequired": "Chọn một đoạn trích"
|
||||
},
|
||||
"paletteHintRun": "Chạy trong tab cửa sổ terminal đang hoạt động",
|
||||
"host": {
|
||||
"startupTitle": "Đoạn mã khởi động",
|
||||
"quickActionsTitle": "Hành động nhanh",
|
||||
"quickActionsDescription": "Hành động nhanh xuất hiện dưới dạng nút trên thanh công cụ Chỉ số máy để thực thi lệnh một lần nhấp.",
|
||||
"noQuickActions": "Chưa có hành động nhanh nào.",
|
||||
"buttonLabel": "Nhãn nút",
|
||||
"selectSnippet": "Chọn đoạn mã...",
|
||||
"addQuickAction": "Thêm hành động"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Đang thực thi {{name}}...",
|
||||
"succeeded": "{{name}} đã hoàn tất thành công",
|
||||
"failed": "{{name}} thất bại",
|
||||
"error": "Thực thi {{name}} thất bại"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "运行“{{name}}”",
|
||||
"variablesDialogDescription": "运行此代码片段之前,请填写所需的值。",
|
||||
"variablesPreviewLabel": "预览",
|
||||
"variablesConfirmButton": "运行",
|
||||
"keybindings": {
|
||||
"runSnippet": "运行现有代码片段",
|
||||
"snippetLabel": "片段",
|
||||
"selectSnippet": "选择代码片段",
|
||||
"snippetMissing": "未找到代码片段",
|
||||
"snippetRequired": "选择代码片段"
|
||||
},
|
||||
"paletteHintRun": "在活动终端标签页中运行",
|
||||
"host": {
|
||||
"startupTitle": "启动代码片段",
|
||||
"quickActionsTitle": "快速操作",
|
||||
"quickActionsDescription": "快速操作以按钮形式显示在主机指标工具栏中,以便一键执行命令。",
|
||||
"noQuickActions": "尚无快速操作。",
|
||||
"buttonLabel": "按钮标签",
|
||||
"selectSnippet": "选择代码片段...",
|
||||
"addQuickAction": "添加操作"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "正在执行 {{name}}...",
|
||||
"succeeded": "{{name}} 执行成功",
|
||||
"failed": "{{name}} 执行失败",
|
||||
"error": "执行 {{name}} 失败"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "運行“{{name}}",
|
||||
"variablesDialogDescription": "在運行此程式碼片段之前,請填寫所需的值。",
|
||||
"variablesPreviewLabel": "預覽",
|
||||
"variablesConfirmButton": "跑步",
|
||||
"keybindings": {
|
||||
"runSnippet": "運行現有程式碼片段",
|
||||
"snippetLabel": "片段",
|
||||
"selectSnippet": "選擇一段文字",
|
||||
"snippetMissing": "未找到程式碼片段",
|
||||
"snippetRequired": "選擇一段文字"
|
||||
},
|
||||
"paletteHintRun": "在活動終端選項卡中運行",
|
||||
"host": {
|
||||
"startupTitle": "啟動程式碼片段",
|
||||
"quickActionsTitle": "快速動作",
|
||||
"quickActionsDescription": "快速動作會以按鈕形式顯示在主機計量工具列上,可一鍵執行指令。",
|
||||
"noQuickActions": "尚無快速動作。",
|
||||
"buttonLabel": "按鈕標籤",
|
||||
"selectSnippet": "選取程式碼片段...",
|
||||
"addQuickAction": "新增動作"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "正在執行 {{name}}...",
|
||||
"succeeded": "{{name}} 已成功完成",
|
||||
"failed": "{{name}} 失敗",
|
||||
"error": "無法執行 {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,51 @@
|
||||
"descriptionKey": "settings.confirmExecution.description",
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"host": {
|
||||
"editorGroup": "ssh",
|
||||
"editorOrder": 15,
|
||||
"fields": [
|
||||
{
|
||||
"key": "startupSnippetId",
|
||||
"type": "number",
|
||||
"labelKey": "host.startupTitle",
|
||||
"descriptionKey": "host.startupDescription",
|
||||
"hidden": true,
|
||||
"min": 1,
|
||||
"shareRead": "view"
|
||||
},
|
||||
{
|
||||
"key": "quickActions",
|
||||
"type": "json",
|
||||
"labelKey": "host.quickActionsTitle",
|
||||
"descriptionKey": "host.quickActionsDescription",
|
||||
"hidden": true,
|
||||
"default": [],
|
||||
"shareRead": "view"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"syncEntities": ["snippetFolders", "snippets"]
|
||||
"syncEntities": ["snippetFolders", "snippets"],
|
||||
"keybindingActions": [
|
||||
{
|
||||
"id": "runSnippet",
|
||||
"params": {
|
||||
"snippetId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$",
|
||||
"maxLength": 20,
|
||||
"syncEntity": "snippets"
|
||||
},
|
||||
"appendEnter": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"http": {
|
||||
"adminImpersonation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,58 +4,8 @@ export interface SnippetExecutionResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SnippetHostVars {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number | string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
|
||||
function replaceVar(content: string, name: string, value?: string): string {
|
||||
if (value === undefined) return content;
|
||||
const pattern = new RegExp(`\\$\\{?${name}\\}?`, "g");
|
||||
return content.replace(pattern, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors src/ui/lib/snippet-variables.ts resolveSnippetContent. Frontend and
|
||||
* backend are separate builds, so this is kept as a small standalone copy
|
||||
* rather than a shared package for one pure function.
|
||||
*/
|
||||
export function resolveSnippetCommand(
|
||||
content: string,
|
||||
host: SnippetHostVars | null,
|
||||
inputValues: Record<string, string> = {},
|
||||
): string {
|
||||
let resolved = content;
|
||||
|
||||
resolved = replaceVar(resolved, "HOST", host?.ip);
|
||||
resolved = replaceVar(resolved, "USER", host?.username);
|
||||
resolved = replaceVar(
|
||||
resolved,
|
||||
"PORT",
|
||||
host?.port !== undefined ? String(host.port) : undefined,
|
||||
);
|
||||
resolved = replaceVar(resolved, "NAME", host?.name);
|
||||
|
||||
resolved = resolved.replace(
|
||||
INPUT_PATTERN,
|
||||
(
|
||||
fullMatch,
|
||||
braceDigits: string | undefined,
|
||||
_label,
|
||||
plainDigits: string | undefined,
|
||||
) => {
|
||||
const key = `INPUT_${braceDigits ?? plainDigits}`;
|
||||
return key in inputValues ? inputValues[key] : fullMatch;
|
||||
},
|
||||
);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
export { resolveSnippetContent as resolveSnippetCommand } from "../shared/variables.js";
|
||||
export type { SnippetHostContext as SnippetHostVars } from "../shared/variables.js";
|
||||
|
||||
export function getSnippetExecutionTimeoutMs(
|
||||
value = process.env.SNIPPET_EXECUTION_TIMEOUT_SECONDS,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
PLUGIN_ID,
|
||||
readQuickActions,
|
||||
readStartupSnippetId,
|
||||
} from "../shared/host-settings.js";
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostImportNormalizer": a 2.8 or early 2.9 export
|
||||
* carried quick actions as a host field and the startup snippet inside
|
||||
* terminalConfig. An export that already carries this plugin's
|
||||
* pluginSettings has had them written by core.
|
||||
*/
|
||||
export function hostImportNormalizer(
|
||||
raw: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
const carried = (raw.pluginSettings as Record<string, unknown> | undefined)?.[
|
||||
PLUGIN_ID
|
||||
];
|
||||
if (
|
||||
carried &&
|
||||
typeof carried === "object" &&
|
||||
Object.keys(carried).length > 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values: Record<string, unknown> = {};
|
||||
const quickActions = readQuickActions(raw.quickActions);
|
||||
if (quickActions.length > 0) values.quickActions = quickActions;
|
||||
|
||||
let terminalConfig = raw.terminalConfig;
|
||||
if (typeof terminalConfig === "string") {
|
||||
try {
|
||||
terminalConfig = JSON.parse(terminalConfig);
|
||||
} catch {
|
||||
terminalConfig = null;
|
||||
}
|
||||
}
|
||||
const startup = readStartupSnippetId(
|
||||
(terminalConfig as { startupSnippetId?: unknown } | null)?.startupSnippetId,
|
||||
);
|
||||
if (startup !== null) values.startupSnippetId = startup;
|
||||
|
||||
return Object.keys(values).length > 0 ? values : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostPayloadLegacy": quickActions and
|
||||
* terminalConfig.startupSnippetId in their 2.8 shape, for clients that
|
||||
* still read them (Termix-Mobile). Remove once the mobile app reads
|
||||
* pluginSettings.snippets.
|
||||
*/
|
||||
export function hostPayloadLegacy(
|
||||
values: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {
|
||||
quickActions: readQuickActions(values.quickActions),
|
||||
};
|
||||
const startup = readStartupSnippetId(values.startupSnippetId);
|
||||
if (startup !== null) out.terminalConfig = { startupSnippetId: startup };
|
||||
return out;
|
||||
}
|
||||
|
||||
/** What hostSettingsSync needs from the repository. */
|
||||
export interface SnippetSyncLookup {
|
||||
findSyncIdById: (id: number) => Promise<string | null>;
|
||||
findIdBySyncId: (syncId: string) => Promise<number | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostSettingsSync": both host settings hold local
|
||||
* snippet ids, so remote sync carries the snippets' sync ids instead, which
|
||||
* are the same on both sides.
|
||||
*/
|
||||
export function createHostSettingsSync(lookup: SnippetSyncLookup) {
|
||||
const toSyncId = async (id: number) => lookup.findSyncIdById(id);
|
||||
const toId = async (value: unknown) =>
|
||||
typeof value === "string" && value ? lookup.findIdBySyncId(value) : null;
|
||||
|
||||
return {
|
||||
exportValue: async (key: string, value: unknown) => {
|
||||
if (key === "startupSnippetId") {
|
||||
const id = readStartupSnippetId(value);
|
||||
return id === null ? null : await toSyncId(id);
|
||||
}
|
||||
if (key === "quickActions") {
|
||||
const actions = readQuickActions(value);
|
||||
const out = [];
|
||||
for (const action of actions) {
|
||||
const syncId = await toSyncId(action.snippetId);
|
||||
if (syncId) out.push({ name: action.name, snippetId: syncId });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
importValue: async (key: string, value: unknown) => {
|
||||
if (key === "startupSnippetId") return await toId(value);
|
||||
if (key === "quickActions") {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const out = [];
|
||||
for (const entry of value) {
|
||||
const row = (entry ?? {}) as { name?: unknown; snippetId?: unknown };
|
||||
const id = await toId(row.snippetId);
|
||||
if (id !== null) {
|
||||
out.push({
|
||||
name: typeof row.name === "string" ? row.name : "",
|
||||
snippetId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,15 @@ import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { snippetAccess, snippetFolders, snippets } from "./tables.js";
|
||||
import { createSnippetRepository } from "./repository.js";
|
||||
import { registerSnippetRoutes } from "./routes.js";
|
||||
import { resolveSnippetCommand } from "./execution.js";
|
||||
import {
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
} from "../shared/variables.js";
|
||||
import {
|
||||
createHostSettingsSync,
|
||||
hostImportNormalizer,
|
||||
hostPayloadLegacy,
|
||||
} from "./host-import.js";
|
||||
|
||||
/** What other plugins get from ctx.services.get("snippets.access", {userId}). */
|
||||
export interface SnippetsService {
|
||||
@@ -74,8 +82,24 @@ export async function activate(ctx: PluginContext) {
|
||||
type: "snippets",
|
||||
table: snippetsTable,
|
||||
order: 60,
|
||||
// Host quick actions and keybindings in core point at snippets by this.
|
||||
answersTo: ["commandSnippet"],
|
||||
});
|
||||
|
||||
// Host settings that name a snippet by its local id, and the 2.8 host
|
||||
// fields they came from. Keybindings name one through the runSnippet
|
||||
// declaration in the manifest, which core translates over sync.
|
||||
ctx.registry.provide("snippets.hostImportNormalizer", hostImportNormalizer);
|
||||
ctx.registry.provide("snippets.hostPayloadLegacy", hostPayloadLegacy);
|
||||
ctx.registry.provide(
|
||||
"snippets.hostSettingsSync",
|
||||
createHostSettingsSync(repo),
|
||||
);
|
||||
|
||||
// $HOST and $INPUT_n handling for commands other plugins run (fleets). A
|
||||
// pure function, so it goes through the registry rather than a service
|
||||
// gated on snippets.view.
|
||||
ctx.registry.provide("snippets.variables", {
|
||||
resolve: resolveSnippetContent,
|
||||
extractInputs: extractSnippetInputs,
|
||||
});
|
||||
|
||||
// The user row survives a password-reset data wipe, so it never triggers
|
||||
@@ -133,7 +157,7 @@ export async function activate(ctx: PluginContext) {
|
||||
if (!userId) return null;
|
||||
const snippet = await findAccessible(userId, id);
|
||||
if (!snippet) return null;
|
||||
return resolveSnippetCommand(snippet.content, vars, inputValues);
|
||||
return resolveSnippetContent(snippet.content, vars, inputValues);
|
||||
},
|
||||
create: async (input) => {
|
||||
const userId = ctx.currentActor();
|
||||
|
||||
@@ -882,6 +882,27 @@ export function createSnippetRepository(
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
|
||||
/** A snippet's sync id, for a host setting that names it over sync. */
|
||||
async findSyncIdById(snippetId: number): Promise<string | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select({ syncId: snippets.syncId })
|
||||
.from(snippets)
|
||||
.where(eq(snippets.id, snippetId))
|
||||
.limit(1);
|
||||
return (rows[0]?.syncId as string | null | undefined) ?? null;
|
||||
},
|
||||
|
||||
async findIdBySyncId(syncId: string): Promise<number | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select({ id: snippets.id })
|
||||
.from(snippets)
|
||||
.where(eq(snippets.syncId, syncId))
|
||||
.limit(1);
|
||||
return (rows[0]?.id as number | undefined) ?? null;
|
||||
},
|
||||
|
||||
/** A user's role ids, for the sharing filters above. */
|
||||
async listUserRoleIds(userId: string): Promise<number[]> {
|
||||
const drizzle = await client();
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Plus, Play, Trash2, Zap } from "lucide-react";
|
||||
import {
|
||||
useTranslation,
|
||||
type HostEditorSectionProps,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Button, Input, SectionCard, Select2 } from "@termix/plugin-sdk/ui";
|
||||
import { PLUGIN_ID, readStartupSnippetId } from "../shared/host-settings.js";
|
||||
import { useSnippetOptions } from "./use-snippet-options";
|
||||
|
||||
type PluginSettingsBag = Record<string, Record<string, unknown>>;
|
||||
|
||||
/** A quick action row while it is being edited: the snippet may be unset. */
|
||||
interface QuickActionRow {
|
||||
name: string;
|
||||
snippetId: number | null;
|
||||
}
|
||||
|
||||
function readRows(value: unknown): QuickActionRow[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((entry) => {
|
||||
const row = (entry ?? {}) as { name?: unknown; snippetId?: unknown };
|
||||
const id = Number(row.snippetId);
|
||||
return {
|
||||
name: typeof row.name === "string" ? row.name : "",
|
||||
snippetId: Number.isInteger(id) && id > 0 ? id : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The host editor's Snippets tab: the snippet a terminal runs when it
|
||||
* connects, and the quick action buttons Host Metrics shows for the host.
|
||||
* Both are this plugin's host settings, saved with the host.
|
||||
*/
|
||||
export function HostSnippetsSection({
|
||||
form,
|
||||
updateForm,
|
||||
adminTargetUserId,
|
||||
}: HostEditorSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options } = useSnippetOptions(adminTargetUserId);
|
||||
const own =
|
||||
(form.pluginSettings as PluginSettingsBag | undefined)?.[PLUGIN_ID] ?? {};
|
||||
const startupSnippetId = readStartupSnippetId(own.startupSnippetId);
|
||||
const quickActions = readRows(own.quickActions);
|
||||
|
||||
const write = (values: Record<string, unknown>) =>
|
||||
updateForm((current) => {
|
||||
const all =
|
||||
(current.pluginSettings as PluginSettingsBag | undefined) ?? {};
|
||||
return {
|
||||
...current,
|
||||
pluginSettings: {
|
||||
...all,
|
||||
[PLUGIN_ID]: { ...(all[PLUGIN_ID] ?? {}), ...values },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const setQuickActions = (rows: QuickActionRow[]) =>
|
||||
write({ quickActions: rows });
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionCard
|
||||
title={t("host.startupTitle")}
|
||||
icon={<Play className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("host.startupDescription")}
|
||||
</p>
|
||||
<Select2
|
||||
aria-label={t("host.startupTitle")}
|
||||
value={startupSnippetId ?? ""}
|
||||
onChange={(e) =>
|
||||
write({
|
||||
startupSnippetId: e.target.value
|
||||
? Number(e.target.value)
|
||||
: null,
|
||||
})
|
||||
}
|
||||
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("host.none")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={snippet.id}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard
|
||||
title={t("host.quickActionsTitle")}
|
||||
icon={<Zap className="size-3.5" />}
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand"
|
||||
onClick={() =>
|
||||
setQuickActions([...quickActions, { name: "", snippetId: null }])
|
||||
}
|
||||
>
|
||||
<Plus className="size-3 mr-1" /> {t("host.addQuickAction")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("host.quickActionsDescription")}
|
||||
</p>
|
||||
{quickActions.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-4 text-muted-foreground/40 gap-1.5">
|
||||
<Zap className="size-6" />
|
||||
<span className="text-xs">{t("host.noQuickActions")}</span>
|
||||
</div>
|
||||
)}
|
||||
{quickActions.map((row, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 p-2 bg-muted/20 border border-border group"
|
||||
>
|
||||
<Input
|
||||
className="h-7 text-xs flex-1"
|
||||
placeholder={t("host.buttonLabel")}
|
||||
value={row.name}
|
||||
onChange={(e) => {
|
||||
const next = [...quickActions];
|
||||
next[index] = { ...row, name: e.target.value };
|
||||
setQuickActions(next);
|
||||
}}
|
||||
/>
|
||||
<Select2
|
||||
aria-label={t("host.selectSnippet")}
|
||||
className="h-7 text-xs flex-1 border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
value={row.snippetId ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...quickActions];
|
||||
next[index] = {
|
||||
...row,
|
||||
snippetId: e.target.value ? Number(e.target.value) : null,
|
||||
};
|
||||
setQuickActions(next);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("host.selectSnippet")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={snippet.id}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("host.removeQuickAction")}
|
||||
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() =>
|
||||
setQuickActions(quickActions.filter((_, i) => i !== index))
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
usePermission,
|
||||
usePluginApi,
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Button, Separator } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
readQuickActions,
|
||||
snippetHostSettings,
|
||||
type QuickAction,
|
||||
} from "../shared/host-settings.js";
|
||||
import { hasSnippetInputs } from "../shared/variables.js";
|
||||
import { askForInputs, shouldConfirmExecution } from "./prompt-store";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import { errorMessage } from "./types";
|
||||
|
||||
interface ToolbarHost {
|
||||
id?: number | string;
|
||||
name?: string;
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number;
|
||||
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The host's quick actions as buttons in the Host Metrics toolbar (the
|
||||
* host-metrics.toolbar slot). Each runs its snippet on the host over a fresh
|
||||
* SSH connection and reports the result in a toast.
|
||||
*/
|
||||
export function QuickActionButtons(props: Record<string, unknown>) {
|
||||
const host = props.host as ToolbarHost | undefined;
|
||||
const { t } = useTranslation();
|
||||
const api = usePluginApi();
|
||||
const client = useMemo(() => createSnippetsApi(api), [api]);
|
||||
const canView = usePermission("view");
|
||||
const [running, setRunning] = useState<Set<number>>(new Set());
|
||||
const actions = readQuickActions(snippetHostSettings(host).quickActions);
|
||||
const hostId = Number(host?.id);
|
||||
|
||||
if (!canView || actions.length === 0 || !Number.isInteger(hostId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const setBusy = (snippetId: number, busy: boolean) =>
|
||||
setRunning((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (busy) next.add(snippetId);
|
||||
else next.delete(snippetId);
|
||||
return next;
|
||||
});
|
||||
|
||||
async function execute(
|
||||
action: QuickAction,
|
||||
inputValues: Record<string, string>,
|
||||
) {
|
||||
const toastId = `quick-action-${hostId}-${action.snippetId}`;
|
||||
setBusy(action.snippetId, true);
|
||||
toast.loading(t("quickActions.running", { name: action.name }), {
|
||||
id: toastId,
|
||||
});
|
||||
try {
|
||||
const result = await client.execute(
|
||||
action.snippetId,
|
||||
hostId,
|
||||
Object.keys(inputValues).length > 0 ? inputValues : undefined,
|
||||
);
|
||||
if (result.success) {
|
||||
toast.success(t("quickActions.succeeded", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: result.output?.substring(0, 200),
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("quickActions.failed", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: result.error || result.output,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("quickActions.error", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: errorMessage(error, t("executionFailed")),
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
setBusy(action.snippetId, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(action: QuickAction) {
|
||||
let inputValues: Record<string, string> = {};
|
||||
const snippet = await client.get(action.snippetId).catch(() => null);
|
||||
if (snippet && hasSnippetInputs(snippet.content)) {
|
||||
const values = await askForInputs(
|
||||
{ name: action.name || snippet.name, content: snippet.content },
|
||||
host
|
||||
? {
|
||||
ip: host.ip,
|
||||
username: host.username,
|
||||
port: host.port,
|
||||
name: host.name,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
if (!values) return;
|
||||
inputValues = values;
|
||||
}
|
||||
const go = () => void execute(action, inputValues);
|
||||
if (!shouldConfirmExecution()) {
|
||||
go();
|
||||
return;
|
||||
}
|
||||
toast(t("confirmRunMessage", { name: action.name }), {
|
||||
action: { label: t("confirmRunButton"), onClick: go },
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mr-3 flex flex-wrap gap-2">
|
||||
{actions.map((action, index) => {
|
||||
const busy = running.has(action.snippetId);
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs font-semibold"
|
||||
disabled={busy}
|
||||
onClick={() => void run(action)}
|
||||
>
|
||||
{busy && <RefreshCw className="mr-1 size-3 animate-spin" />}
|
||||
{action.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-3 h-8" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSettings } from "@termix/plugin-sdk/frontend";
|
||||
import { SnippetVariablesDialog } from "./SnippetVariablesDialog";
|
||||
import { readSnippetSettings } from "./settings";
|
||||
import { setConfirmExecution, usePendingPrompt } from "./prompt-store";
|
||||
|
||||
/**
|
||||
* Always mounted at the shell's root (the shell.overlay slot), so a snippet
|
||||
* started from the command palette, a keybinding or a host button can ask
|
||||
* for its inputs after whatever started it has closed.
|
||||
*/
|
||||
export function SnippetOverlay() {
|
||||
const settings = useSettings("user");
|
||||
const prompt = usePendingPrompt();
|
||||
const confirmExecution = readSnippetSettings(
|
||||
settings.values,
|
||||
).confirmExecution;
|
||||
|
||||
useEffect(() => {
|
||||
setConfirmExecution(confirmExecution);
|
||||
}, [confirmExecution]);
|
||||
|
||||
if (!prompt) return null;
|
||||
return (
|
||||
<SnippetVariablesDialog
|
||||
snippet={prompt.snippet}
|
||||
host={prompt.host}
|
||||
onCancel={() => prompt.settle(null)}
|
||||
onConfirm={(_resolved, values) => prompt.settle(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
|
||||
/** Enough of a snippet to fill in its inputs. */
|
||||
export interface VariablesDialogSnippet {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown before running a snippet that contains $INPUT_n placeholders:
|
||||
* collects a value per placeholder and previews the fully resolved command
|
||||
* (host vars and inputs) before handing the result back to the caller.
|
||||
*/
|
||||
export function SnippetVariablesDialog({
|
||||
snippet,
|
||||
host,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
snippet: VariablesDialogSnippet;
|
||||
host: SnippetHostContext | null;
|
||||
onCancel: () => void;
|
||||
onConfirm: (
|
||||
resolvedContent: string,
|
||||
inputValues: Record<string, string>,
|
||||
) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const inputs = useMemo(
|
||||
() => extractSnippetInputs(snippet.content),
|
||||
[snippet.content],
|
||||
);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setValues({});
|
||||
}, [snippet]);
|
||||
|
||||
const preview = resolveSnippetContent(snippet.content, host, values);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(v) => !v && onCancel()}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-bold">
|
||||
{t("variablesDialogTitle", { name: snippet.name })}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs text-muted-foreground">
|
||||
{t("variablesDialogDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 mt-1">
|
||||
{inputs.map((input) => (
|
||||
<div key={input.key} className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold">{input.label}</label>
|
||||
<Input
|
||||
autoFocus={inputs[0]?.key === input.key}
|
||||
value={values[input.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[input.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted-foreground">
|
||||
{t("variablesPreviewLabel")}
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground font-mono px-2.5 py-2 border border-border bg-muted/20 min-w-0 break-all whitespace-pre-wrap">
|
||||
{preview}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 mt-2">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={() => onConfirm(preview, values)}
|
||||
>
|
||||
{t("variablesConfirmButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,53 @@
|
||||
import { Play } from "lucide-react";
|
||||
import type { PanelProps, TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import { Clipboard, Play } from "lucide-react";
|
||||
import type {
|
||||
PaletteItem,
|
||||
PanelProps,
|
||||
PluginHostRecord,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import type { Snippet } from "./types";
|
||||
import { SnippetsPanel } from "./SnippetsPanel";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import { resolveSnippetContent, hasSnippetInputs } from "./snippet-variables";
|
||||
import {
|
||||
AdminUserSnippets,
|
||||
adminOptions,
|
||||
mapSnippets,
|
||||
} from "./AdminUserSnippets";
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
import {
|
||||
readStartupSnippetId,
|
||||
snippetHostSettings,
|
||||
} from "../shared/host-settings.js";
|
||||
import { AdminUserSnippets } from "./AdminUserSnippets";
|
||||
import { SnippetOverlay } from "./SnippetOverlay";
|
||||
import { HostSnippetsSection } from "./HostSnippetsSection";
|
||||
import { QuickActionButtons } from "./QuickActionButtons";
|
||||
import { resetPrompts } from "./prompt-store";
|
||||
import { fetchSnippet, runInSession } from "./run-flows";
|
||||
import {
|
||||
RUN_SNIPPET_ACTION,
|
||||
RunSnippetEditor,
|
||||
RunSnippetSummary,
|
||||
runSnippetBinding,
|
||||
validateRunSnippet,
|
||||
} from "./keybinding";
|
||||
|
||||
function hostContext(host: unknown): SnippetHostContext | null {
|
||||
if (!host || typeof host !== "object") return null;
|
||||
const record = host as Record<string, unknown>;
|
||||
return {
|
||||
ip: typeof record.ip === "string" ? record.ip : undefined,
|
||||
username: typeof record.username === "string" ? record.username : undefined,
|
||||
port:
|
||||
typeof record.port === "number" || typeof record.port === "string"
|
||||
? record.port
|
||||
: undefined,
|
||||
name: typeof record.name === "string" ? record.name : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
app.onDispose(resetPrompts);
|
||||
|
||||
app.registerRailItem({
|
||||
id: "snippets",
|
||||
icon: Play,
|
||||
@@ -33,6 +71,73 @@ export function activate(app: TermixApp): void {
|
||||
},
|
||||
});
|
||||
|
||||
// Every snippet in the command palette, run in the terminal the user is
|
||||
// working in.
|
||||
app.registerPaletteGroup({
|
||||
id: "snippets",
|
||||
titleKey: "nav.snippets",
|
||||
order: 10,
|
||||
load: async (): Promise<PaletteItem[]> => {
|
||||
if (!(await app.hasPermission("view"))) return [];
|
||||
const snippets: Snippet[] = await createSnippetsApi(app.api)
|
||||
.list()
|
||||
.catch(() => []);
|
||||
return snippets.map((snippet) => ({
|
||||
id: String(snippet.id),
|
||||
title: snippet.name,
|
||||
description: snippet.content,
|
||||
icon: snippet.isNote ? Clipboard : Play,
|
||||
keywords: [snippet.description ?? ""],
|
||||
needsTarget: true,
|
||||
hint: app.t(snippet.isNote ? "paletteHintPaste" : "paletteHintRun"),
|
||||
run: ({ targetTab }) => {
|
||||
if (!targetTab) return;
|
||||
void runInSession(
|
||||
app,
|
||||
snippet,
|
||||
{ sessionId: targetTab.id },
|
||||
hostContext(targetTab.host),
|
||||
{ announce: true },
|
||||
);
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// Keeps the input dialog and the confirm setting alive at the shell root.
|
||||
app.registerSlotContribution("shell.overlay", {
|
||||
actionId: "snippets.overlay",
|
||||
titleKey: "nav.snippets",
|
||||
kind: "component",
|
||||
component: SnippetOverlay as never,
|
||||
});
|
||||
|
||||
app.registerKeybindingAction({
|
||||
id: RUN_SNIPPET_ACTION,
|
||||
titleKey: "keybindings.runSnippet",
|
||||
editor: RunSnippetEditor,
|
||||
summary: RunSnippetSummary,
|
||||
validate: validateRunSnippet,
|
||||
run: (action, context) => runSnippetBinding(app, action, context),
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "snippets",
|
||||
group: "ssh",
|
||||
titleKey: "host.tabTitle",
|
||||
icon: Play,
|
||||
order: 15,
|
||||
component: HostSnippetsSection,
|
||||
});
|
||||
|
||||
// Host Metrics shows a host's quick actions as toolbar buttons.
|
||||
app.registerSlotContribution("host-metrics.toolbar", {
|
||||
actionId: "snippets.quickActions",
|
||||
titleKey: "host.quickActionsTitle",
|
||||
kind: "component",
|
||||
component: QuickActionButtons,
|
||||
});
|
||||
|
||||
app.registerSlotContribution("onboarding.features", {
|
||||
actionId: "snippets.feature",
|
||||
titleKey: "onboarding.feature_snippets",
|
||||
@@ -49,71 +154,17 @@ export function activate(app: TermixApp): void {
|
||||
order: 10,
|
||||
});
|
||||
|
||||
// Core's snippet pickers (the host editor's startup snippet, keybindings,
|
||||
// the command palette) read the list through this. An admin editing
|
||||
// another user's host passes that user's id.
|
||||
app.registerAction("snippet.list", (async (options?: {
|
||||
targetUserId?: string;
|
||||
}) => {
|
||||
const response = options?.targetUserId
|
||||
? await app.api.get("/", adminOptions(options.targetUserId))
|
||||
: await app.api.get("/");
|
||||
const raw = response.data as unknown;
|
||||
const list = Array.isArray(raw)
|
||||
? raw
|
||||
: ((raw as { snippets?: unknown[] })?.snippets ?? []);
|
||||
return (list as Record<string, unknown>[]).map((row) => ({
|
||||
...mapSnippets([row])[0],
|
||||
isNote: row.isNote === true,
|
||||
}));
|
||||
}) as never);
|
||||
|
||||
// Resolves a snippet by id for a terminal (startup snippet, custom
|
||||
// keybindings, the command palette), so core never imports snippet code.
|
||||
const resolveForTerminal = async (
|
||||
snippetId: number,
|
||||
host: {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number;
|
||||
name?: string;
|
||||
} | null,
|
||||
inputValues?: Record<string, string>,
|
||||
// The command a terminal types when it connects to a host: the host's
|
||||
// startup snippet with its variables filled in, or null.
|
||||
app.registerAction("snippets.startupCommand", (async (
|
||||
host: PluginHostRecord | null,
|
||||
vars?: SnippetHostContext,
|
||||
) => {
|
||||
const api = createSnippetsApi(app.api);
|
||||
const snippet = await api.get(snippetId).catch(() => null);
|
||||
const id = readStartupSnippetId(snippetHostSettings(host).startupSnippetId);
|
||||
if (id === null) return null;
|
||||
const snippet = await fetchSnippet(app, id);
|
||||
if (!snippet) return null;
|
||||
if (
|
||||
hasSnippetInputs(snippet.content) &&
|
||||
(!inputValues || Object.keys(inputValues).length === 0)
|
||||
) {
|
||||
return {
|
||||
needsInputs: true,
|
||||
content: snippet.content,
|
||||
isNote: snippet.isNote,
|
||||
};
|
||||
}
|
||||
return {
|
||||
needsInputs: false,
|
||||
content: resolveSnippetContent(snippet.content, host, inputValues ?? {}),
|
||||
isNote: snippet.isNote,
|
||||
};
|
||||
};
|
||||
app.registerAction("snippet.resolveForTerminal", resolveForTerminal as never);
|
||||
app.registerAction(
|
||||
"snippets.resolveForTerminal",
|
||||
resolveForTerminal as never,
|
||||
);
|
||||
|
||||
// For other plugins' quick actions (host metrics): the snippet's content,
|
||||
// so the caller can ask for $INPUT_n values, and a run on a host.
|
||||
app.registerAction("snippets.get", (async (snippetId: number) => {
|
||||
const snippet = await createSnippetsApi(app.api)
|
||||
.get(snippetId)
|
||||
.catch(() => null);
|
||||
return snippet
|
||||
? { id: snippet.id, name: snippet.name, content: snippet.content }
|
||||
: null;
|
||||
return resolveSnippetContent(snippet.content, vars ?? hostContext(host));
|
||||
}) as never);
|
||||
|
||||
// For pickers in other plugins (the AI assistant's @-mentions): id and name.
|
||||
@@ -124,14 +175,7 @@ export function activate(app: TermixApp): void {
|
||||
return snippets.map((snippet) => ({ id: snippet.id, name: snippet.name }));
|
||||
}) as never);
|
||||
|
||||
app.registerAction("snippets.execute", (async (
|
||||
snippetId: number,
|
||||
hostId: number,
|
||||
inputValues?: Record<string, string>,
|
||||
) =>
|
||||
createSnippetsApi(app.api).execute(
|
||||
snippetId,
|
||||
hostId,
|
||||
inputValues,
|
||||
)) as never);
|
||||
// Variable handling for commands other plugins run (fleets' run box).
|
||||
app.registerAction("snippets.extractInputs", ((content: string) =>
|
||||
extractSnippetInputs(typeof content === "string" ? content : "")) as never);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
useTranslation,
|
||||
type KeybindingAction,
|
||||
type KeybindingActionEditorProps,
|
||||
type KeybindingRunContext,
|
||||
type TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { fetchSnippet, runInSession } from "./run-flows";
|
||||
import { useSnippetOptions } from "./use-snippet-options";
|
||||
|
||||
/** Stored as action.type; kept from 2.8 so saved bindings keep working. */
|
||||
export const RUN_SNIPPET_ACTION = "runSnippet";
|
||||
|
||||
function snippetIdOf(action: KeybindingAction): string {
|
||||
return typeof action.snippetId === "string" ? action.snippetId : "";
|
||||
}
|
||||
|
||||
/** Picks the snippet a "Run snippet" binding runs. */
|
||||
export function RunSnippetEditor({
|
||||
action,
|
||||
onChange,
|
||||
}: KeybindingActionEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options } = useSnippetOptions();
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold">
|
||||
{t("keybindings.snippetLabel")}
|
||||
</label>
|
||||
<select
|
||||
aria-label={t("keybindings.snippetLabel")}
|
||||
value={snippetIdOf(action)}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
type: RUN_SNIPPET_ACTION,
|
||||
snippetId: e.target.value || undefined,
|
||||
appendEnter: true,
|
||||
})
|
||||
}
|
||||
className="h-8 border border-border bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("keybindings.selectSnippet")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={String(snippet.id)}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The bound snippet's name in the binding list, or a warning once it is gone. */
|
||||
export function RunSnippetSummary({ action }: KeybindingActionEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options, loaded } = useSnippetOptions();
|
||||
if (!loaded) return null;
|
||||
const snippet = options.find((s) => String(s.id) === snippetIdOf(action));
|
||||
if (snippet) return <span>({snippet.name})</span>;
|
||||
return (
|
||||
<span className="text-destructive">
|
||||
({t("keybindings.snippetMissing")})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function validateRunSnippet(action: KeybindingAction): string | null {
|
||||
return /^\d+$/.test(snippetIdOf(action))
|
||||
? null
|
||||
: "keybindings.snippetRequired";
|
||||
}
|
||||
|
||||
/** Runs a bound snippet in the terminal the key was pressed in. */
|
||||
export function runSnippetBinding(
|
||||
app: Pick<TermixApp, "api" | "t">,
|
||||
action: KeybindingAction,
|
||||
context: KeybindingRunContext,
|
||||
): void {
|
||||
void fetchSnippet(app, Number(snippetIdOf(action))).then((snippet) => {
|
||||
if (!snippet) return;
|
||||
void runInSession(
|
||||
app,
|
||||
{ ...snippet, isNote: false },
|
||||
{ sessionId: context.sessionId, send: context.send },
|
||||
context.host ?? null,
|
||||
{ pressEnter: action.appendEnter !== false },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import type { SnippetHostContext } from "../shared/variables.js";
|
||||
|
||||
export interface PendingPrompt {
|
||||
snippet: { name: string; content: string };
|
||||
host: SnippetHostContext | null;
|
||||
settle: (values: Record<string, string> | null) => void;
|
||||
}
|
||||
|
||||
let pending: PendingPrompt | null = null;
|
||||
let confirmExecution = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notify(): void {
|
||||
for (const listener of [...listeners]) listener();
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks for a snippet's $INPUT_n values through the dialog the overlay draws.
|
||||
* Resolves null when the user cancels. A second request cancels the first.
|
||||
*/
|
||||
export function askForInputs(
|
||||
snippet: { name: string; content: string },
|
||||
host: SnippetHostContext | null,
|
||||
): Promise<Record<string, string> | null> {
|
||||
pending?.settle(null);
|
||||
return new Promise((resolve) => {
|
||||
const prompt: PendingPrompt = {
|
||||
snippet,
|
||||
host,
|
||||
settle: (values) => {
|
||||
if (pending === prompt) {
|
||||
pending = null;
|
||||
notify();
|
||||
}
|
||||
resolve(values);
|
||||
},
|
||||
};
|
||||
pending = prompt;
|
||||
notify();
|
||||
});
|
||||
}
|
||||
|
||||
export function usePendingPrompt(): PendingPrompt | null {
|
||||
return useSyncExternalStore(
|
||||
subscribe,
|
||||
() => pending,
|
||||
() => pending,
|
||||
);
|
||||
}
|
||||
|
||||
/** Kept current by the overlay from the user's confirmExecution setting. */
|
||||
export function setConfirmExecution(value: boolean): void {
|
||||
confirmExecution = value;
|
||||
}
|
||||
|
||||
export function shouldConfirmExecution(): boolean {
|
||||
return confirmExecution;
|
||||
}
|
||||
|
||||
/** Cancels anything waiting, for deactivate. */
|
||||
export function resetPrompts(): void {
|
||||
pending?.settle(null);
|
||||
pending = null;
|
||||
confirmExecution = false;
|
||||
notify();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { toast } from "sonner";
|
||||
import { invokeAction, type TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
import { askForInputs, shouldConfirmExecution } from "./prompt-store";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
|
||||
export interface RunnableSnippet {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote?: boolean;
|
||||
}
|
||||
|
||||
/** Where a run sends its text: a session by id, or a raw writer. */
|
||||
export interface RunTarget {
|
||||
sessionId?: string;
|
||||
send?: (data: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The snippet with its variables filled in, asking for $INPUT_n values when
|
||||
* it has any. Null when the user cancels.
|
||||
*/
|
||||
export async function resolveForRun(
|
||||
snippet: { name: string; content: string },
|
||||
host: SnippetHostContext | null,
|
||||
): Promise<string | null> {
|
||||
if (!hasSnippetInputs(snippet.content)) {
|
||||
return resolveSnippetContent(snippet.content, host);
|
||||
}
|
||||
const values = await askForInputs(snippet, host);
|
||||
if (!values) return null;
|
||||
return resolveSnippetContent(snippet.content, host, values);
|
||||
}
|
||||
|
||||
/** Runs `execute` now, or after the user confirms when they asked to. */
|
||||
export function confirmThenRun(
|
||||
app: Pick<TermixApp, "t">,
|
||||
name: string,
|
||||
execute: () => void,
|
||||
): void {
|
||||
if (!shouldConfirmExecution()) {
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
toast(app.t("confirmRunMessage", { name }), {
|
||||
action: { label: app.t("confirmRunButton"), onClick: execute },
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Types a snippet into a terminal session: resolves its variables, asks for
|
||||
* inputs and confirmation as the user's settings say, then sends it. A note
|
||||
* is pasted without running.
|
||||
*/
|
||||
export async function runInSession(
|
||||
app: Pick<TermixApp, "t">,
|
||||
snippet: RunnableSnippet,
|
||||
target: RunTarget,
|
||||
host: SnippetHostContext | null,
|
||||
options: { pressEnter?: boolean; announce?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const content = await resolveForRun(snippet, host);
|
||||
if (content === null) return;
|
||||
const run = !snippet.isNote && options.pressEnter !== false;
|
||||
const send = async () => {
|
||||
let sent = true;
|
||||
if (target.send) {
|
||||
target.send(content + (run ? "\r" : ""));
|
||||
} else if (target.sessionId) {
|
||||
sent =
|
||||
(await invokeAction(
|
||||
"terminal.sendToSession",
|
||||
target.sessionId,
|
||||
content,
|
||||
{
|
||||
run,
|
||||
},
|
||||
)) === true;
|
||||
} else {
|
||||
sent = false;
|
||||
}
|
||||
if (!options.announce) return;
|
||||
if (!sent) {
|
||||
toast.error(app.t("noTerminalTabsOpen"));
|
||||
return;
|
||||
}
|
||||
toast.success(
|
||||
app.t(snippet.isNote ? "pasteSuccess" : "runSuccess", {
|
||||
name: snippet.name,
|
||||
count: 1,
|
||||
}),
|
||||
);
|
||||
};
|
||||
if (snippet.isNote) void send();
|
||||
else confirmThenRun(app, snippet.name, () => void send());
|
||||
}
|
||||
|
||||
/** One snippet by id, or null when it is gone or unreadable. */
|
||||
export async function fetchSnippet(
|
||||
app: Pick<TermixApp, "api">,
|
||||
id: number,
|
||||
): Promise<RunnableSnippet | null> {
|
||||
if (!Number.isInteger(id) || id <= 0) return null;
|
||||
const snippet = await createSnippetsApi(app.api)
|
||||
.get(id)
|
||||
.catch(() => null);
|
||||
return snippet
|
||||
? {
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
content: snippet.content,
|
||||
isNote: snippet.isNote,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePluginApi } from "@termix/plugin-sdk/frontend";
|
||||
import { adminOptions } from "./AdminUserSnippets";
|
||||
|
||||
export interface SnippetOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's snippets as picker options: their own, or the target user's
|
||||
* while an admin edits that user's host. `loaded` turns true once the list
|
||||
* came back, so a picker can tell "no snippets" from "not loaded yet".
|
||||
*/
|
||||
export function useSnippetOptions(targetUserId?: string): {
|
||||
options: SnippetOption[];
|
||||
loaded: boolean;
|
||||
} {
|
||||
const api = usePluginApi();
|
||||
const [state, setState] = useState<{
|
||||
options: SnippetOption[];
|
||||
loaded: boolean;
|
||||
}>({ options: [], loaded: false });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const request = targetUserId
|
||||
? api.get("/", adminOptions(targetUserId))
|
||||
: api.get("/");
|
||||
request
|
||||
.then((response) => {
|
||||
const raw = response.data as unknown;
|
||||
const list = Array.isArray(raw)
|
||||
? raw
|
||||
: ((raw as { snippets?: unknown[] })?.snippets ?? []);
|
||||
const options = (list as Array<{ id?: unknown; name?: unknown }>)
|
||||
.filter((row) => typeof row.id === "number")
|
||||
.map((row) => ({
|
||||
id: row.id as number,
|
||||
name: typeof row.name === "string" ? row.name : `#${row.id}`,
|
||||
}));
|
||||
if (!cancelled) setState({ options, loaded: true });
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setState({ options: [], loaded: true });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [api, targetUserId]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { SnippetVariablesDialog } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "./snippet-variables";
|
||||
} from "../shared/variables.js";
|
||||
import { SnippetVariablesDialog } from "./SnippetVariablesDialog";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import {
|
||||
ExecutionResultsDialog,
|
||||
@@ -259,7 +259,7 @@ export function useSnippetRunner(confirmExecution = false) {
|
||||
|
||||
const variablesDialog = runningSnippet ? (
|
||||
<SnippetVariablesDialog
|
||||
snippet={runningSnippet.snippet as never}
|
||||
snippet={runningSnippet.snippet}
|
||||
host={runningSnippet.host}
|
||||
onCancel={() => setRunningSnippet(null)}
|
||||
onConfirm={runningSnippet.onConfirm}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The snippets plugin's host settings: the startup snippet a terminal runs
|
||||
* when it connects, and the quick action buttons Host Metrics shows.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = "snippets";
|
||||
|
||||
export interface QuickAction {
|
||||
name: string;
|
||||
snippetId: number;
|
||||
}
|
||||
|
||||
function snippetId(value: unknown): number | null {
|
||||
const id = typeof value === "string" ? Number(value) : value;
|
||||
return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
/** Quick actions in their stored shape, dropping any without a snippet. */
|
||||
export function readQuickActions(value: unknown): QuickAction[] {
|
||||
let list = value;
|
||||
if (typeof list === "string") {
|
||||
try {
|
||||
list = JSON.parse(list);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.flatMap((entry): QuickAction[] => {
|
||||
if (!entry || typeof entry !== "object") return [];
|
||||
const id = snippetId((entry as { snippetId?: unknown }).snippetId);
|
||||
if (id === null) return [];
|
||||
const name = (entry as { name?: unknown }).name;
|
||||
return [{ name: typeof name === "string" ? name : "", snippetId: id }];
|
||||
});
|
||||
}
|
||||
|
||||
export function readStartupSnippetId(value: unknown): number | null {
|
||||
return snippetId(value);
|
||||
}
|
||||
|
||||
/** This plugin's slice of a host record's pluginSettings. */
|
||||
export function snippetHostSettings(host: unknown): Record<string, unknown> {
|
||||
const all = (host as { pluginSettings?: Record<string, unknown> } | null)
|
||||
?.pluginSettings;
|
||||
const own = all?.[PLUGIN_ID];
|
||||
return own && typeof own === "object" ? (own as Record<string, unknown>) : {};
|
||||
}
|
||||
+6
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* $HOST, $USER, $PORT, $NAME and $INPUT_n in a command. The one copy: the
|
||||
* frontend, the backend and other plugins (fleets, through the
|
||||
* "snippets.variables" registry entry and action) all use this.
|
||||
*/
|
||||
|
||||
export interface SnippetInput {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -98,6 +98,28 @@ describe("snippets activate", () => {
|
||||
expect(after).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("registers its host settings hooks and the variables helper", async () => {
|
||||
server = await startServer();
|
||||
const registry = server.mock.ctx.registry;
|
||||
for (const key of [
|
||||
"snippets.hostImportNormalizer",
|
||||
"snippets.hostPayloadLegacy",
|
||||
"snippets.hostSettingsSync",
|
||||
]) {
|
||||
expect(registry.consume(key), key).toBeDefined();
|
||||
}
|
||||
const variables = registry.consume<{
|
||||
resolve: (c: string, h: { ip?: string } | null) => string;
|
||||
extractInputs: (c: string) => Array<{ key: string }>;
|
||||
}>("snippets.variables");
|
||||
expect(variables?.resolve("ping $HOST", { ip: "10.0.0.1" })).toBe(
|
||||
"ping 10.0.0.1",
|
||||
);
|
||||
expect(variables?.extractInputs("echo $INPUT_1")).toEqual([
|
||||
{ key: "INPUT_1", label: "Input 1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails closed without db:own", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
const mock = createMockCtx({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createHostSettingsSync,
|
||||
hostImportNormalizer,
|
||||
hostPayloadLegacy,
|
||||
} from "../../src/backend/host-import.js";
|
||||
|
||||
describe("hostImportNormalizer", () => {
|
||||
it("reads a 2.8 export's quick actions and startup snippet", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
quickActions: [
|
||||
{ name: "Restart", snippetId: "3" },
|
||||
{ name: "Broken", snippetId: "" },
|
||||
],
|
||||
terminalConfig: JSON.stringify({ startupSnippetId: 5, fontSize: 12 }),
|
||||
}),
|
||||
).toEqual({
|
||||
quickActions: [{ name: "Restart", snippetId: 3 }],
|
||||
startupSnippetId: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves an export that carries this plugin's settings to core", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
pluginSettings: { snippets: { startupSnippetId: 1 } },
|
||||
terminalConfig: { startupSnippetId: 5 },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("writes nothing for a host without either", () => {
|
||||
expect(hostImportNormalizer({ name: "web" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostPayloadLegacy", () => {
|
||||
it("puts the 2.8 fields back for older clients", () => {
|
||||
expect(
|
||||
hostPayloadLegacy({
|
||||
quickActions: [{ name: "Up", snippetId: 2 }],
|
||||
startupSnippetId: 7,
|
||||
}),
|
||||
).toEqual({
|
||||
quickActions: [{ name: "Up", snippetId: 2 }],
|
||||
terminalConfig: { startupSnippetId: 7 },
|
||||
});
|
||||
expect(hostPayloadLegacy({})).toEqual({ quickActions: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostSettingsSync", () => {
|
||||
const sync = createHostSettingsSync({
|
||||
findSyncIdById: async (id) => (id === 2 ? "sync-2" : null),
|
||||
findIdBySyncId: async (syncId) => (syncId === "sync-2" ? 9 : null),
|
||||
});
|
||||
|
||||
it("sends sync ids and drops what does not map", async () => {
|
||||
expect(await sync.exportValue("startupSnippetId", 2)).toBe("sync-2");
|
||||
expect(await sync.exportValue("startupSnippetId", 4)).toBeNull();
|
||||
expect(
|
||||
await sync.exportValue("quickActions", [
|
||||
{ name: "A", snippetId: 2 },
|
||||
{ name: "B", snippetId: 4 },
|
||||
]),
|
||||
).toEqual([{ name: "A", snippetId: "sync-2" }]);
|
||||
});
|
||||
|
||||
it("maps sync ids back to this side's rows", async () => {
|
||||
expect(await sync.importValue("startupSnippetId", "sync-2")).toBe(9);
|
||||
expect(await sync.importValue("startupSnippetId", "missing")).toBeNull();
|
||||
expect(
|
||||
await sync.importValue("quickActions", [
|
||||
{ name: "A", snippetId: "sync-2" },
|
||||
]),
|
||||
).toEqual([{ name: "A", snippetId: 9 }]);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -3,7 +3,7 @@ import {
|
||||
extractSnippetInputs,
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
} from "../../src/frontend/snippet-variables";
|
||||
} from "../../src/shared/variables.js";
|
||||
|
||||
describe("extractSnippetInputs", () => {
|
||||
it("extracts a single unlabeled input", () => {
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { act, fireEvent, screen } from "@testing-library/react";
|
||||
import { runKeybindingAction } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
renderWithApp,
|
||||
type RenderedPluginApp,
|
||||
@@ -41,11 +42,24 @@ describe(`${manifest.id} activate`, () => {
|
||||
expect(railItem?.permission).toBe("snippets.view");
|
||||
});
|
||||
|
||||
it("registers the snippets.resolveForTerminal action", async () => {
|
||||
it("registers what other surfaces reach it through", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
expect(rendered.registered.actions()).toContain(
|
||||
"snippets.resolveForTerminal",
|
||||
expect(rendered.registered.actions()).toEqual(
|
||||
expect.arrayContaining([
|
||||
"snippets.startupCommand",
|
||||
"snippets.list",
|
||||
"snippets.extractInputs",
|
||||
]),
|
||||
);
|
||||
expect(rendered.registered.paletteGroups()).toEqual(["snippets"]);
|
||||
expect(rendered.registered.keybindingActions()).toEqual(["runSnippet"]);
|
||||
expect(rendered.registered.hostEditorSections()).toEqual(["snippets"]);
|
||||
expect(rendered.registered.slot("host-metrics.toolbar")).toEqual([
|
||||
"snippets.quickActions",
|
||||
]);
|
||||
expect(rendered.registered.slot("shell.overlay")).toEqual([
|
||||
"snippets.overlay",
|
||||
]);
|
||||
});
|
||||
|
||||
it("removes everything it registered on deactivate", async () => {
|
||||
@@ -54,6 +68,143 @@ describe(`${manifest.id} activate`, () => {
|
||||
expect(app.registered.panels()).toEqual([]);
|
||||
expect(app.registered.railItems()).toEqual([]);
|
||||
expect(app.registered.actions()).toEqual([]);
|
||||
expect(app.registered.paletteGroups()).toEqual([]);
|
||||
expect(app.registered.keybindingActions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const deploy = {
|
||||
id: 3,
|
||||
name: "Deploy",
|
||||
content: "deploy $HOST",
|
||||
description: null,
|
||||
isNote: false,
|
||||
};
|
||||
|
||||
describe("snippets in the command palette", () => {
|
||||
it("lists snippets that run in the target tab", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
permissions: ["snippets.view"],
|
||||
});
|
||||
const items = await rendered.loadPaletteGroup("snippets");
|
||||
expect(items).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "3",
|
||||
title: "Deploy",
|
||||
description: "deploy $HOST",
|
||||
needsTarget: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers nothing without the view permission", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
permissions: [],
|
||||
});
|
||||
expect(await rendered.loadPaletteGroup("snippets")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the runSnippet keybinding", () => {
|
||||
it("types the resolved snippet into the session it was pressed in", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: deploy })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
const send = vi.fn();
|
||||
const handled = runKeybindingAction(
|
||||
{ type: "runSnippet", snippetId: "3", appendEnter: true },
|
||||
{ host: { ip: "10.0.0.9" }, send },
|
||||
);
|
||||
expect(handled).toBe(true);
|
||||
await vi.waitFor(() =>
|
||||
expect(send).toHaveBeenCalledWith("deploy 10.0.0.9\r"),
|
||||
);
|
||||
expect(api.get).toHaveBeenCalledWith("/3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the startup command", () => {
|
||||
it("resolves the host's startup snippet, or nothing without one", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: deploy })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
const host = {
|
||||
id: "1",
|
||||
name: "web",
|
||||
ip: "10.0.0.2",
|
||||
port: 22,
|
||||
pluginSettings: { snippets: { startupSnippetId: 3 } },
|
||||
};
|
||||
expect(
|
||||
await rendered.app.invokeAction("snippets.startupCommand", host),
|
||||
).toBe("deploy 10.0.0.2");
|
||||
expect(
|
||||
await rendered.app.invokeAction("snippets.startupCommand", {
|
||||
...host,
|
||||
pluginSettings: {},
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the host editor's Snippets tab", () => {
|
||||
it("writes the startup snippet and quick actions into this plugin's settings", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
let form: Record<string, unknown> = {
|
||||
pluginSettings: { other: { keep: 1 } },
|
||||
};
|
||||
const updateForm = (
|
||||
patch: (current: Record<string, unknown>) => Record<string, unknown>,
|
||||
) => {
|
||||
form = patch(form);
|
||||
};
|
||||
rendered.renderHostEditorSection("snippets", { form, updateForm });
|
||||
const startup = await screen.findByRole("combobox", {
|
||||
name: "Startup Snippet",
|
||||
});
|
||||
await screen.findAllByRole("option", { name: "Deploy" });
|
||||
act(() => {
|
||||
fireEvent.change(startup, { target: { value: "3" } });
|
||||
});
|
||||
expect(form.pluginSettings).toEqual({
|
||||
other: { keep: 1 },
|
||||
snippets: { startupSnippetId: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the Host Metrics quick action buttons", () => {
|
||||
it("draws a button per quick action and nothing without any", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
permissions: ["snippets.view"],
|
||||
});
|
||||
const host = {
|
||||
id: 4,
|
||||
pluginSettings: {
|
||||
snippets: { quickActions: [{ name: "Restart", snippetId: 3 }] },
|
||||
},
|
||||
};
|
||||
rendered.renderSlot("host-metrics.toolbar", { hostId: 4, host });
|
||||
expect(await screen.findByRole("button", { name: "Restart" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,22 +31,23 @@ afterEach(async () => {
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
describe("snippet.list", () => {
|
||||
it("lists the user's own snippets", async () => {
|
||||
describe("snippet pickers while an admin edits another user's host", () => {
|
||||
it("list the user's own snippets", async () => {
|
||||
const api = fakeApi([{ id: 1, name: "a", content: "ls", isNote: true }]);
|
||||
rendered = await renderWithApp(plugin, { manifest, locales, api });
|
||||
const rows = await rendered.app.invokeAction("snippet.list");
|
||||
rendered.renderHostEditorSection("snippets", { form: {} });
|
||||
await screen.findAllByRole("option", { name: "a" });
|
||||
expect(api.get).toHaveBeenCalledWith("/");
|
||||
expect(rows).toEqual([
|
||||
{ id: 1, name: "a", content: "ls", folder: null, isNote: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists another user's snippets for an admin", async () => {
|
||||
it("list the target user's snippets", async () => {
|
||||
const api = fakeApi([]);
|
||||
rendered = await renderWithApp(plugin, { manifest, locales, api });
|
||||
await rendered.app.invokeAction("snippet.list", { targetUserId: "u2" });
|
||||
expect(api.get).toHaveBeenCalledWith("/", TARGET);
|
||||
rendered.renderHostEditorSection("snippets", {
|
||||
form: {},
|
||||
adminTargetUserId: "u2",
|
||||
});
|
||||
await waitFor(() => expect(api.get).toHaveBeenCalledWith("/", TARGET));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user