11#!/usr/bin/env node
22
3- const fs = require ( "node:fs" ) ;
4- const fsp = require ( "node:fs/promises" ) ;
5- const path = require ( "node:path" ) ;
3+ import fs from "node:fs" ;
4+ import fsp from "node:fs/promises" ;
5+ import path from "node:path" ;
6+ import { fileURLToPath } from "node:url" ;
7+
8+ const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
69
710const DOCS_ROOT = path . resolve ( __dirname , ".." , ".." , "docs" ) ;
8- const OUTPUT_ROOT = path . resolve ( __dirname , "dist" ) ;
11+ const OUTPUT_ROOT = path . resolve ( __dirname , process . env . SKILL_GENERATOR_OUTPUT_ROOT ?? "dist" ) ;
912const TEMPLATE_PATH = path . resolve ( __dirname , "template" , "SKILL.md" ) ;
1013const DOCS_BASE_URL = "https://sandboxagent.dev/docs" ;
1114
12- async function main ( ) {
15+ type Reference = {
16+ slug : string ;
17+ title: string ;
18+ description: string ;
19+ canonicalUrl: string ;
20+ referencePath: string ;
21+ } ;
22+
23+ async function main ( ) : Promise < void > {
1324 if ( ! fs . existsSync ( DOCS_ROOT ) ) {
1425 throw new Error ( `Docs directory not found at ${ DOCS_ROOT } ` ) ;
1526 }
1627
17- await fsp . rm ( OUTPUT_ROOT , { recursive : true , force : true } ) ;
28+ try {
29+ await fsp . rm ( OUTPUT_ROOT , { recursive : true , force : true } ) ;
30+ } catch ( error : any ) {
31+ if ( error ?. code === "EACCES" ) {
32+ throw new Error (
33+ [
34+ `Failed to delete skill output directory due to permissions: ${ OUTPUT_ROOT } ` ,
35+ "" ,
36+ "If this directory was created by a different user (for example via Docker), either fix ownership/permissions" ,
37+ "or rerun with a different output directory:" ,
38+ "" ,
39+ ' SKILL_GENERATOR_OUTPUT_ROOT="dist-dev" npx --yes tsx@4.21.0 scripts/skill-generator/generate.ts' ,
40+ ] . join ( "\n" ) ,
41+ ) ;
42+ }
43+ throw error ;
44+ }
1845 await fsp . mkdir ( path . join ( OUTPUT_ROOT , "references" ) , { recursive : true } ) ;
1946
2047 const docFiles = await listDocFiles ( DOCS_ROOT ) ;
21- const references = [ ] ;
48+ const references : Reference [ ] = [ ] ;
2249
2350 for ( const filePath of docFiles ) {
2451 const relPath = normalizePath ( path . relative ( DOCS_ROOT , filePath ) ) ;
@@ -78,9 +105,9 @@ async function main() {
78105 console . log ( `Generated skill files in ${ OUTPUT_ROOT } ` ) ;
79106}
80107
81- async function listDocFiles ( dir ) {
108+ async function listDocFiles ( dir : string ) : Promise < string [ ] > {
82109 const entries = await fsp . readdir ( dir , { withFileTypes : true } ) ;
83- const files = [ ] ;
110+ const files : string [ ] = [ ] ;
84111
85112 for ( const entry of entries ) {
86113 const fullPath = path . join ( dir , entry . name ) ;
@@ -96,19 +123,19 @@ async function listDocFiles(dir) {
96123 return files ;
97124}
98125
99- function parseFrontmatter ( content ) {
126+ function parseFrontmatter ( content : string ) : { data: Record < string , string > ; body: string } {
100127 if ( ! content . startsWith ( "---" ) ) {
101- return { data : { } , body : content . trim ( ) } ;
128+ return { data : { } as Record < string , string > , body : content . trim ( ) } ;
102129 }
103130
104131 const match = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - \n ? / ) ;
105132 if ( ! match ) {
106- return { data : { } , body : content . trim ( ) } ;
133+ return { data : { } as Record < string , string > , body : content . trim ( ) } ;
107134 }
108135
109136 const frontmatter = match [ 1 ] ;
110137 const body = content . slice ( match [ 0 ] . length ) ;
111- const data = { } ;
138+ const data : Record < string , string > = { } ;
112139
113140 for ( const line of frontmatter . split ( "\n" ) ) {
114141 const trimmed = line . trim ( ) ;
@@ -124,7 +151,7 @@ function parseFrontmatter(content) {
124151 return { data, body : body . trim ( ) } ;
125152}
126153
127- function toSlug ( relPath ) {
154+ function toSlug ( relPath : string ) : string {
128155 const withoutExt = stripExtension ( relPath ) ;
129156 const normalized = withoutExt . replace ( / \\ / g, "/" ) ;
130157 if ( normalized . endsWith ( "/index" ) ) {
@@ -133,18 +160,25 @@ function toSlug(relPath) {
133160 return normalized ;
134161}
135162
136- function stripExtension ( value ) {
163+ function stripExtension ( value : string ) : string {
137164 return value . replace ( / \. m d x ? $ / i, "" ) ;
138165}
139166
140- function titleFromSlug ( value ) {
167+ function titleFromSlug ( value : string ) : string {
141168 const cleaned = value . replace ( / \. m d x ? $ / i, "" ) . replace ( / \\ / g, "/" ) ;
142169 const parts = cleaned . split ( "/" ) . filter ( Boolean ) ;
143170 const last = parts [ parts . length - 1 ] || "index" ;
144171 return formatSegment ( last ) ;
145172}
146173
147- function buildReferenceFile ( { title, description, canonicalUrl, sourcePath, body } ) {
174+ function buildReferenceFile ( args : {
175+ title : string ;
176+ description : string ;
177+ canonicalUrl : string ;
178+ sourcePath : string ;
179+ body : string ;
180+ } ) : string {
181+ const { title, description, canonicalUrl, sourcePath, body } = args ;
148182 const lines = [
149183 `# ${ title } ` ,
150184 "" ,
@@ -159,9 +193,9 @@ function buildReferenceFile({ title, description, canonicalUrl, sourcePath, body
159193 return `${ lines . join ( "\n" ) . trim ( ) } \n` ;
160194}
161195
162- function buildReferenceMap ( references ) {
163- const grouped = new Map ( ) ;
164- const groupRoots = new Set ( ) ;
196+ function buildReferenceMap ( references : Reference [ ] ) : string {
197+ const grouped = new Map < string , Reference [ ] > ( ) ;
198+ const groupRoots = new Set < string > ( ) ;
165199
166200 for ( const ref of references ) {
167201 const segments = ( ref . slug || "" ) . split ( "/" ) . filter ( Boolean ) ;
@@ -179,11 +213,15 @@ function buildReferenceMap(references) {
179213 group = segments [ 0 ] ;
180214 }
181215
182- if ( ! grouped . has ( group ) ) grouped . set ( group , [ ] ) ;
183- grouped . get ( group ) . push ( ref ) ;
216+ const bucket = grouped . get ( group ) ;
217+ if ( bucket ) {
218+ bucket . push ( ref ) ;
219+ } else {
220+ grouped . set ( group , [ ref ] ) ;
221+ }
184222 }
185223
186- const lines = [ ] ;
224+ const lines : string [ ] = [ ] ;
187225 const sortedGroups = [ ...grouped . keys ( ) ] . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
188226
189227 for ( const group of sortedGroups ) {
@@ -198,9 +236,9 @@ function buildReferenceMap(references) {
198236 return lines . join ( "\n" ) . trim ( ) ;
199237}
200238
201- function formatSegment ( value ) {
239+ function formatSegment ( value : string ) : string {
202240 if ( ! value ) return "General" ;
203- const special = {
241+ const special : Record < string , string > = {
204242 ai : "AI" ,
205243 sdks : "SDKs" ,
206244 } ;
@@ -212,11 +250,11 @@ function formatSegment(value) {
212250 . join ( " " ) ;
213251}
214252
215- function normalizePath ( value ) {
253+ function normalizePath ( value : string ) : string {
216254 return value . replace ( / \\ / g, "/" ) ;
217255}
218256
219- function convertDocToMarkdown ( body ) {
257+ function convertDocToMarkdown ( body : string ) : string {
220258 const { replaced, restore } = extractCodeBlocks ( body ?? "" ) ;
221259 let text = replaced ;
222260
@@ -260,8 +298,8 @@ function convertDocToMarkdown(body) {
260298 return restore ( text ) . trim ( ) ;
261299}
262300
263- function extractCodeBlocks ( input ) {
264- const blocks = [ ] ;
301+ function extractCodeBlocks ( input : string ) : { replaced: string ; restore: ( value : string ) => string } {
302+ const blocks : string [ ] = [ ] ;
265303 const replaced = input . replace ( / ` ` ` [ \s \S ] * ?` ` ` / g, ( match ) => {
266304 const token = `@@CODE_BLOCK_${ blocks . length } @@` ;
267305 blocks . push ( normalizeCodeBlock ( match ) ) ;
@@ -274,7 +312,7 @@ function extractCodeBlocks(input) {
274312 } ;
275313}
276314
277- function normalizeCodeBlock ( block ) {
315+ function normalizeCodeBlock ( block : string ) : string {
278316 const lines = block . split ( "\n" ) ;
279317 if ( lines . length < 2 ) return block . trim ( ) ;
280318
@@ -290,24 +328,25 @@ function normalizeCodeBlock(block) {
290328 return [ opening , ...normalizedContent , closing ] . join ( "\n" ) ;
291329}
292330
293- function stripWrapperTags ( input , tag ) {
331+ function stripWrapperTags ( input : string , tag : string ) : string {
294332 const open = new RegExp ( `<${ tag } [^>]*>` , "gi" ) ;
295333 const close = new RegExp ( `</${ tag } >` , "gi" ) ;
296334 return input . replace ( open , "\n" ) . replace ( close , "\n" ) ;
297335}
298336
299- function formatHeadingBlocks ( input , tag , fallback , level ) {
337+ function formatHeadingBlocks ( input : string , tag : string , fallback : string , level : number ) : string {
300338 const heading = "#" . repeat ( level ) ;
301339 const withTitles = input . replace (
302340 new RegExp ( `<${ tag } [^>]*title=(?:\"([^\"]+)\"|'([^']+)')[^>]*>` , "gi" ) ,
303- ( _ , doubleQuoted , singleQuoted ) => `\n${ heading } ${ ( doubleQuoted ?? singleQuoted ?? fallback ) . trim ( ) } \n\n` ,
341+ ( _ , doubleQuoted : string | undefined , singleQuoted : string | undefined ) =>
342+ `\n${ heading } ${ ( doubleQuoted ?? singleQuoted ?? fallback ) . trim ( ) } \n\n` ,
304343 ) ;
305344 const withFallback = withTitles . replace ( new RegExp ( `<${ tag } [^>]*>` , "gi" ) , `\n${ heading } ${ fallback } \n\n` ) ;
306345 return withFallback . replace ( new RegExp ( `</${ tag } >` , "gi" ) , "\n" ) ;
307346}
308347
309- function formatCards ( input ) {
310- return input . replace ( / < C a r d ( [ ^ > ] * ) > ( [ \s \S ] * ?) < \/ C a r d > / gi, ( _ , attrs , content ) => {
348+ function formatCards ( input : string ) : string {
349+ return input . replace ( / < C a r d ( [ ^ > ] * ) > ( [ \s \S ] * ?) < \/ C a r d > / gi, ( _ , attrs : string , content : string ) => {
311350 const title = getAttributeValue ( attrs , "title" ) ?? "Resource" ;
312351 const href = getAttributeValue ( attrs , "href" ) ;
313352 const summary = collapseWhitespace ( stripHtml ( content ) ) ;
@@ -317,17 +356,17 @@ function formatCards(input) {
317356 } ) ;
318357}
319358
320- function applyCallouts ( input , tag ) {
359+ function applyCallouts ( input : string , tag : string ) : string {
321360 const regex = new RegExp ( `<${ tag } [^>]*>([\s\S]*?)</${ tag } >` , "gi" ) ;
322- return input . replace ( regex , ( _ , content ) => {
361+ return input . replace ( regex , ( _ , content : string ) => {
323362 const label = tag . toUpperCase ( ) ;
324363 const text = collapseWhitespace ( stripHtml ( content ) ) ;
325364 return `\n> **${ label } :** ${ text } \n\n` ;
326365 } ) ;
327366}
328367
329- function replaceImages ( input ) {
330- return input . replace ( / < i m g \s + ( [ ^ > ] + ?) \s * \/ ? > (?: \s * < \/ i m g > ) ? / gi, ( _ , attrs ) => {
368+ function replaceImages ( input : string ) : string {
369+ return input . replace ( / < i m g \s + ( [ ^ > ] + ?) \s * \/ ? > (?: \s * < \/ i m g > ) ? / gi, ( _ , attrs : string ) => {
331370 const src = getAttributeValue ( attrs , "src" ) ?? "" ;
332371 const alt = getAttributeValue ( attrs , "alt" ) ?? "" ;
333372 if ( ! src ) return "" ;
@@ -336,29 +375,29 @@ function replaceImages(input) {
336375 } ) ;
337376}
338377
339- function getAttributeValue ( attrs , name ) {
378+ function getAttributeValue ( attrs : string , name : string ) : string | undefined {
340379 const regex = new RegExp ( `${ name } =(?:\"([^\"]+)\"|'([^']+)')` , "i" ) ;
341380 const match = attrs . match ( regex ) ;
342381 if ( ! match ) return undefined ;
343382 return ( match [ 1 ] ?? match [ 2 ] ?? "" ) . trim ( ) ;
344383}
345384
346- function stripHtml ( value ) {
385+ function stripHtml ( value : string ) : string {
347386 return value . replace ( / < [ ^ > ] + > / g, " " ) . replace ( / \s + / g, " " ) . trim ( ) ;
348387}
349388
350- function collapseWhitespace ( value ) {
389+ function collapseWhitespace ( value : string ) : string {
351390 return value . replace ( / \s + / g, " " ) . trim ( ) ;
352391}
353392
354- function stripIndentation ( input ) {
393+ function stripIndentation ( input : string ) : string {
355394 return input
356395 . split ( "\n" )
357396 . map ( ( line ) => line . replace ( / ^ \t + / , "" ) . replace ( / ^ { 2 , } / , "" ) )
358397 . join ( "\n" ) ;
359398}
360399
361- main ( ) . catch ( ( error ) => {
400+ main ( ) . catch ( ( error : unknown ) => {
362401 console . error ( error ) ;
363402 process . exit ( 1 ) ;
364403} ) ;
0 commit comments