@terra-symposium/docs 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +3 -1
  2. package/bin/terra-docs.mjs +79 -22
  3. package/dist/lib/components/LocalDocsWorkspace.js +21 -21
  4. package/dist/lib/components/LocalDocsWorkspace.js.map +1 -1
  5. package/dist/lib/components/blocks.d.ts.map +1 -1
  6. package/dist/lib/components/blocks.js +53 -36
  7. package/dist/lib/components/blocks.js.map +1 -1
  8. package/dist/lib/components/navigation.js +1 -1
  9. package/dist/lib/components/navigation.js.map +1 -1
  10. package/dist/lib/components/quiz-demo-fixture.d.ts +23 -0
  11. package/dist/lib/components/quiz-demo-fixture.d.ts.map +1 -0
  12. package/dist/lib/components/quiz-demo-fixture.js +22 -0
  13. package/dist/lib/components/quiz-demo-fixture.js.map +1 -0
  14. package/dist/lib/components/quiz.d.ts +1 -1
  15. package/dist/lib/components/quiz.d.ts.map +1 -1
  16. package/dist/lib/components/quiz.js +51 -35
  17. package/dist/lib/components/quiz.js.map +1 -1
  18. package/dist/lib/components/terra/Workspace.js +12 -12
  19. package/dist/lib/components/terra/Workspace.js.map +1 -1
  20. package/dist/lib/components/terra/editor/ContentEditor.d.ts.map +1 -1
  21. package/dist/lib/components/terra/editor/ContentEditor.js +34 -36
  22. package/dist/lib/components/terra/editor/ContentEditor.js.map +1 -1
  23. package/dist/lib/components/terra/editor/_blocks.js +6 -6
  24. package/dist/lib/components/terra/editor/_blocks.js.map +1 -1
  25. package/dist/lib/components/terra/editor/docs/BlockContext.js +12 -12
  26. package/dist/lib/components/terra/editor/docs/BlockContext.js.map +1 -1
  27. package/dist/lib/components/terra/editor/docs/DocsAttachmentPicker.js +6 -6
  28. package/dist/lib/components/terra/editor/docs/DocsAttachmentPicker.js.map +1 -1
  29. package/dist/lib/components/terra/editor/docs/TerraToolbar.js +1 -1
  30. package/dist/lib/components/terra/editor/docs/TerraToolbar.js.map +1 -1
  31. package/dist/lib/components/terra/editor/docs/_CodeBlock.js +10 -10
  32. package/dist/lib/components/terra/editor/docs/_CodeBlock.js.map +1 -1
  33. package/dist/lib/components/terra/editor/terra-content.styles.d.ts +1 -1
  34. package/dist/lib/components/terra/editor/terra-content.styles.d.ts.map +1 -1
  35. package/dist/lib/components/terra/editor/terra-content.styles.js +1 -1
  36. package/dist/lib/components/terra/editor/terra-content.styles.js.map +1 -1
  37. package/dist/lib/components/terra/flow/Nodes.js +7 -7
  38. package/dist/lib/components/terra/flow/Nodes.js.map +1 -1
  39. package/dist/lib/components/terra/pages/docs/_attachments.d.ts.map +1 -1
  40. package/dist/lib/components/terra/pages/docs/_attachments.js +11 -11
  41. package/dist/lib/components/terra/pages/docs/_attachments.js.map +1 -1
  42. package/dist/lib/components/terra/pages/docs/_blocks.js +2 -2
  43. package/dist/lib/components/terra/pages/docs/_blocks.js.map +1 -1
  44. package/dist/lib/components/terra/pages/docs/_editable.d.ts.map +1 -1
  45. package/dist/lib/components/terra/pages/docs/_editable.js +2 -2
  46. package/dist/lib/components/terra/pages/docs/_editable.js.map +1 -1
  47. package/dist/lib/components/terra/pages/docs/_main.d.ts.map +1 -1
  48. package/dist/lib/components/terra/pages/docs/_main.js +48 -16
  49. package/dist/lib/components/terra/pages/docs/_main.js.map +1 -1
  50. package/dist/lib/components/terra/pages/docs/_sidebar.js +1 -1
  51. package/dist/lib/components/terra/pages/docs/_toc.js +1 -1
  52. package/dist/lib/components/terra/pages/docs/_toc.js.map +1 -1
  53. package/dist/lib/components/terra/pages/docs/_toolbar.js +1 -1
  54. package/dist/lib/components/terra/pages/docs/_toolbar.js.map +1 -1
  55. package/dist/lib/components/workspace-demo-fixture.d.ts.map +1 -1
  56. package/dist/lib/components/workspace-demo-fixture.js +51 -53
  57. package/dist/lib/components/workspace-demo-fixture.js.map +1 -1
  58. package/dist/lib/lib/host-runtime.js +3 -3
  59. package/dist/lib/lib/host-runtime.js.map +1 -1
  60. package/dist/lib/lib/host.d.ts +1 -1
  61. package/dist/lib/lib/host.d.ts.map +1 -1
  62. package/dist/lib/lib/markdown-model.d.ts.map +1 -1
  63. package/dist/lib/lib/markdown-model.js +106 -107
  64. package/dist/lib/lib/markdown-model.js.map +1 -1
  65. package/dist/lib/lib/markdown.js +4 -4
  66. package/dist/lib/lib/markdown.js.map +1 -1
  67. package/dist/lib/lib/terra/custom-components.d.ts +1 -1
  68. package/dist/lib/lib/terra/custom-components.d.ts.map +1 -1
  69. package/dist/lib/lib/terra/custom-components.js +2 -2
  70. package/dist/lib/lib/terra/custom-components.js.map +1 -1
  71. package/dist/lib/lib/terra/docs/editor-dom.d.ts.map +1 -1
  72. package/dist/lib/lib/terra/docs/editor-dom.js +2 -2
  73. package/dist/lib/lib/terra/docs/editor-dom.js.map +1 -1
  74. package/dist/lib/lib/terra/docs/workspace.d.ts +14 -12
  75. package/dist/lib/lib/terra/docs/workspace.d.ts.map +1 -1
  76. package/dist/lib/lib/terra/docs/workspace.js +5 -5
  77. package/dist/lib/lib/terra/docs/workspace.js.map +1 -1
  78. package/dist/lib/lib/terra/runtime/action-proxy.d.ts +1 -1
  79. package/dist/lib/lib/terra/runtime/action-proxy.d.ts.map +1 -1
  80. package/dist/lib/lib/terra/runtime/action-proxy.js +2 -2
  81. package/dist/lib/lib/terra/runtime/action-proxy.js.map +1 -1
  82. package/dist/lib/lib/terra/runtime/content/markdown.d.ts +0 -7
  83. package/dist/lib/lib/terra/runtime/content/markdown.d.ts.map +1 -1
  84. package/dist/lib/lib/terra/runtime/content/markdown.js +0 -7
  85. package/dist/lib/lib/terra/runtime/content/markdown.js.map +1 -1
  86. package/dist/lib/lib/terra/runtime/nodes.js +3 -3
  87. package/dist/lib/lib/terra/runtime/nodes.js.map +1 -1
  88. package/dist/lib/lib/terra/runtime/runtime.d.ts +1 -2
  89. package/dist/lib/lib/terra/runtime/runtime.d.ts.map +1 -1
  90. package/dist/lib/lib/terra/runtime/runtime.js +3 -8
  91. package/dist/lib/lib/terra/runtime/runtime.js.map +1 -1
  92. package/dist/lib/lib/terra/runtime/shared.d.ts.map +1 -1
  93. package/dist/lib/lib/terra/runtime/shared.js +6 -6
  94. package/dist/lib/lib/terra/runtime/shared.js.map +1 -1
  95. package/dist/lib/style.css +2 -2
  96. package/package.json +8 -12
  97. package/site/docs-content-plugin.mjs +4 -1
  98. package/site/src/components/QuizDemo.tsx +62 -0
  99. package/site/src/lib/catalog.ts +11 -1
  100. package/site/src/pages/[...slug].astro +3 -9
  101. package/site/src/pages/quiz-demo.astro +50 -0
  102. package/src/components/navigation.tsx +2 -2
  103. package/src/components/quiz-demo-fixture.ts +23 -0
  104. package/bin/generate-skills.mjs +0 -186
  105. package/site/src/components/SkillsExplorer.tsx +0 -236
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terra-symposium/docs",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Docs workspace and Markdown site generator.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,6 +16,7 @@
16
16
  "bin",
17
17
  "site",
18
18
  "src/components/navigation.tsx",
19
+ "src/components/quiz-demo-fixture.ts",
19
20
  "src/components/SiteLayout.astro",
20
21
  "src/components/NotFound.astro",
21
22
  "src/components/NotFound.tsx"
@@ -35,11 +36,7 @@
35
36
  "import": "./dist/lib/components/LocalDocsWorkspace.js"
36
37
  },
37
38
  "./site-layout.astro": "./src/components/SiteLayout.astro",
38
- "./not-found.astro": "./src/components/NotFound.astro",
39
- "./skills": {
40
- "types": "./site/src/components/SkillsExplorer.tsx",
41
- "import": "./site/src/components/SkillsExplorer.tsx"
42
- }
39
+ "./not-found.astro": "./src/components/NotFound.astro"
43
40
  },
44
41
  "bin": {
45
42
  "terra-docs": "bin/terra-docs.mjs"
@@ -47,8 +44,8 @@
47
44
  "scripts": {
48
45
  "dev": "terra-quality-dev -- vite --host 127.0.0.1 --open",
49
46
  "docs:dev": "pnpm build && node bin/terra-docs.mjs dev --title 'Docs'",
50
- "docs:build": "pnpm build && node bin/terra-docs.mjs build --title 'Docs'",
51
- "docs:preview": "pnpm docs:build && node bin/terra-docs.mjs preview --title 'Docs'",
47
+ "docs:build": "pnpm build && node bin/terra-docs.mjs build --title 'Docs' --out dist/docs",
48
+ "docs:preview": "pnpm docs:build && node bin/terra-docs.mjs preview --title 'Docs' --out dist/docs",
52
49
  "dev:library": "tsc --watch",
53
50
  "prebuild": "pnpm exec terra-quality error-boundaries --all",
54
51
  "build": "rm -rf dist/lib && tsc && tailwindcss --postcss tailwind.postcss.config.cjs -c tailwind.docs.config.mjs -i src/styles/style.css -o dist/lib/style.css --minify && tailwindcss --postcss tailwind.postcss.config.cjs -c tailwind.docs.config.mjs -i src/styles/workspace.css -o dist/lib/workspace.css --minify && node scripts/copy-runtime.mjs",
@@ -71,7 +68,6 @@
71
68
  "@playwright/test": "^1.64.1",
72
69
  "@terra-symposium/standards-quality": "2.1.3",
73
70
  "@terra-symposium/standards-reporting": "0.6.2",
74
- "@types/katex": "^0.16.8",
75
71
  "@types/node": "^24.19.2",
76
72
  "@types/react": "19.3.0",
77
73
  "@types/react-dom": "19.3.0",
@@ -80,6 +76,7 @@
80
76
  "eslint": "10.13.0",
81
77
  "globals": "^16.5.0",
82
78
  "husky": "^9.1.7",
79
+ "knip": "^6.44.1",
83
80
  "postcss": "8.5.31",
84
81
  "postcss-import": "15.1.0",
85
82
  "prettier": "3.9.10",
@@ -104,14 +101,13 @@
104
101
  "@lexical/rich-text": "^0.35.0",
105
102
  "@portabletext/editor": "7.9.0",
106
103
  "@radix-ui/react-popover": "^1.1.15",
107
- "@radix-ui/react-select": "2.4.1",
108
104
  "@terra-symposium/standards-error-boundaries": "0.1.2",
109
105
  "@terra-symposium/styling": "0.1.34",
110
- "bezier-easing": "2.1.0",
111
- "@xstate/react": "6.1.0",
112
106
  "@uiw/codemirror-theme-github": "^4.25.9",
107
+ "@xstate/react": "6.1.0",
113
108
  "@xyflow/react": "^12.3.6",
114
109
  "astro": "^7.4.2",
110
+ "bezier-easing": "2.1.0",
115
111
  "html-to-image": "1.11.11",
116
112
  "jszip": "^3.10.1",
117
113
  "katex": "^0.19.1",
@@ -8,7 +8,10 @@ function publicRoute(file, docsRoot, base) {
8
8
  .split(sep)
9
9
  .join('/')
10
10
  .replace(/\.(?:md|mdx)$/i, '');
11
- const route = path.replace(/(?:^|\/)index$/i, '').replace(/^\/+|\/+$/g, '');
11
+ const route = path
12
+ .replace(/(?:^|\/)SKILL$/i, '')
13
+ .replace(/(?:^|\/)index$/i, '')
14
+ .replace(/^\/+|\/+$/g, '');
12
15
  return `${base}${route ? `${route}/` : ''}`;
13
16
  }
14
17
 
@@ -0,0 +1,62 @@
1
+ import { DocsSiteIsland } from './DocsSiteIsland.js';
2
+ import type { DocsSection } from '../../../dist/lib/lib/workspace-types.js';
3
+ import type { DocsQuizConfig, DocsQuizGenerationInput } from '../../../dist/lib/components/quiz.js';
4
+ import { quizDemoLeaf, quizDemoResponse } from '../../../src/components/quiz-demo-fixture.js';
5
+
6
+ const markdown = quizDemoLeaf.markdown;
7
+ const visibleLeafText = markdown.split('\n\n', 1)[0] ?? markdown;
8
+
9
+ const sections: DocsSection[] = [
10
+ {
11
+ id: 'course',
12
+ title: 'Course',
13
+ groups: [
14
+ {
15
+ id: 'lessons',
16
+ title: 'Lessons',
17
+ leaves: [
18
+ {
19
+ id: quizDemoLeaf.id,
20
+ title: quizDemoLeaf.title,
21
+ href: '/quiz-demo/',
22
+ description: 'The routing leaf shared with the quiz browser tests.',
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ ];
29
+
30
+ function genDemoQuiz(input: DocsQuizGenerationInput): Promise<unknown> {
31
+ return Promise.resolve({
32
+ title: `${quizDemoResponse.title} · sample`,
33
+ questions: Array.from({ length: input.count }, (_, i) => {
34
+ const type = input.types[i % input.types.length];
35
+ const question =
36
+ quizDemoResponse.questions.find((candidate) => candidate.type === type) ??
37
+ quizDemoResponse.questions[0];
38
+ return { ...question };
39
+ }),
40
+ });
41
+ }
42
+
43
+ const quiz: DocsQuizConfig = { toolbar: true, generate: genDemoQuiz };
44
+
45
+ /** A package-safe interactive demo for the host-configurable leaf quiz flow. */
46
+ export function QuizDemo() {
47
+ return (
48
+ <DocsSiteIsland
49
+ title="Docs quiz demo"
50
+ description="The published Docs workspace with sample leaf quiz generation."
51
+ hue={145}
52
+ sections={sections}
53
+ currentPath="/quiz-demo/"
54
+ rootPath="/quiz-demo/"
55
+ staticContent
56
+ markdown={markdown}
57
+ renderContent={<p className="leading-7">{visibleLeafText}</p>}
58
+ quiz={quiz}
59
+ searchBase="/"
60
+ />
61
+ );
62
+ }
@@ -72,10 +72,17 @@ const sources = import.meta.glob<string>('/docs/**/*.{md,mdx}', {
72
72
  eager: true,
73
73
  });
74
74
  const base = import.meta.env.BASE_URL;
75
+ const includeAgents = import.meta.env.PUBLIC_TERRA_DOCS_INCLUDE_AGENTS !== 'false';
75
76
  const routes = new Map<string, string>();
76
77
  /** Visible Docs pages sorted by configured sidebar order and source path. */
77
78
  export const sitePages: SitePage[] = Object.entries(modules)
79
+ .filter(([file]) => includeAgents || !file.startsWith('/docs/agents/'))
78
80
  .map(([file, module]) => {
81
+ const relativeFile = file.replace(/^\/docs\//, '');
82
+ const isRootIndex = ['index.md', 'index.mdx'].includes(relativeFile.toLowerCase());
83
+ if (!isRootIndex && !relativeFile.includes('/')) {
84
+ throw new Error(`Markdown pages must be grouped in folders directly under docs/: ${file}`);
85
+ }
79
86
  const slug = routeForFile(file);
80
87
  const previous = routes.get(slug);
81
88
  if (previous) throw new Error(`Duplicate Docs route "${slug || '/'}": ${previous} and ${file}`);
@@ -131,7 +138,10 @@ function titleFromSource(source: string, slug: string): string {
131
138
  */
132
139
  function routeForFile(file: string): string {
133
140
  const relative = file.replace(/^\/?docs\//, '').replace(/\.(?:md|mdx)$/i, '');
134
- return relative.replace(/(?:^|\/)index$/i, '').replace(G_CLASS_2(), '');
141
+ return relative
142
+ .replace(/(?:^|\/)SKILL$/i, '')
143
+ .replace(/(?:^|\/)index$/i, '')
144
+ .replace(G_CLASS_2(), '');
135
145
  }
136
146
 
137
147
  /**
@@ -15,11 +15,10 @@ export function getStaticPaths() {
15
15
 
16
16
  const { slug = '' } = Astro.props;
17
17
  const page = pageForSlug(slug);
18
- const PageContent = page?.content;
19
18
  const currentPath = pageHref(slug, base);
20
19
  ---
21
20
 
22
- {page && PageContent ? (
21
+ {page ? (
23
22
  <html lang="en">
24
23
  <head>
25
24
  <meta charset="utf-8" />
@@ -37,14 +36,9 @@ const currentPath = pageHref(slug, base);
37
36
  nodes={siteNavigation}
38
37
  currentPath={currentPath}
39
38
  rootPath={pageHref('', base)}
39
+ markdown={page.markdown}
40
40
  searchBase={base}
41
- >
42
- <div class="terra-docs-page-content"><PageContent /></div>
43
- </DocsSiteLayout>
41
+ />
44
42
  </body>
45
43
  </html>
46
44
  ) : null}
47
-
48
- <style is:global>
49
- .terra-docs-page-content { max-width: 100%; }
50
- </style>
@@ -0,0 +1,50 @@
1
+ ---
2
+ import { QuizDemo } from '../components/QuizDemo.js';
3
+ import '@terra-symposium/docs/style.css';
4
+ ---
5
+ <!doctype html>
6
+ <html lang="en" class="light">
7
+ <head>
8
+ <meta charset="utf-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+ <meta name="description" content="Try the shared Docs leaf quiz flow with the Playwright workspace fixture." />
11
+ <title>Leaf quiz demo · Docs</title>
12
+ </head>
13
+ <body>
14
+ <header class="demo-intro">
15
+ <p>Docs component demo</p>
16
+ <h1>Generate a quiz from a leaf</h1>
17
+ <p>Select a leaf, then choose <strong>Generate quiz</strong>. This fixture returns clearly marked sample questions and never sends content to an AI service.</p>
18
+ </header>
19
+ <QuizDemo client:load />
20
+ </body>
21
+ </html>
22
+
23
+ <style>
24
+ .demo-intro {
25
+ margin-left: 22rem;
26
+ padding: 1.25rem clamp(1rem, 4vw, 3rem);
27
+ background: #f7f4fb;
28
+ color: #24172f;
29
+ }
30
+ .demo-intro > p:first-child {
31
+ margin: 0 0 0.35rem;
32
+ font-size: 0.75rem;
33
+ font-weight: 700;
34
+ letter-spacing: 0.08em;
35
+ text-transform: uppercase;
36
+ }
37
+ .demo-intro h1 {
38
+ margin: 0;
39
+ font-size: clamp(1.25rem, 2.5vw, 1.8rem);
40
+ }
41
+ .demo-intro > p:last-child {
42
+ margin: 0.35rem 0 0;
43
+ font-size: 0.9rem;
44
+ }
45
+ @media (max-width: 767px) {
46
+ .demo-intro {
47
+ margin-left: 0;
48
+ }
49
+ }
50
+ </style>
@@ -27,8 +27,8 @@ export function DocsNavigation({
27
27
  const style = { '--docs-accent': `rgb(${accent.light[8]})` } as CSSProperties;
28
28
  const render = (items: DocsNavNode[]): JSX.Element => (
29
29
  <ul className="terra-docs-nav-list">
30
- {items.map((node, index) => (
31
- <li key={`${node.href ?? node.title}-${index}`}>
30
+ {items.map((node, i) => (
31
+ <li key={`${node.href ?? node.title}-${i}`}>
32
32
  {node.href ? (
33
33
  <a href={node.href} aria-current={node.href === currentPath ? 'page' : undefined}>
34
34
  {node.title}
@@ -0,0 +1,23 @@
1
+ /** Shared quiz leaf data used by the public demo and its Playwright fixture. */
2
+ export const quizDemoLeaf = {
3
+ id: 'quiz-leaf',
4
+ path: 'content/course/quiz-leaf/index.md',
5
+ title: 'Course Leaf',
6
+ markdown:
7
+ 'Leaf source: routing sends packets between networks.\n\n:::terra-docs quiz\n_type: quiz\n:::',
8
+ };
9
+
10
+ /** Stable sample response for review, run, and score coverage. */
11
+ export const quizDemoResponse = {
12
+ title: 'Routing quiz',
13
+ questions: [
14
+ {
15
+ type: 'multipleChoice',
16
+ prompt: 'What does a router do?',
17
+ opts: ['Routes packets', 'Stores files'],
18
+ answer: 'Routes packets',
19
+ explanation: 'It forwards packets between networks.',
20
+ },
21
+ { type: 'keyword', prompt: 'What does it forward?', answers: ['packets'] },
22
+ ],
23
+ } as const;
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /*//////////////////////////////////////////////////
4
- IMPORTS
5
- //////////////////////////////////////////////////*/
6
-
7
- // API
8
- import { dirname, join, relative, resolve, sep } from 'node:path';
9
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
10
- import { parse } from 'yaml';
11
-
12
- /*//////////////////////////////////////////////////
13
- CONFIG
14
- //////////////////////////////////////////////////*/
15
-
16
- const ignoredDirs = new Set([
17
- '.astro',
18
- '.cache',
19
- '.git',
20
- '.next',
21
- '.nuxt',
22
- '.output',
23
- '.svelte-kit',
24
- '.turbo',
25
- '.vite',
26
- '.vercel',
27
- '.wrangler',
28
- 'build',
29
- 'coverage',
30
- 'dist',
31
- 'docs-dist',
32
- 'node_modules',
33
- 'target',
34
- 'vendor',
35
- ]);
36
-
37
- /*//////////////////////////////////////////////////
38
- HELPERS
39
- //////////////////////////////////////////////////*/
40
-
41
- /**
42
- * Finds skill instruction files beneath a directory, skipping generated and dependency folders.
43
- *
44
- * @param dir Directory whose descendants should be searched.
45
- * @param files Accumulator shared by recursive calls.
46
- *
47
- * @return Paths to the discovered `SKILL.md` files.
48
- */
49
- async function findSkillFiles(dir, files = []) {
50
- const entries = await readdir(dir, { withFileTypes: true });
51
- for (const entry of entries) {
52
- if (entry.isDirectory() && ignoredDirs.has(entry.name)) continue;
53
- const path = join(dir, entry.name);
54
- if (entry.isDirectory()) await findSkillFiles(path, files);
55
- else if (entry.isFile() && entry.name === 'SKILL.md') files.push(path);
56
- }
57
- return files;
58
- }
59
-
60
- /**
61
- * Separates a skill document's Markdown body from its optional YAML frontmatter.
62
- *
63
- * @param source Complete skill document contents.
64
- *
65
- * @return Parsed metadata and body, plus a validation issue when YAML parsing fails.
66
- */
67
- function splitFrontmatter(source) {
68
- const match = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(source);
69
- if (!match) return { body: source.trim(), metadata: {}, metadataIssue: undefined };
70
- try {
71
- const metadata = parse(match[1]);
72
- return {
73
- body: source.slice(match[0].length).trim(),
74
- metadata:
75
- metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? metadata : {},
76
- metadataIssue: undefined,
77
- };
78
- } catch (err) {
79
- return {
80
- body: source.slice(match[0].length).trim(),
81
- metadata: {},
82
- metadataIssue: err instanceof Error ? err.message : String(err),
83
- };
84
- }
85
- }
86
-
87
- /**
88
- * Resolves a skill's display scope and directory from its repository-relative path.
89
- *
90
- * @param root Repository root used to make the skill path relative.
91
- * @param path Absolute path of the skill instruction file.
92
- *
93
- * @return Directory and scope labels for the generated index entry.
94
- */
95
- function skillLocation(root, path) {
96
- const segments = relative(root, path).split(sep);
97
- const skillRootIndex = segments.findIndex(
98
- (segment, i) => (segment === '.agents' || segment === '.codex') && segments[i + 1] === 'skills',
99
- );
100
- if (skillRootIndex >= 0) {
101
- const scope = segments.slice(0, skillRootIndex).join('/') || 'repository';
102
- const dir = segments.slice(skillRootIndex + 2, -1).join('/') || 'root';
103
- return { directory: dir, scope };
104
- }
105
- return { directory: segments.slice(0, -1).join('/') || 'root', scope: 'repository' };
106
- }
107
-
108
- /**
109
- * Derives a fallback description from the first prose paragraph in a skill document.
110
- *
111
- * @param body Markdown body with frontmatter removed.
112
- *
113
- * @return The first non-heading paragraph, or an empty string when none exists.
114
- */
115
- function fallbackDescription(body) {
116
- return (
117
- body
118
- .replace(/^\s*#.+\n/, '')
119
- .split(/\n\s*\n/)
120
- .map((paragraph) => paragraph.replace(/\s+/g, ' ').trim())
121
- .find((paragraph) => paragraph && !paragraph.startsWith('#')) ?? ''
122
- );
123
- }
124
-
125
- /**
126
- * Converts a directory or skill name into a title-style label.
127
- *
128
- * @param value Slug containing words separated by hyphens or underscores.
129
- *
130
- * @return A human-readable label with each word capitalized.
131
- */
132
- function humanize(value) {
133
- return value.replace(/[-_]+/g, ' ').replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
134
- }
135
-
136
- /**
137
- * Builds a sorted index of repository skills and writes it as JSON.
138
- *
139
- * @param repoRoot Repository root whose `.agents` and `.codex` skill trees are scanned.
140
- * @param outputPath Destination file for the generated skill index.
141
- *
142
- * @return The indexed skills, including their parsed metadata and Markdown bodies.
143
- */
144
- export async function generateSkillsIndex(repoRoot, outputPath) {
145
- const root = resolve(repoRoot);
146
- const files = await findSkillFiles(root);
147
- const skills = await Promise.all(
148
- files.map(async (path) => {
149
- const source = await readFile(path, 'utf8');
150
- const { body, metadata, metadataIssue } = splitFrontmatter(source);
151
- const location = skillLocation(root, path);
152
- const dirName = location.directory.split('/').at(-1) ?? 'skill';
153
- const name = typeof metadata.name === 'string' ? metadata.name.trim() : dirName;
154
- const title = /^#\s+(.+?)\s*#*\s*$/m.exec(body)?.[1]?.trim() ?? humanize(name);
155
- const description =
156
- typeof metadata.description === 'string'
157
- ? metadata.description.trim()
158
- : fallbackDescription(body);
159
- return {
160
- body,
161
- description,
162
- directory: location.directory,
163
- metadata,
164
- ...(metadataIssue ? { metadataIssue } : {}),
165
- name,
166
- scope: location.scope,
167
- sourcePath: relative(root, path).split(sep).join('/'),
168
- title,
169
- };
170
- }),
171
- );
172
- skills.sort((first, second) =>
173
- `${first.scope}/${first.directory}/${first.name}`.localeCompare(
174
- `${second.scope}/${second.directory}/${second.name}`,
175
- ),
176
- );
177
- await mkdir(dirname(outputPath), { recursive: true });
178
- await writeFile(outputPath, `${JSON.stringify(skills, null, 2)}\n`);
179
- return skills;
180
- }
181
- if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) {
182
- const root = resolve(process.argv[2] ?? process.cwd());
183
- const outputPath = resolve(process.argv[3] ?? join(root, 'dist/cache/skills.generated.json'));
184
- const skills = await generateSkillsIndex(root, outputPath);
185
- console.log(`Indexed ${skills.length} skills from ${root}`);
186
- }