mdat 3.5.0 → 3.6.1

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-D3Djaa1D.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-BU0oLDpf.js";
3
3
  import { createLogger } from "lognow";
4
4
  import picocolors from "picocolors";
5
5
  import prettyMilliseconds from "pretty-ms";
@@ -179,6 +179,7 @@ declare const readmeMetadataTemplate: import("metascope").Template<{
179
179
  licenseFilePath: string | undefined;
180
180
  licenseUrl: string | undefined;
181
181
  name: string | undefined;
182
+ obsidianPluginId: string | undefined;
182
183
  operatingSystem: string[] | undefined;
183
184
  peerDependencies: {
184
185
  name: string;
@@ -186,9 +187,11 @@ declare const readmeMetadataTemplate: import("metascope").Template<{
186
187
  version: string;
187
188
  }[] | undefined;
188
189
  projectDirectory: string | undefined;
190
+ repositoryDirectory: string | undefined;
189
191
  repositoryUrl: string | undefined;
190
192
  runtimePlatform: string[] | undefined;
191
193
  usesGitLfs: boolean;
194
+ vscodeExtensionId: string | undefined;
192
195
  }>;
193
196
  type ReadmeMetadata = ReturnType<typeof readmeMetadataTemplate>;
194
197
  /**
@@ -225,6 +228,7 @@ export declare function getReadmeMetadata(): Promise<{
225
228
  licenseFilePath: string | undefined;
226
229
  licenseUrl: string | undefined;
227
230
  name: string | undefined;
231
+ obsidianPluginId: string | undefined;
228
232
  operatingSystem: string[] | undefined;
229
233
  peerDependencies: {
230
234
  name: string;
@@ -232,9 +236,11 @@ export declare function getReadmeMetadata(): Promise<{
232
236
  version: string;
233
237
  }[] | undefined;
234
238
  projectDirectory: string | undefined;
239
+ repositoryDirectory: string | undefined;
235
240
  repositoryUrl: string | undefined;
236
241
  runtimePlatform: string[] | undefined;
237
242
  usesGitLfs: boolean;
243
+ vscodeExtensionId: string | undefined;
238
244
  }>;
239
245
  /**
240
246
  * Reset all cached metadata. Call between tests or when the underlying project
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-D3Djaa1D.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-BU0oLDpf.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 };
@@ -26,14 +26,13 @@ import { confirm, group, intro, note, outro, select } from "@clack/prompts";
26
26
 
27
27
  //#region package.json
28
28
  var name = "mdat";
29
- var version = "3.5.0";
29
+ var version = "3.6.1";
30
30
 
31
31
  //#endregion
32
32
  //#region src/lib/deep-merge-defined.ts
33
33
  const deepmerge = deepmergeCustom({ mergeArrays: false });
34
34
  function stripUndefinedDeep(object) {
35
- if (Array.isArray(object)) return object.map((v) => v !== null && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
36
- return Object.entries(object).map(([k, v]) => [k, v !== null && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
35
+ return Array.isArray(object) ? object.map((v) => v !== null && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0) : Object.entries(object).map(([k, v]) => [k, v !== null && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
37
36
  ...acc,
38
37
  [k]: v
39
38
  }, {});
@@ -193,6 +192,29 @@ function getHomebrewFormula(packageName, scripts) {
193
192
  }
194
193
  }
195
194
  /**
195
+ * VS Code Marketplace item identifier for packages that are VS Code extensions,
196
+ * identified by an `engines.vscode` field.
197
+ *
198
+ * @returns E.g. `kitschpatrol.tldraw-preview`, or undefined if the package
199
+ * isn't an extension
200
+ */
201
+ function getVscodeExtensionId(nodePackage) {
202
+ return typeof nodePackage?.publisher === "string" && nodePackage.engines?.vscode !== void 0 ? `${nodePackage.publisher}.${nodePackage.name}` : void 0;
203
+ }
204
+ /**
205
+ * Obsidian community plugin identifier, from the plugin's manifest.json.
206
+ */
207
+ function getObsidianPluginId(manifests) {
208
+ return helpers.firstOf(manifests)?.data.id;
209
+ }
210
+ /**
211
+ * Package location within a monorepo, from the package.json
212
+ * `repository.directory` field.
213
+ */
214
+ function getRepositoryDirectory(nodePackage) {
215
+ return typeof nodePackage?.repository === "object" ? nodePackage.repository.directory : void 0;
216
+ }
217
+ /**
196
218
  * Reset cached context metadata. Call between tests or when the underlying
197
219
  * project files may have changed on disk.
198
220
  *
@@ -202,12 +224,12 @@ function resetContextMetadata() {
202
224
  metascopeMetadata = void 0;
203
225
  }
204
226
  const readmeMetadataTemplate = defineTemplate((context) => {
205
- const { githubActions, gitStats, licenseFile, metascope, nodePackageJson } = context;
227
+ const { githubActions, gitStats, licenseFile, metascope, nodePackageJson, obsidianPluginManifestJson } = context;
206
228
  const codemeta = templates.codemetaJson(context, {});
207
229
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
208
230
  const licenseFileData = helpers.firstOf(licenseFile);
209
231
  const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
210
- const repoUrl = codemeta.codeRepository?.replace(GIT_PREFIX_REGEX, "").replace(GIT_SUFFIX_REGEX, "").replace(TRAILING_SLASH_REGEX, "");
232
+ const repositoryUrl = codemeta.codeRepository?.replace(GIT_PREFIX_REGEX, "").replace(GIT_SUFFIX_REGEX, "").replace(TRAILING_SLASH_REGEX, "");
211
233
  const bin = (() => {
212
234
  if (nodePackage === void 0) return;
213
235
  const binField = nodePackage.bin;
@@ -257,6 +279,9 @@ const readmeMetadataTemplate = defineTemplate((context) => {
257
279
  };
258
280
  })();
259
281
  const homebrewFormula = getHomebrewFormula(nodePackage?.name, nodePackage?.scripts);
282
+ const repositoryDirectory = getRepositoryDirectory(nodePackage);
283
+ const vscodeExtensionId = getVscodeExtensionId(nodePackage);
284
+ const obsidianPluginId = getObsidianPluginId(obsidianPluginManifestJson);
260
285
  const firstAuthor = helpers.firstOf(helpers.ensureArray(codemeta.author));
261
286
  return {
262
287
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
@@ -276,12 +301,15 @@ const readmeMetadataTemplate = defineTemplate((context) => {
276
301
  licenseFilePath: licenseFileData?.source,
277
302
  licenseUrl: licenseFileData?.data.match?.spdxUrl,
278
303
  name: codemeta.name,
304
+ obsidianPluginId,
279
305
  operatingSystem: codemeta.operatingSystem ?? getSupportedOperatingSystems(nodePackage?.os),
280
306
  peerDependencies,
281
307
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
282
- repositoryUrl: repoUrl,
308
+ repositoryDirectory,
309
+ repositoryUrl,
283
310
  runtimePlatform: codemeta.runtimePlatform,
284
- usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true
311
+ usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true,
312
+ vscodeExtensionId
285
313
  };
286
314
  });
287
315
  let readmeMetadata;
@@ -316,24 +344,159 @@ function resetMetadataCaches() {
316
344
 
317
345
  //#endregion
318
346
  //#region src/lib/readme/rules/badges.ts
347
+ const HOMEBREW_FORMULA_REGEX = /^(?<owner>[^\/]+)\/(?<tap>[^\/]+)\/(?<formula>[^\/]+)$/v;
348
+ const GITHUB_REPOSITORY_REGEX$1 = /^https:\/\/github\.com\/(?<owner>[^\/]+)\/(?<repo>[^\/]+)$/v;
349
+ const NO_NPM_PACKAGE = "no public npm package was found. Remove \"private\": true from package.json if the package is published, or list packages explicitly, e.g. badges({ npm: [\"package-name\"] })";
350
+ const optionsSchema$2 = z.object({
351
+ bundleSize: z.boolean().optional(),
352
+ ci: z.boolean().optional(),
353
+ custom: z.record(z.string(), z.object({
354
+ image: z.string(),
355
+ link: z.string()
356
+ })).default({}),
357
+ githubRelease: z.boolean().optional(),
358
+ gitLfs: z.boolean().optional(),
359
+ homebrew: z.union([z.boolean(), z.string().regex(HOMEBREW_FORMULA_REGEX, "The badges \"homebrew\" option expects a formula in the form \"owner/tap/formula\"")]).optional(),
360
+ license: z.boolean().optional(),
361
+ npm: z.union([z.boolean(), z.array(z.string()).min(1, "Pass npm: false instead of an empty array to hide the NPM version badge")]).optional(),
362
+ npmDownloads: z.boolean().optional(),
363
+ obsidianDownloads: z.boolean().optional(),
364
+ vscode: z.boolean().optional()
365
+ });
366
+ /**
367
+ * Escape text for a path segment of a shields.io static badge, where `-` and
368
+ * `_` are delimiters and must be doubled to appear literally.
369
+ */
370
+ function escapeStaticBadgeText(text) {
371
+ return encodeURIComponent(text.replaceAll("-", "--").replaceAll("_", "__"));
372
+ }
373
+ /**
374
+ * One badge per npm package, for badges that report on npm packages.
375
+ */
376
+ function mapNpmPackages(npmPackages, toBadge) {
377
+ return npmPackages.length === 0 ? { unavailable: NO_NPM_PACKAGE } : npmPackages.map((packageName) => toBadge(packageName));
378
+ }
379
+ /**
380
+ * Owner and name of the project's GitHub repository, or undefined if the
381
+ * repository isn't on GitHub.
382
+ */
383
+ function getGithubRepository(repositoryUrl = "") {
384
+ const { owner, repo } = GITHUB_REPOSITORY_REGEX$1.exec(repositoryUrl)?.groups ?? {};
385
+ return owner === void 0 || repo === void 0 ? void 0 : {
386
+ owner,
387
+ repo
388
+ };
389
+ }
390
+ /**
391
+ * Reason for badges that need a GitHub repository.
392
+ */
393
+ function getNotGithubReason(repositoryUrl) {
394
+ return `${repositoryUrl === void 0 ? "no repository URL was found in the project metadata" : `the repository URL "${repositoryUrl}" is not a GitHub repository`}. Set the "repository" field in package.json to the project's GitHub repository.`;
395
+ }
396
+ /**
397
+ * Explicit package names are useful for monorepos. Otherwise the package itself
398
+ * is used if it's public, even when the NPM version badge is hidden, so the
399
+ * download and size badges can still appear.
400
+ */
401
+ function getNpmPackages(option, { isPublicNpmPackage, name }) {
402
+ if (Array.isArray(option)) return option;
403
+ return isPublicNpmPackage && name !== void 0 ? [name] : [];
404
+ }
405
+ /**
406
+ * Badges in display order. Each appears when its metadata is available unless
407
+ * it's `optIn`, and its option can hide it (`false`) or require it (`true`).
408
+ */
409
+ const badgeDefinitions = [
410
+ {
411
+ key: "npm",
412
+ render: ({ npmPackages }) => mapNpmPackages(npmPackages, (packageName) => `[![NPM Package ${packageName}](https://img.shields.io/npm/v/${packageName}.svg)](https://www.npmjs.com/package/${packageName})`)
413
+ },
414
+ {
415
+ key: "license",
416
+ render: ({ license, licenseUrl }) => license === void 0 || licenseUrl === void 0 ? { unavailable: "no license file matching a known SPDX license was found" } : [`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](${licenseUrl})`]
417
+ },
418
+ {
419
+ key: "ci",
420
+ render({ ciActionFileName, repositoryUrl }) {
421
+ if (ciActionFileName === void 0) return { unavailable: "no GitHub Actions workflow named \"CI\" was found in .github/workflows" };
422
+ if (repositoryUrl === void 0) return { unavailable: "no repository URL was found in the project metadata" };
423
+ return [`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`];
424
+ }
425
+ },
426
+ {
427
+ key: "gitLfs",
428
+ render: ({ usesGitLfs }) => usesGitLfs ? ["![Git LFS](https://img.shields.io/badge/Git%20LFS-enabled-F64935?logo=gitlfs&logoColor=white)"] : { unavailable: "the repository does not track any files with Git LFS" }
429
+ },
430
+ {
431
+ key: "vscode",
432
+ render({ repositoryDirectory, repositoryUrl, vscodeExtensionId }) {
433
+ if (vscodeExtensionId === void 0) return { unavailable: "package.json is missing a \"publisher\" or \"engines.vscode\" field" };
434
+ const repository = getGithubRepository(repositoryUrl);
435
+ if (repository === void 0) return { unavailable: `the extension version is read from package.json on GitHub, but ${getNotGithubReason(repositoryUrl)}` };
436
+ const packageJsonPath = [repositoryDirectory, "package.json"].filter((segment) => segment !== void 0).join("/");
437
+ const packageJsonUrl = `https://raw.githubusercontent.com/${repository.owner}/${repository.repo}/HEAD/${packageJsonPath}`;
438
+ return [`[![Visual Studio Marketplace Version](https://img.shields.io/badge/dynamic/json?url=${encodeURIComponent(packageJsonUrl)}&query=version&label=VS%20Code%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=${vscodeExtensionId})`];
439
+ }
440
+ },
441
+ {
442
+ key: "homebrew",
443
+ render({ homebrewFormula, options }) {
444
+ const formulaPath = typeof options.homebrew === "string" ? options.homebrew : homebrewFormula;
445
+ if (formulaPath === void 0) return { unavailable: "no \"brewpub --tap <owner>/<tap>\" invocation was found in the package.json scripts. Pass the formula explicitly instead, e.g. badges({ homebrew: \"owner/tap/formula\" })" };
446
+ const formulaParts = HOMEBREW_FORMULA_REGEX.exec(formulaPath)?.groups;
447
+ if (formulaParts?.owner === void 0 || formulaParts.tap === void 0 || formulaParts.formula === void 0) return { unavailable: `the detected formula "${formulaPath}" is not in the form "owner/tap/formula"` };
448
+ const { formula, owner, tap } = formulaParts;
449
+ return [`[![Homebrew](https://img.shields.io/badge/Homebrew-${escapeStaticBadgeText(formulaPath)}-FBB040?logo=homebrew&logoColor=white)](https://github.com/${owner}/homebrew-${tap}/blob/HEAD/Formula/${formula}.rb)`];
450
+ }
451
+ },
452
+ {
453
+ key: "githubRelease",
454
+ optIn: true,
455
+ render({ repositoryUrl }) {
456
+ const repository = getGithubRepository(repositoryUrl);
457
+ if (repository === void 0) return { unavailable: getNotGithubReason(repositoryUrl) };
458
+ const { owner, repo } = repository;
459
+ return [`[![GitHub Release](https://img.shields.io/github/v/release/${owner}/${repo}?label=Release)](https://github.com/${owner}/${repo}/releases/latest)`];
460
+ }
461
+ },
462
+ {
463
+ key: "npmDownloads",
464
+ optIn: true,
465
+ render: ({ npmPackages }) => mapNpmPackages(npmPackages, (packageName) => `[![NPM Downloads ${packageName}](https://img.shields.io/npm/dm/${packageName})](https://www.npmjs.com/package/${packageName})`)
466
+ },
467
+ {
468
+ key: "obsidianDownloads",
469
+ optIn: true,
470
+ render({ obsidianPluginId }) {
471
+ if (obsidianPluginId === void 0) return { unavailable: "no Obsidian plugin manifest.json was found" };
472
+ const statsUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugin-stats.json";
473
+ const query = `$["${obsidianPluginId}"].downloads`;
474
+ return [`[![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23A88BFA&label=Downloads&query=${encodeURIComponent(query)}&url=${encodeURIComponent(statsUrl)})](https://community.obsidian.md/plugins/${obsidianPluginId})`];
475
+ }
476
+ },
477
+ {
478
+ key: "bundleSize",
479
+ optIn: true,
480
+ render: ({ npmPackages }) => mapNpmPackages(npmPackages, (packageName) => `[![Bundle Size ${packageName}](https://img.shields.io/bundlephobia/minzip/${packageName}?label=Size)](https://bundlephobia.com/package/${packageName})`)
481
+ }
482
+ ];
319
483
  var badges_default = { badges: { async content(options) {
320
- const validOptions = z.object({
321
- custom: z.record(z.string(), z.object({
322
- image: z.string(),
323
- link: z.string()
324
- })).optional(),
325
- npm: z.array(z.string()).optional()
326
- }).optional().parse(options);
484
+ const validOptions = optionsSchema$2.parse(options ?? {});
327
485
  const metadata = await getReadmeMetadata();
328
- const { ciActionFileName, license, licenseUrl, name, repositoryUrl, usesGitLfs } = metadata;
486
+ const context = {
487
+ ...metadata,
488
+ npmPackages: getNpmPackages(validOptions.npm, metadata),
489
+ options: validOptions
490
+ };
329
491
  const badges = [];
330
- if (validOptions?.npm === void 0) {
331
- if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://www.npmjs.com/package/${name})`);
332
- } else for (const packageName of validOptions.npm) badges.push(`[![NPM Package ${packageName}](https://img.shields.io/npm/v/${packageName}.svg)](https://www.npmjs.com/package/${packageName})`);
333
- if (license !== void 0 && licenseUrl !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](${licenseUrl})`);
334
- if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
335
- if (usesGitLfs) badges.push("![Git LFS](https://img.shields.io/badge/Git%20LFS-enabled-F64935?logo=gitlfs&logoColor=white)");
336
- if (validOptions?.custom !== void 0) for (const [badgeName, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${badgeName}](${image})](${link})`);
492
+ for (const { key, optIn = false, render } of badgeDefinitions) {
493
+ const option = validOptions[key];
494
+ if (option === false || optIn && option === void 0) continue;
495
+ const result = render(context);
496
+ if (Array.isArray(result)) badges.push(...result);
497
+ else if (option !== void 0) throw new Error(`The "${key}" badge was requested, but ${result.unavailable}`);
498
+ }
499
+ for (const [badgeName, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${badgeName}](${image})](${link})`);
337
500
  return badges.join("\n");
338
501
  } } };
339
502
 
@@ -418,29 +581,51 @@ var code_default = { code: { async content(options) {
418
581
 
419
582
  //#endregion
420
583
  //#region src/lib/readme/rules/utilities/heading.ts
584
+ const MAX_HEADING_LEVEL = 6;
421
585
  /**
422
586
  * Option schema for the Markdown heading level (1-6) at which a rule emits its
423
- * section heading. Sub-headings, if any, are nested one level deeper. Rules
424
- * default to level 3, matching their placement under a level 2 section like
425
- * "Getting started" in the house readme template.
587
+ * section heading. Sub-headings, if any, are nested one level deeper, whether
588
+ * or not the section heading itself is shown. Each rule's default matches its
589
+ * placement in the house readme template.
426
590
  */
427
- const headingLevelSchema = z.number().int().min(1).max(6).optional();
591
+ const headingLevelSchema = z.number().int().min(1).max(MAX_HEADING_LEVEL).optional();
592
+ /**
593
+ * Option schema for a rule's section heading. `true` (the default) emits the
594
+ * rule's standard heading, `false` leaves the heading out, and a string
595
+ * replaces the heading text.
596
+ */
597
+ const headingSchema = z.union([z.boolean(), z.string().trim().min(1)]).optional();
428
598
  /**
429
599
  * The `#` prefix for a Markdown heading, clamped to the maximum level of 6.
430
600
  */
431
601
  function getHeadingPrefix(level) {
432
- return "#".repeat(Math.min(level, 6));
602
+ return "#".repeat(Math.min(level, MAX_HEADING_LEVEL));
603
+ }
604
+ /**
605
+ * The lines of a rule's section heading, including the blank line separating it
606
+ * from the section body, or no lines if the heading is suppressed.
607
+ *
608
+ * @param heading - Value of the rule's `heading` option
609
+ * @param level - Markdown heading level
610
+ * @param defaultText - Heading text used unless `heading` overrides it
611
+ */
612
+ function getHeadingLines(heading, level, defaultText) {
613
+ if (heading === false) return [];
614
+ const text = typeof heading === "string" ? heading : defaultText;
615
+ return [`${getHeadingPrefix(level)} ${text}`, ""];
433
616
  }
434
617
 
435
618
  //#endregion
436
619
  //#region src/lib/readme/rules/contributing.ts
437
620
  var contributing_default = { contributing: { async content(options) {
438
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
621
+ const validOptions = z.object({
622
+ heading: headingSchema,
623
+ headingLevel: headingLevelSchema
624
+ }).optional().parse(options);
439
625
  const { issuesUrl } = await getReadmeMetadata();
440
626
  if (issuesUrl === void 0) throw new Error("Could not find \"bugs.url\" entry in package.json");
441
627
  return [
442
- `${getHeadingPrefix(validOptions?.headingLevel ?? 2)} Contributing`,
443
- "",
628
+ ...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "Contributing"),
444
629
  `[Issues](${issuesUrl}) are welcome and appreciated.`,
445
630
  "",
446
631
  "Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.",
@@ -530,7 +715,10 @@ function getPlatformItem(key, version) {
530
715
  return version === void 0 || version.trim() === "" ? `- ${display}` : `- ${display} ${describeVersionRange(version)}`;
531
716
  }
532
717
  var dependencies_default = { dependencies: { async content(options) {
533
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
718
+ const validOptions = z.object({
719
+ heading: headingSchema,
720
+ headingLevel: headingLevelSchema
721
+ }).optional().parse(options);
534
722
  const { engines, operatingSystem, peerDependencies, runtimePlatform } = await getReadmeMetadata();
535
723
  const items = [];
536
724
  const engineEntries = Object.entries(engines ?? {});
@@ -551,11 +739,7 @@ var dependencies_default = { dependencies: { async content(options) {
551
739
  items.push(`- [${name}](${npmUrl}) \`${version}\` _(peer dependency)_`);
552
740
  }
553
741
  if (items.length === 0) return "";
554
- return [
555
- `${getHeadingPrefix(validOptions?.headingLevel ?? 3)} Dependencies`,
556
- "",
557
- ...items
558
- ].join("\n");
742
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 3, "Dependencies"), ...items].join("\n");
559
743
  } } };
560
744
 
561
745
  //#endregion
@@ -598,7 +782,10 @@ const TOOL_INFO = {
598
782
  }
599
783
  };
600
784
  var development_dependencies_default = { "development-dependencies": { async content(options) {
601
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
785
+ const validOptions = z.object({
786
+ heading: headingSchema,
787
+ headingLevel: headingLevelSchema
788
+ }).optional().parse(options);
602
789
  const { developmentDependencies } = await getReadmeMetadata();
603
790
  if (developmentDependencies === void 0) return "";
604
791
  const { packageManagers, runtimes } = developmentDependencies;
@@ -608,11 +795,7 @@ var development_dependencies_default = { "development-dependencies": { async con
608
795
  const display = info ? `[${info.display}](${info.url})` : name;
609
796
  items.push(version === void 0 || version === "" ? `- ${display}` : `- ${display} ${version}`);
610
797
  }
611
- return [
612
- `${getHeadingPrefix(validOptions?.headingLevel ?? 3)} Development dependencies`,
613
- "",
614
- ...items
615
- ].join("\n");
798
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 3, "Development dependencies"), ...items].join("\n");
616
799
  } } };
617
800
 
618
801
  //#endregion
@@ -625,12 +808,15 @@ var dev_dependencies_default = { "dev-dependencies": getSoleRule(development_dep
625
808
  //#endregion
626
809
  //#region src/lib/readme/rules/license.ts
627
810
  var license_default = { license: { async content(options) {
628
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
811
+ const validOptions = z.object({
812
+ heading: headingSchema,
813
+ headingLevel: headingLevelSchema
814
+ }).optional().parse(options);
629
815
  const { author, authorUrl, license, licenseFilePath } = await getReadmeMetadata();
630
816
  if (author === void 0) throw new Error("Could not find author name in project");
631
817
  if (license === void 0 || licenseFilePath === void 0) throw new Error("Could not find license for project");
632
818
  const authorDisplay = authorUrl === void 0 ? author : `[${author}](${authorUrl})`;
633
- return `${`${getHeadingPrefix(validOptions?.headingLevel ?? 2)} License`}\n[${license}](${licenseFilePath}) © ${authorDisplay}`;
819
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "License"), `[${license}](${licenseFilePath}) © ${authorDisplay}`].join("\n");
634
820
  } } };
635
821
 
636
822
  //#endregion
@@ -732,23 +918,23 @@ function getLibraryLines(name, bin, hasTypes, dev) {
732
918
  var install_default = { install: { async content(options) {
733
919
  const validOptions = z.object({
734
920
  dev: z.boolean().optional(),
921
+ heading: headingSchema,
735
922
  headingLevel: headingLevelSchema,
736
923
  homebrew: z.union([z.string(), z.literal(false)]).optional()
737
- }).optional().parse(options);
924
+ }).parse(options ?? {});
738
925
  const { bin, hasTypes, homebrewFormula, isLibrary, isNodePackage, isPublicNpmPackage, name, runtimePlatform } = await getReadmeMetadata();
739
926
  if (name === void 0) throw new Error("Could not find project name");
740
- const headingLevel = validOptions?.headingLevel ?? 3;
741
- const heading = `${getHeadingPrefix(headingLevel)} Installation`;
927
+ const headingLevel = validOptions.headingLevel ?? 3;
928
+ const headingLines = getHeadingLines(validOptions.heading, headingLevel, "Installation");
742
929
  const subheading = getHeadingPrefix(headingLevel + 1);
743
930
  if (isNodePackage) {
744
931
  if (!isPublicNpmPackage) throw new Error("Install instructions require a publishable package, but package.json sets \"private\": true");
745
- const homebrew = validOptions?.homebrew === void 0 ? homebrewFormula : validOptions.homebrew === false ? void 0 : validOptions.homebrew;
932
+ const homebrew = validOptions.homebrew === void 0 ? homebrewFormula : validOptions.homebrew === false ? void 0 : validOptions.homebrew;
746
933
  const hasCli = bin !== void 0 && bin.length > 0;
747
934
  const cliLines = getCliLines(name, homebrew);
748
- const libraryLines = getLibraryLines(name, bin, hasTypes, validOptions?.dev ?? false);
935
+ const libraryLines = getLibraryLines(name, bin, hasTypes, validOptions.dev ?? false);
749
936
  if (hasCli && isLibrary) return [
750
- heading,
751
- "",
937
+ ...headingLines,
752
938
  `There are several ways to install ${name} depending on how you're planning to use it:`,
753
939
  "",
754
940
  `${subheading} CLI`,
@@ -759,20 +945,12 @@ var install_default = { install: { async content(options) {
759
945
  "",
760
946
  ...libraryLines
761
947
  ].join("\n");
762
- return [
763
- heading,
764
- "",
765
- ...hasCli ? cliLines : libraryLines
766
- ].join("\n");
948
+ return [...headingLines, ...hasCli ? cliLines : libraryLines].join("\n");
767
949
  }
768
950
  const platform = (prefix) => runtimePlatform?.some((p) => p.startsWith(prefix));
769
951
  const command = platform("python") ? `pip install ${name}` : platform("rust") ? `cargo install ${name}` : platform("go") ? `go install ${name}@latest` : platform("ruby") ? `gem install ${name}` : void 0;
770
952
  if (command === void 0) throw new Error("Could not determine project ecosystem for install instructions");
771
- return [
772
- heading,
773
- "",
774
- ...codeBlock$1(command)
775
- ].join("\n");
953
+ return [...headingLines, ...codeBlock$1(command)].join("\n");
776
954
  } } };
777
955
 
778
956
  //#endregion
@@ -913,6 +1091,7 @@ const SKILLS_DIRECTORY = "skills";
913
1091
  const SKILL_FILE_NAME = "SKILL.md";
914
1092
  const GITHUB_REPOSITORY_REGEX = /^https:\/\/github\.com\/(?<slug>[^\/]+\/[^\/]+)$/v;
915
1093
  const WHITESPACE_REGEX = /\s+/gv;
1094
+ const FIRST_SENTENCE_REGEX = /^.*?(?<!\b(?:e\.g|i\.e|vs))[.!?](?=\s+\p{Lu})/v;
916
1095
  const skillFrontmatterSchema = z.object({
917
1096
  description: z.string().trim().min(1),
918
1097
  name: z.string().trim().min(1)
@@ -951,11 +1130,12 @@ async function findSkills(projectDirectory) {
951
1130
  if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
952
1131
  throw error;
953
1132
  }
1133
+ const filePath = `${SKILLS_DIRECTORY}/${entry.name}/${SKILL_FILE_NAME}`;
954
1134
  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`);
1135
+ if (!frontmatter.success) throw new Error(`Skill at "${filePath}" needs "name" and "description" strings in its frontmatter`);
956
1136
  skills.push({
957
1137
  ...frontmatter.data,
958
- filePath: `${SKILLS_DIRECTORY}/${entry.name}/${SKILL_FILE_NAME}`
1138
+ filePath
959
1139
  });
960
1140
  }
961
1141
  return skills.toSorted((a, b) => a.name.localeCompare(b.name));
@@ -964,58 +1144,66 @@ async function findSkills(projectDirectory) {
964
1144
  * The argument identifying a repository to the `skills add` command, which
965
1145
  * accepts an `owner/repo` shorthand for GitHub and full URLs for other hosts.
966
1146
  */
967
- function getSkillsCliSource(repoUrl) {
968
- return GITHUB_REPOSITORY_REGEX.exec(repoUrl)?.groups?.slug ?? repoUrl;
1147
+ function getSkillsCliSource(repositoryUrl) {
1148
+ return GITHUB_REPOSITORY_REGEX.exec(repositoryUrl)?.groups?.slug ?? repositoryUrl;
1149
+ }
1150
+ /**
1151
+ * The first sentence of a skill description, which by convention says what the
1152
+ * skill does, leaving out the guidance on when to use it that follows.
1153
+ */
1154
+ function getFirstSentence(description) {
1155
+ const singleLine = description.replaceAll(WHITESPACE_REGEX, " ");
1156
+ return FIRST_SENTENCE_REGEX.exec(singleLine)?.[0] ?? singleLine;
969
1157
  }
970
1158
  var skills_default = { skills: { async content(options) {
971
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
1159
+ const validOptions = z.object({
1160
+ heading: headingSchema,
1161
+ headingLevel: headingLevelSchema
1162
+ }).optional().parse(options);
972
1163
  const skills = await findSkills(process.cwd());
973
1164
  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
1165
  const { isPublicNpmPackage, name, repositoryUrl } = await getReadmeMetadata();
975
1166
  if (name === void 0) throw new Error("Could not find project name");
976
1167
  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`,
1168
+ const skillHeading = getHeadingPrefix(headingLevel + 1);
1169
+ const skillNoun = plur("skill", skills.length);
1170
+ const agentSkillsLink = `${skills.length === 1 ? "an" : skills.length} [${plur("Agent Skill", skills.length)}](https://agentskills.io)`;
1171
+ const skillsCliLink = "Vercel's [skills CLI](https://github.com/vercel-labs/skills)";
1172
+ const headingLines = getHeadingLines(validOptions?.heading, headingLevel, "Agent skills");
1173
+ const skillLines = [`Included ${skillNoun}:`, ...skills.flatMap((skill) => [
982
1174
  "",
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}:`,
1175
+ `${skillHeading} Skill: [\`${skill.name}\`](${skill.filePath})`,
984
1176
  "",
985
- ...skills.map((skill) => `- **[\`${skill.name}\`](${skill.filePath})**: ${skill.description.replaceAll(WHITESPACE_REGEX, " ")}`)
986
- ];
1177
+ getFirstSentence(skill.description)
1178
+ ])];
987
1179
  if (!isPublicNpmPackage) {
988
1180
  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
1181
  return [
990
- ...introLines,
1182
+ ...headingLines,
1183
+ `This project includes ${agentSkillsLink} to help coding agents work with ${name}.`,
1184
+ "",
1185
+ `To install the ${skillNoun}, run ${skillsCliLink}:`,
991
1186
  "",
992
- `Install ${skillPronoun} from the repository with the [\`skills\`](https://github.com/vercel-labs/skills) CLI:`,
1187
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`),
993
1188
  "",
994
- ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`)
1189
+ ...skillLines
995
1190
  ].join("\n");
996
1191
  }
997
1192
  return [
998
- ...introLines,
1193
+ ...headingLines,
1194
+ `This project bundles ${agentSkillsLink} in its published package to help coding agents work with ${name}.`,
999
1195
  "",
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:`,
1196
+ `To sync the ${skillNoun} into your project, run ${skillsCliLink} from your project root:`,
1005
1197
  "",
1006
1198
  ...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
1199
  ...repositoryUrl === void 0 ? [] : [
1010
1200
  "",
1011
- `${subheading} Install from the repository`,
1201
+ "Or install globally:",
1012
1202
  "",
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
- ]
1203
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)} --global`)
1204
+ ],
1205
+ "",
1206
+ ...skillLines
1019
1207
  ].join("\n");
1020
1208
  } } };
1021
1209
 
@@ -1032,6 +1220,7 @@ var table_of_contents_default = { "table-of-contents": {
1032
1220
  z.literal(5),
1033
1221
  z.literal(6)
1034
1222
  ]).optional(),
1223
+ heading: headingSchema,
1035
1224
  headingLevel: headingLevelSchema
1036
1225
  }).optional().parse(options);
1037
1226
  const result = toc(tree, {
@@ -1040,12 +1229,12 @@ var table_of_contents_default = { "table-of-contents": {
1040
1229
  tight: true
1041
1230
  });
1042
1231
  if (result.map === void 0) throw new Error("Could not generate table of contents");
1043
- const heading = `${getHeadingPrefix(validOptions?.headingLevel ?? 2)} Table of contents`;
1044
1232
  const rootWrapper = {
1045
1233
  children: result.map.children,
1046
1234
  type: "root"
1047
1235
  };
1048
- return [heading, remark().use(remarkGfm).stringify(rootWrapper).replaceAll("\n\n", "\n")].join("\n");
1236
+ const tocString = remark().use(remarkGfm).stringify(rootWrapper).replaceAll("\n\n", "\n");
1237
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "Table of contents"), tocString].join("\n");
1049
1238
  },
1050
1239
  order: 1
1051
1240
  } };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "3.5.0",
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.",
3
+ "version": "3.6.1",
4
+ "description": "CLI tool and TypeScript library to turn comments into content in Markdown files. MDAT makes it easy to generate and update dynamic boilerplate in project readmes.",
5
5
  "keywords": [
6
6
  "mdat",
7
7
  "markdown",
@@ -52,7 +52,7 @@
52
52
  "gray-matter-es": "^0.2.1",
53
53
  "lognow": "^0.8.4",
54
54
  "mdast-util-toc": "^7.1.0",
55
- "metascope": "^0.12.0",
55
+ "metascope": "^0.12.1",
56
56
  "path-type": "^6.0.0",
57
57
  "picocolors": "^1.1.1",
58
58
  "plur": "^6.0.0",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "devDependencies": {
73
73
  "@arethetypeswrong/core": "^0.18.5",
74
- "@kitschpatrol/shared-config": "^8.10.1",
74
+ "@kitschpatrol/shared-config": "^8.11.0",
75
75
  "@types/mdast": "^4.0.4",
76
76
  "@types/node": "~24.13.6",
77
77
  "@types/unist": "^3.0.3",
@@ -80,7 +80,7 @@
80
80
  "brewpub": "^0.4.0",
81
81
  "bumpp": "^12.3.0",
82
82
  "execa": "^10.0.1",
83
- "mdat-plugin-cli-help": "^3.3.0",
83
+ "mdat-plugin-cli-help": "^3.4.0",
84
84
  "mdat-plugin-example": "^3.0.0",
85
85
  "mdat-plugin-tldraw": "^3.0.0",
86
86
  "nanoid": "^6.0.1",
package/readme.md CHANGED
@@ -4,26 +4,19 @@
4
4
 
5
5
  <!-- /title -->
6
6
 
7
- <!-- badges({
8
- custom: {
9
- "Homebrew": {
10
- image:
11
- "https://img.shields.io/badge/Homebrew-kitschpatrol%2Ftap%2Fmdat-FBB040?logo=homebrew&logoColor=white",
12
- link: "https://github.com/kitschpatrol/homebrew-tap/blob/main/Formula/mdat.rb",
13
- },
14
- }
15
- }) -->
7
+ <!-- badges({ npmDownloads: true }) -->
16
8
 
17
9
  [![NPM Package mdat](https://img.shields.io/npm/v/mdat.svg)](https://www.npmjs.com/package/mdat)
18
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit)
19
11
  [![CI](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml)
20
- [![Homebrew](https://img.shields.io/badge/Homebrew-kitschpatrol%2Ftap%2Fmdat-FBB040?logo=homebrew&logoColor=white)](https://github.com/kitschpatrol/homebrew-tap/blob/main/Formula/mdat.rb)
12
+ [![Homebrew](https://img.shields.io/badge/Homebrew-kitschpatrol%2Ftap%2Fmdat-FBB040?logo=homebrew&logoColor=white)](https://github.com/kitschpatrol/homebrew-tap/blob/HEAD/Formula/mdat.rb)
13
+ [![NPM Downloads mdat](https://img.shields.io/npm/dm/mdat)](https://www.npmjs.com/package/mdat)
21
14
 
22
15
  <!-- /badges -->
23
16
 
24
17
  <!-- description -->
25
18
 
26
- **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.**
19
+ **CLI tool and TypeScript library to turn comments into content in Markdown files. MDAT makes it easy to generate and update dynamic boilerplate in project readmes.**
27
20
 
28
21
  <!-- /description -->
29
22
 
@@ -47,8 +40,8 @@
47
40
  - [Creating a rule plugin](#creating-a-rule-plugin)
48
41
  - [Available rule plugins](#available-rule-plugins)
49
42
  - [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)
43
+ - [Skill: `mdat`](#skill-mdat)
44
+ - [Skill: `mdat-plugin-authoring`](#skill-mdat-plugin-authoring)
52
45
  - [Migrating from 1.x to 2.x](#migrating-from-1x-to-2x)
53
46
  - [Flat CLI commands](#flat-cli-commands)
54
47
  - [Polyglot metadata](#polyglot-metadata)
@@ -74,9 +67,9 @@ MDAT is a CLI tool and library that uses HTML comments in Markdown files as plac
74
67
  <!-- tldraw({src: "assets/mdat-flow.tldr"}) -->
75
68
 
76
69
  <picture>
77
- <source media="(prefers-color-scheme: dark)" srcset="assets/mdat-flow-63a3366c-dark.svg">
78
- <source media="(prefers-color-scheme: light)" srcset="assets/mdat-flow-63a3366c-light.svg">
79
- <img alt="tldraw diagram" src="assets/mdat-flow-63a3366c-light.svg">
70
+ <source media="(prefers-color-scheme: dark)" srcset="assets/mdat-flow-c037b16c-dark.svg">
71
+ <source media="(prefers-color-scheme: light)" srcset="assets/mdat-flow-c037b16c-light.svg">
72
+ <img alt="tldraw diagram" src="assets/mdat-flow-c037b16c-light.svg">
80
73
  </picture>
81
74
 
82
75
  <!-- /tldraw -->
@@ -250,7 +243,9 @@ npm install mdat
250
243
 
251
244
  <!-- cli-help -->
252
245
 
253
- #### Command: `mdat`
246
+ #### Commands
247
+
248
+ ##### Command: `mdat`
254
249
 
255
250
  Work with MDAT placeholder comments in Markdown files.
256
251
 
@@ -274,7 +269,7 @@ mdat [command] [files..] [options]
274
269
 
275
270
  _See the sections below for more information on each subcommand._
276
271
 
277
- #### Subcommand: `mdat expand`
272
+ ##### Subcommand: `mdat expand`
278
273
 
279
274
  Expand MDAT placeholder comments. If no files are provided, the closest readme.md is expanded.
280
275
 
@@ -299,7 +294,7 @@ mdat expand [files..] [options]
299
294
  | `--help`<br>`-h` | Show help | `boolean` | |
300
295
  | `--version`<br>`-v` | Show version number | `boolean` | |
301
296
 
302
- #### Subcommand: `mdat collapse`
297
+ ##### Subcommand: `mdat collapse`
303
298
 
304
299
  Collapse MDAT placeholder comments. If no files are provided, the closest readme.md is collapsed.
305
300
 
@@ -323,7 +318,7 @@ mdat collapse [files..] [options]
323
318
  | `--help`<br>`-h` | Show help | `boolean` | |
324
319
  | `--version`<br>`-v` | Show version number | `boolean` | |
325
320
 
326
- #### Subcommand: `mdat strip`
321
+ ##### Subcommand: `mdat strip`
327
322
 
328
323
  Strip MDAT comments while preserving expanded content. If no files are provided, the closest readme.md is stripped.
329
324
 
@@ -347,7 +342,7 @@ mdat strip [files..] [options]
347
342
  | `--help`<br>`-h` | Show help | `boolean` | |
348
343
  | `--version`<br>`-v` | Show version number | `boolean` | |
349
344
 
350
- #### Subcommand: `mdat check`
345
+ ##### Subcommand: `mdat check`
351
346
 
352
347
  Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content.
353
348
 
@@ -369,7 +364,7 @@ mdat check [files..] [options]
369
364
  | `--help`<br>`-h` | Show help | `boolean` |
370
365
  | `--version`<br>`-v` | Show version number | `boolean` |
371
366
 
372
- #### Subcommand: `mdat create`
367
+ ##### Subcommand: `mdat create`
373
368
 
374
369
  Create a new Markdown file from a template.
375
370
 
@@ -592,7 +587,17 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
592
587
 
593
588
  ### Bundled rules
594
589
 
595
- Every rule that emits a section heading accepts a `headingLevel` option (`1`-`6`) to control the heading's Markdown level, with any sub-headings nested one level deeper. The defaults match each rule's placement in the bundled MDAT Readme template: `2` for top-level sections (`table-of-contents`, `contributing`, and `license`), and `3` for the rules that sit under "Getting started" (`dependencies`, `install`, and `development-dependencies`).
590
+ Every stand-alone rule that generates a section of the readme emits its own heading, and accepts two options to control it:
591
+
592
+ - `heading` is `true` by default, which emits the rule's standard heading. Pass a string to replace the heading text, or `false` to leave the heading out, e.g. when you've written your own heading above the comment, or when the same rule appears several times in one section.
593
+
594
+ - `headingLevel` (`1`-`6`) sets the heading's Markdown level. Any sub-headings are nested one level deeper, whether or not the heading itself is shown. The defaults match each rule's placement in the bundled MDAT Readme template: `2` for top-level sections (`table-of-contents`, `contributing`, `license`, and `skills`), and `3` for the rules that sit under "Getting started" (`dependencies`, `install`, and `development-dependencies`).
595
+
596
+ ```md
597
+ <!-- install({ heading: "Setup", headingLevel: 2 }) -->
598
+ ```
599
+
600
+ Rules that generate inline content, like `badges`, `code`, and `size`, have no heading. Rule plugins follow the same convention.
596
601
 
597
602
  #### Stand-alone
598
603
 
@@ -606,7 +611,28 @@ Every rule that emits a section heading accepts a `headingLevel` option (`1`-`6`
606
611
 
607
612
  - ##### `<!-- badges -->`
608
613
 
609
- Generates badges based on project metadata. Supports NPM version, license, CI status, and Git LFS badges.
614
+ Generates badges based on project metadata. Each badge has an option. Leave it out for the default behavior, pass `false` to hide the badge, or pass `true` to require it, which reports an error if the metadata the badge needs is missing. Two options also take a value that replaces the detected metadata.
615
+
616
+ | Option | Badge | Default | Value |
617
+ | ------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- |
618
+ | `npm` | NPM version, for each package | Shown for public packages | Package names (`string[]`), useful in monorepos |
619
+ | `license` | License | Shown when a license file matches a known SPDX license | |
620
+ | `ci` | GitHub Actions CI status | Shown when a workflow is named "CI" | |
621
+ | `gitLfs` | Git LFS | Shown when the repository tracks files with Git LFS | |
622
+ | `vscode` | VS Code Marketplace version | Shown for VS Code extensions hosted on GitHub | |
623
+ | `homebrew` | Homebrew tap | Shown when a `brewpub --tap` invocation is found in the package scripts | Formula (`"owner/tap/formula"`) |
624
+ | `githubRelease` | Latest GitHub release | Hidden | |
625
+ | `npmDownloads` | Monthly NPM downloads, for each package | Hidden | |
626
+ | `obsidianDownloads` | Obsidian community plugin downloads | Hidden | |
627
+ | `bundleSize` | [Bundlephobia](https://bundlephobia.com) minified and gzipped size, for each package | Hidden | |
628
+
629
+ Badges appear in the order of the table, followed by `custom` badges, which take a `Record<string, { image: string, link: string }>` keyed by alt text.
630
+
631
+ The download and size badges cover the packages listed in `npm`, or the current package if it's public, even when `npm` is `false`. VS Code extensions are identified by the `publisher` and `engines.vscode` fields in `package.json`. Shields.io retired its Marketplace badge, so the version is read from `package.json` in the GitHub repository instead, respecting `repository.directory` in monorepos. The Homebrew formula is detected the same way as in the `install` rule. The GitHub release badge needs a GitHub repository URL, and the Obsidian downloads badge reads the plugin ID from `manifest.json`.
632
+
633
+ ```md
634
+ <!-- badges({ ci: false, npmDownloads: true }) -->
635
+ ```
610
636
 
611
637
  - ##### `<!-- description -->`
612
638
 
@@ -665,11 +691,11 @@ Every rule that emits a section heading accepts a `headingLevel` option (`1`-`6`
665
691
 
666
692
  - ##### `<!-- skills -->`
667
693
 
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`.
694
+ 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 gives each one a "Skill:" sub-heading linking to its `SKILL.md` file, followed by the first sentence of the description from its frontmatter. Publishable npm packages get instructions for syncing skills from the installed package with the `experimental_sync` command of Vercel's [skills CLI](https://github.com/vercel-labs/skills), 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
695
 
670
696
  #### Compound
671
697
 
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 }]) -->`.
698
+ 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 }, { heading: "Legal", headingLevel: 3 }]) -->`.
673
699
 
674
700
  - ##### `<!-- header -->`
675
701
 
@@ -767,44 +793,41 @@ Transform a CLI command's `--help` output into Markdown tables. Recursively call
767
793
 
768
794
  Example: `<!-- cli-help -->`
769
795
 
770
- #### [mdat-plugin-api](https://github.com/kitschpatrol/mdat-plugin-api)
796
+ #### [mdat-plugin-api-help](https://github.com/kitschpatrol/mdat-plugin-api-help)
771
797
 
772
798
  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
799
 
774
- Example: `<!-- api({ format: "compact", include: ["sync*"] }) -->`
800
+ Example: `<!-- api-help({ format: "compact", include: ["sync*"] }) -->`
775
801
 
776
- <!-- //skills -->
802
+ <!-- skills -->
777
803
 
778
804
  ## Agent skills
779
805
 
780
- This project includes [Agent Skills](https://agentskills.io) that teach coding agents like Claude Code and Codex how to work with mdat:
806
+ This project bundles 2 [Agent Skills](https://agentskills.io) in its published package to help coding agents work with mdat.
781
807
 
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.
808
+ To sync the skills into your project, run Vercel's [skills CLI](https://github.com/vercel-labs/skills) from your project root:
784
809
 
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)
810
+ ```sh
811
+ npx skills experimental_sync
812
+ ```
788
813
 
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:
814
+ Or install globally:
790
815
 
791
816
  ```sh
792
- npx skills experimental_sync
817
+ npx skills add kitschpatrol/mdat --global
793
818
  ```
794
819
 
795
- Run the command again after upgrading `mdat` to refresh the copies. The `experimental_sync` command is experimental and its behavior may change.
820
+ Included skills:
796
821
 
797
- ### Install from the repository
822
+ ### Skill: [`mdat`](skills/mdat/SKILL.md)
798
823
 
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:
824
+ Use and configure mdat, the Markdown Autophagic Template CLI and library that expands HTML comment placeholders such as `<!-- title -->` in Markdown files.
800
825
 
801
- ```sh
802
- npx skills add kitschpatrol/mdat
803
- ```
826
+ ### Skill: [`mdat-plugin-authoring`](skills/mdat-plugin-authoring/SKILL.md)
804
827
 
805
- Skills installed this way follow the repository's default branch rather than your installed version of `mdat`.
828
+ Write custom mdat expansion rules and publishable mdat rule plugins (mdat-plugin-\* packages).
806
829
 
807
- <!-- //skills -->
830
+ <!-- /skills -->
808
831
 
809
832
  ## Migrating from 1.x to 2.x
810
833
 
@@ -906,6 +929,8 @@ There's quite a bit of prior art and similar explorations of this problem space:
906
929
 
907
930
  This project was split from a monorepo containing both `mdat` and `remark-mdat` into separate repos in July 2024.
908
931
 
932
+ The name "MDAT" is an acronym for the "Markdown Autophagic Template" system, so named because of the circularity of a templating system where the input file is also the output file.
933
+
909
934
  Run `pnpm bench` to compare performance against `test/benchmarks/baseline.json`. Run `pnpm bench:baseline` to refresh the saved results using Vitest 5's benchmark format. Both commands support filters such as `-t loadConfig`; refreshing a filtered subset preserves the other baselines. Use the same machine and Node.js version for meaningful comparisons.
910
935
 
911
936
  ## Maintainers
@@ -61,24 +61,26 @@ Only JSON5 values are accepted. No JavaScript is evaluated. Bundled rules valida
61
61
 
62
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
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. |
64
+ Rules that generate a section emit their own heading. Their `heading` option is `true` by default, takes a string to replace the heading text, and takes `false` to leave the heading out, e.g. under a hand-written heading or when a rule appears several times in one section. `headingLevel` sets the heading's level, with sub-headings nested one level deeper whether or not the heading is shown.
65
+
66
+ | Keyword | Output | Options |
67
+ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `title` | Project name as a heading. | |
69
+ | `banner` | A banner image found in the project directory. | |
70
+ | `badges` | npm version, license, CI, Git LFS, VS Code Marketplace, and Homebrew badges when detected, plus opt-in GitHub release, npm downloads, Obsidian downloads, and bundle size badges. | `npm`, `license`, `ci`, `gitLfs`, `vscode`, `homebrew`, `githubRelease`, `npmDownloads`, `obsidianDownloads`, `bundleSize`: `false` hides, `true` requires. Also `npm: string[]`, `homebrew: "owner/tap/formula"`, `custom: { [name]: { image, link } }` |
71
+ | `description` (alias `short-description`) | Project description. | |
72
+ | `table-of-contents` (alias `toc`) | Table of contents from the document headings. | `depth` (default `3`), `heading`, `headingLevel` (default `2`) |
73
+ | `install` (alias `installation`) | Install instructions for consumers of the package. Private packages are rejected. | `heading`, `headingLevel` (default `3`), `dev`, `homebrew` |
74
+ | `dependencies` | Runtime platform requirements and required peer dependencies. | `heading`, `headingLevel` (default `3`) |
75
+ | `development-dependencies` (alias `dev-dependencies`) | Tools needed to work on the project, from `devEngines` and `packageManager`. | `heading`, `headingLevel` (default `3`) |
76
+ | `contributing` | Invitation to open issues and pull requests. | `heading`, `headingLevel` (default `2`) |
77
+ | `license` | License section. | `heading`, `headingLevel` (default `2`) |
78
+ | `code` | A code block embedded from a file. | `file` (required), `language`, `trim` (default `true`) |
79
+ | `size` | A file's size. | `file` (required), `compression`: `none`, `gzip`, `brotli` |
80
+ | `size-table` | A table of file sizes with Gzip and Brotli columns. | `files` (required), `precision` (default `0`) |
81
+ | `skills` | Agent Skills found in `skills/<name>/SKILL.md` and how to install them. | `heading`, `headingLevel` (default `2`) |
82
+ | `header` | Compound: `title`, `banner`, `badges`, `short-description`. | Array, one entry per sub-rule in order. |
83
+ | `footer` | Compound: `contributing`, `license`. | Array, one entry per sub-rule in order. |
82
84
 
83
85
  File paths in `code`, `size`, and `size-table` resolve from the current working directory.
84
86
 
@@ -101,6 +101,14 @@ const metadata = await getContextMetadata()
101
101
 
102
102
  See the exported `ReadmeMetadata` type for the complete field list. Call `resetMetadataCaches()` between tests that change project files on disk.
103
103
 
104
+ ## Headings
105
+
106
+ Follow the convention of the bundled rules so that readmes stay consistent:
107
+
108
+ - A rule that generates a section emits its own heading. A rule that generates inline content, like a badge or a code block, has no heading.
109
+ - Accept a `heading` option. `true` is the default and emits the rule's standard heading, a string replaces the heading text, and `false` leaves the heading out.
110
+ - Accept a `headingLevel` option (`1`-`6`) for the heading's level. Nest any sub-headings one level deeper, whether or not the heading is shown. Default to the level that suits the section's usual place in a readme.
111
+
104
112
  ## Order and compound rules
105
113
 
106
114
  - `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.