verelease 0.1.2-alpha.2 → 0.1.2-alpha.4

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/dist/bin.js CHANGED
@@ -1,12 +1,250 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/bin.ts
4
+ import { defineCommand, runMain } from "citty";
5
+ import { readFile as readFile15 } from "fs/promises";
6
+ import { fileURLToPath } from "url";
7
+ import { join as join20, dirname as dirname3 } from "path";
8
+
3
9
  // src/cli.ts
4
- import { resolve as resolve2, join as join13 } from "path";
5
- import { writeFile as writeFile5, mkdir as mkdir6, readFile as readFile7, unlink, rm as rm4 } from "fs/promises";
6
- import { mkdtemp as mkdtemp5 } from "fs/promises";
7
- import { tmpdir as tmpdir5 } from "os";
10
+ import { resolve as resolve2, join as join19 } from "path";
11
+ import { writeFile as writeFile6, mkdir as mkdir6, readFile as readFile14, rm as rm6 } from "fs/promises";
12
+ import { mkdtemp as mkdtemp7 } from "fs/promises";
13
+ import { tmpdir as tmpdir7 } from "os";
8
14
  import { existsSync } from "fs";
9
- import { createInterface } from "readline/promises";
15
+ import * as prompts3 from "@clack/prompts";
16
+ import colors3 from "picocolors";
17
+ import * as Effect4 from "effect/Effect";
18
+ import * as Exit2 from "effect/Exit";
19
+
20
+ // ../core/dist/errors.js
21
+ import * as Data from "effect/Data";
22
+ var NpmAuthError = class extends Data.TaggedError("NpmAuthError") {
23
+ };
24
+ var BaselineFetchError = class extends Data.TaggedError("BaselineFetchError") {
25
+ };
26
+ var TestExecutionError = class extends Data.TaggedError("TestExecutionError") {
27
+ };
28
+ var ManifestMutationError = class extends Data.TaggedError("ManifestMutationError") {
29
+ };
30
+ var LockfileSyncError = class extends Data.TaggedError("LockfileSyncError") {
31
+ };
32
+ var PublishError = class extends Data.TaggedError("PublishError") {
33
+ };
34
+ var RegistryCollisionError = class extends Data.TaggedError("RegistryCollisionError") {
35
+ };
36
+ var CargoAuthError = class extends Data.TaggedError("CargoAuthError") {
37
+ };
38
+
39
+ // ../core/dist/transaction.js
40
+ import * as Effect from "effect/Effect";
41
+ import * as Exit from "effect/Exit";
42
+ import { readFile, writeFile, unlink, stat } from "fs/promises";
43
+ import { join } from "path";
44
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
45
+ async function fileExists(path) {
46
+ try {
47
+ const s = await stat(path);
48
+ return s.isFile();
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function executeReleaseTransaction(options, use) {
54
+ const { plan, cwd, lockfileAdapter, onRollback, onStep } = options;
55
+ const acquire = Effect.tryPromise({
56
+ try: async () => {
57
+ onStep?.("Creating manifest backups...");
58
+ const filesToTouch = /* @__PURE__ */ new Set();
59
+ for (const pkgPlan of plan.packages) {
60
+ if (pkgPlan.bump !== "none") {
61
+ const pkgJson = join(pkgPlan.directory, "package.json");
62
+ const jsrJson = join(pkgPlan.directory, "jsr.json");
63
+ const denoJson = join(pkgPlan.directory, "deno.json");
64
+ const cargoToml = join(pkgPlan.directory, "Cargo.toml");
65
+ if (await fileExists(pkgJson))
66
+ filesToTouch.add(pkgJson);
67
+ if (await fileExists(jsrJson))
68
+ filesToTouch.add(jsrJson);
69
+ if (await fileExists(denoJson))
70
+ filesToTouch.add(denoJson);
71
+ if (await fileExists(cargoToml))
72
+ filesToTouch.add(cargoToml);
73
+ }
74
+ for (const update of pkgPlan.dependentUpdates) {
75
+ const depPkgPlan = plan.packages.find((p) => p.name === update.dependentName);
76
+ if (depPkgPlan) {
77
+ const depPkgJson = join(depPkgPlan.directory, "package.json");
78
+ const depCargoToml = join(depPkgPlan.directory, "Cargo.toml");
79
+ if (await fileExists(depPkgJson))
80
+ filesToTouch.add(depPkgJson);
81
+ if (await fileExists(depCargoToml))
82
+ filesToTouch.add(depCargoToml);
83
+ }
84
+ }
85
+ }
86
+ const backups = [];
87
+ for (const filePath of filesToTouch) {
88
+ const originalContent = await readFile(filePath, "utf8");
89
+ backups.push({ filePath, originalContent });
90
+ }
91
+ onStep?.("Applying version bumps to manifests...");
92
+ const versionMap = /* @__PURE__ */ new Map();
93
+ for (const pkgPlan of plan.packages) {
94
+ if (pkgPlan.bump !== "none") {
95
+ versionMap.set(pkgPlan.name, pkgPlan.proposedVersion);
96
+ }
97
+ }
98
+ const depUpdateMap = /* @__PURE__ */ new Map();
99
+ for (const pkgPlan of plan.packages) {
100
+ for (const depUp of pkgPlan.dependentUpdates) {
101
+ if (!depUpdateMap.has(depUp.dependentName)) {
102
+ depUpdateMap.set(depUp.dependentName, []);
103
+ }
104
+ depUpdateMap.get(depUp.dependentName).push({
105
+ dependencyName: depUp.dependencyName,
106
+ newRange: depUp.newRange,
107
+ type: depUp.dependencyType
108
+ });
109
+ }
110
+ }
111
+ const modifiedManifests = [];
112
+ for (const pkgPlan of plan.packages) {
113
+ const pkgJsonPath = join(pkgPlan.directory, "package.json");
114
+ if (filesToTouch.has(pkgJsonPath)) {
115
+ const content = await readFile(pkgJsonPath, "utf8");
116
+ const parsed = JSON.parse(content);
117
+ if (versionMap.has(pkgPlan.name)) {
118
+ parsed.version = versionMap.get(pkgPlan.name);
119
+ }
120
+ const depUpdates = depUpdateMap.get(pkgPlan.name);
121
+ if (depUpdates) {
122
+ for (const up of depUpdates) {
123
+ const sec = parsed[up.type];
124
+ if (sec && sec[up.dependencyName]) {
125
+ sec[up.dependencyName] = up.newRange;
126
+ }
127
+ }
128
+ }
129
+ await writeFile(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
130
+ modifiedManifests.push(pkgJsonPath);
131
+ }
132
+ const jsrJsonPath = join(pkgPlan.directory, "jsr.json");
133
+ if (filesToTouch.has(jsrJsonPath)) {
134
+ const content = await readFile(jsrJsonPath, "utf8");
135
+ const parsed = JSON.parse(content);
136
+ if (versionMap.has(pkgPlan.name)) {
137
+ parsed.version = versionMap.get(pkgPlan.name);
138
+ }
139
+ await writeFile(jsrJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
140
+ modifiedManifests.push(jsrJsonPath);
141
+ }
142
+ const denoJsonPath = join(pkgPlan.directory, "deno.json");
143
+ if (filesToTouch.has(denoJsonPath)) {
144
+ const content = await readFile(denoJsonPath, "utf8");
145
+ const parsed = JSON.parse(content);
146
+ if (versionMap.has(pkgPlan.name)) {
147
+ parsed.version = versionMap.get(pkgPlan.name);
148
+ }
149
+ await writeFile(denoJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
150
+ modifiedManifests.push(denoJsonPath);
151
+ }
152
+ const cargoTomlPath = join(pkgPlan.directory, "Cargo.toml");
153
+ if (filesToTouch.has(cargoTomlPath)) {
154
+ const content = await readFile(cargoTomlPath, "utf8");
155
+ const parsed = parseToml(content);
156
+ if (versionMap.has(pkgPlan.name) && parsed.package && typeof parsed.package === "object") {
157
+ parsed.package.version = versionMap.get(pkgPlan.name);
158
+ }
159
+ const depUpdates = depUpdateMap.get(pkgPlan.name);
160
+ if (depUpdates) {
161
+ for (const up of depUpdates) {
162
+ for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
163
+ const dependencies = parsed[section];
164
+ if (!dependencies || typeof dependencies !== "object" || !(up.dependencyName in dependencies)) {
165
+ continue;
166
+ }
167
+ const dependency = dependencies[up.dependencyName];
168
+ if (typeof dependency === "string") {
169
+ dependencies[up.dependencyName] = up.newRange;
170
+ } else if (dependency && typeof dependency === "object") {
171
+ dependency.version = up.newRange;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ await writeFile(cargoTomlPath, stringifyToml(parsed), "utf8");
177
+ modifiedManifests.push(cargoTomlPath);
178
+ }
179
+ }
180
+ onStep?.(`Regenerating lockfile with ${lockfileAdapter.packageManager}...`);
181
+ const lockRes = await lockfileAdapter.regenerateLockfile(cwd);
182
+ if (!lockRes.success) {
183
+ for (const b of backups) {
184
+ await writeFile(b.filePath, b.originalContent, "utf8");
185
+ }
186
+ throw new Error(`Lockfile generation failed: ${lockRes.output}`);
187
+ }
188
+ const changelogBackups = /* @__PURE__ */ new Map();
189
+ const generatedFiles = /* @__PURE__ */ new Set();
190
+ const scope = {
191
+ plan,
192
+ modifiedManifests,
193
+ changelogBackups,
194
+ generatedFiles,
195
+ backupChangelog(path, original) {
196
+ changelogBackups.set(path, original);
197
+ },
198
+ trackGeneratedFile(path) {
199
+ generatedFiles.add(path);
200
+ }
201
+ };
202
+ return { scope, backups };
203
+ },
204
+ catch: (err) => new ManifestMutationError({
205
+ message: err instanceof Error ? err.message : String(err)
206
+ })
207
+ });
208
+ const release = (resource, exit) => Effect.promise(async () => {
209
+ if (Exit.isFailure(exit)) {
210
+ onRollback?.("Rolling back manifests to previous versions...");
211
+ for (const b of resource.backups) {
212
+ try {
213
+ await writeFile(b.filePath, b.originalContent, "utf8");
214
+ } catch {
215
+ }
216
+ }
217
+ onRollback?.("Restored manifests.");
218
+ onRollback?.("Restoring changelog files...");
219
+ for (const [chPath, original] of resource.scope.changelogBackups) {
220
+ try {
221
+ if (original === null) {
222
+ await unlink(chPath);
223
+ } else {
224
+ await writeFile(chPath, original, "utf8");
225
+ }
226
+ } catch {
227
+ }
228
+ }
229
+ onRollback?.("Restored changelog files.");
230
+ onRollback?.("Removing generated migration guides...");
231
+ for (const genPath of resource.scope.generatedFiles) {
232
+ try {
233
+ await unlink(genPath);
234
+ } catch {
235
+ }
236
+ }
237
+ onRollback?.("Removed generated migration guides.");
238
+ onRollback?.("Synchronizing workspace lockfile...");
239
+ try {
240
+ await lockfileAdapter.regenerateLockfile(cwd);
241
+ onRollback?.("Synchronized lockfile.");
242
+ } catch {
243
+ }
244
+ }
245
+ });
246
+ return Effect.acquireUseRelease(acquire, ({ scope }) => use(scope), release);
247
+ }
10
248
 
11
249
  // ../workspace/dist/graph.js
12
250
  var PackageGraphImpl = class {
@@ -160,12 +398,13 @@ var PackageGraphImpl = class {
160
398
  };
161
399
 
162
400
  // ../workspace/dist/resolver.js
163
- import { readFile, stat, readdir } from "fs/promises";
164
- import { join, relative, resolve } from "path";
401
+ import { readFile as readFile2, stat as stat2, readdir } from "fs/promises";
402
+ import { join as join2, relative, resolve } from "path";
165
403
  import { parse as parseYaml } from "yaml";
166
- async function fileExists(path) {
404
+ import { parse as parseToml2 } from "smol-toml";
405
+ async function fileExists2(path) {
167
406
  try {
168
- const s = await stat(path);
407
+ const s = await stat2(path);
169
408
  return s.isFile();
170
409
  } catch {
171
410
  return false;
@@ -173,113 +412,343 @@ async function fileExists(path) {
173
412
  }
174
413
  async function dirExists(path) {
175
414
  try {
176
- const s = await stat(path);
415
+ const s = await stat2(path);
177
416
  return s.isDirectory();
178
417
  } catch {
179
418
  return false;
180
419
  }
181
420
  }
421
+ function extractCargoDeps(depSection) {
422
+ const result = {};
423
+ if (!depSection || typeof depSection !== "object")
424
+ return result;
425
+ for (const [key, value] of Object.entries(depSection)) {
426
+ if (typeof value === "string") {
427
+ result[key] = value;
428
+ } else if (value && typeof value === "object") {
429
+ const depObj = value;
430
+ if (typeof depObj.version === "string") {
431
+ result[key] = depObj.version;
432
+ } else if (depObj.path) {
433
+ result[key] = "*";
434
+ } else if (depObj.workspace === true) {
435
+ result[key] = "workspace:*";
436
+ }
437
+ }
438
+ }
439
+ return result;
440
+ }
182
441
  var DefaultWorkspaceResolver = class {
183
442
  name = "default-workspace-resolver";
184
443
  async supports(rootDir) {
185
- const pnpmWorkspace = join(rootDir, "pnpm-workspace.yaml");
186
- const packageJson = join(rootDir, "package.json");
187
- return await fileExists(pnpmWorkspace) || await fileExists(packageJson);
444
+ const pnpmWorkspace = join2(rootDir, "pnpm-workspace.yaml");
445
+ const packageJson = join2(rootDir, "package.json");
446
+ const cargoToml = join2(rootDir, "Cargo.toml");
447
+ return await fileExists2(pnpmWorkspace) || await fileExists2(packageJson) || await fileExists2(cargoToml);
188
448
  }
189
449
  async resolve(rootDir) {
190
450
  const absRootDir = resolve(rootDir);
191
- const pnpmWorkspacePath = join(absRootDir, "pnpm-workspace.yaml");
192
- const rootPkgJsonPath = join(absRootDir, "package.json");
193
- let packageManager = "unknown";
194
- let patterns = [];
195
- if (await fileExists(pnpmWorkspacePath)) {
196
- packageManager = "pnpm";
197
- const content = await readFile(pnpmWorkspacePath, "utf8");
198
- const parsed = parseYaml(content);
199
- if (parsed && Array.isArray(parsed.packages)) {
200
- patterns = parsed.packages;
201
- }
202
- } else if (await fileExists(rootPkgJsonPath)) {
203
- const content = await readFile(rootPkgJsonPath, "utf8");
204
- const parsed = JSON.parse(content);
205
- if (Array.isArray(parsed.workspaces)) {
206
- patterns = parsed.workspaces;
207
- packageManager = "npm";
208
- } else if (parsed.workspaces && Array.isArray(parsed.workspaces.packages)) {
209
- patterns = parsed.workspaces.packages;
210
- packageManager = "yarn";
211
- } else {
212
- const pkg = {
213
- name: parsed.name || "unnamed-package",
214
- version: parsed.version || "0.0.0",
215
- directory: absRootDir,
216
- manifestPath: rootPkgJsonPath,
217
- manifest: parsed,
218
- isPrivate: !!parsed.private,
219
- relativeDir: "."
220
- };
221
- const graph2 = new PackageGraphImpl([pkg]);
222
- return {
223
- rootDirectory: absRootDir,
224
- packageManager: "npm",
225
- packages: [pkg],
226
- graph: graph2
227
- };
451
+ const pnpmWorkspacePath = join2(absRootDir, "pnpm-workspace.yaml");
452
+ const rootPkgJsonPath = join2(absRootDir, "package.json");
453
+ const rootCargoTomlPath = join2(absRootDir, "Cargo.toml");
454
+ const allPackages = [];
455
+ const skipped = [];
456
+ const ecosystems = [];
457
+ let detectedPackageManager = "unknown";
458
+ const hasPnpmWorkspace = await fileExists2(pnpmWorkspacePath);
459
+ const hasRootPkgJson = await fileExists2(rootPkgJsonPath);
460
+ if (hasPnpmWorkspace || hasRootPkgJson) {
461
+ let jsPatterns = [];
462
+ let jsPackageManager = "unknown";
463
+ if (hasPnpmWorkspace) {
464
+ jsPackageManager = "pnpm";
465
+ try {
466
+ const content = await readFile2(pnpmWorkspacePath, "utf8");
467
+ const parsed = parseYaml(content);
468
+ if (parsed && Array.isArray(parsed.packages)) {
469
+ jsPatterns = parsed.packages;
470
+ }
471
+ } catch {
472
+ }
473
+ } else if (hasRootPkgJson) {
474
+ try {
475
+ const content = await readFile2(rootPkgJsonPath, "utf8");
476
+ const parsed = JSON.parse(content);
477
+ if (Array.isArray(parsed.workspaces)) {
478
+ jsPatterns = parsed.workspaces;
479
+ jsPackageManager = "npm";
480
+ } else if (parsed.workspaces && Array.isArray(parsed.workspaces.packages)) {
481
+ jsPatterns = parsed.workspaces.packages;
482
+ jsPackageManager = "yarn";
483
+ } else {
484
+ const pkg = {
485
+ name: parsed.name || "unnamed-package",
486
+ version: parsed.version || "0.0.0",
487
+ directory: absRootDir,
488
+ manifestPath: rootPkgJsonPath,
489
+ manifest: parsed,
490
+ isPrivate: !!parsed.private,
491
+ relativeDir: ".",
492
+ ecosystem: "npm"
493
+ };
494
+ allPackages.push(pkg);
495
+ ecosystems.push("npm");
496
+ detectedPackageManager = "npm";
497
+ }
498
+ } catch {
499
+ }
228
500
  }
229
- }
230
- if (patterns.length === 0) {
231
- patterns = ["packages/*"];
232
- }
233
- const matchedDirs = /* @__PURE__ */ new Set();
234
- for (const pattern of patterns) {
235
- if (pattern.endsWith("/*")) {
236
- const baseDir = join(absRootDir, pattern.slice(0, -2));
237
- if (await dirExists(baseDir)) {
238
- const entries = await readdir(baseDir, { withFileTypes: true });
239
- for (const entry of entries) {
240
- if (entry.isDirectory()) {
241
- matchedDirs.add(join(baseDir, entry.name));
242
- }
501
+ if (jsPatterns.length > 0) {
502
+ detectedPackageManager = jsPackageManager;
503
+ ecosystems.push("npm");
504
+ const matchedDirs = /* @__PURE__ */ new Set();
505
+ const { included, excluded } = splitPatterns(jsPatterns);
506
+ for (const pattern of included) {
507
+ for (const dir of await expandPattern(absRootDir, pattern)) {
508
+ matchedDirs.add(dir);
243
509
  }
244
510
  }
245
- } else {
246
- const exactDir = join(absRootDir, pattern);
247
- if (await dirExists(exactDir)) {
248
- matchedDirs.add(exactDir);
511
+ for (const pattern of excluded) {
512
+ for (const dir of await expandPattern(absRootDir, pattern)) {
513
+ matchedDirs.delete(dir);
514
+ }
515
+ }
516
+ for (const dir of matchedDirs) {
517
+ const manifestPath = join2(dir, "package.json");
518
+ if (await fileExists2(manifestPath)) {
519
+ try {
520
+ const content = await readFile2(manifestPath, "utf8");
521
+ const manifest = JSON.parse(content);
522
+ if (!manifest.name) {
523
+ skipped.push({ directory: dir, reason: "package.json names no package" });
524
+ } else if (!manifest.version) {
525
+ skipped.push({
526
+ directory: dir,
527
+ name: manifest.name,
528
+ reason: "package.json declares no version"
529
+ });
530
+ }
531
+ if (manifest.name && manifest.version) {
532
+ allPackages.push({
533
+ name: manifest.name,
534
+ version: manifest.version,
535
+ directory: dir,
536
+ manifestPath,
537
+ manifest,
538
+ isPrivate: !!manifest.private,
539
+ relativeDir: relative(absRootDir, dir),
540
+ ecosystem: "npm"
541
+ });
542
+ }
543
+ } catch {
544
+ }
545
+ }
249
546
  }
250
547
  }
251
548
  }
252
- const packages = [];
253
- for (const dir of matchedDirs) {
254
- const manifestPath = join(dir, "package.json");
255
- if (await fileExists(manifestPath)) {
256
- try {
257
- const content = await readFile(manifestPath, "utf8");
258
- const manifest = JSON.parse(content);
259
- if (manifest.name && manifest.version) {
260
- packages.push({
261
- name: manifest.name,
262
- version: manifest.version,
263
- directory: dir,
264
- manifestPath,
265
- manifest,
266
- isPrivate: !!manifest.private,
267
- relativeDir: relative(absRootDir, dir)
268
- });
549
+ if (await fileExists2(rootCargoTomlPath)) {
550
+ try {
551
+ const rootCargoRaw = await readFile2(rootCargoTomlPath, "utf8");
552
+ const rootCargo = parseToml2(rootCargoRaw);
553
+ const workspaceDefaultVersion = rootCargo.workspace?.package?.version || rootCargo.package?.version || "0.1.0";
554
+ let cargoPatterns = [];
555
+ if (rootCargo.workspace && Array.isArray(rootCargo.workspace.members)) {
556
+ cargoPatterns = rootCargo.workspace.members;
557
+ }
558
+ if (rootCargo.package && rootCargo.package.name) {
559
+ const pkgName = rootCargo.package.name;
560
+ const pkgVersion = typeof rootCargo.package.version === "string" ? rootCargo.package.version : workspaceDefaultVersion;
561
+ const isPrivate = rootCargo.package.publish === false;
562
+ const manifest = {
563
+ name: pkgName,
564
+ version: pkgVersion,
565
+ private: isPrivate,
566
+ dependencies: extractCargoDeps(rootCargo.dependencies),
567
+ devDependencies: extractCargoDeps(rootCargo["dev-dependencies"])
568
+ };
569
+ allPackages.push({
570
+ name: pkgName,
571
+ version: pkgVersion,
572
+ directory: absRootDir,
573
+ manifestPath: rootCargoTomlPath,
574
+ manifest,
575
+ isPrivate,
576
+ relativeDir: ".",
577
+ ecosystem: "cargo"
578
+ });
579
+ }
580
+ if (cargoPatterns.length > 0) {
581
+ const matchedCrateDirs = /* @__PURE__ */ new Set();
582
+ for (const pattern of cargoPatterns) {
583
+ if (pattern.endsWith("/*")) {
584
+ const baseDir = join2(absRootDir, pattern.slice(0, -2));
585
+ if (await dirExists(baseDir)) {
586
+ const entries = await readdir(baseDir, { withFileTypes: true });
587
+ for (const entry of entries) {
588
+ if (entry.isDirectory()) {
589
+ matchedCrateDirs.add(join2(baseDir, entry.name));
590
+ }
591
+ }
592
+ }
593
+ } else {
594
+ const exactDir = join2(absRootDir, pattern);
595
+ if (await dirExists(exactDir)) {
596
+ matchedCrateDirs.add(exactDir);
597
+ }
598
+ }
599
+ }
600
+ for (const dir of matchedCrateDirs) {
601
+ const manifestPath = join2(dir, "Cargo.toml");
602
+ if (manifestPath === rootCargoTomlPath)
603
+ continue;
604
+ if (await fileExists2(manifestPath)) {
605
+ try {
606
+ const crateRaw = await readFile2(manifestPath, "utf8");
607
+ const crateParsed = parseToml2(crateRaw);
608
+ if (crateParsed.package && crateParsed.package.name) {
609
+ const name = crateParsed.package.name;
610
+ let version2 = crateParsed.package.version;
611
+ if (typeof version2 !== "string") {
612
+ version2 = workspaceDefaultVersion;
613
+ }
614
+ const isPrivate = crateParsed.package.publish === false;
615
+ const manifest = {
616
+ name,
617
+ version: version2,
618
+ private: isPrivate,
619
+ dependencies: extractCargoDeps(crateParsed.dependencies),
620
+ devDependencies: extractCargoDeps(crateParsed["dev-dependencies"])
621
+ };
622
+ allPackages.push({
623
+ name,
624
+ version: version2,
625
+ directory: dir,
626
+ manifestPath,
627
+ manifest,
628
+ isPrivate,
629
+ relativeDir: relative(absRootDir, dir),
630
+ ecosystem: "cargo"
631
+ });
632
+ }
633
+ } catch {
634
+ }
635
+ }
269
636
  }
270
- } catch {
271
637
  }
638
+ if (!ecosystems.includes("cargo")) {
639
+ ecosystems.push("cargo");
640
+ }
641
+ if (detectedPackageManager === "unknown") {
642
+ detectedPackageManager = "cargo";
643
+ }
644
+ } catch {
272
645
  }
273
646
  }
274
- const graph = new PackageGraphImpl(packages);
647
+ const graph = new PackageGraphImpl(allPackages);
275
648
  return {
276
649
  rootDirectory: absRootDir,
277
- packageManager,
278
- packages,
279
- graph
650
+ packageManager: detectedPackageManager,
651
+ packages: allPackages,
652
+ ...skipped.length > 0 ? { skipped } : {},
653
+ graph,
654
+ ecosystems
280
655
  };
281
656
  }
282
657
  };
658
+ function splitPatterns(patterns) {
659
+ const included = [];
660
+ const excluded = [];
661
+ for (const raw of patterns) {
662
+ const pattern = raw.trim();
663
+ if (!pattern)
664
+ continue;
665
+ if (pattern.startsWith("!"))
666
+ excluded.push(pattern.slice(1));
667
+ else
668
+ included.push(pattern);
669
+ }
670
+ return { included, excluded };
671
+ }
672
+ var NEVER_A_PACKAGE = /* @__PURE__ */ new Set(["node_modules", ".git"]);
673
+ async function expandPattern(root, pattern) {
674
+ const segments = pattern.split("/").filter((segment) => segment !== "" && segment !== ".");
675
+ let current = [root];
676
+ for (let index = 0; index < segments.length; index += 1) {
677
+ const segment = segments[index];
678
+ const next = /* @__PURE__ */ new Set();
679
+ if (segment === "**") {
680
+ const remaining = segments.slice(index + 1);
681
+ for (const dir of current) {
682
+ for (const descendant of await descendants(dir))
683
+ next.add(descendant);
684
+ }
685
+ current = [...next];
686
+ if (remaining.length === 0)
687
+ return current;
688
+ const results = /* @__PURE__ */ new Set();
689
+ for (const dir of current) {
690
+ for (const match of await expandPattern(dir, remaining.join("/")))
691
+ results.add(match);
692
+ }
693
+ return [...results];
694
+ }
695
+ for (const dir of current) {
696
+ if (segment.includes("*")) {
697
+ const matches = segmentMatcher(segment);
698
+ let entries;
699
+ try {
700
+ entries = await readdir(dir, { withFileTypes: true });
701
+ } catch {
702
+ continue;
703
+ }
704
+ for (const entry of entries) {
705
+ if (!entry.isDirectory())
706
+ continue;
707
+ if (NEVER_A_PACKAGE.has(entry.name))
708
+ continue;
709
+ if (matches(entry.name))
710
+ next.add(join2(dir, entry.name));
711
+ }
712
+ } else {
713
+ const candidate = join2(dir, segment);
714
+ if (await dirExists(candidate))
715
+ next.add(candidate);
716
+ }
717
+ }
718
+ current = [...next];
719
+ if (current.length === 0)
720
+ return [];
721
+ }
722
+ return current;
723
+ }
724
+ async function descendants(root, maxDepth = 8) {
725
+ const found = [root];
726
+ const queue = [{ dir: root, depth: 0 }];
727
+ while (queue.length > 0) {
728
+ const { dir, depth } = queue.shift();
729
+ if (depth >= maxDepth)
730
+ continue;
731
+ let entries;
732
+ try {
733
+ entries = await readdir(dir, { withFileTypes: true });
734
+ } catch {
735
+ continue;
736
+ }
737
+ for (const entry of entries) {
738
+ if (!entry.isDirectory() || NEVER_A_PACKAGE.has(entry.name))
739
+ continue;
740
+ const child = join2(dir, entry.name);
741
+ found.push(child);
742
+ queue.push({ dir: child, depth: depth + 1 });
743
+ }
744
+ }
745
+ return found;
746
+ }
747
+ function segmentMatcher(segment) {
748
+ const source = segment.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&")).join("[^/]*");
749
+ const expression = new RegExp(`^${source}$`, "u");
750
+ return (name) => expression.test(name);
751
+ }
283
752
 
284
753
  // ../workspace/dist/propagator.js
285
754
  import semver from "semver";
@@ -380,9 +849,11 @@ var DefaultSemVerDecisionEngine = class {
380
849
  const reasons = [];
381
850
  let confidenceSum = 0;
382
851
  let confidenceCount = 0;
383
- const majorEvidence = evidence.filter((e) => e.severity === "major");
384
- const minorEvidence = evidence.filter((e) => e.severity === "minor");
385
- const patchEvidence = evidence.filter((e) => e.severity === "patch");
852
+ const blockingEvidence = evidence.filter((e) => e.blocking === true);
853
+ const gradingEvidence = evidence.filter((e) => e.blocking !== true);
854
+ const majorEvidence = gradingEvidence.filter((e) => e.severity === "major");
855
+ const minorEvidence = gradingEvidence.filter((e) => e.severity === "minor");
856
+ const patchEvidence = gradingEvidence.filter((e) => e.severity === "patch");
386
857
  if (majorEvidence.length > 0) {
387
858
  if (isZeroMajor && policy.zeroMajor?.breaking === "minor") {
388
859
  bump = "minor";
@@ -416,10 +887,16 @@ var DefaultSemVerDecisionEngine = class {
416
887
  bump = "none";
417
888
  reasons.push("No compatibility changes detected requiring a version bump.");
418
889
  }
890
+ if (blockingEvidence.length > 0) {
891
+ reasons.unshift(`Release blocked by ${blockingEvidence.length} finding(s); the version below is not proposed until they clear:`);
892
+ for (const e of blockingEvidence) {
893
+ reasons.splice(1, 0, ` - [${e.source}] ${e.title}`);
894
+ }
895
+ }
419
896
  const confidence = confidenceCount > 0 ? confidenceSum / confidenceCount : 1;
420
- return this.createDecision(packageName, currentVersion, bump, evidence, reasons, confidence, policy, channel);
897
+ return this.createDecision(packageName, currentVersion, bump, evidence, reasons, confidence, policy, channel, blockingEvidence.length > 0);
421
898
  }
422
- createDecision(packageName, currentVersion, bump, evidence, reasons, confidence, policy, channel) {
899
+ createDecision(packageName, currentVersion, bump, evidence, reasons, confidence, policy, channel, blocked = false) {
423
900
  let proposedVersion = currentVersion;
424
901
  if (bump !== "none") {
425
902
  if (channel && channel !== "stable") {
@@ -427,6 +904,9 @@ var DefaultSemVerDecisionEngine = class {
427
904
  const currentPrerelease = parsed.prerelease;
428
905
  if (currentPrerelease.length > 0 && currentPrerelease[0] === channel) {
429
906
  proposedVersion = semver2.inc(currentVersion, "prerelease", channel) || currentVersion;
907
+ } else if (currentPrerelease.length > 0) {
908
+ const promoted = `${parsed.major}.${parsed.minor}.${parsed.patch}-${channel}.0`;
909
+ proposedVersion = semver2.gt(promoted, currentVersion) ? promoted : semver2.inc(currentVersion, `pre${bump}`, channel) || currentVersion;
430
910
  } else {
431
911
  const baseBump = `pre${bump}`;
432
912
  proposedVersion = semver2.inc(currentVersion, baseBump, channel) || currentVersion;
@@ -444,14 +924,15 @@ var DefaultSemVerDecisionEngine = class {
444
924
  confidence,
445
925
  reasons,
446
926
  evidence,
447
- requiresApproval
927
+ requiresApproval,
928
+ ...blocked ? { blocked: true } : {}
448
929
  };
449
930
  }
450
931
  };
451
932
 
452
933
  // ../contracts/dist/store.js
453
- import { readFile as readFile2, writeFile, mkdir, readdir as readdir2 } from "fs/promises";
454
- import { join as join2 } from "path";
934
+ import { readFile as readFile3, writeFile as writeFile2, mkdir, readdir as readdir2 } from "fs/promises";
935
+ import { join as join3 } from "path";
455
936
  import semver3 from "semver";
456
937
  var FileContractStore = class {
457
938
  directory;
@@ -471,9 +952,9 @@ var FileContractStore = class {
471
952
  const files = await readdir2(this.directory, { recursive: true });
472
953
  for (const file of files) {
473
954
  if (typeof file === "string" && file.endsWith(".json")) {
474
- const fullPath = join2(this.directory, file);
955
+ const fullPath = join3(this.directory, file);
475
956
  try {
476
- const content = await readFile2(fullPath, "utf8");
957
+ const content = await readFile3(fullPath, "utf8");
477
958
  const parsed = JSON.parse(content);
478
959
  if (!packageName || parsed.package === packageName) {
479
960
  contracts.push(parsed);
@@ -494,8 +975,8 @@ var FileContractStore = class {
494
975
  await this.ensureDir();
495
976
  const sanitizedPackage = contract.package.replace(/[^a-zA-Z0-9_-]/g, "_");
496
977
  const sanitizedId = contract.id.replace(/[^a-zA-Z0-9_-]/g, "_");
497
- const filePath = join2(this.directory, `${sanitizedPackage}__${sanitizedId}.json`);
498
- await writeFile(filePath, JSON.stringify(contract, null, 2), "utf8");
978
+ const filePath = join3(this.directory, `${sanitizedPackage}__${sanitizedId}.json`);
979
+ await writeFile2(filePath, JSON.stringify(contract, null, 2), "utf8");
499
980
  }
500
981
  async getActiveContracts(packageName, targetVersion) {
501
982
  const all = await this.listContracts(packageName);
@@ -519,20 +1000,20 @@ var FileContractStore = class {
519
1000
  import { execFile } from "child_process";
520
1001
  import { promisify } from "util";
521
1002
  import { mkdtemp, rm, mkdir as mkdir2 } from "fs/promises";
522
- import { join as join3 } from "path";
1003
+ import { join as join4 } from "path";
523
1004
  import { tmpdir } from "os";
524
1005
  import * as tar from "tar";
525
1006
  var execFileAsync = promisify(execFile);
526
1007
  var CandidatePacker = class {
527
- async pack(packageDir, packageName, version) {
528
- const tempDir = await mkdtemp(join3(tmpdir(), "verelease-candidate-"));
1008
+ async pack(packageDir, packageName, version2) {
1009
+ const tempDir = await mkdtemp(join4(tmpdir(), "verelease-candidate-"));
529
1010
  const tarballDest = tempDir;
530
1011
  const { stdout } = await execFileAsync("npm", ["pack", "--pack-destination", tarballDest], {
531
1012
  cwd: packageDir
532
1013
  });
533
1014
  const tarballFilename = stdout.trim().split("\n").pop().trim();
534
- const tarballPath = join3(tarballDest, tarballFilename);
535
- const unpackedDir = join3(tempDir, "unpacked");
1015
+ const tarballPath = join4(tarballDest, tarballFilename);
1016
+ const unpackedDir = join4(tempDir, "unpacked");
536
1017
  await mkdir2(unpackedDir, { recursive: true });
537
1018
  await tar.x({
538
1019
  file: tarballPath,
@@ -542,14 +1023,15 @@ var CandidatePacker = class {
542
1023
  });
543
1024
  return {
544
1025
  packageName,
545
- version,
1026
+ version: version2,
546
1027
  tarballPath,
547
- unpackedDir
1028
+ unpackedDir,
1029
+ sourceDir: packageDir
548
1030
  };
549
1031
  }
550
1032
  async cleanup(packResult) {
551
1033
  try {
552
- const parentDir = join3(packResult.unpackedDir, "..");
1034
+ const parentDir = join4(packResult.unpackedDir, "..");
553
1035
  await rm(parentDir, { recursive: true, force: true });
554
1036
  } catch {
555
1037
  }
@@ -557,15 +1039,15 @@ var CandidatePacker = class {
557
1039
  };
558
1040
 
559
1041
  // ../contracts/dist/runner.js
560
- import { mkdir as mkdir3, symlink, rm as rm2, writeFile as writeFile2, copyFile } from "fs/promises";
561
- import { join as join5, basename } from "path";
1042
+ import { access, cp, mkdir as mkdir3, symlink, rm as rm2, readFile as readFile5, writeFile as writeFile3, copyFile } from "fs/promises";
1043
+ import { join as join6, dirname, basename } from "path";
562
1044
  import { tmpdir as tmpdir2 } from "os";
563
1045
  import { mkdtemp as mkdtemp2 } from "fs/promises";
564
1046
 
565
1047
  // ../test-adapter-node/dist/node-test.js
566
1048
  import { spawn } from "child_process";
567
- import { readFile as readFile3 } from "fs/promises";
568
- import { join as join4 } from "path";
1049
+ import { readFile as readFile4 } from "fs/promises";
1050
+ import { join as join5 } from "path";
569
1051
  var NodeTestAdapter = class {
570
1052
  options;
571
1053
  name = "node:test";
@@ -579,6 +1061,11 @@ var NodeTestAdapter = class {
579
1061
  if (hasNodeTestScript) {
580
1062
  return { supported: true, confidence: 0.95 };
581
1063
  }
1064
+ const testScript = scripts.test ?? "";
1065
+ const otherRunner = /\b(vitest|jest|playwright|bun\s+test|deno\s+test|ava|mocha)\b/u;
1066
+ if (otherRunner.test(testScript)) {
1067
+ return { supported: false, confidence: 0 };
1068
+ }
582
1069
  if (hasTestFiles) {
583
1070
  return { supported: true, confidence: 0.7 };
584
1071
  }
@@ -592,15 +1079,13 @@ var NodeTestAdapter = class {
592
1079
  let testFiles = ctx.testFiles && ctx.testFiles.length > 0 ? ctx.testFiles : [];
593
1080
  if (loaderArgs.length === 0 || testFiles.length === 0) {
594
1081
  try {
595
- const pkgRaw = await readFile3(join4(ctx.packagePath, "package.json"), "utf8");
1082
+ const pkgRaw = await readFile4(join5(ctx.packagePath, "package.json"), "utf8");
596
1083
  const pkg = JSON.parse(pkgRaw);
597
- const testScript = pkg.scripts?.test || "";
598
- if (loaderArgs.length === 0 && (testScript.includes("--import tsx") || testScript.includes("tsx"))) {
599
- loaderArgs = ["--import", "tsx"];
600
- }
601
- if (testFiles.length === 0 && testScript.includes("src/**/*.test.ts")) {
602
- testFiles = ["src/**/*.test.ts"];
603
- }
1084
+ const parsed = parseNodeTestScript(pkg.scripts?.test ?? "");
1085
+ if (loaderArgs.length === 0)
1086
+ loaderArgs = parsed.loaderArgs;
1087
+ if (testFiles.length === 0)
1088
+ testFiles = parsed.testFiles;
604
1089
  } catch {
605
1090
  }
606
1091
  }
@@ -757,6 +1242,64 @@ var NodeTestAdapter = class {
757
1242
  };
758
1243
  }
759
1244
  };
1245
+ function parseNodeTestScript(script) {
1246
+ const empty = { loaderArgs: [], testFiles: [] };
1247
+ if (!script.includes("--test"))
1248
+ return empty;
1249
+ const segment = script.split(/&&|;/u).map((part) => part.trim()).find((part) => /(^|\s)node\s/u.test(part) && part.includes("--test"));
1250
+ if (!segment)
1251
+ return empty;
1252
+ const tokens = tokenize(segment);
1253
+ const start = tokens.findIndex((token) => token === "--test");
1254
+ if (start === -1)
1255
+ return empty;
1256
+ const loaderArgs = [];
1257
+ const testFiles = [];
1258
+ for (let index = 0; index < tokens.length; index += 1) {
1259
+ const token = tokens[index];
1260
+ if ((token === "--import" || token === "--loader") && tokens[index + 1]) {
1261
+ loaderArgs.push(token, tokens[index + 1]);
1262
+ index += 1;
1263
+ continue;
1264
+ }
1265
+ if (token.startsWith("--import=") || token.startsWith("--loader=")) {
1266
+ loaderArgs.push(token);
1267
+ continue;
1268
+ }
1269
+ if (index > start && !token.startsWith("-")) {
1270
+ testFiles.push(token);
1271
+ }
1272
+ }
1273
+ return { loaderArgs, testFiles };
1274
+ }
1275
+ function tokenize(command) {
1276
+ const tokens = [];
1277
+ let current = "";
1278
+ let quote = null;
1279
+ for (const character of command) {
1280
+ if (quote) {
1281
+ if (character === quote)
1282
+ quote = null;
1283
+ else
1284
+ current += character;
1285
+ continue;
1286
+ }
1287
+ if (character === '"' || character === "'") {
1288
+ quote = character;
1289
+ continue;
1290
+ }
1291
+ if (/\s/u.test(character)) {
1292
+ if (current)
1293
+ tokens.push(current);
1294
+ current = "";
1295
+ continue;
1296
+ }
1297
+ current += character;
1298
+ }
1299
+ if (current)
1300
+ tokens.push(current);
1301
+ return tokens;
1302
+ }
760
1303
 
761
1304
  // ../contracts/dist/runner.js
762
1305
  var ContractRunner = class {
@@ -764,24 +1307,74 @@ var ContractRunner = class {
764
1307
  constructor(testAdapter = new NodeTestAdapter()) {
765
1308
  this.testAdapter = testAdapter;
766
1309
  }
1310
+ /**
1311
+ * Links the candidate's declared dependencies into the consumer.
1312
+ *
1313
+ * Resolution walks up from the source directory, so it works with pnpm's
1314
+ * per-package symlinks and with hoisted layouts alike. A dependency that
1315
+ * cannot be found is left out: the resulting import error is a more useful
1316
+ * signal than a silent substitution.
1317
+ */
1318
+ async linkDependencies(candidate, nodeModulesDir) {
1319
+ if (!candidate.sourceDir)
1320
+ return;
1321
+ let manifest;
1322
+ try {
1323
+ manifest = JSON.parse(await readFile5(join6(candidate.unpackedDir, "package.json"), "utf8"));
1324
+ } catch {
1325
+ return;
1326
+ }
1327
+ const names = Object.keys({
1328
+ ...manifest.dependencies,
1329
+ ...manifest.optionalDependencies
1330
+ });
1331
+ for (const name of names) {
1332
+ const resolved = await this.resolveFrom(name, candidate.sourceDir);
1333
+ if (!resolved)
1334
+ continue;
1335
+ const linkPath = join6(nodeModulesDir, ...name.split("/"));
1336
+ await mkdir3(dirname(linkPath), { recursive: true });
1337
+ try {
1338
+ await symlink(resolved, linkPath, "junction");
1339
+ } catch {
1340
+ }
1341
+ }
1342
+ }
1343
+ /** Finds an installed package by walking `node_modules` up from a directory. */
1344
+ async resolveFrom(name, fromDir) {
1345
+ let dir = fromDir;
1346
+ for (; ; ) {
1347
+ const candidatePath = join6(dir, "node_modules", ...name.split("/"));
1348
+ try {
1349
+ await access(candidatePath);
1350
+ return candidatePath;
1351
+ } catch {
1352
+ }
1353
+ const parent = dirname(dir);
1354
+ if (parent === dir)
1355
+ return null;
1356
+ dir = parent;
1357
+ }
1358
+ }
767
1359
  async runContract(contract, candidate) {
768
1360
  const startTime = Date.now();
769
- const consumerEnvDir = await mkdtemp2(join5(tmpdir2(), "verelease-consumer-"));
770
- const nodeModulesDir = join5(consumerEnvDir, "node_modules");
1361
+ const consumerEnvDir = await mkdtemp2(join6(tmpdir2(), "verelease-consumer-"));
1362
+ const nodeModulesDir = join6(consumerEnvDir, "node_modules");
771
1363
  try {
772
- await writeFile2(join5(consumerEnvDir, "package.json"), JSON.stringify({ name: "consumer-fixture", type: "module" }, null, 2), "utf8");
773
- let targetPkgLink = join5(nodeModulesDir, candidate.packageName);
1364
+ await writeFile3(join6(consumerEnvDir, "package.json"), JSON.stringify({ name: "consumer-fixture", type: "module" }, null, 2), "utf8");
1365
+ let targetPkgLink = join6(nodeModulesDir, candidate.packageName);
774
1366
  if (candidate.packageName.startsWith("@")) {
775
1367
  const [scope, pkgName] = candidate.packageName.split("/");
776
- const scopeDir = join5(nodeModulesDir, scope);
1368
+ const scopeDir = join6(nodeModulesDir, scope);
777
1369
  await mkdir3(scopeDir, { recursive: true });
778
- targetPkgLink = join5(scopeDir, pkgName);
1370
+ targetPkgLink = join6(scopeDir, pkgName);
779
1371
  } else {
780
1372
  await mkdir3(nodeModulesDir, { recursive: true });
781
1373
  }
782
- await symlink(candidate.unpackedDir, targetPkgLink, "junction");
1374
+ await cp(candidate.unpackedDir, targetPkgLink, { recursive: true });
1375
+ await this.linkDependencies(candidate, nodeModulesDir);
783
1376
  const isolatedTestName = basename(contract.testFile);
784
- const isolatedTestPath = join5(consumerEnvDir, isolatedTestName);
1377
+ const isolatedTestPath = join6(consumerEnvDir, isolatedTestName);
785
1378
  await copyFile(contract.testFile, isolatedTestPath);
786
1379
  const testRun = await this.testAdapter.run({
787
1380
  packagePath: consumerEnvDir,
@@ -862,24 +1455,122 @@ var ContractCompatibilityAnalyzer = class {
862
1455
  };
863
1456
 
864
1457
  // ../api-diff/dist/extractor.js
1458
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
1459
+ import { dirname as dirname2, join as join7, resolve as resolvePath } from "path";
865
1460
  import ts from "typescript";
866
1461
  var ApiExtractor = class {
867
- extractFromSource(sourceCode, fileName = "index.ts") {
868
- const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1462
+ /**
1463
+ * The full public surface reachable from an entry point, following
1464
+ * `export ... from` through the package.
1465
+ *
1466
+ * Every package here is published behind a barrel — `src/index.ts` is often
1467
+ * nothing but `export * from "./engine.js"` — so parsing the entry point
1468
+ * alone reports no API at all, and a diff of nothing against nothing finds
1469
+ * no change however good the baseline is.
1470
+ *
1471
+ * Only relative specifiers are followed. A bare specifier is another
1472
+ * package's surface, and changes there are that package's business, not a
1473
+ * change to this one's API.
1474
+ */
1475
+ async extractFromEntrypoint(entryPath, options = {}) {
1476
+ const maxDepth = options.maxDepth ?? 16;
869
1477
  const exports = {};
870
- const visit = (node) => {
871
- const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
872
- const isExported = (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 || !!modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
873
- if (ts.isFunctionDeclaration(node) && node.name && isExported) {
874
- const fnName = node.name.text;
875
- const parameters = node.parameters.map((param) => {
876
- return {
877
- name: param.name.getText(sourceFile),
878
- isOptional: !!param.questionToken || !!param.initializer,
879
- typeString: param.type ? param.type.getText(sourceFile) : "any"
880
- };
881
- });
882
- exports[fnName] = {
1478
+ const visited = /* @__PURE__ */ new Set();
1479
+ const walk = async (filePath, depth) => {
1480
+ const key = resolvePath(filePath);
1481
+ if (visited.has(key) || depth > maxDepth)
1482
+ return;
1483
+ visited.add(key);
1484
+ let source;
1485
+ try {
1486
+ source = await readFile6(key, "utf8");
1487
+ } catch {
1488
+ return;
1489
+ }
1490
+ const snapshot = this.extractFromSource(source, key);
1491
+ for (const [name, symbol] of Object.entries(snapshot.exports)) {
1492
+ exports[name] ??= symbol;
1493
+ }
1494
+ for (const reExport of snapshot.reExports ?? []) {
1495
+ if (!reExport.specifier.startsWith("."))
1496
+ continue;
1497
+ const target = await this.resolveRelative(reExport.specifier, dirname2(key));
1498
+ if (!target)
1499
+ continue;
1500
+ if (reExport.namespaceAlias) {
1501
+ exports[reExport.namespaceAlias] ??= {
1502
+ kind: "variable",
1503
+ name: reExport.namespaceAlias,
1504
+ typeString: `typeof import(${JSON.stringify(reExport.specifier)})`
1505
+ };
1506
+ continue;
1507
+ }
1508
+ const before = new Set(Object.keys(exports));
1509
+ await walk(target, depth + 1);
1510
+ if (reExport.names) {
1511
+ const wanted = new Map(reExport.names.map((n) => [n.local, n.exported]));
1512
+ for (const name of Object.keys(exports)) {
1513
+ if (before.has(name))
1514
+ continue;
1515
+ const exportedAs = wanted.get(name);
1516
+ if (exportedAs === void 0) {
1517
+ delete exports[name];
1518
+ } else if (exportedAs !== name) {
1519
+ exports[exportedAs] = { ...exports[name], name: exportedAs };
1520
+ delete exports[name];
1521
+ }
1522
+ }
1523
+ }
1524
+ }
1525
+ };
1526
+ await walk(entryPath, 0);
1527
+ return { exports };
1528
+ }
1529
+ /**
1530
+ * Maps an ESM specifier to a file on disk.
1531
+ *
1532
+ * TypeScript sources import with a `.js` extension that only exists after a
1533
+ * build, so the declared path is rarely the path to read.
1534
+ */
1535
+ async resolveRelative(specifier, fromDir) {
1536
+ const base = join7(fromDir, specifier);
1537
+ const withoutJs = base.replace(/\.js$/u, "");
1538
+ const candidates = [
1539
+ base.endsWith(".ts") ? base : null,
1540
+ `${withoutJs}.ts`,
1541
+ `${withoutJs}.d.ts`,
1542
+ `${withoutJs}.tsx`,
1543
+ base,
1544
+ join7(base, "index.ts"),
1545
+ join7(base, "index.d.ts"),
1546
+ join7(withoutJs, "index.ts")
1547
+ ].filter((c) => c !== null);
1548
+ for (const candidate of candidates) {
1549
+ try {
1550
+ if ((await stat3(candidate)).isFile())
1551
+ return candidate;
1552
+ } catch {
1553
+ }
1554
+ }
1555
+ return null;
1556
+ }
1557
+ extractFromSource(sourceCode, fileName = "index.ts") {
1558
+ const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1559
+ const exports = {};
1560
+ const reExports = [];
1561
+ const visit = (node) => {
1562
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
1563
+ const isExported = (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 || !!modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
1564
+ if (ts.isFunctionDeclaration(node) && node.name && isExported) {
1565
+ const fnName = node.name.text;
1566
+ const parameters = node.parameters.map((param) => {
1567
+ return {
1568
+ name: param.name.getText(sourceFile),
1569
+ isOptional: !!param.questionToken || !!param.initializer,
1570
+ typeString: param.type ? param.type.getText(sourceFile) : "any"
1571
+ };
1572
+ });
1573
+ exports[fnName] = {
883
1574
  kind: "function",
884
1575
  name: fnName,
885
1576
  parameters,
@@ -949,11 +1640,45 @@ var ApiExtractor = class {
949
1640
  name: ifaceName,
950
1641
  properties
951
1642
  };
1643
+ } else if (ts.isTypeAliasDeclaration(node) && node.name && isExported) {
1644
+ const aliasName = node.name.text;
1645
+ exports[aliasName] = {
1646
+ kind: "type",
1647
+ name: aliasName,
1648
+ typeString: node.type.getText(sourceFile)
1649
+ };
1650
+ } else if (ts.isEnumDeclaration(node) && node.name && isExported) {
1651
+ const enumName = node.name.text;
1652
+ const members = {};
1653
+ for (const member of node.members) {
1654
+ const memberName = member.name.getText(sourceFile);
1655
+ members[memberName] = {
1656
+ name: memberName,
1657
+ ...member.initializer ? { value: member.initializer.getText(sourceFile) } : {}
1658
+ };
1659
+ }
1660
+ exports[enumName] = { kind: "enum", name: enumName, members };
1661
+ }
1662
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
1663
+ const specifier = node.moduleSpecifier.text;
1664
+ if (!node.exportClause) {
1665
+ reExports.push({ specifier });
1666
+ } else if (ts.isNamespaceExport(node.exportClause)) {
1667
+ reExports.push({ specifier, namespaceAlias: node.exportClause.name.text });
1668
+ } else if (ts.isNamedExports(node.exportClause)) {
1669
+ reExports.push({
1670
+ specifier,
1671
+ names: node.exportClause.elements.map((el) => ({
1672
+ local: (el.propertyName ?? el.name).text,
1673
+ exported: el.name.text
1674
+ }))
1675
+ });
1676
+ }
952
1677
  }
953
1678
  ts.forEachChild(node, visit);
954
1679
  };
955
1680
  ts.forEachChild(sourceFile, visit);
956
- return { exports };
1681
+ return reExports.length > 0 ? { exports, reExports } : { exports };
957
1682
  }
958
1683
  };
959
1684
 
@@ -993,6 +1718,12 @@ var ApiDiffEngine = class {
993
1718
  this.diffFunction(packageName, name, baseSym, candSym, evidence);
994
1719
  } else if (baseSym.kind === "interface" && candSym.kind === "interface") {
995
1720
  this.diffInterface(packageName, name, baseSym, candSym, evidence);
1721
+ } else if (baseSym.kind === "class" && candSym.kind === "class") {
1722
+ this.diffClass(packageName, name, baseSym, candSym, evidence);
1723
+ } else if (baseSym.kind === "type" && candSym.kind === "type") {
1724
+ this.diffTypeAlias(packageName, name, baseSym, candSym, evidence);
1725
+ } else if (baseSym.kind === "enum" && candSym.kind === "enum") {
1726
+ this.diffEnum(packageName, name, baseSym, candSym, evidence);
996
1727
  }
997
1728
  }
998
1729
  for (const [name, candSym] of Object.entries(candidateExports)) {
@@ -1075,6 +1806,112 @@ var ApiDiffEngine = class {
1075
1806
  }
1076
1807
  }
1077
1808
  }
1809
+ /**
1810
+ * Compares the members of an exported class.
1811
+ *
1812
+ * The extractor has always captured methods and properties, but only
1813
+ * interfaces were compared, so a class could gain or lose public methods
1814
+ * without any evidence being produced — the export was still there, so the
1815
+ * package looked unchanged.
1816
+ */
1817
+ diffClass(packageName, className, baseClass, candClass, evidence) {
1818
+ for (const memberKind of ["methods", "properties"]) {
1819
+ const before = baseClass[memberKind];
1820
+ const after = candClass[memberKind];
1821
+ const singular = memberKind === "methods" ? "method" : "property";
1822
+ for (const memberName of Object.keys(before)) {
1823
+ if (!after[memberName]) {
1824
+ evidence.push({
1825
+ id: `api.class.${singular}Removed:${className}:${memberName}`,
1826
+ package: packageName,
1827
+ source: "api",
1828
+ severity: "major",
1829
+ confidence: 0.95,
1830
+ title: `Public ${singular} removed from class: ${className}.${memberName}`,
1831
+ description: `Class "${className}" no longer exposes ${singular} "${memberName}". Callers of it break.`
1832
+ });
1833
+ }
1834
+ }
1835
+ for (const memberName of Object.keys(after)) {
1836
+ if (!before[memberName]) {
1837
+ evidence.push({
1838
+ id: `api.class.${singular}Added:${className}:${memberName}`,
1839
+ package: packageName,
1840
+ source: "api",
1841
+ severity: "minor",
1842
+ confidence: 0.9,
1843
+ title: `Public ${singular} added to class: ${className}.${memberName}`,
1844
+ description: `Class "${className}" gained ${singular} "${memberName}".`
1845
+ });
1846
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ /**
1851
+ * Compares an exported type alias.
1852
+ *
1853
+ * Reported with reduced confidence, because a definition change can go
1854
+ * either way and telling them apart needs the type checker rather than the
1855
+ * syntax: widening a union is safe for code that reads the type and breaking
1856
+ * for code that implements it, and narrowing breaks both. Calling it
1857
+ * breaking and saying why is more useful than staying silent, and the
1858
+ * confidence lets a policy hold it for review.
1859
+ */
1860
+ diffTypeAlias(packageName, typeName, baseAlias, candAlias, evidence) {
1861
+ if (baseAlias.typeString === candAlias.typeString)
1862
+ return;
1863
+ evidence.push({
1864
+ id: `api.type.changed:${typeName}`,
1865
+ package: packageName,
1866
+ source: "types",
1867
+ severity: "major",
1868
+ confidence: 0.6,
1869
+ title: `Exported type changed: ${typeName}`,
1870
+ description: `Type "${typeName}" changed from "${baseAlias.typeString}" to "${candAlias.typeString}". A narrowing breaks consumers; a widening may only break those implementing it.`
1871
+ });
1872
+ }
1873
+ /** Compares the members of an exported enum. */
1874
+ diffEnum(packageName, enumName, baseEnum, candEnum, evidence) {
1875
+ for (const [memberName, member] of Object.entries(baseEnum.members)) {
1876
+ if (!candEnum.members[memberName]) {
1877
+ evidence.push({
1878
+ id: `api.enum.memberRemoved:${enumName}:${memberName}`,
1879
+ package: packageName,
1880
+ source: "types",
1881
+ severity: "major",
1882
+ confidence: 0.95,
1883
+ title: `Enum member removed: ${enumName}.${memberName}`,
1884
+ description: `Enum "${enumName}" no longer declares "${memberName}".`
1885
+ });
1886
+ continue;
1887
+ }
1888
+ const candValue = candEnum.members[memberName].value;
1889
+ if (member.value !== void 0 && candValue !== void 0 && member.value !== candValue) {
1890
+ evidence.push({
1891
+ id: `api.enum.valueChanged:${enumName}:${memberName}`,
1892
+ package: packageName,
1893
+ source: "types",
1894
+ severity: "major",
1895
+ confidence: 0.9,
1896
+ title: `Enum value changed: ${enumName}.${memberName}`,
1897
+ description: `"${enumName}.${memberName}" changed from ${member.value} to ${candValue}.`
1898
+ });
1899
+ }
1900
+ }
1901
+ for (const memberName of Object.keys(candEnum.members)) {
1902
+ if (!baseEnum.members[memberName]) {
1903
+ evidence.push({
1904
+ id: `api.enum.memberAdded:${enumName}:${memberName}`,
1905
+ package: packageName,
1906
+ source: "types",
1907
+ severity: "minor",
1908
+ confidence: 0.9,
1909
+ title: `Enum member added: ${enumName}.${memberName}`,
1910
+ description: `Enum "${enumName}" gained "${memberName}".`
1911
+ });
1912
+ }
1913
+ }
1914
+ }
1078
1915
  diffInterface(packageName, ifaceName, baseIface, candIface, evidence) {
1079
1916
  for (const [propName, prop] of Object.entries(baseIface.properties)) {
1080
1917
  if (!candIface.properties[propName]) {
@@ -1118,11 +1955,11 @@ var ApiDiffEngine = class {
1118
1955
  };
1119
1956
 
1120
1957
  // ../api-diff/dist/analyzer.js
1121
- import { readFile as readFile4, stat as stat2 } from "fs/promises";
1122
- import { join as join6 } from "path";
1123
- async function fileExists2(path) {
1958
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
1959
+ import { join as join8 } from "path";
1960
+ async function fileExists3(path) {
1124
1961
  try {
1125
- const s = await stat2(path);
1962
+ const s = await stat4(path);
1126
1963
  return s.isFile();
1127
1964
  } catch {
1128
1965
  return false;
@@ -1141,15 +1978,14 @@ var ApiDiffAnalyzer = class {
1141
1978
  if (!candidateEntrypoint) {
1142
1979
  return [];
1143
1980
  }
1144
- const candidateSource = await readFile4(candidateEntrypoint, "utf8");
1145
- const candidateSnapshot = this.extractor.extractFromSource(candidateSource, candidateEntrypoint);
1981
+ const candidateSnapshot = await this.extractor.extractFromEntrypoint(candidateEntrypoint);
1146
1982
  let baselineSnapshot;
1147
- if (context.baselinePath && await fileExists2(context.baselinePath)) {
1148
- const baseSource = await readFile4(context.baselinePath, "utf8");
1983
+ if (context.baselinePath && await fileExists3(context.baselinePath)) {
1984
+ const baseSource = await readFile7(context.baselinePath, "utf8");
1149
1985
  if (context.baselinePath.endsWith(".json")) {
1150
1986
  baselineSnapshot = JSON.parse(baseSource);
1151
1987
  } else {
1152
- baselineSnapshot = this.extractor.extractFromSource(baseSource, context.baselinePath);
1988
+ baselineSnapshot = await this.extractor.extractFromEntrypoint(context.baselinePath);
1153
1989
  }
1154
1990
  }
1155
1991
  if (!baselineSnapshot) {
@@ -1159,14 +1995,14 @@ var ApiDiffAnalyzer = class {
1159
1995
  }
1160
1996
  async findEntrypoint(pkgDir) {
1161
1997
  const candidates = [
1162
- join6(pkgDir, "src", "index.ts"),
1163
- join6(pkgDir, "src", "index.js"),
1164
- join6(pkgDir, "index.ts"),
1165
- join6(pkgDir, "index.js"),
1166
- join6(pkgDir, "dist", "index.d.ts")
1998
+ join8(pkgDir, "src", "index.ts"),
1999
+ join8(pkgDir, "src", "index.js"),
2000
+ join8(pkgDir, "index.ts"),
2001
+ join8(pkgDir, "index.js"),
2002
+ join8(pkgDir, "dist", "index.d.ts")
1167
2003
  ];
1168
2004
  for (const c of candidates) {
1169
- if (await fileExists2(c)) {
2005
+ if (await fileExists3(c)) {
1170
2006
  return c;
1171
2007
  }
1172
2008
  }
@@ -1177,14 +2013,14 @@ var ApiDiffAnalyzer = class {
1177
2013
  // ../api-diff/dist/baseline.js
1178
2014
  import { execFile as execFile2 } from "child_process";
1179
2015
  import { promisify as promisify2 } from "util";
1180
- import { mkdtemp as mkdtemp3, mkdir as mkdir4, stat as stat3 } from "fs/promises";
1181
- import { join as join7 } from "path";
2016
+ import { mkdtemp as mkdtemp3, mkdir as mkdir4, stat as stat5 } from "fs/promises";
2017
+ import { join as join9 } from "path";
1182
2018
  import { tmpdir as tmpdir3 } from "os";
1183
2019
  import * as tar2 from "tar";
1184
2020
  var execFileAsync2 = promisify2(execFile2);
1185
- async function fileExists3(path) {
2021
+ async function fileExists4(path) {
1186
2022
  try {
1187
- const s = await stat3(path);
2023
+ const s = await stat5(path);
1188
2024
  return s.isFile();
1189
2025
  } catch {
1190
2026
  return false;
@@ -1199,20 +2035,20 @@ var BaselineFetcher = class {
1199
2035
  * or the extracted package has no recognizable entry point.
1200
2036
  */
1201
2037
  async fetch(packageName, parentTmpDir) {
1202
- const version = await this.getLatestVersion(packageName);
1203
- if (!version)
2038
+ const version2 = await this.getLatestVersion(packageName);
2039
+ if (!version2)
1204
2040
  return null;
1205
- const tempDir = parentTmpDir ? join7(parentTmpDir, packageName.replace(/\//g, "__")) : await mkdtemp3(join7(tmpdir3(), "verelease-baseline-"));
2041
+ const tempDir = parentTmpDir ? join9(parentTmpDir, packageName.replace(/\//g, "__")) : await mkdtemp3(join9(tmpdir3(), "verelease-baseline-"));
1206
2042
  await mkdir4(tempDir, { recursive: true });
1207
2043
  let tarballFilename;
1208
2044
  try {
1209
- const { stdout } = await execFileAsync2("npm", ["pack", `${packageName}@${version}`, "--pack-destination", tempDir], { timeout: 3e4 });
2045
+ const { stdout } = await execFileAsync2("npm", ["pack", `${packageName}@${version2}`, "--pack-destination", tempDir], { timeout: 3e4 });
1210
2046
  tarballFilename = stdout.trim().split("\n").pop().trim();
1211
2047
  } catch {
1212
2048
  return null;
1213
2049
  }
1214
- const tarballPath = join7(tempDir, tarballFilename);
1215
- const extractedDir = join7(tempDir, "extracted");
2050
+ const tarballPath = join9(tempDir, tarballFilename);
2051
+ const extractedDir = join9(tempDir, "extracted");
1216
2052
  await mkdir4(extractedDir, { recursive: true });
1217
2053
  try {
1218
2054
  await tar2.x({
@@ -1227,7 +2063,7 @@ var BaselineFetcher = class {
1227
2063
  const entryPointPath = await this.findEntrypoint(extractedDir);
1228
2064
  if (!entryPointPath)
1229
2065
  return null;
1230
- return { version, entryPointPath, extractedDir };
2066
+ return { version: version2, entryPointPath, extractedDir };
1231
2067
  }
1232
2068
  async getLatestVersion(packageName) {
1233
2069
  try {
@@ -1242,16 +2078,16 @@ var BaselineFetcher = class {
1242
2078
  }
1243
2079
  async findEntrypoint(pkgDir) {
1244
2080
  const candidates = [
1245
- join7(pkgDir, "dist", "index.d.ts"),
1246
- join7(pkgDir, "dist", "index.d.mts"),
1247
- join7(pkgDir, "src", "index.ts"),
1248
- join7(pkgDir, "src", "index.js"),
1249
- join7(pkgDir, "index.d.ts"),
1250
- join7(pkgDir, "index.ts"),
1251
- join7(pkgDir, "index.js")
2081
+ join9(pkgDir, "dist", "index.d.ts"),
2082
+ join9(pkgDir, "dist", "index.d.mts"),
2083
+ join9(pkgDir, "src", "index.ts"),
2084
+ join9(pkgDir, "src", "index.js"),
2085
+ join9(pkgDir, "index.d.ts"),
2086
+ join9(pkgDir, "index.ts"),
2087
+ join9(pkgDir, "index.js")
1252
2088
  ];
1253
2089
  for (const candidate of candidates) {
1254
- if (await fileExists3(candidate)) {
2090
+ if (await fileExists4(candidate)) {
1255
2091
  return candidate;
1256
2092
  }
1257
2093
  }
@@ -1259,141 +2095,381 @@ var BaselineFetcher = class {
1259
2095
  }
1260
2096
  };
1261
2097
 
1262
- // ../manifest/dist/mutator.js
1263
- import { readFile as readFile5, writeFile as writeFile3, stat as stat4 } from "fs/promises";
1264
- import { join as join8 } from "path";
1265
- async function fileExists4(path) {
2098
+ // ../api-diff/dist/cargo-semver.js
2099
+ import { readFile as readFile8, stat as stat6 } from "fs/promises";
2100
+ import { join as join10 } from "path";
2101
+ import { execFile as execFile3 } from "child_process";
2102
+ import { promisify as promisify3 } from "util";
2103
+ var execFileAsync3 = promisify3(execFile3);
2104
+ async function fileExists5(path) {
1266
2105
  try {
1267
- const s = await stat4(path);
2106
+ const s = await stat6(path);
1268
2107
  return s.isFile();
1269
2108
  } catch {
1270
2109
  return false;
1271
2110
  }
1272
2111
  }
1273
- var TransactionalManifestMutator = class {
1274
- async applyPlan(plan) {
1275
- const backups = [];
1276
- const modifiedFiles = [];
1277
- const filesToTouch = /* @__PURE__ */ new Set();
1278
- for (const pkgPlan of plan.packages) {
1279
- if (pkgPlan.bump !== "none") {
1280
- const pkgJson = join8(pkgPlan.directory, "package.json");
1281
- const jsrJson = join8(pkgPlan.directory, "jsr.json");
1282
- const denoJson = join8(pkgPlan.directory, "deno.json");
1283
- if (await fileExists4(pkgJson))
1284
- filesToTouch.add(pkgJson);
1285
- if (await fileExists4(jsrJson))
1286
- filesToTouch.add(jsrJson);
1287
- if (await fileExists4(denoJson))
1288
- filesToTouch.add(denoJson);
1289
- }
1290
- for (const update of pkgPlan.dependentUpdates) {
1291
- const depPkgPlan = plan.packages.find((p) => p.name === update.dependentName);
1292
- if (depPkgPlan) {
1293
- const depPkgJson = join8(depPkgPlan.directory, "package.json");
1294
- if (await fileExists4(depPkgJson))
1295
- filesToTouch.add(depPkgJson);
1296
- }
1297
- }
1298
- }
1299
- for (const filePath of filesToTouch) {
1300
- const originalContent = await readFile5(filePath, "utf8");
1301
- backups.push({ filePath, originalContent });
1302
- }
1303
- try {
1304
- const versionMap = /* @__PURE__ */ new Map();
1305
- for (const pkgPlan of plan.packages) {
1306
- if (pkgPlan.bump !== "none") {
1307
- versionMap.set(pkgPlan.name, pkgPlan.proposedVersion);
1308
- }
1309
- }
1310
- const depUpdateMap = /* @__PURE__ */ new Map();
1311
- for (const pkgPlan of plan.packages) {
1312
- for (const depUp of pkgPlan.dependentUpdates) {
1313
- if (!depUpdateMap.has(depUp.dependentName)) {
1314
- depUpdateMap.set(depUp.dependentName, []);
1315
- }
1316
- depUpdateMap.get(depUp.dependentName).push({
1317
- dependencyName: depUp.dependencyName,
1318
- newRange: depUp.newRange,
1319
- type: depUp.dependencyType
1320
- });
2112
+ function extractRustApiSnapshot(sourceCode, crateName, version2) {
2113
+ const items = {};
2114
+ const stripped = sourceCode.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/g, "");
2115
+ const fnRegex = /pub(?:\s+async)?(?:\s+const)?(?:\s+unsafe)?\s+fn\s+([a-zA-Z0-9_]+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*->\s*([^{;]+))?/g;
2116
+ let match;
2117
+ while ((match = fnRegex.exec(stripped)) !== null) {
2118
+ const name = match[1];
2119
+ const params = match[2]?.trim() ?? "";
2120
+ const returnType = match[3]?.trim() ?? "()";
2121
+ items[`fn:${name}`] = {
2122
+ kind: "fn",
2123
+ name,
2124
+ signature: `pub fn ${name}(${params}) -> ${returnType}`
2125
+ };
2126
+ }
2127
+ const structRegex = /pub\s+struct\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?/g;
2128
+ while ((match = structRegex.exec(stripped)) !== null) {
2129
+ const name = match[1];
2130
+ items[`struct:${name}`] = {
2131
+ kind: "struct",
2132
+ name,
2133
+ signature: `pub struct ${name}`
2134
+ };
2135
+ }
2136
+ const enumRegex = /pub\s+enum\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?/g;
2137
+ while ((match = enumRegex.exec(stripped)) !== null) {
2138
+ const name = match[1];
2139
+ items[`enum:${name}`] = {
2140
+ kind: "enum",
2141
+ name,
2142
+ signature: `pub enum ${name}`
2143
+ };
2144
+ }
2145
+ const traitRegex = /pub\s+trait\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?/g;
2146
+ while ((match = traitRegex.exec(stripped)) !== null) {
2147
+ const name = match[1];
2148
+ items[`trait:${name}`] = {
2149
+ kind: "trait",
2150
+ name,
2151
+ signature: `pub trait ${name}`
2152
+ };
2153
+ }
2154
+ const typeRegex = /pub\s+type\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?\s*=\s*([^;]+);/g;
2155
+ while ((match = typeRegex.exec(stripped)) !== null) {
2156
+ const name = match[1];
2157
+ const target = match[2]?.trim() ?? "";
2158
+ items[`type:${name}`] = {
2159
+ kind: "type",
2160
+ name,
2161
+ signature: `pub type ${name} = ${target}`
2162
+ };
2163
+ }
2164
+ const constRegex = /pub\s+const\s+([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_&<>\[\]]+)/g;
2165
+ while ((match = constRegex.exec(stripped)) !== null) {
2166
+ const name = match[1];
2167
+ const constType = match[2]?.trim() ?? "";
2168
+ items[`const:${name}`] = {
2169
+ kind: "const",
2170
+ name,
2171
+ signature: `pub const ${name}: ${constType}`
2172
+ };
2173
+ }
2174
+ return {
2175
+ crateName,
2176
+ version: version2,
2177
+ items
2178
+ };
2179
+ }
2180
+ function diffRustApi(crateName, baseline, candidate) {
2181
+ const evidence = [];
2182
+ for (const [key, baseItem] of Object.entries(baseline.items)) {
2183
+ const candItem = candidate.items[key];
2184
+ if (!candItem) {
2185
+ evidence.push({
2186
+ id: `rust.api.removed:${crateName}:${baseItem.kind}:${baseItem.name}`,
2187
+ package: crateName,
2188
+ source: "api",
2189
+ severity: "major",
2190
+ confidence: 0.95,
2191
+ title: `Removed public ${baseItem.kind}: ${baseItem.name}`,
2192
+ description: `Breaking API change: The public ${baseItem.kind} '${baseItem.name}' was removed from ${crateName}.`,
2193
+ metadata: {
2194
+ itemKind: baseItem.kind,
2195
+ itemName: baseItem.name,
2196
+ previousSignature: baseItem.signature
1321
2197
  }
1322
- }
1323
- for (const pkgPlan of plan.packages) {
1324
- const pkgJsonPath = join8(pkgPlan.directory, "package.json");
1325
- if (filesToTouch.has(pkgJsonPath)) {
1326
- const content = await readFile5(pkgJsonPath, "utf8");
1327
- const parsed = JSON.parse(content);
1328
- if (versionMap.has(pkgPlan.name)) {
1329
- parsed.version = versionMap.get(pkgPlan.name);
1330
- }
1331
- const depUpdates = depUpdateMap.get(pkgPlan.name);
1332
- if (depUpdates) {
1333
- for (const up of depUpdates) {
1334
- const sec = parsed[up.type];
1335
- if (sec && sec[up.dependencyName]) {
1336
- sec[up.dependencyName] = up.newRange;
1337
- }
1338
- }
1339
- }
1340
- await writeFile3(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
1341
- modifiedFiles.push(pkgJsonPath);
2198
+ });
2199
+ } else if (baseItem.signature !== candItem.signature) {
2200
+ evidence.push({
2201
+ id: `rust.api.changed:${crateName}:${baseItem.kind}:${baseItem.name}`,
2202
+ package: crateName,
2203
+ source: "api",
2204
+ severity: "major",
2205
+ confidence: 0.9,
2206
+ title: `Changed signature of ${baseItem.kind}: ${baseItem.name}`,
2207
+ description: `Breaking API change: Signature changed from '${baseItem.signature}' to '${candItem.signature}'.`,
2208
+ metadata: {
2209
+ itemKind: baseItem.kind,
2210
+ itemName: baseItem.name,
2211
+ previousSignature: baseItem.signature,
2212
+ newSignature: candItem.signature
1342
2213
  }
1343
- const jsrJsonPath = join8(pkgPlan.directory, "jsr.json");
1344
- if (filesToTouch.has(jsrJsonPath)) {
1345
- const content = await readFile5(jsrJsonPath, "utf8");
1346
- const parsed = JSON.parse(content);
1347
- if (versionMap.has(pkgPlan.name)) {
1348
- parsed.version = versionMap.get(pkgPlan.name);
1349
- }
1350
- await writeFile3(jsrJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
1351
- modifiedFiles.push(jsrJsonPath);
2214
+ });
2215
+ }
2216
+ }
2217
+ for (const [key, candItem] of Object.entries(candidate.items)) {
2218
+ if (!baseline.items[key]) {
2219
+ evidence.push({
2220
+ id: `rust.api.added:${crateName}:${candItem.kind}:${candItem.name}`,
2221
+ package: crateName,
2222
+ source: "api",
2223
+ severity: "minor",
2224
+ confidence: 0.9,
2225
+ title: `Added public ${candItem.kind}: ${candItem.name}`,
2226
+ description: `Feature: New public ${candItem.kind} '${candItem.name}' added to ${crateName}.`,
2227
+ metadata: {
2228
+ itemKind: candItem.kind,
2229
+ itemName: candItem.name,
2230
+ newSignature: candItem.signature
1352
2231
  }
1353
- const denoJsonPath = join8(pkgPlan.directory, "deno.json");
1354
- if (filesToTouch.has(denoJsonPath)) {
1355
- const content = await readFile5(denoJsonPath, "utf8");
1356
- const parsed = JSON.parse(content);
1357
- if (versionMap.has(pkgPlan.name)) {
1358
- parsed.version = versionMap.get(pkgPlan.name);
1359
- }
1360
- await writeFile3(denoJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
1361
- modifiedFiles.push(denoJsonPath);
2232
+ });
2233
+ }
2234
+ }
2235
+ return evidence;
2236
+ }
2237
+ function parseCargoSemVerOutput(rawOutput, crateName) {
2238
+ const evidence = [];
2239
+ const lines = rawOutput.split("\n");
2240
+ for (const line of lines) {
2241
+ const trimmed = line.trim();
2242
+ if (!trimmed || !trimmed.startsWith("{"))
2243
+ continue;
2244
+ try {
2245
+ const parsed = JSON.parse(trimmed);
2246
+ const ruleName = parsed.name || parsed.rule_name || "semver_check";
2247
+ const isBreaking = parsed.category === "breaking" || parsed.level === "error" || parsed.severity === "error" || parsed.category === "Major";
2248
+ const severity = isBreaking ? "major" : "minor";
2249
+ const description = parsed.description || parsed.message || `Diagnostic from cargo-semver-checks (${ruleName})`;
2250
+ evidence.push({
2251
+ id: `cargo.semver.diagnostic:${crateName}:${ruleName}`,
2252
+ package: crateName,
2253
+ source: "api",
2254
+ severity,
2255
+ confidence: 0.98,
2256
+ title: isBreaking ? `Cargo semver-checks breaking change: ${ruleName}` : `Cargo semver-checks non-breaking addition: ${ruleName}`,
2257
+ description,
2258
+ metadata: {
2259
+ ruleName,
2260
+ span: parsed.span,
2261
+ rawDiagnostic: parsed
1362
2262
  }
2263
+ });
2264
+ } catch {
2265
+ }
2266
+ }
2267
+ return evidence;
2268
+ }
2269
+ var CargoSemVerAnalyzer = class {
2270
+ name = "cargo-semver-analyzer";
2271
+ async analyze(context) {
2272
+ const { packagePath, packageName } = context;
2273
+ const cargoTomlPath = join10(packagePath, "Cargo.toml");
2274
+ if (!await fileExists5(cargoTomlPath)) {
2275
+ return [];
2276
+ }
2277
+ try {
2278
+ const { stdout } = await execFileAsync3("cargo", [
2279
+ "semver-checks",
2280
+ "check-release",
2281
+ "--manifest-path",
2282
+ cargoTomlPath,
2283
+ "--json",
2284
+ ...context.baselinePath ? ["--baseline-root", context.baselinePath] : []
2285
+ ], {
2286
+ cwd: packagePath,
2287
+ timeout: 6e4
2288
+ });
2289
+ const parsedEvidence = parseCargoSemVerOutput(stdout, packageName);
2290
+ if (parsedEvidence.length > 0) {
2291
+ return parsedEvidence;
1363
2292
  }
1364
2293
  } catch (err) {
1365
- for (const b of backups) {
1366
- await writeFile3(b.filePath, b.originalContent, "utf8");
1367
- }
1368
- throw err;
1369
- }
1370
- const transaction = {
1371
- id: `tx-${Date.now()}`,
1372
- plan,
1373
- backups,
1374
- modifiedFiles,
1375
- appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
1376
- async rollback() {
1377
- for (const b of backups) {
1378
- await writeFile3(b.filePath, b.originalContent, "utf8");
2294
+ const execError = err;
2295
+ if (execError.stdout) {
2296
+ const diagnostics = parseCargoSemVerOutput(execError.stdout, packageName);
2297
+ if (diagnostics.length > 0) {
2298
+ return diagnostics;
1379
2299
  }
1380
- },
1381
- async commit() {
2300
+ }
2301
+ }
2302
+ const candidateEntrypoint = await this.findRustEntrypoint(packagePath);
2303
+ if (!candidateEntrypoint) {
2304
+ return [];
2305
+ }
2306
+ const candidateSource = await readFile8(candidateEntrypoint, "utf8");
2307
+ const candidateSnapshot = extractRustApiSnapshot(candidateSource, packageName, context.currentVersion);
2308
+ let baselineSnapshot;
2309
+ if (context.baselinePath && await fileExists5(context.baselinePath)) {
2310
+ const baseSource = await readFile8(context.baselinePath, "utf8");
2311
+ if (context.baselinePath.endsWith(".json")) {
2312
+ baselineSnapshot = JSON.parse(baseSource);
2313
+ } else {
2314
+ baselineSnapshot = extractRustApiSnapshot(baseSource, packageName, context.previousVersion);
2315
+ }
2316
+ } else if (context.baselinePath) {
2317
+ const baseEntrypoint = await this.findRustEntrypoint(context.baselinePath);
2318
+ if (baseEntrypoint && await fileExists5(baseEntrypoint)) {
2319
+ const baseSource = await readFile8(baseEntrypoint, "utf8");
2320
+ baselineSnapshot = extractRustApiSnapshot(baseSource, packageName, context.previousVersion);
2321
+ }
2322
+ }
2323
+ if (!baselineSnapshot) {
2324
+ return [];
2325
+ }
2326
+ return diffRustApi(packageName, baselineSnapshot, candidateSnapshot);
2327
+ }
2328
+ async findRustEntrypoint(dir) {
2329
+ const candidates = [
2330
+ join10(dir, "src", "lib.rs"),
2331
+ join10(dir, "src", "main.rs"),
2332
+ join10(dir, "lib.rs"),
2333
+ join10(dir, "main.rs")
2334
+ ];
2335
+ for (const c of candidates) {
2336
+ if (await fileExists5(c)) {
2337
+ return c;
2338
+ }
2339
+ }
2340
+ return null;
2341
+ }
2342
+ };
2343
+
2344
+ // ../api-diff/dist/git-baseline.js
2345
+ import { execFile as execFile4 } from "child_process";
2346
+ import { mkdtemp as mkdtemp4, readFile as readFile9, readdir as readdir3, rm as rm3, stat as stat7 } from "fs/promises";
2347
+ import { tmpdir as tmpdir4 } from "os";
2348
+ import { join as join11, relative as relative2 } from "path";
2349
+ import { promisify as promisify4 } from "util";
2350
+ var execFileAsync4 = promisify4(execFile4);
2351
+ var GitBaselineResolver = class {
2352
+ repoRoot;
2353
+ constructor(repoRoot) {
2354
+ this.repoRoot = repoRoot;
2355
+ }
2356
+ /**
2357
+ * Checks out the last release of a package into a temporary worktree.
2358
+ *
2359
+ * Returns null when the package has never been released, which is not a
2360
+ * failure: a first release has nothing to be compared against.
2361
+ */
2362
+ async resolve(packageName, packageDir) {
2363
+ const ref = await this.lastReleaseTag(packageName);
2364
+ if (!ref)
2365
+ return null;
2366
+ const worktree = await mkdtemp4(join11(tmpdir4(), "verelease-baseline-git-"));
2367
+ const cleanup = async () => {
2368
+ try {
2369
+ await execFileAsync4("git", ["worktree", "remove", "--force", worktree], {
2370
+ cwd: this.repoRoot
2371
+ });
2372
+ } catch {
2373
+ await rm3(worktree, { recursive: true, force: true }).catch(() => void 0);
2374
+ await execFileAsync4("git", ["worktree", "prune"], { cwd: this.repoRoot }).catch(() => void 0);
1382
2375
  }
1383
2376
  };
1384
- return transaction;
2377
+ try {
2378
+ await execFileAsync4("git", ["worktree", "add", "--detach", worktree, ref], {
2379
+ cwd: this.repoRoot
2380
+ });
2381
+ } catch {
2382
+ await cleanup();
2383
+ return null;
2384
+ }
2385
+ const relativeDir = relative2(this.repoRoot, packageDir);
2386
+ let baselineDir = join11(worktree, relativeDir);
2387
+ let entryPointPath = await findEntrypoint(baselineDir);
2388
+ if (!entryPointPath) {
2389
+ baselineDir = await findPackageByName(worktree, packageName);
2390
+ entryPointPath = baselineDir ? await findEntrypoint(baselineDir) : null;
2391
+ }
2392
+ if (!entryPointPath) {
2393
+ await cleanup();
2394
+ return null;
2395
+ }
2396
+ return { ref, entryPointPath, cleanup };
2397
+ }
2398
+ /** The newest tag naming this package, by either supported tag format. */
2399
+ async lastReleaseTag(packageName) {
2400
+ for (const pattern of [`${packageName}@*`, "v*"]) {
2401
+ try {
2402
+ const { stdout } = await execFileAsync4("git", ["tag", "--list", pattern, "--sort=-creatordate"], { cwd: this.repoRoot });
2403
+ const [first] = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
2404
+ if (first)
2405
+ return first;
2406
+ } catch {
2407
+ return null;
2408
+ }
2409
+ }
2410
+ return null;
1385
2411
  }
1386
2412
  };
2413
+ async function findEntrypoint(pkgDir) {
2414
+ const candidates = [
2415
+ join11(pkgDir, "src", "index.ts"),
2416
+ join11(pkgDir, "src", "index.js"),
2417
+ join11(pkgDir, "index.ts"),
2418
+ join11(pkgDir, "index.js"),
2419
+ join11(pkgDir, "dist", "index.d.ts")
2420
+ ];
2421
+ for (const candidate of candidates) {
2422
+ try {
2423
+ if ((await stat7(candidate)).isFile())
2424
+ return candidate;
2425
+ } catch {
2426
+ }
2427
+ }
2428
+ return null;
2429
+ }
2430
+ async function findPackageByName(root, packageName, maxDepth = 6) {
2431
+ const queue = [{ dir: root, depth: 0 }];
2432
+ while (queue.length > 0) {
2433
+ const { dir, depth } = queue.shift();
2434
+ try {
2435
+ const manifest = JSON.parse(await readFile9(join11(dir, "package.json"), "utf8"));
2436
+ if (manifest.name === packageName)
2437
+ return dir;
2438
+ } catch {
2439
+ }
2440
+ if (depth >= maxDepth)
2441
+ continue;
2442
+ let entries;
2443
+ try {
2444
+ entries = await readdir3(dir, { withFileTypes: true });
2445
+ } catch {
2446
+ continue;
2447
+ }
2448
+ for (const entry of entries) {
2449
+ if (!entry.isDirectory())
2450
+ continue;
2451
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
2452
+ continue;
2453
+ queue.push({ dir: join11(dir, entry.name), depth: depth + 1 });
2454
+ }
2455
+ }
2456
+ return null;
2457
+ }
2458
+
2459
+ // ../manifest/dist/mutator.js
2460
+ import { readFile as readFile10, writeFile as writeFile4, stat as stat8 } from "fs/promises";
2461
+ import { join as join12 } from "path";
2462
+ import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
1387
2463
 
1388
2464
  // ../manifest/dist/lockfile.js
1389
- import { execFile as execFile3 } from "child_process";
1390
- import { promisify as promisify3 } from "util";
1391
- import { stat as stat5 } from "fs/promises";
1392
- import { join as join9 } from "path";
1393
- var execFileAsync3 = promisify3(execFile3);
1394
- async function fileExists5(path) {
2465
+ import { execFile as execFile5 } from "child_process";
2466
+ import { promisify as promisify5 } from "util";
2467
+ import { stat as stat9 } from "fs/promises";
2468
+ import { join as join13 } from "path";
2469
+ var execFileAsync5 = promisify5(execFile5);
2470
+ async function fileExists6(path) {
1395
2471
  try {
1396
- const s = await stat5(path);
2472
+ const s = await stat9(path);
1397
2473
  return s.isFile();
1398
2474
  } catch {
1399
2475
  return false;
@@ -1404,7 +2480,7 @@ var PnpmLockfileAdapter = class {
1404
2480
  async regenerateLockfile(rootDirectory) {
1405
2481
  const start = Date.now();
1406
2482
  try {
1407
- const { stdout, stderr } = await execFileAsync3("pnpm", ["install", "--lockfile-only"], {
2483
+ const { stdout, stderr } = await execFileAsync5("pnpm", ["install", "--lockfile-only"], {
1408
2484
  cwd: rootDirectory
1409
2485
  });
1410
2486
  return {
@@ -1426,7 +2502,29 @@ var NpmLockfileAdapter = class {
1426
2502
  async regenerateLockfile(rootDirectory) {
1427
2503
  const start = Date.now();
1428
2504
  try {
1429
- const { stdout, stderr } = await execFileAsync3("npm", ["install", "--package-lock-only"], {
2505
+ const { stdout, stderr } = await execFileAsync5("npm", ["install", "--package-lock-only"], {
2506
+ cwd: rootDirectory
2507
+ });
2508
+ return {
2509
+ success: true,
2510
+ output: (stdout + "\n" + stderr).trim(),
2511
+ durationMs: Date.now() - start
2512
+ };
2513
+ } catch (err) {
2514
+ return {
2515
+ success: false,
2516
+ output: err.message || String(err),
2517
+ durationMs: Date.now() - start
2518
+ };
2519
+ }
2520
+ }
2521
+ };
2522
+ var CargoLockfileAdapter = class {
2523
+ packageManager = "cargo";
2524
+ async regenerateLockfile(rootDirectory) {
2525
+ const start = Date.now();
2526
+ try {
2527
+ const { stdout, stderr } = await execFileAsync5("cargo", ["generate-lockfile"], {
1430
2528
  cwd: rootDirectory
1431
2529
  });
1432
2530
  return {
@@ -1446,55 +2544,99 @@ var NpmLockfileAdapter = class {
1446
2544
  var AutoLockfileAdapter = class {
1447
2545
  packageManager = "npm";
1448
2546
  async regenerateLockfile(rootDirectory) {
1449
- if (await fileExists5(join9(rootDirectory, "pnpm-lock.yaml"))) {
2547
+ const hasPnpm = await fileExists6(join13(rootDirectory, "pnpm-lock.yaml"));
2548
+ const hasNpm = await fileExists6(join13(rootDirectory, "package-lock.json"));
2549
+ const hasCargo = await fileExists6(join13(rootDirectory, "Cargo.toml")) || await fileExists6(join13(rootDirectory, "Cargo.lock"));
2550
+ if (hasCargo && !hasPnpm && !hasNpm) {
2551
+ this.packageManager = "cargo";
2552
+ return new CargoLockfileAdapter().regenerateLockfile(rootDirectory);
2553
+ }
2554
+ if (hasPnpm) {
1450
2555
  this.packageManager = "pnpm";
1451
- return new PnpmLockfileAdapter().regenerateLockfile(rootDirectory);
2556
+ const res = await new PnpmLockfileAdapter().regenerateLockfile(rootDirectory);
2557
+ if (hasCargo) {
2558
+ await new CargoLockfileAdapter().regenerateLockfile(rootDirectory).catch(() => {
2559
+ });
2560
+ }
2561
+ return res;
1452
2562
  }
1453
- if (await fileExists5(join9(rootDirectory, "package-lock.json"))) {
2563
+ if (hasNpm) {
1454
2564
  this.packageManager = "npm";
1455
- return new NpmLockfileAdapter().regenerateLockfile(rootDirectory);
2565
+ const res = await new NpmLockfileAdapter().regenerateLockfile(rootDirectory);
2566
+ if (hasCargo) {
2567
+ await new CargoLockfileAdapter().regenerateLockfile(rootDirectory).catch(() => {
2568
+ });
2569
+ }
2570
+ return res;
2571
+ }
2572
+ if (hasCargo) {
2573
+ this.packageManager = "cargo";
2574
+ return new CargoLockfileAdapter().regenerateLockfile(rootDirectory);
1456
2575
  }
1457
2576
  return new NpmLockfileAdapter().regenerateLockfile(rootDirectory);
1458
2577
  }
1459
2578
  };
1460
2579
 
1461
2580
  // ../publisher-npm/dist/publisher.js
1462
- import { execFile as execFile4, spawn as spawn2 } from "child_process";
1463
- import { promisify as promisify4 } from "util";
1464
- var execFileAsync4 = promisify4(execFile4);
2581
+ import { execFile as execFile6, spawn as spawn2 } from "child_process";
2582
+ import { promisify as promisify6 } from "util";
2583
+ import * as Effect2 from "effect/Effect";
2584
+ import * as Schedule from "effect/Schedule";
2585
+ import * as prompts from "@clack/prompts";
2586
+ import colors from "picocolors";
2587
+ var execFileAsync6 = promisify6(execFile6);
1465
2588
  function openBrowser(url) {
1466
2589
  const platform = process.platform;
1467
2590
  try {
1468
2591
  if (platform === "darwin") {
1469
- execFile4("open", [url]);
2592
+ execFile6("open", [url]);
1470
2593
  } else if (platform === "win32") {
1471
- execFile4("cmd", ["/c", "start", '""', url]);
2594
+ execFile6("cmd", ["/c", "start", '""', url]);
1472
2595
  } else {
1473
- execFile4("xdg-open", [url]);
2596
+ execFile6("xdg-open", [url]);
1474
2597
  }
1475
2598
  } catch {
1476
2599
  }
1477
2600
  }
1478
2601
  async function pollForWebAuthToken(doneUrl, timeoutMs = 9e4) {
1479
- const start = Date.now();
1480
- const pollIntervalMs = 1500;
1481
- while (Date.now() - start < timeoutMs) {
2602
+ const deadline = Date.now() + timeoutMs;
2603
+ while (Date.now() < deadline) {
1482
2604
  try {
1483
- const res = await fetch(doneUrl, {
2605
+ const response = await fetch(doneUrl, {
1484
2606
  headers: { "user-agent": "verelease-npm-publisher" }
1485
2607
  });
1486
- if (res.status === 200) {
1487
- const json = await res.json();
1488
- if (json && json.token) {
1489
- return json.token;
1490
- }
2608
+ if (response.status === 200) {
2609
+ const body = await response.json();
2610
+ if (body.token)
2611
+ return body.token;
2612
+ } else if (response.status !== 202) {
2613
+ return null;
1491
2614
  }
2615
+ const retryAfter = Number(response.headers.get("retry-after"));
2616
+ await new Promise((resolve3) => setTimeout(resolve3, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : 1500));
1492
2617
  } catch {
2618
+ await new Promise((resolve3) => setTimeout(resolve3, 1500));
1493
2619
  }
1494
- await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
1495
2620
  }
1496
2621
  return null;
1497
2622
  }
2623
+ function getNpmLoginUrl(authUrl) {
2624
+ const parsed = new URL(authUrl);
2625
+ return `https://www.npmjs.com/login?next=${encodeURIComponent(parsed.pathname + parsed.search)}`;
2626
+ }
2627
+ function mergePublishedAndUnpublishedVersions(versions, time) {
2628
+ const published = Array.isArray(versions) ? versions : typeof versions === "string" ? [versions] : [];
2629
+ const ghosts = [];
2630
+ if (time && typeof time === "object") {
2631
+ for (const key of Object.keys(time)) {
2632
+ if (key === "created" || key === "modified")
2633
+ continue;
2634
+ if (!published.includes(key))
2635
+ ghosts.push(key);
2636
+ }
2637
+ }
2638
+ return [...published, ...ghosts];
2639
+ }
1498
2640
  var NpmPublishProvider = class {
1499
2641
  options;
1500
2642
  name = "npm";
@@ -1507,20 +2649,90 @@ var NpmPublishProvider = class {
1507
2649
  }
1508
2650
  return channel;
1509
2651
  }
2652
+ /**
2653
+ * Check npm authentication status without throwing.
2654
+ */
2655
+ async checkAuth() {
2656
+ try {
2657
+ const { stdout } = await execFileAsync6("npm", ["whoami"]);
2658
+ const username = stdout.trim();
2659
+ return { authenticated: true, username };
2660
+ } catch (err) {
2661
+ return {
2662
+ authenticated: false,
2663
+ error: err.stderr || err.message || "401 Unauthorized"
2664
+ };
2665
+ }
2666
+ }
2667
+ /**
2668
+ * Proactively verifies npm authentication.
2669
+ * If unauthenticated and interactive, prompts the user to log in now,
2670
+ * runs `npm login` interactively, verifies the login, and continues.
2671
+ */
2672
+ async ensureAuth(options = {}) {
2673
+ const isInteractive = options.interactive ?? (this.options.interactive || !process.env.CI && process.stdin.isTTY);
2674
+ const status = await this.checkAuth();
2675
+ if (status.authenticated && status.username) {
2676
+ return status.username;
2677
+ }
2678
+ if (!isInteractive) {
2679
+ throw new NpmAuthError({
2680
+ message: "You are not logged in to npm (npm whoami failed with 401). Run 'npm login' or configure NPM_TOKEN / NODE_AUTH_TOKEN in CI."
2681
+ });
2682
+ }
2683
+ prompts.log.warn(colors.yellow("You are not currently logged in to npm."));
2684
+ const shouldLogin = await prompts.confirm({
2685
+ message: "Would you like to log in to npm now?",
2686
+ initialValue: true
2687
+ });
2688
+ if (prompts.isCancel(shouldLogin) || !shouldLogin) {
2689
+ throw new NpmAuthError({
2690
+ message: "npm authentication cancelled. Run 'npm login' before releasing."
2691
+ });
2692
+ }
2693
+ prompts.log.info(colors.cyan("Launching npm login in your terminal..."));
2694
+ await new Promise((resolve3, reject) => {
2695
+ const child = spawn2("npm", ["login"], { stdio: "inherit" });
2696
+ child.on("close", (code) => {
2697
+ if (code === 0) {
2698
+ resolve3();
2699
+ } else {
2700
+ reject(new NpmAuthError({
2701
+ message: `npm login exited with code ${code}`,
2702
+ code: code ?? void 0
2703
+ }));
2704
+ }
2705
+ });
2706
+ child.on("error", (err) => {
2707
+ reject(new NpmAuthError({
2708
+ message: `Failed to launch npm login: ${err.message}`
2709
+ }));
2710
+ });
2711
+ });
2712
+ const recheck = await this.checkAuth();
2713
+ if (recheck.authenticated && recheck.username) {
2714
+ prompts.log.success(colors.green(`Authenticated as ${colors.bold(recheck.username)} on npm.`));
2715
+ return recheck.username;
2716
+ }
2717
+ throw new NpmAuthError({
2718
+ message: "npm login completed, but npm authentication could not be verified."
2719
+ });
2720
+ }
1510
2721
  async prepare(ctx) {
1511
2722
  try {
1512
- await execFileAsync4("npm", ["pack", "--dry-run"], {
2723
+ await execFileAsync6("npm", ["pack", "--dry-run"], {
1513
2724
  cwd: ctx.packagePlan.directory
1514
2725
  });
1515
2726
  } catch (err) {
1516
- throw new Error(`Failed packing verification for package ${ctx.packagePlan.name}: ${err.message || String(err)}`);
2727
+ throw new PublishError({
2728
+ packageName: ctx.packagePlan.name,
2729
+ message: `Failed packing verification for package ${ctx.packagePlan.name}: ${err.message || String(err)}`
2730
+ });
1517
2731
  }
1518
- if (!ctx.dryRun && !process.env.CI) {
1519
- try {
1520
- await execFileAsync4("npm", ["whoami"]);
1521
- } catch {
1522
- throw new Error(`You are not logged in to npm (npm whoami failed with 401). Please run 'npm login' first.`);
1523
- }
2732
+ if (!ctx.dryRun) {
2733
+ await this.ensureAuth({
2734
+ interactive: this.options.interactive ?? (!process.env.CI && process.stdin.isTTY)
2735
+ });
1524
2736
  }
1525
2737
  }
1526
2738
  async publish(ctx) {
@@ -1530,8 +2742,8 @@ var NpmPublishProvider = class {
1530
2742
  if (ctx.dryRun) {
1531
2743
  args.push("--dry-run");
1532
2744
  }
1533
- const access = ctx.access || this.options.defaultAccess || "public";
1534
- args.push("--access", access);
2745
+ const access2 = ctx.access || this.options.defaultAccess || "public";
2746
+ args.push("--access", access2);
1535
2747
  const hasOidc = !!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL || process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
1536
2748
  const useProvenance = ctx.provenance ?? this.options.provenance ?? hasOidc;
1537
2749
  if (useProvenance) {
@@ -1545,8 +2757,7 @@ var NpmPublishProvider = class {
1545
2757
  }
1546
2758
  const isInteractive = this.options.interactive ?? (!ctx.dryRun && !process.env.CI && process.stdin.isTTY);
1547
2759
  if (isInteractive) {
1548
- console.log(`
1549
- \u{1F510} Interactive terminal detected for npm publish:`);
2760
+ prompts.log.info(colors.cyan("Terminal ready for npm publish authentication:"));
1550
2761
  console.log(` \u2022 WebAuthn / Passkey / Touch ID: press ENTER when prompted to open browser.`);
1551
2762
  console.log(` \u2022 Authenticator App: enter your 6-digit TOTP code if prompted.
1552
2763
  `);
@@ -1596,87 +2807,338 @@ var NpmPublishProvider = class {
1596
2807
  });
1597
2808
  });
1598
2809
  }
1599
- try {
1600
- const { stdout, stderr } = await execFileAsync4("npm", args, {
1601
- cwd: ctx.packagePlan.directory
2810
+ return new Promise((resolve3) => {
2811
+ const child = spawn2("npm", args, {
2812
+ cwd: ctx.packagePlan.directory,
2813
+ stdio: ["ignore", "pipe", "pipe"]
1602
2814
  });
1603
- return {
1604
- provider: this.name,
1605
- packageName: ctx.packagePlan.name,
1606
- version: ctx.packagePlan.proposedVersion,
1607
- published: true,
1608
- dryRun: ctx.dryRun,
1609
- distTag,
1610
- durationMs: Date.now() - startTime,
1611
- output: (stdout + "\n" + stderr).trim()
1612
- };
1613
- } catch (err) {
1614
- const combinedOutput = `${err.stdout || ""}
1615
- ${err.stderr || ""}
1616
- ${err.message || ""}`;
1617
- const authUrlMatch = combinedOutput.match(/https:\/\/www\.npmjs\.com\/auth\/cli\/[^\s\x1b"']+/);
1618
- const doneUrlMatch = combinedOutput.match(/https:\/\/registry\.npmjs\.org\/-\/v1\/done\?authId=[^\s\x1b"']+/);
1619
- if (authUrlMatch && doneUrlMatch && !ctx.dryRun) {
1620
- const authUrl = authUrlMatch[0];
1621
- const doneUrl = doneUrlMatch[0];
1622
- console.log(`
1623
- \x1B[36m\u{1F511} WebAuthn / Security Key 2FA Detected!\x1B[0m`);
1624
- console.log(` Your npm account uses Touch ID / security key (WebAuthn) instead of a 6-digit code.`);
1625
- console.log(` Opening browser to authenticate:`);
1626
- console.log(` \u{1F449} \x1B[4m${authUrl}\x1B[0m
1627
- `);
1628
- openBrowser(authUrl);
1629
- console.log(`\u23F3 Waiting for Touch ID / security key approval in your browser (up to 90s)...`);
1630
- const token = await pollForWebAuthToken(doneUrl, 9e4);
1631
- if (token) {
1632
- console.log(`\x1B[32m\u2713 Touch ID / Passkey approved! Completing publish...\x1B[0m
1633
- `);
1634
- return this.publish({
1635
- ...ctx,
1636
- extraArgs: [...ctx.extraArgs || [], "--otp", token]
1637
- });
1638
- } else {
1639
- console.error(`\x1B[31m\u274C WebAuthn approval timed out or was cancelled.\x1B[0m
2815
+ let stdout = "";
2816
+ let stderr = "";
2817
+ let openedAuthUrl = false;
2818
+ let authUrl;
2819
+ let doneUrl;
2820
+ const handleOutput = (chunk, isError) => {
2821
+ const text = chunk.toString();
2822
+ if (isError)
2823
+ stderr += text;
2824
+ else
2825
+ stdout += text;
2826
+ const doneUrlMatch = `${stdout}
2827
+ ${stderr}`.match(/https:\/\/registry\.npmjs\.org\/-\/v1\/done\?authId=[^\s\x1b"']+/);
2828
+ doneUrl ??= doneUrlMatch?.[0];
2829
+ if (!openedAuthUrl && !ctx.dryRun) {
2830
+ const authUrlMatch = `${stdout}
2831
+ ${stderr}`.match(/https:\/\/www\.npmjs\.com\/auth\/cli\/[^\s\x1b"']+/);
2832
+ if (authUrlMatch) {
2833
+ openedAuthUrl = true;
2834
+ authUrl = authUrlMatch[0];
2835
+ const loginUrl = getNpmLoginUrl(authUrl);
2836
+ prompts.log.info(colors.cyan(colors.bold("\u{1F511} WebAuthn / Security Key 2FA Detected!")));
2837
+ console.log(" Opening npm's authentication page while publish is waiting:");
2838
+ console.log(` \u{1F449} ${colors.underline(loginUrl)}
1640
2839
  `);
2840
+ openBrowser(loginUrl);
2841
+ }
1641
2842
  }
1642
- }
1643
- return {
1644
- provider: this.name,
1645
- packageName: ctx.packagePlan.name,
1646
- version: ctx.packagePlan.proposedVersion,
1647
- published: false,
1648
- dryRun: ctx.dryRun,
1649
- distTag,
1650
- durationMs: Date.now() - startTime,
1651
- output: err.stdout || "",
1652
- error: err.message || String(err)
1653
2843
  };
1654
- }
2844
+ child.stdout?.on("data", (chunk) => handleOutput(chunk, false));
2845
+ child.stderr?.on("data", (chunk) => handleOutput(chunk, true));
2846
+ child.on("error", (error) => {
2847
+ resolve3({
2848
+ provider: this.name,
2849
+ packageName: ctx.packagePlan.name,
2850
+ version: ctx.packagePlan.proposedVersion,
2851
+ published: false,
2852
+ dryRun: ctx.dryRun,
2853
+ distTag,
2854
+ durationMs: Date.now() - startTime,
2855
+ output: `${stdout}
2856
+ ${stderr}`.trim(),
2857
+ error: error.message
2858
+ });
2859
+ });
2860
+ child.on("close", async (code) => {
2861
+ if (code !== 0 && doneUrl && !ctx.dryRun) {
2862
+ const token = await pollForWebAuthToken(doneUrl);
2863
+ if (token) {
2864
+ resolve3(await this.publish({
2865
+ ...ctx,
2866
+ extraArgs: [...ctx.extraArgs || [], "--otp", token]
2867
+ }));
2868
+ return;
2869
+ }
2870
+ }
2871
+ const output = `${stdout}
2872
+ ${stderr}`.trim();
2873
+ resolve3({
2874
+ provider: this.name,
2875
+ packageName: ctx.packagePlan.name,
2876
+ version: ctx.packagePlan.proposedVersion,
2877
+ published: code === 0,
2878
+ dryRun: ctx.dryRun,
2879
+ distTag,
2880
+ durationMs: Date.now() - startTime,
2881
+ output,
2882
+ error: code === 0 ? void 0 : `npm publish exited with code ${code}.`
2883
+ });
2884
+ });
2885
+ });
1655
2886
  }
2887
+ /**
2888
+ * Resiliently fetches published versions using EffectTS retry schedule.
2889
+ *
2890
+ * The result also includes versions that were previously published and
2891
+ * then unpublished. npm permanently blocks republishing an unpublished
2892
+ * `name@version` (a supply-chain safeguard introduced after the left-pad
2893
+ * incident), so those "ghost" versions must be treated as unavailable
2894
+ * just like currently-published ones when proposing the next version.
2895
+ */
1656
2896
  async getPublishedVersions(packageName) {
2897
+ const fetchVersions = Effect2.tryPromise({
2898
+ try: async () => {
2899
+ const { stdout } = await execFileAsync6("npm", ["view", packageName, "--json", "versions", "time"]);
2900
+ const parsed = JSON.parse(stdout.trim());
2901
+ return mergePublishedAndUnpublishedVersions(parsed.versions, parsed.time);
2902
+ },
2903
+ catch: (err) => err
2904
+ });
2905
+ const withRetry = Effect2.retry(fetchVersions, {
2906
+ schedule: Schedule.recurs(2)
2907
+ });
1657
2908
  try {
1658
- const { stdout } = await execFileAsync4("npm", ["view", packageName, "versions", "--json"]);
1659
- const parsed = JSON.parse(stdout.trim());
1660
- if (Array.isArray(parsed)) {
1661
- return parsed;
1662
- }
1663
- if (typeof parsed === "string") {
1664
- return [parsed];
1665
- }
1666
- return [];
2909
+ return await Effect2.runPromise(withRetry);
1667
2910
  } catch {
1668
2911
  return [];
1669
2912
  }
1670
2913
  }
1671
2914
  };
1672
2915
 
2916
+ // ../publisher-cargo/dist/publisher.js
2917
+ import { execFile as execFile7, spawn as spawn3 } from "child_process";
2918
+ import { promisify as promisify7 } from "util";
2919
+ import { readFile as readFile11, stat as stat10 } from "fs/promises";
2920
+ import { join as join14 } from "path";
2921
+ import { homedir } from "os";
2922
+ import * as Effect3 from "effect/Effect";
2923
+ import * as Schedule2 from "effect/Schedule";
2924
+ import * as prompts2 from "@clack/prompts";
2925
+ import colors2 from "picocolors";
2926
+ import { parse as parseToml4 } from "smol-toml";
2927
+ var execFileAsync7 = promisify7(execFile7);
2928
+ async function fileExists7(path) {
2929
+ try {
2930
+ const s = await stat10(path);
2931
+ return s.isFile();
2932
+ } catch {
2933
+ return false;
2934
+ }
2935
+ }
2936
+ var CargoPublishProvider = class {
2937
+ options;
2938
+ name = "cargo";
2939
+ constructor(options = {}) {
2940
+ this.options = options;
2941
+ }
2942
+ /**
2943
+ * Checks whether crates.io / cargo credentials exist.
2944
+ * Looks in CARGO_REGISTRY_TOKEN env var, options.token,
2945
+ * ~/.cargo/credentials.toml, or ~/.cargo/credentials.
2946
+ */
2947
+ async checkAuth() {
2948
+ if (this.options.token) {
2949
+ return { authenticated: true, token: this.options.token };
2950
+ }
2951
+ if (process.env.CARGO_REGISTRY_TOKEN) {
2952
+ return { authenticated: true, token: process.env.CARGO_REGISTRY_TOKEN };
2953
+ }
2954
+ const home = homedir();
2955
+ const credToml = join14(home, ".cargo", "credentials.toml");
2956
+ const credOld = join14(home, ".cargo", "credentials");
2957
+ for (const credPath of [credToml, credOld]) {
2958
+ if (await fileExists7(credPath)) {
2959
+ try {
2960
+ const content = await readFile11(credPath, "utf8");
2961
+ const parsed = parseToml4(content);
2962
+ const token = parsed.registry?.token || parsed.registries?.["crates-io"]?.token;
2963
+ if (token) {
2964
+ return { authenticated: true, token };
2965
+ }
2966
+ } catch {
2967
+ }
2968
+ }
2969
+ }
2970
+ return {
2971
+ authenticated: false,
2972
+ error: "No Cargo token found in environment (CARGO_REGISTRY_TOKEN) or ~/.cargo/credentials.toml"
2973
+ };
2974
+ }
2975
+ /**
2976
+ * Ensures auth is present. If interactive and missing, prompts the user to enter a token.
2977
+ */
2978
+ async ensureAuth(options = {}) {
2979
+ const isInteractive = options.interactive !== void 0 ? options.interactive : this.options.interactive !== false && Boolean(process.stdout.isTTY);
2980
+ const status = await this.checkAuth();
2981
+ if (status.authenticated && status.token) {
2982
+ return status.token;
2983
+ }
2984
+ if (!isInteractive) {
2985
+ throw new CargoAuthError({
2986
+ message: status.error || "Missing crates.io token in non-interactive mode"
2987
+ });
2988
+ }
2989
+ prompts2.note(`Crates.io authentication is required to publish Rust crates.
2990
+ Generate an API token at: ${colors2.cyan("https://crates.io/settings/tokens")}`, colors2.yellow("crates.io Token Required"));
2991
+ const enteredToken = await prompts2.password({
2992
+ message: "Paste your crates.io API token:",
2993
+ validate(val) {
2994
+ if (!val || val.trim().length === 0)
2995
+ return "Token cannot be empty.";
2996
+ }
2997
+ });
2998
+ if (prompts2.isCancel(enteredToken)) {
2999
+ throw new CargoAuthError({
3000
+ message: "Crates.io authentication cancelled by user."
3001
+ });
3002
+ }
3003
+ const token = String(enteredToken).trim();
3004
+ process.env.CARGO_REGISTRY_TOKEN = token;
3005
+ return token;
3006
+ }
3007
+ /**
3008
+ * Queries crates.io API for crate existence and released versions.
3009
+ */
3010
+ async queryCratesIo(crateName) {
3011
+ const runQuery = Effect3.tryPromise({
3012
+ try: async () => {
3013
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(crateName)}`;
3014
+ const res = await fetch(url, {
3015
+ headers: {
3016
+ "User-Agent": "verelease-cargo-publisher (https://github.com/ivanjeremic/verelease)"
3017
+ }
3018
+ });
3019
+ if (res.status === 404) {
3020
+ return { exists: false, versions: [] };
3021
+ }
3022
+ if (!res.ok) {
3023
+ throw new Error(`crates.io API error: ${res.status} ${res.statusText}`);
3024
+ }
3025
+ const data = await res.json();
3026
+ const versions = data.versions?.map((v) => v.num) || [];
3027
+ return {
3028
+ exists: true,
3029
+ maxVersion: data.crate?.max_version,
3030
+ versions
3031
+ };
3032
+ },
3033
+ catch: (err) => err
3034
+ });
3035
+ const withRetry = Effect3.retry(runQuery, {
3036
+ schedule: Schedule2.recurs(3)
3037
+ });
3038
+ return Effect3.runPromise(withRetry);
3039
+ }
3040
+ async prepare(ctx) {
3041
+ if (!ctx.dryRun) {
3042
+ await this.ensureAuth({ interactive: this.options.interactive });
3043
+ }
3044
+ }
3045
+ async publish(ctx) {
3046
+ const { packagePlan, dryRun } = ctx;
3047
+ const startTime = Date.now();
3048
+ const cargoBin = this.options.cargoBinary || "cargo";
3049
+ let token;
3050
+ if (!dryRun) {
3051
+ token = await this.ensureAuth({ interactive: this.options.interactive });
3052
+ }
3053
+ const args = ["publish"];
3054
+ if (dryRun) {
3055
+ args.push("--dry-run");
3056
+ }
3057
+ if (this.options.registry) {
3058
+ args.push("--registry", this.options.registry);
3059
+ }
3060
+ if (this.options.allowDirty ?? true) {
3061
+ args.push("--allow-dirty");
3062
+ }
3063
+ if (ctx.extraArgs) {
3064
+ args.push(...ctx.extraArgs);
3065
+ }
3066
+ return new Promise((resolve3, reject) => {
3067
+ const child = spawn3(cargoBin, args, {
3068
+ cwd: packagePlan.directory,
3069
+ env: {
3070
+ ...process.env,
3071
+ ...token ? { CARGO_REGISTRY_TOKEN: token } : {}
3072
+ },
3073
+ stdio: ["ignore", "pipe", "pipe"]
3074
+ });
3075
+ let stdout = "";
3076
+ let stderr = "";
3077
+ child.stdout?.on("data", (chunk) => {
3078
+ stdout += chunk.toString();
3079
+ });
3080
+ child.stderr?.on("data", (chunk) => {
3081
+ stderr += chunk.toString();
3082
+ });
3083
+ child.on("close", (code) => {
3084
+ const durationMs = Date.now() - startTime;
3085
+ const combinedOutput = (stdout + "\n" + stderr).trim();
3086
+ if (code !== 0) {
3087
+ const errMsg = `cargo publish exited with code ${code}:
3088
+ ${combinedOutput}`;
3089
+ const pubErr = new PublishError({
3090
+ message: errMsg,
3091
+ packageName: packagePlan.name,
3092
+ code: code ?? 1
3093
+ });
3094
+ return resolve3({
3095
+ provider: this.name,
3096
+ packageName: packagePlan.name,
3097
+ version: packagePlan.proposedVersion,
3098
+ published: false,
3099
+ dryRun,
3100
+ distTag: "latest",
3101
+ durationMs,
3102
+ output: combinedOutput,
3103
+ error: errMsg
3104
+ });
3105
+ }
3106
+ resolve3({
3107
+ provider: this.name,
3108
+ packageName: packagePlan.name,
3109
+ version: packagePlan.proposedVersion,
3110
+ published: !dryRun,
3111
+ dryRun,
3112
+ distTag: "latest",
3113
+ durationMs,
3114
+ output: combinedOutput
3115
+ });
3116
+ });
3117
+ child.on("error", (err) => {
3118
+ const durationMs = Date.now() - startTime;
3119
+ resolve3({
3120
+ provider: this.name,
3121
+ packageName: packagePlan.name,
3122
+ version: packagePlan.proposedVersion,
3123
+ published: false,
3124
+ dryRun,
3125
+ distTag: "latest",
3126
+ durationMs,
3127
+ output: "",
3128
+ error: `Failed to execute ${cargoBin}: ${err.message}`
3129
+ });
3130
+ });
3131
+ });
3132
+ }
3133
+ };
3134
+
1673
3135
  // ../config/dist/config.js
1674
- import { stat as stat6 } from "fs/promises";
1675
- import { join as join10 } from "path";
3136
+ import { stat as stat11 } from "fs/promises";
3137
+ import { join as join15 } from "path";
1676
3138
  import { pathToFileURL } from "url";
1677
- async function fileExists6(path) {
3139
+ async function fileExists8(path) {
1678
3140
  try {
1679
- const s = await stat6(path);
3141
+ const s = await stat11(path);
1680
3142
  return s.isFile();
1681
3143
  } catch {
1682
3144
  return false;
@@ -1694,8 +3156,8 @@ async function loadConfig(rootDir) {
1694
3156
  "autoversion.config.mjs"
1695
3157
  ];
1696
3158
  for (const name of candidateNames) {
1697
- const fullPath = join10(rootDir, name);
1698
- if (await fileExists6(fullPath)) {
3159
+ const fullPath = join15(rootDir, name);
3160
+ if (await fileExists8(fullPath)) {
1699
3161
  try {
1700
3162
  const fileUrl = pathToFileURL(fullPath).href;
1701
3163
  const mod = await import(fileUrl);
@@ -1740,8 +3202,8 @@ var MigrationScriptSchema = z.object({
1740
3202
  });
1741
3203
 
1742
3204
  // ../ai/dist/changelog.js
1743
- function renderChangelogMarkdown(version, date, data) {
1744
- const parts = [`# ${version} (${date})`, ""];
3205
+ function renderChangelogMarkdown(version2, date, data) {
3206
+ const parts = [`# ${version2} (${date})`, ""];
1745
3207
  if (data.breakingChanges.length > 0) {
1746
3208
  parts.push("## \u26A0\uFE0F Breaking Changes");
1747
3209
  for (const item of data.breakingChanges) {
@@ -1949,13 +3411,13 @@ walkAndTransform(targetDir);
1949
3411
  }
1950
3412
 
1951
3413
  // ../ai/dist/validation.js
1952
- import { cp, mkdtemp as mkdtemp4, rm as rm3, writeFile as writeFile4, mkdir as mkdir5, symlink as symlink2, readdir as readdir3 } from "fs/promises";
1953
- import { execFile as execFile5 } from "child_process";
1954
- import { promisify as promisify5 } from "util";
1955
- import { join as join11 } from "path";
1956
- import { tmpdir as tmpdir4 } from "os";
3414
+ import { cp as cp2, mkdtemp as mkdtemp5, rm as rm4, writeFile as writeFile5, mkdir as mkdir5, symlink as symlink2, readdir as readdir4 } from "fs/promises";
3415
+ import { execFile as execFile8 } from "child_process";
3416
+ import { promisify as promisify8 } from "util";
3417
+ import { join as join16 } from "path";
3418
+ import { tmpdir as tmpdir5 } from "os";
1957
3419
  import * as tar3 from "tar";
1958
- var execFileAsync5 = promisify5(execFile5);
3420
+ var execFileAsync8 = promisify8(execFile8);
1959
3421
 
1960
3422
  // ../ai/dist/provider.js
1961
3423
  var TanStackReleaseAI = class {
@@ -2013,9 +3475,9 @@ ${JSON.stringify(input.breakingChanges, null, 2)}`;
2013
3475
  };
2014
3476
 
2015
3477
  // ../git/dist/git.js
2016
- import { execFile as execFile6 } from "child_process";
2017
- import { promisify as promisify6 } from "util";
2018
- var execFileAsync6 = promisify6(execFile6);
3478
+ import { execFile as execFile9 } from "child_process";
3479
+ import { promisify as promisify9 } from "util";
3480
+ var execFileAsync9 = promisify9(execFile9);
2019
3481
  var GitManager = class {
2020
3482
  cwd;
2021
3483
  constructor(cwd = process.cwd()) {
@@ -2030,35 +3492,86 @@ var GitManager = class {
2030
3492
  }
2031
3493
  return `${options.packageName}@${options.version}`;
2032
3494
  }
3495
+ /**
3496
+ * The most recent release tag for a package, or null if it has never been
3497
+ * released.
3498
+ *
3499
+ * Tags are matched by the same formats `formatTagName` writes, so this finds
3500
+ * what a previous release actually created rather than assuming a convention.
3501
+ */
3502
+ async lastReleaseTag(packageName, strategy) {
3503
+ const pattern = strategy === "fixed" ? "v*" : `${packageName}@*`;
3504
+ try {
3505
+ const { stdout } = await execFileAsync9("git", ["tag", "--list", pattern, "--sort=-creatordate"], { cwd: this.cwd });
3506
+ const [first] = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
3507
+ return first ?? null;
3508
+ } catch {
3509
+ return null;
3510
+ }
3511
+ }
3512
+ /**
3513
+ * Files under `directory` that changed between `ref` and the working tree.
3514
+ *
3515
+ * Used to answer whether a package changed at all since its last release. A
3516
+ * package nobody touched should not be republished, and asking git is exact
3517
+ * — far better evidence than inferring activity from a green test suite.
3518
+ */
3519
+ async changedFilesSince(ref, directory) {
3520
+ try {
3521
+ const { stdout } = await execFileAsync9("git", ["diff", "--name-only", `${ref}...HEAD`, "--", directory], { cwd: this.cwd });
3522
+ const committed = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
3523
+ const { stdout: dirty } = await execFileAsync9("git", ["status", "--porcelain", "--", directory], { cwd: this.cwd });
3524
+ const uncommitted = dirty.split("\n").map((line) => line.slice(3).trim()).filter(Boolean);
3525
+ return [.../* @__PURE__ */ new Set([...committed, ...uncommitted])];
3526
+ } catch {
3527
+ return [];
3528
+ }
3529
+ }
3530
+ /**
3531
+ * The newest release tag in the repository, whatever package it names.
3532
+ *
3533
+ * A monorepo that tags only its root package still has a meaningful "last
3534
+ * release" for everything released alongside it; without this, packages that
3535
+ * are never tagged individually would have no reference point at all.
3536
+ */
3537
+ async latestReleaseTag() {
3538
+ try {
3539
+ const { stdout } = await execFileAsync9("git", ["tag", "--list", "--sort=-creatordate"], { cwd: this.cwd });
3540
+ const [first] = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
3541
+ return first ?? null;
3542
+ } catch {
3543
+ return null;
3544
+ }
3545
+ }
2033
3546
  async isClean() {
2034
- const { stdout } = await execFileAsync6("git", ["status", "--porcelain"], {
3547
+ const { stdout } = await execFileAsync9("git", ["status", "--porcelain"], {
2035
3548
  cwd: this.cwd
2036
3549
  });
2037
3550
  return stdout.trim().length === 0;
2038
3551
  }
2039
3552
  async getHeadCommit() {
2040
- const { stdout } = await execFileAsync6("git", ["rev-parse", "HEAD"], {
3553
+ const { stdout } = await execFileAsync9("git", ["rev-parse", "HEAD"], {
2041
3554
  cwd: this.cwd
2042
3555
  });
2043
3556
  return stdout.trim();
2044
3557
  }
2045
3558
  async createTag(tagName, message) {
2046
3559
  const args = ["tag", "-a", tagName, "-m", message || `Release ${tagName}`];
2047
- await execFileAsync6("git", args, { cwd: this.cwd });
3560
+ await execFileAsync9("git", args, { cwd: this.cwd });
2048
3561
  }
2049
3562
  async createReleaseCommit(files, message = "chore(release): publish") {
2050
3563
  if (files.length > 0) {
2051
- await execFileAsync6("git", ["add", ...files], { cwd: this.cwd });
3564
+ await execFileAsync9("git", ["add", ...files], { cwd: this.cwd });
2052
3565
  } else {
2053
- await execFileAsync6("git", ["add", "-A"], { cwd: this.cwd });
3566
+ await execFileAsync9("git", ["add", "-A"], { cwd: this.cwd });
2054
3567
  }
2055
- await execFileAsync6("git", ["commit", "-m", message], { cwd: this.cwd });
3568
+ await execFileAsync9("git", ["commit", "-m", message], { cwd: this.cwd });
2056
3569
  return this.getHeadCommit();
2057
3570
  }
2058
3571
  };
2059
3572
 
2060
3573
  // ../test-adapter/dist/command.js
2061
- import { spawn as spawn3 } from "child_process";
3574
+ import { spawn as spawn4 } from "child_process";
2062
3575
  var CommandTestAdapter = class {
2063
3576
  config;
2064
3577
  name = "command";
@@ -2083,7 +3596,7 @@ var CommandTestAdapter = class {
2083
3596
  const [executable, ...args] = cmd;
2084
3597
  const finalArgs = [...args, ...ctx.extraArgs || []];
2085
3598
  return new Promise((resolve3) => {
2086
- const child = spawn3(executable, finalArgs, {
3599
+ const child = spawn4(executable, finalArgs, {
2087
3600
  cwd: ctx.packagePath,
2088
3601
  env: { ...process.env, ...ctx.env },
2089
3602
  stdio: ["ignore", "pipe", "pipe"]
@@ -2154,7 +3667,173 @@ var CommandTestAdapter = class {
2154
3667
  stack: err.stack
2155
3668
  }
2156
3669
  ],
2157
- totalTests: 1,
3670
+ totalTests: 1,
3671
+ passedTests: 0,
3672
+ failedTests: 1,
3673
+ skippedTests: 0
3674
+ });
3675
+ });
3676
+ });
3677
+ }
3678
+ };
3679
+
3680
+ // ../test-adapter/dist/cargo.js
3681
+ import { spawn as spawn5 } from "child_process";
3682
+ function parseCargoTestOutput(stdout, stderr) {
3683
+ const tests = [];
3684
+ const lines = stdout.split("\n");
3685
+ let passed = 0;
3686
+ let failed = 0;
3687
+ let skipped = 0;
3688
+ const testLineRegex = /^\s*test\s+([^\s]+)\s+\.\.\.\s+(ok|FAILED|ignored)/;
3689
+ const failureSection = [];
3690
+ let inFailureSection = false;
3691
+ for (const line of lines) {
3692
+ if (line.startsWith("failures:")) {
3693
+ inFailureSection = true;
3694
+ continue;
3695
+ }
3696
+ if (inFailureSection) {
3697
+ if (line.startsWith("test result:")) {
3698
+ inFailureSection = false;
3699
+ } else if (line.trim()) {
3700
+ failureSection.push(line);
3701
+ }
3702
+ }
3703
+ const match = line.match(testLineRegex);
3704
+ if (match) {
3705
+ const [, testName, statusStr] = match;
3706
+ if (statusStr === "ok") {
3707
+ passed++;
3708
+ tests.push({
3709
+ name: testName,
3710
+ status: "passed"
3711
+ });
3712
+ } else if (statusStr === "FAILED") {
3713
+ failed++;
3714
+ tests.push({
3715
+ name: testName,
3716
+ status: "failed",
3717
+ failure: {
3718
+ message: `Cargo test assertion failed: ${testName}`
3719
+ }
3720
+ });
3721
+ } else if (statusStr === "ignored") {
3722
+ skipped++;
3723
+ tests.push({
3724
+ name: testName,
3725
+ status: "skipped"
3726
+ });
3727
+ }
3728
+ }
3729
+ }
3730
+ const summaryRegex = /test result:\s+(ok|FAILED)\.\s+(\d+)\s+passed;\s+(\d+)\s+failed;\s+(\d+)\s+ignored/;
3731
+ const summaryMatch = stdout.match(summaryRegex);
3732
+ if (summaryMatch) {
3733
+ const [, resultStr, pStr, fStr, iStr] = summaryMatch;
3734
+ passed = parseInt(pStr, 10);
3735
+ failed = parseInt(fStr, 10);
3736
+ skipped = parseInt(iStr, 10);
3737
+ }
3738
+ const total = passed + failed + skipped;
3739
+ const success = failed === 0;
3740
+ return {
3741
+ success,
3742
+ totalTests: total,
3743
+ passedTests: passed,
3744
+ failedTests: failed,
3745
+ skippedTests: skipped,
3746
+ tests,
3747
+ failures: failureSection
3748
+ };
3749
+ }
3750
+ var CargoTestAdapter = class {
3751
+ options;
3752
+ name = "cargo:test";
3753
+ constructor(options = {}) {
3754
+ this.options = options;
3755
+ }
3756
+ async detect(ctx) {
3757
+ const isCargo = ctx.files.includes("Cargo.toml") || Boolean(ctx.manifest && ("package" in ctx.manifest || "workspace" in ctx.manifest));
3758
+ if (isCargo) {
3759
+ return {
3760
+ supported: true,
3761
+ confidence: 0.95,
3762
+ suggestedConfig: { command: ["cargo", "test"] }
3763
+ };
3764
+ }
3765
+ return { supported: false, confidence: 0 };
3766
+ }
3767
+ async run(ctx) {
3768
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3769
+ const startTime = Date.now();
3770
+ const cargoBin = this.options.cargoBinary || "cargo";
3771
+ const baseArgs = ["test", ...this.options.extraArgs || []];
3772
+ const finalArgs = [...baseArgs, ...ctx.extraArgs || []];
3773
+ return new Promise((resolve3) => {
3774
+ const child = spawn5(cargoBin, finalArgs, {
3775
+ cwd: ctx.packagePath,
3776
+ env: { ...process.env, ...ctx.env },
3777
+ stdio: ["ignore", "pipe", "pipe"]
3778
+ });
3779
+ let stdout = "";
3780
+ let stderr = "";
3781
+ child.stdout?.on("data", (chunk) => {
3782
+ stdout += chunk.toString();
3783
+ });
3784
+ child.stderr?.on("data", (chunk) => {
3785
+ stderr += chunk.toString();
3786
+ });
3787
+ child.on("close", (code) => {
3788
+ const durationMs = Date.now() - startTime;
3789
+ const parsed = parseCargoTestOutput(stdout, stderr);
3790
+ const suites = [
3791
+ {
3792
+ name: "cargo-test-suite",
3793
+ status: parsed.success && code === 0 ? "passed" : "failed",
3794
+ durationMs,
3795
+ tests: parsed.tests
3796
+ }
3797
+ ];
3798
+ const diagnostics = [];
3799
+ if (code !== 0 || !parsed.success) {
3800
+ diagnostics.push({
3801
+ severity: "error",
3802
+ message: parsed.failures.join("\n") || stderr.trim() || `cargo test exited with code ${code}`
3803
+ });
3804
+ }
3805
+ resolve3({
3806
+ adapter: this.name,
3807
+ runtime: "rust",
3808
+ command: [cargoBin, ...finalArgs],
3809
+ startedAt,
3810
+ durationMs,
3811
+ success: code === 0 && parsed.success,
3812
+ suites,
3813
+ diagnostics,
3814
+ totalTests: parsed.totalTests,
3815
+ passedTests: parsed.passedTests,
3816
+ failedTests: parsed.failedTests,
3817
+ skippedTests: parsed.skippedTests
3818
+ });
3819
+ });
3820
+ child.on("error", (err) => {
3821
+ const durationMs = Date.now() - startTime;
3822
+ resolve3({
3823
+ adapter: this.name,
3824
+ runtime: "rust",
3825
+ command: [cargoBin, ...finalArgs],
3826
+ startedAt,
3827
+ durationMs,
3828
+ success: false,
3829
+ suites: [],
3830
+ diagnostics: [
3831
+ {
3832
+ severity: "error",
3833
+ message: `Failed to spawn ${cargoBin}: ${err.message}`
3834
+ }
3835
+ ],
3836
+ totalTests: 0,
2158
3837
  passedTests: 0,
2159
3838
  failedTests: 1,
2160
3839
  skippedTests: 0
@@ -2165,8 +3844,9 @@ var CommandTestAdapter = class {
2165
3844
  };
2166
3845
 
2167
3846
  // ../test-adapter/dist/test-suite-analyzer.js
2168
- import { readFile as readFile6, readdir as readdir4 } from "fs/promises";
2169
- import { join as join12 } from "path";
3847
+ import { readFile as readFile12, readdir as readdir5 } from "fs/promises";
3848
+ import { join as join17 } from "path";
3849
+ import { parse as parseToml5 } from "smol-toml";
2170
3850
  var TestSuiteAnalyzer = class {
2171
3851
  name = "test-suite-analyzer";
2172
3852
  adapters;
@@ -2174,6 +3854,7 @@ var TestSuiteAnalyzer = class {
2174
3854
  constructor(options) {
2175
3855
  this.adapters = [
2176
3856
  ...options?.adapters ?? [],
3857
+ new CargoTestAdapter(),
2177
3858
  new CommandTestAdapter()
2178
3859
  // always available as lowest-confidence fallback
2179
3860
  ];
@@ -2181,24 +3862,27 @@ var TestSuiteAnalyzer = class {
2181
3862
  }
2182
3863
  async analyze(context) {
2183
3864
  const { packageName, packagePath } = context;
2184
- let manifest;
3865
+ let manifest = {};
3866
+ let isCargo = false;
2185
3867
  try {
2186
- const raw = await readFile6(join12(packagePath, "package.json"), "utf8");
3868
+ const raw = await readFile12(join17(packagePath, "package.json"), "utf8");
2187
3869
  manifest = JSON.parse(raw);
2188
3870
  } catch {
2189
- return [];
2190
- }
2191
- const scripts = manifest.scripts || {};
2192
- if (!scripts.test) {
2193
- return [];
3871
+ try {
3872
+ const cargoRaw = await readFile12(join17(packagePath, "Cargo.toml"), "utf8");
3873
+ manifest = parseToml5(cargoRaw);
3874
+ isCargo = true;
3875
+ } catch {
3876
+ return [];
3877
+ }
2194
3878
  }
2195
- let files;
2196
- try {
2197
- const entries = await readdir4(packagePath, { recursive: true });
2198
- files = entries.map(String);
2199
- } catch {
2200
- files = [];
3879
+ if (!isCargo) {
3880
+ const scripts = manifest.scripts || {};
3881
+ if (!scripts.test) {
3882
+ return [];
3883
+ }
2201
3884
  }
3885
+ const files = await listProjectFiles(packagePath);
2202
3886
  const detectionCtx = { packagePath, manifest, files };
2203
3887
  const best = await this.detectBestAdapter(detectionCtx);
2204
3888
  if (!best) {
@@ -2215,6 +3899,7 @@ var TestSuiteAnalyzer = class {
2215
3899
  return [
2216
3900
  {
2217
3901
  id: `test.suite.error:${packageName}`,
3902
+ blocking: true,
2218
3903
  package: packageName,
2219
3904
  source: "test",
2220
3905
  severity: "major",
@@ -2247,25 +3932,7 @@ var TestSuiteAnalyzer = class {
2247
3932
  return [];
2248
3933
  }
2249
3934
  if (testRun.success) {
2250
- return [
2251
- {
2252
- id: `test.suite.pass:${packageName}`,
2253
- package: packageName,
2254
- source: "test",
2255
- severity: "patch",
2256
- confidence: 0.9,
2257
- title: `Test suite passed (${testRun.passedTests}/${testRun.totalTests} tests)`,
2258
- description: `All ${testRun.totalTests} tests passed via ${adapterName} in ${testRun.durationMs}ms. Code changes are confirmed compatible.`,
2259
- metadata: {
2260
- adapter: adapterName,
2261
- total: testRun.totalTests,
2262
- passed: testRun.passedTests,
2263
- failed: testRun.failedTests,
2264
- skipped: testRun.skippedTests,
2265
- durationMs: testRun.durationMs
2266
- }
2267
- }
2268
- ];
3935
+ return [];
2269
3936
  }
2270
3937
  const failedSuites = testRun.suites.filter((s) => s.status === "failed");
2271
3938
  const failedTests = failedSuites.flatMap((s) => s.tests.filter((t) => t.status === "failed"));
@@ -2276,6 +3943,7 @@ var TestSuiteAnalyzer = class {
2276
3943
  package: packageName,
2277
3944
  source: "test",
2278
3945
  severity: "major",
3946
+ blocking: true,
2279
3947
  confidence: 0.85,
2280
3948
  title: `Test suite failed (${testRun.failedTests}/${testRun.totalTests} tests failed)`,
2281
3949
  description: `${testRun.failedTests} tests failed via ${adapterName}:
@@ -2292,9 +3960,53 @@ ${failureMessages}`,
2292
3960
  ];
2293
3961
  }
2294
3962
  };
3963
+ var UNINTERESTING_DIRECTORIES = /* @__PURE__ */ new Set([
3964
+ "node_modules",
3965
+ "dist",
3966
+ "build",
3967
+ "out",
3968
+ "coverage",
3969
+ "target",
3970
+ "tmp",
3971
+ "test-results",
3972
+ "playwright-report"
3973
+ ]);
3974
+ async function listProjectFiles(root, { maxDepth = 3, maxEntries = 5e3 } = {}) {
3975
+ const files = [];
3976
+ const queue = [
3977
+ { dir: root, depth: 0, prefix: "" }
3978
+ ];
3979
+ while (queue.length > 0 && files.length < maxEntries) {
3980
+ const { dir, depth, prefix } = queue.shift();
3981
+ let entries;
3982
+ try {
3983
+ entries = await readdir5(dir, { withFileTypes: true });
3984
+ } catch {
3985
+ continue;
3986
+ }
3987
+ for (const entry of entries) {
3988
+ if (files.length >= maxEntries)
3989
+ break;
3990
+ const relative3 = prefix ? `${prefix}/${entry.name}` : entry.name;
3991
+ if (entry.isDirectory()) {
3992
+ if (UNINTERESTING_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
3993
+ continue;
3994
+ if (depth < maxDepth) {
3995
+ queue.push({ dir: join17(dir, entry.name), depth: depth + 1, prefix: relative3 });
3996
+ }
3997
+ } else {
3998
+ files.push(relative3);
3999
+ }
4000
+ }
4001
+ }
4002
+ return files;
4003
+ }
2295
4004
 
2296
4005
  // ../test-adapter-vitest/dist/vitest.js
2297
- import { spawn as spawn4 } from "child_process";
4006
+ import { spawn as spawn6 } from "child_process";
4007
+ import { mkdtemp as mkdtemp6, readFile as readFile13, rm as rm5 } from "fs/promises";
4008
+ import { tmpdir as tmpdir6 } from "os";
4009
+ import { join as join18 } from "path";
2298
4010
  var VitestTestAdapter = class {
2299
4011
  name = "vitest";
2300
4012
  async detect(ctx) {
@@ -2315,9 +4027,20 @@ var VitestTestAdapter = class {
2315
4027
  async run(ctx) {
2316
4028
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2317
4029
  const startTime = Date.now();
2318
- const command = ["npx", "vitest", "run", "--reporter=json", ...ctx.testFiles || []];
4030
+ const reportDirectory = await mkdtemp6(join18(tmpdir6(), "verelease-vitest-"));
4031
+ const reportFile = join18(reportDirectory, "report.json");
4032
+ const declaredArgs = await readVitestScriptArgs(ctx.packagePath);
4033
+ const command = [
4034
+ "npx",
4035
+ "vitest",
4036
+ "run",
4037
+ "--reporter=json",
4038
+ `--outputFile=${reportFile}`,
4039
+ ...declaredArgs,
4040
+ ...ctx.testFiles || []
4041
+ ];
2319
4042
  return new Promise((resolve3) => {
2320
- const child = spawn4(command[0], command.slice(1), {
4043
+ const child = spawn6(command[0], command.slice(1), {
2321
4044
  cwd: ctx.packagePath,
2322
4045
  env: {
2323
4046
  ...process.env,
@@ -2334,12 +4057,19 @@ var VitestTestAdapter = class {
2334
4057
  child.stderr?.on("data", (chunk) => {
2335
4058
  stderr += chunk.toString();
2336
4059
  });
2337
- child.on("close", (code) => {
4060
+ child.on("close", async (code) => {
2338
4061
  const durationMs = Date.now() - startTime;
2339
- const normalized = this.parseJsonOutput(stdout, stderr, code === 0, durationMs, startedAt, command);
2340
- resolve3(normalized);
4062
+ let jsonOutput = stdout;
4063
+ try {
4064
+ jsonOutput = await readFile13(reportFile, "utf8");
4065
+ } catch {
4066
+ } finally {
4067
+ await rm5(reportDirectory, { recursive: true, force: true }).catch(() => void 0);
4068
+ }
4069
+ resolve3(this.parseJsonOutput(jsonOutput, stderr, code === 0, durationMs, startedAt, command));
2341
4070
  });
2342
4071
  child.on("error", (err) => {
4072
+ void rm5(reportDirectory, { recursive: true, force: true });
2343
4073
  resolve3({
2344
4074
  adapter: this.name,
2345
4075
  runtime: process.version,
@@ -2432,9 +4162,74 @@ var VitestTestAdapter = class {
2432
4162
  };
2433
4163
  }
2434
4164
  };
4165
+ async function readVitestScriptArgs(packagePath) {
4166
+ let script = "";
4167
+ try {
4168
+ const { readFile: readFile16 } = await import("fs/promises");
4169
+ const { join: join21 } = await import("path");
4170
+ const manifest = JSON.parse(await readFile16(join21(packagePath, "package.json"), "utf8"));
4171
+ script = manifest.scripts?.test ?? "";
4172
+ } catch {
4173
+ return [];
4174
+ }
4175
+ if (!script.includes("vitest"))
4176
+ return [];
4177
+ const tokens = tokenizeCommand(script);
4178
+ const start = tokens.indexOf("vitest");
4179
+ if (start === -1)
4180
+ return [];
4181
+ const args = [];
4182
+ for (let index = start + 1; index < tokens.length; index += 1) {
4183
+ const token = tokens[index];
4184
+ if (token === "run")
4185
+ continue;
4186
+ if (token.startsWith("--reporter")) {
4187
+ if (!token.includes("="))
4188
+ index += 1;
4189
+ continue;
4190
+ }
4191
+ if (token.startsWith("--outputFile")) {
4192
+ if (!token.includes("="))
4193
+ index += 1;
4194
+ continue;
4195
+ }
4196
+ if (token === "&&" || token === ";")
4197
+ break;
4198
+ args.push(token);
4199
+ }
4200
+ return args;
4201
+ }
4202
+ function tokenizeCommand(command) {
4203
+ const tokens = [];
4204
+ let current = "";
4205
+ let quote = null;
4206
+ for (const character of command) {
4207
+ if (quote) {
4208
+ if (character === quote)
4209
+ quote = null;
4210
+ else
4211
+ current += character;
4212
+ continue;
4213
+ }
4214
+ if (character === '"' || character === "'") {
4215
+ quote = character;
4216
+ continue;
4217
+ }
4218
+ if (/\s/u.test(character)) {
4219
+ if (current)
4220
+ tokens.push(current);
4221
+ current = "";
4222
+ continue;
4223
+ }
4224
+ current += character;
4225
+ }
4226
+ if (current)
4227
+ tokens.push(current);
4228
+ return tokens;
4229
+ }
2435
4230
 
2436
4231
  // ../test-adapter-jest/dist/jest.js
2437
- import { spawn as spawn5 } from "child_process";
4232
+ import { spawn as spawn7 } from "child_process";
2438
4233
  var JestTestAdapter = class {
2439
4234
  name = "jest";
2440
4235
  async detect(ctx) {
@@ -2458,7 +4253,7 @@ var JestTestAdapter = class {
2458
4253
  const startTime = Date.now();
2459
4254
  const command = ["npx", "jest", "--json", ...ctx.testFiles || []];
2460
4255
  return new Promise((resolve3) => {
2461
- const child = spawn5(command[0], command.slice(1), {
4256
+ const child = spawn7(command[0], command.slice(1), {
2462
4257
  cwd: ctx.packagePath,
2463
4258
  env: {
2464
4259
  ...process.env,
@@ -2574,7 +4369,7 @@ var JestTestAdapter = class {
2574
4369
  };
2575
4370
 
2576
4371
  // ../test-adapter-bun/dist/bun.js
2577
- import { spawn as spawn6 } from "child_process";
4372
+ import { spawn as spawn8 } from "child_process";
2578
4373
  var BunTestAdapter = class {
2579
4374
  name = "bun:test";
2580
4375
  async detect(ctx) {
@@ -2591,7 +4386,7 @@ var BunTestAdapter = class {
2591
4386
  const startTime = Date.now();
2592
4387
  const command = ["bun", "test", ...ctx.testFiles || []];
2593
4388
  return new Promise((resolve3) => {
2594
- const child = spawn6(command[0], command.slice(1), {
4389
+ const child = spawn8(command[0], command.slice(1), {
2595
4390
  cwd: ctx.packagePath,
2596
4391
  env: {
2597
4392
  ...process.env,
@@ -2679,7 +4474,7 @@ var BunTestAdapter = class {
2679
4474
  };
2680
4475
 
2681
4476
  // ../test-adapter-deno/dist/deno.js
2682
- import { spawn as spawn7 } from "child_process";
4477
+ import { spawn as spawn9 } from "child_process";
2683
4478
  var DenoTestAdapter = class {
2684
4479
  name = "deno:test";
2685
4480
  async detect(ctx) {
@@ -2696,7 +4491,7 @@ var DenoTestAdapter = class {
2696
4491
  const startTime = Date.now();
2697
4492
  const command = ["deno", "test", "--allow-all", ...ctx.testFiles || []];
2698
4493
  return new Promise((resolve3) => {
2699
- const child = spawn7(command[0], command.slice(1), {
4494
+ const child = spawn9(command[0], command.slice(1), {
2700
4495
  cwd: ctx.packagePath,
2701
4496
  env: {
2702
4497
  ...process.env,
@@ -2784,18 +4579,29 @@ var DenoTestAdapter = class {
2784
4579
 
2785
4580
  // src/cli.ts
2786
4581
  import semver4 from "semver";
2787
- async function promptConfirm(question, defaultYes = false) {
2788
- const rl = createInterface({
2789
- input: process.stdin,
2790
- output: process.stdout
2791
- });
2792
- try {
2793
- const answer = (await rl.question(question)).trim().toLowerCase();
2794
- if (!answer) return defaultYes;
2795
- return answer === "y" || answer === "yes";
2796
- } finally {
2797
- rl.close();
4582
+ var RELEASE_CHANNELS = /* @__PURE__ */ new Set(["stable", "alpha", "beta", "rc", "canary"]);
4583
+ function resolveReleaseChannel(requestedChannel, configuredChannel, packageVersions) {
4584
+ const explicitChannel = requestedChannel || configuredChannel;
4585
+ if (explicitChannel) {
4586
+ if (!RELEASE_CHANNELS.has(explicitChannel)) {
4587
+ throw new Error(
4588
+ `Unsupported release channel "${explicitChannel}". Choose stable, alpha, beta, rc, or canary.`
4589
+ );
4590
+ }
4591
+ return explicitChannel;
4592
+ }
4593
+ const prereleaseChannels = new Set(
4594
+ packageVersions.map((version2) => semver4.parse(version2)?.prerelease[0]).filter((channel) => typeof channel === "string")
4595
+ );
4596
+ if (prereleaseChannels.size === 1) {
4597
+ return [...prereleaseChannels][0];
4598
+ }
4599
+ if (prereleaseChannels.size > 1) {
4600
+ throw new Error(
4601
+ `Packages are on multiple prerelease channels (${[...prereleaseChannels].join(", ")}). Choose an explicit channel with --channel.`
4602
+ );
2798
4603
  }
4604
+ return "stable";
2799
4605
  }
2800
4606
  var VereleaseCli = class {
2801
4607
  cwd;
@@ -2803,15 +4609,30 @@ var VereleaseCli = class {
2803
4609
  this.cwd = resolve2(options.cwd || process.cwd());
2804
4610
  }
2805
4611
  async createPlan(options = {}) {
4612
+ const isInteractive = process.stdout.isTTY && !options.json;
4613
+ const spin = isInteractive ? prompts3.spinner() : void 0;
4614
+ spin?.start("Resolving workspace package graph");
2806
4615
  const config = await loadConfig(this.cwd);
2807
4616
  const resolver = new DefaultWorkspaceResolver();
2808
4617
  const discovery = await resolver.resolve(this.cwd);
4618
+ const channel = resolveReleaseChannel(
4619
+ options.channel,
4620
+ config.versioning?.channel,
4621
+ discovery.packages.map((pkg) => pkg.version)
4622
+ );
4623
+ spin?.stop(`Discovered ${colors3.bold(discovery.packages.length)} workspace package${discovery.packages.length === 1 ? "" : "s"}`);
4624
+ for (const skippedPackage of discovery.skipped ?? []) {
4625
+ console.log(
4626
+ `${colors3.dim("skipped")} ${skippedPackage.name ?? skippedPackage.directory}: ${skippedPackage.reason}`
4627
+ );
4628
+ }
2809
4629
  const contractStore = new FileContractStore(
2810
- join13(this.cwd, config.contracts?.directory || ".verelease/contracts")
4630
+ join19(this.cwd, config.contracts?.directory || ".verelease/contracts")
2811
4631
  );
2812
4632
  const contractAnalyzer = new ContractCompatibilityAnalyzer(contractStore);
2813
4633
  const apiDiffAnalyzer = new ApiDiffAnalyzer();
2814
4634
  const baselineFetcher = new BaselineFetcher();
4635
+ const gitBaselineResolver = new GitBaselineResolver(this.cwd);
2815
4636
  const testSuiteAnalyzer = config.tests?.skip ? null : new TestSuiteAnalyzer({
2816
4637
  adapters: [
2817
4638
  new VitestTestAdapter(),
@@ -2824,12 +4645,32 @@ var VereleaseCli = class {
2824
4645
  });
2825
4646
  const semverEngine = new DefaultSemVerDecisionEngine();
2826
4647
  const npmPublisher = new NpmPublishProvider();
4648
+ const cargoPublisher = new CargoPublishProvider();
4649
+ const cargoSemVerAnalyzer = new CargoSemVerAnalyzer();
2827
4650
  const evidenceMap = /* @__PURE__ */ new Map();
4651
+ const comparedAgainst = /* @__PURE__ */ new Map();
2828
4652
  const initialBumps = /* @__PURE__ */ new Map();
2829
- const baselineTmpDir = await mkdtemp5(join13(tmpdir5(), "verelease-baseline-"));
4653
+ const baselineTmpDir = await mkdtemp7(join19(tmpdir7(), "verelease-baseline-"));
2830
4654
  try {
4655
+ spin?.start("Evaluating compatibility evidence (API diffs, tests & contracts)");
4656
+ const planGit = new GitManager(this.cwd);
2831
4657
  for (const pkg of discovery.packages) {
2832
4658
  const evidence = [];
4659
+ const releaseRef = await planGit.lastReleaseTag(pkg.name) ?? await planGit.latestReleaseTag();
4660
+ if (releaseRef) {
4661
+ const changed = await planGit.changedFilesSince(releaseRef, pkg.directory);
4662
+ if (changed.length === 0) {
4663
+ evidenceMap.set(pkg.name, []);
4664
+ initialBumps.set(pkg.name, {
4665
+ name: pkg.name,
4666
+ currentVersion: pkg.version,
4667
+ proposedVersion: pkg.version,
4668
+ bump: "none",
4669
+ reasons: [`Unchanged since ${releaseRef}.`]
4670
+ });
4671
+ continue;
4672
+ }
4673
+ }
2833
4674
  try {
2834
4675
  const contractEv = await contractAnalyzer.analyze({
2835
4676
  packageName: pkg.name,
@@ -2839,23 +4680,58 @@ var VereleaseCli = class {
2839
4680
  evidence.push(...contractEv);
2840
4681
  } catch {
2841
4682
  }
2842
- let baselineResult = null;
2843
- try {
2844
- if (!pkg.isPrivate) {
2845
- baselineResult = await baselineFetcher.fetch(pkg.name, baselineTmpDir);
4683
+ if (pkg.ecosystem === "cargo") {
4684
+ try {
4685
+ const cargoEv = await cargoSemVerAnalyzer.analyze({
4686
+ packageName: pkg.name,
4687
+ packagePath: pkg.directory,
4688
+ currentVersion: pkg.version
4689
+ });
4690
+ evidence.push(...cargoEv);
4691
+ } catch {
4692
+ }
4693
+ } else {
4694
+ let baselinePath;
4695
+ let previousVersion;
4696
+ let releaseBaselineCleanup;
4697
+ let baselineOrigin;
4698
+ try {
4699
+ const fromHistory = await gitBaselineResolver.resolve(pkg.name, pkg.directory);
4700
+ if (fromHistory) {
4701
+ baselinePath = fromHistory.entryPointPath;
4702
+ previousVersion = fromHistory.ref;
4703
+ releaseBaselineCleanup = fromHistory.cleanup;
4704
+ baselineOrigin = `git tag ${fromHistory.ref}`;
4705
+ }
4706
+ } catch {
4707
+ }
4708
+ if (!baselinePath && !pkg.isPrivate) {
4709
+ try {
4710
+ const fromRegistry = await baselineFetcher.fetch(pkg.name, baselineTmpDir);
4711
+ if (fromRegistry) {
4712
+ baselinePath = fromRegistry.entryPointPath;
4713
+ previousVersion = fromRegistry.version;
4714
+ baselineOrigin = `npm ${pkg.name}@${fromRegistry.version}`;
4715
+ }
4716
+ } catch {
4717
+ }
4718
+ }
4719
+ try {
4720
+ const apiEv = await apiDiffAnalyzer.analyze({
4721
+ packageName: pkg.name,
4722
+ packagePath: pkg.directory,
4723
+ currentVersion: pkg.version,
4724
+ baselinePath,
4725
+ previousVersion
4726
+ });
4727
+ evidence.push(...apiEv);
4728
+ } catch {
4729
+ } finally {
4730
+ await releaseBaselineCleanup?.().catch(() => void 0);
4731
+ }
4732
+ if (baselineOrigin) {
4733
+ comparedAgainst.set(pkg.name, baselineOrigin);
2846
4734
  }
2847
- } catch {
2848
- }
2849
- try {
2850
- const apiEv = await apiDiffAnalyzer.analyze({
2851
- packageName: pkg.name,
2852
- packagePath: pkg.directory,
2853
- currentVersion: pkg.version,
2854
- baselinePath: baselineResult?.entryPointPath,
2855
- previousVersion: baselineResult?.version
2856
- });
2857
- evidence.push(...apiEv);
2858
- } catch {
2859
4735
  }
2860
4736
  if (testSuiteAnalyzer) {
2861
4737
  try {
@@ -2874,14 +4750,13 @@ var VereleaseCli = class {
2874
4750
  pkg.version,
2875
4751
  evidence,
2876
4752
  config.versioning,
2877
- options.channel || "stable"
4753
+ channel
2878
4754
  );
2879
4755
  const forced = options.forceBump?.[pkg.name] || options.defaultBump;
2880
4756
  let bump = forced || decision.bump;
2881
4757
  let proposedVersion = decision.proposedVersion;
2882
4758
  const reasons = [...decision.reasons];
2883
4759
  if (forced && decision.bump === "none") {
2884
- const channel = options.channel || config.versioning?.channel || "stable";
2885
4760
  if (channel !== "stable") {
2886
4761
  const baseBump = `pre${forced}`;
2887
4762
  proposedVersion = semver4.inc(pkg.version, baseBump, channel) || pkg.version;
@@ -2892,10 +4767,20 @@ var VereleaseCli = class {
2892
4767
  }
2893
4768
  if (!pkg.isPrivate) {
2894
4769
  try {
2895
- const publishedVersions = await npmPublisher.getPublishedVersions(pkg.name);
2896
- const channel = options.channel || config.versioning?.channel || "stable";
2897
- if (publishedVersions.includes(proposedVersion) || bump !== "none" && publishedVersions.includes(pkg.version)) {
2898
- reasons.push(`Version ${proposedVersion} is already published on npm registry.`);
4770
+ let publishedVersions = [];
4771
+ const registryName = pkg.ecosystem === "cargo" ? "crates.io" : "npm registry";
4772
+ if (pkg.ecosystem === "cargo") {
4773
+ const res = await cargoPublisher.queryCratesIo(pkg.name);
4774
+ publishedVersions = res.versions;
4775
+ } else {
4776
+ publishedVersions = await npmPublisher.getPublishedVersions(pkg.name);
4777
+ }
4778
+ const proposedCollides = publishedVersions.includes(proposedVersion);
4779
+ const currentCollidesWithoutBump = bump === "none" && publishedVersions.includes(pkg.version);
4780
+ if (proposedCollides || currentCollidesWithoutBump) {
4781
+ reasons.push(
4782
+ proposedCollides ? `Version ${proposedVersion} is already published on ${registryName}.` : `Current version ${pkg.version} is already published on ${registryName}; a bump is required.`
4783
+ );
2899
4784
  let nextCandidate = proposedVersion;
2900
4785
  if (channel !== "stable") {
2901
4786
  const baseBump = bump === "none" ? "patch" : bump;
@@ -2922,6 +4807,14 @@ var VereleaseCli = class {
2922
4807
  } catch {
2923
4808
  }
2924
4809
  }
4810
+ const origin = comparedAgainst.get(pkg.name);
4811
+ if (origin) {
4812
+ reasons.push(`Compared against ${origin}.`);
4813
+ } else if (pkg.ecosystem !== "cargo") {
4814
+ reasons.push(
4815
+ "No previous release to compare against: this package has no release tag and is not published. Its first release cannot be inferred from an API diff."
4816
+ );
4817
+ }
2925
4818
  initialBumps.set(pkg.name, {
2926
4819
  name: pkg.name,
2927
4820
  currentVersion: pkg.version,
@@ -2930,10 +4823,12 @@ var VereleaseCli = class {
2930
4823
  reasons
2931
4824
  });
2932
4825
  }
4826
+ spin?.stop("Compatibility evidence evaluated");
2933
4827
  } finally {
2934
- await rm4(baselineTmpDir, { recursive: true, force: true }).catch(() => {
4828
+ await rm6(baselineTmpDir, { recursive: true, force: true }).catch(() => {
2935
4829
  });
2936
4830
  }
4831
+ spin?.start("Propagating dependency updates across workspace graph");
2937
4832
  const propagator = new VersionPropagator(discovery.graph);
2938
4833
  const { bumps: finalBumps, dependencyUpdates } = propagator.propagate(initialBumps);
2939
4834
  const publishOrder = discovery.graph.getTopologicalOrder();
@@ -2949,7 +4844,7 @@ var VereleaseCli = class {
2949
4844
  pkg.version,
2950
4845
  evidence,
2951
4846
  config.versioning,
2952
- options.channel || "stable"
4847
+ channel
2953
4848
  );
2954
4849
  packagePlans.push({
2955
4850
  name: pkg.name,
@@ -2957,11 +4852,12 @@ var VereleaseCli = class {
2957
4852
  currentVersion: pkg.version,
2958
4853
  proposedVersion: bumpTarget.proposedVersion,
2959
4854
  bump: bumpTarget.bump,
2960
- channel: options.channel || "stable",
4855
+ channel,
2961
4856
  evidence,
2962
4857
  reasons: bumpTarget.reasons,
2963
4858
  confidence: decision.confidence,
2964
4859
  requiresApproval: decision.requiresApproval,
4860
+ ...decision.blocked ? { blocked: true } : {},
2965
4861
  dependentUpdates: depUpdates.map((d) => ({
2966
4862
  dependentName: pkgName,
2967
4863
  dependencyName: d.dependencyName,
@@ -2970,15 +4866,17 @@ var VereleaseCli = class {
2970
4866
  newRange: d.newRange
2971
4867
  })),
2972
4868
  filesToUpdate: [pkg.manifestPath],
2973
- publish: !pkg.isPrivate && bumpTarget.bump !== "none"
4869
+ publish: !pkg.isPrivate && bumpTarget.bump !== "none",
4870
+ ecosystem: pkg.ecosystem
2974
4871
  });
2975
4872
  }
4873
+ spin?.stop(`Calculated release plan [Channel: ${colors3.cyan(channel)}]`);
2976
4874
  const plan = {
2977
4875
  schemaVersion: 1,
2978
4876
  id: `plan-${Date.now()}`,
2979
4877
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2980
4878
  rootDirectory: this.cwd,
2981
- channel: options.channel || "stable",
4879
+ channel,
2982
4880
  packages: packagePlans,
2983
4881
  publishOrder,
2984
4882
  summary: {
@@ -2993,6 +4891,11 @@ var VereleaseCli = class {
2993
4891
  return { plan, evidenceMap };
2994
4892
  }
2995
4893
  async runPlanCommand(options = {}) {
4894
+ if (!options.json) {
4895
+ prompts3.intro(
4896
+ colors3.cyan(colors3.bold("\u25C9 Verelease")) + colors3.dim(" evidence-based release plan")
4897
+ );
4898
+ }
2996
4899
  const { plan } = await this.createPlan(options);
2997
4900
  if (options.json) {
2998
4901
  console.log(JSON.stringify(plan, null, 2));
@@ -3000,198 +4903,269 @@ var VereleaseCli = class {
3000
4903
  }
3001
4904
  this.printPlan(plan);
3002
4905
  try {
3003
- const verelDir = join13(this.cwd, ".verelease");
4906
+ const verelDir = join19(this.cwd, ".verelease");
3004
4907
  await mkdir6(verelDir, { recursive: true });
3005
- await writeFile5(
3006
- join13(verelDir, "release-plan.json"),
4908
+ await writeFile6(
4909
+ join19(verelDir, "release-plan.json"),
3007
4910
  JSON.stringify(plan, null, 2),
3008
4911
  "utf8"
3009
4912
  );
3010
4913
  } catch {
3011
4914
  }
4915
+ prompts3.outro(
4916
+ plan.summary.bumpedPackages === 0 ? colors3.green("\u2728 All packages are up to date.") : colors3.cyan(
4917
+ `Plan ready: run ${colors3.bold(`verelease release --channel ${plan.channel}`)} to execute.`
4918
+ )
4919
+ );
3012
4920
  return plan;
3013
4921
  }
3014
4922
  async runReleaseCommand(options = {}) {
4923
+ if (!options.dryRun) {
4924
+ const git2 = new GitManager(this.cwd);
4925
+ let isClean;
4926
+ try {
4927
+ isClean = await git2.isClean();
4928
+ } catch (error) {
4929
+ throw new Error(
4930
+ "Cannot verify that the release working tree is clean. Run verelease from a Git repository before publishing.",
4931
+ { cause: error }
4932
+ );
4933
+ }
4934
+ if (!isClean) {
4935
+ throw new Error(
4936
+ "Refusing to release from a dirty working tree. Commit, stash, or remove all changes before publishing."
4937
+ );
4938
+ }
4939
+ }
4940
+ if (!options.json) {
4941
+ prompts3.intro(
4942
+ colors3.cyan(colors3.bold("\u25C9 Verelease")) + colors3.dim(
4943
+ ` release pipeline [${colors3.bold(options.channel || "auto")}]` + (options.dryRun ? colors3.yellow(" (DRY RUN)") : "")
4944
+ )
4945
+ );
4946
+ }
3015
4947
  const { plan, evidenceMap } = await this.createPlan(options);
3016
- console.log(`
3017
- \u{1F680} Verelease Pipeline [Channel: ${plan.channel}, Dry-run: ${!!options.dryRun}]
3018
- `);
3019
4948
  this.printPlan(plan);
3020
4949
  if (plan.summary.bumpedPackages === 0) {
3021
- console.log("\n\u2728 No packages require release.");
4950
+ prompts3.outro(colors3.green("\u2728 No packages require release."));
3022
4951
  return;
3023
4952
  }
3024
4953
  const publishable = plan.packages.filter((p) => p.publish && p.bump !== "none");
3025
4954
  const internalBumps = plan.packages.filter((p) => !p.publish && p.bump !== "none");
3026
- const promptDescription = publishable.length > 0 ? `publishing ${publishable.map((p) => `${p.name}@${p.proposedVersion}`).join(", ")}` + (internalBumps.length > 0 ? ` and bumping ${internalBumps.length} internal package(s)` : "") : `bumping ${internalBumps.length} internal package(s)`;
3027
- const shouldPrompt = !options.dryRun && !options.yes && !process.env.CI && process.stdin.isTTY;
3028
- if (shouldPrompt) {
3029
- const confirmed = await promptConfirm(
3030
- `
3031
- ? Proceed with ${promptDescription}? [y/N] `
3032
- );
3033
- if (!confirmed) {
3034
- console.log("\n\u274C Release cancelled by user. No manifests, lockfiles, or packages were modified.\n");
4955
+ const promptDescription = publishable.length > 0 ? `publishing ${publishable.map((p) => colors3.cyan(`${p.name}@${p.proposedVersion}`)).join(", ")}` + (internalBumps.length > 0 ? ` and bumping ${internalBumps.length} internal package(s)` : "") : `bumping ${internalBumps.length} internal package(s)`;
4956
+ const isInteractive = !options.dryRun && !options.yes && !process.env.CI && process.stdin.isTTY;
4957
+ const npmPublisher = new NpmPublishProvider();
4958
+ const cargoPublisher = new CargoPublishProvider();
4959
+ const hasNpmPackages = publishable.some((p) => p.ecosystem !== "cargo");
4960
+ const hasCargoPackages = publishable.some((p) => p.ecosystem === "cargo");
4961
+ if (publishable.length > 0 && !options.dryRun) {
4962
+ if (hasNpmPackages) {
4963
+ const authStatus = await npmPublisher.checkAuth();
4964
+ if (!authStatus.authenticated) {
4965
+ if (isInteractive) {
4966
+ await npmPublisher.ensureAuth({ interactive: true });
4967
+ } else {
4968
+ throw new NpmAuthError({
4969
+ message: "You are not logged in to npm. Run 'npm login' or configure NPM_TOKEN / NODE_AUTH_TOKEN in CI."
4970
+ });
4971
+ }
4972
+ }
4973
+ }
4974
+ if (hasCargoPackages) {
4975
+ const authStatus = await cargoPublisher.checkAuth();
4976
+ if (!authStatus.authenticated) {
4977
+ if (isInteractive) {
4978
+ await cargoPublisher.ensureAuth({ interactive: true });
4979
+ } else {
4980
+ throw new CargoAuthError({
4981
+ message: "Missing crates.io token. Set CARGO_REGISTRY_TOKEN or configure credentials."
4982
+ });
4983
+ }
4984
+ }
4985
+ }
4986
+ }
4987
+ if (isInteractive) {
4988
+ const confirmed = await prompts3.confirm({
4989
+ message: `Proceed with ${promptDescription}?`,
4990
+ initialValue: true
4991
+ });
4992
+ if (prompts3.isCancel(confirmed) || !confirmed) {
4993
+ prompts3.cancel("Release cancelled by user. No manifests, lockfiles, or packages were modified.");
3035
4994
  return;
3036
4995
  }
3037
4996
  }
3038
- const mutator = new TransactionalManifestMutator();
4997
+ if (options.dryRun) {
4998
+ prompts3.log.info(colors3.yellow("DRY RUN: Manifest mutations, changelogs, git tags, and registry publishing skipped."));
4999
+ prompts3.outro(colors3.green("Dry run completed successfully."));
5000
+ return;
5001
+ }
3039
5002
  const lockfileAdapter = new AutoLockfileAdapter();
3040
- const npmPublisher = new NpmPublishProvider();
3041
5003
  const ai = new TanStackReleaseAI();
3042
5004
  const git = new GitManager(this.cwd);
3043
- let transaction;
3044
- const changelogBackups = /* @__PURE__ */ new Map();
3045
- if (!options.dryRun) {
3046
- console.log("\n\u{1F4E6} Applying version bumps to manifests...");
3047
- transaction = await mutator.applyPlan(plan);
3048
- console.log(`\u2713 Updated ${transaction.modifiedFiles.length} manifest files`);
3049
- console.log(`
3050
- \u{1F512} Regenerating lockfile using ${lockfileAdapter.packageManager}...`);
3051
- const lockRes = await lockfileAdapter.regenerateLockfile(this.cwd);
3052
- if (!lockRes.success) {
3053
- console.error(`\u274C Lockfile regeneration failed: ${lockRes.output}`);
3054
- await transaction.rollback();
3055
- console.log("\u21A9 Rolled back manifest updates");
3056
- process.exit(1);
3057
- }
3058
- console.log("\u2713 Lockfile regenerated successfully");
3059
- console.log("\n\u{1F916} Generating changelogs & migration documentation via AI layer...");
3060
- for (const pkgPlan of plan.packages) {
3061
- if (pkgPlan.bump === "none") continue;
3062
- const evidence = evidenceMap.get(pkgPlan.name) || [];
3063
- const changelog = await ai.generateChangelog({
3064
- packageName: pkgPlan.name,
3065
- previousVersion: pkgPlan.currentVersion,
3066
- newVersion: pkgPlan.proposedVersion,
3067
- evidence
3068
- });
3069
- const changelogPath = join13(pkgPlan.directory, "CHANGELOG.md");
3070
- let existingChangelog = null;
3071
- try {
3072
- existingChangelog = await readFile7(changelogPath, "utf8");
3073
- } catch {
3074
- existingChangelog = null;
5005
+ const spin = process.stdout.isTTY ? prompts3.spinner() : void 0;
5006
+ const releaseProgram = executeReleaseTransaction(
5007
+ {
5008
+ plan,
5009
+ cwd: this.cwd,
5010
+ lockfileAdapter,
5011
+ onStep: (msg) => {
5012
+ spin?.message(msg);
5013
+ },
5014
+ onRollback: (msg) => {
5015
+ prompts3.log.warn(colors3.yellow(`\u21A9 ${msg}`));
3075
5016
  }
3076
- changelogBackups.set(changelogPath, existingChangelog);
3077
- await writeFile5(
3078
- changelogPath,
3079
- `${changelog.markdown}
5017
+ },
5018
+ (scope) => Effect4.gen(function* () {
5019
+ spin?.start("Generating changelogs & migration documentation via AI layer");
5020
+ for (const pkgPlan of plan.packages) {
5021
+ if (pkgPlan.bump === "none") continue;
5022
+ const evidence = evidenceMap.get(pkgPlan.name) || [];
5023
+ const changelog = yield* Effect4.promise(
5024
+ () => ai.generateChangelog({
5025
+ packageName: pkgPlan.name,
5026
+ previousVersion: pkgPlan.currentVersion,
5027
+ newVersion: pkgPlan.proposedVersion,
5028
+ evidence
5029
+ })
5030
+ );
5031
+ const changelogPath = join19(pkgPlan.directory, "CHANGELOG.md");
5032
+ let existingChangelog = null;
5033
+ try {
5034
+ existingChangelog = yield* Effect4.promise(() => readFile14(changelogPath, "utf8"));
5035
+ } catch {
5036
+ existingChangelog = null;
5037
+ }
5038
+ scope.backupChangelog(changelogPath, existingChangelog);
5039
+ yield* Effect4.promise(
5040
+ () => writeFile6(
5041
+ changelogPath,
5042
+ `${changelog.markdown}
3080
5043
 
3081
5044
  ${existingChangelog || ""}`.trim() + "\n",
3082
- "utf8"
3083
- );
3084
- console.log(` \u2713 Updated ${pkgPlan.name} CHANGELOG.md`);
3085
- if (pkgPlan.bump === "major") {
3086
- const breakingEvidence = evidence.filter((e) => e.severity === "major");
3087
- const guide = await ai.generateMigrationGuide({
3088
- packageName: pkgPlan.name,
3089
- previousVersion: pkgPlan.currentVersion,
3090
- newVersion: pkgPlan.proposedVersion,
3091
- breakingChanges: breakingEvidence
3092
- });
3093
- const migrationsDir = join13(pkgPlan.directory, "docs", "migrations");
3094
- await mkdir6(migrationsDir, { recursive: true });
3095
- const guidePath = join13(
3096
- migrationsDir,
3097
- `${pkgPlan.currentVersion.split(".")[0]}-to-${pkgPlan.proposedVersion.split(".")[0]}.md`
5045
+ "utf8"
5046
+ )
3098
5047
  );
3099
- await writeFile5(guidePath, guide.markdown, "utf8");
3100
- console.log(` \u2713 Generated migration guide: ${guidePath}`);
3101
- }
3102
- }
3103
- if (options.tag) {
3104
- try {
3105
- console.log("\n\u{1F3F7}\uFE0F Creating Git release commit and tags...");
3106
- await git.createReleaseCommit([], `chore(release): publish version ${plan.id}`);
3107
- for (const pkgPlan of plan.packages) {
3108
- if (pkgPlan.publish) {
3109
- const tagName = git.formatTagName({
5048
+ if (pkgPlan.bump === "major") {
5049
+ const breakingEvidence = evidence.filter((e) => e.severity === "major");
5050
+ const guide = yield* Effect4.promise(
5051
+ () => ai.generateMigrationGuide({
3110
5052
  packageName: pkgPlan.name,
3111
- version: pkgPlan.proposedVersion
3112
- });
3113
- await git.createTag(tagName);
3114
- console.log(` \u2713 Created git tag: ${tagName}`);
3115
- }
5053
+ previousVersion: pkgPlan.currentVersion,
5054
+ newVersion: pkgPlan.proposedVersion,
5055
+ breakingChanges: breakingEvidence
5056
+ })
5057
+ );
5058
+ const migrationsDir = join19(pkgPlan.directory, "docs", "migrations");
5059
+ yield* Effect4.promise(async () => {
5060
+ await mkdir6(migrationsDir, { recursive: true });
5061
+ const guidePath = join19(
5062
+ migrationsDir,
5063
+ `${pkgPlan.currentVersion.split(".")[0]}-to-${pkgPlan.proposedVersion.split(".")[0]}.md`
5064
+ );
5065
+ await writeFile6(guidePath, guide.markdown, "utf8");
5066
+ scope.trackGeneratedFile(guidePath);
5067
+ });
3116
5068
  }
3117
- } catch (err) {
3118
- console.warn(` \u26A0\uFE0F Git tagging warning: ${err.message || String(err)}`);
3119
5069
  }
3120
- }
3121
- } else {
3122
- console.log("\n[DRY RUN] Skipped manifest, lockfile, changelog mutations, and git tags.");
3123
- }
3124
- console.log("\n\u{1F4E4} Publishing packages in dependency order:");
3125
- for (const pkgName of plan.publishOrder) {
3126
- const pkgPlan = plan.packages.find((p) => p.name === pkgName);
3127
- if (!pkgPlan || !pkgPlan.publish) continue;
3128
- console.log(`
3129
- \u2192 Publishing ${pkgPlan.name}@${pkgPlan.proposedVersion}...`);
3130
- const result = await npmPublisher.publish({
3131
- packagePlan: pkgPlan,
3132
- fullPlan: plan,
3133
- dryRun: !!options.dryRun,
3134
- extraArgs: options.otp ? ["--otp", options.otp] : void 0
3135
- });
3136
- if (result.published) {
3137
- console.log(` \u2713 ${pkgPlan.name}@${pkgPlan.proposedVersion} published [tag: ${result.distTag}]`);
3138
- } else {
3139
- console.error(` \u274C Failed to publish ${pkgPlan.name}: ${result.error}`);
3140
- if (!options.dryRun) {
3141
- console.log("\n\u21A9 Rolling back manifest updates due to publish failure...");
3142
- if (transaction) {
3143
- await transaction.rollback();
3144
- console.log(" \u2713 Restored manifests to previous versions");
5070
+ spin?.stop("Changelogs and documentation generated");
5071
+ spin?.start("Publishing packages to registries");
5072
+ for (const pkgName of plan.publishOrder) {
5073
+ const pkgPlan = plan.packages.find((p) => p.name === pkgName);
5074
+ if (!pkgPlan || !pkgPlan.publish) continue;
5075
+ const isCargo = pkgPlan.ecosystem === "cargo";
5076
+ const registryLabel = isCargo ? "crates.io" : "npm registry";
5077
+ spin?.message(`Publishing ${colors3.bold(pkgPlan.name)}@${pkgPlan.proposedVersion} to ${registryLabel}`);
5078
+ const result = yield* Effect4.promise(
5079
+ () => isCargo ? cargoPublisher.publish({
5080
+ packagePlan: pkgPlan,
5081
+ fullPlan: plan,
5082
+ dryRun: !!options.dryRun
5083
+ }) : npmPublisher.publish({
5084
+ packagePlan: pkgPlan,
5085
+ fullPlan: plan,
5086
+ dryRun: !!options.dryRun,
5087
+ extraArgs: options.otp ? ["--otp", options.otp] : void 0
5088
+ })
5089
+ );
5090
+ if (!result.published) {
5091
+ return yield* Effect4.fail(
5092
+ new PublishError({
5093
+ packageName: pkgPlan.name,
5094
+ message: result.error || `${registryLabel} publish failed`
5095
+ })
5096
+ );
3145
5097
  }
3146
- for (const [chPath, original] of changelogBackups) {
5098
+ prompts3.log.success(
5099
+ colors3.green(
5100
+ `Published ${colors3.bold(pkgPlan.name)}@${pkgPlan.proposedVersion} [${registryLabel}]`
5101
+ )
5102
+ );
5103
+ }
5104
+ spin?.stop("All packages published successfully");
5105
+ if (options.tag) {
5106
+ spin?.start("Creating Git release commit and tags");
5107
+ yield* Effect4.promise(async () => {
3147
5108
  try {
3148
- if (original === null) {
3149
- await unlink(chPath);
3150
- } else {
3151
- await writeFile5(chPath, original, "utf8");
5109
+ await git.createReleaseCommit([], `chore(release): publish version ${plan.id}`);
5110
+ for (const pkgPlan of plan.packages) {
5111
+ if (pkgPlan.publish) {
5112
+ const tagName = git.formatTagName({
5113
+ packageName: pkgPlan.name,
5114
+ version: pkgPlan.proposedVersion
5115
+ });
5116
+ await git.createTag(tagName);
5117
+ }
3152
5118
  }
3153
- } catch {
5119
+ } catch (err) {
5120
+ prompts3.log.warn(`Git tagging warning: ${err.message || String(err)}`);
3154
5121
  }
3155
- }
3156
- console.log(" \u2713 Restored changelog files");
3157
- await lockfileAdapter.regenerateLockfile(this.cwd);
3158
- console.log(" \u2713 Synchronized lockfile\n");
3159
- process.exit(1);
5122
+ });
5123
+ spin?.stop("Git commit and tags created");
3160
5124
  }
3161
- }
5125
+ })
5126
+ );
5127
+ const exit = await Effect4.runPromiseExit(releaseProgram);
5128
+ if (Exit2.isFailure(exit)) {
5129
+ prompts3.log.error(
5130
+ colors3.red("Publish failed. Transactional rollback has restored all files to their clean state.")
5131
+ );
5132
+ process.exit(1);
3162
5133
  }
3163
- console.log("\n\u{1F389} Release completed successfully!");
5134
+ prompts3.outro(colors3.green(colors3.bold("\u{1F389} Release completed successfully!")));
3164
5135
  }
3165
5136
  async runExplainCommand(packageName) {
5137
+ prompts3.intro(
5138
+ colors3.cyan(colors3.bold("\u25C9 Verelease")) + colors3.dim(` explanation for ${colors3.bold(packageName)}`)
5139
+ );
3166
5140
  const { plan, evidenceMap } = await this.createPlan();
3167
5141
  const pkgPlan = plan.packages.find((p) => p.name === packageName);
3168
5142
  if (!pkgPlan) {
3169
- console.error(`Package "${packageName}" not found in workspace.`);
5143
+ prompts3.log.error(`Package "${packageName}" not found in workspace.`);
3170
5144
  return;
3171
5145
  }
3172
5146
  const evidence = evidenceMap.get(packageName) || [];
3173
- console.log(`
3174
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
3175
- console.log(` EXPLANATION FOR ${packageName}`);
3176
- console.log(`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3177
- `);
3178
- console.log(`Current Version: ${pkgPlan.currentVersion}`);
3179
- console.log(`Proposed Version: ${pkgPlan.proposedVersion} [${pkgPlan.bump.toUpperCase()}]`);
3180
- console.log(`Confidence: ${(pkgPlan.confidence * 100).toFixed(0)}%`);
3181
- console.log(`Requires Review: ${pkgPlan.requiresApproval ? "YES" : "NO"}
3182
- `);
3183
- console.log(`Evidence items (${evidence.length}):`);
5147
+ const badge = pkgPlan.bump === "major" ? colors3.red("\u{1F534} MAJOR") : pkgPlan.bump === "minor" ? colors3.yellow("\u{1F7E1} MINOR") : pkgPlan.bump === "patch" ? colors3.green("\u{1F7E2} PATCH") : colors3.dim("\u26AA NONE");
5148
+ const lines = [
5149
+ `${colors3.bold("Current Version:")} ${pkgPlan.currentVersion}`,
5150
+ `${colors3.bold("Proposed Version:")} ${colors3.cyan(colors3.bold(pkgPlan.proposedVersion))} [${badge}]`,
5151
+ `${colors3.bold("Confidence:")} ${(pkgPlan.confidence * 100).toFixed(0)}%`,
5152
+ `${colors3.bold("Requires Review:")} ${pkgPlan.requiresApproval ? colors3.yellow("YES") : colors3.green("NO")}`,
5153
+ "",
5154
+ `${colors3.bold(`Evidence Items (${evidence.length}):`)}`
5155
+ ];
3184
5156
  for (const ev of evidence) {
3185
- console.log(` \u2022 [${ev.source.toUpperCase()}] (${ev.severity.toUpperCase()}) ${ev.title}`);
3186
- console.log(` ${ev.description}
3187
- `);
5157
+ lines.push(` \u2022 [${colors3.cyan(ev.source.toUpperCase())}] (${colors3.bold(ev.severity.toUpperCase())}) ${ev.title}`);
5158
+ lines.push(` ${colors3.dim(ev.description)}`);
3188
5159
  }
3189
5160
  if (pkgPlan.reasons.length > 0) {
3190
- console.log("Decision reasons:");
5161
+ lines.push("");
5162
+ lines.push(colors3.bold("Decision Reasons:"));
3191
5163
  for (const reason of pkgPlan.reasons) {
3192
- console.log(` ${reason}`);
5164
+ lines.push(` \u2022 ${reason}`);
3193
5165
  }
3194
5166
  }
5167
+ prompts3.note(lines.join("\n"), `Evidence Breakdown`);
5168
+ prompts3.outro("Explanation complete.");
3195
5169
  }
3196
5170
  async runGuideCommand(options = {}) {
3197
5171
  if (options.json) {
@@ -3199,18 +5173,19 @@ ${existingChangelog || ""}`.trim() + "\n",
3199
5173
  JSON.stringify(
3200
5174
  {
3201
5175
  name: "verelease",
3202
- description: "Compatibility-aware SemVer release engine",
5176
+ description: "Universal compatibility-aware SemVer release engine",
5177
+ supportedEcosystems: ["npm", "cargo"],
3203
5178
  rules: [
3204
- "Never manually edit 'version' in package.json",
5179
+ "Never manually edit 'version' in package.json or Cargo.toml",
3205
5180
  "SemVer bumps are calculated from contract tests, API diffs, and dependency ranges",
3206
5181
  "Pass --yes to bypass interactive prompts in CI"
3207
5182
  ],
3208
5183
  commands: {
3209
- plan: "verel plan [--channel <channel>] [--json]",
3210
- release: "verel release [--channel <channel>] [-y, --yes]",
3211
- explain: "verel explain <pkg>",
3212
- guide: "verel guide [--json]",
3213
- init: "verel init"
5184
+ plan: "verelease plan [--channel <channel>] [--json]",
5185
+ release: "verelease release [--channel <channel>] [-y, --yes]",
5186
+ explain: "verelease explain <pkg>",
5187
+ guide: "verelease guide [--json]",
5188
+ init: "verelease init"
3214
5189
  },
3215
5190
  configuration: "verelease.config.ts",
3216
5191
  contractsDirectory: ".verelease/contracts",
@@ -3222,37 +5197,33 @@ ${existingChangelog || ""}`.trim() + "\n",
3222
5197
  );
3223
5198
  return;
3224
5199
  }
3225
- console.log(`
3226
- # Verelease Agent Guide
3227
-
3228
- Verelease is an evidence-based SemVer release engine for JavaScript & TypeScript.
3229
-
3230
- ## Core Rules for AI Coding Agents
3231
- 1. **NEVER manually edit "version" in package.json**: Always let Verelease determine versions.
3232
- 2. **Inspect first**: Run \`pnpm verel plan\` to preview proposed version bumps without modifying files.
3233
- 3. **Diagnose bumps**: Run \`pnpm verel explain <pkg>\` to understand the evidence behind any bump.
3234
- 4. **Automated releases**: In CI or automated workflows, run \`pnpm verel release --yes\` to bypass prompts.
3235
- 5. **Interactive releases**: When run locally, Verelease asks for [y/N] confirmation and supports Touch ID / WebAuthn.
3236
-
3237
- ## Command Reference
3238
- - \`pnpm verel plan\` Preview release plan for stable channel
3239
- - \`pnpm verel plan --channel alpha\` Preview pre-release bumps for alpha channel
3240
- - \`pnpm verel release\` Interactive release pipeline
3241
- - \`pnpm verel release -y\` Non-interactive release (for agents/CI)
3242
- - \`pnpm verel explain <package>\` Inspect compatibility evidence and decision reasons
3243
- - \`pnpm verel init\` Initialize config, contracts, and AGENTS.md
3244
-
3245
- ## Configuration
3246
- Config is loaded from \`verelease.config.ts\` or \`verelease.config.js\`.
3247
- Contract test suites live in \`.verelease/contracts/\`.
3248
- `);
5200
+ prompts3.intro(colors3.cyan(colors3.bold("\u25C9 Verelease")) + colors3.dim(" agent guide"));
5201
+ const content = [
5202
+ colors3.bold("Core Rules for AI Coding Agents:"),
5203
+ `1. ${colors3.bold("NEVER manually edit 'version' in package.json or Cargo.toml")}: Always let Verelease compute and bump versions based on compatibility evidence.`,
5204
+ `2. ${colors3.bold("Inspect first")}: Run ${colors3.cyan("verelease plan")} to preview proposed version bumps across npm and Cargo packages.`,
5205
+ `3. ${colors3.bold("Diagnose bumps")}: Run ${colors3.cyan("verelease explain <pkg>")} to inspect compatibility evidence (TypeScript AST diffs, Rust semver-checks, test suites).`,
5206
+ `4. ${colors3.bold("Automated releases")}: In CI or agents, run ${colors3.cyan("verelease release --yes")} to bypass prompts.`,
5207
+ `5. ${colors3.bold("Interactive releases")}: Locally, Verelease prompts [y/N] and supports npm 2FA & crates.io tokens.`,
5208
+ "",
5209
+ colors3.bold("Command Reference:"),
5210
+ `\u2022 ${colors3.cyan("verelease plan")} Preview release plan for stable channel`,
5211
+ `\u2022 ${colors3.cyan("verelease plan --channel alpha")} Preview pre-release bumps for alpha channel`,
5212
+ `\u2022 ${colors3.cyan("verelease release")} Interactive release pipeline`,
5213
+ `\u2022 ${colors3.cyan("verelease release -y")} Non-interactive release (for agents/CI)`,
5214
+ `\u2022 ${colors3.cyan("verelease explain <package>")} Inspect evidence and decision reasons`,
5215
+ `\u2022 ${colors3.cyan("verelease init")} Initialize config, contracts, and AGENTS.md`
5216
+ ].join("\n");
5217
+ prompts3.note(content, "Universal Agent Guide & Workflow");
5218
+ prompts3.outro("Guide complete.");
3249
5219
  }
3250
5220
  async runInitCommand(options = {}) {
3251
- const configPath = join13(this.cwd, "verelease.config.ts");
3252
- const contractsDir = join13(this.cwd, ".verelease", "contracts");
3253
- const agentsPath = join13(this.cwd, "AGENTS.md");
3254
- const pkgJsonPath = join13(this.cwd, "package.json");
3255
- console.log("\n\u{1F680} Initializing Verelease in current project...\n");
5221
+ const configPath = join19(this.cwd, "verelease.config.ts");
5222
+ const contractsDir = join19(this.cwd, ".verelease", "contracts");
5223
+ const agentsPath = join19(this.cwd, "AGENTS.md");
5224
+ const gitignorePath = join19(this.cwd, ".gitignore");
5225
+ const pkgJsonPath = join19(this.cwd, "package.json");
5226
+ prompts3.intro(colors3.cyan(colors3.bold("\u25C9 Verelease")) + colors3.dim(" project initialization"));
3256
5227
  if (!existsSync(configPath)) {
3257
5228
  const sampleConfig = `import { defineConfig } from "verelease/config";
3258
5229
 
@@ -3275,21 +5246,48 @@ export default defineConfig({
3275
5246
  },
3276
5247
  });
3277
5248
  `;
3278
- await writeFile5(configPath, sampleConfig, "utf8");
3279
- console.log(" \u2713 Created verelease.config.ts");
5249
+ await writeFile6(configPath, sampleConfig, "utf8");
5250
+ prompts3.log.success(colors3.green("Created verelease.config.ts"));
3280
5251
  } else {
3281
- console.log(" \u2022 verelease.config.ts already exists");
5252
+ prompts3.log.info("verelease.config.ts already exists");
3282
5253
  }
3283
5254
  if (!existsSync(contractsDir)) {
3284
5255
  await mkdir6(contractsDir, { recursive: true });
3285
- await writeFile5(
3286
- join13(contractsDir, "README.md"),
5256
+ await writeFile6(
5257
+ join19(contractsDir, "README.md"),
3287
5258
  "# Verelease Contracts\n\nPlace consumer-driven contract definitions (.json or .yaml) here.\n",
3288
5259
  "utf8"
3289
5260
  );
3290
- console.log(" \u2713 Created .verelease/contracts/");
5261
+ prompts3.log.success(colors3.green("Created .verelease/contracts/"));
3291
5262
  } else {
3292
- console.log(" \u2022 .verelease/contracts/ already exists");
5263
+ prompts3.log.info(".verelease/contracts/ already exists");
5264
+ }
5265
+ const generatedIgnoreEntries = [
5266
+ ".verelease/*",
5267
+ "!.verelease/contracts/",
5268
+ ".vitest/"
5269
+ ];
5270
+ const existingGitignore = existsSync(gitignorePath) ? await readFile14(gitignorePath, "utf8") : "";
5271
+ const existingIgnoreLines = new Set(
5272
+ existingGitignore.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
5273
+ );
5274
+ const missingIgnoreEntries = generatedIgnoreEntries.filter(
5275
+ (entry) => !existingIgnoreLines.has(entry)
5276
+ );
5277
+ if (missingIgnoreEntries.length > 0) {
5278
+ const separator = existingGitignore.length === 0 ? "" : existingGitignore.endsWith("\n") ? "\n" : "\n\n";
5279
+ await writeFile6(
5280
+ gitignorePath,
5281
+ `${existingGitignore}${separator}# Verelease generated files
5282
+ ${missingIgnoreEntries.join("\n")}
5283
+ `,
5284
+ "utf8"
5285
+ );
5286
+ prompts3.log.success(
5287
+ colors3.green("Ignored generated Verelease and Vitest state in .gitignore")
5288
+ );
5289
+ } else {
5290
+ prompts3.log.info(".gitignore already excludes generated Verelease and Vitest state");
3293
5291
  }
3294
5292
  const agentGuidance = `
3295
5293
  ## Releases & Versioning (Verelease)
@@ -3301,24 +5299,24 @@ This repository uses [Verelease](https://github.com/ivanjeremic/verelease) for a
3301
5299
  - **NEVER manually edit "version" in package.json.**
3302
5300
  `;
3303
5301
  if (!existsSync(agentsPath)) {
3304
- await writeFile5(agentsPath, `# Agent Guidelines
5302
+ await writeFile6(agentsPath, `# Agent Guidelines
3305
5303
  ${agentGuidance}
3306
5304
  `, "utf8");
3307
- console.log(" \u2713 Created AGENTS.md with Verelease instructions");
5305
+ prompts3.log.success(colors3.green("Created AGENTS.md with Verelease instructions"));
3308
5306
  } else {
3309
- const currentContent = await readFile7(agentsPath, "utf8");
5307
+ const currentContent = await readFile14(agentsPath, "utf8");
3310
5308
  if (!currentContent.includes("Verelease")) {
3311
- await writeFile5(agentsPath, `${currentContent.trim()}
5309
+ await writeFile6(agentsPath, `${currentContent.trim()}
3312
5310
  ${agentGuidance}
3313
5311
  `, "utf8");
3314
- console.log(" \u2713 Appended Verelease instructions to AGENTS.md");
5312
+ prompts3.log.success(colors3.green("Appended Verelease instructions to AGENTS.md"));
3315
5313
  } else {
3316
- console.log(" \u2022 AGENTS.md already contains Verelease instructions");
5314
+ prompts3.log.info("AGENTS.md already contains Verelease instructions");
3317
5315
  }
3318
5316
  }
3319
5317
  if (existsSync(pkgJsonPath)) {
3320
5318
  try {
3321
- const pkg = JSON.parse(await readFile7(pkgJsonPath, "utf8"));
5319
+ const pkg = JSON.parse(await readFile14(pkgJsonPath, "utf8"));
3322
5320
  pkg.scripts = pkg.scripts || {};
3323
5321
  let modified = false;
3324
5322
  if (!pkg.scripts.plan) {
@@ -3330,127 +5328,237 @@ ${agentGuidance}
3330
5328
  modified = true;
3331
5329
  }
3332
5330
  if (modified) {
3333
- await writeFile5(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
3334
- console.log(" \u2713 Added 'plan' and 'release' scripts to package.json");
5331
+ await writeFile6(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5332
+ prompts3.log.success(colors3.green("Added 'plan' and 'release' scripts to package.json"));
3335
5333
  }
3336
5334
  } catch {
3337
5335
  }
3338
5336
  }
3339
- console.log("\n\u2728 Verelease successfully initialized!\n");
5337
+ prompts3.outro(colors3.green(colors3.bold("\u2728 Verelease successfully initialized!")));
3340
5338
  }
3341
5339
  printPlan(plan) {
3342
- console.log(`
3343
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
3344
- console.log(` VERELEASE RELEASE PLAN`);
3345
- console.log(` Channel: ${plan.channel} | Packages: ${plan.summary.totalPackages} | Bumps: ${plan.summary.bumpedPackages}`);
3346
- console.log(`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3347
- `);
5340
+ const lines = [];
3348
5341
  for (const pkg of plan.packages) {
3349
- const bumpColor = pkg.bump === "major" ? "\u{1F534} MAJOR" : pkg.bump === "minor" ? "\u{1F7E1} MINOR" : pkg.bump === "patch" ? "\u{1F7E2} PATCH" : "\u26AA NONE";
3350
- console.log(`${pkg.name}: ${pkg.currentVersion} \u2192 ${pkg.proposedVersion} [${bumpColor}]`);
3351
- console.log(` Confidence: ${(pkg.confidence * 100).toFixed(0)}% | Publish: ${pkg.publish ? "YES" : "NO"}`);
5342
+ const badge = pkg.bump === "major" ? colors3.red("\u{1F534} MAJOR") : pkg.bump === "minor" ? colors3.yellow("\u{1F7E1} MINOR") : pkg.bump === "patch" ? colors3.green("\u{1F7E2} PATCH") : colors3.dim("\u26AA NONE");
5343
+ const registryName = pkg.ecosystem === "cargo" ? "crates.io" : "npm";
5344
+ const pubTag = pkg.publish ? colors3.cyan(` [${registryName} publish]`) : colors3.dim(" [internal]");
5345
+ if (pkg.blocked) {
5346
+ lines.push(
5347
+ `${colors3.red("\u26D4 BLOCKED")} ${colors3.bold(pkg.name)} ${colors3.dim(
5348
+ `would be ${pkg.bump}`
5349
+ )} ${colors3.dim(`(${pkg.currentVersion} \u2192 ${pkg.proposedVersion})`)}${pubTag}`
5350
+ );
5351
+ } else {
5352
+ lines.push(
5353
+ `${badge} ${colors3.bold(pkg.name)} ${colors3.dim(pkg.currentVersion)} \u2192 ${colors3.cyan(colors3.bold(pkg.proposedVersion))}${pubTag}`
5354
+ );
5355
+ }
3352
5356
  if (pkg.reasons.length > 0) {
3353
- console.log(" Reasons:");
3354
- for (const reason of pkg.reasons) {
3355
- console.log(` ${reason}`);
5357
+ for (const reason of pkg.reasons.slice(0, 2)) {
5358
+ lines.push(` ${colors3.dim("\u2022")} ${colors3.dim(reason)}`);
3356
5359
  }
3357
5360
  }
3358
5361
  if (pkg.dependentUpdates.length > 0) {
3359
- console.log(" Dependent Updates:");
3360
- for (const dep of pkg.dependentUpdates) {
3361
- console.log(` ${dep.dependentName} depends on ${dep.dependencyName} (${dep.previousRange} \u2192 ${dep.newRange})`);
5362
+ for (const dep of pkg.dependentUpdates.slice(0, 2)) {
5363
+ lines.push(
5364
+ ` ${colors3.dim("\u21B3")} ${colors3.dim(`${dep.dependentName} depends on ${dep.dependencyName} (${dep.previousRange} \u2192 ${dep.newRange})`)}`
5365
+ );
3362
5366
  }
3363
5367
  }
3364
- console.log("");
5368
+ lines.push("");
3365
5369
  }
5370
+ const summaryLine = [
5371
+ `${colors3.bold(plan.summary.totalPackages)} packages`,
5372
+ `${colors3.green(colors3.bold(plan.summary.bumpedPackages))} bumps`,
5373
+ `(${colors3.green(`${plan.summary.patchBumps} patch`)}, ${colors3.yellow(`${plan.summary.minorBumps} minor`)}, ${colors3.red(`${plan.summary.majorBumps} major`)})`,
5374
+ `Channel: ${colors3.cyan(plan.channel)}`
5375
+ ].join(" | ");
5376
+ lines.push(summaryLine);
5377
+ prompts3.note(lines.join("\n"), `Release Plan Summary`);
3366
5378
  }
3367
5379
  };
3368
5380
 
3369
5381
  // src/bin.ts
3370
- async function main() {
3371
- const args = process.argv.slice(2);
3372
- const command = args[0] || "plan";
3373
- if (args.includes("--help") || args.includes("-h") || command === "help") {
3374
- console.log(`
3375
- Usage: verelease [command] [options]
3376
-
3377
- Commands:
3378
- plan Preview the release plan without making changes (default)
3379
- release Apply version bumps, generate changelogs, and publish
3380
- explain <pkg> Explain compatibility evidence and bump decision for a package
3381
- guide Show agent-optimized cheat sheet and machine-readable instructions
3382
- init Initialize verelease.config.ts, contracts, and AGENTS.md
3383
-
3384
- Options:
3385
- --channel <name> Release channel (e.g. stable, alpha, beta, rc) [default: stable]
3386
- --bump <type> Manual bump override (patch | minor | major)
3387
- --dry-run Simulate pipeline without modifying manifests or publishing
3388
- -y, --yes Skip interactive confirmation prompt
3389
- --otp <code> One-time password for npm 2FA
3390
- --tag Create git commit and tags for release
3391
- --json Output machine-readable JSON
3392
- -h, --help Show this help message
3393
- `);
3394
- return;
5382
+ var __dirname2 = dirname3(fileURLToPath(import.meta.url));
5383
+ async function getCliVersion() {
5384
+ try {
5385
+ const pkgJsonPath = join20(__dirname2, "../package.json");
5386
+ const content = await readFile15(pkgJsonPath, "utf8");
5387
+ const parsed = JSON.parse(content);
5388
+ return parsed.version || "0.1.0";
5389
+ } catch {
5390
+ return "0.1.0";
5391
+ }
5392
+ }
5393
+ var commonArgs = {
5394
+ channel: {
5395
+ type: "string",
5396
+ description: "Release channel (e.g. stable, alpha, beta, rc)"
5397
+ },
5398
+ bump: {
5399
+ type: "string",
5400
+ description: "Manual bump override (patch | minor | major)"
5401
+ },
5402
+ dryRun: {
5403
+ type: "boolean",
5404
+ description: "Simulate pipeline without modifying manifests or publishing"
5405
+ },
5406
+ yes: {
5407
+ type: "boolean",
5408
+ alias: "y",
5409
+ description: "Skip interactive confirmation prompt"
5410
+ },
5411
+ json: {
5412
+ type: "boolean",
5413
+ description: "Output machine-readable JSON"
5414
+ },
5415
+ tag: {
5416
+ type: "boolean",
5417
+ description: "Create git commit and tags for release"
5418
+ },
5419
+ otp: {
5420
+ type: "string",
5421
+ description: "One-time password for npm 2FA"
5422
+ },
5423
+ cwd: {
5424
+ type: "string",
5425
+ description: "Working directory path"
3395
5426
  }
3396
- const options = {
3397
- dryRun: args.includes("--dry-run"),
3398
- json: args.includes("--json"),
3399
- tag: args.includes("--tag"),
3400
- yes: args.includes("--yes") || args.includes("-y")
5427
+ };
5428
+ function toCliOptions(args) {
5429
+ return {
5430
+ channel: args.channel,
5431
+ defaultBump: args.bump,
5432
+ dryRun: Boolean(args.dryRun),
5433
+ yes: Boolean(args.yes),
5434
+ json: Boolean(args.json),
5435
+ tag: Boolean(args.tag),
5436
+ otp: args.otp,
5437
+ cwd: args.cwd
3401
5438
  };
3402
- const channelIdx = args.indexOf("--channel");
3403
- if (channelIdx !== -1 && args[channelIdx + 1]) {
3404
- options.channel = args[channelIdx + 1];
5439
+ }
5440
+ var subcommandExecuted = false;
5441
+ var planCommand = defineCommand({
5442
+ meta: {
5443
+ name: "plan",
5444
+ description: "Preview the release plan without making changes"
5445
+ },
5446
+ args: commonArgs,
5447
+ async run({ args }) {
5448
+ subcommandExecuted = true;
5449
+ const opts = toCliOptions(args);
5450
+ const cli = new VereleaseCli(opts);
5451
+ await cli.runPlanCommand(opts);
3405
5452
  }
3406
- const bumpIdx = args.indexOf("--bump");
3407
- if (bumpIdx !== -1 && args[bumpIdx + 1]) {
3408
- options.defaultBump = args[bumpIdx + 1];
5453
+ });
5454
+ var releaseCommand = defineCommand({
5455
+ meta: {
5456
+ name: "release",
5457
+ description: "Apply version bumps, generate changelogs, and publish"
5458
+ },
5459
+ args: commonArgs,
5460
+ async run({ args }) {
5461
+ subcommandExecuted = true;
5462
+ const opts = toCliOptions(args);
5463
+ const cli = new VereleaseCli(opts);
5464
+ await cli.runReleaseCommand(opts);
3409
5465
  }
3410
- const cwdIdx = args.indexOf("--cwd");
3411
- if (cwdIdx !== -1 && args[cwdIdx + 1]) {
3412
- options.cwd = args[cwdIdx + 1];
5466
+ });
5467
+ var versionCommand = defineCommand({
5468
+ meta: {
5469
+ name: "version",
5470
+ description: "Print the verelease version"
5471
+ },
5472
+ async run() {
5473
+ subcommandExecuted = true;
5474
+ console.log(version);
3413
5475
  }
3414
- const otpIdx = args.indexOf("--otp");
3415
- if (otpIdx !== -1 && args[otpIdx + 1]) {
3416
- options.otp = args[otpIdx + 1];
5476
+ });
5477
+ var explainCommand = defineCommand({
5478
+ meta: {
5479
+ name: "explain",
5480
+ description: "Explain compatibility evidence and bump decision for a package"
5481
+ },
5482
+ args: {
5483
+ package: {
5484
+ type: "positional",
5485
+ description: "Package name to explain (e.g. @verelease/core)",
5486
+ required: false
5487
+ },
5488
+ ...commonArgs
5489
+ },
5490
+ async run({ args }) {
5491
+ subcommandExecuted = true;
5492
+ const opts = toCliOptions(args);
5493
+ const cli = new VereleaseCli(opts);
5494
+ const targetPkg = args.package || args._ && args._[0];
5495
+ if (!targetPkg) {
5496
+ console.error("\n\u274C Please specify a package name to explain, e.g.: verelease explain @verelease/core\n");
5497
+ process.exit(1);
5498
+ }
5499
+ await cli.runExplainCommand(targetPkg);
3417
5500
  }
3418
- const cli = new VereleaseCli(options);
3419
- try {
3420
- switch (command) {
3421
- case "plan":
3422
- await cli.runPlanCommand(options);
3423
- break;
3424
- case "release":
3425
- await cli.runReleaseCommand(options);
3426
- break;
3427
- case "version":
3428
- await cli.runReleaseCommand({ ...options, dryRun: false });
3429
- break;
3430
- case "guide":
3431
- await cli.runGuideCommand(options);
3432
- break;
3433
- case "init":
3434
- await cli.runInitCommand(options);
3435
- break;
3436
- case "explain":
3437
- const targetPkg = args[1];
3438
- if (!targetPkg) {
3439
- console.error("Please specify a package name to explain, e.g.: verel explain @acme/core");
3440
- process.exit(1);
3441
- }
3442
- await cli.runExplainCommand(targetPkg);
3443
- break;
3444
- default:
3445
- console.log(`Unknown command: ${command}`);
3446
- console.log(`Run 'verelease --help' for available commands and options.`);
3447
- process.exit(1);
5501
+ });
5502
+ var guideCommand = defineCommand({
5503
+ meta: {
5504
+ name: "guide",
5505
+ description: "Show agent-optimized cheat sheet and machine-readable instructions"
5506
+ },
5507
+ args: {
5508
+ json: {
5509
+ type: "boolean",
5510
+ description: "Output machine-readable JSON"
5511
+ },
5512
+ cwd: {
5513
+ type: "string",
5514
+ description: "Working directory path"
3448
5515
  }
3449
- } catch (err) {
3450
- console.error(`
3451
- \u274C Error: ${err.message || String(err)}`);
3452
- process.exit(1);
5516
+ },
5517
+ async run({ args }) {
5518
+ subcommandExecuted = true;
5519
+ const cli = new VereleaseCli({ cwd: args.cwd });
5520
+ await cli.runGuideCommand({ json: Boolean(args.json) });
3453
5521
  }
3454
- }
3455
- main();
5522
+ });
5523
+ var initCommand = defineCommand({
5524
+ meta: {
5525
+ name: "init",
5526
+ description: "Initialize verelease.config.ts, contracts, and AGENTS.md"
5527
+ },
5528
+ args: {
5529
+ cwd: {
5530
+ type: "string",
5531
+ description: "Working directory path"
5532
+ }
5533
+ },
5534
+ async run({ args }) {
5535
+ subcommandExecuted = true;
5536
+ const cli = new VereleaseCli({ cwd: args.cwd });
5537
+ await cli.runInitCommand({ cwd: args.cwd });
5538
+ }
5539
+ });
5540
+ var version = await getCliVersion();
5541
+ var mainCommand = defineCommand({
5542
+ meta: {
5543
+ name: "verelease",
5544
+ version,
5545
+ description: "Evidence-based semantic versioning release engine"
5546
+ },
5547
+ args: commonArgs,
5548
+ subCommands: {
5549
+ plan: planCommand,
5550
+ release: releaseCommand,
5551
+ version: versionCommand,
5552
+ explain: explainCommand,
5553
+ guide: guideCommand,
5554
+ init: initCommand
5555
+ },
5556
+ async run({ args }) {
5557
+ if (subcommandExecuted) return;
5558
+ const opts = toCliOptions(args);
5559
+ const cli = new VereleaseCli(opts);
5560
+ await cli.runPlanCommand(opts);
5561
+ }
5562
+ });
5563
+ void runMain(mainCommand);
3456
5564
  //# sourceMappingURL=bin.js.map