tegami 0.0.1 → 0.1.0-beta.1

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-Da6P6gSc.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,34 @@ 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, {
15
- ...commandOptions,
16
- cli: options
17
- })));
18
- program.command("version").description("create a publish plan and update package versions").action((commandOptions) => runAction(tegami, () => versionPackages(tegami, {
19
- ...commandOptions,
20
- cli: options
21
- })));
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, {
16
+ program.name("tegami").description("create changelogs").action((commandOptions) => runAction(tegami, () => createChangelogs(tegami, {
23
17
  ...commandOptions,
24
18
  cli: options
25
19
  })));
20
+ program.command("version").description("draft and apply a publish plan").action((commandOptions) => runAction(tegami, async () => {
21
+ await versionPackages(tegami, {
22
+ ...commandOptions,
23
+ cli: options
24
+ });
25
+ }));
26
+ program.command("ci").description("version and publish packages").action(() => runAction(tegami, async () => {
27
+ if (await versionPackages(tegami, { cli: options })) return;
28
+ await publishPackages(tegami, { cli: options });
29
+ }));
30
+ 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 () => {
31
+ await publishPackages(tegami, {
32
+ ...commandOptions,
33
+ cli: options
34
+ });
35
+ }));
36
+ program.command("cleanup").description("remove the publish plan after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
26
37
  return program;
27
38
  }
28
39
  async function createChangelogs(tegami, _options) {
40
+ const context = await tegami._internal.context();
41
+ await assertPublishPlanFinished(context);
29
42
  intro("Create changelogs");
30
- const { graph, cwd, changelogDir } = await tegami._internal.context();
31
- const packages = graph.getPackages();
43
+ const packages = context.graph.getPackages();
32
44
  let selectedPackages = [];
33
45
  if (isCI()) selectedPackages = [];
34
46
  else {
@@ -92,11 +104,10 @@ async function createChangelogs(tegami, _options) {
92
104
  });
93
105
  if (isCancel(message)) throw new CancelledError();
94
106
  const filename = changelogFilename();
95
- const directory = resolve(cwd, changelogDir);
96
107
  const s = spinner();
97
108
  s.start("Creating changelog");
98
- await mkdir(directory, { recursive: true });
99
- await writeFile(join(directory, filename), renderManualChangelog(selectedPackages, type, message.trim()));
109
+ await mkdir(context.changelogDir, { recursive: true });
110
+ await writeFile(join(context.changelogDir, filename), renderManualChangelog(selectedPackages, type, message.trim()));
100
111
  s.stop("Created changelog file");
101
112
  note(`${filename}\n${selectedPackages.join(", ")}: ${type}`, "Created changelog");
102
113
  outro("Changelog ready.");
@@ -104,36 +115,35 @@ async function createChangelogs(tegami, _options) {
104
115
  async function versionPackages(tegami, options) {
105
116
  intro("Version Packages");
106
117
  const { version: customVersion } = options.cli;
118
+ const context = await tegami._internal.context();
107
119
  const draft = customVersion ? await customVersion() : await tegami.draft();
108
- const packageIds = draft.getPackageIds();
109
- if (packageIds.length === 0) {
120
+ if (!draft.canApply()) throw new Error(`The draft plan from custom "version" hook must not be applied`);
121
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanCreated", () => plugin.cli?.publishPlanCreated?.call(context, draft));
122
+ if (!draft.hasPending()) {
110
123
  note("No pending changelog entries matched workspace packages.", "Nothing to version");
111
124
  outro("No versions changed.");
112
- return;
125
+ return false;
113
126
  }
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");
127
+ const planEntries = [];
128
+ for (const pkg of context.graph.getPackages()) {
129
+ const plan = draft.getPackagePlan(pkg.id);
130
+ if (!plan?.type) continue;
131
+ planEntries.push(`${pkg.id}: ${plan.type} (${plan.changelogs?.length ?? 0} changelogs)`);
132
+ if (plan.bumpReasons) for (const reason of plan.bumpReasons) planEntries.push(` - ${reason}`);
128
133
  }
129
- const context = await tegami._internal.context();
130
- for (const plugin of context.plugins) try {
131
- await plugin.cli?.afterVersion?.call(context, draft);
134
+ note(planEntries.join("\n"), "Release plan");
135
+ const s = spinner();
136
+ s.start("Updating package versions");
137
+ try {
138
+ await draft.applyPlan();
132
139
  } 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 });
140
+ s.stop("Failed to apply publish plan");
141
+ throw error;
135
142
  }
136
- outro("Publish plan created.");
143
+ s.stop("Package versions updated");
144
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.publishPlanApplied", () => plugin.cli?.publishPlanApplied?.call(context, draft));
145
+ outro("Publish plan applied.");
146
+ return true;
137
147
  }
138
148
  async function publishPackages(tegami, options) {
139
149
  const dryRun = options.dryRun ?? false;
@@ -143,22 +153,41 @@ async function publishPackages(tegami, options) {
143
153
  s.start(dryRun ? "Validating publish plan" : "Publishing packages");
144
154
  const result = customPublish ? await customPublish() : await tegami.publish({ dryRun });
145
155
  if (result.state === "skipped") {
156
+ const { planPath } = await tegami._internal.context();
146
157
  s.stop(dryRun ? "No publish plan to validate" : "Nothing to publish");
147
- outro(`No publishable packages were found in ${result.planPath}.`);
148
- return;
158
+ outro(`No publishable packages were found in ${planPath}.`);
159
+ return false;
149
160
  }
150
161
  s.stop(dryRun ? "Publish plan validated" : "Publish complete");
151
162
  note(result.packages.map((pkg) => {
152
- const tag = pkg.distTag ? ` (${pkg.distTag})` : "";
163
+ const tag = pkg.npm?.distTag ? ` (${pkg.npm.distTag})` : "";
153
164
  const suffix = pkg.state === "failed" && pkg.error ? `: ${pkg.error}` : "";
154
165
  return `${pkg.state === "success" ? "success" : "failed"} ${pkg.name}@${pkg.version}${tag}${suffix}`;
155
166
  }).join("\n"), dryRun ? "Publish dry run" : "Publish result");
156
167
  if (result.state === "failed") {
157
168
  process.exitCode = 1;
158
169
  outro("Some packages failed to publish.");
159
- return;
170
+ return false;
160
171
  }
161
172
  outro(dryRun ? "Publish plan is valid." : "Packages published.");
173
+ return true;
174
+ }
175
+ async function runCleanup(tegami) {
176
+ intro("Cleanup publish plan");
177
+ const s = spinner();
178
+ s.start("Checking publish plan status");
179
+ const result = await tegami.cleanup();
180
+ const { planPath } = await tegami._internal.context();
181
+ s.stop(result.state === "removed" ? "Publish plan removed" : "Publish plan kept");
182
+ if (result.state === "removed") {
183
+ outro(`Removed ${planPath}.`);
184
+ return;
185
+ }
186
+ if (result.reason === "missing") {
187
+ outro(`No publish plan found at ${planPath}.`);
188
+ return;
189
+ }
190
+ outro(`Publish plan at ${planPath} is still pending. Publish it before cleanup.`);
162
191
  }
163
192
  function renderManualChangelog(packages, type, message) {
164
193
  const heading = "#".repeat(type === "major" ? 1 : type === "minor" ? 2 : 3);
@@ -178,21 +207,15 @@ function changelogFilename() {
178
207
  async function runAction(tegami, action) {
179
208
  try {
180
209
  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
- }
210
+ for (const plugin of context.plugins) await handlePluginError(plugin, "cli.init", () => plugin.cli?.init?.call(context));
187
211
  await action();
188
212
  } catch (error) {
189
- process.exitCode = 1;
190
- if (error instanceof CancelledError) {
191
- outro(error.message);
192
- return;
213
+ if (error instanceof CancelledError) outro(error.message);
214
+ else {
215
+ note(error instanceof Error ? error.message : String(error), "Error");
216
+ outro("Command failed.");
193
217
  }
194
- note(error instanceof Error ? error.message : String(error), "Error");
195
- outro("Command failed.");
218
+ process.exit(1);
196
219
  }
197
220
  }
198
221
  //#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-Da6P6gSc.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 };