#!/usr/bin/env node /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { createHash } from 'node:crypto'; import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); const sourcesDir = join(repositoryRoot, 'packages', 'runtime', 'resources', 'bundled-skills'); const outFile = join( repositoryRoot, 'packages', 'runtime', 'src', 'bundled-skill-catalog.generated.ts', ); const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$/; const LEGACY_CONTENT_SHA256_BY_ID = { 'computer-use': [ 'sha256:419088b2f8a0b12061b4811323abc381869ebe8fccbfc8f2bdfc96ff37a1e45b', 'sha256:8e4404349be4e5493fcf13981624ed55198c0670a794fbf88e2bad81ddb79f6c', ], }; export function readBundledSkillSources(dir = sourcesDir) { const ids = readdirSync(dir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && ID_RE.test(entry.name)) .map((entry) => entry.name) .sort((a, b) => a.localeCompare(b)); const sourceIds = new Set(ids); const orphanedLegacyIds = Object.keys(LEGACY_CONTENT_SHA256_BY_ID).filter( (id) => !sourceIds.has(id), ); if (orphanedLegacyIds.length > 0) { throw new Error( `legacy content hashes reference missing bundled skills: ${orphanedLegacyIds.join(', ')}`, ); } return ids.map((id) => { const body = readFileSync(join(dir, id, 'SKILL.md'), 'utf8'); if (!body.startsWith('---')) { throw new Error(`bundled skill ${id} is missing YAML front-matter`); } return { id, body, sourceName: 'maka-bundled', sourceVersion: '1', contentSha256: `sha256:${createHash('sha256').update(body).digest('hex')}`, legacyContentSha256: LEGACY_CONTENT_SHA256_BY_ID[id] ?? [], }; }); } export function renderGeneratedModule(skills) { const entries = skills .map( (skill) => ` { id: ${JSON.stringify(skill.id)}, body: ${JSON.stringify(skill.body)}, sourceName: ${JSON.stringify(skill.sourceName)}, sourceVersion: ${JSON.stringify(skill.sourceVersion)}, contentSha256: ${JSON.stringify(skill.contentSha256)}, legacyContentSha256: ${JSON.stringify(skill.legacyContentSha256)} },`, ) .join('\n'); return `// @generated by scripts/gen-bundled-skill-catalog.mjs — do not edit by hand. // Source of truth: packages/runtime/resources/bundled-skills//SKILL.md // Regenerate: npm run generate:bundled-skills export interface BundledSkillSource { id: string; body: string; sourceName: string; sourceVersion: string; contentSha256: string; legacyContentSha256: readonly string[]; } // biome-ignore format: generated catalog keeps one reviewable source per line. export const BUNDLED_SKILL_CATALOG: ReadonlyArray = [ ${entries} ]; `; } export function expectedBundledSkillCatalog() { return renderGeneratedModule(readBundledSkillSources()); } function main() { const expected = expectedBundledSkillCatalog(); if (process.argv.includes('--check')) { if (readFileSync(outFile, 'utf8') !== expected) { throw new Error('Bundled Skill catalog is stale; run npm run generate:bundled-skills'); } console.log('[gen] bundled Skill catalog is current'); return; } writeFileSync(outFile, expected, 'utf8'); console.log(`[gen] wrote ${readBundledSkillSources().length} bundled skills -> ${outFile}`); } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();