mdat 3.4.1 → 3.6.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.
@@ -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,14 +26,13 @@ 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.1";
29
+ var version = "3.6.0";
29
30
 
30
31
  //#endregion
31
32
  //#region src/lib/deep-merge-defined.ts
32
33
  const deepmerge = deepmergeCustom({ mergeArrays: false });
33
34
  function stripUndefinedDeep(object) {
34
- if (Array.isArray(object)) return object.map((v) => v !== null && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
35
- 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 : {
36
36
  ...acc,
37
37
  [k]: v
38
38
  }, {});
@@ -192,6 +192,29 @@ function getHomebrewFormula(packageName, scripts) {
192
192
  }
193
193
  }
194
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
+ /**
195
218
  * Reset cached context metadata. Call between tests or when the underlying
196
219
  * project files may have changed on disk.
197
220
  *
@@ -201,12 +224,12 @@ function resetContextMetadata() {
201
224
  metascopeMetadata = void 0;
202
225
  }
203
226
  const readmeMetadataTemplate = defineTemplate((context) => {
204
- const { githubActions, gitStats, licenseFile, metascope, nodePackageJson } = context;
227
+ const { githubActions, gitStats, licenseFile, metascope, nodePackageJson, obsidianPluginManifestJson } = context;
205
228
  const codemeta = templates.codemetaJson(context, {});
206
229
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
207
230
  const licenseFileData = helpers.firstOf(licenseFile);
208
231
  const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
209
- 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, "");
210
233
  const bin = (() => {
211
234
  if (nodePackage === void 0) return;
212
235
  const binField = nodePackage.bin;
@@ -256,6 +279,9 @@ const readmeMetadataTemplate = defineTemplate((context) => {
256
279
  };
257
280
  })();
258
281
  const homebrewFormula = getHomebrewFormula(nodePackage?.name, nodePackage?.scripts);
282
+ const repositoryDirectory = getRepositoryDirectory(nodePackage);
283
+ const vscodeExtensionId = getVscodeExtensionId(nodePackage);
284
+ const obsidianPluginId = getObsidianPluginId(obsidianPluginManifestJson);
259
285
  const firstAuthor = helpers.firstOf(helpers.ensureArray(codemeta.author));
260
286
  return {
261
287
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
@@ -275,12 +301,15 @@ const readmeMetadataTemplate = defineTemplate((context) => {
275
301
  licenseFilePath: licenseFileData?.source,
276
302
  licenseUrl: licenseFileData?.data.match?.spdxUrl,
277
303
  name: codemeta.name,
304
+ obsidianPluginId,
278
305
  operatingSystem: codemeta.operatingSystem ?? getSupportedOperatingSystems(nodePackage?.os),
279
306
  peerDependencies,
280
307
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
281
- repositoryUrl: repoUrl,
308
+ repositoryDirectory,
309
+ repositoryUrl,
282
310
  runtimePlatform: codemeta.runtimePlatform,
283
- usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true
311
+ usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true,
312
+ vscodeExtensionId
284
313
  };
285
314
  });
286
315
  let readmeMetadata;
@@ -315,24 +344,159 @@ function resetMetadataCaches() {
315
344
 
316
345
  //#endregion
317
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
+ ];
318
483
  var badges_default = { badges: { async content(options) {
319
- const validOptions = z.object({
320
- custom: z.record(z.string(), z.object({
321
- image: z.string(),
322
- link: z.string()
323
- })).optional(),
324
- npm: z.array(z.string()).optional()
325
- }).optional().parse(options);
484
+ const validOptions = optionsSchema$2.parse(options ?? {});
326
485
  const metadata = await getReadmeMetadata();
327
- const { ciActionFileName, license, licenseUrl, name, repositoryUrl, usesGitLfs } = metadata;
486
+ const context = {
487
+ ...metadata,
488
+ npmPackages: getNpmPackages(validOptions.npm, metadata),
489
+ options: validOptions
490
+ };
328
491
  const badges = [];
329
- if (validOptions?.npm === void 0) {
330
- if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://www.npmjs.com/package/${name})`);
331
- } 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})`);
332
- if (license !== void 0 && licenseUrl !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](${licenseUrl})`);
333
- if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
334
- if (usesGitLfs) badges.push("![Git LFS](https://img.shields.io/badge/Git%20LFS-enabled-F64935?logo=gitlfs&logoColor=white)");
335
- 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})`);
336
500
  return badges.join("\n");
337
501
  } } };
338
502
 
@@ -417,29 +581,51 @@ var code_default = { code: { async content(options) {
417
581
 
418
582
  //#endregion
419
583
  //#region src/lib/readme/rules/utilities/heading.ts
584
+ const MAX_HEADING_LEVEL = 6;
420
585
  /**
421
586
  * Option schema for the Markdown heading level (1-6) at which a rule emits its
422
- * section heading. Sub-headings, if any, are nested one level deeper. Rules
423
- * default to level 3, matching their placement under a level 2 section like
424
- * "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.
590
+ */
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.
425
596
  */
426
- const headingLevelSchema = z.number().int().min(1).max(6).optional();
597
+ const headingSchema = z.union([z.boolean(), z.string().trim().min(1)]).optional();
427
598
  /**
428
599
  * The `#` prefix for a Markdown heading, clamped to the maximum level of 6.
429
600
  */
430
601
  function getHeadingPrefix(level) {
431
- 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}`, ""];
432
616
  }
433
617
 
434
618
  //#endregion
435
619
  //#region src/lib/readme/rules/contributing.ts
436
620
  var contributing_default = { contributing: { async content(options) {
437
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
621
+ const validOptions = z.object({
622
+ heading: headingSchema,
623
+ headingLevel: headingLevelSchema
624
+ }).optional().parse(options);
438
625
  const { issuesUrl } = await getReadmeMetadata();
439
626
  if (issuesUrl === void 0) throw new Error("Could not find \"bugs.url\" entry in package.json");
440
627
  return [
441
- `${getHeadingPrefix(validOptions?.headingLevel ?? 2)} Contributing`,
442
- "",
628
+ ...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "Contributing"),
443
629
  `[Issues](${issuesUrl}) are welcome and appreciated.`,
444
630
  "",
445
631
  "Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.",
@@ -529,7 +715,10 @@ function getPlatformItem(key, version) {
529
715
  return version === void 0 || version.trim() === "" ? `- ${display}` : `- ${display} ${describeVersionRange(version)}`;
530
716
  }
531
717
  var dependencies_default = { dependencies: { async content(options) {
532
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
718
+ const validOptions = z.object({
719
+ heading: headingSchema,
720
+ headingLevel: headingLevelSchema
721
+ }).optional().parse(options);
533
722
  const { engines, operatingSystem, peerDependencies, runtimePlatform } = await getReadmeMetadata();
534
723
  const items = [];
535
724
  const engineEntries = Object.entries(engines ?? {});
@@ -550,11 +739,7 @@ var dependencies_default = { dependencies: { async content(options) {
550
739
  items.push(`- [${name}](${npmUrl}) \`${version}\` _(peer dependency)_`);
551
740
  }
552
741
  if (items.length === 0) return "";
553
- return [
554
- `${getHeadingPrefix(validOptions?.headingLevel ?? 3)} Dependencies`,
555
- "",
556
- ...items
557
- ].join("\n");
742
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 3, "Dependencies"), ...items].join("\n");
558
743
  } } };
559
744
 
560
745
  //#endregion
@@ -597,7 +782,10 @@ const TOOL_INFO = {
597
782
  }
598
783
  };
599
784
  var development_dependencies_default = { "development-dependencies": { async content(options) {
600
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
785
+ const validOptions = z.object({
786
+ heading: headingSchema,
787
+ headingLevel: headingLevelSchema
788
+ }).optional().parse(options);
601
789
  const { developmentDependencies } = await getReadmeMetadata();
602
790
  if (developmentDependencies === void 0) return "";
603
791
  const { packageManagers, runtimes } = developmentDependencies;
@@ -607,11 +795,7 @@ var development_dependencies_default = { "development-dependencies": { async con
607
795
  const display = info ? `[${info.display}](${info.url})` : name;
608
796
  items.push(version === void 0 || version === "" ? `- ${display}` : `- ${display} ${version}`);
609
797
  }
610
- return [
611
- `${getHeadingPrefix(validOptions?.headingLevel ?? 3)} Development dependencies`,
612
- "",
613
- ...items
614
- ].join("\n");
798
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 3, "Development dependencies"), ...items].join("\n");
615
799
  } } };
616
800
 
617
801
  //#endregion
@@ -624,12 +808,15 @@ var dev_dependencies_default = { "dev-dependencies": getSoleRule(development_dep
624
808
  //#endregion
625
809
  //#region src/lib/readme/rules/license.ts
626
810
  var license_default = { license: { async content(options) {
627
- const validOptions = z.object({ headingLevel: headingLevelSchema }).optional().parse(options);
811
+ const validOptions = z.object({
812
+ heading: headingSchema,
813
+ headingLevel: headingLevelSchema
814
+ }).optional().parse(options);
628
815
  const { author, authorUrl, license, licenseFilePath } = await getReadmeMetadata();
629
816
  if (author === void 0) throw new Error("Could not find author name in project");
630
817
  if (license === void 0 || licenseFilePath === void 0) throw new Error("Could not find license for project");
631
818
  const authorDisplay = authorUrl === void 0 ? author : `[${author}](${authorUrl})`;
632
- return `${`${getHeadingPrefix(validOptions?.headingLevel ?? 2)} License`}\n[${license}](${licenseFilePath}) © ${authorDisplay}`;
819
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "License"), `[${license}](${licenseFilePath}) © ${authorDisplay}`].join("\n");
633
820
  } } };
634
821
 
635
822
  //#endregion
@@ -683,7 +870,7 @@ const listFormatter = new Intl.ListFormat("en", {
683
870
  style: "long",
684
871
  type: "conjunction"
685
872
  });
686
- function codeBlock(command) {
873
+ function codeBlock$1(command) {
687
874
  return [
688
875
  "```sh",
689
876
  command,
@@ -698,17 +885,17 @@ function getCliLines(name, homebrewFormula) {
698
885
  return [
699
886
  "Run it once without installing:",
700
887
  "",
701
- ...codeBlock(`npx ${name}`),
888
+ ...codeBlock$1(`npx ${name}`),
702
889
  ...homebrewFormula === void 0 ? [] : [
703
890
  "",
704
891
  "Or install it globally with Homebrew:",
705
892
  "",
706
- ...codeBlock(`brew install ${homebrewFormula}`)
893
+ ...codeBlock$1(`brew install ${homebrewFormula}`)
707
894
  ],
708
895
  "",
709
896
  "Or install it globally with npm:",
710
897
  "",
711
- ...codeBlock(`npm install --global ${name}`)
898
+ ...codeBlock$1(`npm install --global ${name}`)
712
899
  ];
713
900
  }
714
901
  /**
@@ -720,34 +907,34 @@ function getLibraryLines(name, bin, hasTypes, dev) {
720
907
  if (bin === void 0 || bin.length === 0) return [
721
908
  `${lead}:`,
722
909
  "",
723
- ...codeBlock(command)
910
+ ...codeBlock$1(command)
724
911
  ];
725
912
  return [
726
913
  `${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
914
  "",
728
- ...codeBlock(command)
915
+ ...codeBlock$1(command)
729
916
  ];
730
917
  }
731
918
  var install_default = { install: { async content(options) {
732
919
  const validOptions = z.object({
733
920
  dev: z.boolean().optional(),
921
+ heading: headingSchema,
734
922
  headingLevel: headingLevelSchema,
735
923
  homebrew: z.union([z.string(), z.literal(false)]).optional()
736
- }).optional().parse(options);
924
+ }).parse(options ?? {});
737
925
  const { bin, hasTypes, homebrewFormula, isLibrary, isNodePackage, isPublicNpmPackage, name, runtimePlatform } = await getReadmeMetadata();
738
926
  if (name === void 0) throw new Error("Could not find project name");
739
- const headingLevel = validOptions?.headingLevel ?? 3;
740
- const heading = `${getHeadingPrefix(headingLevel)} Installation`;
927
+ const headingLevel = validOptions.headingLevel ?? 3;
928
+ const headingLines = getHeadingLines(validOptions.heading, headingLevel, "Installation");
741
929
  const subheading = getHeadingPrefix(headingLevel + 1);
742
930
  if (isNodePackage) {
743
931
  if (!isPublicNpmPackage) throw new Error("Install instructions require a publishable package, but package.json sets \"private\": true");
744
- 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;
745
933
  const hasCli = bin !== void 0 && bin.length > 0;
746
934
  const cliLines = getCliLines(name, homebrew);
747
- const libraryLines = getLibraryLines(name, bin, hasTypes, validOptions?.dev ?? false);
935
+ const libraryLines = getLibraryLines(name, bin, hasTypes, validOptions.dev ?? false);
748
936
  if (hasCli && isLibrary) return [
749
- heading,
750
- "",
937
+ ...headingLines,
751
938
  `There are several ways to install ${name} depending on how you're planning to use it:`,
752
939
  "",
753
940
  `${subheading} CLI`,
@@ -758,22 +945,21 @@ var install_default = { install: { async content(options) {
758
945
  "",
759
946
  ...libraryLines
760
947
  ].join("\n");
761
- return [
762
- heading,
763
- "",
764
- ...hasCli ? cliLines : libraryLines
765
- ].join("\n");
948
+ return [...headingLines, ...hasCli ? cliLines : libraryLines].join("\n");
766
949
  }
767
950
  const platform = (prefix) => runtimePlatform?.some((p) => p.startsWith(prefix));
768
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;
769
952
  if (command === void 0) throw new Error("Could not determine project ecosystem for install instructions");
770
- return [
771
- heading,
772
- "",
773
- ...codeBlock(command)
774
- ].join("\n");
953
+ return [...headingLines, ...codeBlock$1(command)].join("\n");
775
954
  } } };
776
955
 
956
+ //#endregion
957
+ //#region src/lib/readme/rules/installation.ts
958
+ /**
959
+ * Simple alias for `install`
960
+ */
961
+ var installation_default = { installation: getSoleRule(install_default) };
962
+
777
963
  //#endregion
778
964
  //#region src/lib/readme/rules/utilities/size/size-report.ts
779
965
  const brotliCompressAsync = promisify(brotliCompress);
@@ -899,6 +1085,127 @@ var size_table_default = { "size-table": { async content(options) {
899
1085
  });
900
1086
  } } };
901
1087
 
1088
+ //#endregion
1089
+ //#region src/lib/readme/rules/skills.ts
1090
+ const SKILLS_DIRECTORY = "skills";
1091
+ const SKILL_FILE_NAME = "SKILL.md";
1092
+ const GITHUB_REPOSITORY_REGEX = /^https:\/\/github\.com\/(?<slug>[^\/]+\/[^\/]+)$/v;
1093
+ const WHITESPACE_REGEX = /\s+/gv;
1094
+ const FIRST_SENTENCE_REGEX = /^.*?(?<!\b(?:e\.g|i\.e|vs))[.!?](?=\s+\p{Lu})/v;
1095
+ const skillFrontmatterSchema = z.object({
1096
+ description: z.string().trim().min(1),
1097
+ name: z.string().trim().min(1)
1098
+ });
1099
+ function codeBlock(command) {
1100
+ return [
1101
+ "```sh",
1102
+ command,
1103
+ "```"
1104
+ ];
1105
+ }
1106
+ /**
1107
+ * Find Agent Skills in the project's `skills` directory, where each skill is a
1108
+ * subdirectory containing a `SKILL.md` file with `name` and `description`
1109
+ * frontmatter.
1110
+ *
1111
+ * @throws {Error} If a `SKILL.md` file has missing or invalid frontmatter
1112
+ */
1113
+ async function findSkills(projectDirectory) {
1114
+ const skillsDirectory = path.join(projectDirectory, SKILLS_DIRECTORY);
1115
+ let entries;
1116
+ try {
1117
+ entries = await fs.readdir(skillsDirectory, { withFileTypes: true });
1118
+ } catch (error) {
1119
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
1120
+ throw error;
1121
+ }
1122
+ const skills = [];
1123
+ for (const entry of entries) {
1124
+ if (!entry.isDirectory()) continue;
1125
+ const skillFilePath = path.join(skillsDirectory, entry.name, SKILL_FILE_NAME);
1126
+ let skillFile;
1127
+ try {
1128
+ skillFile = await fs.readFile(skillFilePath, "utf8");
1129
+ } catch (error) {
1130
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
1131
+ throw error;
1132
+ }
1133
+ const frontmatter = skillFrontmatterSchema.safeParse(matter(skillFile).data);
1134
+ if (!frontmatter.success) throw new Error(`Skill at "${path.relative(projectDirectory, skillFilePath)}" needs "name" and "description" strings in its frontmatter`);
1135
+ skills.push({
1136
+ ...frontmatter.data,
1137
+ filePath: `${SKILLS_DIRECTORY}/${entry.name}/${SKILL_FILE_NAME}`
1138
+ });
1139
+ }
1140
+ return skills.toSorted((a, b) => a.name.localeCompare(b.name));
1141
+ }
1142
+ /**
1143
+ * The argument identifying a repository to the `skills add` command, which
1144
+ * accepts an `owner/repo` shorthand for GitHub and full URLs for other hosts.
1145
+ */
1146
+ function getSkillsCliSource(repositoryUrl) {
1147
+ return GITHUB_REPOSITORY_REGEX.exec(repositoryUrl)?.groups?.slug ?? repositoryUrl;
1148
+ }
1149
+ /**
1150
+ * The first sentence of a skill description, which by convention says what the
1151
+ * skill does, leaving out the guidance on when to use it that follows.
1152
+ */
1153
+ function getFirstSentence(description) {
1154
+ const singleLine = description.replaceAll(WHITESPACE_REGEX, " ");
1155
+ return FIRST_SENTENCE_REGEX.exec(singleLine)?.[0] ?? singleLine;
1156
+ }
1157
+ var skills_default = { skills: { async content(options) {
1158
+ const validOptions = z.object({
1159
+ heading: headingSchema,
1160
+ headingLevel: headingLevelSchema
1161
+ }).optional().parse(options);
1162
+ const skills = await findSkills(process.cwd());
1163
+ 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`);
1164
+ const { isPublicNpmPackage, name, repositoryUrl } = await getReadmeMetadata();
1165
+ if (name === void 0) throw new Error("Could not find project name");
1166
+ const headingLevel = validOptions?.headingLevel ?? 2;
1167
+ const skillHeading = getHeadingPrefix(headingLevel + 1);
1168
+ const skillNoun = plur("skill", skills.length);
1169
+ const agentSkillsLink = `${skills.length === 1 ? "an" : skills.length} [${plur("Agent Skill", skills.length)}](https://agentskills.io)`;
1170
+ const skillsCliLink = "Vercel's [skills CLI](https://github.com/vercel-labs/skills)";
1171
+ const headingLines = getHeadingLines(validOptions?.heading, headingLevel, "Agent skills");
1172
+ const skillLines = [`Included ${skillNoun}:`, ...skills.flatMap((skill) => [
1173
+ "",
1174
+ `${skillHeading} Skill: [\`${skill.name}\`](${skill.filePath})`,
1175
+ "",
1176
+ getFirstSentence(skill.description)
1177
+ ])];
1178
+ if (!isPublicNpmPackage) {
1179
+ if (repositoryUrl === void 0) throw new Error("Skill install instructions require either a publishable npm package or a repository URL in the project metadata");
1180
+ return [
1181
+ ...headingLines,
1182
+ `This project includes ${agentSkillsLink} to help coding agents work with ${name}.`,
1183
+ "",
1184
+ `To install the ${skillNoun}, run ${skillsCliLink}:`,
1185
+ "",
1186
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`),
1187
+ "",
1188
+ ...skillLines
1189
+ ].join("\n");
1190
+ }
1191
+ return [
1192
+ ...headingLines,
1193
+ `This project bundles ${agentSkillsLink} in its published package to help coding agents work with ${name}.`,
1194
+ "",
1195
+ `To sync the ${skillNoun} into your project, run ${skillsCliLink} from your project root:`,
1196
+ "",
1197
+ ...codeBlock("npx skills experimental_sync"),
1198
+ ...repositoryUrl === void 0 ? [] : [
1199
+ "",
1200
+ "Or install globally:",
1201
+ "",
1202
+ ...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)} --global`)
1203
+ ],
1204
+ "",
1205
+ ...skillLines
1206
+ ].join("\n");
1207
+ } } };
1208
+
902
1209
  //#endregion
903
1210
  //#region src/lib/readme/rules/table-of-contents.ts
904
1211
  var table_of_contents_default = { "table-of-contents": {
@@ -912,6 +1219,7 @@ var table_of_contents_default = { "table-of-contents": {
912
1219
  z.literal(5),
913
1220
  z.literal(6)
914
1221
  ]).optional(),
1222
+ heading: headingSchema,
915
1223
  headingLevel: headingLevelSchema
916
1224
  }).optional().parse(options);
917
1225
  const result = toc(tree, {
@@ -920,12 +1228,12 @@ var table_of_contents_default = { "table-of-contents": {
920
1228
  tight: true
921
1229
  });
922
1230
  if (result.map === void 0) throw new Error("Could not generate table of contents");
923
- const heading = `${getHeadingPrefix(validOptions?.headingLevel ?? 2)} Table of contents`;
924
1231
  const rootWrapper = {
925
1232
  children: result.map.children,
926
1233
  type: "root"
927
1234
  };
928
- return [heading, remark().use(remarkGfm).stringify(rootWrapper).replaceAll("\n\n", "\n")].join("\n");
1235
+ const tocString = remark().use(remarkGfm).stringify(rootWrapper).replaceAll("\n\n", "\n");
1236
+ return [...getHeadingLines(validOptions?.heading, validOptions?.headingLevel ?? 2, "Table of contents"), tocString].join("\n");
929
1237
  },
930
1238
  order: 1
931
1239
  } };
@@ -951,10 +1259,12 @@ var rules_default = {
951
1259
  ...footer_default,
952
1260
  ...header_default,
953
1261
  ...install_default,
1262
+ ...installation_default,
954
1263
  ...license_default,
955
1264
  ...short_description_default,
956
1265
  ...size_default,
957
1266
  ...size_table_default,
1267
+ ...skills_default,
958
1268
  ...table_of_contents_default,
959
1269
  ...title_default,
960
1270
  ...toc_default
@@ -1238,19 +1548,19 @@ var mdat_readme_default = "<!-- title -->\n\n<!-- banner -->\n\n<!-- badges -->\
1238
1548
 
1239
1549
  //#endregion
1240
1550
  //#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";
1551
+ 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
1552
 
1243
1553
  //#endregion
1244
1554
  //#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";
1555
+ 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
1556
 
1247
1557
  //#endregion
1248
1558
  //#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";
1559
+ 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
1560
 
1251
1561
  //#endregion
1252
1562
  //#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";
1563
+ 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
1564
 
1255
1565
  //#endregion
1256
1566
  //#region src/lib/readme/templates/index.ts
@@ -1283,6 +1593,8 @@ var templates_default = {
1283
1593
 
1284
1594
  //#endregion
1285
1595
  //#region src/lib/readme/create.ts
1596
+ const USAGE_HEADING = "\n## Usage\n";
1597
+ const NEXT_SECTION_REGEX = /\n(?:## |<!-- (?:contributing|footer) -->)/v;
1286
1598
  /**
1287
1599
  * Creates a new readme file interactively.
1288
1600
  *
@@ -1339,7 +1651,8 @@ async function createReadme(options) {
1339
1651
  expand: true,
1340
1652
  template: Object.keys(templates_default)[0]
1341
1653
  }, options ?? {});
1342
- const templateString = getTemplateForConfig(resolvedOptions.template, resolvedOptions.compound);
1654
+ const baseTemplateString = getTemplateForConfig(resolvedOptions.template, resolvedOptions.compound);
1655
+ const templateString = (await findSkills(process.cwd())).length > 0 ? addSkillsPlaceholder(baseTemplateString) : baseTemplateString;
1343
1656
  const readmePath = path.join(resolvedOptions.output, "readme.md");
1344
1657
  if (!resolvedOptions.overwrite) {
1345
1658
  let exists = true;
@@ -1358,6 +1671,20 @@ async function createReadme(options) {
1358
1671
  }
1359
1672
  return readmePath;
1360
1673
  }
1674
+ /**
1675
+ * Adds a `<!-- skills -->` placeholder to a template, between the end of its
1676
+ * "Usage" section and whatever follows it.
1677
+ *
1678
+ * @throws {Error} If the template has no "Usage" section followed by another
1679
+ * section
1680
+ */
1681
+ function addSkillsPlaceholder(template) {
1682
+ const usageIndex = template.indexOf(USAGE_HEADING);
1683
+ const nextSectionOffset = usageIndex === -1 ? void 0 : NEXT_SECTION_REGEX.exec(template.slice(usageIndex + 10))?.index;
1684
+ if (nextSectionOffset === void 0) throw new Error("Could not find a \"## Usage\" section followed by another section in the template");
1685
+ const insertIndex = usageIndex + 10 + nextSectionOffset + 1;
1686
+ return `${template.slice(0, insertIndex)}<!-- skills -->\n\n${template.slice(insertIndex)}`;
1687
+ }
1361
1688
  function getTemplateForConfig(templateKey, compound) {
1362
1689
  if (!Object.hasOwn(templates_default, templateKey)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
1363
1690
  return templates_default[templateKey].content[compound ? "compound" : "explicit"];