UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

192 lines • 7.13 kB
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