tegami 0.1.0-beta.2 → 0.1.0-beta.3

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.
@@ -2,6 +2,12 @@ import { n as handlePluginError } from "./error-DBK-9uBa.js";
2
2
  import { n as jsonCodec, t as bumpTypeSchema } from "./schemas-CurBAaW5.js";
3
3
  import z$1 from "zod";
4
4
  import { readFile } from "fs/promises";
5
+ //#region src/utils/changelog.ts
6
+ function changelogFilename(disambiguator = 0) {
7
+ const date = /* @__PURE__ */ new Date();
8
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${(Date.now() + disambiguator).toString(36)}.md`;
9
+ }
10
+ //#endregion
5
11
  //#region src/plans/store.ts
6
12
  const packagePlanStoreSchema = z$1.object({
7
13
  type: bumpTypeSchema.optional(),
@@ -21,6 +27,7 @@ const planStoreSchema = jsonCodec(z$1.object({
21
27
  subject: z$1.string().optional(),
22
28
  packages: z$1.record(z$1.string(), bumpTypeSchema),
23
29
  sections: z$1.array(z$1.object({
30
+ depth: z$1.number(),
24
31
  title: z$1.string(),
25
32
  content: z$1.string()
26
33
  }))
@@ -88,4 +95,4 @@ async function assertPublishPlanFinished(context) {
88
95
  if ((await publishPlanStatus(store, context)).state === "pending") throw new Error(`Publish plan already exists at ${context.planPath} and is pending. Publish it before applying a new plan.`);
89
96
  }
90
97
  //#endregion
91
- export { readPlanStore as i, publishPlanStatus as n, createPlanStore as r, assertPublishPlanFinished as t };
98
+ export { changelogFilename as a, readPlanStore as i, publishPlanStatus as n, createPlanStore as r, assertPublishPlanFinished as t };
@@ -1,4 +1,4 @@
1
- import { C as PublishResult, O as DraftPlan, _ as Tegami, t as Awaitable } from "../types-BNhcu4-X.js";
1
+ import { C as PublishResult, O as DraftPlan, _ as Tegami, t as Awaitable } from "../types-CrA7ZKxb.js";
2
2
  import { Command } from "commander";
3
3
 
4
4
  //#region src/cli/index.d.ts
package/dist/cli/index.js CHANGED
@@ -1,12 +1,224 @@
1
+ import { a as changelogFilename, t as assertPublishPlanFinished } from "../checks-BQISvt_o.js";
1
2
  import { n as handlePluginError } from "../error-DBK-9uBa.js";
2
- import { t as bumpDepth } from "../semver-CPtl0XNq.js";
3
- import { t as assertPublishPlanFinished } from "../checks-Fz8-N09y.js";
3
+ import { r as formatNpmDistTag, t as bumpDepth } from "../semver-CPtl0XNq.js";
4
4
  import { t as isCI } from "../constants-B9qjNfvr.js";
5
- import { mkdir, writeFile } from "node:fs/promises";
6
- import { join } from "node:path";
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import path, { basename, isAbsolute, join, normalize, relative } from "node:path";
7
+ import { x } from "tinyexec";
7
8
  import { dump } from "js-yaml";
9
+ import { detect, resolveCommand } from "package-manager-detector";
8
10
  import { autocompleteMultiselect, confirm, intro, isCancel, multiline, note, outro, select, spinner } from "@clack/prompts";
9
11
  import { Command } from "commander";
12
+ //#region src/cli/init-agent.ts
13
+ const CHANGELOG_DOCS_URL$1 = "https://tegami.fuma-nama.dev/changelog";
14
+ async function initAgent(context, options = {}) {
15
+ const output = path.resolve(context.cwd, options.output ?? "AGENTS.md");
16
+ const content = renderAgentsMd(context);
17
+ try {
18
+ await writeFile(output, (await readFile(output, "utf8")).trimEnd() + "\n\n" + content);
19
+ return {
20
+ path: output,
21
+ created: false
22
+ };
23
+ } catch {
24
+ await writeFile(output, content);
25
+ return {
26
+ path: output,
27
+ created: true
28
+ };
29
+ }
30
+ }
31
+ function renderAgentsMd(context) {
32
+ const changelogDir = path.relative(context.cwd, context.changelogDir) || "project root";
33
+ const planPath = path.relative(context.cwd, context.planPath) || "project root";
34
+ return [
35
+ "# Release workflow",
36
+ "",
37
+ "This repository uses [Tegami](https://tegami.fuma-nama.dev) for versioning and publishing.",
38
+ "",
39
+ "## Write changelog files",
40
+ "",
41
+ `Create pending changelog files under \`${changelogDir}/\` as \`YYYY-MM-DD-{hash}.md\`.`,
42
+ "",
43
+ `See the [changelog format docs](${CHANGELOG_DOCS_URL$1}) for details.`,
44
+ "",
45
+ "### Example",
46
+ "",
47
+ "```md",
48
+ "---",
49
+ "packages:",
50
+ ` "npm:@acme/ui": patch`,
51
+ "---",
52
+ "",
53
+ "### Fix button hover state",
54
+ "",
55
+ "The hover color now matches the design system.",
56
+ "```",
57
+ "",
58
+ "### Package references",
59
+ "",
60
+ "Use package names, ids, or groups in frontmatter. For example:",
61
+ "",
62
+ "- `\"@acme/ui\"` — package name",
63
+ "- `\"npm:@acme/ui\"` — package id",
64
+ "- `\"group:acme\"` — every package in a group",
65
+ "",
66
+ "Rules:",
67
+ "",
68
+ "- Include YAML frontmatter with `packages`",
69
+ "- Include at least one `#`, `##`, or `###` heading in the body",
70
+ "- Write user-facing release notes under each heading",
71
+ `- Do not edit \`${planPath}\` or package \`CHANGELOG.md\` files directly`,
72
+ ""
73
+ ].join("\n");
74
+ }
75
+ //#endregion
76
+ //#region src/utils/package-manager.ts
77
+ async function formatRunScriptCommand(cwd, script, agent) {
78
+ const resolved = resolveCommand(agent ?? (await detect({ cwd }))?.agent ?? "npm", "run", [script]);
79
+ if (!resolved) return `npm run ${script}`;
80
+ return [resolved.command, ...resolved.args].join(" ");
81
+ }
82
+ //#endregion
83
+ //#region src/cli/ci-pr.ts
84
+ const TEGAMI_DOCS_URL = "https://tegami.fuma-nama.dev";
85
+ const CHANGELOG_DOCS_URL = `${TEGAMI_DOCS_URL}/changelog`;
86
+ async function runCiPr(context, draft, options = {}) {
87
+ const event = await resolvePullRequestEvent();
88
+ return renderCiPrPreview(context, draft, {
89
+ repo: options.repo ?? context.github?.repo ?? process.env.GITHUB_REPOSITORY,
90
+ branch: options.branch ?? event?.headRef,
91
+ prChangelogFiles: event && !options.branch ? await getPullRequestChangelogFiles(context, event.baseSha, event.headSha) : [],
92
+ tegamiCommand: await formatRunScriptCommand(context.cwd, "tegami", context.options.npm?.client)
93
+ });
94
+ }
95
+ function renderCiPrPreview(context, draft, options = {}) {
96
+ const changelogDir = relative(context.cwd, context.changelogDir) || ".tegami";
97
+ const tegamiCommand = options.tegamiCommand ?? "npm run tegami";
98
+ const createLink = options.repo && options.branch ? createChangelogUrl(options.repo, options.branch, changelogDir, changelogFilename()) : void 0;
99
+ const lines = [
100
+ "### Tegami",
101
+ "",
102
+ `This repository uses [Tegami](${TEGAMI_DOCS_URL}) to manage releases. When your changes affect published packages, add a changelog file under \`.tegami/\` before merging.`,
103
+ ""
104
+ ];
105
+ if (createLink) lines.push(`[**Create a changelog →**](${createLink}) · [Changelog format](${CHANGELOG_DOCS_URL})`, "");
106
+ else lines.push(`Run \`${tegamiCommand}\` locally or see the [changelog format](${CHANGELOG_DOCS_URL}).`, "");
107
+ const pendingPackages = getPendingPackages(context, draft);
108
+ const prChangelogs = filterChangelogsInPullRequest(draft, options.prChangelogFiles);
109
+ if (pendingPackages.length > 0) {
110
+ lines.push("#### Release preview", "", "| Package | Bump | Version |", "| --- | --- | --- |");
111
+ for (const { name, type, from, to, distTag, publish } of pendingPackages) {
112
+ const publishNote = publish ? "" : " (no publish)";
113
+ lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)}${publishNote} |`);
114
+ }
115
+ lines.push("");
116
+ }
117
+ if (prChangelogs.length > 0) {
118
+ lines.push("#### Changelogs in this PR", "");
119
+ for (const entry of prChangelogs) {
120
+ const title = entry.sections[0]?.title ?? entry.filename;
121
+ lines.push(`- \`${entry.filename}\` — ${title}`);
122
+ }
123
+ lines.push("");
124
+ } else if (pendingPackages.length === 0) lines.push("#### No changelogs yet", "", "This PR has no pending changelog files. If your changes require a release, add a changelog before merging.", "");
125
+ else if (options.prChangelogFiles?.length === 0) lines.push("This PR does not add changelog files. Pending changelogs from other branches are included in the preview above.", "");
126
+ lines.push(`Run \`${tegamiCommand}\` locally to create a changelog interactively.`, "", `<sub>Managed by [Tegami](${TEGAMI_DOCS_URL}).</sub>`, "");
127
+ return lines.join("\n");
128
+ }
129
+ async function resolvePullRequestEvent() {
130
+ const eventPath = process.env.GITHUB_EVENT_PATH;
131
+ if (!eventPath) return;
132
+ let event;
133
+ try {
134
+ event = JSON.parse(await readFile(eventPath, "utf8"));
135
+ } catch {
136
+ return;
137
+ }
138
+ const pullRequest = event.pull_request;
139
+ if (!pullRequest) return;
140
+ return {
141
+ number: pullRequest.number,
142
+ headRef: pullRequest.head.ref,
143
+ baseSha: pullRequest.base.sha,
144
+ headSha: pullRequest.head.sha
145
+ };
146
+ }
147
+ async function getPullRequestChangelogFiles(context, baseSha, headSha) {
148
+ const dir = relative(context.cwd, context.changelogDir);
149
+ const result = await x("git", [
150
+ "diff",
151
+ "--name-only",
152
+ "--diff-filter=AM",
153
+ `${baseSha}...${headSha}`,
154
+ "--",
155
+ `${dir}/`
156
+ ], { nodeOptions: { cwd: context.cwd } });
157
+ if (result.exitCode !== 0) return [];
158
+ return result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".md")).map((line) => basename(line));
159
+ }
160
+ function getPendingPackages(context, draft) {
161
+ const packages = [];
162
+ for (const pkg of context.graph.getPackages()) {
163
+ const plan = draft.getPackagePlan(pkg.id);
164
+ if (!plan?.type) continue;
165
+ packages.push({
166
+ name: pkg.name,
167
+ type: plan.type,
168
+ from: pkg.version,
169
+ to: plan.bumpVersion(pkg),
170
+ distTag: plan.npm?.distTag,
171
+ publish: plan.publish ?? false
172
+ });
173
+ }
174
+ return packages;
175
+ }
176
+ function filterChangelogsInPullRequest(draft, filenames) {
177
+ if (!filenames) return draft.getChangelogs();
178
+ const names = new Set(filenames);
179
+ return draft.getChangelogs().filter((entry) => names.has(entry.filename));
180
+ }
181
+ function createChangelogUrl(repo, branch, changelogDir, filename) {
182
+ const segments = [...changelogDir.split("/").filter(Boolean), filename].map((part) => encodeURIComponent(part));
183
+ return `https://github.com/${repo}/new/${branch.split("/").map(encodeURIComponent).join("/")}/${segments.join("/")}`;
184
+ }
185
+ //#endregion
186
+ //#region src/utils/git-changes.ts
187
+ async function getChangedPackages(graph, cwd) {
188
+ return resolveChangedPackages(graph, await getChangedFilePaths(cwd), cwd);
189
+ }
190
+ async function getChangedFilePaths(cwd) {
191
+ const files = /* @__PURE__ */ new Set();
192
+ for (const args of [["diff", "--name-only"], [
193
+ "diff",
194
+ "--cached",
195
+ "--name-only"
196
+ ]]) await addGitOutput(files, cwd, args);
197
+ return Array.from(files);
198
+ }
199
+ function resolveChangedPackages(graph, files, cwd) {
200
+ const packages = [...graph.getPackages()].sort((a, b) => b.path.length - a.path.length);
201
+ const matched = /* @__PURE__ */ new Map();
202
+ for (const file of files) for (const pkg of packages) if (isUnderDir(file, pkg.path, cwd)) {
203
+ matched.set(pkg.id, pkg);
204
+ break;
205
+ }
206
+ return [...matched.values()];
207
+ }
208
+ function isUnderDir(file, dir, cwd) {
209
+ const absolute = join(cwd, file);
210
+ const rel = relative(normalize(dir), absolute);
211
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
212
+ }
213
+ async function addGitOutput(files, cwd, args) {
214
+ const result = await x("git", args, { nodeOptions: { cwd } });
215
+ if (result.exitCode !== 0) return;
216
+ for (const line of result.stdout.split("\n")) {
217
+ const trimmed = line.trim();
218
+ if (trimmed) files.add(trimmed);
219
+ }
220
+ }
221
+ //#endregion
10
222
  //#region src/cli/index.ts
11
223
  var CancelledError = class extends Error {
12
224
  constructor() {
@@ -29,6 +241,7 @@ function createCli(tegami, options = {}) {
29
241
  if (await versionPackages(tegami, { cli: options })) return;
30
242
  await publishPackages(tegami, { cli: options });
31
243
  }));
244
+ program.command("ci-pr").description("show a pull request release preview and changelog guidance").option("--repo <repo>", "GitHub repository (owner/name)").option("--branch <branch>", "PR head branch for the create-changelog link").option("--print", "print the preview markdown to stdout").action((commandOptions) => runAction(tegami, () => runCiPrCommand(tegami, commandOptions)));
32
245
  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 () => {
33
246
  await publishPackages(tegami, {
34
247
  ...commandOptions,
@@ -36,6 +249,7 @@ function createCli(tegami, options = {}) {
36
249
  });
37
250
  }));
38
251
  program.command("cleanup").description("remove the publish plan after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
252
+ 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)));
39
253
  return program;
40
254
  }
41
255
  async function createChangelogs(tegami, _options) {
@@ -44,22 +258,33 @@ async function createChangelogs(tegami, _options) {
44
258
  intro("Create changelogs");
45
259
  let selectedPackages = [];
46
260
  if (!isCI()) {
47
- const packages = context.graph.getPackages();
48
261
  const useShortname = /* @__PURE__ */ new Map();
49
- for (const pkg of packages) if (useShortname.has(pkg.name)) useShortname.set(pkg.name, false);
262
+ for (const pkg of context.graph.getPackages()) if (useShortname.has(pkg.name)) useShortname.set(pkg.name, false);
50
263
  else useShortname.set(pkg.name, true);
51
264
  const getPackageLabel = (pkg) => {
52
265
  return useShortname.get(pkg.name) ? pkg.name : pkg.id;
53
266
  };
267
+ const changedPackages = new Set(await getChangedPackages(context.graph, context.cwd));
54
268
  const selectOptions = [];
55
- for (const group of context.graph.getGroups()) selectOptions.push({
56
- label: `Group ${group.name}`,
57
- value: `group:${group.name}`,
58
- hint: group.packages.map(getPackageLabel).join(", ")
59
- });
269
+ const groups = [];
270
+ for (const group of context.graph.getGroups()) {
271
+ const changed = group.packages.some((pkg) => changedPackages.has(pkg));
272
+ groups.push([group, changed]);
273
+ }
274
+ groups.sort((a, b) => (a[1] ? 0 : 1) - (b[1] ? 0 : 1));
275
+ for (const [group, changed] of groups) {
276
+ const members = group.packages.map(getPackageLabel).join(", ");
277
+ selectOptions.push({
278
+ label: `Group ${group.name}`,
279
+ value: `group:${group.name}`,
280
+ hint: changed ? `changed · ${members}` : members
281
+ });
282
+ }
283
+ const packages = context.graph.getPackages().toSorted((a, b) => (changedPackages.has(a) ? 0 : 1) - (changedPackages.has(b) ? 0 : 1));
60
284
  for (const pkg of packages) selectOptions.push({
61
285
  label: getPackageLabel(pkg),
62
- value: pkg.id
286
+ value: pkg.id,
287
+ hint: changedPackages.has(pkg) ? "changed" : void 0
63
288
  });
64
289
  const selected = await autocompleteMultiselect({
65
290
  message: "Select packages (leave empty to auto-generate from commits)",
@@ -186,6 +411,26 @@ async function publishPackages(tegami, options) {
186
411
  outro(dryRun ? "Publish plan is valid." : "Packages published.");
187
412
  return true;
188
413
  }
414
+ async function runCiPrCommand(tegami, options) {
415
+ const body = await runCiPr(await tegami._internal.context(), await tegami.draft(), options);
416
+ if (options.print) {
417
+ process.stdout.write(`${body}\n`);
418
+ if (!isCI()) outro("Release preview ready.");
419
+ return;
420
+ }
421
+ note(body, "Release preview");
422
+ outro("Release preview ready.");
423
+ }
424
+ async function runInitAgent(tegami, options) {
425
+ intro("Init agent instructions");
426
+ const context = await tegami._internal.context();
427
+ const s = spinner();
428
+ s.start("Writing AGENTS.md");
429
+ const result = await initAgent(context, options);
430
+ s.stop(result.created ? "Created AGENTS.md" : "Appended to AGENTS.md");
431
+ note(relative(context.cwd, result.path) || "AGENTS.md", "Agent instructions");
432
+ outro("Agents can follow AGENTS.md to write changelogs.");
433
+ }
189
434
  async function runCleanup(tegami) {
190
435
  intro("Cleanup publish plan");
191
436
  const s = spinner();
@@ -216,10 +461,6 @@ function renderManualChangelog(packages, type, message) {
216
461
  ""
217
462
  ].join("\n");
218
463
  }
219
- function changelogFilename() {
220
- const date = /* @__PURE__ */ new Date();
221
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${Date.now().toString(36)}.md`;
222
- }
223
464
  async function runAction(tegami, action) {
224
465
  try {
225
466
  const context = await tegami._internal.context();
@@ -1,4 +1,4 @@
1
- import { r as LogGenerator } from "../types-BNhcu4-X.js";
1
+ import { r as LogGenerator } from "../types-CrA7ZKxb.js";
2
2
 
3
3
  //#region src/generators/simple.d.ts
4
4
  declare function simpleGenerator(): LogGenerator;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as PublishResult, D as WorkspacePackage, E as PackageGroup, O as DraftPlan, S as PublishOptions, T as PackageGraph, _ as Tegami, a as RegistryClient, b as CreatedChangelog, c as TegamiPluginOption, i as PackageOptions, k as PackagePlan, n as GroupOptions, o as TegamiOptions, r as LogGenerator, s as TegamiPlugin, v as tegami, x as PackagePublishResult, y as CreateChangelogOptions } from "./types-BNhcu4-X.js";
1
+ import { C as PublishResult, D as WorkspacePackage, E as PackageGroup, O as DraftPlan, S as PublishOptions, T as PackageGraph, _ as Tegami, a as RegistryClient, b as CreatedChangelog, c as TegamiPluginOption, i as PackageOptions, k as PackagePlan, n as GroupOptions, o as TegamiOptions, r as LogGenerator, s as TegamiPlugin, v as tegami, x as PackagePublishResult, y as CreateChangelogOptions } from "./types-CrA7ZKxb.js";
2
2
  export { type CreateChangelogOptions, type CreatedChangelog, type DraftPlan, type GroupOptions, type LogGenerator, type PackageGraph, type PackageGroup, type PackageOptions, type PackagePlan, type PackagePublishResult, type PublishOptions, type PublishResult, type RegistryClient, Tegami, type TegamiOptions, type TegamiPlugin, type TegamiPluginOption, type WorkspacePackage, tegami };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { n as handlePluginError } from "./error-DBK-9uBa.js";
1
+ import { a as changelogFilename, i as readPlanStore, n as publishPlanStatus, r as createPlanStore, t as assertPublishPlanFinished } from "./checks-BQISvt_o.js";
2
+ import { n as handlePluginError, t as execFailure } from "./error-DBK-9uBa.js";
2
3
  import { a as maxBump } from "./semver-CPtl0XNq.js";
3
4
  import { t as PackageGraph } from "./graph-B22NBRUG.js";
4
5
  import { cargo } from "./providers/cargo.js";
5
6
  import { t as bumpTypeSchema } from "./schemas-CurBAaW5.js";
6
7
  import { npm } from "./providers/npm.js";
7
8
  import { simpleGenerator } from "./generators/simple.js";
8
- import { i as readPlanStore, n as publishPlanStatus, r as createPlanStore, t as assertPublishPlanFinished } from "./checks-Fz8-N09y.js";
9
9
  import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
10
10
  import path, { basename, dirname, join } from "node:path";
11
11
  import { x } from "tinyexec";
@@ -13,8 +13,73 @@ import { load } from "js-yaml";
13
13
  import z$1 from "zod";
14
14
  import { fromMarkdown } from "mdast-util-from-markdown";
15
15
  import { toMarkdown } from "mdast-util-to-markdown";
16
- //#region src/changelog/create.ts
17
- async function createChangelog(context, options = {}) {
16
+ //#region src/utils/conventional-commit.ts
17
+ /**
18
+ * Conventional commit header per conventionalcommits.org and semantic-release defaults.
19
+ * @see https://www.conventionalcommits.org/en/v1.0.0/
20
+ */
21
+ const CONVENTIONAL_COMMIT_HEADER = /^(?<type>\w+)(?:\((?<scope>[^)]*)\))?(?<breaking>!)?: (?<title>.+)$/;
22
+ const BREAKING_CHANGE_FOOTER = /^BREAKING[ -]CHANGE:/m;
23
+ /** Parse conventional commits and resolve scopes against the workspace graph. */
24
+ function createConventionalCommitParser(graph) {
25
+ const byShortName = /* @__PURE__ */ new Map();
26
+ for (const pkg of graph.getPackages()) {
27
+ const slash = pkg.name.lastIndexOf("/");
28
+ const short = slash >= 0 ? pkg.name.slice(slash + 1) : pkg.name;
29
+ const names = byShortName.get(short);
30
+ if (names) names.push(pkg.name);
31
+ else byShortName.set(short, [pkg.name]);
32
+ }
33
+ function resolvePackages(scope) {
34
+ if (!scope) return [];
35
+ const packages = /* @__PURE__ */ new Set();
36
+ for (const item of scope.split(",")) {
37
+ const name = item.trim();
38
+ if (!name) continue;
39
+ const direct = graph.getByName(name);
40
+ if (direct.length > 0) {
41
+ for (const pkg of direct) packages.add(pkg.name);
42
+ continue;
43
+ }
44
+ const byShort = byShortName.get(name);
45
+ if (byShort) {
46
+ for (const pkgName of byShort) packages.add(pkgName);
47
+ continue;
48
+ }
49
+ packages.add(name);
50
+ }
51
+ return Array.from(packages);
52
+ }
53
+ return function parseConventionalCommit(subject, body = "") {
54
+ const match = CONVENTIONAL_COMMIT_HEADER.exec(subject.trim());
55
+ if (!match?.groups) return;
56
+ const { type, scope, breaking, title } = match.groups;
57
+ if (!type || !title) return;
58
+ const trimmedTitle = title.trim();
59
+ if (!trimmedTitle) return;
60
+ const trimmedScope = scope?.trim();
61
+ return {
62
+ type: type.toLowerCase(),
63
+ packages: resolvePackages(trimmedScope || void 0),
64
+ breaking: Boolean(breaking) || BREAKING_CHANGE_FOOTER.test(body),
65
+ title: trimmedTitle
66
+ };
67
+ };
68
+ }
69
+ /** Map releasable conventional commit types to semver bumps (semantic-release defaults). */
70
+ function conventionalCommitToBump(type, breaking) {
71
+ if (breaking) return "major";
72
+ switch (type) {
73
+ case "feat": return "minor";
74
+ case "fix":
75
+ case "perf":
76
+ case "revert": return "patch";
77
+ default: return;
78
+ }
79
+ }
80
+ //#endregion
81
+ //#region src/changelog/generate.ts
82
+ async function generateChangelog(context, options = {}) {
18
83
  const commits = await readConventionalCommits(context, options);
19
84
  const groups = /* @__PURE__ */ new Map();
20
85
  for (const commit of commits) {
@@ -23,23 +88,19 @@ async function createChangelog(context, options = {}) {
23
88
  if (group) group.push(commit);
24
89
  else groups.set(key, [commit]);
25
90
  }
26
- const directory = context.changelogDir;
27
- await mkdir(directory, { recursive: true });
28
- const created = [];
29
- const stamp = Date.now().toString(36);
30
- for (const [key, changes] of groups) {
91
+ await mkdir(context.changelogDir, { recursive: true });
92
+ return Promise.all(Array.from(groups, async ([key, changes], index) => {
31
93
  const packages = key ? key.split("\0") : [];
32
- const filename = `changes-${stamp}-${slugify(packages.join("-") || "workspace")}.md`;
33
- const path = join(directory, filename);
94
+ const filename = changelogFilename(index);
95
+ const path = join(context.changelogDir, filename);
34
96
  await writeFile(path, renderChangelog(packages, changes));
35
- created.push({
97
+ return {
36
98
  filename,
37
99
  path,
38
100
  packages,
39
101
  changes: changes.length
40
- });
41
- }
42
- return created;
102
+ };
103
+ }));
43
104
  }
44
105
  async function readConventionalCommits(context, options) {
45
106
  const to = options.to ?? "HEAD";
@@ -52,13 +113,24 @@ async function readConventionalCommits(context, options) {
52
113
  if (from) args.push(`${from}..${to}`);
53
114
  else if (to !== "HEAD") args.push(to);
54
115
  const result = await x("git", args, { nodeOptions: { cwd: context.cwd } });
55
- if (result.exitCode !== 0) throw new Error(`Unable to read git commits: ${commandOutput(result).trim()}`);
116
+ if (result.exitCode !== 0) throw execFailure(`Unable to read git commits from ${from ?? "start"} to ${to}`, result);
117
+ const parseCommit = createConventionalCommitParser(context.graph);
56
118
  const changes = [];
57
119
  for (const record of result.stdout.split("")) {
58
120
  const [hash, subject, body = ""] = record.replace(/^\n+|\n+$/g, "").split("");
59
121
  if (!hash || !subject) continue;
60
- const change = parseConventionalCommit(context, hash, subject, body);
61
- if (change) changes.push(change);
122
+ const parsed = parseCommit(subject, body);
123
+ if (!parsed) continue;
124
+ const bump = conventionalCommitToBump(parsed.type, parsed.breaking);
125
+ if (!bump) continue;
126
+ changes.push({
127
+ hash,
128
+ subject,
129
+ body: body.trim(),
130
+ packages: parsed.packages,
131
+ type: bump,
132
+ title: titleCase(parsed.title)
133
+ });
62
134
  }
63
135
  return changes;
64
136
  }
@@ -68,47 +140,9 @@ async function latestTag(cwd) {
68
140
  "--tags",
69
141
  "--abbrev=0"
70
142
  ], { nodeOptions: { cwd } });
71
- if (result.exitCode !== 0) return void 0;
143
+ if (result.exitCode !== 0) return;
72
144
  return result.stdout.trim() || void 0;
73
145
  }
74
- function parseConventionalCommit(context, hash, subject, body) {
75
- const match = /^(?<type>[a-zA-Z]+)(?:\((?<scope>[^)]+)\))?(?<breaking>!)?:\s*(?<title>.+)$/.exec(subject);
76
- if (!match?.groups) return void 0;
77
- const bump = commitTypeToBump(match.groups.type.toLowerCase(), Boolean(match.groups.breaking) || /^BREAKING(?:-| )CHANGE:/m.test(body));
78
- if (!bump) return void 0;
79
- return {
80
- hash,
81
- subject,
82
- body: body.trim(),
83
- packages: resolvePackages(context, match.groups.scope),
84
- type: bump,
85
- title: titleCase(match.groups.title)
86
- };
87
- }
88
- function commitTypeToBump(type, breaking) {
89
- if (breaking) return "major";
90
- if (type === "feat") return "minor";
91
- if (type === "fix" || type === "perf") return "patch";
92
- }
93
- function resolvePackages(context, scope) {
94
- if (!scope) return [];
95
- const packages = /* @__PURE__ */ new Set();
96
- for (const item of scope.split(",")) {
97
- const name = item.trim();
98
- if (!name) continue;
99
- if (context.graph.getByName(name).length > 0) {
100
- packages.add(name);
101
- continue;
102
- }
103
- const byShortName = context.graph.getPackages().filter((pkg) => pkg.name.split("/").at(-1) === name);
104
- if (byShortName.length > 0) {
105
- for (const pkg of byShortName) packages.add(pkg.name);
106
- continue;
107
- }
108
- packages.add(name);
109
- }
110
- return Array.from(packages).sort();
111
- }
112
146
  function renderChangelog(packages, changes) {
113
147
  return [
114
148
  "---",
@@ -127,12 +161,6 @@ function renderChange(change) {
127
161
  function titleCase(value) {
128
162
  return value.charAt(0).toUpperCase() + value.slice(1);
129
163
  }
130
- function slugify(value) {
131
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
132
- }
133
- function commandOutput(result) {
134
- return [result.stdout, result.stderr].filter(Boolean).join("\n");
135
- }
136
164
  //#endregion
137
165
  //#region src/context.ts
138
166
  async function createTegamiContext(options = {}) {
@@ -543,7 +571,7 @@ function tegami(options = {}) {
543
571
  }
544
572
  return {
545
573
  async generateChangelog(createOptions = {}) {
546
- return createChangelog(await $context, createOptions);
574
+ return generateChangelog(await $context, createOptions);
547
575
  },
548
576
  _internal: {
549
577
  options,
@@ -1,4 +1,4 @@
1
- import { s as TegamiPlugin } from "../types-BNhcu4-X.js";
1
+ import { s as TegamiPlugin } from "../types-CrA7ZKxb.js";
2
2
 
3
3
  //#region src/plugins/git.d.ts
4
4
  interface GitPluginOptions {
@@ -1,4 +1,4 @@
1
- import { O as DraftPlan, s as TegamiPlugin, t as Awaitable, w as TegamiContext, x as PackagePublishResult } from "../types-BNhcu4-X.js";
1
+ import { O as DraftPlan, s as TegamiPlugin, t as Awaitable, w as TegamiContext, x as PackagePublishResult } from "../types-CrA7ZKxb.js";
2
2
  import { GitPluginOptions } from "./git.js";
3
3
 
4
4
  //#region src/plugins/github.d.ts
@@ -17,6 +17,12 @@ interface VersionPullRequest {
17
17
  body?: string;
18
18
  }
19
19
  interface VersionPullRequestOptions {
20
+ /**
21
+ * Create the PR even outside of CI.
22
+ *
23
+ * @default false
24
+ */
25
+ forceCreate?: boolean;
20
26
  /**
21
27
  * Pull request branch.
22
28
  *
@@ -39,23 +39,27 @@ function github(options = {}) {
39
39
  function resolvePROptions() {
40
40
  const setting = options.cli?.versionPr ?? isCI();
41
41
  if (setting === false) return [false];
42
- return [true, typeof setting === "object" ? setting : {}];
42
+ if (setting === true) return [true, {}];
43
+ if (setting.forceCreate || isCI()) return [true, setting];
44
+ return [false];
43
45
  }
44
46
  function defaultVersionPRBody(draft, context) {
45
47
  const packageLines = [];
46
48
  for (const pkg of context.graph.getPackages()) {
47
49
  const packagePlan = draft.getPackagePlan(pkg.id);
48
- if (!packagePlan?.type) continue;
50
+ if (!packagePlan) continue;
51
+ const originalVersion = cliOriginalPackageVersions.get(pkg.id) ?? pkg.version;
52
+ if (originalVersion === pkg.version) continue;
49
53
  const publishTxt = packagePlan.publish ? "" : " (no publish)";
50
54
  const distTagTxt = formatNpmDistTag(packagePlan.npm?.distTag);
51
- packageLines.push(`- ${pkg.name}@${cliOriginalPackageVersions.get(pkg.id) ?? pkg.version} → ${pkg.name}@${pkg.version}${distTagTxt}${publishTxt}`);
55
+ packageLines.push(`- ${pkg.name}@${originalVersion} → ${pkg.name}@${pkg.version}${distTagTxt}${publishTxt}`);
52
56
  }
53
57
  const changelogLines = [];
54
58
  for (const entry of draft.getChangelogs()) {
55
59
  changelogLines.push(`### ${entry.subject ?? `\`${entry.filename}\``}`, "");
56
60
  for (const section of entry.sections) {
57
61
  changelogLines.push(`#### ${section.title}`, "");
58
- changelogLines.push(section.content);
62
+ if (section.content) changelogLines.push(section.content);
59
63
  }
60
64
  }
61
65
  const sections = [
@@ -1,2 +1,2 @@
1
- import { d as CargoRegistryClient, f as cargo, l as CargoPackage, u as CargoPluginOptions } from "../types-BNhcu4-X.js";
1
+ import { d as CargoRegistryClient, f as cargo, l as CargoPackage, u as CargoPluginOptions } from "../types-CrA7ZKxb.js";
2
2
  export { CargoPackage, CargoPluginOptions, CargoRegistryClient, cargo };
@@ -1,2 +1,2 @@
1
- import { g as npm, h as NpmRegistryClient, m as NpmPluginOptions, p as NpmPackage } from "../types-BNhcu4-X.js";
1
+ import { g as npm, h as NpmRegistryClient, m as NpmPluginOptions, p as NpmPackage } from "../types-CrA7ZKxb.js";
2
2
  export { NpmPackage, NpmPluginOptions, NpmRegistryClient, npm };
@@ -2,7 +2,7 @@ import { r as isNodeError, t as execFailure } from "../error-DBK-9uBa.js";
2
2
  import { n as WorkspacePackage } from "../graph-B22NBRUG.js";
3
3
  import { i as pnpmWorkspaceSchema, r as packageManifestSchema } from "../schemas-CurBAaW5.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
- import { join } from "node:path";
5
+ import path from "node:path";
6
6
  import { x } from "tinyexec";
7
7
  import * as semver from "semver";
8
8
  import { glob } from "tinyglobby";
@@ -31,7 +31,7 @@ var NpmPackage = class extends WorkspacePackage {
31
31
  return this.manifest.version ?? "0.0.0";
32
32
  }
33
33
  async write() {
34
- await writeFile(join(this.path, "package.json"), `${JSON.stringify(this.manifest, null, 2)}\n`);
34
+ await writeFile(path.join(this.path, "package.json"), `${JSON.stringify(this.manifest, null, 2)}\n`);
35
35
  }
36
36
  onPlan(context) {
37
37
  const defaults = super.onPlan(context);
@@ -96,13 +96,22 @@ function isMissingRegistryEntry(output) {
96
96
  const normalized = output.toLowerCase();
97
97
  return normalized.includes("e404") || normalized.includes("404") || normalized.includes("no match") || normalized.includes("no matching version") || normalized.includes("not found");
98
98
  }
99
- function parseDependencySpec(context, name, range) {
99
+ function parseDependencySpec(context, dependent, name, range) {
100
100
  const { graph } = context;
101
101
  if (range.startsWith("workspace:")) return {
102
102
  range: range.slice(10),
103
103
  linked: graph.get(`npm:${name}`),
104
104
  protocol: "workspace"
105
105
  };
106
+ if (range.startsWith("file:")) {
107
+ let target = path.resolve(dependent.path, range.slice(5));
108
+ if (path.basename(target) === "package.json") target = path.dirname(target);
109
+ return {
110
+ protocol: "file",
111
+ raw: range,
112
+ linked: graph.getPackages().find((pkg) => pkg instanceof NpmPackage && pkg.path === target)
113
+ };
114
+ }
106
115
  if (range.startsWith("npm:")) {
107
116
  const spec = range.slice(4);
108
117
  const separator = spec.lastIndexOf("@");
@@ -122,6 +131,7 @@ function parseDependencySpec(context, name, range) {
122
131
  }
123
132
  function formatDependencySpec(spec) {
124
133
  if (spec.protocol === "workspace") return `workspace:${spec.range}`;
134
+ if (spec.protocol === "file") return spec.raw;
125
135
  if (spec.protocol === "npm") return `npm:${spec.alias}@${spec.range}`;
126
136
  return spec.range;
127
137
  }
@@ -148,8 +158,9 @@ function npm({ client: defaultClient, onBreakPeerDep = "set", updateLockFile = f
148
158
  case "^":
149
159
  case "~": return !semver.satisfies(target, `${spec.range}${spec.linked.version}`);
150
160
  }
161
+ if (spec.protocol === "file") return true;
151
162
  }
152
- if (!semver.validRange(spec.range)) return false;
163
+ if (spec.protocol === "file" || !semver.validRange(spec.range)) return false;
153
164
  return !semver.satisfies(target, spec.range);
154
165
  }
155
166
  return {
@@ -162,7 +173,7 @@ function npm({ client: defaultClient, onBreakPeerDep = "set", updateLockFile = f
162
173
  const dependencies = dependent.manifest[field];
163
174
  if (!dependencies) continue;
164
175
  for (const [k, v] of Object.entries(dependencies)) {
165
- const spec = parseDependencySpec(context, k, v);
176
+ const spec = parseDependencySpec(context, dependent, k, v);
166
177
  if (!spec || spec.linked !== pkg) continue;
167
178
  if (!needsUpdate(dependent, spec, plan.bumpVersion(pkg))) continue;
168
179
  const bumpType = getBumpDepType({
@@ -215,9 +226,9 @@ function npm({ client: defaultClient, onBreakPeerDep = "set", updateLockFile = f
215
226
  const dependencies = pkg.manifest[field];
216
227
  if (!dependencies) continue;
217
228
  for (const [k, v] of Object.entries(dependencies)) {
218
- const spec = parseDependencySpec(this, k, v);
219
- if (!spec || !spec.linked) continue;
220
- if (!semver.validRange(spec.range) || spec.protocol === "workspace") continue;
229
+ const spec = parseDependencySpec(this, pkg, k, v);
230
+ if (!spec?.linked || spec.protocol === "workspace" || spec.protocol === "file") continue;
231
+ if (!semver.validRange(spec.range)) continue;
221
232
  if (semver.satisfies(spec.linked.version, spec.range)) continue;
222
233
  let updatedRange;
223
234
  const isPeer = field === "peerDependencies";
@@ -251,7 +262,7 @@ function npm({ client: defaultClient, onBreakPeerDep = "set", updateLockFile = f
251
262
  async function discoverNpmPackages(cwd, add) {
252
263
  let patterns;
253
264
  const rootManifest = await readManifest(cwd).catch(() => void 0);
254
- const pnpmPatterns = await readFile(join(cwd, "pnpm-workspace.yaml"), "utf8").then((content) => pnpmWorkspaceSchema.parse(load(content) ?? {})).catch((error) => {
265
+ const pnpmPatterns = await readFile(path.join(cwd, "pnpm-workspace.yaml"), "utf8").then((content) => pnpmWorkspaceSchema.parse(load(content) ?? {})).catch((error) => {
255
266
  if (isNodeError(error) && error.code === "ENOENT") return void 0;
256
267
  throw error;
257
268
  });
@@ -270,16 +281,18 @@ async function discoverNpmPackages(cwd, add) {
270
281
  }
271
282
  async function expandWorkspacePatterns(cwd, patterns) {
272
283
  if (patterns.length === 0) return [];
273
- return glob(patterns, {
284
+ return (await glob(patterns, {
274
285
  absolute: true,
275
286
  cwd,
276
287
  ignore: ["**/node_modules/**", "**/dist/**"],
277
288
  onlyDirectories: true,
278
289
  onlyFiles: false
290
+ })).map((item) => {
291
+ return item.endsWith(path.sep) ? item.slice(0, -1) : item;
279
292
  });
280
293
  }
281
294
  async function readManifest(packagePath) {
282
- const content = await readFile(join(packagePath, "package.json"), "utf8");
295
+ const content = await readFile(path.join(packagePath, "package.json"), "utf8");
283
296
  const parsed = JSON.parse(content);
284
297
  packageManifestSchema.parse(parsed);
285
298
  return parsed;
@@ -168,6 +168,7 @@ declare const planStoreSchema: z$1.ZodCodec<z$1.ZodString, z$1.ZodObject<{
168
168
  patch: "patch";
169
169
  }>>;
170
170
  sections: z$1.ZodArray<z$1.ZodObject<{
171
+ depth: z$1.ZodNumber;
171
172
  title: z$1.ZodString;
172
173
  content: z$1.ZodString;
173
174
  }, z$1.core.$strip>>;
@@ -222,7 +223,7 @@ type PackagePublishResult = ({
222
223
  changelogs: ChangelogEntry[];
223
224
  };
224
225
  //#endregion
225
- //#region src/changelog/create.d.ts
226
+ //#region src/changelog/generate.d.ts
226
227
  interface CreateChangelogOptions {
227
228
  /** Start revision. Defaults to the latest reachable git tag, or all history if none exists. */
228
229
  from?: string;
@@ -310,9 +311,17 @@ type DependencySpec = {
310
311
  range: string;
311
312
  linked?: WorkspacePackage;
312
313
  } | {
313
- protocol?: "workspace";
314
+ protocol: "workspace";
314
315
  range: string;
315
316
  linked?: WorkspacePackage;
317
+ } | {
318
+ protocol: "file";
319
+ raw: string;
320
+ linked?: WorkspacePackage;
321
+ } | {
322
+ range: string;
323
+ linked?: WorkspacePackage;
324
+ protocol?: undefined;
316
325
  };
317
326
  interface NpmPluginOptions {
318
327
  /** Package manager command used for npm registry operations. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tegami",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Utility for package versioning & publish",
5
5
  "license": "MIT",
6
6
  "author": "Fuma Nama",