trigger.dev
Version:
A Command-Line Interface for Trigger.dev projects
192 lines • 7.13 kB
JavaScript
import { readFile, readdir } from "fs/promises";
import { join } from "path";
import { z } from "zod";
import { RulesFileInstallStrategy } from "./types.js";
const RulesManifestDataSchema = z.object({
name: z.string(),
description: z.string(),
currentVersion: z.string(),
versions: z.record(z.string(), z.object({
options: z.array(z.object({
name: z.string(),
title: z.string(),
label: z.string(),
path: z.string(),
tokens: z.number(),
client: z.string().optional(),
installStrategy: z.string().optional(),
applyTo: z.string().optional(),
})),
})),
});
export class RulesManifest {
manifest;
loader;
constructor(manifest, loader) {
this.manifest = manifest;
this.loader = loader;
}
get name() {
return this.manifest.name;
}
get description() {
return this.manifest.description;
}
get currentVersion() {
return this.manifest.currentVersion;
}
async getCurrentVersion() {
const version = this.versions[this.manifest.currentVersion];
if (!version) {
throw new Error(`Version ${this.manifest.currentVersion} not found in manifest`);
}
const options = await Promise.all(version.options.map(async (option) => {
const contents = await this.loader.loadRulesFile(option.path);
// Omit path
const { path, installStrategy, ...rest } = option;
const $installStrategy = RulesFileInstallStrategy.safeParse(installStrategy ?? "skills");
// Skip variants with invalid install strategies
if (!$installStrategy.success) {
return;
}
return { ...rest, contents, installStrategy: $installStrategy.data };
}));
return {
version: this.manifest.currentVersion,
options: options.filter(Boolean),
};
}
get versions() {
return this.manifest.versions;
}
}
export async function loadRulesManifest(loader) {
const content = await loader.loadManifestContent();
return new RulesManifest(RulesManifestDataSchema.parse(JSON.parse(content)), loader);
}
/**
* Loads agent skills bundled inside the `trigger.dev` CLI (the `skills/` folder shipped
* via the package's `files[]`). The CLI is the only source of skills; `skillsDir` and
* `version` are resolved from the CLI's own package by the caller and injected here, so
* this stays a pure reader. Synthesizes the manifest shape the install pipeline consumes
* so skills flow through `loadRulesManifest` -> `getCurrentVersion` -> install, with
* `installStrategy: "skills"`. `version` (== the CLI/SDK version) is stamped into each
* skill on read in place of the `{{TRIGGER_SDK_VERSION}}` placeholder.
*/
export class BundledSkillsLoader {
skillsDir;
version;
constructor(skillsDir, version) {
this.skillsDir = skillsDir;
this.version = version;
}
async loadManifestContent() {
let entries = [];
try {
const dirents = await readdir(this.skillsDir, { withFileTypes: true });
entries = dirents
.filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith("."))
.map((d) => d.name)
.sort();
}
catch (error) {
throw new Error(`No skills found in ${this.skillsDir}: ${error}`);
}
const options = [];
for (const name of entries) {
const skillMdPath = join(this.skillsDir, name, "SKILL.md");
let contents;
try {
contents = await readFile(skillMdPath, "utf8");
}
catch {
// a directory without a SKILL.md isn't a skill
continue;
}
const description = extractSkillDescription(contents) ?? humanizeSkillName(name);
options.push({
name,
title: humanizeSkillName(name),
label: description,
path: join(name, "SKILL.md"),
tokens: Math.max(1, Math.round(contents.length / 4)),
installStrategy: "skills",
});
}
if (options.length === 0) {
throw new Error(`No skills with a SKILL.md found in ${this.skillsDir}`);
}
return JSON.stringify({
name: "trigger.dev",
description: "Trigger.dev agent skills",
currentVersion: this.version,
versions: {
[this.version]: { options },
},
});
}
async loadRulesFile(relativePath) {
const path = join(this.skillsDir, relativePath);
try {
const raw = await readFile(path, "utf8");
// Stamp the CLI/SDK version into the skill so the copy on disk reflects the
// version the user is on, not a hardcoded number that drifts.
return raw.replace(/\{\{TRIGGER_SDK_VERSION\}\}/g, this.version);
}
catch (error) {
throw new Error(`Failed to load skill file: ${relativePath} - ${error}`);
}
}
}
function humanizeSkillName(name) {
const spaced = name.replace(/[-_]+/g, " ").trim();
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
/**
* Best-effort extraction of the `description` field from a SKILL.md YAML frontmatter
* block, used only for the picker label. Handles single-line, quoted, and folded/literal
* (`>`, `|`) scalars. Returns undefined if there's no frontmatter or description.
*/
function extractSkillDescription(skillMd) {
const match = skillMd.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = match?.[1];
if (!frontmatter) {
return undefined;
}
const lines = frontmatter.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === undefined) {
continue;
}
const m = line.match(/^description:\s*(.*)$/);
if (!m) {
continue;
}
const value = (m[1] ?? "").trim();
// Folded (>) or literal (|) block scalar: collect the indented continuation lines.
if (/^[>|][+-]?$/.test(value)) {
const collected = [];
for (let j = i + 1; j < lines.length; j++) {
const next = lines[j];
if (next === undefined) {
break;
}
if (/^\s+\S/.test(next)) {
collected.push(next.trim());
}
else if (next.trim() === "") {
collected.push("");
}
else {
break;
}
}
return collected.join(" ").replace(/\s+/g, " ").trim() || undefined;
}
// Inline scalar, possibly quoted.
return value.replace(/^["']|["']$/g, "").trim() || undefined;
}
return undefined;
}
//# sourceMappingURL=manifest.js.map