tegami 1.1.3 → 1.2.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,427 @@
1
+ import { r as formatNpmDistTag } from "./semver-EKJ8yK5U.js";
2
+ import { n as execFailure, s as isCI } from "./error-CAsrGb6e.js";
3
+ import { t as _accessExpressionAsString } from "./_accessExpressionAsString-QhbUZzwv.js";
4
+ import { n as _validateReport, t as _createStandardSchema } from "./_createStandardSchema-BGQyz-uA.js";
5
+ import { o as PublishLock, r as runPreflights, s as parsePublishLock, t as initPublishPlan } from "./publish-Bt3e1RPs.js";
6
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { join, relative } from "node:path";
8
+ import { x } from "tinyexec";
9
+ import { tmpdir } from "node:os";
10
+ //#region src/utils/version-request.ts
11
+ const validatePublishGroupStore = (() => {
12
+ const _io0 = (input) => "object" === typeof input.groups && null !== input.groups && false === Array.isArray(input.groups) && _io1(input.groups);
13
+ const _io1 = (input) => Object.keys(input).every((key) => {
14
+ const value = input[key];
15
+ if (void 0 === value) return true;
16
+ return "pending" === value || "active" === value;
17
+ });
18
+ const _vo0 = (input, _path, _exceptionable = true) => [("object" === typeof input.groups && null !== input.groups && false === Array.isArray(input.groups) || _report(_exceptionable, {
19
+ path: _path + ".groups",
20
+ expected: "Record<string, \"pending\" | \"active\">",
21
+ value: input.groups
22
+ })) && _vo1(input.groups, _path + ".groups", _exceptionable) || _report(_exceptionable, {
23
+ path: _path + ".groups",
24
+ expected: "Record<string, \"pending\" | \"active\">",
25
+ value: input.groups
26
+ })].every((flag) => flag);
27
+ const _vo1 = (input, _path, _exceptionable = true) => [false === _exceptionable || Object.keys(input).map((key) => {
28
+ const value = input[key];
29
+ if (void 0 === value) return true;
30
+ return "pending" === value || "active" === value || _report(_exceptionable, {
31
+ path: _path + _accessExpressionAsString(key),
32
+ expected: "(\"active\" | \"pending\")",
33
+ value
34
+ });
35
+ }).every((flag) => flag)].every((flag) => flag);
36
+ const __is = (input) => "object" === typeof input && null !== input && _io0(input);
37
+ let errors;
38
+ let _report;
39
+ return _createStandardSchema((input) => {
40
+ if (false === __is(input)) {
41
+ errors = [];
42
+ _report = _validateReport(errors);
43
+ ((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || _report(true, {
44
+ path: _path + "",
45
+ expected: "PublishGroupStore",
46
+ value: input
47
+ })) && _vo0(input, _path + "", true) || _report(true, {
48
+ path: _path + "",
49
+ expected: "PublishGroupStore",
50
+ value: input
51
+ }))(input, "$input", true);
52
+ const success = 0 === errors.length;
53
+ return success ? {
54
+ success,
55
+ data: input
56
+ } : {
57
+ success,
58
+ errors,
59
+ data: input
60
+ };
61
+ }
62
+ return {
63
+ success: true,
64
+ data: input
65
+ };
66
+ });
67
+ })();
68
+ function onVersionRequest(provider) {
69
+ const namespace = `${provider.name}:publish-group`;
70
+ /** package id -> version before applying the CLI draft */
71
+ const snapshots = /* @__PURE__ */ new Map();
72
+ function resolveConfig(graph) {
73
+ if (provider.options === false) return;
74
+ const { forceCreate = false, branch = "tegami/version-packages", base = "main", groups, ...rest } = provider.options ?? {};
75
+ const groupMap = /* @__PURE__ */ new Map();
76
+ if (groups?.length) {
77
+ const unlistedPackages = /* @__PURE__ */ new Set();
78
+ for (const pkg of graph.getPackages()) unlistedPackages.add(pkg.id);
79
+ for (let group of groups ?? []) {
80
+ if (!Array.isArray(group)) group = [group];
81
+ let id = publishGroupId(group);
82
+ if (id === "unlisted") id = "custom-unlisted";
83
+ groupMap.set(id, group);
84
+ for (const member of group) for (const pkg of graph.getByName(member)) unlistedPackages.delete(pkg.id);
85
+ }
86
+ if (unlistedPackages.size > 0) groupMap.set("unlisted", Array.from(unlistedPackages));
87
+ }
88
+ return {
89
+ forceCreate,
90
+ branch,
91
+ base,
92
+ groupMap,
93
+ ...rest
94
+ };
95
+ }
96
+ /**
97
+ * Commit the group's lock state on top of `parent` and force-push its branch.
98
+ *
99
+ * Returns `true` and skips the push when the branch on `remote` already matches.
100
+ */
101
+ async function syncGroupBranch(context, opts) {
102
+ const lock = new PublishLock(opts.baseLock);
103
+ while (lock.read(namespace));
104
+ lock.write(namespace, opts.store);
105
+ const commit = await createLockCommit(context, opts.parent, lock.serialize(), await opts.config.commit?.call(context, {
106
+ type: "update-lock",
107
+ store: opts.store,
108
+ updatedLock: lock
109
+ }));
110
+ if (opts.remote && opts.remote.get(opts.branch) === commit) return true;
111
+ await run(context.cwd, [
112
+ "push",
113
+ "--force",
114
+ "origin",
115
+ `${commit}:refs/heads/${opts.branch}`
116
+ ], "Failed to push the version branch to origin. Ensure `origin` is configured and you have push access.");
117
+ return false;
118
+ }
119
+ return {
120
+ /** snapshot package versions, hook into `initCliDraft` */
121
+ initCliDraft() {
122
+ for (const pkg of this.graph.getPackages()) snapshots.set(pkg.id, pkg.version);
123
+ },
124
+ /** commit & push version branches, then upsert their requests, hook into `applyCliDraft` */
125
+ async applyCliDraft(draft) {
126
+ const config = resolveConfig(this.graph);
127
+ if (!config || !provider.canCreate(this)) return;
128
+ if (!(config.forceCreate || isCI()) || !await hasGitChanges(this.cwd)) return;
129
+ const requests = [];
130
+ const updatedPackages = /* @__PURE__ */ new Map();
131
+ for (const id of draft.getPackageDrafts().keys()) {
132
+ const snapshot = snapshots.get(id);
133
+ const pkg = this.graph.get(id);
134
+ if (pkg?.version && snapshot && pkg.version !== snapshot) updatedPackages.set(id, pkg);
135
+ }
136
+ for (const [id, members] of config.groupMap) {
137
+ const packages = [];
138
+ for (const member of members) for (const pkg of this.graph.getByName(member)) {
139
+ if (!updatedPackages.delete(pkg.id)) continue;
140
+ packages.push(pkg);
141
+ }
142
+ if (packages.length === 0) continue;
143
+ requests.push({
144
+ branch: `${config.branch}/${id}`,
145
+ packages,
146
+ publishGroup: id
147
+ });
148
+ }
149
+ if (updatedPackages.size > 0) requests.push({
150
+ branch: config.branch,
151
+ packages: Array.from(updatedPackages.values())
152
+ });
153
+ if (requests.length === 0) return;
154
+ await createVersionCommit(this.cwd, await config.commit?.call(this, { type: "version-packages" }));
155
+ let baseLock;
156
+ let parent;
157
+ const tasks = [];
158
+ for (const { branch, packages, publishGroup } of requests) {
159
+ const plan = await initPublishPlan(this, { packages: publishGroup ? packages.map((pkg) => pkg.id) : void 0 });
160
+ if (plan) await runPreflights(this, plan);
161
+ const ctx = {
162
+ draft,
163
+ plan,
164
+ getPreviousVersion: (id) => snapshots.get(id)
165
+ };
166
+ const custom = await config.create?.call(this, ctx);
167
+ const title = custom?.title ?? (publishGroup ? `Release ${packages.map((pkg) => `${pkg.name} (${pkg.manager})`).join(", ")}` : "Version Packages");
168
+ if (publishGroup) {
169
+ parent ??= await resolveHead(this.cwd);
170
+ baseLock ??= await parsePublishLock(await readFile(this.lockPath, "utf8"));
171
+ const newGroups = {};
172
+ for (const req of requests) {
173
+ if (!req.publishGroup) continue;
174
+ newGroups[req.publishGroup] = req.publishGroup !== publishGroup ? "pending" : "active";
175
+ }
176
+ const inSync = await syncGroupBranch(this, {
177
+ baseLock,
178
+ config,
179
+ branch,
180
+ parent,
181
+ store: { groups: newGroups }
182
+ });
183
+ tasks.push(provider.upsert(this, {
184
+ title,
185
+ body: custom?.body ?? renderRequestBody(this, ctx),
186
+ head: branch,
187
+ base: config.base
188
+ }, !inSync));
189
+ continue;
190
+ }
191
+ await pushBranch(this.cwd, branch);
192
+ await provider.upsert(this, {
193
+ title,
194
+ body: custom?.body ?? renderRequestBody(this, ctx),
195
+ head: branch,
196
+ base: config.base
197
+ }, true);
198
+ }
199
+ await Promise.all(tasks);
200
+ },
201
+ /** restore publish groups from the lock into the plan, hook into `initPublishPlan` */
202
+ initPublishPlan({ lock, plan }) {
203
+ const config = resolveConfig(this.graph);
204
+ if (!config) return;
205
+ let data;
206
+ const publishGroups = /* @__PURE__ */ new Map();
207
+ plan.$versionRequest = { publishGroups };
208
+ while (data = lock.read(namespace)) {
209
+ const validated = validatePublishGroupStore(data);
210
+ if (!validated.success) continue;
211
+ const store = validated.data;
212
+ for (const [id, state] of Object.entries(store.groups)) {
213
+ if (!config.groupMap.has(id) || publishGroups.get(id) === "active") continue;
214
+ publishGroups.set(id, state);
215
+ }
216
+ }
217
+ if (publishGroups.size === 0) return;
218
+ const packages = [];
219
+ if (plan.options.packages) packages.push(...plan.options.packages);
220
+ for (const [id, state] of publishGroups) {
221
+ if (state === "pending") continue;
222
+ for (const pkg of config.groupMap.get(id)) packages.push(pkg);
223
+ }
224
+ plan.options = {
225
+ ...plan.options,
226
+ packages
227
+ };
228
+ },
229
+ /** report `pending` while publish groups are waiting for their request */
230
+ resolvePlanStatus({ plan }) {
231
+ const publishGroups = plan.$versionRequest?.publishGroups;
232
+ if (!publishGroups) return;
233
+ for (const s of publishGroups.values()) if (s === "pending") return "pending";
234
+ },
235
+ /** re-sync the version requests of pending publish groups, hook into `beforePublishAll` */
236
+ async beforePublishAll({ plan }) {
237
+ if (plan.options.dryRun || !provider.canCreate(this)) return;
238
+ const config = resolveConfig(this.graph);
239
+ if (!config) return;
240
+ const publishGroups = plan.$versionRequest?.publishGroups;
241
+ if (!publishGroups || publishGroups.size === 0) return;
242
+ const pending = [];
243
+ for (const [id, state] of publishGroups) if (state === "pending") pending.push(id);
244
+ if (pending.length === 0) return;
245
+ const parent = await resolveHead(this.cwd);
246
+ const baseLock = parsePublishLock(await readFile(this.lockPath, "utf8"));
247
+ const remote = await resolveRemoteBranches(this.cwd, pending.map((id) => `${config.branch}/${id}`));
248
+ for (const id of pending) {
249
+ const newGroups = Object.fromEntries(publishGroups.entries());
250
+ newGroups[id] = "active";
251
+ await syncGroupBranch(this, {
252
+ branch: `${config.branch}/${id}`,
253
+ baseLock,
254
+ parent,
255
+ remote,
256
+ config,
257
+ store: { groups: newGroups }
258
+ });
259
+ }
260
+ }
261
+ };
262
+ }
263
+ function renderRequestBody(ctx, { draft, getPreviousVersion, plan }) {
264
+ const packageLines = [];
265
+ const changelogLines = [];
266
+ const publishLines = [];
267
+ const changesets = /* @__PURE__ */ new Map();
268
+ for (const [id, packageDraft] of draft.getPackageDrafts()) {
269
+ const pkg = ctx.graph.get(id);
270
+ if (!pkg) continue;
271
+ for (const changelog of packageDraft.changelogs ?? []) {
272
+ const list = changesets.get(changelog);
273
+ if (list) list.push(pkg);
274
+ else changesets.set(changelog, [pkg]);
275
+ }
276
+ const from = getPreviousVersion(id);
277
+ if (!from || from === pkg.version) continue;
278
+ packageLines.push(`| \`${pkg.name}\` | \`${from}\` | \`${pkg.version}\`${formatNpmDistTag(packageDraft.npm?.distTag)} |`);
279
+ }
280
+ for (const [entry, linkedPackages] of changesets) {
281
+ changelogLines.push(`### ${entry.subject ?? `\`${entry.filename}\``}`, "");
282
+ changelogLines.push("<details>", `<summary>Show Bumped Packages (${linkedPackages.length})</summary>`, "", ...linkedPackages.map((pkg) => `- \`${pkg.id}\``), "", "</details>", "");
283
+ for (const section of entry.sections) {
284
+ changelogLines.push(`#### ${section.title}`, "");
285
+ if (section.content) changelogLines.push(section.content);
286
+ }
287
+ }
288
+ if (plan) {
289
+ for (const [id, { preflight }] of plan.packages) if (preflight.shouldPublish) publishLines.push(`- ${id}`);
290
+ }
291
+ const sections = [
292
+ "## Summary",
293
+ "",
294
+ "All bumped packages.",
295
+ ""
296
+ ];
297
+ if (packageLines.length > 0) sections.push("| Package | From | To |", "| --- | --- | --- |", ...packageLines);
298
+ if (changelogLines.length > 0) sections.push("", "## Changelogs", ...changelogLines);
299
+ if (publishLines.length > 0) sections.push("", "## Publish", "", "The following packages will be published if merged:", ...publishLines);
300
+ sections.push("");
301
+ return sections.join("\n");
302
+ }
303
+ /**
304
+ * Commit a lock file change on top of `parent` via a detached index, without touching the
305
+ * working tree. Commit dates are pinned to the parent, so unchanged content always produces
306
+ * the exact same commit.
307
+ */
308
+ async function createLockCommit(context, parent, content, commit = {}) {
309
+ const { cwd } = context;
310
+ const dir = await mkdtemp(join(tmpdir(), "tegami-"));
311
+ try {
312
+ const blobFile = join(dir, "lock");
313
+ await writeFile(blobFile, content);
314
+ const blob = await run(cwd, [
315
+ "hash-object",
316
+ "-w",
317
+ blobFile
318
+ ], "Failed to store the lock file update.");
319
+ const path = relative(cwd, context.lockPath).replaceAll("\\", "/");
320
+ const env = {
321
+ ...process.env,
322
+ GIT_INDEX_FILE: join(dir, "index")
323
+ };
324
+ await run(cwd, ["read-tree", parent.commit], "Failed to read the current git tree.", env);
325
+ await run(cwd, [
326
+ "update-index",
327
+ "--add",
328
+ "--cacheinfo",
329
+ `100644,${blob},${path}`
330
+ ], "Failed to update the lock file entry.", env);
331
+ const args = [
332
+ "commit-tree",
333
+ await run(cwd, ["write-tree"], "Failed to write the updated git tree.", env),
334
+ "-p",
335
+ parent.commit,
336
+ "-m",
337
+ commit.title ?? "Update lock file"
338
+ ];
339
+ if (commit.body) args.push("-m", commit.body);
340
+ return await run(cwd, args, "Failed to commit the lock file update.", {
341
+ ...env,
342
+ GIT_AUTHOR_DATE: parent.date,
343
+ GIT_COMMITTER_DATE: parent.date
344
+ });
345
+ } finally {
346
+ await rm(dir, {
347
+ recursive: true,
348
+ force: true
349
+ });
350
+ }
351
+ }
352
+ /** the checked out commit and its committer date */
353
+ async function resolveHead(cwd) {
354
+ const [commit = "", date = ""] = (await run(cwd, [
355
+ "show",
356
+ "-s",
357
+ "--format=%H%n%cI"
358
+ ], "Failed to resolve the current commit.")).split("\n");
359
+ return {
360
+ commit,
361
+ date
362
+ };
363
+ }
364
+ /** branch -> commit on origin, `undefined` when the remote cannot be queried */
365
+ async function resolveRemoteBranches(cwd, branches) {
366
+ const result = await x("git", [
367
+ "ls-remote",
368
+ "origin",
369
+ ...branches.map((branch) => `refs/heads/${branch}`)
370
+ ], { nodeOptions: { cwd } });
371
+ if (result.exitCode !== 0) return;
372
+ const out = /* @__PURE__ */ new Map();
373
+ for (const line of result.stdout.split("\n")) {
374
+ const [commit, name] = line.split(" ");
375
+ const ref = name?.trim();
376
+ if (commit && ref?.startsWith("refs/heads/")) out.set(ref.slice(11), commit.trim());
377
+ }
378
+ return out;
379
+ }
380
+ async function hasGitChanges(cwd) {
381
+ return (await run(cwd, ["status", "--porcelain"], "Failed to check git status.")).length > 0;
382
+ }
383
+ /** commit the working tree changes onto a detached HEAD */
384
+ async function createVersionCommit(cwd, { title = "Version Packages", body } = {}) {
385
+ await run(cwd, ["checkout", "--detach"], "Failed to detach HEAD for version branches.");
386
+ await run(cwd, ["add", "-A"], "Failed to stage version changes.");
387
+ const args = [
388
+ "commit",
389
+ "-m",
390
+ title
391
+ ];
392
+ if (body) args.push("-m", body);
393
+ await run(cwd, args, "Failed to commit version changes.");
394
+ }
395
+ async function pushBranch(cwd, branch) {
396
+ await run(cwd, [
397
+ "checkout",
398
+ "-B",
399
+ branch
400
+ ], "Failed to create the version branch.");
401
+ await run(cwd, [
402
+ "push",
403
+ "--force",
404
+ "-u",
405
+ "origin",
406
+ branch
407
+ ], "Failed to push the version branch to origin. Ensure `origin` is configured and you have push access.");
408
+ }
409
+ async function run(cwd, args, message, env) {
410
+ const result = await x("git", args, { nodeOptions: {
411
+ cwd,
412
+ env
413
+ } });
414
+ if (result.exitCode !== 0) throw execFailure(message, result);
415
+ return result.stdout.trim();
416
+ }
417
+ function publishGroupId(members) {
418
+ const slugs = [];
419
+ for (const name of members.toSorted()) {
420
+ const v = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
421
+ if (v.length > 0) slugs.push(v);
422
+ }
423
+ if (slugs.length === 0) slugs.push("group");
424
+ return slugs.join("-");
425
+ }
426
+ //#endregion
427
+ export { onVersionRequest as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tegami",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "A tool to manage changelogs, versioning, and publishing in monorepo",
5
5
  "license": "MIT",
6
6
  "author": "Fuma Nama",