@kitschpatrol/chagent 0.5.0 → 0.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.
package/dist/bin/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { C as defaultModels, E as version, S as setLogger, T as maxTimeoutMinutes, a as writeChangelog, d as efforts, f as rootChangelogs, i as generateChangelog, n as previewChangelog, r as reviewChangelog, t as assertReviewTerminal, u as agentNames, w as defaults } from "../review-Bl10DPO7.js";
3
- import { n as assertAmendable, t as amendRelease } from "../lib-DVZXk4YE.js";
4
- import { t as createProgressLine } from "../progress-DQtRnH1N.js";
2
+ import { C as defaultModels, E as version, S as setLogger, T as maxTimeoutMinutes, a as writeChangelog, d as efforts, f as rootChangelogs, i as generateChangelog, n as previewChangelog, r as reviewChangelog, t as assertReviewTerminal, u as agentNames, w as defaults } from "../review-cNqsjqhu.js";
3
+ import { n as assertAmendable, t as amendRelease } from "../lib-BkbC8akL.js";
4
+ import { t as createProgressLine } from "../progress-BYDpiH8C.js";
5
5
  import { createLogger } from "lognow";
6
6
  import path from "node:path";
7
7
  import yargs from "yargs";
package/dist/lib/bumpp.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a as writeChangelog, g as localDate, i as generateChangelog, r as reviewChangelog, t as assertReviewTerminal, y as git } from "../review-Bl10DPO7.js";
2
- import { t as createProgressLine } from "../progress-DQtRnH1N.js";
1
+ import { a as writeChangelog, g as localDate, i as generateChangelog, r as reviewChangelog, t as assertReviewTerminal, y as git } from "../review-cNqsjqhu.js";
2
+ import { t as createProgressLine } from "../progress-BYDpiH8C.js";
3
3
 
4
4
  // src/lib/bumpp.ts
5
5
  const TAG_TOKEN = /\{(version|oldVersion|tag|releaseType|major|minor|patch|date)\}/gv;
package/dist/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as setLogger, a as writeChangelog, i as generateChangelog, l as createAgent, n as previewChangelog, o as defineConfig, r as reviewChangelog, s as loadConfig } from "../review-Bl10DPO7.js";
2
- import { t as amendRelease } from "../lib-DVZXk4YE.js";
1
+ import { S as setLogger, a as writeChangelog, i as generateChangelog, l as createAgent, n as previewChangelog, o as defineConfig, r as reviewChangelog, s as loadConfig } from "../review-cNqsjqhu.js";
2
+ import { t as amendRelease } from "../lib-BkbC8akL.js";
3
3
 
4
4
  export { amendRelease, createAgent, defineConfig, generateChangelog, loadConfig, previewChangelog, reviewChangelog, setLogger, writeChangelog };
@@ -1,4 +1,4 @@
1
- import { _ as manifestAt, a as writeChangelog, b as isAncestor, c as resolveConfig, h as isVersionOnlyChange, m as inspectRepository, p as LOCKFILE_PATH, s as loadConfig, v as tagVersion, x as runCommand, y as git } from "./review-Bl10DPO7.js";
1
+ import { _ as manifestAt, a as writeChangelog, b as isAncestor, c as resolveConfig, h as isVersionOnlyChange, m as inspectRepository, p as LOCKFILE_PATH, s as loadConfig, v as tagVersion, x as runCommand, y as git } from "./review-cNqsjqhu.js";
2
2
  import path from "node:path";
3
3
  import { realpath } from "node:fs/promises";
4
4
  import semver from "semver";
@@ -38,7 +38,7 @@ function createProgressLine(stream) {
38
38
  tick += 1;
39
39
  const elapsed = formatElapsed(performance.now() - started);
40
40
  const { columns = 0 } = stream;
41
- const width = columns > 0 ? Math.max(0, columns - 1 - frame.length - 2) : Infinity;
41
+ const width = columns > 0 ? Math.max(0, columns - 1 - frame.length - 2) : 1e400;
42
42
  const text = `${message.slice(0, Math.max(0, width - elapsed.length - 1))} ${elapsed}`.slice(0, width);
43
43
  return `\r${styleText("magenta", frame, { stream })}${FRAME_PADDING}${text}${CLEAR_TO_LINE_END}`;
44
44
  };
@@ -15,7 +15,7 @@ import { isCancel, select } from "@clack/prompts";
15
15
 
16
16
  // package.json
17
17
  var name = "@kitschpatrol/chagent";
18
- var version = "0.5.0";
18
+ var version = "0.6.0";
19
19
  var engines = { "node": "^24.16.0 || >=26.3.0" };
20
20
 
21
21
  // src/lib/defaults.ts
@@ -453,10 +453,26 @@ async function leadsSeries(root, candidate, candidates) {
453
453
  function pendingTag(pkg, version, previous, patterns) {
454
454
  return (previous?.tag?.includes(previous.version) === true ? previous.tag.replace(previous.version, "{version}") : patterns?.[0] ?? "v{version}").replaceAll("{name}", () => pkg.name).replaceAll("{version}", () => version);
455
455
  }
456
+ /**
457
+ * Order releases by Git ancestry, using SemVer only for tags on the same
458
+ * commit.
459
+ */
460
+ async function sortReleases(repository, releases) {
461
+ const history = await git(repository.root, [
462
+ "rev-list",
463
+ "--topo-order",
464
+ "--reverse",
465
+ repository.head
466
+ ]);
467
+ const positions = new Map(history.split("\n").map((ref, index) => [ref, index]));
468
+ return releases.toSorted((a, b) => {
469
+ const difference = (positions.get(a.ref) ?? -1) - (positions.get(b.ref) ?? -1);
470
+ return difference === 0 ? semver.compare(a.version, b.version) : difference;
471
+ });
472
+ }
456
473
  /** Resolve release intervals from reachable tags and locally bumped manifests. */
457
474
  async function resolveReleases(repository, pkg, config, context) {
458
- const releases = await taggedReleases(repository, pkg, config);
459
- releases.sort((a, b) => semver.compare(a.version, b.version));
475
+ const releases = await sortReleases(repository, await taggedReleases(repository, pkg, config));
460
476
  for (let index = 1; index < releases.length; index++) {
461
477
  const current = releases[index];
462
478
  const previous = await precedingRelease(repository.root, releases.slice(0, index), current.ref);
@@ -465,7 +481,7 @@ async function resolveReleases(repository, pkg, config, context) {
465
481
  }
466
482
  const latest = releases.at(-1);
467
483
  const currentVersion = pkg.version ?? (pkg.directory === "." ? context?.version : void 0);
468
- if (await isPendingRelease(repository, pkg, latest, context) && currentVersion !== void 0) releases.push({
484
+ if (await isPendingRelease(repository, pkg, releases, context) && currentVersion !== void 0) releases.push({
469
485
  date: context?.date ?? localDate(),
470
486
  pending: true,
471
487
  previousRef: latest?.ref,
@@ -617,13 +633,13 @@ async function taggedReleases(repository, pkg, config) {
617
633
  async function precedingRelease(root, candidates, ref) {
618
634
  for (const candidate of candidates.toReversed()) if (await isAncestor(root, candidate.ref, ref)) return candidate;
619
635
  }
620
- async function isPendingRelease(repository, pkg, latest, context) {
636
+ async function isPendingRelease(repository, pkg, releases, context) {
621
637
  const currentVersion = pkg.version ?? (pkg.directory === "." ? context?.version : void 0);
622
638
  const committed = await manifestAt(repository.root, pkg.directory, repository.head);
623
639
  const versionChanged = currentVersion !== void 0 && currentVersion !== committed?.version;
624
640
  const parent = await manifestAt(repository.root, pkg.directory, repository.head + "^");
625
641
  const committedBump = parent?.version !== void 0 && parent.version !== committed?.version;
626
- return currentVersion !== void 0 && semver.valid(currentVersion) !== null && (latest === void 0 || semver.gt(currentVersion, latest.version)) && (versionChanged || context !== void 0 || committedBump);
642
+ return currentVersion !== void 0 && semver.valid(currentVersion) !== null && releases.every((release) => release.version !== semver.valid(currentVersion)) && (versionChanged || context !== void 0 || committedBump && releases.every((release) => release.ref !== repository.head));
627
643
  }
628
644
 
629
645
  // src/lib/cache.ts
@@ -1459,7 +1475,7 @@ function releaseLink(github, release) {
1459
1475
  return release.previousTag === void 0 ? `${base}/releases/tag/${encodeURIComponent(release.tag)}` : `${base}/compare/${encodeURIComponent(release.previousTag)}...${encodeURIComponent(release.tag)}`;
1460
1476
  }
1461
1477
  /** Insert new sections and update only the draft and its comparison link. */
1462
- function updateMarkdown(original, entries, nodeException, github) {
1478
+ function updateMarkdown(original, entries, nodeException, github, releaseOrder) {
1463
1479
  const content = original ?? "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n";
1464
1480
  const parsed = parseChangelog(content);
1465
1481
  const { newline } = parsed;
@@ -1468,7 +1484,10 @@ function updateMarkdown(original, entries, nodeException, github) {
1468
1484
  const definitions = [];
1469
1485
  let { footer } = parsed;
1470
1486
  const draft = parsed.sections.find((section) => section.version === "Unreleased");
1471
- const released = parsed.sections.filter((section) => section.version !== "Unreleased").map((section) => content.slice(section.start, section.end));
1487
+ const released = parsed.sections.filter((section) => section.version !== "Unreleased").map((section) => ({
1488
+ text: content.slice(section.start, section.end),
1489
+ version: section.version
1490
+ }));
1472
1491
  let draftText = draft === void 0 ? "" : content.slice(draft.start, draft.end);
1473
1492
  for (const entry of entries) {
1474
1493
  const { existingBody, release } = entry;
@@ -1485,7 +1504,10 @@ function updateMarkdown(original, entries, nodeException, github) {
1485
1504
  const section = heading + newline + newline + body.trim() + newline + newline;
1486
1505
  if (release.version === "Unreleased") draftText = section;
1487
1506
  else {
1488
- additions.push(section);
1507
+ additions.push({
1508
+ text: section,
1509
+ version: release.version
1510
+ });
1489
1511
  if (existingBody !== void 0) draftText = "";
1490
1512
  }
1491
1513
  if (link === void 0) continue;
@@ -1497,9 +1519,13 @@ function updateMarkdown(original, entries, nodeException, github) {
1497
1519
  }
1498
1520
  if (entries.length === 0 && header === parsed.header) return content;
1499
1521
  if (draftText === "") footer = footer.replace(UNRELEASED_REFERENCE, "");
1500
- const generated = sectionSpacing(sectionSpacing(header, newline) + draftText, newline) + additions.toReversed().join("");
1501
- const preserved = released.join("") + footer;
1502
- return appendDefinitions(preserved === "" ? generated.trimEnd() + newline : generated + preserved, footer, definitions, newline);
1522
+ const sections = [...additions.toReversed(), ...released];
1523
+ if (releaseOrder !== void 0) {
1524
+ const positions = new Map(releaseOrder.map((version, index) => [version, index]));
1525
+ sections.sort((a, b) => (positions.get(b.version) ?? -1) - (positions.get(a.version) ?? -1));
1526
+ }
1527
+ const result = sectionSpacing(sectionSpacing(header, newline) + draftText, newline) + sections.map((section) => section.text).join("") + footer;
1528
+ return appendDefinitions(footer === "" && released.length === 0 ? result.trimEnd() + newline : result, footer, definitions, newline);
1503
1529
  }
1504
1530
  /** Add link reference definitions after any existing footer. */
1505
1531
  function appendDefinitions(content, footer, definitions, newline) {
@@ -2108,16 +2134,15 @@ function leavesGeneratedDraft(entries, previously) {
2108
2134
  const draft = entries.find((entry) => entry.release.version === "Unreleased");
2109
2135
  return previously || draft !== void 0 && (draft.existingBody === void 0 || draft.revise === true);
2110
2136
  }
2111
- /** Select the releases newer than anything the changelog already records. */
2137
+ /** Select every reachable release the changelog does not already record. */
2112
2138
  function missingReleases(releases, content = "") {
2113
2139
  const { sections } = parseChangelog(content);
2114
2140
  const known = new Set(sections.map((section) => section.version));
2115
2141
  const recorded = sections.filter((section) => section.version !== "Unreleased").map((section) => section.version);
2116
2142
  const unrecognized = recorded.filter((version) => semver.valid(version) === null);
2117
2143
  if (unrecognized.length > 0) throw new Error(`The existing changelog has unrecognized release headings (${unrecognized.join(", ")}). Normalize its release headings before updating it.`);
2118
- const newest = recorded.toSorted(semver.rcompare)[0];
2119
- if (newest !== void 0 && releases.every((release) => release.version !== newest)) throw new Error(`Cannot locate the Git release boundary for existing changelog version ${newest}. Fetch its tags or configure tagPatterns.`);
2120
- return releases.filter((release) => release.version === "Unreleased" || !known.has(release.version) && (newest === void 0 || semver.gt(release.version, newest)));
2144
+ for (const version of recorded) if (releases.every((release) => release.version !== version)) throw new Error(`Cannot locate the Git release boundary for existing changelog version ${version}. Fetch its tags or configure tagPatterns.`);
2145
+ return releases.filter((release) => release.version === "Unreleased" || !known.has(release.version));
2121
2146
  }
2122
2147
  /** Apply an explicit release context only to the package being released. */
2123
2148
  function releaseContext(options, pkg) {
@@ -2216,7 +2241,7 @@ function renderPackage(context, prepared, results, linksFor) {
2216
2241
  revise
2217
2242
  });
2218
2243
  }
2219
- const after = updateMarkdown(content, entries, exception, repository.github);
2244
+ const after = updateMarkdown(content, entries, exception, repository.github, releases.map((release) => release.version));
2220
2245
  if (after !== file.content) plan.files.push({
2221
2246
  after,
2222
2247
  before: file.content,
@@ -2240,14 +2265,7 @@ function headingAnchor(heading) {
2240
2265
  function missingPackageSummaries(summaries, content = "") {
2241
2266
  const parsed = parseChangelog(content);
2242
2267
  const known = new Set(parsed.sections.map((section) => section.version));
2243
- return summaries.toSorted((a, b) => (a.date ?? "9999").localeCompare(b.date ?? "9999")).filter((summary) => {
2244
- if (summary.version === "Unreleased" || known.has(`${summary.packageName}@${summary.version}`)) return false;
2245
- return parsed.sections.every((section) => {
2246
- const prefix = summary.packageName + "@";
2247
- const version = section.version.startsWith(prefix) ? section.version.slice(prefix.length) : "";
2248
- return semver.valid(version) === null || semver.lt(version, summary.version);
2249
- });
2250
- });
2268
+ return summaries.filter((summary) => summary.version !== "Unreleased" && !known.has(`${summary.packageName}@${summary.version}`));
2251
2269
  }
2252
2270
 
2253
2271
  // src/lib/workspace.ts
@@ -2320,10 +2338,11 @@ async function prepareIndependent(context, packages) {
2320
2338
  * Write a section for each package release the root does not record yet, and
2321
2339
  * keep the draft's links to packages with unreleased changes current.
2322
2340
  */
2323
- function renderIndependent(context, prepared, results, summaries, exception) {
2341
+ async function renderIndependent(context, prepared, results, summaries, exception) {
2324
2342
  const { plan, repository } = context;
2325
2343
  const { content, draft, file, unreleased } = prepared;
2326
- const entries = Array.from(missingPackageSummaries(summaries, content), (summary) => ({
2344
+ const ordered = await sortReleases(repository, summaries.filter((summary) => summary.version !== "Unreleased"));
2345
+ const entries = Array.from(missingPackageSummaries(ordered, content), (summary) => ({
2327
2346
  changes: results.get(summaryId(summary)) ?? [],
2328
2347
  links: packageLink(repository, summary),
2329
2348
  release: {
@@ -2343,7 +2362,7 @@ function renderIndependent(context, prepared, results, summaries, exception) {
2343
2362
  release: unreleased.release,
2344
2363
  revise
2345
2364
  });
2346
- const after = updateMarkdown(content, entries, exception, repository.github);
2365
+ const after = updateMarkdown(content, entries, exception, repository.github, ordered.map((summary) => `${summary.packageName}@${summary.version}`));
2347
2366
  if (after !== file.content) plan.files.push({
2348
2367
  after,
2349
2368
  before: file.content,
@@ -2387,7 +2406,7 @@ async function generateWorkspace(context) {
2387
2406
  ...root,
2388
2407
  exception: exception || root.exception
2389
2408
  }, results, (release) => summaries.filter((summary) => summary.version === release.version && summary.ref === release.ref && !quiet.has(`${summary.changelogPath}@${summary.version}`)).flatMap((summary) => packageLink(repository, summary)));
2390
- } else if (independent !== void 0) renderIndependent(context, independent, results, summaries, exception);
2409
+ } else if (independent !== void 0) await renderIndependent(context, independent, results, summaries, exception);
2391
2410
  }
2392
2411
 
2393
2412
  // src/lib/generate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitschpatrol/chagent",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Changelog agent. CLI tool and TypeScript library to generate and update changelogs with a little help from your LLMs.",
5
5
  "keywords": [
6
6
  "changelog",
@@ -45,7 +45,7 @@
45
45
  "dist"
46
46
  ],
47
47
  "dependencies": {
48
- "@clack/prompts": "^1.8.1",
48
+ "@clack/prompts": "^1.9.0",
49
49
  "@types/node": "~24.13.6",
50
50
  "cosmiconfig": "^10.0.1",
51
51
  "cross-spawn": "^7.0.6",
@@ -67,7 +67,7 @@
67
67
  "mdat-plugin-cli-help": "^3.4.2",
68
68
  "publint": "^0.3.25",
69
69
  "shx": "^0.4.0",
70
- "tsdown": "^0.23.5",
70
+ "tsdown": "^0.23.7",
71
71
  "typescript": "~6.0.3",
72
72
  "vitest": "^5.1.0"
73
73
  },
package/readme.md CHANGED
@@ -287,6 +287,8 @@ Use `packages` and `exclude` directory globs to customize workspace discovery, a
287
287
 
288
288
  ### Implementation notes
289
289
 
290
+ Release order follows Git ancestry, with SemVer breaking ties between tags on the same commit. Fork releases such as `1.2.0-fork.2` can therefore follow upstream `1.2.0`. Updates fill missing reachable releases even when their versions compare lower than an existing heading, while preserving the text of recorded sections.
291
+
290
292
  Chagent reads reachable version tags and commit history regardless of the project's language. Optional npm metadata supplies workspace discovery, local version-bump detection before tagging, and the conditional Node.js compatibility note. Repository tags remain authoritative even when a root `package.json` has an unrelated tooling version. Other projects use tagged releases, or an explicit `release` context through the library API. Without reliable released-version boundaries, new changes belong to `Unreleased`; version numbers are not invented. An existing release heading with no matching Git boundary requires its tags or a corrected tag pattern. Release headings can be level one or two, with optional links, dates, and a `v` prefix, including the Conventional Changelog format. Headings can name short versions the way tags do, so `## 1.2` and `## v1` record the `1.2.0` and `1.0.0` releases. Level three headings count as releases when they name a version, as standard-version writes patch releases. Chagent stops rather than risk duplicating history when a level one or two heading names a version in another form, or when an unclosed code fence would hide later headings. Broken `package.json` files in history are tolerated. Shallow clones must be completed before generation.
291
293
 
292
294
  GitHub enrichment uses release dates and commit-associated PR information. PR lookups batch unique commits across all requested releases and packages; each release receives only its associated PRs. **GitHub release-note bodies are never generation inputs.** Changelog files, lockfiles, and `node_modules` are never source evidence. Existing changelog prose is supplied only to preserve edits, avoid duplicate entries, and show the writing style to match: each request carries the changelog's newest release section as a style reference, so new entries follow its voice, punctuation, and reference conventions.