shipseal 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -49,7 +49,7 @@ jobs:
49
49
  steps:
50
50
  - uses: actions/checkout@v7
51
51
  with: { fetch-depth: 0 }
52
- - uses: akii09/shipseal@v1
52
+ - uses: akii09/shipseal@v0.0.7
53
53
  env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
54
54
  ```
55
55
 
package/dist/cli.js CHANGED
@@ -307,8 +307,11 @@ function deterministicCopy(facts, maxHighlights = 4) {
307
307
  const features = (facts.release?.features ?? []).map((item) => item.value);
308
308
  const fixes = (facts.release?.fixes ?? []).map((item) => item.value);
309
309
  const breaking = (facts.release?.breaking ?? []).map((item) => item.value);
310
- const headlineSource = breaking[0] ?? features[0] ?? fixes[0];
311
- const headline = headlineSource !== void 0 ? cleanLine(firstSentence(headlineSource)) : version === void 0 ? name : `${name} ${version}`;
310
+ const headline = [
311
+ ...breaking,
312
+ ...features,
313
+ ...fixes
314
+ ].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS) ?? (version === void 0 ? name : `${name} ${version}`);
312
315
  const tagline = facts.project.tagline?.value;
313
316
  const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
314
317
  const highlights = [];
@@ -319,12 +322,27 @@ function deterministicCopy(facts, maxHighlights = 4) {
319
322
  for (const line of breaking) addHighlight(line, "Breaking: ");
320
323
  for (const line of features) addHighlight(line);
321
324
  for (const line of fixes) addHighlight(line);
322
- return {
325
+ const copy = {
323
326
  headline,
324
327
  subheadline,
325
328
  highlights,
326
329
  cta: ctaFor(facts)
327
330
  };
331
+ const snippet = facts.release?.codeSnippet?.value.code;
332
+ if (snippet !== void 0 && isInstallOnly(snippet)) copy.codeTitle = "Get started";
333
+ return copy;
334
+ }
335
+ /**
336
+ * Is this snippet nothing but install commands?
337
+ *
338
+ * Pairing "npm i thing" with a release headline reads as though the release was about
339
+ * installing, which is how v0.0.6 shipped a bug-fix headline over the README's install block.
340
+ * Install commands are still worth showing, they just need their own title.
341
+ */
342
+ const INSTALL_COMMAND = /^\s*(?:\$\s*)?(?:npm\s+(?:i|install|add)|pnpm\s+(?:i|install|add|dlx)|yarn\s+(?:add|install)|bun\s+(?:a|add|install)|npx|deno\s+add|pip\s+install|cargo\s+add|go\s+get|gem\s+install|brew\s+install)\b/;
343
+ function isInstallOnly(code) {
344
+ const lines = code.split("\n").map((line) => line.replace(/\s+#.*$/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
345
+ return lines.length > 0 && lines.every((line) => INSTALL_COMMAND.test(line));
328
346
  }
329
347
  function formatCount$1(value) {
330
348
  return value.toLocaleString("en-US");
@@ -363,6 +381,12 @@ function firstSentence(text) {
363
381
  const collapsed = text.replace(/\s+/g, " ").trim();
364
382
  return /^(.+?[.?!])\s+[A-Z]/.exec(collapsed)?.[1] ?? collapsed;
365
383
  }
384
+ /**
385
+ * Longest headline that fits two lines at the minimum font size on a 1200 wide card.
386
+ * Measured rather than guessed: §13.1 originally said 48, which rejects most real changelog
387
+ * sentences, and the fitting pass comfortably handles more.
388
+ */
389
+ const HEADLINE_MAX_CHARS = 72;
366
390
  function cleanLine(text) {
367
391
  const stripped = text.replace(/\s*\u2014\s*/g, ": ").replace(/\s+\u2013\s+/g, ", ").replaceAll("–", "-").replaceAll("!", ".").replace(/\s+/g, " ").trim();
368
392
  const noTrail = stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
@@ -579,7 +603,7 @@ const codeCard = {
579
603
  });
580
604
  return {
581
605
  props: {
582
- headline: copy.headline,
606
+ headline: copy.codeTitle ?? copy.headline,
583
607
  headingFamily: brand.fonts.heading.family,
584
608
  monoFamily: brand.fonts.mono.family,
585
609
  headingWeight: brand.fonts.heading.weight,
@@ -1460,7 +1484,9 @@ const releaseHighlights = {
1460
1484
  style: {
1461
1485
  display: "flex",
1462
1486
  flexDirection: "column",
1463
- gap: 16
1487
+ flexGrow: 1,
1488
+ justifyContent: props.highlights.length > 2 ? "flex-start" : "center",
1489
+ gap: props.highlights.length > 2 ? 16 : 20
1464
1490
  },
1465
1491
  children: props.highlights.map((line, index) => {
1466
1492
  const fitted = ctx.fitted[`highlight-${String(index)}`];
@@ -1684,6 +1710,20 @@ async function copyForTemplate(templateId, copy, facts, theme) {
1684
1710
  };
1685
1711
  }
1686
1712
 
1713
+ //#endregion
1714
+ //#region src/facts/fact.ts
1715
+ function fact(value, provenance) {
1716
+ const fetchedAt = provenance.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1717
+ return {
1718
+ value,
1719
+ provenance: {
1720
+ source: provenance.source,
1721
+ ref: provenance.ref,
1722
+ fetchedAt
1723
+ }
1724
+ };
1725
+ }
1726
+
1687
1727
  //#endregion
1688
1728
  //#region src/facts/merge.ts
1689
1729
  function mergeFacts(parts) {
@@ -1756,20 +1796,6 @@ function isCompleteRelease(release) {
1756
1796
  return release.version !== void 0 && release.tag !== void 0 && release.date !== void 0;
1757
1797
  }
1758
1798
 
1759
- //#endregion
1760
- //#region src/facts/fact.ts
1761
- function fact(value, provenance) {
1762
- const fetchedAt = provenance.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1763
- return {
1764
- value,
1765
- provenance: {
1766
- source: provenance.source,
1767
- ref: provenance.ref,
1768
- fetchedAt
1769
- }
1770
- };
1771
- }
1772
-
1773
1799
  //#endregion
1774
1800
  //#region src/sources/bench-file.ts
1775
1801
  const metricSchema = z.object({
@@ -1844,6 +1870,117 @@ async function collectBenchFile(cwd, filePath = ".shipseal/bench.json") {
1844
1870
  return { bench };
1845
1871
  }
1846
1872
 
1873
+ //#endregion
1874
+ //#region src/sources/readme.ts
1875
+ async function collectReadme(cwd) {
1876
+ let markdown;
1877
+ try {
1878
+ markdown = await readFile(join(cwd, "README.md"), "utf8");
1879
+ } catch {
1880
+ return {};
1881
+ }
1882
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1883
+ const out = {};
1884
+ const h1 = extractH1(markdown);
1885
+ const tagline = extractTagline(markdown);
1886
+ const snippet = extractFirstCodeFence(markdown);
1887
+ const project = {};
1888
+ if (h1 !== void 0) project.name = fact(h1, {
1889
+ source: "readme",
1890
+ ref: "README.md H1",
1891
+ fetchedAt
1892
+ });
1893
+ if (tagline !== void 0) project.tagline = fact(tagline, {
1894
+ source: "readme",
1895
+ ref: "README.md first paragraph",
1896
+ fetchedAt
1897
+ });
1898
+ if (project.name !== void 0 || project.tagline !== void 0) out.project = project;
1899
+ if (snippet !== void 0) out.release = { codeSnippet: fact(snippet, {
1900
+ source: "readme",
1901
+ ref: "README.md first fenced code block",
1902
+ fetchedAt
1903
+ }) };
1904
+ return out;
1905
+ }
1906
+ async function collectConfiguredSnippet(cwd, spec) {
1907
+ const parsed = parseSnippetSpec(spec);
1908
+ if (parsed === void 0) return {};
1909
+ let contents;
1910
+ try {
1911
+ contents = await readFile(join(cwd, parsed.path), "utf8");
1912
+ } catch {
1913
+ return {};
1914
+ }
1915
+ const slice = contents.split(/\r?\n/).slice(parsed.start - 1, parsed.end).join("\n");
1916
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1917
+ return { release: { codeSnippet: fact({
1918
+ code: slice,
1919
+ lang: langFromPath(parsed.path)
1920
+ }, {
1921
+ source: "user-config",
1922
+ ref: spec,
1923
+ fetchedAt
1924
+ }) } };
1925
+ }
1926
+ /**
1927
+ * Strip fenced code and HTML before reading prose out of a README.
1928
+ *
1929
+ * Skipping blocks that merely *start* with a fence is not enough. A `#` comment inside a YAML
1930
+ * example reads as a markdown H1, and a blank line inside a fence makes its body look like a
1931
+ * paragraph. Shipseal's own README hit both: the detected name became
1932
+ * ".github/workflows/shipseal.yml" and the tagline became a chunk of workflow YAML.
1933
+ */
1934
+ function stripNonProse(markdown) {
1935
+ return markdown.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<[^>]+>/g, "");
1936
+ }
1937
+ function extractH1(markdown) {
1938
+ const heading = /^#\s+(.+)$/m.exec(stripNonProse(markdown))?.[1];
1939
+ if (heading === void 0) return;
1940
+ return heading.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
1941
+ }
1942
+ function extractTagline(markdown) {
1943
+ const prose = stripNonProse(markdown);
1944
+ const h1 = /^#\s+.+$/m.exec(prose);
1945
+ const start = h1 === null ? 0 : (h1.index ?? 0) + h1[0].length;
1946
+ for (const block of prose.slice(start).split(/\n\s*\n/)) {
1947
+ const cleaned = stripBadges(block).trim();
1948
+ if (cleaned.length >= 20 && !cleaned.startsWith("#") && !cleaned.startsWith("```")) return cleaned;
1949
+ }
1950
+ }
1951
+ function extractFirstCodeFence(markdown) {
1952
+ const match = /```([a-zA-Z0-9+-]+)\n([\s\S]*?)```/.exec(markdown);
1953
+ if (match === null || match[1] === void 0 || match[2] === void 0) return;
1954
+ return {
1955
+ lang: match[1],
1956
+ code: match[2].replace(/\n$/, "")
1957
+ };
1958
+ }
1959
+ function parseSnippetSpec(spec) {
1960
+ const match = /^(?<path>.+)#L(?<start>\d+)(?:-L(?<end>\d+))?$/.exec(spec);
1961
+ if (match === null || match.groups === void 0) return;
1962
+ const path = match.groups.path;
1963
+ const start = Number.parseInt(match.groups.start ?? "0", 10);
1964
+ const end = Number.parseInt(match.groups.end ?? match.groups.start ?? "0", 10);
1965
+ if (path === void 0 || start < 1 || end < start) return;
1966
+ return {
1967
+ path,
1968
+ start,
1969
+ end
1970
+ };
1971
+ }
1972
+ function langFromPath(path) {
1973
+ const ext = extname(path).replace(".", "");
1974
+ if (ext === "ts") return "ts";
1975
+ if (ext === "tsx") return "tsx";
1976
+ if (ext === "js" || ext === "mjs" || ext === "cjs") return "js";
1977
+ if (ext === "jsx") return "jsx";
1978
+ return ext.length > 0 ? ext : "text";
1979
+ }
1980
+ function stripBadges(text) {
1981
+ return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
1982
+ }
1983
+
1847
1984
  //#endregion
1848
1985
  //#region src/sources/changelog.ts
1849
1986
  async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
@@ -1891,6 +2028,15 @@ async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
1891
2028
  fixes,
1892
2029
  breaking
1893
2030
  } };
2031
+ const snippet = extractFirstCodeFence(section.body);
2032
+ if (snippet !== void 0) out.release = {
2033
+ ...out.release,
2034
+ codeSnippet: fact(snippet, {
2035
+ source: "changelog",
2036
+ ref: `${changelogPath} ${section.heading} first fenced code block`,
2037
+ fetchedAt
2038
+ })
2039
+ };
1894
2040
  if (section.date !== void 0) out.release = {
1895
2041
  ...out.release,
1896
2042
  date: fact(section.date, {
@@ -2277,6 +2423,7 @@ function lastPageFromLink(link) {
2277
2423
  //#endregion
2278
2424
  //#region src/sources/npm.ts
2279
2425
  const API_ROOT = "https://api.npmjs.org/downloads/point/last-week";
2426
+ const REGISTRY_ROOT = "https://registry.npmjs.org";
2280
2427
  const pointSchema = z.object({
2281
2428
  downloads: z.number().int().nonnegative(),
2282
2429
  start: z.string().optional(),
@@ -2314,6 +2461,31 @@ async function npmGet(url, fetchImpl, cache) {
2314
2461
  cache.set(url, json);
2315
2462
  return json;
2316
2463
  }
2464
+ /**
2465
+ * Is this name actually published?
2466
+ *
2467
+ * The downloads endpoint cannot answer this: it 404s for a package published minutes ago that
2468
+ * has no download data yet, which is exactly the case a first release is in. The registry
2469
+ * document is the authority.
2470
+ *
2471
+ * Only a definitive 404 counts as "no". A network error, a rate limit or anything else returns
2472
+ * undefined, so an unreachable registry never removes a call to action that was probably right.
2473
+ */
2474
+ async function npmPackageExists(name, fetchImpl = fetch) {
2475
+ const trimmed = name.trim();
2476
+ if (trimmed.length === 0) return false;
2477
+ let response;
2478
+ try {
2479
+ response = await fetchImpl(`${REGISTRY_ROOT}/${encodeURIComponent(trimmed)}`, {
2480
+ method: "HEAD",
2481
+ headers: { accept: "application/json" }
2482
+ });
2483
+ } catch {
2484
+ return;
2485
+ }
2486
+ if (response.status === 404) return false;
2487
+ return response.ok ? true : void 0;
2488
+ }
2317
2489
 
2318
2490
  //#endregion
2319
2491
  //#region src/sources/package-json.ts
@@ -2343,9 +2515,10 @@ async function collectPackageJson(cwd, packagePath = "package.json") {
2343
2515
  const pkg = pkgSchema.safeParse(parsed);
2344
2516
  if (!pkg.success) return {};
2345
2517
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2346
- const project = { name: fact(stripScope$1(pkg.data.name ?? basenameFromPath(cwd)), {
2518
+ const rawName = pkg.data.private === true ? void 0 : pkg.data.name;
2519
+ const project = { name: fact(stripScope$1(rawName ?? basenameFromPath(cwd)), {
2347
2520
  source: "package-json",
2348
- ref: `${packagePath}#name`,
2521
+ ref: rawName === void 0 ? `${packagePath} directory name` : `${packagePath}#name`,
2349
2522
  fetchedAt
2350
2523
  }) };
2351
2524
  if (pkg.data.description !== void 0 && pkg.data.description.length > 0) project.tagline = fact(pkg.data.description, {
@@ -2409,117 +2582,6 @@ function normalizeGitUrl$1(url) {
2409
2582
  return url.replace(/^git\+/, "").replace(/\.git$/, "");
2410
2583
  }
2411
2584
 
2412
- //#endregion
2413
- //#region src/sources/readme.ts
2414
- async function collectReadme(cwd) {
2415
- let markdown;
2416
- try {
2417
- markdown = await readFile(join(cwd, "README.md"), "utf8");
2418
- } catch {
2419
- return {};
2420
- }
2421
- const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2422
- const out = {};
2423
- const h1 = extractH1(markdown);
2424
- const tagline = extractTagline(markdown);
2425
- const snippet = extractFirstCodeFence(markdown);
2426
- const project = {};
2427
- if (h1 !== void 0) project.name = fact(h1, {
2428
- source: "readme",
2429
- ref: "README.md H1",
2430
- fetchedAt
2431
- });
2432
- if (tagline !== void 0) project.tagline = fact(tagline, {
2433
- source: "readme",
2434
- ref: "README.md first paragraph",
2435
- fetchedAt
2436
- });
2437
- if (project.name !== void 0 || project.tagline !== void 0) out.project = project;
2438
- if (snippet !== void 0) out.release = { codeSnippet: fact(snippet, {
2439
- source: "readme",
2440
- ref: "README.md first fenced code block",
2441
- fetchedAt
2442
- }) };
2443
- return out;
2444
- }
2445
- async function collectConfiguredSnippet(cwd, spec) {
2446
- const parsed = parseSnippetSpec(spec);
2447
- if (parsed === void 0) return {};
2448
- let contents;
2449
- try {
2450
- contents = await readFile(join(cwd, parsed.path), "utf8");
2451
- } catch {
2452
- return {};
2453
- }
2454
- const slice = contents.split(/\r?\n/).slice(parsed.start - 1, parsed.end).join("\n");
2455
- const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
2456
- return { release: { codeSnippet: fact({
2457
- code: slice,
2458
- lang: langFromPath(parsed.path)
2459
- }, {
2460
- source: "user-config",
2461
- ref: spec,
2462
- fetchedAt
2463
- }) } };
2464
- }
2465
- /**
2466
- * Strip fenced code and HTML before reading prose out of a README.
2467
- *
2468
- * Skipping blocks that merely *start* with a fence is not enough. A `#` comment inside a YAML
2469
- * example reads as a markdown H1, and a blank line inside a fence makes its body look like a
2470
- * paragraph. Shipseal's own README hit both: the detected name became
2471
- * ".github/workflows/shipseal.yml" and the tagline became a chunk of workflow YAML.
2472
- */
2473
- function stripNonProse(markdown) {
2474
- return markdown.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<[^>]+>/g, "");
2475
- }
2476
- function extractH1(markdown) {
2477
- const heading = /^#\s+(.+)$/m.exec(stripNonProse(markdown))?.[1];
2478
- if (heading === void 0) return;
2479
- return heading.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
2480
- }
2481
- function extractTagline(markdown) {
2482
- const prose = stripNonProse(markdown);
2483
- const h1 = /^#\s+.+$/m.exec(prose);
2484
- const start = h1 === null ? 0 : (h1.index ?? 0) + h1[0].length;
2485
- for (const block of prose.slice(start).split(/\n\s*\n/)) {
2486
- const cleaned = stripBadges(block).trim();
2487
- if (cleaned.length >= 20 && !cleaned.startsWith("#") && !cleaned.startsWith("```")) return cleaned;
2488
- }
2489
- }
2490
- function extractFirstCodeFence(markdown) {
2491
- const match = /```([a-zA-Z0-9+-]+)\n([\s\S]*?)```/.exec(markdown);
2492
- if (match === null || match[1] === void 0 || match[2] === void 0) return;
2493
- return {
2494
- lang: match[1],
2495
- code: match[2].replace(/\n$/, "")
2496
- };
2497
- }
2498
- function parseSnippetSpec(spec) {
2499
- const match = /^(?<path>.+)#L(?<start>\d+)(?:-L(?<end>\d+))?$/.exec(spec);
2500
- if (match === null || match.groups === void 0) return;
2501
- const path = match.groups.path;
2502
- const start = Number.parseInt(match.groups.start ?? "0", 10);
2503
- const end = Number.parseInt(match.groups.end ?? match.groups.start ?? "0", 10);
2504
- if (path === void 0 || start < 1 || end < start) return;
2505
- return {
2506
- path,
2507
- start,
2508
- end
2509
- };
2510
- }
2511
- function langFromPath(path) {
2512
- const ext = extname(path).replace(".", "");
2513
- if (ext === "ts") return "ts";
2514
- if (ext === "tsx") return "tsx";
2515
- if (ext === "js" || ext === "mjs" || ext === "cjs") return "js";
2516
- if (ext === "jsx") return "jsx";
2517
- return ext.length > 0 ? ext : "text";
2518
- }
2519
- function stripBadges(text) {
2520
- return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
2521
- }
2522
-
2523
2585
  //#endregion
2524
2586
  //#region src/sources/collect.ts
2525
2587
  async function collectFacts(options) {
@@ -2534,13 +2596,15 @@ async function collectFacts(options) {
2534
2596
  const remote = await collectGitRemote(options.cwd);
2535
2597
  const changelog = await collectBestChangelog(options.cwd, version, options.changelogPath ?? "CHANGELOG.md", options.packagePath);
2536
2598
  const readme = await collectReadme(options.cwd);
2599
+ const snippet = options.snippet !== void 0 && options.snippet !== null && options.snippet.length > 0 ? await collectConfiguredSnippet(options.cwd, options.snippet) : {};
2537
2600
  const parts = [
2538
- remote,
2539
2601
  pkg,
2602
+ pkg.project?.npmPackage === void 0 ? await collectWorkspaceNpmPackage(options.cwd, options.skipNetwork === true ? {} : { verify: async (name) => npmPackageExists(name, options.fetchImpl ?? fetch) }) : {},
2603
+ remote,
2540
2604
  git,
2541
- readme,
2605
+ snippet,
2542
2606
  changelog,
2543
- options.snippet !== void 0 && options.snippet !== null && options.snippet.length > 0 ? await collectConfiguredSnippet(options.cwd, options.snippet) : {}
2607
+ readme
2544
2608
  ];
2545
2609
  if (options.event.kind === "bench") parts.push(await collectBenchFile(options.cwd, options.benchFile ?? ".shipseal/bench.json"));
2546
2610
  if (options.skipNetwork !== true) {
@@ -2581,6 +2645,31 @@ async function collectBestChangelog(cwd, version, changelogPath, packagePath) {
2581
2645
  const candidates = await workspaceChangelogs(cwd);
2582
2646
  return (await Promise.all(candidates.map((candidate) => collectChangelog(cwd, version, candidate)))).find((facts) => changelogHasNotes(facts)) ?? {};
2583
2647
  }
2648
+ /**
2649
+ * Find the one publishable package in a workspace.
2650
+ *
2651
+ * Only when there is exactly one candidate: two publishable packages make the CTA a guess, and
2652
+ * guessing which one to tell people to install is worse than falling back to the repository
2653
+ * URL. `--package` resolves the ambiguity explicitly.
2654
+ */
2655
+ async function collectWorkspaceNpmPackage(cwd, options = {}) {
2656
+ const paths = await workspaceManifests(cwd);
2657
+ const found = (await Promise.all(paths.map((path) => collectPackageJson(cwd, path)))).map((facts, index) => ({
2658
+ name: facts.project?.npmPackage?.value,
2659
+ path: paths[index] ?? ""
2660
+ })).filter((entry) => entry.name !== void 0);
2661
+ const only = found.length === 1 ? found[0] : void 0;
2662
+ if (only === void 0) return {};
2663
+ if (options.verify !== void 0 && await options.verify(only.name) === false) return {};
2664
+ return { project: { npmPackage: fact(only.name, {
2665
+ source: "package-json",
2666
+ ref: `${only.path}#name`,
2667
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2668
+ }) } };
2669
+ }
2670
+ async function workspaceManifests(cwd) {
2671
+ return (await workspaceChangelogs(cwd)).map((path) => path.replace(/CHANGELOG\.md$/, "package.json"));
2672
+ }
2584
2673
  async function workspaceChangelogs(cwd) {
2585
2674
  return (await Promise.all(["packages", "apps"].map(async (root) => {
2586
2675
  let entries;