mdat 3.4.0 → 3.5.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.
package/dist/bin/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as version, a as expand, b as setLogger$1, d as templates_default, f as ensureArray, l as createReadme, r as collapse, s as strip, t as check, u as createReadmeInteractive, x as name, y as log } from "../lib-D8QaJZ-l.js";
2
+ import { S as version, a as expand, b as setLogger$1, d as templates_default, f as ensureArray, l as createReadme, r as collapse, s as strip, t as check, u as createReadmeInteractive, x as name, y as log } from "../lib-D3Djaa1D.js";
3
3
  import { createLogger } from "lognow";
4
4
  import picocolors from "picocolors";
5
5
  import prettyMilliseconds from "pretty-ms";
package/dist/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as getReadmeMetadata, a as expand, b as setLogger, c as stripString, g as getContextMetadata, h as mergeConfig, i as collapseString, l as createReadme, m as loadConfig, n as checkString, o as expandString, p as defineConfig, r as collapse, s as strip, t as check, u as createReadmeInteractive, v as resetMetadataCaches } from "../lib-D8QaJZ-l.js";
1
+ import { _ as getReadmeMetadata, a as expand, b as setLogger, c as stripString, g as getContextMetadata, h as mergeConfig, i as collapseString, l as createReadme, m as loadConfig, n as checkString, o as expandString, p as defineConfig, r as collapse, s as strip, t as check, u as createReadmeInteractive, v as resetMetadataCaches } from "../lib-D3Djaa1D.js";
2
2
 
3
3
  export { check, checkString, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
@@ -17,6 +17,7 @@ import { isFile, isFileSync } from "path-type";
17
17
  import { promisify } from "node:util";
18
18
  import { brotliCompress, gzip } from "node:zlib";
19
19
  import prettyBytes from "pretty-bytes";
20
+ import { matter } from "gray-matter-es";
20
21
  import { toc } from "mdast-util-toc";
21
22
  import { VFile } from "vfile";
22
23
  import { Configuration } from "unified-engine";
@@ -25,7 +26,7 @@ import { confirm, group, intro, note, outro, select } from "@clack/prompts";
25
26
 
26
27
  //#region package.json
27
28
  var name = "mdat";
28
- var version = "3.4.0";
29
+ var version = "3.5.0";
29
30
 
30
31
  //#endregion
31
32
  //#region src/lib/deep-merge-defined.ts
@@ -683,7 +684,7 @@ const listFormatter = new Intl.ListFormat("en", {
683
684
  style: "long",
684
685
  type: "conjunction"
685
686
  });
686
- function codeBlock(command) {
687
+ function codeBlock$1(command) {
687
688
  return [
688
689
  "```sh",
689
690
  command,
@@ -698,17 +699,17 @@ function getCliLines(name, homebrewFormula) {
698
699
  return [
699
700
  "Run it once without installing:",
700
701
  "",
701
- ...codeBlock(`npx ${name}`),
702
+ ...codeBlock$1(`npx ${name}`),
702
703
  ...homebrewFormula === void 0 ? [] : [
703
704
  "",
704
705
  "Or install it globally with Homebrew:",
705
706
  "",
706
- ...codeBlock(`brew install ${homebrewFormula}`)
707
+ ...codeBlock$1(`brew install ${homebrewFormula}`)
707
708
  ],
708
709
  "",
709
710
  "Or install it globally with npm:",
710
711
  "",
711
- ...codeBlock(`npm install --global ${name}`)
712
+ ...codeBlock$1(`npm install --global ${name}`)
712
713
  ];
713
714
  }
714
715
  /**
@@ -720,12 +721,12 @@ function getLibraryLines(name, bin, hasTypes, dev) {
720
721
  if (bin === void 0 || bin.length === 0) return [
721
722
  `${lead}:`,
722
723
  "",
723
- ...codeBlock(command)
724
+ ...codeBlock$1(command)
724
725
  ];
725
726
  return [
726
727
  `${lead} to import the ${hasTypes ? "TypeScript API" : "API"}. This also puts the ${listFormatter.format(bin.map((binName) => `\`${binName}\``))} ${bin.length === 1 ? "CLI" : "CLIs"} on your project's path:`,
727
728
  "",
728
- ...codeBlock(command)
729
+ ...codeBlock$1(command)
729
730
  ];
730
731
  }
731
732
  var install_default = { install: { async content(options) {
@@ -748,7 +749,7 @@ var install_default = { install: { async content(options) {
748
749
  if (hasCli && isLibrary) return [
749
750
  heading,
750
751
  "",
751
- "Pick the option that matches how you plan to use it.",
752
+ `There are several ways to install ${name} depending on how you're planning to use it:`,
752
753
  "",
753
754
  `${subheading} CLI`,
754
755
  "",
@@ -770,10 +771,17 @@ var install_default = { install: { async content(options) {
770
771
  return [
771
772
  heading,
772
773
  "",
773
- ...codeBlock(command)
774
+ ...codeBlock$1(command)
774
775
  ].join("\n");
775
776
  } } };
776
777
 
778
+ //#endregion
779
+ //#region src/lib/readme/rules/installation.ts
780
+ /**
781
+ * Simple alias for `install`
782
+ */
783
+ var installation_default = { installation: getSoleRule(install_default) };
784
+
777
785
  //#endregion
778
786
  //#region src/lib/readme/rules/utilities/size/size-report.ts
779
787
  const brotliCompressAsync = promisify(brotliCompress);
@@ -899,6 +907,118 @@ var size_table_default = { "size-table": { async content(options) {
899
907
  });
900
908
  } } };
901
909
 
910
+ //#endregion
911
+ //#region src/lib/readme/rules/skills.ts
912
+ const SKILLS_DIRECTORY = "skills";
913
+ const SKILL_FILE_NAME = "SKILL.md";
914
+ const GITHUB_REPOSITORY_REGEX = /^https:\/\/github\.com\/(?<slug>[^\/]+\/[^\/]+)$/v;
915
+ const WHITESPACE_REGEX = /\s+/gv;
916
+ const skillFrontmatterSchema = z.object({
917
+ description: z.string().trim().min(1),
918
+ name: z.string().trim().min(1)
919
+ });
920
+ function codeBlock(command) {
921
+ return [
922
+ "```sh",
923
+ command,
924
+ "```"
925
+ ];
926
+ }
927
+ /**
928
+ * Find Agent Skills in the project's `skills` directory, where each skill is a
929
+ * subdirectory containing a `SKILL.md` file with `name` and `description`
930
+ * frontmatter.
931
+ *
932
+ * @throws {Error} If a `SKILL.md` file has missing or invalid frontmatter
933
+ */
934
+ async function findSkills(projectDirectory) {
935
+ const skillsDirectory = path.join(projectDirectory, SKILLS_DIRECTORY);
936
+ let entries;
937
+ try {
938
+ entries = await fs.readdir(skillsDirectory, { withFileTypes: true });
939
+ } catch (error) {
940
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
941
+ throw error;
942
+ }
943
+ const skills = [];
944
+ for (const entry of entries) {
945
+ if (!entry.isDirectory()) continue;
946
+ const skillFilePath = path.join(skillsDirectory, entry.name, SKILL_FILE_NAME);
947
+ let skillFile;
948
+ try {
949
+ skillFile = await fs.readFile(skillFilePath, "utf8");
950
+ } catch (error) {
951
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
952
+ throw error;
953
+ }
954
+ const frontmatter = skillFrontmatterSchema.safeParse(matter(skillFile).data);
955
+ if (!frontmatter.success) throw new Error(`Skill at "${path.relative(projectDirectory, skillFilePath)}" needs "name" and "description" strings in its frontmatter`);
956
+ skills.push({
957
+ ...frontmatter.data,
958
+ filePath: `${SKILLS_DIRECTORY}/${entry.name}/${SKILL_FILE_NAME}`
959
+ });
960
+ }
961
+ return skills.toSorted((a, b) => a.name.localeCompare(b.name));
962
+ }
963
+ /**
964
+ * The argument identifying a repository to the `skills add` command, which
965
+ * accepts an `owner/repo` shorthand for GitHub and full URLs for other hosts.
966
+ */
967
+ function getSkillsCliSource(repoUrl) {
968
+ return GITHUB_REPOSITORY_REGEX.exec(repoUrl)?.groups?.slug ?? repoUrl;
969
+ }
970
+ var skills_default = { skills: { async content(options) {
971
+ const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
972
+ const skills = await findSkills(process.cwd());
973
+ if (skills.length === 0) throw new Error(`Could not find any skills, expected at least one "${SKILLS_DIRECTORY}/<name>/${SKILL_FILE_NAME}" file in the project`);
974
+ const { isPublicNpmPackage, name, repositoryUrl } = await getReadmeMetadata();
975
+ if (name === void 0) throw new Error("Could not find project name");
976
+ const headingLevel = validOptions?.headingLevel ?? 2;
977
+ const subheading = getHeadingPrefix(headingLevel + 1);
978
+ const skillNoun = skills.length === 1 ? "skill" : "skills";
979
+ const skillPronoun = skills.length === 1 ? "it" : "them";
980
+ const introLines = [
981
+ `${getHeadingPrefix(headingLevel)} Agent skills`,
982
+ "",
983
+ `This project includes ${skills.length === 1 ? "an [Agent Skill](https://agentskills.io)" : "[Agent Skills](https://agentskills.io)"} that ${skills.length === 1 ? "teaches" : "teach"} coding agents like Claude Code and Codex how to work with ${name}:`,
984
+ "",
985
+ ...skills.map((skill) => `- **[\`${skill.name}\`](${skill.filePath})**: ${skill.description.replaceAll(WHITESPACE_REGEX, " ")}`)
986
+ ];
987
+ if (!isPublicNpmPackage) {
988
+ if (repositoryUrl === void 0) throw new Error("Skill install instructions require either a publishable npm package or a repository URL in the project metadata");
989
+ return [
990
+ ...introLines,
991
+ "",
992
+ `Install ${skillPronoun} from the repository with the [\`skills\`](https://github.com/vercel-labs/skills) CLI:`,
993
+ "",
994
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`)
995
+ ].join("\n");
996
+ }
997
+ return [
998
+ ...introLines,
999
+ "",
1000
+ `The ${skillNoun} ${skills.length === 1 ? "is" : "are"} published in the \`${SKILLS_DIRECTORY}\` directory of the \`${name}\` package. Nothing is added to your project until you install ${skillPronoun} with one of the tools below.`,
1001
+ "",
1002
+ `${subheading} Sync from the installed package (recommended)`,
1003
+ "",
1004
+ `With \`${name}\` installed as a project dependency, the [\`skills\`](https://github.com/vercel-labs/skills) CLI finds skills bundled in your dependencies and copies them into your project's agent skill directories, so they match the version of \`${name}\` you have installed:`,
1005
+ "",
1006
+ ...codeBlock("npx skills experimental_sync"),
1007
+ "",
1008
+ `Run the command again after upgrading \`${name}\` to refresh the copies. The \`experimental_sync\` command is experimental and its behavior may change.`,
1009
+ ...repositoryUrl === void 0 ? [] : [
1010
+ "",
1011
+ `${subheading} Install from the repository`,
1012
+ "",
1013
+ `If \`${name}\` is not a dependency of your project, for example because you use a global installation, install the ${skillNoun} from the repository instead:`,
1014
+ "",
1015
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`),
1016
+ "",
1017
+ `${skills.length === 1 ? "A skill" : "Skills"} installed this way ${skills.length === 1 ? "follows" : "follow"} the repository's default branch rather than your installed version of \`${name}\`.`
1018
+ ]
1019
+ ].join("\n");
1020
+ } } };
1021
+
902
1022
  //#endregion
903
1023
  //#region src/lib/readme/rules/table-of-contents.ts
904
1024
  var table_of_contents_default = { "table-of-contents": {
@@ -951,10 +1071,12 @@ var rules_default = {
951
1071
  ...footer_default,
952
1072
  ...header_default,
953
1073
  ...install_default,
1074
+ ...installation_default,
954
1075
  ...license_default,
955
1076
  ...short_description_default,
956
1077
  ...size_default,
957
1078
  ...size_table_default,
1079
+ ...skills_default,
958
1080
  ...table_of_contents_default,
959
1081
  ...title_default,
960
1082
  ...toc_default
@@ -1238,19 +1360,19 @@ var mdat_readme_default = "<!-- title -->\n\n<!-- banner -->\n\n<!-- badges -->\
1238
1360
 
1239
1361
  //#endregion
1240
1362
  //#region src/lib/readme/templates/standard-readme-basic-compound.md?raw
1241
- var standard_readme_basic_compound_default = "<!-- header -->\n\n## Install\n\n```sh\n# Code block illustrating how to install.\n```\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ src: \"path/to/example.ts\" }) --> comment as well.\n```\n\n<!-- footer -->\n";
1363
+ var standard_readme_basic_compound_default = "<!-- header -->\n\n<!-- install({ headingLevel: 2 }) -->\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ file: \"path/to/example.ts\" }) --> comment as well.\n```\n\n<!-- footer -->\n";
1242
1364
 
1243
1365
  //#endregion
1244
1366
  //#region src/lib/readme/templates/standard-readme-basic.md?raw
1245
- var standard_readme_basic_default = "<!-- title -->\n\n<!-- short-description -->\n\n<!-- table-of-contents -->\n\n## Install\n\n```sh\n# Code block illustrating how to install.\n```\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ src: \"path/to/example.ts\" })--> comment as well.\n```\n\n<!-- contributing -->\n\n<!-- license -->\n";
1367
+ var standard_readme_basic_default = "<!-- title -->\n\n<!-- short-description -->\n\n<!-- table-of-contents -->\n\n<!-- install({ headingLevel: 2 }) -->\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ file: \"path/to/example.ts\" }) --> comment as well.\n```\n\n<!-- contributing -->\n\n<!-- license -->\n";
1246
1368
 
1247
1369
  //#endregion
1248
1370
  //#region src/lib/readme/templates/standard-readme-full-compound.md?raw
1249
- var standard_readme_full_compound_default = "<!-- header -->\n\n_Long description goes here._\n\n_No heading. Cover the main reasons for building the repository. This should describe your module in broad terms, generally in just a few paragraphs; more detail of the module's routines or methods, lengthy code examples, or other in-depth material should be given in subsequent sections._\n\n<!-- table-of-contents -->\n\n## Background\n\n_Cover motivation. Cover abstract dependencies. Cover intellectual provenance: A See Also section is also fitting._\n\n### See also\n\n## Install\n\n```sh\n# Code block illustrating how to install.\n```\n\n### Dependencies\n\n_Required if there are unusual dependencies or dependencies that must be manually installed._\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ src: \"path/to/example.ts\" })--> comment as well.\n```\n\n```ts\n// If importable, code block indicating both import functionality and usage.\n```\n\n### CLI\n\n```ts\n// If CLI compatible, code block indicating common usage.\n```\n\n## _Extra Sections (Rename)_\n\n## Security\n\n## API\n\n_Describe exported functions and objects._\n\n- _Describe signatures, return types, callbacks, and events._\n- _Cover types covered where not obvious._\n- _Describe caveats._\n- _If using an external API generator (like go-doc, js-doc, or so on), point to an external API.md file. This can be the only item in the section, if present._\n\n## Maintainers\n\n_List maintainer(s) for a repository, along with one way of contacting them (e.g. GitHub link or email)._\n\n## Thanks\n\n_State anyone or anything that significantly helped with the development of your project. State public contact hyper-links if applicable._\n\n<!-- footer -->\n";
1371
+ var standard_readme_full_compound_default = "<!-- header -->\n\n_Long description goes here._\n\n_No heading. Cover the main reasons for building the repository. This should describe your module in broad terms, generally in just a few paragraphs; more detail of the module's routines or methods, lengthy code examples, or other in-depth material should be given in subsequent sections._\n\n<!-- table-of-contents -->\n\n## Background\n\n_Cover motivation. Cover abstract dependencies. Cover intellectual provenance: A See Also section is also fitting._\n\n### See also\n\n<!-- install({ headingLevel: 2 }) -->\n\n<!-- dependencies -->\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ file: \"path/to/example.ts\" }) --> comment as well.\n```\n\n```ts\n// If importable, code block indicating both import functionality and usage.\n```\n\n### CLI\n\n```ts\n// If CLI compatible, code block indicating common usage.\n```\n\n## _Extra Sections (Rename)_\n\n## Security\n\n## API\n\n_Describe exported functions and objects._\n\n- _Describe signatures, return types, callbacks, and events._\n- _Cover types covered where not obvious._\n- _Describe caveats._\n- _If using an external API generator (like go-doc, js-doc, or so on), point to an external API.md file. This can be the only item in the section, if present._\n\n## Maintainers\n\n_List maintainer(s) for a repository, along with one way of contacting them (e.g. GitHub link or email)._\n\n## Thanks\n\n_State anyone or anything that significantly helped with the development of your project. State public contact hyper-links if applicable._\n\n<!-- footer -->\n";
1250
1372
 
1251
1373
  //#endregion
1252
1374
  //#region src/lib/readme/templates/standard-readme-full.md?raw
1253
- var standard_readme_full_default = "<!-- title -->\n\n<!-- banner -->\n\n<!-- badges -->\n\n<!-- short-description -->\n\n_Long description goes here._\n\n_No heading. Cover the main reasons for building the repository. This should describe your module in broad terms, generally in just a few paragraphs; more detail of the module's routines or methods, lengthy code examples, or other in-depth material should be given in subsequent sections._\n\n<!-- table-of-contents -->\n\n## Background\n\n_Cover motivation. Cover abstract dependencies. Cover intellectual provenance: A See Also section is also fitting._\n\n### See also\n\n## Install\n\n```sh\n# Code block illustrating how to install.\n```\n\n### Dependencies\n\n_Required if there are unusual dependencies or dependencies that must be manually installed._\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ src: \"path/to/example.ts\" })--> comment as well.\n```\n\n```ts\n// If importable, code block indicating both import functionality and usage.\n```\n\n### CLI\n\n```ts\n// If CLI compatible, code block indicating common usage.\n```\n\n## _Extra Sections (Rename)_\n\n## Security\n\n## API\n\n_Describe exported functions and objects._\n\n- _Describe signatures, return types, callbacks, and events._\n- _Cover types covered where not obvious._\n- _Describe caveats._\n- _If using an external API generator (like go-doc, js-doc, or so on), point to an external API.md file. This can be the only item in the section, if present._\n\n## Maintainers\n\n_List maintainer(s) for a repository, along with one way of contacting them (e.g. GitHub link or email)._\n\n## Thanks\n\n_State anyone or anything that significantly helped with the development of your project. State public contact hyper-links if applicable._\n\n<!-- contributing -->\n\n<!-- license -->\n";
1375
+ var standard_readme_full_default = "<!-- title -->\n\n<!-- banner -->\n\n<!-- badges -->\n\n<!-- short-description -->\n\n_Long description goes here._\n\n_No heading. Cover the main reasons for building the repository. This should describe your module in broad terms, generally in just a few paragraphs; more detail of the module's routines or methods, lengthy code examples, or other in-depth material should be given in subsequent sections._\n\n<!-- table-of-contents -->\n\n## Background\n\n_Cover motivation. Cover abstract dependencies. Cover intellectual provenance: A See Also section is also fitting._\n\n### See also\n\n<!-- install({ headingLevel: 2 }) -->\n\n<!-- dependencies -->\n\n## Usage\n\n```ts\n// Code block illustrating common usage.\n// Consider using the <!-- code({ file: \"path/to/example.ts\" }) --> comment as well.\n```\n\n```ts\n// If importable, code block indicating both import functionality and usage.\n```\n\n### CLI\n\n```ts\n// If CLI compatible, code block indicating common usage.\n```\n\n## _Extra Sections (Rename)_\n\n## Security\n\n## API\n\n_Describe exported functions and objects._\n\n- _Describe signatures, return types, callbacks, and events._\n- _Cover types covered where not obvious._\n- _Describe caveats._\n- _If using an external API generator (like go-doc, js-doc, or so on), point to an external API.md file. This can be the only item in the section, if present._\n\n## Maintainers\n\n_List maintainer(s) for a repository, along with one way of contacting them (e.g. GitHub link or email)._\n\n## Thanks\n\n_State anyone or anything that significantly helped with the development of your project. State public contact hyper-links if applicable._\n\n<!-- contributing -->\n\n<!-- license -->\n";
1254
1376
 
1255
1377
  //#endregion
1256
1378
  //#region src/lib/readme/templates/index.ts
@@ -1283,6 +1405,8 @@ var templates_default = {
1283
1405
 
1284
1406
  //#endregion
1285
1407
  //#region src/lib/readme/create.ts
1408
+ const USAGE_HEADING = "\n## Usage\n";
1409
+ const NEXT_SECTION_REGEX = /\n(?:## |<!-- (?:contributing|footer) -->)/v;
1286
1410
  /**
1287
1411
  * Creates a new readme file interactively.
1288
1412
  *
@@ -1339,7 +1463,8 @@ async function createReadme(options) {
1339
1463
  expand: true,
1340
1464
  template: Object.keys(templates_default)[0]
1341
1465
  }, options ?? {});
1342
- const templateString = getTemplateForConfig(resolvedOptions.template, resolvedOptions.compound);
1466
+ const baseTemplateString = getTemplateForConfig(resolvedOptions.template, resolvedOptions.compound);
1467
+ const templateString = (await findSkills(process.cwd())).length > 0 ? addSkillsPlaceholder(baseTemplateString) : baseTemplateString;
1343
1468
  const readmePath = path.join(resolvedOptions.output, "readme.md");
1344
1469
  if (!resolvedOptions.overwrite) {
1345
1470
  let exists = true;
@@ -1358,6 +1483,20 @@ async function createReadme(options) {
1358
1483
  }
1359
1484
  return readmePath;
1360
1485
  }
1486
+ /**
1487
+ * Adds a `<!-- skills -->` placeholder to a template, between the end of its
1488
+ * "Usage" section and whatever follows it.
1489
+ *
1490
+ * @throws {Error} If the template has no "Usage" section followed by another
1491
+ * section
1492
+ */
1493
+ function addSkillsPlaceholder(template) {
1494
+ const usageIndex = template.indexOf(USAGE_HEADING);
1495
+ const nextSectionOffset = usageIndex === -1 ? void 0 : NEXT_SECTION_REGEX.exec(template.slice(usageIndex + 10))?.index;
1496
+ if (nextSectionOffset === void 0) throw new Error("Could not find a \"## Usage\" section followed by another section in the template");
1497
+ const insertIndex = usageIndex + 10 + nextSectionOffset + 1;
1498
+ return `${template.slice(0, insertIndex)}<!-- skills -->\n\n${template.slice(insertIndex)}`;
1499
+ }
1361
1500
  function getTemplateForConfig(templateKey, compound) {
1362
1501
  if (!Object.hasOwn(templates_default, templateKey)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
1363
1502
  return templates_default[templateKey].content[compound ? "compound" : "explicit"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "CLI tool and TypeScript library implementing the Markdown Autophagic Template (MDAT) system. MDAT lets you use comments as dynamic content templates in Markdown files, making it easy to generate and update readme boilerplate.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -40,7 +40,8 @@
40
40
  "mdat": "dist/bin/cli.js"
41
41
  },
42
42
  "files": [
43
- "dist/*"
43
+ "dist/*",
44
+ "skills/*"
44
45
  ],
45
46
  "dependencies": {
46
47
  "@clack/prompts": "^1.8.1",
@@ -48,13 +49,14 @@
48
49
  "cosmiconfig-typescript-loader": "^6.3.0",
49
50
  "deepmerge-ts": "^8.0.2",
50
51
  "globby": "^16.2.4",
52
+ "gray-matter-es": "^0.2.1",
51
53
  "lognow": "^0.8.4",
52
54
  "mdast-util-toc": "^7.1.0",
53
55
  "metascope": "^0.12.0",
54
56
  "path-type": "^6.0.0",
55
57
  "picocolors": "^1.1.1",
56
58
  "plur": "^6.0.0",
57
- "pretty-bytes": "^7.1.3",
59
+ "pretty-bytes": "^7.2.0",
58
60
  "pretty-ms": "^9.3.1",
59
61
  "remark": "^15.0.1",
60
62
  "remark-gfm": "^4.0.1",
@@ -69,26 +71,26 @@
69
71
  },
70
72
  "devDependencies": {
71
73
  "@arethetypeswrong/core": "^0.18.5",
72
- "@kitschpatrol/shared-config": "^8.10.0",
74
+ "@kitschpatrol/shared-config": "^8.10.1",
73
75
  "@types/mdast": "^4.0.4",
74
76
  "@types/node": "~24.13.6",
75
77
  "@types/unist": "^3.0.3",
76
78
  "@types/yargs": "^17.0.35",
77
- "@vitest/coverage-v8": "^5.0.1",
79
+ "@vitest/coverage-v8": "^5.0.3",
78
80
  "brewpub": "^0.4.0",
79
81
  "bumpp": "^12.3.0",
80
82
  "execa": "^10.0.1",
81
- "mdat-plugin-cli-help": "^3.2.0",
83
+ "mdat-plugin-cli-help": "^3.3.0",
82
84
  "mdat-plugin-example": "^3.0.0",
83
85
  "mdat-plugin-tldraw": "^3.0.0",
84
86
  "nanoid": "^6.0.1",
85
- "prettier": "^3.9.8",
86
- "publint": "^0.3.24",
87
+ "prettier": "^3.9.9",
88
+ "publint": "^0.3.25",
87
89
  "shx": "^0.4.0",
88
90
  "tsdown": "^0.23.0",
89
91
  "typescript": "~6.0.3",
90
92
  "unplugin-raw": "^0.8.0",
91
- "vitest": "^5.0.1"
93
+ "vitest": "^5.0.3"
92
94
  },
93
95
  "peerDependencies": {
94
96
  "prettier": "^3.0.0"
package/readme.md CHANGED
@@ -46,6 +46,9 @@
46
46
  - [Installing a rule plugin](#installing-a-rule-plugin)
47
47
  - [Creating a rule plugin](#creating-a-rule-plugin)
48
48
  - [Available rule plugins](#available-rule-plugins)
49
+ - [Agent skills](#agent-skills)
50
+ - [Sync from the installed package (recommended)](#sync-from-the-installed-package-recommended)
51
+ - [Install from the repository](#install-from-the-repository)
49
52
  - [Migrating from 1.x to 2.x](#migrating-from-1x-to-2x)
50
53
  - [Flat CLI commands](#flat-cli-commands)
51
54
  - [Polyglot metadata](#polyglot-metadata)
@@ -124,7 +127,7 @@ Written in TypeScript with bundled type definitions.
124
127
 
125
128
  ### Installation
126
129
 
127
- Pick the option that matches how you plan to use it.
130
+ There are several ways to install mdat depending on how you're planning to use it:
128
131
 
129
132
  #### CLI
130
133
 
@@ -617,6 +620,8 @@ Every rule that emits a section heading accepts a `headingLevel` option (`1`-`6`
617
620
  <!-- install({ headingLevel: 2, dev: true, homebrew: false }) -->
618
621
  ```
619
622
 
623
+ Also aliased as `<!-- installation -->`.
624
+
620
625
  - ##### `<!-- dependencies -->`
621
626
 
622
627
  Documents platform requirements and peer dependencies. Lists runtime platforms (Node, Python, Rust, Go, Ruby, etc.) with version constraints from `engines` or equivalent metadata, supported operating systems, and required peer dependencies with links to npm. Optional peer dependencies are left out. Version ranges are described in plain language (`>=24.16.0` becomes "24.16.0 or newer"), with the exact range included when it can't be summarized that simply. The optional `headingLevel` sets the Markdown heading level and defaults to `3`.
@@ -658,6 +663,10 @@ Every rule that emits a section heading accepts a `headingLevel` option (`1`-`6`
658
663
 
659
664
  <!-- /size-table -->
660
665
 
666
+ - ##### `<!-- skills -->`
667
+
668
+ Documents the [Agent Skills](https://agentskills.io) bundled with the project and how to install them. Detects skills from `skills/<name>/SKILL.md` files, and lists each skill's name and description from its frontmatter, linking the name to the skill's `SKILL.md` file. Publishable npm packages get instructions for syncing skills from the installed package with the [`skills`](https://github.com/vercel-labs/skills) CLI's `experimental_sync` command (recommended), and for installing them from the repository with `skills add`. Private packages and non-Node projects only get the repository instructions. Reports an error if no skills are found. Remember to include the `skills` directory in the `files` field of `package.json`. The optional `headingLevel` sets the Markdown heading level and defaults to `2`.
669
+
661
670
  #### Compound
662
671
 
663
672
  Compound rules combine several stand-alone rules under a single keyword. Options are passed as an array with one entry per stand-alone rule, in order, e.g. `<!-- footer([{ headingLevel: 3 }, { headingLevel: 3 }]) -->`.
@@ -690,6 +699,8 @@ The `create` command provides starter readme templates:
690
699
  - **Standard Readme basic** — Only the "required" sections from the [Standard Readme](https://github.com/RichardLitt/standard-readme/blob/main/spec.md) spec.
691
700
  - **Standard Readme full** — All sections from the [Standard Readme](https://github.com/RichardLitt/standard-readme/blob/main/spec.md) spec.
692
701
 
702
+ If the project has agent skills in `skills/<name>/SKILL.md`, a `<!-- skills -->` placeholder is added after the template's "Usage" section.
703
+
693
704
  ## Plugins
694
705
 
695
706
  Rule plugins are packages for sharing mdat expansion rules across projects.
@@ -756,6 +767,45 @@ Transform a CLI command's `--help` output into Markdown tables. Recursively call
756
767
 
757
768
  Example: `<!-- cli-help -->`
758
769
 
770
+ #### [mdat-plugin-api](https://github.com/kitschpatrol/mdat-plugin-api)
771
+
772
+ Generate API documentation for a TypeScript package's public exports. Runs [TypeDoc](https://typedoc.org) over the package's entry point and embeds signatures, JSDoc descriptions, parameter and property tables, and examples as Markdown. A compact format renders one table row per export for large or namespaced APIs, and exports can be selected by name.
773
+
774
+ Example: `<!-- api({ format: "compact", include: ["sync*"] }) -->`
775
+
776
+ <!-- //skills -->
777
+
778
+ ## Agent skills
779
+
780
+ This project includes [Agent Skills](https://agentskills.io) that teach coding agents like Claude Code and Codex how to work with mdat:
781
+
782
+ - **[`mdat`](skills/mdat/SKILL.md)**: Use and configure mdat, the Markdown Autophagic Template CLI and library that expands HTML comment placeholders such as `<!-- title -->` in Markdown files. Use when a readme or other Markdown file contains mdat comment placeholders, when editing mdat.config.ts or an "mdat" key in package.json, when generating or updating readme boilerplate from project metadata, when `mdat check` fails, or when calling the mdat TypeScript API.
783
+ - **[`mdat-plugin-authoring`](skills/mdat-plugin-authoring/SKILL.md)**: Write custom mdat expansion rules and publishable mdat rule plugins (mdat-plugin-\* packages). Use when creating or debugging rule functions for mdat comment placeholders, validating comment arguments, reading project metadata or the Markdown tree inside a rule, building compound rules, or packaging and testing rules for reuse across projects.
784
+
785
+ The skills are published in the `skills` directory of the `mdat` package. Nothing is added to your project until you install them with one of the tools below.
786
+
787
+ ### Sync from the installed package (recommended)
788
+
789
+ With `mdat` installed as a project dependency, the [`skills`](https://github.com/vercel-labs/skills) CLI finds skills bundled in your dependencies and copies them into your project's agent skill directories, so they match the version of `mdat` you have installed:
790
+
791
+ ```sh
792
+ npx skills experimental_sync
793
+ ```
794
+
795
+ Run the command again after upgrading `mdat` to refresh the copies. The `experimental_sync` command is experimental and its behavior may change.
796
+
797
+ ### Install from the repository
798
+
799
+ If `mdat` is not a dependency of your project, for example because you use a global installation, install the skills from the repository instead:
800
+
801
+ ```sh
802
+ npx skills add kitschpatrol/mdat
803
+ ```
804
+
805
+ Skills installed this way follow the repository's default branch rather than your installed version of `mdat`.
806
+
807
+ <!-- //skills -->
808
+
759
809
  ## Migrating from 1.x to 2.x
760
810
 
761
811
  The 2.0 version introduces significant breaking changes in the interest of simplicity and a somewhat narrowed scope of concerns.
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: mdat
3
+ description: Use and configure mdat, the Markdown Autophagic Template CLI and library that expands HTML comment placeholders such as `<!-- title -->` in Markdown files. Use when a readme or other Markdown file contains mdat comment placeholders, when editing mdat.config.ts or an "mdat" key in package.json, when generating or updating readme boilerplate from project metadata, when `mdat check` fails, or when calling the mdat TypeScript API.
4
+ license: MIT
5
+ ---
6
+
7
+ # Using mdat
8
+
9
+ mdat expands HTML comments in Markdown files into generated content. A comment like `<!-- title -->` is a placeholder. Running `mdat` fills it in from a rule and adds a closing tag:
10
+
11
+ ```md
12
+ <!-- title -->
13
+
14
+ # my-project
15
+
16
+ <!-- /title -->
17
+ ```
18
+
19
+ The comments stay in the file, so the content can be regenerated whenever the underlying data changes.
20
+
21
+ ## Ground rules
22
+
23
+ - **Never hand-edit content between an opening tag and its closing tag.** It is overwritten on the next run. Change the comment's arguments, the rule, or the project metadata the rule reads from (e.g. `package.json`), then re-run `mdat`.
24
+ - **Text outside the tags is ordinary Markdown.** Edit it freely.
25
+ - **Re-run `mdat` after any change that affects generated content**, such as editing headings (the table of contents), `package.json` fields, or embedded files. Then confirm with `mdat check`.
26
+ - `mdat` rewrites files in place. Do not run it on files with uncommitted work you cannot recover.
27
+ - To add a placeholder, write only the opening comment. `mdat` adds the closing tag.
28
+ - Comments whose first word is not a known rule keyword are left alone, as are code-style comments like `<!-- // note -->`.
29
+
30
+ ## CLI
31
+
32
+ If mdat is a project dependency, run it through the package manager (`pnpm mdat`, `npx mdat`). Run `mdat <command> --help` for the full option list.
33
+
34
+ | Command | Effect |
35
+ | ----------------------- | -------------------------------------------------------------------------------------- |
36
+ | `mdat` or `mdat expand` | Expand placeholders. With no files, expands the closest `readme.md`. |
37
+ | `mdat check` | Dry-run. Exits with code 1 if any file has stale or unexpanded content. Good for CI. |
38
+ | `mdat collapse` | Remove generated content, leaving only the opening comments. |
39
+ | `mdat strip` | Remove the mdat comments but keep the generated content. |
40
+ | `mdat create` | Create a new readme from a bundled template. Interactive unless `--interactive false`. |
41
+
42
+ Common options:
43
+
44
+ - `mdat docs/*.md` processes specific files.
45
+ - `--config a.ts b.json` (`-c`) loads additional rule files. Available on `expand` and `check`.
46
+ - `--format` (`-f`) formats output with Prettier, which must be installed. If the project formats Markdown with Prettier, pass `--format` to both `mdat` and `mdat check` so they agree.
47
+ - `--print` writes to stdout instead of the file.
48
+ - `--output` (`-o`) and `--name` (`-n`) write to a different directory or file name.
49
+
50
+ ## Comment arguments
51
+
52
+ Arguments use function-call syntax with a single [JSON5](https://json5.org) value. Parentheses are required:
53
+
54
+ ```md
55
+ <!-- install({ headingLevel: 2, dev: true }) -->
56
+ ```
57
+
58
+ Only JSON5 values are accepted. No JavaScript is evaluated. Bundled rules validate their arguments and report an error on unknown shapes.
59
+
60
+ ## Bundled rules
61
+
62
+ These work without any configuration. They read normalized project metadata, so most also work in Python, Rust, Go, and Ruby projects, not only Node.
63
+
64
+ | Keyword | Output | Options |
65
+ | ----------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------- |
66
+ | `title` | Project name as a heading. | |
67
+ | `banner` | A banner image found in the project directory. | |
68
+ | `badges` | npm version, license, CI status, and Git LFS badges. | `npm: string[]`, `custom: { [name]: { image, link } }` |
69
+ | `description` (alias `short-description`) | Project description. | |
70
+ | `table-of-contents` (alias `toc`) | Table of contents from the document headings. | `depth` (default `3`), `headingLevel` (default `2`) |
71
+ | `install` (alias `installation`) | Install instructions for consumers of the package. Private packages are rejected. | `headingLevel` (default `3`), `dev`, `homebrew` |
72
+ | `dependencies` | Runtime platform requirements and required peer dependencies. | `headingLevel` (default `3`) |
73
+ | `development-dependencies` (alias `dev-dependencies`) | Tools needed to work on the project, from `devEngines` and `packageManager`. | `headingLevel` (default `3`) |
74
+ | `contributing` | Invitation to open issues and pull requests. | `headingLevel` (default `2`) |
75
+ | `license` | License section. | `headingLevel` (default `2`) |
76
+ | `code` | A code block embedded from a file. | `file` (required), `language`, `trim` (default `true`) |
77
+ | `size` | A file's size. | `file` (required), `compression`: `none`, `gzip`, `brotli` |
78
+ | `size-table` | A table of file sizes with Gzip and Brotli columns. | `files` (required), `precision` (default `0`) |
79
+ | `skills` | Agent Skills found in `skills/<name>/SKILL.md` and how to install them. | `headingLevel` (default `2`) |
80
+ | `header` | Compound: `title`, `banner`, `badges`, `short-description`. | Array, one entry per sub-rule in order. |
81
+ | `footer` | Compound: `contributing`, `license`. | Array, one entry per sub-rule in order. |
82
+
83
+ File paths in `code`, `size`, and `size-table` resolve from the current working directory.
84
+
85
+ ## Configuration
86
+
87
+ Custom rules live in a config file found by [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). `mdat.config.ts` in the project root is the usual choice. The file default-exports a record whose keys become comment keywords:
88
+
89
+ ```ts
90
+ import { defineConfig } from 'mdat'
91
+
92
+ export default defineConfig({
93
+ // Object form: adds processing order
94
+ date: {
95
+ content: () => new Date().toISOString(),
96
+ order: 1,
97
+ },
98
+ // String: static replacement
99
+ greeting: 'Hello, world!',
100
+ // Function: sync or async, receives the comment's arguments, e.g. <!-- repeat(3) -->
101
+ repeat: (options) => 'ha'.repeat(Number(options)),
102
+ })
103
+ ```
104
+
105
+ - A rule must return non-empty Markdown. Empty content and thrown errors are reported as errors.
106
+ - Custom rules merge with the bundled rules. A custom rule with the same keyword as a bundled rule replaces it.
107
+ - With multiple config files, the last rule for a keyword wins, and `--config` files take precedence over discovered config.
108
+ - JSON files work as rule sets. Nested keys are flattened into dot-notated keywords, so `{ "a": { "b": "x" } }` provides `<!-- a.b -->`.
109
+ - `package.json` can hold rules under an `"mdat"` key, or name a shared config package: `"mdat": "@scope/mdat-config"`.
110
+ - Rules from a plugin package are spread into the config: `defineConfig({ ...examplePlugin })`.
111
+
112
+ For rules that take validated arguments, read the document tree, or are shared across projects, use the `mdat-plugin-authoring` skill if it is available.
113
+
114
+ ## Library API
115
+
116
+ ```ts
117
+ import { check, expand, expandString } from 'mdat'
118
+ import { write } from 'to-vfile'
119
+
120
+ // Returns VFiles. Writing is the caller's responsibility.
121
+ const [file] = await expand('readme.md')
122
+ await write(file)
123
+
124
+ const result = await expandString('<!-- title -->')
125
+ console.log(result.toString())
126
+ ```
127
+
128
+ - `expand`, `collapse`, `strip`, and `check` take file paths. With no files they find the closest readme.
129
+ - `expandString`, `collapseString`, `stripString`, and `checkString` take Markdown strings.
130
+ - `create` and `createInteractive` create a readme from a bundled template.
131
+ - `loadConfig` discovers and merges config. `defineConfig` and `mergeConfig` build config objects.
132
+
133
+ ## Troubleshooting
134
+
135
+ - **A comment is not expanding.** The keyword does not match a loaded rule. Check spelling, confirm the config file is discovered or passed with `--config`, and run with `--verbose`.
136
+ - **`mdat check` fails.** Run `mdat` (with `--format` if the check used it) and commit the result. If it still fails, the rule output is non-deterministic, e.g. a timestamp.
137
+ - **Arguments are ignored or rejected.** Use parentheses: `<!-- keyword({ key: "value" }) -->`. The 1.x form without parentheses is no longer supported.
138
+ - **Old commands.** `mdat readme` is now `mdat`, `mdat readme init` is `mdat create`, and `mdat readme check` is `mdat check`. The `--rules`, `--assets`, `--package`, `--meta`, and `--prefix` options were removed in favor of `--config`.
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: mdat-plugin-authoring
3
+ description: Write custom mdat expansion rules and publishable mdat rule plugins (mdat-plugin-* packages). Use when creating or debugging rule functions for mdat comment placeholders, validating comment arguments, reading project metadata or the Markdown tree inside a rule, building compound rules, or packaging and testing rules for reuse across projects.
4
+ license: MIT
5
+ ---
6
+
7
+ # Authoring mdat rules and plugins
8
+
9
+ An mdat rule maps a comment keyword to generated Markdown. A rule plugin is an npm package whose default export is a record of rules. Start with a rule in the project's `mdat.config.ts`. Move it to a plugin package only when more than one project needs it.
10
+
11
+ ## Rule shapes
12
+
13
+ ```ts
14
+ import type { Config } from 'mdat'
15
+
16
+ export default {
17
+ // String: static replacement for <!-- greeting -->
18
+ greeting: 'Hello, world!',
19
+ // Array: compound rule, each entry is itself a rule
20
+ intro: ['# My Project', () => 'Generated description.'],
21
+ // Object: adds processing order
22
+ summary: {
23
+ content: (_options, context) => `Source: ${context.filePath ?? 'unknown'}`,
24
+ order: 1,
25
+ },
26
+ // Function: sync or async
27
+ time: () => new Date().toDateString(),
28
+ } satisfies Config
29
+ ```
30
+
31
+ The full signature of a content function:
32
+
33
+ ```ts
34
+ type Content = (options: JsonValue, context: RuleContext) => Promise<string> | string
35
+
36
+ type RuleContext = {
37
+ /** File path of the source document, if known. */
38
+ filePath: string | undefined
39
+ /** Parsed YAML frontmatter from the document, if present. */
40
+ frontmatter: Record<string, unknown> | undefined
41
+ /** The full mdast tree of the document. Do not mutate. */
42
+ tree: Root
43
+ }
44
+ ```
45
+
46
+ ## Requirements for a rule
47
+
48
+ - **Return a non-empty Markdown string.** Empty output is reported as an error. If there is nothing to output, throw an error that says why.
49
+ - **Be deterministic.** `mdat check` compares a fresh expansion against the file on disk, so timestamps, random values, and unstable ordering make the check fail on every run.
50
+ - **Fail with a clear message.** Thrown errors are reported against the comment that triggered them. Include what was missing and how to fix it.
51
+ - **Resolve relative paths deliberately.** The bundled rules resolve file arguments from `process.cwd()`. Use `context.filePath` to resolve relative to the document instead.
52
+ - **Do not include the comment tags in the output.** mdat adds the opening and closing tags.
53
+
54
+ ## Keywords
55
+
56
+ A keyword consists of letters, numbers, `_`, `$`, `-`, and `.`, and cannot start with `/`, `*`, `#`, `-`, or `.`. Prefer kebab-case, e.g. `cli-help`. Pick a specific keyword. A rule with the same keyword as a bundled rule (`title`, `badges`, `install`, `license`, and so on) replaces it.
57
+
58
+ ## Arguments
59
+
60
+ A comment passes a single JSON5 value: `<!-- keyword({ file: "./a.ts", trim: false }) -->`. A comment without parentheses passes `{}`. Arguments are untrusted input, so validate them. The bundled rules use [Zod](https://zod.dev):
61
+
62
+ ```ts
63
+ import type { Config } from 'mdat'
64
+ import fs from 'node:fs/promises'
65
+ import path from 'node:path'
66
+ import { z } from 'zod'
67
+
68
+ const optionsSchema = z.object({
69
+ file: z.string(),
70
+ language: z.string().optional(),
71
+ })
72
+
73
+ export default {
74
+ embed: {
75
+ async content(options) {
76
+ const { file, language } = optionsSchema.parse(options)
77
+ const code = await fs.readFile(path.join(process.cwd(), file), 'utf8')
78
+ return `\`\`\`${language ?? path.extname(file).slice(1)}\n${code.trim()}\n\`\`\``
79
+ },
80
+ },
81
+ } satisfies Config
82
+ ```
83
+
84
+ Prefer an object argument over a bare primitive so options can be added later without breaking existing comments.
85
+
86
+ ## Project metadata
87
+
88
+ mdat exports helpers so rules do not need to parse `package.json` themselves. Both are memoized and work across ecosystems (Node, Python, Rust, Go, Ruby) via [metascope](https://github.com/kitschpatrol/metascope):
89
+
90
+ ```ts
91
+ import { getContextMetadata, getReadmeMetadata } from 'mdat'
92
+
93
+ // Normalized fields the bundled readme rules use, e.g. name, description,
94
+ // repositoryUrl, issuesUrl, license, author, bin, engines, peerDependencies.
95
+ // Any field may be undefined.
96
+ const { name, repositoryUrl } = await getReadmeMetadata()
97
+
98
+ // The full metascope metadata context for anything not covered above.
99
+ const metadata = await getContextMetadata()
100
+ ```
101
+
102
+ See the exported `ReadmeMetadata` type for the complete field list. Call `resetMetadataCaches()` between tests that change project files on disk.
103
+
104
+ ## Order and compound rules
105
+
106
+ - `order` defaults to `0`. Rules with a higher order run later. Use it when a rule reads content other rules generate. The bundled `table-of-contents` rule uses `order: 1` so that it sees headings produced by other rules.
107
+ - A compound rule is an array of rules whose output is joined under one keyword. Its comment takes an array of arguments, one entry per sub-rule in order: `<!-- footer([{ headingLevel: 3 }, { headingLevel: 3 }]) -->`.
108
+ - To reuse a rule from another single-rule record inside a compound rule, use `getSoleRule` from `remark-mdat`.
109
+
110
+ ## Packaging a plugin
111
+
112
+ - Name the package with the `mdat-plugin-` prefix and add the `mdat-plugin` keyword.
113
+ - Publish ESM with a default export of the rule record, typed with `satisfies Config`.
114
+ - Declare `mdat` as a peer dependency, and as a dev dependency for tests.
115
+ - One package may export several related rules.
116
+
117
+ ```json
118
+ {
119
+ "name": "mdat-plugin-example",
120
+ "type": "module",
121
+ "exports": {
122
+ ".": {
123
+ "types": "./dist/index.d.ts",
124
+ "import": "./dist/index.js"
125
+ }
126
+ },
127
+ "keywords": ["mdat", "mdat-plugin"],
128
+ "peerDependencies": {
129
+ "mdat": "^3.0.0"
130
+ }
131
+ }
132
+ ```
133
+
134
+ Consumers spread the plugin into their config:
135
+
136
+ ```ts
137
+ import { defineConfig } from 'mdat'
138
+ import example from 'mdat-plugin-example'
139
+
140
+ export default defineConfig({ ...example })
141
+ ```
142
+
143
+ [mdat-plugin-example](https://github.com/kitschpatrol/mdat-plugin-example) is a minimal template repository. [mdat-plugin-cli-help](https://github.com/kitschpatrol/mdat-plugin-cli-help) and [mdat-plugin-tldraw](https://github.com/kitschpatrol/mdat-plugin-tldraw) are fuller examples with arguments and file output.
144
+
145
+ ## Testing
146
+
147
+ Test rules through the public API with `expandString`, passing the rule record as config:
148
+
149
+ ```ts
150
+ import { expandString } from 'mdat'
151
+ import { expect, it } from 'vitest'
152
+ import plugin from '../src/index'
153
+
154
+ it('expands the example rule', async () => {
155
+ const result = await expandString('<!-- example -->', plugin)
156
+ expect(result.toString()).toContain('Hello from')
157
+ expect(result.messages.filter((message) => message.fatal)).toHaveLength(0)
158
+ })
159
+
160
+ it('is stable across runs', async () => {
161
+ const first = await expandString('<!-- example -->', plugin)
162
+ const second = await expandString(first.toString(), plugin)
163
+ expect(second.toString()).toEqual(first.toString())
164
+ })
165
+ ```
166
+
167
+ Also cover invalid arguments. Rule errors are not thrown. They are reported as messages on the returned VFile with `fatal: true`, alongside informational messages for each successful expansion, so assert on the fatal messages in `result.messages`.
168
+
169
+ To try a rule file against a real document without touching it, run `mdat --config ./my-rules.ts --print some-file.md`.