tegami 0.1.5 → 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-BXk2fMqa.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,13 +1,14 @@
1
1
  import { r as formatNpmDistTag } from "../semver-C4vJ4SK8.js";
2
- import { a as changelogFilename, l as renderChangelog, s as generateReplays, t as assertPublishPlanFinished } from "../checks-Cwz-Ezkq.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 { 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";
5
6
  import path, { basename, isAbsolute, join, normalize, relative } from "node:path";
6
7
  import { x } from "tinyexec";
8
+ import z from "zod";
7
9
  import { resolveCommand } from "package-manager-detector";
8
10
  import { autocompleteMultiselect, confirm, intro, isCancel, multiline, note, outro, select, spinner } from "@clack/prompts";
9
11
  import { Command, InvalidArgumentError } from "commander";
10
- import { tmpdir } from "node:os";
11
12
  //#region src/utils/git-changes.ts
12
13
  async function getChangedPackages(graph, cwd) {
13
14
  return resolveChangedPackages(graph, await getChangedFilePaths(cwd), cwd);
@@ -47,7 +48,7 @@ async function addGitOutput(files, cwd, args) {
47
48
  //#region src/cli/changelog.ts
48
49
  async function runChangelogTui(tegami) {
49
50
  const context = await tegami._internal.context();
50
- 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.`);
51
52
  intro("Create changelogs");
52
53
  let selectedPackages = [];
53
54
  if (!isCI()) selectedPackages = await promptPackageSelection(context.graph, context.cwd);
@@ -63,7 +64,7 @@ async function runChangelogTui(tegami) {
63
64
  return;
64
65
  }
65
66
  }
66
- await persistChangelogs(context, (await tegami.generateChangelog({ write: false })).map(({ filename, content, packages }) => ({
67
+ await persistChangelogs(context, (await generateFromCommits(context)).map(({ filename, content, packages }) => ({
67
68
  filename,
68
69
  content,
69
70
  packages
@@ -122,6 +123,7 @@ async function promptPackageSelection(graph, cwd) {
122
123
  return useShortname.get(pkg.name) ? pkg.name : pkg.id;
123
124
  };
124
125
  const changedPackages = new Set(await getChangedPackages(graph, cwd));
126
+ const initialValues = /* @__PURE__ */ new Set();
125
127
  const selectOptions = [];
126
128
  const groups = [];
127
129
  for (const group of graph.getGroups()) {
@@ -130,23 +132,27 @@ async function promptPackageSelection(graph, cwd) {
130
132
  }
131
133
  groups.sort((a, b) => (a[1] ? 0 : 1) - (b[1] ? 0 : 1));
132
134
  for (const [group, changed] of groups) {
133
- const members = group.packages.map(getPackageLabel).join(", ");
135
+ if (changed) initialValues.add(`group:${group.name}`);
134
136
  selectOptions.push({
135
- label: `(Group) ${group.name}`,
137
+ label: `(Group) ${group.name}` + (changed ? "*" : ""),
136
138
  value: `group:${group.name}`,
137
- hint: changed ? `changed · ${members}` : members
139
+ hint: group.packages.map(getPackageLabel).join(", ")
138
140
  });
139
141
  }
140
142
  const packages = graph.getPackages().toSorted((a, b) => (changedPackages.has(a) ? 0 : 1) - (changedPackages.has(b) ? 0 : 1));
141
- for (const pkg of packages) selectOptions.push({
142
- label: getPackageLabel(pkg),
143
- value: pkg.id,
144
- hint: changedPackages.has(pkg) ? "changed" : void 0
145
- });
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
+ }
146
151
  const selected = await autocompleteMultiselect({
147
152
  message: "Select packages (leave empty to auto-generate from commits)",
148
153
  required: false,
149
- options: selectOptions
154
+ options: selectOptions,
155
+ initialValues: Array.from(initialValues)
150
156
  });
151
157
  if (isCancel(selected)) throw new CancelledError();
152
158
  return selected;
@@ -221,7 +227,7 @@ async function initAgent(context, options = {}) {
221
227
  }
222
228
  function renderAgentsMd(context) {
223
229
  const changelogDir = path.relative(context.cwd, context.changelogDir) || "project root";
224
- const planPath = path.relative(context.cwd, context.planPath) || "project root";
230
+ const lockPath = path.relative(context.cwd, context.lockPath) || "project root";
225
231
  return [
226
232
  "# Release workflow",
227
233
  "",
@@ -259,7 +265,7 @@ function renderAgentsMd(context) {
259
265
  "- Include YAML frontmatter with `packages`",
260
266
  "- Include at least one `#`, `##`, or `###` heading in the body",
261
267
  "- Write user-facing release notes under each heading",
262
- `- 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`,
263
269
  ""
264
270
  ].join("\n");
265
271
  }
@@ -284,24 +290,20 @@ async function buildPrPreview(context, draft, options = {}) {
284
290
  ];
285
291
  const pendingPackages = [];
286
292
  for (const pkg of context.graph.getPackages()) {
287
- const plan = draft.getPackagePlan(pkg.id);
293
+ const plan = draft.getPackageDraft(pkg.id);
288
294
  if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
289
295
  pendingPackages.push({
290
296
  name: pkg.name,
291
297
  type: plan.type ?? "—",
292
298
  from: pkg.version,
293
299
  to: plan.bumpVersion(pkg),
294
- distTag: plan.npm?.distTag,
295
- publish: plan.publish ?? false
300
+ distTag: plan.npm?.distTag
296
301
  });
297
302
  }
298
303
  const prChangelogs = draft.getChangelogs().filter((entry) => prChangelogFiles.has(entry.filename));
299
304
  if (pendingPackages.length > 0) {
300
305
  lines.push("#### Release preview", "", "| Package | Bump | Version |", "| --- | --- | --- |");
301
- for (const { name, type, from, to, distTag, publish } of pendingPackages) {
302
- const publishNote = publish ? "" : " (no publish)";
303
- lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)}${publishNote} |`);
304
- }
306
+ for (const { name, type, from, to, distTag } of pendingPackages) lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)} |`);
305
307
  lines.push("");
306
308
  }
307
309
  if (prChangelogs.length > 0) {
@@ -318,74 +320,69 @@ async function buildPrPreview(context, draft, options = {}) {
318
320
  }
319
321
  async function postPrComment(body) {
320
322
  const repo = process.env.GITHUB_REPOSITORY;
321
- if (!repo) throw new Error("GITHUB_REPOSITORY is required.");
322
- const number = await readPullRequestNumberFromWorkflowRunEvent();
323
+ if (!repo) {
324
+ outro("GITHUB_REPOSITORY is required.");
325
+ return;
326
+ }
327
+ const pr = await readPullRequestFromWorkflowRunEvent();
328
+ if (!pr.found) {
329
+ outro(pr.reason);
330
+ return;
331
+ }
323
332
  const markedBody = `${COMMENT_MARKER}\n${body}`;
324
- const listResult = await x("gh", [
325
- "api",
326
- `repos/${repo}/issues/${number}/comments`,
327
- "--paginate",
328
- "--jq",
329
- `[.[] | select(.body | startswith("${COMMENT_MARKER}")) | .id][0] // empty`
330
- ]);
331
- if (listResult.exitCode !== 0) throw execFailure("Failed to list pull request comments.", listResult);
332
- 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);
333
335
  if (existingId) {
334
- const dir = await mkdtemp(join(tmpdir(), "tegami-pr-comment-"));
335
- const inputPath = join(dir, "body.json");
336
- try {
337
- await writeFile(inputPath, JSON.stringify({ body: markedBody }));
338
- const updateResult = await x("gh", [
339
- "api",
340
- "-X",
341
- "PATCH",
342
- `repos/${repo}/issues/comments/${existingId}`,
343
- "--input",
344
- inputPath
345
- ]);
346
- if (updateResult.exitCode !== 0) throw execFailure("Failed to update pull request comment.", updateResult);
347
- } finally {
348
- await rm(dir, {
349
- recursive: true,
350
- force: true
351
- });
352
- }
336
+ await updateIssueComment(repo, existingId, markedBody, token);
353
337
  return;
354
338
  }
355
- const createResult = await x("gh", [
356
- "pr",
357
- "comment",
358
- String(number),
359
- "--body",
360
- markedBody,
361
- "--repo",
362
- repo
363
- ]);
364
- if (createResult.exitCode !== 0) throw execFailure("Failed to create pull request comment.", createResult);
339
+ await createIssueComment(repo, pr.number, markedBody, token);
365
340
  }
366
- async function readPullRequestNumberFromWorkflowRunEvent() {
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)
345
+ }, { error: "A workflow_run event is required." }) });
346
+ async function readPullRequestFromWorkflowRunEvent() {
367
347
  const eventPath = process.env.GITHUB_EVENT_PATH;
368
- if (!eventPath) throw new Error("GITHUB_EVENT_PATH is required.");
369
- let event;
348
+ if (!eventPath) return {
349
+ found: false,
350
+ reason: "GITHUB_EVENT_PATH is required."
351
+ };
352
+ let raw;
370
353
  try {
371
- event = JSON.parse(await readFile(eventPath, "utf8"));
354
+ raw = JSON.parse(await readFile(eventPath, "utf8"));
372
355
  } catch {
373
- throw new Error("Failed to read workflow_run event.");
356
+ return {
357
+ found: false,
358
+ reason: "Failed to read workflow_run event."
359
+ };
374
360
  }
375
- const workflowRun = event.workflow_run;
376
- if (!workflowRun) throw new Error("A workflow_run event is required.");
377
- if (workflowRun.event !== "pull_request") throw new Error("The preview workflow was not triggered by a pull request.");
378
- if (workflowRun.conclusion !== "success") throw new Error("The preview workflow did not complete successfully.");
379
- const number = workflowRun.pull_requests?.[0]?.number;
380
- if (!Number.isInteger(number) || !number || number <= 0) throw new Error("The preview workflow is not associated with a pull request.");
381
- return number;
361
+ const { error, data } = eventSchema.safeParse(raw);
362
+ if (error) return {
363
+ found: false,
364
+ reason: z.prettifyError(error)
365
+ };
366
+ return {
367
+ found: true,
368
+ number: data.workflow_run.pull_requests[0].number
369
+ };
382
370
  }
383
371
  async function resolvePullRequest(context, options) {
384
372
  const repo = context.github?.repo ?? process.env.GITHUB_REPOSITORY;
385
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.");
386
376
  if (options.number !== void 0) {
387
377
  if (!Number.isInteger(options.number) || options.number <= 0) throw new Error("--number must be a positive integer.");
388
- 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
+ };
389
386
  }
390
387
  const event = await readPullRequestEvent();
391
388
  if (event) return {
@@ -394,26 +391,6 @@ async function resolvePullRequest(context, options) {
394
391
  };
395
392
  throw new Error("A pull request event or --number is required.");
396
393
  }
397
- async function readPullRequestFromGh(repo, number) {
398
- const result = await x("gh", [
399
- "pr",
400
- "view",
401
- String(number),
402
- "--repo",
403
- repo,
404
- "--json",
405
- "headRefName,baseRefOid,headRefOid,headRepository"
406
- ]);
407
- if (result.exitCode !== 0) throw execFailure(`Failed to resolve pull request #${number}.`, result);
408
- const data = JSON.parse(result.stdout);
409
- return {
410
- repo,
411
- headRepo: data.headRepository ? `${data.headRepository.owner.login}/${data.headRepository.name}` : repo,
412
- headRef: data.headRefName,
413
- baseSha: data.baseRefOid,
414
- headSha: data.headRefOid
415
- };
416
- }
417
394
  async function readPullRequestEvent() {
418
395
  const eventPath = process.env.GITHUB_EVENT_PATH;
419
396
  if (!eventPath) return;
@@ -459,13 +436,17 @@ function createChangelogUrl(context, repo, branch, filename) {
459
436
  function createCli(tegami, options = {}) {
460
437
  const program = new Command();
461
438
  program.name("tegami").description("create changelogs").action(() => runAction(tegami, () => runChangelogTui(tegami)));
462
- 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 () => {
463
440
  await versionPackages(tegami, { cli: options });
464
441
  }));
465
442
  program.command("ci").description("version and publish packages").action(() => runAction(tegami, async () => {
466
443
  if (await versionPackages(tegami, { cli: options })) return;
467
444
  await publishPackages(tegami, { cli: options });
468
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
+ }));
469
450
  const programPr = program.command("pr");
470
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) => {
471
452
  const number = Number(value);
@@ -500,13 +481,13 @@ function createCli(tegami, options = {}) {
500
481
  process.exit(1);
501
482
  }
502
483
  });
503
- 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 () => {
504
485
  await publishPackages(tegami, {
505
486
  ...commandOptions,
506
487
  cli: options
507
488
  });
508
489
  }));
509
- 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)));
510
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)));
511
492
  return program;
512
493
  }
@@ -515,59 +496,69 @@ async function versionPackages(tegami, options) {
515
496
  const { version: customVersion } = options.cli;
516
497
  const context = await tegami._internal.context();
517
498
  const draft = customVersion ? await customVersion() : await tegami.draft();
518
- if (!draft.canApply()) throw new Error(`The draft plan from custom "version" hook must not be applied`);
519
- 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));
520
501
  if (!draft.hasPending()) {
521
502
  note("No pending version changes matched workspace packages.", "Nothing to version");
522
503
  outro("No versions changed.");
523
504
  return false;
524
505
  }
525
- 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 = [];
526
508
  for (const pkg of context.graph.getPackages()) {
527
- const plan = draft.getPackagePlan(pkg.id);
509
+ const plan = draft.getPackageDraft(pkg.id);
528
510
  if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
529
- planEntries.push(`${pkg.id}: ${pkg.version} → ${plan.bumpVersion(pkg)} (${plan.changelogs?.length ?? 0} changelogs)`);
530
- 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}`);
531
513
  }
532
- note(planEntries.join("\n"), "Release plan");
514
+ note(lines.join("\n"), "Release plan");
533
515
  const s = spinner();
534
516
  s.start("Updating package versions");
535
517
  try {
536
- await draft.applyPlan();
518
+ await draft.apply();
537
519
  } catch (error) {
538
- s.stop("Failed to apply publish plan");
520
+ s.stop("Failed to apply draft");
539
521
  throw error;
540
522
  }
541
523
  s.stop("Package versions updated");
542
- for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanApplied", () => plugin.cli?.publishPlanApplied?.call(context, draft));
543
- 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.");
544
526
  return true;
545
527
  }
546
528
  async function publishPackages(tegami, options) {
547
529
  const dryRun = options.dryRun ?? false;
530
+ const context = await tegami._internal.context();
548
531
  const { publish: customPublish } = options.cli;
549
532
  intro(dryRun ? "Publish packages (dry run)" : "Publish packages");
550
533
  const s = spinner();
551
- s.start(dryRun ? "Validating publish plan" : "Publishing packages");
552
- const result = customPublish ? await customPublish() : await tegami.publish({ dryRun });
553
- if (result.state === "skipped") {
554
- const { planPath } = await tegami._internal.context();
555
- s.stop(dryRun ? "No publish plan to validate" : "Nothing to publish");
556
- 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}.`);
557
539
  return false;
558
540
  }
559
- s.stop(dryRun ? "Publish plan validated" : "Publish complete");
560
- note(result.packages.map((pkg) => {
561
- const tag = pkg.npm?.distTag ? ` (${pkg.npm.distTag})` : "";
562
- const suffix = pkg.state === "failed" && pkg.error ? `: ${pkg.error}` : "";
563
- return `${pkg.state === "success" ? "success" : "failed"} ${pkg.name}@${pkg.version}${tag}${suffix}`;
564
- }).join("\n"), dryRun ? "Publish dry run" : "Publish result");
565
- 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) {
566
557
  process.exitCode = 1;
567
- outro("Some packages failed to publish.");
558
+ outro("Failed to publish.");
568
559
  return false;
569
560
  }
570
- outro(dryRun ? "Publish plan is valid." : "Packages published.");
561
+ outro(dryRun ? "Publish lock is valid." : "Packages published.");
571
562
  return true;
572
563
  }
573
564
  async function runInitAgent(tegami, options) {
@@ -581,21 +572,21 @@ async function runInitAgent(tegami, options) {
581
572
  outro("Agents can follow AGENTS.md to write changelogs.");
582
573
  }
583
574
  async function runCleanup(tegami) {
584
- intro("Cleanup publish plan");
575
+ intro("Cleanup publish lock");
585
576
  const s = spinner();
586
- s.start("Checking publish plan status");
577
+ s.start("Checking publish lock status");
587
578
  const result = await tegami.cleanup();
588
- const { planPath } = await tegami._internal.context();
589
- 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");
590
581
  if (result.state === "removed") {
591
- outro(`Removed ${planPath}.`);
582
+ outro(`Removed ${lockPath}.`);
592
583
  return;
593
584
  }
594
- if (result.reason === "missing") {
595
- outro(`No publish plan found at ${planPath}.`);
585
+ if (result.reason === "no-plan") {
586
+ outro(`No publish lock found at ${lockPath}.`);
596
587
  return;
597
588
  }
598
- 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.`);
599
590
  }
600
591
  async function runAction(tegami, action) {
601
592
  try {