pubm 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,6 +58,11 @@ npm i -g pubm
58
58
  pubm patch --preview
59
59
  ```
60
60
 
61
+ > **Publishing to jsr?** Install the jsr CLI as a devDependency in your project:
62
+ > ```bash
63
+ > pnpm add -D jsr # or npm i -D jsr / yarn add -D jsr
64
+ > ```
65
+
61
66
  ---
62
67
 
63
68
  ## 🔑 Core CLI Options
package/bin/cli.js CHANGED
@@ -5249,50 +5249,6 @@ function link2(text, url) {
5249
5249
  return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
5250
5250
  }
5251
5251
 
5252
- // src/utils/rollback.ts
5253
- var rollbacks = [];
5254
- function addRollback(rollback2, context) {
5255
- rollbacks.push({ fn: rollback2, ctx: context });
5256
- }
5257
- var called = false;
5258
- async function rollback() {
5259
- if (called) return void 0;
5260
- called = true;
5261
- if (rollbacks.length <= 0) return void 0;
5262
- console.log("Rollback...");
5263
- const results = await Promise.allSettled(
5264
- rollbacks.map(({ fn, ctx }) => fn(ctx))
5265
- );
5266
- const failures = results.filter(
5267
- (r) => r.status === "rejected"
5268
- );
5269
- if (failures.length > 0) {
5270
- for (const failure of failures) {
5271
- console.error(
5272
- `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
5273
- );
5274
- }
5275
- console.log(
5276
- "Rollback completed with errors. Some operations may require manual recovery."
5277
- );
5278
- } else {
5279
- console.log("Rollback completed");
5280
- }
5281
- }
5282
-
5283
- // src/utils/listr.ts
5284
- function createListr(...args) {
5285
- const listr = new Listr(...args);
5286
- listr.isRoot = () => false;
5287
- listr.externalSignalHandler = rollback;
5288
- return listr;
5289
- }
5290
-
5291
- // src/utils/package.ts
5292
- import { readFile as readFile2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
5293
- import path8 from "node:path";
5294
- import process11 from "node:process";
5295
-
5296
5252
  // src/ecosystem/rust.ts
5297
5253
  import { readFile, stat as stat2, writeFile } from "node:fs/promises";
5298
5254
  import path7 from "node:path";
@@ -5339,6 +5295,17 @@ var RustEcosystem = class extends Ecosystem {
5339
5295
  pkg.version = newVersion;
5340
5296
  await writeFile(filePath, stringify(cargo));
5341
5297
  }
5298
+ async dependencies() {
5299
+ const cargo = await this.readCargoToml();
5300
+ const deps = [];
5301
+ for (const section of ["dependencies", "build-dependencies"]) {
5302
+ const sectionData = cargo[section];
5303
+ if (sectionData) {
5304
+ deps.push(...Object.keys(sectionData));
5305
+ }
5306
+ }
5307
+ return deps;
5308
+ }
5342
5309
  manifestFiles() {
5343
5310
  return ["Cargo.toml"];
5344
5311
  }
@@ -5353,7 +5320,96 @@ var RustEcosystem = class extends Ecosystem {
5353
5320
  }
5354
5321
  };
5355
5322
 
5323
+ // src/utils/crate-graph.ts
5324
+ async function sortCratesByDependencyOrder(cratePaths) {
5325
+ if (cratePaths.length <= 1) return cratePaths;
5326
+ const crateInfos = await Promise.all(
5327
+ cratePaths.map(async (cratePath) => {
5328
+ const eco = new RustEcosystem(cratePath);
5329
+ const name = await eco.packageName();
5330
+ const deps = await eco.dependencies();
5331
+ return { cratePath, name, deps };
5332
+ })
5333
+ );
5334
+ const nameSet = new Set(crateInfos.map((c) => c.name));
5335
+ const nameToPath = new Map(crateInfos.map((c) => [c.name, c.cratePath]));
5336
+ const internalDeps = /* @__PURE__ */ new Map();
5337
+ const inDegree = /* @__PURE__ */ new Map();
5338
+ for (const name of nameSet) {
5339
+ inDegree.set(name, 0);
5340
+ }
5341
+ for (const crate of crateInfos) {
5342
+ const filtered = crate.deps.filter((d2) => nameSet.has(d2));
5343
+ internalDeps.set(crate.name, filtered);
5344
+ for (const _dep of filtered) {
5345
+ inDegree.set(crate.name, (inDegree.get(crate.name) ?? 0) + 1);
5346
+ }
5347
+ }
5348
+ const queue = [];
5349
+ for (const [name, degree] of inDegree) {
5350
+ if (degree === 0) queue.push(name);
5351
+ }
5352
+ const sorted = [];
5353
+ while (queue.length > 0) {
5354
+ const current = queue.shift();
5355
+ sorted.push(current);
5356
+ for (const [name, deps] of internalDeps) {
5357
+ if (deps.includes(current)) {
5358
+ const newDegree = (inDegree.get(name) ?? 0) - 1;
5359
+ inDegree.set(name, newDegree);
5360
+ if (newDegree === 0) queue.push(name);
5361
+ }
5362
+ }
5363
+ }
5364
+ if (sorted.length !== nameSet.size) {
5365
+ throw new Error("Circular dependency detected among configured crates");
5366
+ }
5367
+ return sorted.map((name) => nameToPath.get(name));
5368
+ }
5369
+
5370
+ // src/utils/rollback.ts
5371
+ var rollbacks = [];
5372
+ function addRollback(rollback2, context) {
5373
+ rollbacks.push({ fn: rollback2, ctx: context });
5374
+ }
5375
+ var called = false;
5376
+ async function rollback() {
5377
+ if (called) return void 0;
5378
+ called = true;
5379
+ if (rollbacks.length <= 0) return void 0;
5380
+ console.log("Rollback...");
5381
+ const results = await Promise.allSettled(
5382
+ rollbacks.map(({ fn, ctx }) => fn(ctx))
5383
+ );
5384
+ const failures = results.filter(
5385
+ (r) => r.status === "rejected"
5386
+ );
5387
+ if (failures.length > 0) {
5388
+ for (const failure of failures) {
5389
+ console.error(
5390
+ `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
5391
+ );
5392
+ }
5393
+ console.log(
5394
+ "Rollback completed with errors. Some operations may require manual recovery."
5395
+ );
5396
+ } else {
5397
+ console.log("Rollback completed");
5398
+ }
5399
+ }
5400
+
5401
+ // src/utils/listr.ts
5402
+ function createListr(...args) {
5403
+ const listr = new Listr(...args);
5404
+ listr.isRoot = () => false;
5405
+ listr.externalSignalHandler = rollback;
5406
+ return listr;
5407
+ }
5408
+
5356
5409
  // src/utils/package.ts
5410
+ import { readFile as readFile2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
5411
+ import path8 from "node:path";
5412
+ import process11 from "node:process";
5357
5413
  var cachedPackageJson = {};
5358
5414
  var cachedJsrJson = {};
5359
5415
  function patchCachedJsrJson(contents, { cwd = process11.cwd() } = {}) {
@@ -5588,7 +5644,7 @@ function collectRegistries(ctx) {
5588
5644
 
5589
5645
  // src/registry/crates.ts
5590
5646
  import path9 from "node:path";
5591
- import { exec as exec3 } from "tinyexec";
5647
+ import { exec as exec3, NonZeroExitError } from "tinyexec";
5592
5648
 
5593
5649
  // src/registry/registry.ts
5594
5650
  var Registry = class {
@@ -5596,6 +5652,8 @@ var Registry = class {
5596
5652
  this.packageName = packageName;
5597
5653
  this.registry = registry;
5598
5654
  }
5655
+ async dryRunPublish(_manifestDir) {
5656
+ }
5599
5657
  };
5600
5658
 
5601
5659
  // src/registry/crates.ts
@@ -5670,9 +5728,24 @@ var CratesRegistry = class extends Registry {
5670
5728
  await exec3("cargo", args, { throwOnError: true });
5671
5729
  return true;
5672
5730
  } catch (error) {
5673
- throw new CratesError("Failed to run `cargo publish`", {
5674
- cause: error
5675
- });
5731
+ const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
5732
+ const message = stderr ? `Failed to run \`cargo publish\`:
5733
+ ${stderr}` : "Failed to run `cargo publish`";
5734
+ throw new CratesError(message, { cause: error });
5735
+ }
5736
+ }
5737
+ async dryRunPublish(manifestDir) {
5738
+ try {
5739
+ const args = ["publish", "--dry-run"];
5740
+ if (manifestDir) {
5741
+ args.push("--manifest-path", path9.join(manifestDir, "Cargo.toml"));
5742
+ }
5743
+ await exec3("cargo", args, { throwOnError: true });
5744
+ } catch (error) {
5745
+ const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
5746
+ const message = stderr ? `Dry-run failed for \`cargo publish\`:
5747
+ ${stderr}` : "Dry-run failed for `cargo publish`";
5748
+ throw new CratesError(message, { cause: error });
5676
5749
  }
5677
5750
  }
5678
5751
  async isPublished() {
@@ -5761,13 +5834,8 @@ function createCratesPublishTask(packagePath) {
5761
5834
  var cratesAvailableCheckTasks = createCratesAvailableCheckTask();
5762
5835
  var cratesPublishTasks = createCratesPublishTask();
5763
5836
 
5764
- // src/tasks/jsr.ts
5765
- import process12 from "node:process";
5766
- import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
5767
- import npmCli from "@npmcli/promise-spawn";
5768
-
5769
5837
  // src/registry/jsr.ts
5770
- import { exec as exec4, NonZeroExitError } from "tinyexec";
5838
+ import { exec as exec4, NonZeroExitError as NonZeroExitError2 } from "tinyexec";
5771
5839
 
5772
5840
  // src/utils/db.ts
5773
5841
  import { createCipheriv, createDecipheriv, createHash } from "node:crypto";
@@ -5946,7 +6014,7 @@ var JsrRegisry = class extends Registry {
5946
6014
  this.packageCreationUrls = void 0;
5947
6015
  return true;
5948
6016
  } catch (error) {
5949
- const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
6017
+ const stderr = error instanceof NonZeroExitError2 ? error.output?.stderr : void 0;
5950
6018
  if (stderr?.includes("don't exist")) {
5951
6019
  const urls = [...stderr.matchAll(/https:\/\/jsr\.io\/new\S+/g)].map(
5952
6020
  (m) => m[0]
@@ -5965,6 +6033,28 @@ ${stderr}` : ""}`,
5965
6033
  );
5966
6034
  }
5967
6035
  }
6036
+ async dryRunPublish() {
6037
+ try {
6038
+ await exec4(
6039
+ "jsr",
6040
+ [
6041
+ "publish",
6042
+ "--dry-run",
6043
+ "--allow-dirty",
6044
+ "--token",
6045
+ `${JsrClient.token}`
6046
+ ],
6047
+ { throwOnError: true }
6048
+ );
6049
+ } catch (error) {
6050
+ const stderr = error instanceof NonZeroExitError2 ? error.output?.stderr : void 0;
6051
+ throw new JsrError(
6052
+ `Dry-run failed for \`jsr publish\`${stderr ? `
6053
+ ${stderr}` : ""}`,
6054
+ { cause: error }
6055
+ );
6056
+ }
6057
+ }
5968
6058
  async version() {
5969
6059
  return await this.jsr(["--version"]);
5970
6060
  }
@@ -6206,7 +6296,7 @@ async function jsrRegistry() {
6206
6296
  }
6207
6297
 
6208
6298
  // src/registry/npm.ts
6209
- import { exec as exec5, NonZeroExitError as NonZeroExitError2 } from "tinyexec";
6299
+ import { exec as exec5, NonZeroExitError as NonZeroExitError3 } from "tinyexec";
6210
6300
  var NpmError = class extends AbstractError {
6211
6301
  constructor() {
6212
6302
  super(...arguments);
@@ -6263,7 +6353,7 @@ var NpmRegistry = class extends Registry {
6263
6353
  await this.npm(["whoami"]);
6264
6354
  return true;
6265
6355
  } catch (error) {
6266
- if (error instanceof NonZeroExitError2) {
6356
+ if (error instanceof NonZeroExitError3) {
6267
6357
  return false;
6268
6358
  }
6269
6359
  throw new NpmError("Failed to run `npm whoami`", { cause: error });
@@ -6341,7 +6431,7 @@ var NpmRegistry = class extends Registry {
6341
6431
  await this.npm(["publish", "--provenance", "--access", "public"]);
6342
6432
  return true;
6343
6433
  } catch (error) {
6344
- if (error instanceof NonZeroExitError2 && error.output?.stderr.includes("EOTP")) {
6434
+ if (error instanceof NonZeroExitError3 && error.output?.stderr.includes("EOTP")) {
6345
6435
  return false;
6346
6436
  }
6347
6437
  throw this.classifyPublishError(error);
@@ -6353,12 +6443,28 @@ var NpmRegistry = class extends Registry {
6353
6443
  await this.npm(args);
6354
6444
  return true;
6355
6445
  } catch (error) {
6356
- if (error instanceof NonZeroExitError2 && error.output?.stderr.includes("EOTP")) {
6446
+ if (error instanceof NonZeroExitError3 && error.output?.stderr.includes("EOTP")) {
6357
6447
  return false;
6358
6448
  }
6359
6449
  throw this.classifyPublishError(error);
6360
6450
  }
6361
6451
  }
6452
+ async dryRunPublish() {
6453
+ try {
6454
+ await this.npm(["publish", "--dry-run"]);
6455
+ } catch (error) {
6456
+ throw this.classifyPublishError(error);
6457
+ }
6458
+ }
6459
+ async twoFactorAuthMode() {
6460
+ try {
6461
+ const output = await this.npm(["profile", "get", "--json"]);
6462
+ const profile = JSON.parse(output);
6463
+ return profile?.tfa?.mode ?? null;
6464
+ } catch {
6465
+ return null;
6466
+ }
6467
+ }
6362
6468
  async isPackageNameAvaliable() {
6363
6469
  return isValidPackageName(this.packageName);
6364
6470
  }
@@ -6369,7 +6475,7 @@ var NpmRegistry = class extends Registry {
6369
6475
  };
6370
6476
  }
6371
6477
  classifyPublishError(error) {
6372
- if (error instanceof NonZeroExitError2) {
6478
+ if (error instanceof NonZeroExitError3) {
6373
6479
  const stderr = error.output?.stderr ?? "";
6374
6480
  if (stderr.includes("EOTP")) {
6375
6481
  return new NpmError("OTP required for publishing", { cause: error });
@@ -6395,7 +6501,71 @@ async function npmRegistry() {
6395
6501
  return new NpmRegistry(packageJson.name);
6396
6502
  }
6397
6503
 
6504
+ // src/tasks/dry-run-publish.ts
6505
+ var npmDryRunPublishTask = {
6506
+ title: "Dry-run npm publish",
6507
+ task: async (_, task) => {
6508
+ const npm = await npmRegistry();
6509
+ task.output = "Running npm publish --dry-run...";
6510
+ await npm.dryRunPublish();
6511
+ }
6512
+ };
6513
+ var jsrDryRunPublishTask = {
6514
+ title: "Dry-run jsr publish",
6515
+ skip: () => !JsrClient.token,
6516
+ task: async (_, task) => {
6517
+ const jsr = await jsrRegistry();
6518
+ task.output = "Running jsr publish --dry-run...";
6519
+ await jsr.dryRunPublish();
6520
+ }
6521
+ };
6522
+ function createCratesDryRunPublishTask(packagePath) {
6523
+ const label = packagePath ? ` (${packagePath})` : "";
6524
+ return {
6525
+ title: `Dry-run cargo publish${label}`,
6526
+ task: async (_, task) => {
6527
+ const eco = new RustEcosystem(packagePath ?? process.cwd());
6528
+ const packageName = await eco.packageName();
6529
+ const registry = new CratesRegistry(packageName);
6530
+ task.output = "Running cargo publish --dry-run...";
6531
+ await registry.dryRunPublish(packagePath);
6532
+ }
6533
+ };
6534
+ }
6535
+ var cratesDryRunPublishTask = createCratesDryRunPublishTask();
6536
+ function registryDryRunTask(registryKey) {
6537
+ switch (registryKey) {
6538
+ case "npm":
6539
+ return npmDryRunPublishTask;
6540
+ case "jsr":
6541
+ return jsrDryRunPublishTask;
6542
+ case "crates":
6543
+ return cratesDryRunPublishTask;
6544
+ default:
6545
+ return npmDryRunPublishTask;
6546
+ }
6547
+ }
6548
+ var dryRunPublishTask = {
6549
+ title: "Validating publish (dry-run)",
6550
+ task: (ctx, parentTask) => {
6551
+ if (ctx.packages?.length) {
6552
+ const tasks = ctx.packages.flatMap(
6553
+ (pkg) => pkg.registries.map(
6554
+ (registryKey) => registryKey === "crates" ? createCratesDryRunPublishTask(pkg.path) : registryDryRunTask(registryKey)
6555
+ )
6556
+ );
6557
+ return parentTask.newListr(tasks, { concurrent: true });
6558
+ }
6559
+ return parentTask.newListr(collectRegistries(ctx).map(registryDryRunTask), {
6560
+ concurrent: true
6561
+ });
6562
+ }
6563
+ };
6564
+
6398
6565
  // src/tasks/jsr.ts
6566
+ import process12 from "node:process";
6567
+ import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
6568
+ import npmCli from "@npmcli/promise-spawn";
6399
6569
  var { open } = npmCli;
6400
6570
  var JsrAvailableError = class extends AbstractError {
6401
6571
  constructor(message, { cause } = {}) {
@@ -6628,7 +6798,6 @@ var NpmAvailableError = class extends AbstractError {
6628
6798
  };
6629
6799
  var npmAvailableCheckTasks = {
6630
6800
  title: "Checking npm avaliable for publising",
6631
- skip: (ctx) => !!ctx.preview,
6632
6801
  task: async (ctx, task) => {
6633
6802
  const npm = await npmRegistry();
6634
6803
  if (!await npm.isLoggedIn()) {
@@ -6691,6 +6860,14 @@ var npmAvailableCheckTasks = {
6691
6860
  More information: ${link2("npm naming rules", "https://github.com/npm/validate-npm-package-name?tab=readme-ov-file#naming-rules")}`
6692
6861
  );
6693
6862
  }
6863
+ if (!ctx.promptEnabled) {
6864
+ const tfaMode = await npm.twoFactorAuthMode();
6865
+ if (tfaMode === "auth-and-writes") {
6866
+ throw new NpmAvailableError(
6867
+ `npm account has 2FA enabled for writes (auth-and-writes). CI publish will fail with EOTP. Use an automation token or configure granular access token at https://www.npmjs.com/package/${npm.packageName}/access`
6868
+ );
6869
+ }
6870
+ }
6694
6871
  }
6695
6872
  };
6696
6873
  var npmPublishTasks = {
@@ -7108,13 +7285,24 @@ function registryTask(registry) {
7108
7285
  return npmPublishTasks;
7109
7286
  }
7110
7287
  }
7111
- function collectPublishTasks(ctx) {
7288
+ async function collectPublishTasks(ctx) {
7112
7289
  if (ctx.packages?.length) {
7113
- return ctx.packages.flatMap(
7114
- (pkg) => pkg.registries.map(
7115
- (reg) => reg === "crates" ? createCratesPublishTask(pkg.path) : registryTask(reg)
7116
- )
7290
+ const nonCratesTasks = ctx.packages.flatMap(
7291
+ (pkg) => pkg.registries.filter((reg) => reg !== "crates").map((reg) => registryTask(reg))
7117
7292
  );
7293
+ const cratesPaths = ctx.packages.filter((pkg) => pkg.registries.includes("crates")).map((pkg) => pkg.path);
7294
+ if (cratesPaths.length === 0) {
7295
+ return nonCratesTasks;
7296
+ }
7297
+ const sortedPaths = await sortCratesByDependencyOrder(cratesPaths);
7298
+ const sequentialCratesTask = {
7299
+ title: "Publishing to crates.io (sequential)",
7300
+ task: (_ctx, task) => task.newListr(
7301
+ sortedPaths.map((p) => createCratesPublishTask(p)),
7302
+ { concurrent: false }
7303
+ )
7304
+ };
7305
+ return [...nonCratesTasks, sequentialCratesTask];
7118
7306
  }
7119
7307
  return collectRegistries(ctx).map(registryTask);
7120
7308
  }
@@ -7136,7 +7324,7 @@ async function run(options) {
7136
7324
  await createListr(
7137
7325
  options.publishOnly ? {
7138
7326
  title: "Publishing",
7139
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
7327
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
7140
7328
  concurrent: true
7141
7329
  })
7142
7330
  } : [
@@ -7174,6 +7362,10 @@ async function run(options) {
7174
7362
  }
7175
7363
  }
7176
7364
  },
7365
+ {
7366
+ ...dryRunPublishTask,
7367
+ skip: (ctx2) => options.skipPublish || !!ctx2.preview
7368
+ },
7177
7369
  {
7178
7370
  title: "Bumping version",
7179
7371
  skip: (ctx2) => !!ctx2.preview,
@@ -7225,7 +7417,7 @@ async function run(options) {
7225
7417
  {
7226
7418
  skip: (ctx2) => options.skipPublish || !!ctx2.preview,
7227
7419
  title: "Publishing",
7228
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
7420
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
7229
7421
  concurrent: true
7230
7422
  })
7231
7423
  },
package/dist/index.cjs CHANGED
@@ -4814,50 +4814,6 @@ function link2(text, url) {
4814
4814
  return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
4815
4815
  }
4816
4816
 
4817
- // src/utils/rollback.ts
4818
- var rollbacks = [];
4819
- function addRollback(rollback2, context) {
4820
- rollbacks.push({ fn: rollback2, ctx: context });
4821
- }
4822
- var called = false;
4823
- async function rollback() {
4824
- if (called) return void 0;
4825
- called = true;
4826
- if (rollbacks.length <= 0) return void 0;
4827
- console.log("Rollback...");
4828
- const results = await Promise.allSettled(
4829
- rollbacks.map(({ fn, ctx }) => fn(ctx))
4830
- );
4831
- const failures = results.filter(
4832
- (r) => r.status === "rejected"
4833
- );
4834
- if (failures.length > 0) {
4835
- for (const failure of failures) {
4836
- console.error(
4837
- `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
4838
- );
4839
- }
4840
- console.log(
4841
- "Rollback completed with errors. Some operations may require manual recovery."
4842
- );
4843
- } else {
4844
- console.log("Rollback completed");
4845
- }
4846
- }
4847
-
4848
- // src/utils/listr.ts
4849
- function createListr(...args) {
4850
- const listr = new Listr(...args);
4851
- listr.isRoot = () => false;
4852
- listr.externalSignalHandler = rollback;
4853
- return listr;
4854
- }
4855
-
4856
- // src/utils/package.ts
4857
- var import_promises3 = require("fs/promises");
4858
- var import_node_path3 = __toESM(require("path"), 1);
4859
- var import_node_process5 = __toESM(require("process"), 1);
4860
-
4861
4817
  // src/ecosystem/rust.ts
4862
4818
  var import_promises2 = require("fs/promises");
4863
4819
  var import_node_path2 = __toESM(require("path"), 1);
@@ -4904,6 +4860,17 @@ var RustEcosystem = class extends Ecosystem {
4904
4860
  pkg.version = newVersion;
4905
4861
  await (0, import_promises2.writeFile)(filePath, (0, import_smol_toml.stringify)(cargo));
4906
4862
  }
4863
+ async dependencies() {
4864
+ const cargo = await this.readCargoToml();
4865
+ const deps = [];
4866
+ for (const section of ["dependencies", "build-dependencies"]) {
4867
+ const sectionData = cargo[section];
4868
+ if (sectionData) {
4869
+ deps.push(...Object.keys(sectionData));
4870
+ }
4871
+ }
4872
+ return deps;
4873
+ }
4907
4874
  manifestFiles() {
4908
4875
  return ["Cargo.toml"];
4909
4876
  }
@@ -4918,7 +4885,96 @@ var RustEcosystem = class extends Ecosystem {
4918
4885
  }
4919
4886
  };
4920
4887
 
4888
+ // src/utils/crate-graph.ts
4889
+ async function sortCratesByDependencyOrder(cratePaths) {
4890
+ if (cratePaths.length <= 1) return cratePaths;
4891
+ const crateInfos = await Promise.all(
4892
+ cratePaths.map(async (cratePath) => {
4893
+ const eco = new RustEcosystem(cratePath);
4894
+ const name = await eco.packageName();
4895
+ const deps = await eco.dependencies();
4896
+ return { cratePath, name, deps };
4897
+ })
4898
+ );
4899
+ const nameSet = new Set(crateInfos.map((c) => c.name));
4900
+ const nameToPath = new Map(crateInfos.map((c) => [c.name, c.cratePath]));
4901
+ const internalDeps = /* @__PURE__ */ new Map();
4902
+ const inDegree = /* @__PURE__ */ new Map();
4903
+ for (const name of nameSet) {
4904
+ inDegree.set(name, 0);
4905
+ }
4906
+ for (const crate of crateInfos) {
4907
+ const filtered = crate.deps.filter((d2) => nameSet.has(d2));
4908
+ internalDeps.set(crate.name, filtered);
4909
+ for (const _dep of filtered) {
4910
+ inDegree.set(crate.name, (inDegree.get(crate.name) ?? 0) + 1);
4911
+ }
4912
+ }
4913
+ const queue = [];
4914
+ for (const [name, degree] of inDegree) {
4915
+ if (degree === 0) queue.push(name);
4916
+ }
4917
+ const sorted = [];
4918
+ while (queue.length > 0) {
4919
+ const current = queue.shift();
4920
+ sorted.push(current);
4921
+ for (const [name, deps] of internalDeps) {
4922
+ if (deps.includes(current)) {
4923
+ const newDegree = (inDegree.get(name) ?? 0) - 1;
4924
+ inDegree.set(name, newDegree);
4925
+ if (newDegree === 0) queue.push(name);
4926
+ }
4927
+ }
4928
+ }
4929
+ if (sorted.length !== nameSet.size) {
4930
+ throw new Error("Circular dependency detected among configured crates");
4931
+ }
4932
+ return sorted.map((name) => nameToPath.get(name));
4933
+ }
4934
+
4935
+ // src/utils/rollback.ts
4936
+ var rollbacks = [];
4937
+ function addRollback(rollback2, context) {
4938
+ rollbacks.push({ fn: rollback2, ctx: context });
4939
+ }
4940
+ var called = false;
4941
+ async function rollback() {
4942
+ if (called) return void 0;
4943
+ called = true;
4944
+ if (rollbacks.length <= 0) return void 0;
4945
+ console.log("Rollback...");
4946
+ const results = await Promise.allSettled(
4947
+ rollbacks.map(({ fn, ctx }) => fn(ctx))
4948
+ );
4949
+ const failures = results.filter(
4950
+ (r) => r.status === "rejected"
4951
+ );
4952
+ if (failures.length > 0) {
4953
+ for (const failure of failures) {
4954
+ console.error(
4955
+ `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
4956
+ );
4957
+ }
4958
+ console.log(
4959
+ "Rollback completed with errors. Some operations may require manual recovery."
4960
+ );
4961
+ } else {
4962
+ console.log("Rollback completed");
4963
+ }
4964
+ }
4965
+
4966
+ // src/utils/listr.ts
4967
+ function createListr(...args) {
4968
+ const listr = new Listr(...args);
4969
+ listr.isRoot = () => false;
4970
+ listr.externalSignalHandler = rollback;
4971
+ return listr;
4972
+ }
4973
+
4921
4974
  // src/utils/package.ts
4975
+ var import_promises3 = require("fs/promises");
4976
+ var import_node_path3 = __toESM(require("path"), 1);
4977
+ var import_node_process5 = __toESM(require("process"), 1);
4922
4978
  var cachedPackageJson = {};
4923
4979
  var cachedJsrJson = {};
4924
4980
  function patchCachedJsrJson(contents, { cwd = import_node_process5.default.cwd() } = {}) {
@@ -5161,6 +5217,8 @@ var Registry = class {
5161
5217
  this.packageName = packageName;
5162
5218
  this.registry = registry;
5163
5219
  }
5220
+ async dryRunPublish(_manifestDir) {
5221
+ }
5164
5222
  };
5165
5223
 
5166
5224
  // src/registry/crates.ts
@@ -5235,9 +5293,24 @@ var CratesRegistry = class extends Registry {
5235
5293
  await (0, import_tinyexec2.exec)("cargo", args, { throwOnError: true });
5236
5294
  return true;
5237
5295
  } catch (error) {
5238
- throw new CratesError("Failed to run `cargo publish`", {
5239
- cause: error
5240
- });
5296
+ const stderr = error instanceof import_tinyexec2.NonZeroExitError ? error.output?.stderr : void 0;
5297
+ const message = stderr ? `Failed to run \`cargo publish\`:
5298
+ ${stderr}` : "Failed to run `cargo publish`";
5299
+ throw new CratesError(message, { cause: error });
5300
+ }
5301
+ }
5302
+ async dryRunPublish(manifestDir) {
5303
+ try {
5304
+ const args = ["publish", "--dry-run"];
5305
+ if (manifestDir) {
5306
+ args.push("--manifest-path", import_node_path4.default.join(manifestDir, "Cargo.toml"));
5307
+ }
5308
+ await (0, import_tinyexec2.exec)("cargo", args, { throwOnError: true });
5309
+ } catch (error) {
5310
+ const stderr = error instanceof import_tinyexec2.NonZeroExitError ? error.output?.stderr : void 0;
5311
+ const message = stderr ? `Dry-run failed for \`cargo publish\`:
5312
+ ${stderr}` : "Dry-run failed for `cargo publish`";
5313
+ throw new CratesError(message, { cause: error });
5241
5314
  }
5242
5315
  }
5243
5316
  async isPublished() {
@@ -5326,11 +5399,6 @@ function createCratesPublishTask(packagePath) {
5326
5399
  var cratesAvailableCheckTasks = createCratesAvailableCheckTask();
5327
5400
  var cratesPublishTasks = createCratesPublishTask();
5328
5401
 
5329
- // src/tasks/jsr.ts
5330
- var import_node_process6 = __toESM(require("process"), 1);
5331
- var import_prompt_adapter_enquirer = require("@listr2/prompt-adapter-enquirer");
5332
- var import_promise_spawn = __toESM(require("@npmcli/promise-spawn"), 1);
5333
-
5334
5402
  // src/registry/jsr.ts
5335
5403
  var import_tinyexec3 = require("tinyexec");
5336
5404
 
@@ -5532,6 +5600,28 @@ ${stderr}` : ""}`,
5532
5600
  );
5533
5601
  }
5534
5602
  }
5603
+ async dryRunPublish() {
5604
+ try {
5605
+ await (0, import_tinyexec3.exec)(
5606
+ "jsr",
5607
+ [
5608
+ "publish",
5609
+ "--dry-run",
5610
+ "--allow-dirty",
5611
+ "--token",
5612
+ `${JsrClient.token}`
5613
+ ],
5614
+ { throwOnError: true }
5615
+ );
5616
+ } catch (error) {
5617
+ const stderr = error instanceof import_tinyexec3.NonZeroExitError ? error.output?.stderr : void 0;
5618
+ throw new JsrError(
5619
+ `Dry-run failed for \`jsr publish\`${stderr ? `
5620
+ ${stderr}` : ""}`,
5621
+ { cause: error }
5622
+ );
5623
+ }
5624
+ }
5535
5625
  async version() {
5536
5626
  return await this.jsr(["--version"]);
5537
5627
  }
@@ -5926,6 +6016,22 @@ var NpmRegistry = class extends Registry {
5926
6016
  throw this.classifyPublishError(error);
5927
6017
  }
5928
6018
  }
6019
+ async dryRunPublish() {
6020
+ try {
6021
+ await this.npm(["publish", "--dry-run"]);
6022
+ } catch (error) {
6023
+ throw this.classifyPublishError(error);
6024
+ }
6025
+ }
6026
+ async twoFactorAuthMode() {
6027
+ try {
6028
+ const output = await this.npm(["profile", "get", "--json"]);
6029
+ const profile = JSON.parse(output);
6030
+ return profile?.tfa?.mode ?? null;
6031
+ } catch {
6032
+ return null;
6033
+ }
6034
+ }
5929
6035
  async isPackageNameAvaliable() {
5930
6036
  return isValidPackageName(this.packageName);
5931
6037
  }
@@ -5962,7 +6068,71 @@ async function npmRegistry() {
5962
6068
  return new NpmRegistry(packageJson.name);
5963
6069
  }
5964
6070
 
6071
+ // src/tasks/dry-run-publish.ts
6072
+ var npmDryRunPublishTask = {
6073
+ title: "Dry-run npm publish",
6074
+ task: async (_, task) => {
6075
+ const npm = await npmRegistry();
6076
+ task.output = "Running npm publish --dry-run...";
6077
+ await npm.dryRunPublish();
6078
+ }
6079
+ };
6080
+ var jsrDryRunPublishTask = {
6081
+ title: "Dry-run jsr publish",
6082
+ skip: () => !JsrClient.token,
6083
+ task: async (_, task) => {
6084
+ const jsr = await jsrRegistry();
6085
+ task.output = "Running jsr publish --dry-run...";
6086
+ await jsr.dryRunPublish();
6087
+ }
6088
+ };
6089
+ function createCratesDryRunPublishTask(packagePath) {
6090
+ const label = packagePath ? ` (${packagePath})` : "";
6091
+ return {
6092
+ title: `Dry-run cargo publish${label}`,
6093
+ task: async (_, task) => {
6094
+ const eco = new RustEcosystem(packagePath ?? process.cwd());
6095
+ const packageName = await eco.packageName();
6096
+ const registry = new CratesRegistry(packageName);
6097
+ task.output = "Running cargo publish --dry-run...";
6098
+ await registry.dryRunPublish(packagePath);
6099
+ }
6100
+ };
6101
+ }
6102
+ var cratesDryRunPublishTask = createCratesDryRunPublishTask();
6103
+ function registryDryRunTask(registryKey) {
6104
+ switch (registryKey) {
6105
+ case "npm":
6106
+ return npmDryRunPublishTask;
6107
+ case "jsr":
6108
+ return jsrDryRunPublishTask;
6109
+ case "crates":
6110
+ return cratesDryRunPublishTask;
6111
+ default:
6112
+ return npmDryRunPublishTask;
6113
+ }
6114
+ }
6115
+ var dryRunPublishTask = {
6116
+ title: "Validating publish (dry-run)",
6117
+ task: (ctx, parentTask) => {
6118
+ if (ctx.packages?.length) {
6119
+ const tasks = ctx.packages.flatMap(
6120
+ (pkg) => pkg.registries.map(
6121
+ (registryKey) => registryKey === "crates" ? createCratesDryRunPublishTask(pkg.path) : registryDryRunTask(registryKey)
6122
+ )
6123
+ );
6124
+ return parentTask.newListr(tasks, { concurrent: true });
6125
+ }
6126
+ return parentTask.newListr(collectRegistries(ctx).map(registryDryRunTask), {
6127
+ concurrent: true
6128
+ });
6129
+ }
6130
+ };
6131
+
5965
6132
  // src/tasks/jsr.ts
6133
+ var import_node_process6 = __toESM(require("process"), 1);
6134
+ var import_prompt_adapter_enquirer = require("@listr2/prompt-adapter-enquirer");
6135
+ var import_promise_spawn = __toESM(require("@npmcli/promise-spawn"), 1);
5966
6136
  var { open } = import_promise_spawn.default;
5967
6137
  var JsrAvailableError = class extends AbstractError {
5968
6138
  constructor(message, { cause } = {}) {
@@ -6195,7 +6365,6 @@ var NpmAvailableError = class extends AbstractError {
6195
6365
  };
6196
6366
  var npmAvailableCheckTasks = {
6197
6367
  title: "Checking npm avaliable for publising",
6198
- skip: (ctx) => !!ctx.preview,
6199
6368
  task: async (ctx, task) => {
6200
6369
  const npm = await npmRegistry();
6201
6370
  if (!await npm.isLoggedIn()) {
@@ -6258,6 +6427,14 @@ var npmAvailableCheckTasks = {
6258
6427
  More information: ${link2("npm naming rules", "https://github.com/npm/validate-npm-package-name?tab=readme-ov-file#naming-rules")}`
6259
6428
  );
6260
6429
  }
6430
+ if (!ctx.promptEnabled) {
6431
+ const tfaMode = await npm.twoFactorAuthMode();
6432
+ if (tfaMode === "auth-and-writes") {
6433
+ throw new NpmAvailableError(
6434
+ `npm account has 2FA enabled for writes (auth-and-writes). CI publish will fail with EOTP. Use an automation token or configure granular access token at https://www.npmjs.com/package/${npm.packageName}/access`
6435
+ );
6436
+ }
6437
+ }
6261
6438
  }
6262
6439
  };
6263
6440
  var npmPublishTasks = {
@@ -6676,13 +6853,24 @@ function registryTask(registry) {
6676
6853
  return npmPublishTasks;
6677
6854
  }
6678
6855
  }
6679
- function collectPublishTasks(ctx) {
6856
+ async function collectPublishTasks(ctx) {
6680
6857
  if (ctx.packages?.length) {
6681
- return ctx.packages.flatMap(
6682
- (pkg) => pkg.registries.map(
6683
- (reg) => reg === "crates" ? createCratesPublishTask(pkg.path) : registryTask(reg)
6684
- )
6858
+ const nonCratesTasks = ctx.packages.flatMap(
6859
+ (pkg) => pkg.registries.filter((reg) => reg !== "crates").map((reg) => registryTask(reg))
6685
6860
  );
6861
+ const cratesPaths = ctx.packages.filter((pkg) => pkg.registries.includes("crates")).map((pkg) => pkg.path);
6862
+ if (cratesPaths.length === 0) {
6863
+ return nonCratesTasks;
6864
+ }
6865
+ const sortedPaths = await sortCratesByDependencyOrder(cratesPaths);
6866
+ const sequentialCratesTask = {
6867
+ title: "Publishing to crates.io (sequential)",
6868
+ task: (_ctx, task) => task.newListr(
6869
+ sortedPaths.map((p) => createCratesPublishTask(p)),
6870
+ { concurrent: false }
6871
+ )
6872
+ };
6873
+ return [...nonCratesTasks, sequentialCratesTask];
6686
6874
  }
6687
6875
  return collectRegistries(ctx).map(registryTask);
6688
6876
  }
@@ -6704,7 +6892,7 @@ async function run(options) {
6704
6892
  await createListr(
6705
6893
  options.publishOnly ? {
6706
6894
  title: "Publishing",
6707
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
6895
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
6708
6896
  concurrent: true
6709
6897
  })
6710
6898
  } : [
@@ -6742,6 +6930,10 @@ async function run(options) {
6742
6930
  }
6743
6931
  }
6744
6932
  },
6933
+ {
6934
+ ...dryRunPublishTask,
6935
+ skip: (ctx2) => options.skipPublish || !!ctx2.preview
6936
+ },
6745
6937
  {
6746
6938
  title: "Bumping version",
6747
6939
  skip: (ctx2) => !!ctx2.preview,
@@ -6793,7 +6985,7 @@ async function run(options) {
6793
6985
  {
6794
6986
  skip: (ctx2) => options.skipPublish || !!ctx2.preview,
6795
6987
  title: "Publishing",
6796
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
6988
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
6797
6989
  concurrent: true
6798
6990
  })
6799
6991
  },
package/dist/index.js CHANGED
@@ -4781,50 +4781,6 @@ function link2(text, url) {
4781
4781
  return `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`;
4782
4782
  }
4783
4783
 
4784
- // src/utils/rollback.ts
4785
- var rollbacks = [];
4786
- function addRollback(rollback2, context) {
4787
- rollbacks.push({ fn: rollback2, ctx: context });
4788
- }
4789
- var called = false;
4790
- async function rollback() {
4791
- if (called) return void 0;
4792
- called = true;
4793
- if (rollbacks.length <= 0) return void 0;
4794
- console.log("Rollback...");
4795
- const results = await Promise.allSettled(
4796
- rollbacks.map(({ fn, ctx }) => fn(ctx))
4797
- );
4798
- const failures = results.filter(
4799
- (r) => r.status === "rejected"
4800
- );
4801
- if (failures.length > 0) {
4802
- for (const failure of failures) {
4803
- console.error(
4804
- `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
4805
- );
4806
- }
4807
- console.log(
4808
- "Rollback completed with errors. Some operations may require manual recovery."
4809
- );
4810
- } else {
4811
- console.log("Rollback completed");
4812
- }
4813
- }
4814
-
4815
- // src/utils/listr.ts
4816
- function createListr(...args) {
4817
- const listr = new Listr(...args);
4818
- listr.isRoot = () => false;
4819
- listr.externalSignalHandler = rollback;
4820
- return listr;
4821
- }
4822
-
4823
- // src/utils/package.ts
4824
- import { readFile as readFile2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
4825
- import path3 from "node:path";
4826
- import process7 from "node:process";
4827
-
4828
4784
  // src/ecosystem/rust.ts
4829
4785
  import { readFile, stat as stat2, writeFile } from "node:fs/promises";
4830
4786
  import path2 from "node:path";
@@ -4871,6 +4827,17 @@ var RustEcosystem = class extends Ecosystem {
4871
4827
  pkg.version = newVersion;
4872
4828
  await writeFile(filePath, stringify(cargo));
4873
4829
  }
4830
+ async dependencies() {
4831
+ const cargo = await this.readCargoToml();
4832
+ const deps = [];
4833
+ for (const section of ["dependencies", "build-dependencies"]) {
4834
+ const sectionData = cargo[section];
4835
+ if (sectionData) {
4836
+ deps.push(...Object.keys(sectionData));
4837
+ }
4838
+ }
4839
+ return deps;
4840
+ }
4874
4841
  manifestFiles() {
4875
4842
  return ["Cargo.toml"];
4876
4843
  }
@@ -4885,7 +4852,96 @@ var RustEcosystem = class extends Ecosystem {
4885
4852
  }
4886
4853
  };
4887
4854
 
4855
+ // src/utils/crate-graph.ts
4856
+ async function sortCratesByDependencyOrder(cratePaths) {
4857
+ if (cratePaths.length <= 1) return cratePaths;
4858
+ const crateInfos = await Promise.all(
4859
+ cratePaths.map(async (cratePath) => {
4860
+ const eco = new RustEcosystem(cratePath);
4861
+ const name = await eco.packageName();
4862
+ const deps = await eco.dependencies();
4863
+ return { cratePath, name, deps };
4864
+ })
4865
+ );
4866
+ const nameSet = new Set(crateInfos.map((c) => c.name));
4867
+ const nameToPath = new Map(crateInfos.map((c) => [c.name, c.cratePath]));
4868
+ const internalDeps = /* @__PURE__ */ new Map();
4869
+ const inDegree = /* @__PURE__ */ new Map();
4870
+ for (const name of nameSet) {
4871
+ inDegree.set(name, 0);
4872
+ }
4873
+ for (const crate of crateInfos) {
4874
+ const filtered = crate.deps.filter((d2) => nameSet.has(d2));
4875
+ internalDeps.set(crate.name, filtered);
4876
+ for (const _dep of filtered) {
4877
+ inDegree.set(crate.name, (inDegree.get(crate.name) ?? 0) + 1);
4878
+ }
4879
+ }
4880
+ const queue = [];
4881
+ for (const [name, degree] of inDegree) {
4882
+ if (degree === 0) queue.push(name);
4883
+ }
4884
+ const sorted = [];
4885
+ while (queue.length > 0) {
4886
+ const current = queue.shift();
4887
+ sorted.push(current);
4888
+ for (const [name, deps] of internalDeps) {
4889
+ if (deps.includes(current)) {
4890
+ const newDegree = (inDegree.get(name) ?? 0) - 1;
4891
+ inDegree.set(name, newDegree);
4892
+ if (newDegree === 0) queue.push(name);
4893
+ }
4894
+ }
4895
+ }
4896
+ if (sorted.length !== nameSet.size) {
4897
+ throw new Error("Circular dependency detected among configured crates");
4898
+ }
4899
+ return sorted.map((name) => nameToPath.get(name));
4900
+ }
4901
+
4902
+ // src/utils/rollback.ts
4903
+ var rollbacks = [];
4904
+ function addRollback(rollback2, context) {
4905
+ rollbacks.push({ fn: rollback2, ctx: context });
4906
+ }
4907
+ var called = false;
4908
+ async function rollback() {
4909
+ if (called) return void 0;
4910
+ called = true;
4911
+ if (rollbacks.length <= 0) return void 0;
4912
+ console.log("Rollback...");
4913
+ const results = await Promise.allSettled(
4914
+ rollbacks.map(({ fn, ctx }) => fn(ctx))
4915
+ );
4916
+ const failures = results.filter(
4917
+ (r) => r.status === "rejected"
4918
+ );
4919
+ if (failures.length > 0) {
4920
+ for (const failure of failures) {
4921
+ console.error(
4922
+ `Rollback operation failed: ${failure.reason instanceof Error ? failure.reason.message : failure.reason}`
4923
+ );
4924
+ }
4925
+ console.log(
4926
+ "Rollback completed with errors. Some operations may require manual recovery."
4927
+ );
4928
+ } else {
4929
+ console.log("Rollback completed");
4930
+ }
4931
+ }
4932
+
4933
+ // src/utils/listr.ts
4934
+ function createListr(...args) {
4935
+ const listr = new Listr(...args);
4936
+ listr.isRoot = () => false;
4937
+ listr.externalSignalHandler = rollback;
4938
+ return listr;
4939
+ }
4940
+
4888
4941
  // src/utils/package.ts
4942
+ import { readFile as readFile2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
4943
+ import path3 from "node:path";
4944
+ import process7 from "node:process";
4889
4945
  var cachedPackageJson = {};
4890
4946
  var cachedJsrJson = {};
4891
4947
  function patchCachedJsrJson(contents, { cwd = process7.cwd() } = {}) {
@@ -5120,7 +5176,7 @@ function collectRegistries(ctx) {
5120
5176
 
5121
5177
  // src/registry/crates.ts
5122
5178
  import path4 from "node:path";
5123
- import { exec as exec3 } from "tinyexec";
5179
+ import { exec as exec3, NonZeroExitError } from "tinyexec";
5124
5180
 
5125
5181
  // src/registry/registry.ts
5126
5182
  var Registry = class {
@@ -5128,6 +5184,8 @@ var Registry = class {
5128
5184
  this.packageName = packageName;
5129
5185
  this.registry = registry;
5130
5186
  }
5187
+ async dryRunPublish(_manifestDir) {
5188
+ }
5131
5189
  };
5132
5190
 
5133
5191
  // src/registry/crates.ts
@@ -5202,9 +5260,24 @@ var CratesRegistry = class extends Registry {
5202
5260
  await exec3("cargo", args, { throwOnError: true });
5203
5261
  return true;
5204
5262
  } catch (error) {
5205
- throw new CratesError("Failed to run `cargo publish`", {
5206
- cause: error
5207
- });
5263
+ const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
5264
+ const message = stderr ? `Failed to run \`cargo publish\`:
5265
+ ${stderr}` : "Failed to run `cargo publish`";
5266
+ throw new CratesError(message, { cause: error });
5267
+ }
5268
+ }
5269
+ async dryRunPublish(manifestDir) {
5270
+ try {
5271
+ const args = ["publish", "--dry-run"];
5272
+ if (manifestDir) {
5273
+ args.push("--manifest-path", path4.join(manifestDir, "Cargo.toml"));
5274
+ }
5275
+ await exec3("cargo", args, { throwOnError: true });
5276
+ } catch (error) {
5277
+ const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
5278
+ const message = stderr ? `Dry-run failed for \`cargo publish\`:
5279
+ ${stderr}` : "Dry-run failed for `cargo publish`";
5280
+ throw new CratesError(message, { cause: error });
5208
5281
  }
5209
5282
  }
5210
5283
  async isPublished() {
@@ -5293,13 +5366,8 @@ function createCratesPublishTask(packagePath) {
5293
5366
  var cratesAvailableCheckTasks = createCratesAvailableCheckTask();
5294
5367
  var cratesPublishTasks = createCratesPublishTask();
5295
5368
 
5296
- // src/tasks/jsr.ts
5297
- import process8 from "node:process";
5298
- import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
5299
- import npmCli from "@npmcli/promise-spawn";
5300
-
5301
5369
  // src/registry/jsr.ts
5302
- import { exec as exec4, NonZeroExitError } from "tinyexec";
5370
+ import { exec as exec4, NonZeroExitError as NonZeroExitError2 } from "tinyexec";
5303
5371
 
5304
5372
  // src/utils/db.ts
5305
5373
  import { createCipheriv, createDecipheriv, createHash } from "node:crypto";
@@ -5478,7 +5546,7 @@ var JsrRegisry = class extends Registry {
5478
5546
  this.packageCreationUrls = void 0;
5479
5547
  return true;
5480
5548
  } catch (error) {
5481
- const stderr = error instanceof NonZeroExitError ? error.output?.stderr : void 0;
5549
+ const stderr = error instanceof NonZeroExitError2 ? error.output?.stderr : void 0;
5482
5550
  if (stderr?.includes("don't exist")) {
5483
5551
  const urls = [...stderr.matchAll(/https:\/\/jsr\.io\/new\S+/g)].map(
5484
5552
  (m) => m[0]
@@ -5497,6 +5565,28 @@ ${stderr}` : ""}`,
5497
5565
  );
5498
5566
  }
5499
5567
  }
5568
+ async dryRunPublish() {
5569
+ try {
5570
+ await exec4(
5571
+ "jsr",
5572
+ [
5573
+ "publish",
5574
+ "--dry-run",
5575
+ "--allow-dirty",
5576
+ "--token",
5577
+ `${JsrClient.token}`
5578
+ ],
5579
+ { throwOnError: true }
5580
+ );
5581
+ } catch (error) {
5582
+ const stderr = error instanceof NonZeroExitError2 ? error.output?.stderr : void 0;
5583
+ throw new JsrError(
5584
+ `Dry-run failed for \`jsr publish\`${stderr ? `
5585
+ ${stderr}` : ""}`,
5586
+ { cause: error }
5587
+ );
5588
+ }
5589
+ }
5500
5590
  async version() {
5501
5591
  return await this.jsr(["--version"]);
5502
5592
  }
@@ -5738,7 +5828,7 @@ async function jsrRegistry() {
5738
5828
  }
5739
5829
 
5740
5830
  // src/registry/npm.ts
5741
- import { exec as exec5, NonZeroExitError as NonZeroExitError2 } from "tinyexec";
5831
+ import { exec as exec5, NonZeroExitError as NonZeroExitError3 } from "tinyexec";
5742
5832
  var NpmError = class extends AbstractError {
5743
5833
  constructor() {
5744
5834
  super(...arguments);
@@ -5795,7 +5885,7 @@ var NpmRegistry = class extends Registry {
5795
5885
  await this.npm(["whoami"]);
5796
5886
  return true;
5797
5887
  } catch (error) {
5798
- if (error instanceof NonZeroExitError2) {
5888
+ if (error instanceof NonZeroExitError3) {
5799
5889
  return false;
5800
5890
  }
5801
5891
  throw new NpmError("Failed to run `npm whoami`", { cause: error });
@@ -5873,7 +5963,7 @@ var NpmRegistry = class extends Registry {
5873
5963
  await this.npm(["publish", "--provenance", "--access", "public"]);
5874
5964
  return true;
5875
5965
  } catch (error) {
5876
- if (error instanceof NonZeroExitError2 && error.output?.stderr.includes("EOTP")) {
5966
+ if (error instanceof NonZeroExitError3 && error.output?.stderr.includes("EOTP")) {
5877
5967
  return false;
5878
5968
  }
5879
5969
  throw this.classifyPublishError(error);
@@ -5885,12 +5975,28 @@ var NpmRegistry = class extends Registry {
5885
5975
  await this.npm(args);
5886
5976
  return true;
5887
5977
  } catch (error) {
5888
- if (error instanceof NonZeroExitError2 && error.output?.stderr.includes("EOTP")) {
5978
+ if (error instanceof NonZeroExitError3 && error.output?.stderr.includes("EOTP")) {
5889
5979
  return false;
5890
5980
  }
5891
5981
  throw this.classifyPublishError(error);
5892
5982
  }
5893
5983
  }
5984
+ async dryRunPublish() {
5985
+ try {
5986
+ await this.npm(["publish", "--dry-run"]);
5987
+ } catch (error) {
5988
+ throw this.classifyPublishError(error);
5989
+ }
5990
+ }
5991
+ async twoFactorAuthMode() {
5992
+ try {
5993
+ const output = await this.npm(["profile", "get", "--json"]);
5994
+ const profile = JSON.parse(output);
5995
+ return profile?.tfa?.mode ?? null;
5996
+ } catch {
5997
+ return null;
5998
+ }
5999
+ }
5894
6000
  async isPackageNameAvaliable() {
5895
6001
  return isValidPackageName(this.packageName);
5896
6002
  }
@@ -5901,7 +6007,7 @@ var NpmRegistry = class extends Registry {
5901
6007
  };
5902
6008
  }
5903
6009
  classifyPublishError(error) {
5904
- if (error instanceof NonZeroExitError2) {
6010
+ if (error instanceof NonZeroExitError3) {
5905
6011
  const stderr = error.output?.stderr ?? "";
5906
6012
  if (stderr.includes("EOTP")) {
5907
6013
  return new NpmError("OTP required for publishing", { cause: error });
@@ -5927,7 +6033,71 @@ async function npmRegistry() {
5927
6033
  return new NpmRegistry(packageJson.name);
5928
6034
  }
5929
6035
 
6036
+ // src/tasks/dry-run-publish.ts
6037
+ var npmDryRunPublishTask = {
6038
+ title: "Dry-run npm publish",
6039
+ task: async (_, task) => {
6040
+ const npm = await npmRegistry();
6041
+ task.output = "Running npm publish --dry-run...";
6042
+ await npm.dryRunPublish();
6043
+ }
6044
+ };
6045
+ var jsrDryRunPublishTask = {
6046
+ title: "Dry-run jsr publish",
6047
+ skip: () => !JsrClient.token,
6048
+ task: async (_, task) => {
6049
+ const jsr = await jsrRegistry();
6050
+ task.output = "Running jsr publish --dry-run...";
6051
+ await jsr.dryRunPublish();
6052
+ }
6053
+ };
6054
+ function createCratesDryRunPublishTask(packagePath) {
6055
+ const label = packagePath ? ` (${packagePath})` : "";
6056
+ return {
6057
+ title: `Dry-run cargo publish${label}`,
6058
+ task: async (_, task) => {
6059
+ const eco = new RustEcosystem(packagePath ?? process.cwd());
6060
+ const packageName = await eco.packageName();
6061
+ const registry = new CratesRegistry(packageName);
6062
+ task.output = "Running cargo publish --dry-run...";
6063
+ await registry.dryRunPublish(packagePath);
6064
+ }
6065
+ };
6066
+ }
6067
+ var cratesDryRunPublishTask = createCratesDryRunPublishTask();
6068
+ function registryDryRunTask(registryKey) {
6069
+ switch (registryKey) {
6070
+ case "npm":
6071
+ return npmDryRunPublishTask;
6072
+ case "jsr":
6073
+ return jsrDryRunPublishTask;
6074
+ case "crates":
6075
+ return cratesDryRunPublishTask;
6076
+ default:
6077
+ return npmDryRunPublishTask;
6078
+ }
6079
+ }
6080
+ var dryRunPublishTask = {
6081
+ title: "Validating publish (dry-run)",
6082
+ task: (ctx, parentTask) => {
6083
+ if (ctx.packages?.length) {
6084
+ const tasks = ctx.packages.flatMap(
6085
+ (pkg) => pkg.registries.map(
6086
+ (registryKey) => registryKey === "crates" ? createCratesDryRunPublishTask(pkg.path) : registryDryRunTask(registryKey)
6087
+ )
6088
+ );
6089
+ return parentTask.newListr(tasks, { concurrent: true });
6090
+ }
6091
+ return parentTask.newListr(collectRegistries(ctx).map(registryDryRunTask), {
6092
+ concurrent: true
6093
+ });
6094
+ }
6095
+ };
6096
+
5930
6097
  // src/tasks/jsr.ts
6098
+ import process8 from "node:process";
6099
+ import { ListrEnquirerPromptAdapter } from "@listr2/prompt-adapter-enquirer";
6100
+ import npmCli from "@npmcli/promise-spawn";
5931
6101
  var { open } = npmCli;
5932
6102
  var JsrAvailableError = class extends AbstractError {
5933
6103
  constructor(message, { cause } = {}) {
@@ -6160,7 +6330,6 @@ var NpmAvailableError = class extends AbstractError {
6160
6330
  };
6161
6331
  var npmAvailableCheckTasks = {
6162
6332
  title: "Checking npm avaliable for publising",
6163
- skip: (ctx) => !!ctx.preview,
6164
6333
  task: async (ctx, task) => {
6165
6334
  const npm = await npmRegistry();
6166
6335
  if (!await npm.isLoggedIn()) {
@@ -6223,6 +6392,14 @@ var npmAvailableCheckTasks = {
6223
6392
  More information: ${link2("npm naming rules", "https://github.com/npm/validate-npm-package-name?tab=readme-ov-file#naming-rules")}`
6224
6393
  );
6225
6394
  }
6395
+ if (!ctx.promptEnabled) {
6396
+ const tfaMode = await npm.twoFactorAuthMode();
6397
+ if (tfaMode === "auth-and-writes") {
6398
+ throw new NpmAvailableError(
6399
+ `npm account has 2FA enabled for writes (auth-and-writes). CI publish will fail with EOTP. Use an automation token or configure granular access token at https://www.npmjs.com/package/${npm.packageName}/access`
6400
+ );
6401
+ }
6402
+ }
6226
6403
  }
6227
6404
  };
6228
6405
  var npmPublishTasks = {
@@ -6640,13 +6817,24 @@ function registryTask(registry) {
6640
6817
  return npmPublishTasks;
6641
6818
  }
6642
6819
  }
6643
- function collectPublishTasks(ctx) {
6820
+ async function collectPublishTasks(ctx) {
6644
6821
  if (ctx.packages?.length) {
6645
- return ctx.packages.flatMap(
6646
- (pkg) => pkg.registries.map(
6647
- (reg) => reg === "crates" ? createCratesPublishTask(pkg.path) : registryTask(reg)
6648
- )
6822
+ const nonCratesTasks = ctx.packages.flatMap(
6823
+ (pkg) => pkg.registries.filter((reg) => reg !== "crates").map((reg) => registryTask(reg))
6649
6824
  );
6825
+ const cratesPaths = ctx.packages.filter((pkg) => pkg.registries.includes("crates")).map((pkg) => pkg.path);
6826
+ if (cratesPaths.length === 0) {
6827
+ return nonCratesTasks;
6828
+ }
6829
+ const sortedPaths = await sortCratesByDependencyOrder(cratesPaths);
6830
+ const sequentialCratesTask = {
6831
+ title: "Publishing to crates.io (sequential)",
6832
+ task: (_ctx, task) => task.newListr(
6833
+ sortedPaths.map((p) => createCratesPublishTask(p)),
6834
+ { concurrent: false }
6835
+ )
6836
+ };
6837
+ return [...nonCratesTasks, sequentialCratesTask];
6650
6838
  }
6651
6839
  return collectRegistries(ctx).map(registryTask);
6652
6840
  }
@@ -6668,7 +6856,7 @@ async function run(options) {
6668
6856
  await createListr(
6669
6857
  options.publishOnly ? {
6670
6858
  title: "Publishing",
6671
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
6859
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
6672
6860
  concurrent: true
6673
6861
  })
6674
6862
  } : [
@@ -6706,6 +6894,10 @@ async function run(options) {
6706
6894
  }
6707
6895
  }
6708
6896
  },
6897
+ {
6898
+ ...dryRunPublishTask,
6899
+ skip: (ctx2) => options.skipPublish || !!ctx2.preview
6900
+ },
6709
6901
  {
6710
6902
  title: "Bumping version",
6711
6903
  skip: (ctx2) => !!ctx2.preview,
@@ -6757,7 +6949,7 @@ async function run(options) {
6757
6949
  {
6758
6950
  skip: (ctx2) => options.skipPublish || !!ctx2.preview,
6759
6951
  title: "Publishing",
6760
- task: (ctx2, parentTask) => parentTask.newListr(collectPublishTasks(ctx2), {
6952
+ task: async (ctx2, parentTask) => parentTask.newListr(await collectPublishTasks(ctx2), {
6761
6953
  concurrent: true
6762
6954
  })
6763
6955
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubm",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "engines": {
5
5
  "node": ">=18",
6
6
  "git": ">=2.11.0"