mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-21 19:00:49 +00:00
chore: migrate skill generator to TypeScript
This commit is contained in:
parent
783ea1086a
commit
3545139cd3
2 changed files with 84 additions and 45 deletions
2
.github/workflows/skill-generator.yml
vendored
2
.github/workflows/skill-generator.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Generate skill artifacts
|
- name: Generate skill artifacts
|
||||||
run: node scripts/skill-generator/generate.js
|
run: npx --yes tsx@4.21.0 scripts/skill-generator/generate.ts
|
||||||
|
|
||||||
- name: Sync to skills repo
|
- name: Sync to skills repo
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,51 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const fs = require("node:fs");
|
import fs from "node:fs";
|
||||||
const fsp = require("node:fs/promises");
|
import fsp from "node:fs/promises";
|
||||||
const path = require("node:path");
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const DOCS_ROOT = path.resolve(__dirname, "..", "..", "docs");
|
const DOCS_ROOT = path.resolve(__dirname, "..", "..", "docs");
|
||||||
const OUTPUT_ROOT = path.resolve(__dirname, "dist");
|
const OUTPUT_ROOT = path.resolve(__dirname, process.env.SKILL_GENERATOR_OUTPUT_ROOT ?? "dist");
|
||||||
const TEMPLATE_PATH = path.resolve(__dirname, "template", "SKILL.md");
|
const TEMPLATE_PATH = path.resolve(__dirname, "template", "SKILL.md");
|
||||||
const DOCS_BASE_URL = "https://sandboxagent.dev/docs";
|
const DOCS_BASE_URL = "https://sandboxagent.dev/docs";
|
||||||
|
|
||||||
async function main() {
|
type Reference = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
canonicalUrl: string;
|
||||||
|
referencePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
if (!fs.existsSync(DOCS_ROOT)) {
|
if (!fs.existsSync(DOCS_ROOT)) {
|
||||||
throw new Error(`Docs directory not found at ${DOCS_ROOT}`);
|
throw new Error(`Docs directory not found at ${DOCS_ROOT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await fsp.rm(OUTPUT_ROOT, { recursive: true, force: true });
|
await fsp.rm(OUTPUT_ROOT, { recursive: true, force: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code === "EACCES") {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`Failed to delete skill output directory due to permissions: ${OUTPUT_ROOT}`,
|
||||||
|
"",
|
||||||
|
"If this directory was created by a different user (for example via Docker), either fix ownership/permissions",
|
||||||
|
"or rerun with a different output directory:",
|
||||||
|
"",
|
||||||
|
' SKILL_GENERATOR_OUTPUT_ROOT="dist-dev" npx --yes tsx@4.21.0 scripts/skill-generator/generate.ts',
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
await fsp.mkdir(path.join(OUTPUT_ROOT, "references"), { recursive: true });
|
await fsp.mkdir(path.join(OUTPUT_ROOT, "references"), { recursive: true });
|
||||||
|
|
||||||
const docFiles = await listDocFiles(DOCS_ROOT);
|
const docFiles = await listDocFiles(DOCS_ROOT);
|
||||||
const references = [];
|
const references: Reference[] = [];
|
||||||
|
|
||||||
for (const filePath of docFiles) {
|
for (const filePath of docFiles) {
|
||||||
const relPath = normalizePath(path.relative(DOCS_ROOT, filePath));
|
const relPath = normalizePath(path.relative(DOCS_ROOT, filePath));
|
||||||
|
|
@ -78,9 +105,9 @@ async function main() {
|
||||||
console.log(`Generated skill files in ${OUTPUT_ROOT}`);
|
console.log(`Generated skill files in ${OUTPUT_ROOT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDocFiles(dir) {
|
async function listDocFiles(dir: string): Promise<string[]> {
|
||||||
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||||
const files = [];
|
const files: string[] = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = path.join(dir, entry.name);
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
@ -96,19 +123,19 @@ async function listDocFiles(dir) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFrontmatter(content) {
|
function parseFrontmatter(content: string): { data: Record<string, string>; body: string } {
|
||||||
if (!content.startsWith("---")) {
|
if (!content.startsWith("---")) {
|
||||||
return { data: {}, body: content.trim() };
|
return { data: {} as Record<string, string>, body: content.trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return { data: {}, body: content.trim() };
|
return { data: {} as Record<string, string>, body: content.trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
const frontmatter = match[1];
|
const frontmatter = match[1];
|
||||||
const body = content.slice(match[0].length);
|
const body = content.slice(match[0].length);
|
||||||
const data = {};
|
const data: Record<string, string> = {};
|
||||||
|
|
||||||
for (const line of frontmatter.split("\n")) {
|
for (const line of frontmatter.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
@ -124,7 +151,7 @@ function parseFrontmatter(content) {
|
||||||
return { data, body: body.trim() };
|
return { data, body: body.trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSlug(relPath) {
|
function toSlug(relPath: string): string {
|
||||||
const withoutExt = stripExtension(relPath);
|
const withoutExt = stripExtension(relPath);
|
||||||
const normalized = withoutExt.replace(/\\/g, "/");
|
const normalized = withoutExt.replace(/\\/g, "/");
|
||||||
if (normalized.endsWith("/index")) {
|
if (normalized.endsWith("/index")) {
|
||||||
|
|
@ -133,18 +160,25 @@ function toSlug(relPath) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripExtension(value) {
|
function stripExtension(value: string): string {
|
||||||
return value.replace(/\.mdx?$/i, "");
|
return value.replace(/\.mdx?$/i, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function titleFromSlug(value) {
|
function titleFromSlug(value: string): string {
|
||||||
const cleaned = value.replace(/\.mdx?$/i, "").replace(/\\/g, "/");
|
const cleaned = value.replace(/\.mdx?$/i, "").replace(/\\/g, "/");
|
||||||
const parts = cleaned.split("/").filter(Boolean);
|
const parts = cleaned.split("/").filter(Boolean);
|
||||||
const last = parts[parts.length - 1] || "index";
|
const last = parts[parts.length - 1] || "index";
|
||||||
return formatSegment(last);
|
return formatSegment(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReferenceFile({ title, description, canonicalUrl, sourcePath, body }) {
|
function buildReferenceFile(args: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
canonicalUrl: string;
|
||||||
|
sourcePath: string;
|
||||||
|
body: string;
|
||||||
|
}): string {
|
||||||
|
const { title, description, canonicalUrl, sourcePath, body } = args;
|
||||||
const lines = [
|
const lines = [
|
||||||
`# ${title}`,
|
`# ${title}`,
|
||||||
"",
|
"",
|
||||||
|
|
@ -159,9 +193,9 @@ function buildReferenceFile({ title, description, canonicalUrl, sourcePath, body
|
||||||
return `${lines.join("\n").trim()}\n`;
|
return `${lines.join("\n").trim()}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReferenceMap(references) {
|
function buildReferenceMap(references: Reference[]): string {
|
||||||
const grouped = new Map();
|
const grouped = new Map<string, Reference[]>();
|
||||||
const groupRoots = new Set();
|
const groupRoots = new Set<string>();
|
||||||
|
|
||||||
for (const ref of references) {
|
for (const ref of references) {
|
||||||
const segments = (ref.slug || "").split("/").filter(Boolean);
|
const segments = (ref.slug || "").split("/").filter(Boolean);
|
||||||
|
|
@ -179,11 +213,15 @@ function buildReferenceMap(references) {
|
||||||
group = segments[0];
|
group = segments[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!grouped.has(group)) grouped.set(group, []);
|
const bucket = grouped.get(group);
|
||||||
grouped.get(group).push(ref);
|
if (bucket) {
|
||||||
|
bucket.push(ref);
|
||||||
|
} else {
|
||||||
|
grouped.set(group, [ref]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = [];
|
const lines: string[] = [];
|
||||||
const sortedGroups = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
|
const sortedGroups = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
for (const group of sortedGroups) {
|
for (const group of sortedGroups) {
|
||||||
|
|
@ -198,9 +236,9 @@ function buildReferenceMap(references) {
|
||||||
return lines.join("\n").trim();
|
return lines.join("\n").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSegment(value) {
|
function formatSegment(value: string): string {
|
||||||
if (!value) return "General";
|
if (!value) return "General";
|
||||||
const special = {
|
const special: Record<string, string> = {
|
||||||
ai: "AI",
|
ai: "AI",
|
||||||
sdks: "SDKs",
|
sdks: "SDKs",
|
||||||
};
|
};
|
||||||
|
|
@ -212,11 +250,11 @@ function formatSegment(value) {
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(value) {
|
function normalizePath(value: string): string {
|
||||||
return value.replace(/\\/g, "/");
|
return value.replace(/\\/g, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDocToMarkdown(body) {
|
function convertDocToMarkdown(body: string): string {
|
||||||
const { replaced, restore } = extractCodeBlocks(body ?? "");
|
const { replaced, restore } = extractCodeBlocks(body ?? "");
|
||||||
let text = replaced;
|
let text = replaced;
|
||||||
|
|
||||||
|
|
@ -260,8 +298,8 @@ function convertDocToMarkdown(body) {
|
||||||
return restore(text).trim();
|
return restore(text).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractCodeBlocks(input) {
|
function extractCodeBlocks(input: string): { replaced: string; restore: (value: string) => string } {
|
||||||
const blocks = [];
|
const blocks: string[] = [];
|
||||||
const replaced = input.replace(/```[\s\S]*?```/g, (match) => {
|
const replaced = input.replace(/```[\s\S]*?```/g, (match) => {
|
||||||
const token = `@@CODE_BLOCK_${blocks.length}@@`;
|
const token = `@@CODE_BLOCK_${blocks.length}@@`;
|
||||||
blocks.push(normalizeCodeBlock(match));
|
blocks.push(normalizeCodeBlock(match));
|
||||||
|
|
@ -274,7 +312,7 @@ function extractCodeBlocks(input) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCodeBlock(block) {
|
function normalizeCodeBlock(block: string): string {
|
||||||
const lines = block.split("\n");
|
const lines = block.split("\n");
|
||||||
if (lines.length < 2) return block.trim();
|
if (lines.length < 2) return block.trim();
|
||||||
|
|
||||||
|
|
@ -290,24 +328,25 @@ function normalizeCodeBlock(block) {
|
||||||
return [opening, ...normalizedContent, closing].join("\n");
|
return [opening, ...normalizedContent, closing].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripWrapperTags(input, tag) {
|
function stripWrapperTags(input: string, tag: string): string {
|
||||||
const open = new RegExp(`<${tag}[^>]*>`, "gi");
|
const open = new RegExp(`<${tag}[^>]*>`, "gi");
|
||||||
const close = new RegExp(`</${tag}>`, "gi");
|
const close = new RegExp(`</${tag}>`, "gi");
|
||||||
return input.replace(open, "\n").replace(close, "\n");
|
return input.replace(open, "\n").replace(close, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHeadingBlocks(input, tag, fallback, level) {
|
function formatHeadingBlocks(input: string, tag: string, fallback: string, level: number): string {
|
||||||
const heading = "#".repeat(level);
|
const heading = "#".repeat(level);
|
||||||
const withTitles = input.replace(
|
const withTitles = input.replace(
|
||||||
new RegExp(`<${tag}[^>]*title=(?:\"([^\"]+)\"|'([^']+)')[^>]*>`, "gi"),
|
new RegExp(`<${tag}[^>]*title=(?:\"([^\"]+)\"|'([^']+)')[^>]*>`, "gi"),
|
||||||
(_, doubleQuoted, singleQuoted) => `\n${heading} ${(doubleQuoted ?? singleQuoted ?? fallback).trim()}\n\n`,
|
(_, doubleQuoted: string | undefined, singleQuoted: string | undefined) =>
|
||||||
|
`\n${heading} ${(doubleQuoted ?? singleQuoted ?? fallback).trim()}\n\n`,
|
||||||
);
|
);
|
||||||
const withFallback = withTitles.replace(new RegExp(`<${tag}[^>]*>`, "gi"), `\n${heading} ${fallback}\n\n`);
|
const withFallback = withTitles.replace(new RegExp(`<${tag}[^>]*>`, "gi"), `\n${heading} ${fallback}\n\n`);
|
||||||
return withFallback.replace(new RegExp(`</${tag}>`, "gi"), "\n");
|
return withFallback.replace(new RegExp(`</${tag}>`, "gi"), "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCards(input) {
|
function formatCards(input: string): string {
|
||||||
return input.replace(/<Card([^>]*)>([\s\S]*?)<\/Card>/gi, (_, attrs, content) => {
|
return input.replace(/<Card([^>]*)>([\s\S]*?)<\/Card>/gi, (_, attrs: string, content: string) => {
|
||||||
const title = getAttributeValue(attrs, "title") ?? "Resource";
|
const title = getAttributeValue(attrs, "title") ?? "Resource";
|
||||||
const href = getAttributeValue(attrs, "href");
|
const href = getAttributeValue(attrs, "href");
|
||||||
const summary = collapseWhitespace(stripHtml(content));
|
const summary = collapseWhitespace(stripHtml(content));
|
||||||
|
|
@ -317,17 +356,17 @@ function formatCards(input) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCallouts(input, tag) {
|
function applyCallouts(input: string, tag: string): string {
|
||||||
const regex = new RegExp(`<${tag}[^>]*>([\s\S]*?)</${tag}>`, "gi");
|
const regex = new RegExp(`<${tag}[^>]*>([\s\S]*?)</${tag}>`, "gi");
|
||||||
return input.replace(regex, (_, content) => {
|
return input.replace(regex, (_, content: string) => {
|
||||||
const label = tag.toUpperCase();
|
const label = tag.toUpperCase();
|
||||||
const text = collapseWhitespace(stripHtml(content));
|
const text = collapseWhitespace(stripHtml(content));
|
||||||
return `\n> **${label}:** ${text}\n\n`;
|
return `\n> **${label}:** ${text}\n\n`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceImages(input) {
|
function replaceImages(input: string): string {
|
||||||
return input.replace(/<img\s+([^>]+?)\s*\/?>(?:\s*<\/img>)?/gi, (_, attrs) => {
|
return input.replace(/<img\s+([^>]+?)\s*\/?>(?:\s*<\/img>)?/gi, (_, attrs: string) => {
|
||||||
const src = getAttributeValue(attrs, "src") ?? "";
|
const src = getAttributeValue(attrs, "src") ?? "";
|
||||||
const alt = getAttributeValue(attrs, "alt") ?? "";
|
const alt = getAttributeValue(attrs, "alt") ?? "";
|
||||||
if (!src) return "";
|
if (!src) return "";
|
||||||
|
|
@ -336,29 +375,29 @@ function replaceImages(input) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttributeValue(attrs, name) {
|
function getAttributeValue(attrs: string, name: string): string | undefined {
|
||||||
const regex = new RegExp(`${name}=(?:\"([^\"]+)\"|'([^']+)')`, "i");
|
const regex = new RegExp(`${name}=(?:\"([^\"]+)\"|'([^']+)')`, "i");
|
||||||
const match = attrs.match(regex);
|
const match = attrs.match(regex);
|
||||||
if (!match) return undefined;
|
if (!match) return undefined;
|
||||||
return (match[1] ?? match[2] ?? "").trim();
|
return (match[1] ?? match[2] ?? "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripHtml(value) {
|
function stripHtml(value: string): string {
|
||||||
return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapseWhitespace(value) {
|
function collapseWhitespace(value: string): string {
|
||||||
return value.replace(/\s+/g, " ").trim();
|
return value.replace(/\s+/g, " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripIndentation(input) {
|
function stripIndentation(input: string): string {
|
||||||
return input
|
return input
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => line.replace(/^\t+/, "").replace(/^ {2,}/, ""))
|
.map((line) => line.replace(/^\t+/, "").replace(/^ {2,}/, ""))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error: unknown) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue