Skip to content

Commit 3545139

Browse files
committed
chore: migrate skill generator to TypeScript
1 parent 783ea10 commit 3545139

File tree

2 files changed

+84
-45
lines changed

2 files changed

+84
-45
lines changed

.github/workflows/skill-generator.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
node-version: 20
1717

1818
- name: Generate skill artifacts
19-
run: node scripts/skill-generator/generate.js
19+
run: npx --yes tsx@4.21.0 scripts/skill-generator/generate.ts
2020

2121
- name: Sync to skills repo
2222
env:
Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
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

710
const 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");
912
const TEMPLATE_PATH = path.resolve(__dirname, "template", "SKILL.md");
1013
const 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(/\.mdx?$/i, "");
138165
}
139166

140-
function titleFromSlug(value) {
167+
function titleFromSlug(value: string): string {
141168
const cleaned = value.replace(/\.mdx?$/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(/<Card([^>]*)>([\s\S]*?)<\/Card>/gi, (_, attrs, content) => {
348+
function formatCards(input: string): string {
349+
return input.replace(/<Card([^>]*)>([\s\S]*?)<\/Card>/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(/<img\s+([^>]+?)\s*\/?>(?:\s*<\/img>)?/gi, (_, attrs) => {
368+
function replaceImages(input: string): string {
369+
return input.replace(/<img\s+([^>]+?)\s*\/?>(?:\s*<\/img>)?/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

Comments
 (0)