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 +1 -1
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.js +1 -1
- package/dist/{lib-D3Djaa1D.js → lib-BU0oLDpf.js} +283 -94
- package/package.json +5 -5
- package/readme.md +70 -45
- package/skills/mdat/SKILL.md +20 -18
- package/skills/mdat-plugin-authoring/SKILL.md +8 -0
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-
|
|
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";
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) => `[](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" } : [`[}-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 [`[](${repositoryUrl}/actions/workflows/${ciActionFileName})`];
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
key: "gitLfs",
|
|
428
|
+
render: ({ usesGitLfs }) => usesGitLfs ? [""] : { 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 [`[}&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 [`[}-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 [`[](https://github.com/${owner}/${repo}/releases/latest)`];
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
key: "npmDownloads",
|
|
464
|
+
optIn: true,
|
|
465
|
+
render: ({ npmPackages }) => mapNpmPackages(npmPackages, (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 [`[}&url=${encodeURIComponent(statsUrl)})](https://community.obsidian.md/plugins/${obsidianPluginId})`];
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
key: "bundleSize",
|
|
479
|
+
optIn: true,
|
|
480
|
+
render: ({ npmPackages }) => mapNpmPackages(npmPackages, (packageName) => `[](https://bundlephobia.com/package/${packageName})`)
|
|
481
|
+
}
|
|
482
|
+
];
|
|
319
483
|
var badges_default = { badges: { async content(options) {
|
|
320
|
-
const validOptions =
|
|
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
|
|
486
|
+
const context = {
|
|
487
|
+
...metadata,
|
|
488
|
+
npmPackages: getNpmPackages(validOptions.npm, metadata),
|
|
489
|
+
options: validOptions
|
|
490
|
+
};
|
|
329
491
|
const badges = [];
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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(`[](${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
|
|
424
|
-
*
|
|
425
|
-
*
|
|
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(
|
|
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,
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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
|
-
}).
|
|
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
|
|
741
|
-
const
|
|
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
|
|
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
|
|
935
|
+
const libraryLines = getLibraryLines(name, bin, hasTypes, validOptions.dev ?? false);
|
|
749
936
|
if (hasCli && isLibrary) return [
|
|
750
|
-
|
|
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 "${
|
|
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
|
|
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(
|
|
968
|
-
return GITHUB_REPOSITORY_REGEX.exec(
|
|
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({
|
|
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
|
|
978
|
-
const skillNoun =
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
|
|
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
|
-
|
|
1175
|
+
`${skillHeading} Skill: [\`${skill.name}\`](${skill.filePath})`,
|
|
984
1176
|
"",
|
|
985
|
-
|
|
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
|
-
...
|
|
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
|
-
`
|
|
1187
|
+
...codeBlock(`npx skills add ${getSkillsCliSource(repositoryUrl)}`),
|
|
993
1188
|
"",
|
|
994
|
-
...
|
|
1189
|
+
...skillLines
|
|
995
1190
|
].join("\n");
|
|
996
1191
|
}
|
|
997
1192
|
return [
|
|
998
|
-
...
|
|
1193
|
+
...headingLines,
|
|
1194
|
+
`This project bundles ${agentSkillsLink} in its published package to help coding agents work with ${name}.`,
|
|
999
1195
|
"",
|
|
1000
|
-
`
|
|
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
|
-
|
|
1201
|
+
"Or install globally:",
|
|
1012
1202
|
"",
|
|
1013
|
-
`
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "CLI tool and TypeScript library
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
[](https://www.npmjs.com/package/mdat)
|
|
18
10
|
[](https://opensource.org/license/mit)
|
|
19
11
|
[](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml)
|
|
20
|
-
[](https://github.com/kitschpatrol/homebrew-tap/blob/
|
|
12
|
+
[](https://github.com/kitschpatrol/homebrew-tap/blob/HEAD/Formula/mdat.rb)
|
|
13
|
+
[](https://www.npmjs.com/package/mdat)
|
|
21
14
|
|
|
22
15
|
<!-- /badges -->
|
|
23
16
|
|
|
24
17
|
<!-- description -->
|
|
25
18
|
|
|
26
|
-
**CLI tool and TypeScript library
|
|
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
|
-
- [
|
|
51
|
-
- [
|
|
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-
|
|
78
|
-
<source media="(prefers-color-scheme: light)" srcset="assets/mdat-flow-
|
|
79
|
-
<img alt="tldraw diagram" src="assets/mdat-flow-
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
<!--
|
|
802
|
+
<!-- skills -->
|
|
777
803
|
|
|
778
804
|
## Agent skills
|
|
779
805
|
|
|
780
|
-
This project
|
|
806
|
+
This project bundles 2 [Agent Skills](https://agentskills.io) in its published package to help coding agents work with mdat.
|
|
781
807
|
|
|
782
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
810
|
+
```sh
|
|
811
|
+
npx skills experimental_sync
|
|
812
|
+
```
|
|
788
813
|
|
|
789
|
-
|
|
814
|
+
Or install globally:
|
|
790
815
|
|
|
791
816
|
```sh
|
|
792
|
-
npx skills
|
|
817
|
+
npx skills add kitschpatrol/mdat --global
|
|
793
818
|
```
|
|
794
819
|
|
|
795
|
-
|
|
820
|
+
Included skills:
|
|
796
821
|
|
|
797
|
-
###
|
|
822
|
+
### Skill: [`mdat`](skills/mdat/SKILL.md)
|
|
798
823
|
|
|
799
|
-
|
|
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
|
-
|
|
802
|
-
npx skills add kitschpatrol/mdat
|
|
803
|
-
```
|
|
826
|
+
### Skill: [`mdat-plugin-authoring`](skills/mdat-plugin-authoring/SKILL.md)
|
|
804
827
|
|
|
805
|
-
|
|
828
|
+
Write custom mdat expansion rules and publishable mdat rule plugins (mdat-plugin-\* packages).
|
|
806
829
|
|
|
807
|
-
<!--
|
|
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
|
package/skills/mdat/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
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.
|