tegami 0.0.0 → 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fuma Nama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ import { n as handlePluginError } from "./error-DBK-9uBa.js";
2
+ import { n as jsonCodec } from "./schemas-Cc4h6bq5.js";
3
+ import z$1 from "zod";
4
+ import { readFile } from "fs/promises";
5
+ //#region src/plans/store.ts
6
+ const packagePlanStoreSchema = z$1.object({
7
+ type: z$1.enum([
8
+ "major",
9
+ "minor",
10
+ "patch"
11
+ ]).optional(),
12
+ changelogIds: z$1.array(z$1.string()).optional(),
13
+ bumpReasons: z$1.array(z$1.string()).optional(),
14
+ npm: z$1.object({ distTag: z$1.string().optional() }).optional(),
15
+ publish: z$1.boolean()
16
+ });
17
+ /** the persisted plan data for actual publishing */
18
+ const planStoreSchema = jsonCodec(z$1.object({
19
+ id: z$1.string(),
20
+ createdAt: z$1.iso.datetime(),
21
+ version: z$1.literal("0.0.0").default("0.0.0"),
22
+ /** release note entries */
23
+ changelogs: z$1.record(z$1.string(), z$1.object({
24
+ filename: z$1.string(),
25
+ subject: z$1.string().optional(),
26
+ packages: z$1.array(z$1.string()),
27
+ type: z$1.enum([
28
+ "major",
29
+ "minor",
30
+ "patch"
31
+ ]),
32
+ title: z$1.string(),
33
+ content: z$1.string()
34
+ })),
35
+ /** package id -> package info */
36
+ packages: z$1.record(z$1.string(), packagePlanStoreSchema)
37
+ }));
38
+ function createPlanStore(draft, context) {
39
+ const store = {
40
+ version: "0.0.0",
41
+ id: `tegami-${Date.now().toString(36)}`,
42
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
43
+ changelogs: {},
44
+ packages: {}
45
+ };
46
+ for (const entry of draft.getChangelogs()) store.changelogs[entry.id] = {
47
+ filename: entry.filename,
48
+ subject: entry.subject,
49
+ packages: Array.from(entry.packages),
50
+ type: entry.type,
51
+ title: entry.title,
52
+ content: entry.content
53
+ };
54
+ for (const pkg of context.graph.getPackages()) {
55
+ const plan = draft.getPackagePlan(pkg.id);
56
+ if (plan) store.packages[pkg.id] = {
57
+ publish: plan.publish ?? false,
58
+ type: plan.type,
59
+ npm: plan.npm,
60
+ changelogIds: plan.changelogs?.map((entry) => entry.id),
61
+ bumpReasons: plan.bumpReasons ? Array.from(plan.bumpReasons) : void 0
62
+ };
63
+ }
64
+ return planStoreSchema.encode(store);
65
+ }
66
+ async function readPlanStore(context) {
67
+ try {
68
+ return planStoreSchema.decode(await readFile(context.planPath, "utf8"));
69
+ } catch {}
70
+ }
71
+ //#endregion
72
+ //#region src/plans/checks.ts
73
+ async function publishPlanStatus(store, context) {
74
+ async function defaultStatus() {
75
+ try {
76
+ await Promise.all(Object.entries(store.packages).map(async ([id, plan]) => {
77
+ const pkg = context.graph.get(id);
78
+ if (!pkg || !plan.publish) return;
79
+ if (!await context.getRegistryClient(pkg).isPackagePublished(pkg)) throw "pending";
80
+ }));
81
+ return { state: "success" };
82
+ } catch (err) {
83
+ if (err === "pending") return { state: "pending" };
84
+ throw err;
85
+ }
86
+ }
87
+ let status = await defaultStatus();
88
+ for (const plugin of context.plugins) {
89
+ const resolved = await handlePluginError(plugin, "resolvePlanStatus", () => plugin.resolvePlanStatus?.call(context, status, { plan: store }));
90
+ if (resolved) status = resolved;
91
+ }
92
+ return status;
93
+ }
94
+ async function assertPublishPlanFinished(context) {
95
+ const store = await readPlanStore(context);
96
+ if (!store) return;
97
+ 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.`);
98
+ }
99
+ //#endregion
100
+ export { readPlanStore as i, publishPlanStatus as n, createPlanStore as r, assertPublishPlanFinished as t };
@@ -0,0 +1,12 @@
1
+ import { C as PublishResult, a as Awaitable, t as Tegami, w as DraftPlan } from "../index-Bhh2dJZp.js";
2
+ import { Command } from "commander";
3
+
4
+ //#region src/cli/index.d.ts
5
+ interface TegamiCLIOptions {
6
+ /** create a custom draft plan, it must not be applied */
7
+ version?: () => Awaitable<DraftPlan>;
8
+ publish?: () => Awaitable<PublishResult>;
9
+ }
10
+ declare function createCli(tegami: Tegami, options?: TegamiCLIOptions): Command;
11
+ //#endregion
12
+ export { TegamiCLIOptions, createCli };
@@ -0,0 +1,212 @@
1
+ import { n as handlePluginError } from "../error-DBK-9uBa.js";
2
+ import { t as assertPublishPlanFinished } from "../checks-CKBRRjgQ.js";
3
+ import { t as isCI } from "../constants-B9qjNfvr.js";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { confirm, intro, isCancel, multiselect, note, outro, select, spinner, text } from "@clack/prompts";
7
+ import { Command } from "commander";
8
+ //#region src/cli/index.ts
9
+ var CancelledError = class extends Error {
10
+ constructor() {
11
+ super("Cancelled.");
12
+ }
13
+ };
14
+ function createCli(tegami, options = {}) {
15
+ const program = new Command();
16
+ program.name("tegami").description("create changelogs").action((commandOptions) => runAction(tegami, () => createChangelogs(tegami, {
17
+ ...commandOptions,
18
+ cli: options
19
+ })));
20
+ program.command("version").description("draft and apply a publish plan").action((commandOptions) => runAction(tegami, () => versionPackages(tegami, {
21
+ ...commandOptions,
22
+ cli: options
23
+ })));
24
+ 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, () => publishPackages(tegami, {
25
+ ...commandOptions,
26
+ cli: options
27
+ })));
28
+ program.command("cleanup").description("remove the publish plan after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
29
+ return program;
30
+ }
31
+ async function createChangelogs(tegami, _options) {
32
+ const context = await tegami._internal.context();
33
+ await assertPublishPlanFinished(context);
34
+ intro("Create changelogs");
35
+ const packages = context.graph.getPackages();
36
+ let selectedPackages = [];
37
+ if (isCI()) selectedPackages = [];
38
+ else {
39
+ const selected = await multiselect({
40
+ message: "Select packages (leave empty to auto-generate from commits)",
41
+ required: false,
42
+ options: packages.map((pkg) => ({
43
+ value: pkg.id,
44
+ label: pkg.id,
45
+ hint: pkg.version
46
+ }))
47
+ });
48
+ if (isCancel(selected)) throw new CancelledError();
49
+ selectedPackages = selected;
50
+ }
51
+ if (selectedPackages.length === 0) {
52
+ if (!isCI()) {
53
+ const confirmed = await confirm({
54
+ message: "Auto-generate changelog files from commits?",
55
+ initialValue: true
56
+ });
57
+ if (isCancel(confirmed)) throw new CancelledError();
58
+ if (!confirmed) {
59
+ outro("No changelogs created.");
60
+ return;
61
+ }
62
+ }
63
+ const s = spinner();
64
+ s.start("Reading commits and creating changelogs");
65
+ const created = await tegami.generateChangelog();
66
+ s.stop(created.length === 1 ? "Created 1 changelog file" : `Created ${created.length} changelog files`);
67
+ if (created.length === 0) note("No matching conventional commits were found.", "No changelogs created");
68
+ else note(created.map((entry) => `${entry.filename} (${entry.changes} changes)`).join("\n"), "Created changelogs");
69
+ outro("Changelogs ready.");
70
+ return;
71
+ }
72
+ const type = await select({
73
+ message: "Select release type",
74
+ options: [
75
+ {
76
+ value: "patch",
77
+ label: "patch"
78
+ },
79
+ {
80
+ value: "minor",
81
+ label: "minor"
82
+ },
83
+ {
84
+ value: "major",
85
+ label: "major"
86
+ }
87
+ ]
88
+ });
89
+ if (isCancel(type)) throw new CancelledError();
90
+ const message = await text({
91
+ message: "Change message",
92
+ placeholder: "Add a concise release note",
93
+ validate(value) {
94
+ if (!value?.trim()) return "Enter a message.";
95
+ }
96
+ });
97
+ if (isCancel(message)) throw new CancelledError();
98
+ const filename = changelogFilename();
99
+ const s = spinner();
100
+ s.start("Creating changelog");
101
+ await mkdir(context.changelogDir, { recursive: true });
102
+ await writeFile(join(context.changelogDir, filename), renderManualChangelog(selectedPackages, type, message.trim()));
103
+ s.stop("Created changelog file");
104
+ note(`${filename}\n${selectedPackages.join(", ")}: ${type}`, "Created changelog");
105
+ outro("Changelog ready.");
106
+ }
107
+ async function versionPackages(tegami, options) {
108
+ intro("Version Packages");
109
+ const { version: customVersion } = options.cli;
110
+ const context = await tegami._internal.context();
111
+ const draft = customVersion ? await customVersion() : await tegami.draft();
112
+ if (!draft.canApply()) throw new Error(`The draft plan from custom "version" hook must not be applied`);
113
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanCreated", () => plugin.cli?.publishPlanCreated?.call(context, draft));
114
+ if (!draft.hasPending()) {
115
+ note("No pending changelog entries matched workspace packages.", "Nothing to version");
116
+ outro("No versions changed.");
117
+ return;
118
+ }
119
+ const planEntries = [];
120
+ for (const pkg of context.graph.getPackages()) {
121
+ const plan = draft.getPackagePlan(pkg.id);
122
+ if (!plan?.type) continue;
123
+ planEntries.push(`${pkg.id}: ${plan.type} (${plan.changelogs?.length ?? 0} changelogs)`);
124
+ if (plan.bumpReasons) for (const reason of plan.bumpReasons) planEntries.push(` - ${reason}`);
125
+ }
126
+ note(planEntries.join("\n"), "Release plan");
127
+ const s = spinner();
128
+ s.start("Updating package versions");
129
+ try {
130
+ await draft.applyPlan();
131
+ } catch (error) {
132
+ s.stop("Failed to apply publish plan");
133
+ throw error;
134
+ }
135
+ s.stop("Package versions updated");
136
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanApplied", () => plugin.cli?.publishPlanApplied?.call(context, draft));
137
+ outro("Publish plan applied.");
138
+ }
139
+ async function publishPackages(tegami, options) {
140
+ const dryRun = options.dryRun ?? false;
141
+ const { publish: customPublish } = options.cli;
142
+ intro(dryRun ? "Publish packages (dry run)" : "Publish packages");
143
+ const s = spinner();
144
+ s.start(dryRun ? "Validating publish plan" : "Publishing packages");
145
+ const result = customPublish ? await customPublish() : await tegami.publish({ dryRun });
146
+ if (result.state === "skipped") {
147
+ const { planPath } = await tegami._internal.context();
148
+ s.stop(dryRun ? "No publish plan to validate" : "Nothing to publish");
149
+ outro(`No publishable packages were found in ${planPath}.`);
150
+ return;
151
+ }
152
+ s.stop(dryRun ? "Publish plan validated" : "Publish complete");
153
+ note(result.packages.map((pkg) => {
154
+ const tag = pkg.npm?.distTag ? ` (${pkg.npm.distTag})` : "";
155
+ const suffix = pkg.state === "failed" && pkg.error ? `: ${pkg.error}` : "";
156
+ return `${pkg.state === "success" ? "success" : "failed"} ${pkg.name}@${pkg.version}${tag}${suffix}`;
157
+ }).join("\n"), dryRun ? "Publish dry run" : "Publish result");
158
+ if (result.state === "failed") {
159
+ process.exitCode = 1;
160
+ outro("Some packages failed to publish.");
161
+ return;
162
+ }
163
+ outro(dryRun ? "Publish plan is valid." : "Packages published.");
164
+ }
165
+ async function runCleanup(tegami) {
166
+ intro("Cleanup publish plan");
167
+ const s = spinner();
168
+ s.start("Checking publish plan status");
169
+ const result = await tegami.cleanup();
170
+ const { planPath } = await tegami._internal.context();
171
+ s.stop(result.state === "removed" ? "Publish plan removed" : "Publish plan kept");
172
+ if (result.state === "removed") {
173
+ outro(`Removed ${planPath}.`);
174
+ return;
175
+ }
176
+ if (result.reason === "missing") {
177
+ outro(`No publish plan found at ${planPath}.`);
178
+ return;
179
+ }
180
+ outro(`Publish plan at ${planPath} is still pending. Publish it before cleanup.`);
181
+ }
182
+ function renderManualChangelog(packages, type, message) {
183
+ const heading = "#".repeat(type === "major" ? 1 : type === "minor" ? 2 : 3);
184
+ return [
185
+ "---",
186
+ `packages: ${JSON.stringify(packages)}`,
187
+ "---",
188
+ "",
189
+ `${heading} ${message}`,
190
+ ""
191
+ ].join("\n");
192
+ }
193
+ function changelogFilename() {
194
+ const date = /* @__PURE__ */ new Date();
195
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${Date.now().toString(36)}.md`;
196
+ }
197
+ async function runAction(tegami, action) {
198
+ try {
199
+ const context = await tegami._internal.context();
200
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.init", () => plugin.cli?.init?.call(context));
201
+ await action();
202
+ } catch (error) {
203
+ if (error instanceof CancelledError) outro(error.message);
204
+ else {
205
+ note(error instanceof Error ? error.message : String(error), "Error");
206
+ outro("Command failed.");
207
+ }
208
+ process.exit(1);
209
+ }
210
+ }
211
+ //#endregion
212
+ export { createCli };
@@ -0,0 +1,4 @@
1
+ //#region src/utils/constants.ts
2
+ const isCI = () => Boolean(process.env.CI);
3
+ //#endregion
4
+ export { isCI as t };
@@ -0,0 +1,23 @@
1
+ //#region src/utils/error.ts
2
+ function commandOutput(result) {
3
+ return [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
4
+ }
5
+ function execFailure(context, result) {
6
+ const lines = [context, `(exit ${result.exitCode})`];
7
+ const output = commandOutput(result);
8
+ if (output) lines.push(output);
9
+ return new Error(lines.join("\n"));
10
+ }
11
+ function isNodeError(error) {
12
+ return error instanceof Error && "code" in error;
13
+ }
14
+ async function handlePluginError(plugin, hookName, callback) {
15
+ try {
16
+ return await callback();
17
+ } catch (error) {
18
+ const details = error instanceof Error ? error.message : String(error);
19
+ throw new Error(`Plugin "${plugin.name}" failed during ${hookName}:\n${details}`, { cause: error });
20
+ }
21
+ }
22
+ //#endregion
23
+ export { handlePluginError as n, isNodeError as r, execFailure as t };
@@ -1,4 +1,4 @@
1
- import { n as LogGenerator } from "../types-DdIMewK9.mjs";
1
+ import { s as LogGenerator } from "../index-Bhh2dJZp.js";
2
2
 
3
3
  //#region src/generators/simple.d.ts
4
4
  declare function simpleGenerator(): LogGenerator;
@@ -1,8 +1,9 @@
1
+ import { r as formatPackageVersion } from "../semver-mWK2Khi2.js";
1
2
  //#region src/generators/simple.ts
2
3
  function simpleGenerator() {
3
- return { generate({ changelogs, version }) {
4
+ return { generate({ changelogs, version, packageName, plan }) {
4
5
  return [
5
- `## ${version}`,
6
+ `## ${formatPackageVersion(packageName, version, plan.npm?.distTag)}`,
6
7
  "",
7
8
  ...changelogs.flatMap((entry) => [
8
9
  `### ${entry.title}`,
@@ -0,0 +1,107 @@
1
+ import { t as bumpVersion } from "./semver-mWK2Khi2.js";
2
+ //#region src/graph.ts
3
+ /** Package discovered in the workspace. */
4
+ var WorkspacePackage = class {
5
+ get id() {
6
+ return `${this.manager}:${this.name}`;
7
+ }
8
+ opts = {};
9
+ /** note: this will only be available after package graph is resolved */
10
+ getPackageOptions() {
11
+ return this.opts;
12
+ }
13
+ setPackageOptions(options) {
14
+ this.opts = options;
15
+ }
16
+ /** create an empty draft plan. */
17
+ onPlan(_context) {
18
+ const { publish, prerelease, npm } = this.opts;
19
+ return {
20
+ publish,
21
+ prerelease,
22
+ npm: npm ? { distTag: npm.distTag } : void 0,
23
+ bumpVersion(pkg) {
24
+ return bumpVersion(pkg.version, this.type, this.prerelease);
25
+ }
26
+ };
27
+ }
28
+ };
29
+ /** Dependency graph for discovered workspace packages. */
30
+ var PackageGraph = class {
31
+ packages = /* @__PURE__ */ new Map();
32
+ groups = /* @__PURE__ */ new Map();
33
+ constructor(packages = []) {
34
+ for (const pkg of packages) this.add(pkg);
35
+ }
36
+ getPackages() {
37
+ const out = [];
38
+ for (const pkg of this.packages.values()) out.push(pkg.value);
39
+ return out;
40
+ }
41
+ /** Get a package by exact id. */
42
+ get(id) {
43
+ return this.packages.get(id)?.value;
44
+ }
45
+ /** Get packages by id, `group:name`, or every package matching a name. */
46
+ getByName(nameOrId) {
47
+ const exact = this.packages.get(nameOrId);
48
+ if (exact) return [exact.value];
49
+ if (nameOrId.startsWith("group:")) return this.getGroup(nameOrId.slice(6))?.packages ?? [];
50
+ const out = [];
51
+ for (const { value } of this.packages.values()) if (value.name === nameOrId) out.push(value);
52
+ return out;
53
+ }
54
+ /** scan package into graph, if the package id already exists, replace the existing one in graph */
55
+ add(pkg) {
56
+ this.delete(pkg.id);
57
+ this.packages.set(pkg.id, { value: pkg });
58
+ }
59
+ delete(id) {
60
+ this.packages.delete(id);
61
+ for (const group of this.groups.values()) {
62
+ const index = group.packages.findIndex((pkg) => pkg.id === id);
63
+ if (index >= 0) group.packages.splice(index, 1);
64
+ }
65
+ }
66
+ getPackageGroup(pkgId) {
67
+ return this.packages.get(pkgId)?.group;
68
+ }
69
+ getGroups() {
70
+ return Array.from(this.groups.values());
71
+ }
72
+ getGroup(name) {
73
+ return this.groups.get(name);
74
+ }
75
+ registerGroup(name, options) {
76
+ const existing = this.groups.get(name);
77
+ if (existing) {
78
+ existing.options = options;
79
+ return existing;
80
+ }
81
+ const group = {
82
+ name,
83
+ options,
84
+ packages: []
85
+ };
86
+ this.groups.set(name, group);
87
+ return group;
88
+ }
89
+ addGroupMember(groupId, id) {
90
+ const group = this.groups.get(groupId);
91
+ const pkg = this.packages.get(id);
92
+ if (!group || !pkg || pkg.group) return;
93
+ pkg.group = group;
94
+ group.packages.push(pkg.value);
95
+ }
96
+ removeGroupMember(group, id) {
97
+ const entry = this.groups.get(group);
98
+ if (!entry) return;
99
+ const index = entry.packages.findIndex((pkg) => pkg.id === id);
100
+ if (index >= 0) entry.packages.splice(index, 1);
101
+ }
102
+ unregisterGroup(name) {
103
+ this.groups.delete(name);
104
+ }
105
+ };
106
+ //#endregion
107
+ export { WorkspacePackage as n, PackageGraph as t };