tegami 0.1.6 → 0.2.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.
@@ -0,0 +1,116 @@
1
+ //#region src/plugins/github/api.ts
2
+ function parseGitHubRepo(repo) {
3
+ const [owner, name] = repo.split("/", 2);
4
+ if (!owner || !name) throw new Error(`Invalid GitHub repository: ${repo}`);
5
+ return {
6
+ owner,
7
+ repo: name
8
+ };
9
+ }
10
+ async function githubRequest(path, token, init = {}) {
11
+ const headers = new Headers(init.headers);
12
+ headers.set("Accept", "application/vnd.github+json");
13
+ headers.set("X-GitHub-Api-Version", "2022-11-28");
14
+ if (token) headers.set("Authorization", `Bearer ${token}`);
15
+ return fetch(`https://api.github.com${path}`, {
16
+ ...init,
17
+ headers
18
+ });
19
+ }
20
+ async function releaseExistsByTag(repo, tag, token) {
21
+ const { owner, repo: name } = parseGitHubRepo(repo);
22
+ const response = await githubRequest(`/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag)}`, token, { method: "HEAD" });
23
+ if (response.status === 404) return false;
24
+ if (!response.ok) throw new Error(`Failed to get GitHub release for ${tag}.`);
25
+ return true;
26
+ }
27
+ async function createRelease(repo, options) {
28
+ const { owner, repo: name } = parseGitHubRepo(repo);
29
+ if (!(await githubRequest(`/repos/${owner}/${name}/releases`, options.token, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ tag_name: options.tag,
34
+ name: options.title,
35
+ body: options.notes,
36
+ prerelease: options.prerelease ?? false
37
+ })
38
+ })).ok) throw new Error(`Failed to create GitHub release for ${options.tag}.`);
39
+ }
40
+ async function findOpenPullRequest(repo, headBranch, token) {
41
+ const { owner, repo: name } = parseGitHubRepo(repo);
42
+ const response = await githubRequest(`/repos/${owner}/${name}/pulls?${new URLSearchParams({
43
+ head: `${owner}:${headBranch}`,
44
+ state: "open"
45
+ })}`, token);
46
+ if (!response.ok) throw new Error("Failed to check for an existing version pull request.");
47
+ return (await response.json())[0]?.number;
48
+ }
49
+ async function updatePullRequest(repo, number, options) {
50
+ const { owner, repo: name } = parseGitHubRepo(repo);
51
+ if (!(await githubRequest(`/repos/${owner}/${name}/pulls/${number}`, options.token, {
52
+ method: "PATCH",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({
55
+ title: options.title,
56
+ body: options.body
57
+ })
58
+ })).ok) throw new Error("Failed to update the version pull request.");
59
+ }
60
+ async function createPullRequest(repo, options) {
61
+ const { owner, repo: name } = parseGitHubRepo(repo);
62
+ if (!(await githubRequest(`/repos/${owner}/${name}/pulls`, options.token, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({
66
+ title: options.title,
67
+ body: options.body,
68
+ head: options.head,
69
+ base: options.base
70
+ })
71
+ })).ok) throw new Error("Failed to create the version pull request.");
72
+ }
73
+ async function getPullRequest(repo, number, token) {
74
+ const { owner, repo: name } = parseGitHubRepo(repo);
75
+ const response = await githubRequest(`/repos/${owner}/${name}/pulls/${number}`, token);
76
+ if (!response.ok) throw new Error(`Failed to resolve pull request #${number}.`);
77
+ const data = await response.json();
78
+ return {
79
+ headRefName: data.head.ref,
80
+ baseRefOid: data.base.sha,
81
+ headRefOid: data.head.sha,
82
+ headRepository: data.head.repo
83
+ };
84
+ }
85
+ async function findIssueCommentByPrefix(repo, issueNumber, prefix, token) {
86
+ const { owner, repo: name } = parseGitHubRepo(repo);
87
+ let page = 1;
88
+ while (true) {
89
+ const response = await githubRequest(`/repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100&page=${page}`, token);
90
+ if (!response.ok) throw new Error("Failed to list pull request comments.");
91
+ const batch = await response.json();
92
+ if (batch.length === 0) break;
93
+ const comment = batch.find((comment) => comment.body.startsWith(prefix));
94
+ if (comment) return comment.id;
95
+ if (batch.length < 100) break;
96
+ page += 1;
97
+ }
98
+ }
99
+ async function updateIssueComment(repo, commentId, body, token) {
100
+ const { owner, repo: name } = parseGitHubRepo(repo);
101
+ if (!(await githubRequest(`/repos/${owner}/${name}/issues/comments/${commentId}`, token, {
102
+ method: "PATCH",
103
+ headers: { "Content-Type": "application/json" },
104
+ body: JSON.stringify({ body })
105
+ })).ok) throw new Error("Failed to update pull request comment.");
106
+ }
107
+ async function createIssueComment(repo, issueNumber, body, token) {
108
+ const { owner, repo: name } = parseGitHubRepo(repo);
109
+ if (!(await githubRequest(`/repos/${owner}/${name}/issues/${issueNumber}/comments`, token, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ body })
113
+ })).ok) throw new Error("Failed to create pull request comment.");
114
+ }
115
+ //#endregion
116
+ export { findOpenPullRequest as a, updateIssueComment as c, findIssueCommentByPrefix as i, updatePullRequest as l, createPullRequest as n, getPullRequest as o, createRelease as r, releaseExistsByTag as s, createIssueComment as t };
@@ -1,11 +1,11 @@
1
- import { k as DraftPlan, t as Awaitable, v as Tegami, w as PublishResult } from "../types-DexeJdl7.js";
1
+ import { O as Draft, S as PublishPlan, g as Tegami, t as Awaitable } from "../types-CurHqnWl.js";
2
2
  import { Command } from "commander";
3
3
 
4
4
  //#region src/cli/index.d.ts
5
5
  interface TegamiCLIOptions {
6
- /** create a custom draft plan, it must not be applied */
7
- version?: () => Awaitable<DraftPlan>;
8
- publish?: () => Awaitable<PublishResult>;
6
+ /** create a custom draft, it must not be applied */
7
+ version?: () => Awaitable<Draft>;
8
+ publish?: () => Awaitable<PublishPlan>;
9
9
  }
10
10
  declare function createCli(tegami: Tegami, options?: TegamiCLIOptions): Command;
11
11
  //#endregion
package/dist/cli/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  import { r as formatNpmDistTag } from "../semver-C4vJ4SK8.js";
2
- import { r as generateReplays, t as changelogFilename } from "../generate-B3bcuKLY.js";
2
+ import { a as renderChangelog, n as generateFromCommits, r as generateReplays, t as changelogFilename } from "../generate-BMlrn-2e.js";
3
3
  import { a as isCI, n as execFailure, r as handlePluginError, t as CancelledError } from "../error-DNy8R5ue.js";
4
- import { o as renderChangelog, t as assertPublishPlanFinished } from "../checks-azjuIFt7.js";
5
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { c as updateIssueComment, i as findIssueCommentByPrefix, o as getPullRequest, t as createIssueComment } from "../api-D-jf_8xY.js";
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path, { basename, isAbsolute, join, normalize, relative } from "node:path";
7
7
  import { x } from "tinyexec";
8
- import z$1 from "zod";
8
+ import z from "zod";
9
9
  import { resolveCommand } from "package-manager-detector";
10
10
  import { autocompleteMultiselect, confirm, intro, isCancel, multiline, note, outro, select, spinner } from "@clack/prompts";
11
11
  import { Command, InvalidArgumentError } from "commander";
12
- import { tmpdir } from "node:os";
13
12
  //#region src/utils/git-changes.ts
14
13
  async function getChangedPackages(graph, cwd) {
15
14
  return resolveChangedPackages(graph, await getChangedFilePaths(cwd), cwd);
@@ -49,7 +48,7 @@ async function addGitOutput(files, cwd, args) {
49
48
  //#region src/cli/changelog.ts
50
49
  async function runChangelogTui(tegami) {
51
50
  const context = await tegami._internal.context();
52
- await assertPublishPlanFinished(context);
51
+ if (await tegami.publishStatus() === "pending") throw new Error(`Publish lock at ${context.lockPath} is still pending. Publish it before applying a new draft.`);
53
52
  intro("Create changelogs");
54
53
  let selectedPackages = [];
55
54
  if (!isCI()) selectedPackages = await promptPackageSelection(context.graph, context.cwd);
@@ -65,7 +64,7 @@ async function runChangelogTui(tegami) {
65
64
  return;
66
65
  }
67
66
  }
68
- await persistChangelogs(context, (await tegami.generateChangelog({ write: false })).map(({ filename, content, packages }) => ({
67
+ await persistChangelogs(context, (await generateFromCommits(context)).map(({ filename, content, packages }) => ({
69
68
  filename,
70
69
  content,
71
70
  packages
@@ -124,6 +123,7 @@ async function promptPackageSelection(graph, cwd) {
124
123
  return useShortname.get(pkg.name) ? pkg.name : pkg.id;
125
124
  };
126
125
  const changedPackages = new Set(await getChangedPackages(graph, cwd));
126
+ const initialValues = /* @__PURE__ */ new Set();
127
127
  const selectOptions = [];
128
128
  const groups = [];
129
129
  for (const group of graph.getGroups()) {
@@ -132,23 +132,27 @@ async function promptPackageSelection(graph, cwd) {
132
132
  }
133
133
  groups.sort((a, b) => (a[1] ? 0 : 1) - (b[1] ? 0 : 1));
134
134
  for (const [group, changed] of groups) {
135
- const members = group.packages.map(getPackageLabel).join(", ");
135
+ if (changed) initialValues.add(`group:${group.name}`);
136
136
  selectOptions.push({
137
- label: `(Group) ${group.name}`,
137
+ label: `(Group) ${group.name}` + (changed ? "*" : ""),
138
138
  value: `group:${group.name}`,
139
- hint: changed ? `changed · ${members}` : members
139
+ hint: group.packages.map(getPackageLabel).join(", ")
140
140
  });
141
141
  }
142
142
  const packages = graph.getPackages().toSorted((a, b) => (changedPackages.has(a) ? 0 : 1) - (changedPackages.has(b) ? 0 : 1));
143
- for (const pkg of packages) selectOptions.push({
144
- label: getPackageLabel(pkg),
145
- value: pkg.id,
146
- hint: changedPackages.has(pkg) ? "changed" : void 0
147
- });
143
+ for (const pkg of packages) {
144
+ const changed = changedPackages.has(pkg);
145
+ if (changed) initialValues.add(pkg.id);
146
+ selectOptions.push({
147
+ label: getPackageLabel(pkg) + (changed ? "*" : ""),
148
+ value: pkg.id
149
+ });
150
+ }
148
151
  const selected = await autocompleteMultiselect({
149
152
  message: "Select packages (leave empty to auto-generate from commits)",
150
153
  required: false,
151
- options: selectOptions
154
+ options: selectOptions,
155
+ initialValues: Array.from(initialValues)
152
156
  });
153
157
  if (isCancel(selected)) throw new CancelledError();
154
158
  return selected;
@@ -223,7 +227,7 @@ async function initAgent(context, options = {}) {
223
227
  }
224
228
  function renderAgentsMd(context) {
225
229
  const changelogDir = path.relative(context.cwd, context.changelogDir) || "project root";
226
- const planPath = path.relative(context.cwd, context.planPath) || "project root";
230
+ const lockPath = path.relative(context.cwd, context.lockPath) || "project root";
227
231
  return [
228
232
  "# Release workflow",
229
233
  "",
@@ -261,7 +265,7 @@ function renderAgentsMd(context) {
261
265
  "- Include YAML frontmatter with `packages`",
262
266
  "- Include at least one `#`, `##`, or `###` heading in the body",
263
267
  "- Write user-facing release notes under each heading",
264
- `- Do not edit \`${planPath}\` or package \`CHANGELOG.md\` files directly`,
268
+ `- Do not edit the publish lock file (\`${lockPath}\`) or package \`CHANGELOG.md\` files directly`,
265
269
  ""
266
270
  ].join("\n");
267
271
  }
@@ -286,24 +290,20 @@ async function buildPrPreview(context, draft, options = {}) {
286
290
  ];
287
291
  const pendingPackages = [];
288
292
  for (const pkg of context.graph.getPackages()) {
289
- const plan = draft.getPackagePlan(pkg.id);
293
+ const plan = draft.getPackageDraft(pkg.id);
290
294
  if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
291
295
  pendingPackages.push({
292
296
  name: pkg.name,
293
297
  type: plan.type ?? "—",
294
298
  from: pkg.version,
295
299
  to: plan.bumpVersion(pkg),
296
- distTag: plan.npm?.distTag,
297
- publish: plan.publish ?? false
300
+ distTag: plan.npm?.distTag
298
301
  });
299
302
  }
300
303
  const prChangelogs = draft.getChangelogs().filter((entry) => prChangelogFiles.has(entry.filename));
301
304
  if (pendingPackages.length > 0) {
302
305
  lines.push("#### Release preview", "", "| Package | Bump | Version |", "| --- | --- | --- |");
303
- for (const { name, type, from, to, distTag, publish } of pendingPackages) {
304
- const publishNote = publish ? "" : " (no publish)";
305
- lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)}${publishNote} |`);
306
- }
306
+ for (const { name, type, from, to, distTag } of pendingPackages) lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)} |`);
307
307
  lines.push("");
308
308
  }
309
309
  if (prChangelogs.length > 0) {
@@ -330,52 +330,18 @@ async function postPrComment(body) {
330
330
  return;
331
331
  }
332
332
  const markedBody = `${COMMENT_MARKER}\n${body}`;
333
- const listResult = await x("gh", [
334
- "api",
335
- `repos/${repo}/issues/${pr.number}/comments`,
336
- "--paginate",
337
- "--jq",
338
- `[.[] | select(.body | startswith("${COMMENT_MARKER}")) | .id][0] // empty`
339
- ]);
340
- if (listResult.exitCode !== 0) throw execFailure("Failed to list pull request comments.", listResult);
341
- const existingId = listResult.stdout.trim();
333
+ const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
334
+ const existingId = await findIssueCommentByPrefix(repo, pr.number, COMMENT_MARKER, token);
342
335
  if (existingId) {
343
- const dir = await mkdtemp(join(tmpdir(), "tegami-pr-comment-"));
344
- const inputPath = join(dir, "body.json");
345
- try {
346
- await writeFile(inputPath, JSON.stringify({ body: markedBody }));
347
- const updateResult = await x("gh", [
348
- "api",
349
- "-X",
350
- "PATCH",
351
- `repos/${repo}/issues/comments/${existingId}`,
352
- "--input",
353
- inputPath
354
- ]);
355
- if (updateResult.exitCode !== 0) throw execFailure("Failed to update pull request comment.", updateResult);
356
- } finally {
357
- await rm(dir, {
358
- recursive: true,
359
- force: true
360
- });
361
- }
336
+ await updateIssueComment(repo, existingId, markedBody, token);
362
337
  return;
363
338
  }
364
- const createResult = await x("gh", [
365
- "pr",
366
- "comment",
367
- String(pr.number),
368
- "--body",
369
- markedBody,
370
- "--repo",
371
- repo
372
- ]);
373
- if (createResult.exitCode !== 0) throw execFailure("Failed to create pull request comment.", createResult);
339
+ await createIssueComment(repo, pr.number, markedBody, token);
374
340
  }
375
- const eventSchema = z$1.looseObject({ workflow_run: z$1.looseObject({
376
- event: z$1.literal("pull_request", { error: "The preview workflow was not triggered by a pull request." }),
377
- conclusion: z$1.literal("success", { error: "The preview workflow did not complete successfully." }),
378
- pull_requests: z$1.array(z$1.looseObject({ number: z$1.int() }), { error: "The preview workflow is not associated with a pull request." }).min(1)
341
+ const eventSchema = z.looseObject({ workflow_run: z.looseObject({
342
+ event: z.literal("pull_request", { error: "The preview workflow was not triggered by a pull request." }),
343
+ conclusion: z.literal("success", { error: "The preview workflow did not complete successfully." }),
344
+ pull_requests: z.array(z.looseObject({ number: z.int() }), { error: "The preview workflow is not associated with a pull request." }).min(1)
379
345
  }, { error: "A workflow_run event is required." }) });
380
346
  async function readPullRequestFromWorkflowRunEvent() {
381
347
  const eventPath = process.env.GITHUB_EVENT_PATH;
@@ -395,7 +361,7 @@ async function readPullRequestFromWorkflowRunEvent() {
395
361
  const { error, data } = eventSchema.safeParse(raw);
396
362
  if (error) return {
397
363
  found: false,
398
- reason: z$1.prettifyError(error)
364
+ reason: z.prettifyError(error)
399
365
  };
400
366
  return {
401
367
  found: true,
@@ -405,9 +371,18 @@ async function readPullRequestFromWorkflowRunEvent() {
405
371
  async function resolvePullRequest(context, options) {
406
372
  const repo = context.github?.repo ?? process.env.GITHUB_REPOSITORY;
407
373
  if (!repo) throw new Error("GITHUB_REPOSITORY is required.");
374
+ const token = context.github?.token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
375
+ if (!repo) throw new Error("GITHUB_TOKEN is required.");
408
376
  if (options.number !== void 0) {
409
377
  if (!Number.isInteger(options.number) || options.number <= 0) throw new Error("--number must be a positive integer.");
410
- return readPullRequestFromGh(repo, options.number);
378
+ const data = await getPullRequest(repo, options.number, token);
379
+ return {
380
+ repo,
381
+ headRepo: data.headRepository ? `${data.headRepository.owner.login}/${data.headRepository.name}` : repo,
382
+ headRef: data.headRefName,
383
+ baseSha: data.baseRefOid,
384
+ headSha: data.headRefOid
385
+ };
411
386
  }
412
387
  const event = await readPullRequestEvent();
413
388
  if (event) return {
@@ -416,26 +391,6 @@ async function resolvePullRequest(context, options) {
416
391
  };
417
392
  throw new Error("A pull request event or --number is required.");
418
393
  }
419
- async function readPullRequestFromGh(repo, number) {
420
- const result = await x("gh", [
421
- "pr",
422
- "view",
423
- String(number),
424
- "--repo",
425
- repo,
426
- "--json",
427
- "headRefName,baseRefOid,headRefOid,headRepository"
428
- ]);
429
- if (result.exitCode !== 0) throw execFailure(`Failed to resolve pull request #${number}.`, result);
430
- const data = JSON.parse(result.stdout);
431
- return {
432
- repo,
433
- headRepo: data.headRepository ? `${data.headRepository.owner.login}/${data.headRepository.name}` : repo,
434
- headRef: data.headRefName,
435
- baseSha: data.baseRefOid,
436
- headSha: data.headRefOid
437
- };
438
- }
439
394
  async function readPullRequestEvent() {
440
395
  const eventPath = process.env.GITHUB_EVENT_PATH;
441
396
  if (!eventPath) return;
@@ -481,13 +436,17 @@ function createChangelogUrl(context, repo, branch, filename) {
481
436
  function createCli(tegami, options = {}) {
482
437
  const program = new Command();
483
438
  program.name("tegami").description("create changelogs").action(() => runAction(tegami, () => runChangelogTui(tegami)));
484
- program.command("version").description("draft and apply a publish plan").action(() => runAction(tegami, async () => {
439
+ program.command("version").description("draft version changes and write the publish lock").action(() => runAction(tegami, async () => {
485
440
  await versionPackages(tegami, { cli: options });
486
441
  }));
487
442
  program.command("ci").description("version and publish packages").action(() => runAction(tegami, async () => {
488
443
  if (await versionPackages(tegami, { cli: options })) return;
489
444
  await publishPackages(tegami, { cli: options });
490
445
  }));
446
+ program.command("check-publish").description("exit with code 1 if no publishing needed, otherwise 0").action(() => runAction(tegami, async () => {
447
+ const status = await tegami.publishStatus();
448
+ process.exit(status === "pending" ? 0 : 1);
449
+ }));
491
450
  const programPr = program.command("pr");
492
451
  programPr.command("preview").description("(should be executed in a GitHub action) show a pull request release preview and changelog guidance").option("--artifact <path>", "write preview markdown to a file").option("--number <number>", "pull request number", (value) => {
493
452
  const number = Number(value);
@@ -522,13 +481,13 @@ function createCli(tegami, options = {}) {
522
481
  process.exit(1);
523
482
  }
524
483
  });
525
- program.command("publish").description("publish packages from the applied publish plan").option("--dry-run", "validate the publish plan without publishing packages").action((commandOptions) => runAction(tegami, async () => {
484
+ program.command("publish").description("publish packages from the publish lock").option("--dry-run", "validate the publish lock without publishing packages").action((commandOptions) => runAction(tegami, async () => {
526
485
  await publishPackages(tegami, {
527
486
  ...commandOptions,
528
487
  cli: options
529
488
  });
530
489
  }));
531
- program.command("cleanup").description("remove the publish plan after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
490
+ program.command("cleanup").description("remove the publish lock after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
532
491
  program.command("init-agent").description("write AGENTS.md with changelog instructions for AI agents").option("-o, --output <path>", "output path", "AGENTS.md").action((commandOptions) => runAction(tegami, () => runInitAgent(tegami, commandOptions)));
533
492
  return program;
534
493
  }
@@ -537,60 +496,69 @@ async function versionPackages(tegami, options) {
537
496
  const { version: customVersion } = options.cli;
538
497
  const context = await tegami._internal.context();
539
498
  const draft = customVersion ? await customVersion() : await tegami.draft();
540
- if (!draft.canApply()) throw new Error(`The draft plan from custom "version" hook must not be applied`);
541
- for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanCreated", () => plugin.cli?.publishPlanCreated?.call(context, draft));
499
+ if (!draft.canApply()) throw new Error(`The draft from custom "version" hook must not be applied`);
500
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.draftCreated", () => plugin.cli?.draftCreated?.call(context, draft));
542
501
  if (!draft.hasPending()) {
543
502
  note("No pending version changes matched workspace packages.", "Nothing to version");
544
503
  outro("No versions changed.");
545
504
  return false;
546
505
  }
547
- const planEntries = [];
506
+ if (await tegami.publishStatus() === "pending") throw new Error(`Publish lock at ${context.lockPath} is still pending. Publish it before applying a new draft.`);
507
+ const lines = [];
548
508
  for (const pkg of context.graph.getPackages()) {
549
- const plan = draft.getPackagePlan(pkg.id);
509
+ const plan = draft.getPackageDraft(pkg.id);
550
510
  if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
551
- planEntries.push(`${pkg.id}: ${pkg.version} → ${plan.bumpVersion(pkg)} (${plan.changelogs?.length ?? 0} changelogs)`);
552
- if (plan.bumpReasons) for (const reason of plan.bumpReasons) planEntries.push(` - ${reason}`);
511
+ lines.push(`${pkg.id}: ${pkg.version} → ${plan.bumpVersion(pkg)} (${plan.changelogs?.length ?? 0} changelogs)`);
512
+ if (plan.bumpReasons) for (const reason of plan.bumpReasons) lines.push(` - ${reason}`);
553
513
  }
554
- note(planEntries.join("\n"), "Release plan");
514
+ note(lines.join("\n"), "Release plan");
555
515
  const s = spinner();
556
516
  s.start("Updating package versions");
557
517
  try {
558
- await draft.applyPlan();
518
+ await draft.apply();
559
519
  } catch (error) {
560
- s.stop("Failed to apply publish plan");
520
+ s.stop("Failed to apply draft");
561
521
  throw error;
562
522
  }
563
523
  s.stop("Package versions updated");
564
- for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanApplied", () => plugin.cli?.publishPlanApplied?.call(context, draft));
565
- outro("Publish plan applied.");
524
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.draftApplied", () => plugin.cli?.draftApplied?.call(context, draft));
525
+ outro("Publish lock written.");
566
526
  return true;
567
527
  }
568
528
  async function publishPackages(tegami, options) {
569
529
  const dryRun = options.dryRun ?? false;
530
+ const context = await tegami._internal.context();
570
531
  const { publish: customPublish } = options.cli;
571
532
  intro(dryRun ? "Publish packages (dry run)" : "Publish packages");
572
533
  const s = spinner();
573
- s.start(dryRun ? "Validating publish plan" : "Publishing packages");
574
- const result = customPublish ? await customPublish() : await tegami.publish({ dryRun });
575
- if (result.state === "skipped") {
576
- const { planPath } = await tegami._internal.context();
577
- s.stop(dryRun ? "No publish plan to validate" : "Nothing to publish");
578
- outro(`No publishable packages were found in ${planPath}.`);
534
+ s.start(dryRun ? "Validating publish lock" : "Publishing packages");
535
+ const plan = customPublish ? await customPublish() : await tegami.publish({ dryRun });
536
+ if (plan === "skipped") {
537
+ s.stop(dryRun ? "No publish lock to validate" : "Nothing to publish");
538
+ outro(`No publishable packages were found in ${context.lockPath}.`);
579
539
  return false;
580
540
  }
581
- s.stop(dryRun ? "Publish plan validated" : "Publish complete");
582
- note(result.packages.map((pkg) => {
583
- const tag = pkg.npm?.distTag ? ` (${pkg.npm.distTag})` : "";
584
- const suffix = pkg.state === "failed" && pkg.error ? `: ${pkg.error}` : "";
585
- return `${pkg.state === "success" ? "success" : "failed"} ${pkg.name}@${pkg.version}${tag}${suffix}`;
586
- }).join("\n"), dryRun ? "Publish dry run" : "Publish result");
587
- if (result.state === "failed") {
541
+ s.stop(dryRun ? "Publish lock validated" : "Publish complete");
542
+ const lines = [];
543
+ let hasFailed = false;
544
+ for (const [id, packagePlan] of plan.packages) {
545
+ if (!packagePlan.updated) continue;
546
+ const result = packagePlan.publishResult;
547
+ const pkg = context.graph.get(id);
548
+ if (result.type === "failed") hasFailed = true;
549
+ let message = `${result.type} ${pkg.id} - ${pkg.version}`;
550
+ const distTag = packagePlan.npm?.distTag;
551
+ if (distTag) message += ` (npm dist-tag: ${distTag})`;
552
+ if (result.type === "failed" && result.error) message += `: ${result.error}`;
553
+ lines.push(message);
554
+ }
555
+ note(lines.join("\n"), dryRun ? "Publish dry run" : "Publish result");
556
+ if (hasFailed) {
588
557
  process.exitCode = 1;
589
- if (result.error) note(result.error, "Error when publishing");
590
558
  outro("Failed to publish.");
591
559
  return false;
592
560
  }
593
- outro(dryRun ? "Publish plan is valid." : "Packages published.");
561
+ outro(dryRun ? "Publish lock is valid." : "Packages published.");
594
562
  return true;
595
563
  }
596
564
  async function runInitAgent(tegami, options) {
@@ -604,21 +572,21 @@ async function runInitAgent(tegami, options) {
604
572
  outro("Agents can follow AGENTS.md to write changelogs.");
605
573
  }
606
574
  async function runCleanup(tegami) {
607
- intro("Cleanup publish plan");
575
+ intro("Cleanup publish lock");
608
576
  const s = spinner();
609
- s.start("Checking publish plan status");
577
+ s.start("Checking publish lock status");
610
578
  const result = await tegami.cleanup();
611
- const { planPath } = await tegami._internal.context();
612
- s.stop(result.state === "removed" ? "Publish plan removed" : "Publish plan kept");
579
+ const { lockPath } = await tegami._internal.context();
580
+ s.stop(result.state === "removed" ? "Publish lock removed" : "Publish lock kept");
613
581
  if (result.state === "removed") {
614
- outro(`Removed ${planPath}.`);
582
+ outro(`Removed ${lockPath}.`);
615
583
  return;
616
584
  }
617
- if (result.reason === "missing") {
618
- outro(`No publish plan found at ${planPath}.`);
585
+ if (result.reason === "no-plan") {
586
+ outro(`No publish lock found at ${lockPath}.`);
619
587
  return;
620
588
  }
621
- outro(`Publish plan at ${planPath} is still pending. Publish it before cleanup.`);
589
+ outro(`Publish lock at ${lockPath} is still pending. Publish it before cleanup.`);
622
590
  }
623
591
  async function runAction(tegami, action) {
624
592
  try {
@@ -1,10 +1,10 @@
1
1
  import { a as maxBump, t as bumpDepth } from "./semver-C4vJ4SK8.js";
2
2
  import { n as execFailure } from "./error-DNy8R5ue.js";
3
- import { o as renderChangelog } from "./checks-azjuIFt7.js";
4
- import { mkdir, writeFile } from "node:fs/promises";
5
- import { join } from "node:path";
3
+ import { t as bumpTypeSchema } from "./schemas-B7N6EE2k.js";
6
4
  import { x } from "tinyexec";
7
5
  import * as semver from "semver";
6
+ import z from "zod";
7
+ import { dump } from "js-yaml";
8
8
  //#region src/utils/conventional-commit.ts
9
9
  /**
10
10
  * Conventional commit header per conventionalcommits.org and semantic-release defaults.
@@ -70,10 +70,33 @@ function conventionalCommitToBump(type, breaking) {
70
70
  }
71
71
  }
72
72
  //#endregion
73
+ //#region src/changelog/shared.ts
74
+ const changelogPackageConfigSchema = z.object({
75
+ type: bumpTypeSchema.optional(),
76
+ replay: z.array(z.string().min(1)).optional()
77
+ });
78
+ const changelogFrontmatterSchema = z.object({
79
+ subject: z.string().optional(),
80
+ packages: z.union([z.array(z.string()), z.record(z.string(), z.union([
81
+ bumpTypeSchema,
82
+ z.null(),
83
+ changelogPackageConfigSchema
84
+ ]))]).optional()
85
+ });
86
+ function renderChangelog(frontmatter, body) {
87
+ return [
88
+ "---",
89
+ dump(frontmatter).trim(),
90
+ "---",
91
+ "",
92
+ body.trim(),
93
+ ""
94
+ ].join("\n");
95
+ }
96
+ //#endregion
73
97
  //#region src/changelog/generate.ts
74
- async function generateChangelog(context, options = {}) {
75
- const write = options.write ?? true;
76
- const commits = await readConventionalCommits(context, options);
98
+ async function generateFromCommits(context, { from, to } = {}) {
99
+ const commits = await readConventionalCommits(context, from, to);
77
100
  const groups = /* @__PURE__ */ new Map();
78
101
  for (const commit of commits) {
79
102
  const key = commit.packages.join("\0");
@@ -81,7 +104,7 @@ async function generateChangelog(context, options = {}) {
81
104
  if (group) group.push(commit);
82
105
  else groups.set(key, [commit]);
83
106
  }
84
- const changelogs = Array.from(groups, ([key, changes], index) => {
107
+ return Array.from(groups, ([key, changes], index) => {
85
108
  const packageNames = key ? key.split("\0") : [];
86
109
  let bumpType = "patch";
87
110
  for (const change of changes) bumpType = maxBump(change.type, bumpType);
@@ -99,15 +122,10 @@ async function generateChangelog(context, options = {}) {
99
122
  changes
100
123
  };
101
124
  });
102
- if (write && changelogs.length > 0) {
103
- await mkdir(context.changelogDir, { recursive: true });
104
- await Promise.all(changelogs.map((entry) => writeFile(join(context.changelogDir, entry.filename), entry.content)));
105
- }
106
- return changelogs;
107
125
  }
108
- async function readConventionalCommits(context, options) {
109
- const to = options.to ?? "HEAD";
110
- const from = options.from ?? await latestTag(context.cwd);
126
+ async function readConventionalCommits(context, from, to) {
127
+ from ??= await latestTag(context.cwd);
128
+ to ??= "HEAD";
111
129
  const args = [
112
130
  "log",
113
131
  "--no-merges",
@@ -151,8 +169,8 @@ function generateReplays(graph, base) {
151
169
  for (const [ref, type] of Object.entries(base)) {
152
170
  const resolved = graph.getByName(ref);
153
171
  for (const pkg of resolved) {
154
- const plan = pkg.initPlan();
155
- pkg.configurePlan(plan, graph.getPackageGroup(pkg.id));
172
+ const plan = pkg.initDraft();
173
+ pkg.configureDraft(plan, graph.getPackageGroup(pkg.id));
156
174
  const prerelease = semver.prerelease(pkg.version)?.[0];
157
175
  const targetPrerelease = plan.prerelease;
158
176
  if (targetPrerelease && !prerelease || targetPrerelease && prerelease && targetPrerelease === prerelease) packages[pkg.id] = {
@@ -168,4 +186,4 @@ function changelogFilename(disambiguator = 0) {
168
186
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${(Date.now() + disambiguator).toString(36)}.md`;
169
187
  }
170
188
  //#endregion
171
- export { generateChangelog as n, generateReplays as r, changelogFilename as t };
189
+ export { renderChangelog as a, changelogFrontmatterSchema as i, generateFromCommits as n, generateReplays as r, changelogFilename as t };
@@ -1,4 +1,4 @@
1
- import { r as LogGenerator } from "../types-DexeJdl7.js";
1
+ import { r as LogGenerator } from "../types-CurHqnWl.js";
2
2
 
3
3
  //#region src/generators/simple.d.ts
4
4
  declare function simpleGenerator(): LogGenerator;
@@ -1,9 +1,9 @@
1
1
  import { i as formatPackageVersion } from "../semver-C4vJ4SK8.js";
2
2
  //#region src/generators/simple.ts
3
3
  function simpleGenerator() {
4
- return { generate({ changelogs, version, packageName, plan }) {
5
- const lines = [`## ${formatPackageVersion(packageName, version, plan.npm?.distTag)}`, ""];
6
- for (const entry of changelogs) {
4
+ return { generate({ pkg, packageDraft }) {
5
+ const lines = [`## ${formatPackageVersion(pkg.name, pkg.version, packageDraft.npm?.distTag)}`, ""];
6
+ for (const entry of packageDraft.changelogs ?? []) {
7
7
  let sectionDepth = 4;
8
8
  if (entry.subject) lines.push(`### ${entry.subject}`, "");
9
9
  else sectionDepth--;