tegami 0.0.1 → 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.
@@ -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 };
@@ -1,9 +1,9 @@
1
- import { _ as PublishResult, n as Awaitable, y as DraftPlan } from "../context-CC36jtz1.mjs";
2
- import { t as Tegami } from "../index-DJ7YxxdM.mjs";
1
+ import { C as PublishResult, a as Awaitable, t as Tegami, w as DraftPlan } from "../index-Bhh2dJZp.js";
3
2
  import { Command } from "commander";
4
3
 
5
4
  //#region src/cli/index.d.ts
6
5
  interface TegamiCLIOptions {
6
+ /** create a custom draft plan, it must not be applied */
7
7
  version?: () => Awaitable<DraftPlan>;
8
8
  publish?: () => Awaitable<PublishResult>;
9
9
  }
@@ -1,6 +1,8 @@
1
- import { t as isCI } from "../constants-B9qjNfvr.mjs";
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";
2
4
  import { mkdir, writeFile } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
5
+ import { join } from "node:path";
4
6
  import { confirm, intro, isCancel, multiselect, note, outro, select, spinner, text } from "@clack/prompts";
5
7
  import { Command } from "commander";
6
8
  //#region src/cli/index.ts
@@ -11,24 +13,26 @@ var CancelledError = class extends Error {
11
13
  };
12
14
  function createCli(tegami, options = {}) {
13
15
  const program = new Command();
14
- program.name("tegami").description("Create changelogs, version packages, and publish releases.").action((commandOptions) => runAction(tegami, () => createChangelogs(tegami, {
16
+ program.name("tegami").description("create changelogs").action((commandOptions) => runAction(tegami, () => createChangelogs(tegami, {
15
17
  ...commandOptions,
16
18
  cli: options
17
19
  })));
18
- program.command("version").description("create a publish plan and update package versions").action((commandOptions) => runAction(tegami, () => versionPackages(tegami, {
20
+ program.command("version").description("draft and apply a publish plan").action((commandOptions) => runAction(tegami, () => versionPackages(tegami, {
19
21
  ...commandOptions,
20
22
  cli: options
21
23
  })));
22
- program.command("publish").description("publish packages from the current publish plan").option("--dry-run", "validate the publish plan without publishing packages").action((commandOptions) => runAction(tegami, () => publishPackages(tegami, {
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, {
23
25
  ...commandOptions,
24
26
  cli: options
25
27
  })));
28
+ program.command("cleanup").description("remove the publish plan after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
26
29
  return program;
27
30
  }
28
31
  async function createChangelogs(tegami, _options) {
32
+ const context = await tegami._internal.context();
33
+ await assertPublishPlanFinished(context);
29
34
  intro("Create changelogs");
30
- const { graph, cwd, changelogDir } = await tegami._internal.context();
31
- const packages = graph.getPackages();
35
+ const packages = context.graph.getPackages();
32
36
  let selectedPackages = [];
33
37
  if (isCI()) selectedPackages = [];
34
38
  else {
@@ -92,11 +96,10 @@ async function createChangelogs(tegami, _options) {
92
96
  });
93
97
  if (isCancel(message)) throw new CancelledError();
94
98
  const filename = changelogFilename();
95
- const directory = resolve(cwd, changelogDir);
96
99
  const s = spinner();
97
100
  s.start("Creating changelog");
98
- await mkdir(directory, { recursive: true });
99
- await writeFile(join(directory, filename), renderManualChangelog(selectedPackages, type, message.trim()));
101
+ await mkdir(context.changelogDir, { recursive: true });
102
+ await writeFile(join(context.changelogDir, filename), renderManualChangelog(selectedPackages, type, message.trim()));
100
103
  s.stop("Created changelog file");
101
104
  note(`${filename}\n${selectedPackages.join(", ")}: ${type}`, "Created changelog");
102
105
  outro("Changelog ready.");
@@ -104,36 +107,34 @@ async function createChangelogs(tegami, _options) {
104
107
  async function versionPackages(tegami, options) {
105
108
  intro("Version Packages");
106
109
  const { version: customVersion } = options.cli;
110
+ const context = await tegami._internal.context();
107
111
  const draft = customVersion ? await customVersion() : await tegami.draft();
108
- const packageIds = draft.getPackageIds();
109
- if (packageIds.length === 0) {
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()) {
110
115
  note("No pending changelog entries matched workspace packages.", "Nothing to version");
111
116
  outro("No versions changed.");
112
117
  return;
113
118
  }
114
- note(packageIds.map((id) => {
115
- const plan = draft.getPackage(id);
116
- return `${id}: ${plan.type} (${plan.changelogIds.size} changelogs)`;
117
- }).join("\n"), "Release plan");
118
- if (draft.editable()) {
119
- const s = spinner();
120
- s.start("Updating package versions");
121
- try {
122
- await draft.createPublishPlan();
123
- } catch (error) {
124
- s.stop("Failed to create publish plan");
125
- throw error;
126
- }
127
- s.stop("Package versions updated");
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}`);
128
125
  }
129
- const context = await tegami._internal.context();
130
- for (const plugin of context.plugins) try {
131
- await plugin.cli?.afterVersion?.call(context, draft);
126
+ note(planEntries.join("\n"), "Release plan");
127
+ const s = spinner();
128
+ s.start("Updating package versions");
129
+ try {
130
+ await draft.applyPlan();
132
131
  } catch (error) {
133
- const details = error instanceof Error ? error.message : String(error);
134
- throw new Error(`Plugin "${plugin.name}" failed during afterVersion:\n${details}`, { cause: error });
132
+ s.stop("Failed to apply publish plan");
133
+ throw error;
135
134
  }
136
- outro("Publish plan created.");
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.");
137
138
  }
138
139
  async function publishPackages(tegami, options) {
139
140
  const dryRun = options.dryRun ?? false;
@@ -143,13 +144,14 @@ async function publishPackages(tegami, options) {
143
144
  s.start(dryRun ? "Validating publish plan" : "Publishing packages");
144
145
  const result = customPublish ? await customPublish() : await tegami.publish({ dryRun });
145
146
  if (result.state === "skipped") {
147
+ const { planPath } = await tegami._internal.context();
146
148
  s.stop(dryRun ? "No publish plan to validate" : "Nothing to publish");
147
- outro(`No publishable packages were found in ${result.planPath}.`);
149
+ outro(`No publishable packages were found in ${planPath}.`);
148
150
  return;
149
151
  }
150
152
  s.stop(dryRun ? "Publish plan validated" : "Publish complete");
151
153
  note(result.packages.map((pkg) => {
152
- const tag = pkg.distTag ? ` (${pkg.distTag})` : "";
154
+ const tag = pkg.npm?.distTag ? ` (${pkg.npm.distTag})` : "";
153
155
  const suffix = pkg.state === "failed" && pkg.error ? `: ${pkg.error}` : "";
154
156
  return `${pkg.state === "success" ? "success" : "failed"} ${pkg.name}@${pkg.version}${tag}${suffix}`;
155
157
  }).join("\n"), dryRun ? "Publish dry run" : "Publish result");
@@ -160,6 +162,23 @@ async function publishPackages(tegami, options) {
160
162
  }
161
163
  outro(dryRun ? "Publish plan is valid." : "Packages published.");
162
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
+ }
163
182
  function renderManualChangelog(packages, type, message) {
164
183
  const heading = "#".repeat(type === "major" ? 1 : type === "minor" ? 2 : 3);
165
184
  return [
@@ -178,21 +197,15 @@ function changelogFilename() {
178
197
  async function runAction(tegami, action) {
179
198
  try {
180
199
  const context = await tegami._internal.context();
181
- for (const plugin of context.plugins) try {
182
- await plugin.cli?.init?.call(context);
183
- } catch (error) {
184
- const details = error instanceof Error ? error.message : String(error);
185
- throw new Error(`Plugin "${plugin.name}" failed during cli.init:\n${details}`, { cause: error });
186
- }
200
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.init", () => plugin.cli?.init?.call(context));
187
201
  await action();
188
202
  } catch (error) {
189
- process.exitCode = 1;
190
- if (error instanceof CancelledError) {
191
- outro(error.message);
192
- return;
203
+ if (error instanceof CancelledError) outro(error.message);
204
+ else {
205
+ note(error instanceof Error ? error.message : String(error), "Error");
206
+ outro("Command failed.");
193
207
  }
194
- note(error instanceof Error ? error.message : String(error), "Error");
195
- outro("Command failed.");
208
+ process.exit(1);
196
209
  }
197
210
  }
198
211
  //#endregion
@@ -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 { r as LogGenerator } from "../context-CC36jtz1.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,9 +1,9 @@
1
- import { n as formatPackageVersion } from "../semver-FSWx3_34.mjs";
1
+ import { r as formatPackageVersion } from "../semver-mWK2Khi2.js";
2
2
  //#region src/generators/simple.ts
3
3
  function simpleGenerator() {
4
- return { generate({ changelogs, version, packageName, distTag }) {
4
+ return { generate({ changelogs, version, packageName, plan }) {
5
5
  return [
6
- `## ${formatPackageVersion(packageName, version, distTag)}`,
6
+ `## ${formatPackageVersion(packageName, version, plan.npm?.distTag)}`,
7
7
  "",
8
8
  ...changelogs.flatMap((entry) => [
9
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 };