1- import { createHash } from "node:crypto" ;
21import { readFile } from "node:fs/promises" ;
3- import { dirname , isAbsolute , join , resolve as resolvePath } from "node:path" ;
2+ import { isAbsolute , join , resolve as resolvePath } from "node:path" ;
43import type { BuildManifest , SkillManifest } from "@trigger.dev/core/v3/schemas" ;
54import { copyDirectoryRecursive } from "@trigger.dev/build/internal" ;
65import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js" ;
@@ -21,13 +20,84 @@ export type BundleSkillsResult = {
2120 skills : SkillManifest [ ] ;
2221} ;
2322
23+ export type CopySkillFoldersOptions = {
24+ skills : SkillManifest [ ] ;
25+ /** Root where `{destinationRoot}/{id}/` folders will be created. */
26+ destinationRoot : string ;
27+ /** Used to resolve relative `filePath` references in skill manifests. */
28+ workingDir : string ;
29+ /** Only `debug` is used. `BuildLogger` and the cli `logger` both satisfy this shape. */
30+ logger : { debug : ( ...args : unknown [ ] ) => void } ;
31+ } ;
32+
33+ /**
34+ * Copy each skill's source folder to `{destinationRoot}/{id}/`. Validates
35+ * that `SKILL.md` exists and has the required frontmatter. Pure file IO —
36+ * no indexer subprocess, no env handling.
37+ *
38+ * Used by the dev path (driven by the main worker indexer's skills list)
39+ * and indirectly by the deploy path (via `bundleSkills` which discovers
40+ * skills via its own indexer pass first, then delegates here).
41+ */
42+ export async function copySkillFolders (
43+ options : CopySkillFoldersOptions
44+ ) : Promise < SkillManifest [ ] > {
45+ const { skills, destinationRoot, workingDir, logger } = options ;
46+
47+ if ( skills . length === 0 ) {
48+ return [ ] ;
49+ }
50+
51+ for ( const skill of skills ) {
52+ const callerDir = skill . filePath
53+ ? resolvePath ( workingDir , skill . filePath , ".." )
54+ : workingDir ;
55+ const sourcePath = isAbsolute ( skill . sourcePath )
56+ ? skill . sourcePath
57+ : resolvePath ( callerDir , skill . sourcePath ) ;
58+ const skillMdPath = join ( sourcePath , "SKILL.md" ) ;
59+
60+ let skillMd : string ;
61+ try {
62+ skillMd = await readFile ( skillMdPath , "utf8" ) ;
63+ } catch {
64+ throw new Error (
65+ `Skill "${ skill . id } ": SKILL.md not found at ${ skillMdPath } . ` +
66+ `Registered via skills.define({ id: "${ skill . id } ", path: "${ skill . sourcePath } " }) ` +
67+ `at ${ skill . filePath } .`
68+ ) ;
69+ }
70+
71+ if ( ! / ^ - - - \r ? \n [ \s \S ] * ?\r ? \n - - - / . test ( skillMd ) ) {
72+ throw new Error (
73+ `Skill "${ skill . id } ": SKILL.md at ${ skillMdPath } is missing a frontmatter block.`
74+ ) ;
75+ }
76+ if ( ! / \b n a m e : \s * \S / . test ( skillMd ) || ! / \b d e s c r i p t i o n : \s * \S / . test ( skillMd ) ) {
77+ throw new Error (
78+ `Skill "${ skill . id } ": SKILL.md at ${ skillMdPath } frontmatter must include both \`name\` and \`description\`.`
79+ ) ;
80+ }
81+
82+ const skillDest = join ( destinationRoot , skill . id ) ;
83+ logger . debug ( `[copySkillFolders] Copying ${ sourcePath } → ${ skillDest } ` ) ;
84+ await copyDirectoryRecursive ( sourcePath , skillDest ) ;
85+ }
86+
87+ return [ ...skills ] . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
88+ }
89+
2490/**
2591 * Built-in skill bundler — not an extension. Runs the indexer locally
26- * against the bundled worker output to discover `ai.defineSkill (...)`
92+ * against the bundled worker output to discover `skills.define (...)`
2793 * registrations, validates each skill's `SKILL.md`, and copies the
2894 * folder into `{outputPath}/.trigger/skills/{id}/` so the deploy image
2995 * picks it up via the existing Dockerfile `COPY`.
3096 *
97+ * Used by the deploy path. The dev path uses `copySkillFolders` directly,
98+ * driven by the main worker indexer that already runs in `BackgroundWorker.initialize` —
99+ * no duplicate indexer pass needed there.
100+ *
31101 * No `trigger.config.ts` changes required — discovery is side-effect
32102 * based, same mechanism as task/prompt registration.
33103 */
@@ -71,65 +141,20 @@ export async function bundleSkills(
71141 return { buildManifest, skills : [ ] } ;
72142 }
73143
74- // Destination layout differs between dev and deploy:
75- // - Dev: the worker runs with cwd = workingDir, so skills must live at
76- // {workingDir}/.trigger/skills/{id}/ for skill.local() to find them.
77- // - Deploy: the Dockerfile COPY picks up everything under outputPath into
78- // /app, so we target {outputPath}/.trigger/skills/{id}/ and the
79- // container's cwd (/app) resolves correctly.
80- const destinationRoot =
81- buildManifest . target === "dev"
82- ? join ( workingDir , ".trigger" , "skills" )
83- : join ( buildManifest . outputPath , ".trigger" , "skills" ) ;
144+ // Deploy target: the Dockerfile COPY picks up everything under outputPath
145+ // into /app, so we target {outputPath}/.trigger/skills/{id}/ and the
146+ // container's cwd (/app) resolves correctly.
147+ const destinationRoot = join ( buildManifest . outputPath , ".trigger" , "skills" ) ;
84148
85- for ( const skill of skills ) {
86- // Resolve the skill's source folder relative to the file that called
87- // `skills.define(...)`. Absolute paths are honored as-is.
88- const callerDir = skill . filePath
89- ? dirname ( resolvePath ( workingDir , skill . filePath ) )
90- : workingDir ;
91- const sourcePath = isAbsolute ( skill . sourcePath )
92- ? skill . sourcePath
93- : resolvePath ( callerDir , skill . sourcePath ) ;
94- const skillMdPath = join ( sourcePath , "SKILL.md" ) ;
95-
96- let skillMd : string ;
97- try {
98- skillMd = await readFile ( skillMdPath , "utf8" ) ;
99- } catch {
100- throw new Error (
101- `Skill "${ skill . id } ": SKILL.md not found at ${ skillMdPath } . ` +
102- `Registered via ai.defineSkill({ id: "${ skill . id } ", path: "${ skill . sourcePath } " }) ` +
103- `at ${ skill . filePath } .`
104- ) ;
105- }
106-
107- if ( ! / ^ - - - \r ? \n [ \s \S ] * ?\r ? \n - - - / . test ( skillMd ) ) {
108- throw new Error (
109- `Skill "${ skill . id } ": SKILL.md at ${ skillMdPath } is missing a frontmatter block.`
110- ) ;
111- }
112- if ( ! / \b n a m e : \s * \S / . test ( skillMd ) || ! / \b d e s c r i p t i o n : \s * \S / . test ( skillMd ) ) {
113- throw new Error (
114- `Skill "${ skill . id } ": SKILL.md at ${ skillMdPath } frontmatter must include both \`name\` and \`description\`.`
115- ) ;
116- }
117-
118- const skillDest = join ( destinationRoot , skill . id ) ;
119- logger . debug ( `[bundleSkills] Copying ${ sourcePath } → ${ skillDest } ` ) ;
120- await copyDirectoryRecursive ( sourcePath , skillDest ) ;
121- }
122-
123- // Sort by id for deterministic manifest output
124- skills = [ ...skills ] . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
125-
126- // Content hash is derived from each SKILL.md's content for cache invalidation
127- // downstream (dashboard persistence in Phase 2). Not used in Phase 1.
128- void createHash ;
129- void dirname ;
149+ const sortedSkills = await copySkillFolders ( {
150+ skills,
151+ destinationRoot,
152+ workingDir,
153+ logger,
154+ } ) ;
130155
131156 return {
132- buildManifest : { ...buildManifest , skills } ,
133- skills,
157+ buildManifest : { ...buildManifest , skills : sortedSkills } ,
158+ skills : sortedSkills ,
134159 } ;
135160}
0 commit comments