mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45: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
@@ -16,6 +16,8 @@ import { runTunnelsSettingsMigration } from "./tunnels-settings-migration.js";
|
||||
import { runWebEndpointSettingsMigration } from "./web-endpoint-settings-migration.js";
|
||||
import { runSshTerminalSettingsMigration } from "./ssh-terminal-settings-migration.js";
|
||||
import { runSshTerminalLookMigration } from "./ssh-terminal-look-migration.js";
|
||||
import { runSshTerminalMacrosMigration } from "./ssh-terminal-macros-migration.js";
|
||||
import { runSnippetsSettingsMigration } from "./snippets-settings-migration.js";
|
||||
import { runTmuxMonitorSettingsMigration } from "./tmux-monitor-settings-migration.js";
|
||||
import { runSessionSharingSettingsMigration } from "./session-sharing-settings-migration.js";
|
||||
import { runSessionRecordingSettingsMigration } from "./session-recording-settings-migration.js";
|
||||
@@ -41,6 +43,8 @@ const MIGRATIONS: Array<[string, () => Promise<unknown>]> = [
|
||||
["runWebEndpointSettingsMigration", runWebEndpointSettingsMigration],
|
||||
["runSshTerminalSettingsMigration", runSshTerminalSettingsMigration],
|
||||
["runSshTerminalLookMigration", runSshTerminalLookMigration],
|
||||
["runSshTerminalMacrosMigration", runSshTerminalMacrosMigration],
|
||||
["runSnippetsSettingsMigration", runSnippetsSettingsMigration],
|
||||
["runTmuxMonitorSettingsMigration", runTmuxMonitorSettingsMigration],
|
||||
["runSessionSharingSettingsMigration", runSessionSharingSettingsMigration],
|
||||
[
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Moves what core kept for snippets into the snippets plugin's settings.
|
||||
*
|
||||
* Per host: ssh_data.quick_actions becomes the quickActions host setting,
|
||||
* and terminal_config.startupSnippetId the startupSnippetId one. Per user:
|
||||
* user_preferences.confirm_snippet_execution and folders_collapsed become the
|
||||
* confirmExecution and foldersCollapsed user settings.
|
||||
*
|
||||
* The columns keep their values until 3.0.0 drops them.
|
||||
*
|
||||
* Idempotent: a host that already has either host setting, and a user key
|
||||
* already set, are skipped, so this is safe on every boot.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { databaseLogger } from "../utils/logger.js";
|
||||
import {
|
||||
legacyFlag,
|
||||
selectLegacyRows,
|
||||
} from "../utils/crypto-migration/raw-rows.js";
|
||||
import {
|
||||
createCurrentPluginRepository,
|
||||
createCurrentPluginSettingsRepository,
|
||||
} from "../database/repositories/factory.js";
|
||||
|
||||
const PLUGIN_ID = "snippets";
|
||||
const HOST_KEYS = ["quickActions", "startupSnippetId"];
|
||||
|
||||
function parseJson(value: unknown): unknown {
|
||||
if (typeof value !== "string" || !value) return value ?? undefined;
|
||||
try {
|
||||
let parsed = JSON.parse(value);
|
||||
if (typeof parsed === "string") parsed = JSON.parse(parsed);
|
||||
return parsed;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** The snippets host settings one 2.8 ssh_data row carried. */
|
||||
export function snippetHostSettingsFromRow(row: {
|
||||
quick_actions?: unknown;
|
||||
terminal_config?: unknown;
|
||||
}): Record<string, unknown> {
|
||||
const values: Record<string, unknown> = {};
|
||||
const actions = parseJson(row.quick_actions);
|
||||
if (Array.isArray(actions)) {
|
||||
const quickActions = actions.flatMap((entry) => {
|
||||
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 }];
|
||||
});
|
||||
if (quickActions.length > 0) values.quickActions = quickActions;
|
||||
}
|
||||
const config = parseJson(row.terminal_config) as
|
||||
{ startupSnippetId?: unknown } | undefined;
|
||||
const startup = snippetId(config?.startupSnippetId);
|
||||
if (startup !== null) values.startupSnippetId = startup;
|
||||
return values;
|
||||
}
|
||||
|
||||
export interface SnippetsSettingsMigrationResult {
|
||||
hostsMoved: number;
|
||||
hostsSkipped: number;
|
||||
usersMoved: number;
|
||||
}
|
||||
|
||||
export async function runSnippetsSettingsMigration(): Promise<SnippetsSettingsMigrationResult> {
|
||||
const result: SnippetsSettingsMigrationResult = {
|
||||
hostsMoved: 0,
|
||||
hostsSkipped: 0,
|
||||
usersMoved: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID);
|
||||
if (!plugin) return result;
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
const pluginSettings = createCurrentPluginSettingsRepository();
|
||||
|
||||
try {
|
||||
const rows = await selectLegacyRows<{
|
||||
id: number;
|
||||
quick_actions: unknown;
|
||||
terminal_config: unknown;
|
||||
}>(sql`
|
||||
SELECT id, quick_actions, terminal_config FROM ssh_data
|
||||
WHERE quick_actions IS NOT NULL OR terminal_config IS NOT NULL
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const values = snippetHostSettingsFromRow(row);
|
||||
if (Object.keys(values).length === 0) continue;
|
||||
const scopeId = String(row.id);
|
||||
let done = false;
|
||||
for (const key of HOST_KEYS) {
|
||||
if (await pluginSettings.get(PLUGIN_ID, "host", scopeId, key)) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
result.hostsSkipped++;
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
await pluginSettings.set(
|
||||
PLUGIN_ID,
|
||||
"host",
|
||||
scopeId,
|
||||
key,
|
||||
JSON.stringify(value),
|
||||
);
|
||||
}
|
||||
result.hostsMoved++;
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Snippets host settings migration failed", {
|
||||
operation: "snippets_settings_migration",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await selectLegacyRows<{
|
||||
user_id: string;
|
||||
confirm_snippet_execution: unknown;
|
||||
folders_collapsed: unknown;
|
||||
}>(sql`
|
||||
SELECT user_id, confirm_snippet_execution, folders_collapsed
|
||||
FROM user_preferences
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const values: Record<string, boolean> = {};
|
||||
if (row.confirm_snippet_execution != null) {
|
||||
values.confirmExecution = legacyFlag(
|
||||
row.confirm_snippet_execution,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (row.folders_collapsed != null) {
|
||||
values.foldersCollapsed = legacyFlag(row.folders_collapsed, true);
|
||||
}
|
||||
let moved = false;
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
const existing = await pluginSettings.get(
|
||||
PLUGIN_ID,
|
||||
"user",
|
||||
row.user_id,
|
||||
key,
|
||||
);
|
||||
if (existing && existing.value !== null) continue;
|
||||
await pluginSettings.set(
|
||||
PLUGIN_ID,
|
||||
"user",
|
||||
row.user_id,
|
||||
key,
|
||||
JSON.stringify(value),
|
||||
);
|
||||
moved = true;
|
||||
}
|
||||
if (moved) result.usersMoved++;
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Snippets user settings migration failed", {
|
||||
operation: "snippets_settings_migration",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
if (result.hostsMoved > 0 || result.usersMoved > 0) {
|
||||
databaseLogger.info(
|
||||
`Moved the snippet settings of ${result.hostsMoved} host(s) and ${result.usersMoved} user(s) into plugin settings`,
|
||||
{ operation: "snippets_settings_migration" },
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Moves user_preferences.terminal_macros into the ssh-terminal plugin's
|
||||
* macros user setting. Macros a browser kept on its own (storage mode
|
||||
* "local") are moved by the plugin's frontend the first time the panel opens.
|
||||
*
|
||||
* The column keeps its value until 3.0.0 drops it.
|
||||
*
|
||||
* Idempotent: a user whose macros setting is already set is skipped.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { databaseLogger } from "../utils/logger.js";
|
||||
import { selectLegacyRows } from "../utils/crypto-migration/raw-rows.js";
|
||||
import {
|
||||
createCurrentPluginRepository,
|
||||
createCurrentPluginSettingsRepository,
|
||||
} from "../database/repositories/factory.js";
|
||||
|
||||
const PLUGIN_ID = "ssh-terminal";
|
||||
const MAX_MACROS = 100;
|
||||
|
||||
/** The saved macros in a 2.8 terminal_macros value, or null for none. */
|
||||
export function macrosFromPreferences(value: unknown): unknown[] | null {
|
||||
if (typeof value !== "string" || !value) return null;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
const macros = parsed
|
||||
.filter(
|
||||
(macro) =>
|
||||
!!macro &&
|
||||
typeof macro === "object" &&
|
||||
typeof (macro as { id?: unknown }).id === "string" &&
|
||||
typeof (macro as { name?: unknown }).name === "string" &&
|
||||
Array.isArray((macro as { steps?: unknown }).steps),
|
||||
)
|
||||
.slice(0, MAX_MACROS);
|
||||
return macros.length > 0 ? macros : null;
|
||||
}
|
||||
|
||||
export async function runSshTerminalMacrosMigration(): Promise<{
|
||||
usersMoved: number;
|
||||
}> {
|
||||
const result = { usersMoved: 0 };
|
||||
try {
|
||||
const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID);
|
||||
if (!plugin) return result;
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
const pluginSettings = createCurrentPluginSettingsRepository();
|
||||
try {
|
||||
const rows = await selectLegacyRows<{
|
||||
user_id: string;
|
||||
terminal_macros: unknown;
|
||||
}>(sql`
|
||||
SELECT user_id, terminal_macros FROM user_preferences
|
||||
WHERE terminal_macros IS NOT NULL
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const macros = macrosFromPreferences(row.terminal_macros);
|
||||
if (!macros) continue;
|
||||
const existing = await pluginSettings.get(
|
||||
PLUGIN_ID,
|
||||
"user",
|
||||
row.user_id,
|
||||
"macros",
|
||||
);
|
||||
if (existing && existing.value !== null) continue;
|
||||
await pluginSettings.set(
|
||||
PLUGIN_ID,
|
||||
"user",
|
||||
row.user_id,
|
||||
"macros",
|
||||
JSON.stringify(macros),
|
||||
);
|
||||
result.usersMoved++;
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Terminal macros migration failed", {
|
||||
operation: "ssh_terminal_macros_migration",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
if (result.usersMoved > 0) {
|
||||
databaseLogger.info(
|
||||
`Moved the terminal macros of ${result.usersMoved} user(s) into plugin settings`,
|
||||
{ operation: "ssh_terminal_macros_migration" },
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user