mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05: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
@@ -23,6 +23,9 @@
|
||||
"network:serve",
|
||||
"ui:surface"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"snippets": "^1.0.0"
|
||||
},
|
||||
"provides": [
|
||||
{
|
||||
"service": "fleets.access",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
export interface CommandHostVars {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* $HOST/$USER/$PORT/$NAME/$INPUT_n substitution for an ad hoc fleet command,
|
||||
* typed directly into the "run on fleet" box rather than loaded from a saved
|
||||
* snippet. Mirrors the snippets plugin's own resolveSnippetCommand and the
|
||||
* frontend's src/ui/lib/snippet-variables.ts - kept as a small standalone
|
||||
* copy rather than a cross-plugin dependency for one pure function with no
|
||||
* snippet data of its own to fetch.
|
||||
*/
|
||||
export function resolveCommandVariables(
|
||||
content: string,
|
||||
host: CommandHostVars | 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;
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ElevationError,
|
||||
} from "@termix/plugin-sdk/host-commands";
|
||||
import type { FleetRepository } from "./repository.js";
|
||||
import { resolveCommandVariables } from "./command-variables.js";
|
||||
|
||||
const FLEET_TRANSFER_MAX_BYTES = 200 * 1024 * 1024;
|
||||
const upload = multer({
|
||||
@@ -74,6 +73,35 @@ function parseShareTargets(
|
||||
return targets;
|
||||
}
|
||||
|
||||
interface CommandVariables {
|
||||
resolve: (
|
||||
content: string,
|
||||
host: {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number | string;
|
||||
name?: string;
|
||||
} | null,
|
||||
inputValues?: Record<string, string>,
|
||||
) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* $HOST, $USER, $PORT, $NAME and $INPUT_n in an ad hoc fleet command, through
|
||||
* the snippets plugin, which owns the syntax. Without it the command runs as
|
||||
* typed.
|
||||
*/
|
||||
export function resolveCommandVariables(
|
||||
ctx: Pick<PluginContext, "registry">,
|
||||
command: string,
|
||||
host: Parameters<CommandVariables["resolve"]>[1],
|
||||
inputValues: Record<string, string> = {},
|
||||
): string {
|
||||
const variables =
|
||||
ctx.registry.consume<CommandVariables>("snippets.variables");
|
||||
return variables ? variables.resolve(command, host, inputValues) : command;
|
||||
}
|
||||
|
||||
function actor(ctx: PluginContext): string {
|
||||
return ctx.currentActor() as string;
|
||||
}
|
||||
@@ -884,6 +912,7 @@ export function registerFleetRoutes(
|
||||
}
|
||||
|
||||
const resolvedCommand = resolveCommandVariables(
|
||||
ctx,
|
||||
command,
|
||||
{
|
||||
ip: fullHost.ip,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
getErrorMessage,
|
||||
extractSnippetInputs,
|
||||
type SnippetInput,
|
||||
} from "./helpers.js";
|
||||
import { getErrorMessage, type SnippetInput } from "./helpers.js";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
invokeAction,
|
||||
useTranslation,
|
||||
useHosts,
|
||||
type PluginHostRecord,
|
||||
@@ -792,8 +789,16 @@ function RunCommandTab({ api, fleetId }: { api: FleetsApi; fleetId: number }) {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [results, setResults] = useState<FleetHostResult[] | null>(null);
|
||||
|
||||
// The snippets plugin owns $INPUT_n parsing; without it the command
|
||||
// runs as typed.
|
||||
useEffect(() => {
|
||||
setInputs(extractSnippetInputs(command));
|
||||
let cancelled = false;
|
||||
void invokeAction("snippets.extractInputs", command).then((found) => {
|
||||
if (!cancelled) setInputs(Array.isArray(found) ? found : []);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [command]);
|
||||
|
||||
async function handleRun() {
|
||||
|
||||
@@ -21,24 +21,3 @@ export interface SnippetInput {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
|
||||
/**
|
||||
* Finds the $INPUT_n placeholders in a command, for the fleet run box's
|
||||
* input fields. Mirrors src/ui/lib/snippet-variables.ts extractSnippetInputs.
|
||||
*/
|
||||
export function extractSnippetInputs(content: string): SnippetInput[] {
|
||||
const seen = new Map<string, SnippetInput>();
|
||||
let match: RegExpExecArray | null;
|
||||
INPUT_PATTERN.lastIndex = 0;
|
||||
while ((match = INPUT_PATTERN.exec(content)) !== null) {
|
||||
const digits = match[1] ?? match[3];
|
||||
const key = `INPUT_${digits}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.set(key, { key, label: match[2]?.trim() || `Input ${digits}` });
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCommandVariables } from "../../src/backend/routes.js";
|
||||
|
||||
function registry(value?: unknown) {
|
||||
return {
|
||||
registry: {
|
||||
provide: () => {},
|
||||
revoke: () => false,
|
||||
consume: <T>() => value as T | undefined,
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
describe("resolveCommandVariables", () => {
|
||||
it("fills variables through the snippets plugin's resolver", () => {
|
||||
const ctx = registry({
|
||||
resolve: (content: string, host: { ip?: string } | null) =>
|
||||
content.replace("$HOST", host?.ip ?? ""),
|
||||
});
|
||||
expect(resolveCommandVariables(ctx, "ping $HOST", { ip: "10.0.0.1" })).toBe(
|
||||
"ping 10.0.0.1",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the command as typed while snippets is off", () => {
|
||||
expect(
|
||||
resolveCommandVariables(registry(), "ping $HOST", { ip: "10.0.0.1" }),
|
||||
).toBe("ping $HOST");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user