repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/skillz/match.ts
Rawimport { lstatSync } from "node:fs";
import { hostname } from "node:os";
import { extname, isAbsolute, join, win32 } from "node:path";
import { findExecutable } from "./src/pi-ext-executable.ts";
const platforms: Record<string, string> = {
lnx: "linux",
win32: "win32",
mac: "darwin",
};
const keys = ["os", "host", "path", "which", "env"] as const;
type Field = (typeof keys)[number];
type Mode = "single" | "any" | "all";
export interface ConditionCheck {
field: Field;
mode: Mode;
options: { value: string; matches: boolean }[];
matches: boolean;
}
export type MatchResult =
| { matches: boolean; checks: ConditionCheck[]; error?: never }
| { matches: false; error: string; checks?: never };
function values(value: unknown): { mode: Mode; items: string[] } | undefined {
let mode: Mode;
let items: unknown;
if (typeof value === "string") {
mode = "single";
items = [value];
} else if (Array.isArray(value)) {
mode = "any";
items = value;
} else if (value !== null && typeof value === "object") {
const entries = Object.entries(value);
if (entries.length !== 1) return undefined;
const [operator, choices] = entries[0];
if (operator !== "any" && operator !== "all") return undefined;
mode = operator;
items = choices;
} else return undefined;
return Array.isArray(items) &&
items.length > 0 &&
items.every((item) => typeof item === "string" && item.length > 0)
? { mode, items }
: undefined;
}
function validMarker(marker: string): boolean {
return !(
marker.includes("\\") ||
marker.includes("\0") ||
isAbsolute(marker) ||
win32.isAbsolute(marker) ||
marker
.split("/")
.some(
(part) => !part || part === "." || part === ".." || part.includes(":"),
)
);
}
function markerExists(cwd: string, marker: string): boolean {
let current = cwd;
try {
for (const part of marker.split("/")) {
current = join(current, part);
if (lstatSync(current).isSymbolicLink()) return false;
}
return true;
} catch {
return false;
}
}
function commandExists(command: string, platform: string): boolean {
const windows = platform === "win32";
const extensions =
windows && !extname(command)
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
.split(";")
.map((ext) => ext.trim())
.filter(Boolean)
: [];
const names = [
command,
...extensions.flatMap((ext) => [
command + ext,
command + ext.toLowerCase(),
]),
];
return Boolean(findExecutable(names, { platform }));
}
export function matchSkill(
frontmatter: Record<string, unknown>,
cwd: string,
platform = process.platform,
host = hostname(),
): MatchResult {
const conditions: { field: Field; mode: Mode; items: string[] }[] = [];
for (const field of keys) {
if (!(field in frontmatter)) continue;
const condition = values(frontmatter[field]);
if (!condition)
return { matches: false, error: `invalid ${field} condition` };
if (field === "os" && condition.items.some((item) => !(item in platforms)))
return {
matches: false,
error: "invalid os condition (expected lnx, win32, or mac)",
};
if (field === "path" && condition.items.some((item) => !validMarker(item)))
return { matches: false, error: "invalid path condition" };
if (
field === "which" &&
condition.items.some(
(item) => item === "." || item === ".." || /[\\/:\s\0]/.test(item),
)
)
return { matches: false, error: "invalid which condition" };
if (
field === "env" &&
condition.items.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))
)
return { matches: false, error: "invalid env condition" };
conditions.push({ field, mode: condition.mode, items: condition.items });
}
if (conditions.length === 0)
return {
matches: false,
error: "missing os, host, path, which, or env condition",
};
const checks: ConditionCheck[] = conditions.map(({ field, mode, items }) => {
const options = items.map((value) => ({
value,
matches:
field === "os"
? platforms[value] === platform
: field === "host"
? value === host
: field === "path"
? markerExists(cwd, value)
: field === "which"
? commandExists(value, platform)
: (process.env[value]?.length ?? 0) > 0,
}));
return {
field,
mode,
options,
matches:
mode === "all"
? options.every((option) => option.matches)
: options.some((option) => option.matches),
};
});
return { matches: checks.every((check) => check.matches), checks };
}