commands/ward-track.ts
14 KB · sha256:c57061866281fc3a3ad55cfde9ab09b72cffb6638137476619cf2f094184acb9
import {
addRungCost,
addRungRequirement,
appendToTrack,
createTrack,
deleteTrack,
findTrack,
insertIntoTrack,
listTracks,
removeFromTrack,
removeRungCost,
removeRungRequirement,
setRungAutoPromote,
setTrackGroups,
setTrackPollSeconds,
type TransactionConfig,
} from "../models/Track";
import { findGroup } from "../models/Group";
import { groupSuggester, refreshTracks, trackSuggester } from "../lib/cache";
import { err, escapeMM, ok, raw, requirePerm, warn } from "../lib/format";
import { PERM } from "./ward";
import { compileExpr, ExprParseError } from "../lib/expr";
import { getHandler, listHandlers, missingPluginFor } from "../lib/transactions";
@Command("ward track list")
export class WardTrackList {
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackInfo)) return;
const all = await listTracks();
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(
ctx.sender,
`<gradient:#9b87f5:#5b3df5>Tracks</gradient> <gray>(${all.length})`,
);
if (all.length === 0) raw(ctx.sender, "<dark_gray>(none)");
for (const t of all) {
const ladder = t.rungs.map((r) => escapeMM(r.group)).join(" → ");
raw(
ctx.sender,
` <white>${escapeMM(t.name)}</white> <gray>[${ladder}]`,
);
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
@Command("ward track create")
export class WardTrackCreate {
@Arg("name", { type: "string" })
name!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
if (await findTrack(this.name)) {
warn(
ctx.sender,
`track <white>${escapeMM(this.name)}</white> already exists`,
);
return;
}
const created = await createTrack(this.name);
await refreshTracks();
ok(
ctx.sender,
`created track <white>${escapeMM(created.name)}</white> <gray>(empty)`,
);
}
}
@Command("ward track delete")
export class WardTrackDelete {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const removed = await deleteTrack(this.track);
if (!removed) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
await refreshTracks();
ok(ctx.sender, `deleted track <white>${escapeMM(this.track)}`);
}
}
@Command("ward track")
export class WardTrack {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackInfo)) return;
await renderTrackCard(ctx, this.track);
}
}
@Command("ward track info")
export class WardTrackInfo {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackInfo)) return;
await renderTrackCard(ctx, this.track);
}
}
async function renderTrackCard(ctx: CommandCtx, name: string) {
const t = await findTrack(name);
if (!t) {
err(ctx.sender, `unknown track: <white>${escapeMM(name)}`);
return;
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(ctx.sender, `<gradient:#9b87f5:#5b3df5>${escapeMM(t.name)}</gradient>`);
if (t.pollSeconds != null) {
raw(ctx.sender, `<gray>Poll: <white>${t.pollSeconds}s`);
}
raw(ctx.sender, `<gray>Rungs (${t.rungs.length}):`);
if (t.rungs.length === 0) {
raw(
ctx.sender,
" <dark_gray>(empty -- /ward track <name> append <group>)",
);
} else {
t.rungs.forEach((r, i) => {
const tags: string[] = [];
if (r.autoPromote) tags.push("<aqua>auto");
if (r.requirements.length) tags.push(`<yellow>${r.requirements.length} req`);
if (r.costs.length) tags.push(`<gold>${r.costs.length} cost`);
const suffix = tags.length ? ` <dark_gray>[${tags.join(" ")}<dark_gray>]` : "";
raw(ctx.sender, ` <gray>${i + 1}.</gray> <white>${escapeMM(r.group)}</white>${suffix}`);
});
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
@Command("ward track append")
export class WardTrackAppend {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
if (!(await findTrack(this.track))) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
await appendToTrack(this.track, this.group);
ok(
ctx.sender,
`appended <white>${escapeMM(this.group)}</white> to <white>${escapeMM(this.track)}`,
);
}
}
@Command("ward track insert")
export class WardTrackInsert {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Arg("position", { type: "int", min: 0 })
position!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const t = await findTrack(this.track);
if (!t) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
if (!(await findGroup(this.group))) {
err(ctx.sender, `unknown group: <white>${escapeMM(this.group)}`);
return;
}
if (this.position > t.rungs.length) {
err(
ctx.sender,
`position ${this.position} out of range (track has ${t.rungs.length} rung${t.rungs.length === 1 ? "" : "s"})`,
);
return;
}
await insertIntoTrack(this.track, this.group, this.position);
ok(
ctx.sender,
`inserted <white>${escapeMM(this.group)}</white> at position ${this.position} of <white>${escapeMM(this.track)}`,
);
}
}
@Command("ward track remove")
export class WardTrackRemove {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Arg("group", { type: "string", suggest: groupSuggester })
group!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
if (!(await findTrack(this.track))) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
await removeFromTrack(this.track, this.group);
ok(
ctx.sender,
`removed <white>${escapeMM(this.group)}</white> from <white>${escapeMM(this.track)}`,
);
}
}
@Command("ward track clear")
export class WardTrackClear {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
if (!(await findTrack(this.track))) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
await setTrackGroups(this.track, []);
ok(ctx.sender, `cleared track <white>${escapeMM(this.track)}`);
}
}
@Command("ward track pollseconds")
export class WardTrackPollSeconds {
@Arg("track", { type: "string", suggest: trackSuggester })
track!: string;
@Arg("seconds", { type: "int", min: 5 })
seconds!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const res = await setTrackPollSeconds(this.track, this.seconds);
if (!res) {
err(ctx.sender, `unknown track: <white>${escapeMM(this.track)}`);
return;
}
ok(ctx.sender, `poll cadence on <white>${escapeMM(this.track)}</white> set to <white>${this.seconds}s`);
}
}
async function findRung(ctx: CommandCtx, trackName: string, index: number) {
const t = await findTrack(trackName);
if (!t) {
err(ctx.sender, `unknown track: <white>${escapeMM(trackName)}`);
return null;
}
const rung = t.rungs[index];
if (!rung) {
err(
ctx.sender,
`rung index ${index} out of range (track has ${t.rungs.length} rung${t.rungs.length === 1 ? "" : "s"})`,
);
return null;
}
return { track: t, rung };
}
@Command("ward track rung info")
export class WardRungInfo {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackInfo)) return;
const found = await findRung(ctx, this.track, this.rung);
if (!found) return;
const { track, rung } = found;
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(
ctx.sender,
`<gradient:#9b87f5:#5b3df5>${escapeMM(track.name)}</gradient> <dark_gray>· <white>rung ${this.rung}</white> <dark_gray>· <gold>${escapeMM(rung.group)}`,
);
raw(ctx.sender, `<gray>autoPromote: <white>${rung.autoPromote}`);
raw(ctx.sender, `<gray>requirements (${rung.requirements.length}):`);
if (rung.requirements.length === 0) raw(ctx.sender, " <dark_gray>(none)");
rung.requirements.forEach((r, i) =>
raw(ctx.sender, ` <gray>${i}.</gray> <white>${escapeMM(r)}`),
);
raw(ctx.sender, `<gray>costs (${rung.costs.length}):`);
if (rung.costs.length === 0) raw(ctx.sender, " <dark_gray>(none)");
rung.costs.forEach((c, i) => {
const handler = getHandler(c.handler);
const desc = handler ? handler.describe(c) : `<red>${escapeMM(c.handler)} (missing)`;
raw(ctx.sender, ` <gray>${i}.</gray> <white>${desc}`);
});
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
@Command("ward track rung requirement add")
export class WardRungReqAdd {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Arg("expression", { type: "greedy", greedy: true }) expression!: string;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
try { compileExpr(this.expression); }
catch (e) {
err(ctx.sender, `bad expression: <white>${escapeMM(e instanceof ExprParseError ? e.message : String((e as Error)?.message ?? e))}`);
return;
}
const updated = await addRungRequirement(this.track, this.rung, this.expression);
if (!updated) {
err(ctx.sender, `unknown track or rung`);
return;
}
ok(ctx.sender, `added requirement to rung ${this.rung}: <white>${escapeMM(this.expression)}`);
}
}
@Command("ward track rung requirement remove")
export class WardRungReqRemove {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Arg("index", { type: "int", min: 0 }) index!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const updated = await removeRungRequirement(this.track, this.rung, this.index);
if (!updated) {
err(ctx.sender, `unknown requirement index`);
return;
}
ok(ctx.sender, `removed requirement ${this.index} from rung ${this.rung}`);
}
}
@Command("ward track rung cost add")
export class WardRungCostAdd {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Arg("handler", { type: "string" }) handler!: string;
@Arg("amount", { type: "double", min: 0 }) amount!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
if (!getHandler(this.handler)) {
const missing = missingPluginFor(this.handler);
const available = listHandlers().join(", ");
err(
ctx.sender,
missing
? `handler <white>${escapeMM(this.handler)}</white> requires plugin <white>${escapeMM(missing)}`
: `unknown handler <white>${escapeMM(this.handler)}</white><gray>; available: <white>${escapeMM(available)}`,
);
return;
}
const cfg: TransactionConfig = { handler: this.handler.toLowerCase(), amount: this.amount };
const updated = await addRungCost(this.track, this.rung, cfg);
if (!updated) {
err(ctx.sender, `unknown track or rung`);
return;
}
ok(ctx.sender, `added cost to rung ${this.rung}: <white>${escapeMM(this.handler)} ${this.amount}`);
}
}
@Command("ward track rung cost remove")
export class WardRungCostRemove {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Arg("index", { type: "int", min: 0 }) index!: number;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const updated = await removeRungCost(this.track, this.rung, this.index);
if (!updated) {
err(ctx.sender, `unknown cost index`);
return;
}
ok(ctx.sender, `removed cost ${this.index} from rung ${this.rung}`);
}
}
@Command("ward track rung autopromote")
export class WardRungAutoPromote {
@Arg("track", { type: "string", suggest: trackSuggester }) track!: string;
@Arg("rung", { type: "int", min: 0 }) rung!: number;
@Arg("on", { type: "bool" }) on!: boolean;
@Run
async run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackEdit)) return;
const updated = await setRungAutoPromote(this.track, this.rung, this.on);
if (!updated) {
err(ctx.sender, `unknown track or rung`);
return;
}
ok(ctx.sender, `rung ${this.rung} autoPromote = <white>${this.on}`);
}
}
@Command("ward track handlers")
export class WardTrackHandlers {
@Run
run(ctx: CommandCtx) {
if (!requirePerm(ctx, PERM.trackInfo)) return;
const all = listHandlers();
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
raw(ctx.sender, `<gradient:#9b87f5:#5b3df5>Registered transaction handlers</gradient> <gray>(${all.length})`);
if (all.length === 0) raw(ctx.sender, "<dark_gray>(none -- no defaults available + no scripts registered)");
for (const id of all) {
raw(ctx.sender, ` <white>${escapeMM(id)}`);
}
raw(ctx.sender, "<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━");
}
}