mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: wire ai, automations, fleets and ssh-terminal to snippets (B2)
This commit is contained in:
1 parent
3656caf168
commit
c77301577f
16 files changed
+473
-52
No files matched your search
@@ -26,6 +26,16 @@
|
||||
"network:serve",
|
||||
"ui:surface"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"snippets": "^1.0.0"
|
||||
},
|
||||
"requires": [
|
||||
{
|
||||
"service": "snippets.access",
|
||||
"versionRange": "^1.0.0",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"contributes": {
|
||||
"tabs": [
|
||||
{
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
shellSingleQuote,
|
||||
} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js";
|
||||
import { withSshConnection } from "../ssh.js";
|
||||
import { resolveSnippetCommand } from "../../../../../src/backend/database/routes/snippets-execution.js";
|
||||
import {
|
||||
createCurrentNotificationChannelRepository,
|
||||
createCurrentSnippetRepository,
|
||||
} from "../../../../../src/backend/database/repositories/factory.js";
|
||||
import { createCurrentNotificationChannelRepository } from "../../../../../src/backend/database/repositories/factory.js";
|
||||
import { getSnippet, resolveSnippetCommandFor } from "../snippets.js";
|
||||
import { sendAutomationNotification } from "../notify.js";
|
||||
import { automationFetch } from "../http.js";
|
||||
import { renderRecord, renderTemplate } from "../template.js";
|
||||
@@ -176,11 +173,9 @@ async function runSnippet(
|
||||
step: Extract<Step, { type: "run_snippet" }>,
|
||||
context: StepExecutionContext,
|
||||
): Promise<StepResult> {
|
||||
const snippet = await createCurrentSnippetRepository()
|
||||
.findOwnedById(context.userId, step.snippetId)
|
||||
.catch(() => null);
|
||||
const snippet = await getSnippet(context.userId, step.snippetId);
|
||||
|
||||
if (!snippet) return fail("Snippet not found");
|
||||
if (!snippet) return fail("Snippet not found, or the snippets plugin is off");
|
||||
if (snippet.isNote) return fail("Notes cannot be executed on a host");
|
||||
|
||||
// Template values only ever reach the snippet through inputValues, never by
|
||||
@@ -189,8 +184,9 @@ async function runSnippet(
|
||||
const inputValues = renderRecord(step.inputValues, context.template) ?? {};
|
||||
|
||||
return runOnTargets(step.hostSelector, context, async (target) => {
|
||||
const command = resolveSnippetCommand(
|
||||
snippet.content,
|
||||
const command = await resolveSnippetCommandFor(
|
||||
context.userId,
|
||||
step.snippetId,
|
||||
{
|
||||
ip: target.host.ip,
|
||||
username: target.host.username,
|
||||
@@ -199,6 +195,9 @@ async function runSnippet(
|
||||
},
|
||||
inputValues,
|
||||
);
|
||||
if (!command) {
|
||||
return { output: "", error: "Snippet could not be resolved" };
|
||||
}
|
||||
|
||||
if (context.dryRun) {
|
||||
return { output: `Would run snippet on ${target.name}: ${command}` };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { setPluginSsh } from "./ssh.js";
|
||||
import { setPluginServices } from "./snippets.js";
|
||||
import { startAutomationsService, stopAutomationsService } from "./routes.js";
|
||||
import {
|
||||
startAutomationScheduler,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
export async function activate(ctx: PluginContext) {
|
||||
setPluginSsh(ctx.ssh);
|
||||
ctx.disposables.add(() => setPluginSsh(null));
|
||||
setPluginServices(ctx.services);
|
||||
ctx.disposables.add(() => setPluginServices(null));
|
||||
startAutomationsService(
|
||||
// The webhook route authenticates on its own per-automation token rather
|
||||
// than a session, which is the point of an inbound webhook.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { PluginServices } from "@termix/plugin-sdk/backend";
|
||||
|
||||
let current: PluginServices | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setPluginServices(services: PluginServices | null): void {
|
||||
current = services;
|
||||
}
|
||||
|
||||
/** Another plugin's snippets, as ctx.services.get("snippets.access") returns them. */
|
||||
interface SnippetsAccess {
|
||||
get: (id: number) => Promise<{
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote: boolean;
|
||||
} | null>;
|
||||
resolveCommand: (
|
||||
id: number,
|
||||
vars: { ip?: string; username?: string; port?: number; name?: string },
|
||||
inputValues?: Record<string, string>,
|
||||
) => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A snippet's command, resolved against a target host and run as userId.
|
||||
* Null when the snippets plugin is off, the user may not use it, or the
|
||||
* snippet does not exist. Optional: the run_snippet step fails with a clear
|
||||
* message without it, rather than the plugin failing to activate.
|
||||
*/
|
||||
export async function resolveSnippetCommandFor(
|
||||
userId: string,
|
||||
id: number,
|
||||
vars: { ip?: string; username?: string; port?: number; name?: string },
|
||||
inputValues?: Record<string, string>,
|
||||
): Promise<string | null> {
|
||||
if (!current) return null;
|
||||
try {
|
||||
return await current
|
||||
.get<SnippetsAccess>("snippets.access", { userId })
|
||||
.resolveCommand(id, vars, inputValues);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSnippet(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<{
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote: boolean;
|
||||
} | null> {
|
||||
if (!current) return null;
|
||||
try {
|
||||
return await current
|
||||
.get<SnippetsAccess>("snippets.access", { userId })
|
||||
.get(id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user