@kb-labs/release-manager-cli 2.75.0 → 2.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,10 +52,142 @@ function createChangelogGenerator(config, llm) {
52
52
  }
53
53
  };
54
54
  }
55
+ function rewriteWorkspaceDeps(pkgPath, versionMap, packageManager) {
56
+ const pkgJsonPath = join(pkgPath, "package.json");
57
+ const original = readFileSync(pkgJsonPath, "utf-8");
58
+ const pkgJson = JSON.parse(original);
59
+ let modified = false;
60
+ for (const section of ["dependencies", "peerDependencies"]) {
61
+ const deps = pkgJson[section];
62
+ if (!deps) {
63
+ continue;
64
+ }
65
+ for (const depName of Object.keys(deps)) {
66
+ const val = deps[depName];
67
+ if (typeof val !== "string") {
68
+ continue;
69
+ }
70
+ if (val.startsWith("link:")) {
71
+ const pinned = versionMap.get(depName);
72
+ if (pinned) {
73
+ deps[depName] = `^${pinned}`;
74
+ } else {
75
+ try {
76
+ const linkPath = val.slice("link:".length);
77
+ const linked = JSON.parse(readFileSync(join(pkgPath, linkPath, "package.json"), "utf-8"));
78
+ deps[depName] = `^${linked.version ?? "*"}`;
79
+ } catch {
80
+ deps[depName] = "*";
81
+ }
82
+ }
83
+ modified = true;
84
+ } else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
85
+ const pinned = versionMap.get(depName);
86
+ if (pinned) {
87
+ deps[depName] = val === "workspace:*" ? `^${pinned}` : val.replace("workspace:", "");
88
+ modified = true;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ if (modified) {
94
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
95
+ }
96
+ return () => writeFileSync(pkgJsonPath, original);
97
+ }
98
+ function topoSort(packages, packageManager) {
99
+ const nameSet = new Set(packages.map((p) => p.name));
100
+ const deps = /* @__PURE__ */ new Map();
101
+ for (const pkg of packages) {
102
+ const intra = /* @__PURE__ */ new Set();
103
+ try {
104
+ const pkgJson = JSON.parse(readFileSync(join(pkg.path, "package.json"), "utf-8"));
105
+ for (const section of ["dependencies", "peerDependencies"]) {
106
+ for (const depName of Object.keys(pkgJson[section] ?? {})) {
107
+ if (nameSet.has(depName)) {
108
+ intra.add(depName);
109
+ }
110
+ }
111
+ }
112
+ } catch {
113
+ }
114
+ deps.set(pkg.name, intra);
115
+ }
116
+ const inDegree = new Map(packages.map((p) => [p.name, 0]));
117
+ for (const [, d] of deps) {
118
+ for (const dep of d) {
119
+ inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
120
+ }
121
+ }
122
+ const dependants = /* @__PURE__ */ new Map();
123
+ for (const pkg of packages) {
124
+ for (const dep of deps.get(pkg.name) ?? []) {
125
+ if (!dependants.has(dep)) {
126
+ dependants.set(dep, /* @__PURE__ */ new Set());
127
+ }
128
+ dependants.get(dep).add(pkg.name);
129
+ }
130
+ }
131
+ const remaining = new Map(
132
+ packages.map((p) => [p.name, (deps.get(p.name) ?? /* @__PURE__ */ new Set()).size])
133
+ );
134
+ const byName = new Map(packages.map((p) => [p.name, p]));
135
+ const waves = [];
136
+ while (remaining.size > 0) {
137
+ const wave = [...remaining.entries()].filter(([, count]) => count === 0).map(([name]) => byName.get(name));
138
+ if (wave.length === 0) {
139
+ waves.push([...remaining.keys()].map((n) => byName.get(n)));
140
+ break;
141
+ }
142
+ waves.push(wave);
143
+ for (const pkg of wave) {
144
+ remaining.delete(pkg.name);
145
+ for (const dependant of dependants.get(pkg.name) ?? []) {
146
+ if (remaining.has(dependant)) {
147
+ remaining.set(dependant, (remaining.get(dependant) ?? 1) - 1);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ return waves.filter((w) => w.length > 0);
153
+ }
154
+
155
+ // src/shared/publish-programmatic.ts
156
+ var MAX_RETRIES = 5;
157
+ var RETRY_BASE_DELAYS_MS = [1e4, 2e4, 4e4, 8e4, 16e4];
158
+ var RETRYABLE_PATTERNS = [
159
+ "E429",
160
+ "429 Too Many Requests",
161
+ "ECONNRESET",
162
+ "ETIMEDOUT",
163
+ "ECONNREFUSED",
164
+ "ENOTFOUND",
165
+ "EAI_AGAIN",
166
+ "socket hang up",
167
+ "network timeout",
168
+ "read ECONNRESET"
169
+ ];
170
+ var ALREADY_PUBLISHED_PATTERNS = [
171
+ "You cannot publish over the previously published versions",
172
+ "cannot publish over the previously published version",
173
+ "EPUBLISHCONFLICT"
174
+ // Some registries return 403 for conflicts
175
+ ];
176
+ function isRetryable(message) {
177
+ return RETRYABLE_PATTERNS.some((p) => message.includes(p));
178
+ }
179
+ function isAlreadyPublished(message) {
180
+ return ALREADY_PUBLISHED_PATTERNS.some((p) => message.includes(p));
181
+ }
182
+ function retryDelay(attempt) {
183
+ const base = RETRY_BASE_DELAYS_MS[Math.min(attempt, RETRY_BASE_DELAYS_MS.length - 1)];
184
+ const jitter = 0.8 + Math.random() * 0.4;
185
+ return Math.round(base * jitter);
186
+ }
55
187
  function resolveToken(token) {
56
188
  return token ?? useEnv("NPM_TOKEN") ?? useEnv("NODE_AUTH_TOKEN");
57
189
  }
58
- function publishSinglePackage(options) {
190
+ function spawnPublish(options) {
59
191
  const { packagePath, packageManager, token, otp, dryRun, tag, access, registry } = options;
60
192
  return new Promise((resolve, reject) => {
61
193
  const args = ["publish"];
@@ -91,6 +223,19 @@ function publishSinglePackage(options) {
91
223
  writeFileSync(npmrcPath, existing + authLine);
92
224
  }
93
225
  }
226
+ const restoreNpmrc = () => {
227
+ if (!token) {
228
+ return;
229
+ }
230
+ try {
231
+ if (npmrcBackup !== null) {
232
+ writeFileSync(npmrcPath, npmrcBackup);
233
+ } else if (existsSync(npmrcPath)) {
234
+ unlinkSync(npmrcPath);
235
+ }
236
+ } catch {
237
+ }
238
+ };
94
239
  const child = spawn(packageManager, args, {
95
240
  cwd: packagePath,
96
241
  stdio: ["ignore", "pipe", "pipe"],
@@ -99,24 +244,14 @@ function publishSinglePackage(options) {
99
244
  });
100
245
  let stdout = "";
101
246
  let stderr = "";
102
- child.stdout?.on("data", (data) => {
103
- stdout += data.toString();
247
+ child.stdout?.on("data", (d) => {
248
+ stdout += d.toString();
104
249
  });
105
- child.stderr?.on("data", (data) => {
106
- stderr += data.toString();
250
+ child.stderr?.on("data", (d) => {
251
+ stderr += d.toString();
107
252
  });
108
- const cleanupNpmrc = () => {
109
- if (!token) {
110
- return;
111
- }
112
- if (npmrcBackup !== null) {
113
- writeFileSync(npmrcPath, npmrcBackup);
114
- } else if (existsSync(npmrcPath)) {
115
- unlinkSync(npmrcPath);
116
- }
117
- };
118
253
  child.on("close", (code) => {
119
- cleanupNpmrc();
254
+ restoreNpmrc();
120
255
  if (code === 0) {
121
256
  resolve();
122
257
  } else {
@@ -124,12 +259,66 @@ function publishSinglePackage(options) {
124
259
  }
125
260
  });
126
261
  child.on("error", (err) => {
262
+ restoreNpmrc();
127
263
  reject(err);
128
264
  });
129
265
  });
130
266
  }
267
+ async function publishOne(pkg, opts, logger) {
268
+ const restore = rewriteWorkspaceDeps(pkg.path, opts.versionMap, opts.packageManager);
269
+ try {
270
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
271
+ try {
272
+ await spawnPublish({
273
+ packagePath: pkg.path,
274
+ packageManager: opts.packageManager,
275
+ token: opts.token,
276
+ otp: opts.otp,
277
+ dryRun: opts.dryRun,
278
+ tag: opts.tag,
279
+ access: opts.access,
280
+ registry: opts.registry
281
+ });
282
+ logger.info(`Published ${pkg.name}@${pkg.version}`);
283
+ return { name: pkg.name, version: pkg.version, success: true };
284
+ } catch (err) {
285
+ const message = err instanceof Error ? err.message : String(err);
286
+ if (isAlreadyPublished(message)) {
287
+ logger.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
288
+ return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
289
+ }
290
+ if (isRetryable(message) && attempt < MAX_RETRIES) {
291
+ const delay = retryDelay(attempt);
292
+ logger.warn(
293
+ `Transient error for ${pkg.name}@${pkg.version} (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${(delay / 1e3).toFixed(1)}s: ${message.split("\n")[0]}`
294
+ );
295
+ await new Promise((r) => setTimeout(r, delay));
296
+ continue;
297
+ }
298
+ logger.error(`Failed to publish ${pkg.name}@${pkg.version}`, void 0, { error: message });
299
+ return { name: pkg.name, version: pkg.version, success: false, error: message };
300
+ }
301
+ }
302
+ return {
303
+ name: pkg.name,
304
+ version: pkg.version,
305
+ success: false,
306
+ error: `Publish failed after ${MAX_RETRIES} retries (transient errors)`
307
+ };
308
+ } finally {
309
+ restore();
310
+ }
311
+ }
131
312
  async function publishPackagesProgrammatic(options) {
132
- const { packages, packageManager = "pnpm", dryRun, otp, tag, access, registry } = options;
313
+ const {
314
+ packages,
315
+ packageManager = "pnpm",
316
+ dryRun,
317
+ otp,
318
+ tag,
319
+ access,
320
+ registry
321
+ } = options;
133
322
  const logger = useLogger();
134
323
  const token = resolveToken(options.token);
135
324
  if (!token && !dryRun) {
@@ -138,192 +327,184 @@ async function publishPackagesProgrammatic(options) {
138
327
  return {
139
328
  results: [],
140
329
  published: [],
330
+ alreadyPublished: [],
141
331
  failed: packages.map((p) => `${p.name}@${p.version}`),
142
332
  skipped: [],
143
333
  errors: [error]
144
334
  };
145
335
  }
146
336
  const versionMap = new Map(packages.map((p) => [p.name, p.version]));
147
- const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 8);
148
- const MAX_429_RETRIES = 3;
149
- const RETRY_429_DELAYS = [15e3, 3e4, 6e4];
150
- const publishOne = async (pkg) => {
151
- logger.info(`Publishing ${pkg.name}@${pkg.version}`, { path: pkg.path, dryRun });
152
- const pkgJsonPath = join(pkg.path, "package.json");
153
- const originalPkgJson = readFileSync(pkgJsonPath, "utf-8");
154
- let restored = false;
155
- try {
156
- const pkgJson = JSON.parse(originalPkgJson);
157
- let modified = false;
158
- for (const section of ["dependencies", "peerDependencies"]) {
159
- const deps = pkgJson[section];
160
- if (!deps) {
161
- continue;
162
- }
163
- for (const [depName, depValue] of Object.entries(deps)) {
164
- if (typeof depValue !== "string") {
165
- continue;
166
- }
167
- const val = depValue;
168
- if (val.startsWith("link:")) {
169
- const planVersion = versionMap.get(depName);
170
- if (planVersion) {
171
- deps[depName] = `^${planVersion}`;
172
- modified = true;
173
- } else {
174
- try {
175
- const linkPath = val.replace("link:", "");
176
- const linkedPkg = JSON.parse(readFileSync(join(pkg.path, linkPath, "package.json"), "utf-8"));
177
- deps[depName] = `^${linkedPkg.version}`;
178
- modified = true;
179
- } catch {
180
- deps[depName] = "*";
181
- modified = true;
182
- }
183
- }
184
- } else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
185
- const planVersion = versionMap.get(depName);
186
- if (planVersion) {
187
- deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
188
- modified = true;
189
- }
190
- }
191
- }
192
- }
193
- if (modified) {
194
- writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
195
- }
196
- for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
197
- try {
198
- await publishSinglePackage({
199
- packagePath: pkg.path,
200
- packageManager,
201
- token,
202
- otp,
203
- dryRun,
204
- tag,
205
- access: access ?? "public",
206
- registry
207
- });
208
- logger.info(`Published ${pkg.name}@${pkg.version}`);
209
- return { name: pkg.name, version: pkg.version, success: true };
210
- } catch (error) {
211
- const message = error instanceof Error ? error.message : String(error);
212
- if ((message.includes("E429") || message.includes("429 Too Many Requests")) && attempt < MAX_429_RETRIES) {
213
- const delay = RETRY_429_DELAYS[attempt];
214
- logger.warn(`429 rate limit for ${pkg.name}, retrying in ${delay / 1e3}s`, { attempt: attempt + 1 });
215
- await new Promise((r) => setTimeout(r, delay));
216
- continue;
337
+ const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
338
+ const waves = topoSort(packages);
339
+ logger.info(
340
+ `Publishing ${packages.length} package(s) in ${waves.length} topological wave(s) (concurrency=${CONCURRENCY}, dryRun=${dryRun ?? false})`
341
+ );
342
+ const allResults = [];
343
+ for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
344
+ const wave = waves[waveIdx];
345
+ logger.info(`Wave ${waveIdx + 1}/${waves.length}: publishing ${wave.length} package(s)`);
346
+ for (let i = 0; i < wave.length; i += CONCURRENCY) {
347
+ const batch = wave.slice(i, i + CONCURRENCY);
348
+ const batchResults = await Promise.all(
349
+ batch.map(
350
+ (pkg) => publishOne(pkg, { packageManager, token, versionMap, dryRun, otp, tag, access, registry }, logger)
351
+ )
352
+ );
353
+ allResults.push(...batchResults);
354
+ }
355
+ const waveFailed = allResults.slice(-wave.length).filter((r) => !r.success);
356
+ if (waveFailed.length > 0) {
357
+ const remaining = waves.slice(waveIdx + 1).reduce((acc, w) => acc + w.length, 0);
358
+ if (remaining > 0) {
359
+ logger.warn(
360
+ `Wave ${waveIdx + 1} had ${waveFailed.length} failure(s) \u2014 skipping ${remaining} package(s) in later waves to avoid broken dependency chain`
361
+ );
362
+ for (const laterWave of waves.slice(waveIdx + 1)) {
363
+ for (const pkg of laterWave) {
364
+ allResults.push({
365
+ name: pkg.name,
366
+ version: pkg.version,
367
+ success: false,
368
+ error: "Skipped: dependency wave failed"
369
+ });
217
370
  }
218
- logger.error(`Failed to publish ${pkg.name}@${pkg.version}`, void 0, { error: message });
219
- return { name: pkg.name, version: pkg.version, success: false, error: message };
220
371
  }
221
372
  }
222
- return { name: pkg.name, version: pkg.version, success: false, error: "429 rate limit: max retries exceeded" };
223
- } finally {
224
- if (!restored) {
225
- writeFileSync(pkgJsonPath, originalPkgJson);
226
- restored = true;
227
- }
373
+ break;
228
374
  }
229
- };
230
- const results = [];
231
- for (let i = 0; i < packages.length; i += CONCURRENCY) {
232
- const batch = packages.slice(i, i + CONCURRENCY);
233
- const batchResults = await Promise.all(batch.map(publishOne));
234
- results.push(...batchResults);
235
375
  }
236
- const published = results.filter((r) => r.success).map((r) => `${r.name}@${r.version}`);
237
- const failed = results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
238
- const errors = results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
376
+ const published = allResults.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
377
+ const alreadyPublished = allResults.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`);
378
+ const failed = allResults.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
379
+ const errors = allResults.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
380
+ if (alreadyPublished.length > 0) {
381
+ logger.info(`${alreadyPublished.length} package(s) already published \u2014 counted as success`);
382
+ }
239
383
  return {
240
- results,
384
+ results: allResults,
241
385
  published,
386
+ alreadyPublished,
242
387
  failed,
243
388
  skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
244
389
  errors
245
390
  };
246
391
  }
392
+ var MAX_RETRIES2 = 5;
393
+ var RETRY_BASE_DELAYS_MS2 = [1e4, 2e4, 4e4, 8e4, 16e4];
394
+ var RETRYABLE_PATTERNS2 = [
395
+ "E429",
396
+ "429 Too Many Requests",
397
+ "ECONNRESET",
398
+ "ETIMEDOUT",
399
+ "ECONNREFUSED",
400
+ "ENOTFOUND",
401
+ "EAI_AGAIN",
402
+ "socket hang up",
403
+ "network timeout",
404
+ "read ECONNRESET"
405
+ ];
406
+ var ALREADY_PUBLISHED_PATTERNS2 = [
407
+ "You cannot publish over the previously published versions",
408
+ "cannot publish over the previously published version",
409
+ "EPUBLISHCONFLICT"
410
+ ];
411
+ function isRetryable2(msg) {
412
+ return RETRYABLE_PATTERNS2.some((p) => msg.includes(p));
413
+ }
414
+ function isAlreadyPublished2(msg) {
415
+ return ALREADY_PUBLISHED_PATTERNS2.some((p) => msg.includes(p));
416
+ }
417
+ function retryDelay2(attempt) {
418
+ const base = RETRY_BASE_DELAYS_MS2[Math.min(attempt, RETRY_BASE_DELAYS_MS2.length - 1)];
419
+ const jitter = 0.8 + Math.random() * 0.4;
420
+ return Math.round(base * jitter);
421
+ }
422
+ function publishSinglePackage(options) {
423
+ const { packagePath, packageManager = "pnpm", otp, dryRun, tag, access } = options;
424
+ return new Promise((resolve, reject) => {
425
+ const args = ["publish"];
426
+ if (packageManager === "pnpm") {
427
+ args.push("--no-git-checks");
428
+ }
429
+ if (dryRun) {
430
+ args.push("--dry-run");
431
+ }
432
+ if (otp) {
433
+ args.push(`--otp=${otp}`);
434
+ }
435
+ if (tag) {
436
+ args.push(`--tag=${tag}`);
437
+ }
438
+ if (access) {
439
+ args.push(`--access=${access}`);
440
+ }
441
+ const child = spawn(packageManager, args, {
442
+ cwd: packagePath,
443
+ stdio: ["inherit", "pipe", "pipe"],
444
+ shell: true
445
+ });
446
+ let stdout = "";
447
+ let stderr = "";
448
+ child.stdout?.on("data", (d) => {
449
+ stdout += d.toString();
450
+ });
451
+ child.stderr?.on("data", (d) => {
452
+ stderr += d.toString();
453
+ });
454
+ child.on("close", (code) => {
455
+ if (code === 0) {
456
+ resolve();
457
+ } else {
458
+ reject(new Error(stderr || stdout || `${packageManager} publish exited with code ${code}`));
459
+ }
460
+ });
461
+ child.on("error", (err) => reject(err));
462
+ });
463
+ }
247
464
  async function publishPackagesWithOTP(options) {
248
465
  const { packages, packageManager = "pnpm", dryRun, tag, access, ui, logger } = options;
249
466
  let otp = options.otp;
250
467
  const results = [];
251
468
  const versionMap = new Map(packages.map((p) => [p.name, p.version]));
252
- const preparePackage = (pkg) => {
253
- const pkgJsonPath = join(pkg.path, "package.json");
254
- const originalPkgJson = readFileSync(pkgJsonPath, "utf-8");
255
- const pkgJson = JSON.parse(originalPkgJson);
256
- let modified = false;
257
- for (const section of ["dependencies", "peerDependencies"]) {
258
- const deps = pkgJson[section];
259
- if (!deps) {
260
- continue;
261
- }
262
- for (const [depName, depValue] of Object.entries(deps)) {
263
- if (typeof depValue !== "string") {
264
- continue;
265
- }
266
- const val = depValue;
267
- if (val.startsWith("link:")) {
268
- const planVersion = versionMap.get(depName);
269
- if (planVersion) {
270
- deps[depName] = `^${planVersion}`;
271
- modified = true;
272
- } else {
273
- try {
274
- const linkPath = val.replace("link:", "");
275
- const linkedPkg = JSON.parse(readFileSync(join(pkg.path, linkPath, "package.json"), "utf-8"));
276
- deps[depName] = `^${linkedPkg.version}`;
277
- modified = true;
278
- } catch {
279
- deps[depName] = "*";
280
- modified = true;
281
- }
282
- }
283
- } else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
284
- const planVersion = versionMap.get(depName);
285
- if (planVersion) {
286
- deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
287
- modified = true;
288
- }
289
- }
290
- }
291
- }
292
- if (modified) {
293
- writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
294
- }
295
- return () => writeFileSync(pkgJsonPath, originalPkgJson);
296
- };
297
- const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 8);
469
+ const CONCURRENCY = Number(useEnv("KB_PUBLISH_CONCURRENCY") ?? 4);
298
470
  const remaining = [];
299
471
  let otpFailureDetected = false;
300
- const MAX_429_RETRIES = 3;
301
- const RETRY_429_DELAYS = [15e3, 3e4, 6e4];
302
472
  const fastPublishOne = async (pkg) => {
303
- const restore = preparePackage(pkg);
473
+ const restore = rewriteWorkspaceDeps(pkg.path, versionMap, packageManager);
304
474
  try {
305
- for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
475
+ for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
306
476
  try {
307
- await publishSinglePackage2({ packagePath: pkg.path, packageManager, otp, dryRun, tag, access });
308
- logger?.info("Package published successfully (fast-path)", { name: pkg.name, version: pkg.version, dryRun });
477
+ await publishSinglePackage({ packagePath: pkg.path, packageManager, otp, dryRun, tag, access });
478
+ logger?.info("Published (fast-path)", { name: pkg.name, version: pkg.version });
309
479
  return { name: pkg.name, version: pkg.version, success: true };
310
- } catch (error) {
311
- const msg = error.message || String(error);
480
+ } catch (err) {
481
+ const msg = err instanceof Error ? err.message : String(err);
482
+ if (isAlreadyPublished2(msg)) {
483
+ logger?.info(`${pkg.name}@${pkg.version} already published \u2014 skipping`);
484
+ return { name: pkg.name, version: pkg.version, success: true, alreadyPublished: true };
485
+ }
312
486
  if (msg.includes("EOTP") || msg.includes("one-time password")) {
313
487
  otpFailureDetected = true;
314
488
  return null;
315
489
  }
316
- if ((msg.includes("E429") || msg.includes("429 Too Many Requests")) && attempt < MAX_429_RETRIES) {
317
- const delay = RETRY_429_DELAYS[attempt];
318
- logger?.warn(`429 rate limit for ${pkg.name}, retrying in ${delay / 1e3}s`, { attempt: attempt + 1 });
490
+ if (isRetryable2(msg) && attempt < MAX_RETRIES2) {
491
+ const delay = retryDelay2(attempt);
492
+ logger?.warn(
493
+ `Transient error for ${pkg.name} (attempt ${attempt + 1}/${MAX_RETRIES2}), retrying in ${(delay / 1e3).toFixed(1)}s`
494
+ );
319
495
  await new Promise((r) => setTimeout(r, delay));
320
496
  continue;
321
497
  }
322
- logger?.error("Package publish failed (fast-path)", { name: pkg.name, error: msg });
498
+ logger?.error("Publish failed (fast-path)", { name: pkg.name, error: msg });
323
499
  return { name: pkg.name, version: pkg.version, success: false, error: msg };
324
500
  }
325
501
  }
326
- return { name: pkg.name, version: pkg.version, success: false, error: "429 rate limit: max retries exceeded" };
502
+ return {
503
+ name: pkg.name,
504
+ version: pkg.version,
505
+ success: false,
506
+ error: `Publish failed after ${MAX_RETRIES2} retries (transient errors)`
507
+ };
327
508
  } finally {
328
509
  restore();
329
510
  }
@@ -347,231 +528,87 @@ async function publishPackagesWithOTP(options) {
347
528
  }
348
529
  }
349
530
  if (remaining.length === 0) {
350
- const published2 = results.filter((r) => r.success).map((r) => `${r.name}@${r.version}`);
351
- const failed2 = results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
352
- const errors2 = results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
353
- return {
354
- results,
355
- published: published2,
356
- failed: failed2,
357
- skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
358
- errors: errors2
359
- };
531
+ return buildResult(results, packages, dryRun);
360
532
  }
361
533
  ui.write?.(` OTP required \u2014 falling back to sequential publish for ${remaining.length} package(s)
362
534
  `);
363
535
  for (const pkg of remaining) {
364
- logger?.info("Publishing package", { name: pkg.name, version: pkg.version });
365
- const pkgJsonPath = join(pkg.path, "package.json");
366
- const originalPkgJson = readFileSync(pkgJsonPath, "utf-8");
367
- let restored = false;
536
+ logger?.info("Publishing package (sequential)", { name: pkg.name, version: pkg.version });
537
+ const loader = useLoader(`Publishing ${pkg.name}@${pkg.version}...`);
538
+ loader.start();
539
+ const restore = rewriteWorkspaceDeps(pkg.path, versionMap, packageManager);
368
540
  try {
369
- const pkgJson = JSON.parse(originalPkgJson);
370
- let modified = false;
371
- for (const section of ["dependencies", "peerDependencies"]) {
372
- const deps = pkgJson[section];
373
- if (!deps) {
374
- continue;
375
- }
376
- for (const [depName, depValue] of Object.entries(deps)) {
377
- if (typeof depValue !== "string") {
378
- continue;
379
- }
380
- const val = depValue;
381
- if (val.startsWith("link:")) {
382
- const planVersion = versionMap.get(depName);
383
- if (planVersion) {
384
- deps[depName] = `^${planVersion}`;
385
- modified = true;
386
- } else {
387
- try {
388
- const linkPath = val.replace("link:", "");
389
- const linkedPkg = JSON.parse(readFileSync(join(pkg.path, linkPath, "package.json"), "utf-8"));
390
- deps[depName] = `^${linkedPkg.version}`;
391
- modified = true;
392
- } catch {
393
- deps[depName] = "*";
394
- modified = true;
395
- }
396
- }
397
- } else if (val.startsWith("workspace:") && packageManager !== "pnpm") {
398
- const planVersion = versionMap.get(depName);
399
- if (planVersion) {
400
- deps[depName] = val === "workspace:*" ? `^${planVersion}` : val.replace("workspace:", "");
401
- modified = true;
402
- }
403
- }
404
- }
405
- }
406
- if (modified) {
407
- writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
408
- logger?.info(`Replaced link: deps for ${pkg.name}`);
409
- }
410
- const loader = useLoader(`Publishing ${pkg.name}@${pkg.version}...`);
411
- loader.start();
412
- let attempts = 0;
413
- const maxAttempts = 3;
414
- let published2 = false;
415
- while (!published2 && attempts < maxAttempts) {
416
- attempts++;
541
+ let published = false;
542
+ let otpAttempts = 0;
543
+ const MAX_OTP_ATTEMPTS = 3;
544
+ while (!published && otpAttempts < MAX_OTP_ATTEMPTS) {
545
+ otpAttempts++;
417
546
  try {
418
- await publishSinglePackage2({
419
- packagePath: pkg.path,
420
- packageManager,
421
- otp,
422
- dryRun,
423
- tag,
424
- access
425
- });
426
- published2 = true;
547
+ await publishSinglePackage({ packagePath: pkg.path, packageManager, otp, dryRun, tag, access });
548
+ published = true;
427
549
  results.push({ name: pkg.name, version: pkg.version, success: true });
428
- const successMsg = dryRun ? `Dry-run succeeded for ${pkg.name}@${pkg.version}` : `Published ${pkg.name}@${pkg.version}`;
429
- loader.succeed(successMsg);
430
- logger?.info("Package published successfully", {
431
- name: pkg.name,
432
- version: pkg.version,
433
- dryRun
434
- });
435
- } catch (error) {
436
- const errorMessage = error.message || String(error);
437
- if (errorMessage.includes("EOTP") || errorMessage.includes("one-time password")) {
438
- if (attempts < maxAttempts) {
550
+ loader.succeed(dryRun ? `Dry-run: ${pkg.name}@${pkg.version}` : `Published ${pkg.name}@${pkg.version}`);
551
+ } catch (err) {
552
+ const msg = err instanceof Error ? err.message : String(err);
553
+ if (isAlreadyPublished2(msg)) {
554
+ published = true;
555
+ results.push({ name: pkg.name, version: pkg.version, success: true, alreadyPublished: true });
556
+ loader.succeed(`${pkg.name}@${pkg.version} already published`);
557
+ break;
558
+ }
559
+ if (msg.includes("EOTP") || msg.includes("one-time password")) {
560
+ if (otpAttempts < MAX_OTP_ATTEMPTS) {
439
561
  loader.succeed(`\u{1F510} 2FA required for ${pkg.name}`);
440
- logger?.info("OTP required for publishing", {
441
- name: pkg.name,
442
- attempt: attempts
443
- });
444
- const rl = readline.createInterface({
445
- input: process.stdin,
446
- output: process.stdout
447
- });
562
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
448
563
  try {
449
- otp = await rl.question(" Enter OTP code: ");
564
+ const entered = await rl.question(" Enter OTP code: ");
450
565
  rl.close();
451
- if (!otp || otp.trim().length !== 6) {
452
- ui.write?.(" \u26A0\uFE0F Invalid OTP code (must be 6 digits)\n");
453
- logger?.warn("Invalid OTP code provided", { length: otp?.trim().length });
566
+ if (!entered || entered.trim().length !== 6) {
567
+ ui.write?.(" \u26A0\uFE0F Invalid OTP (must be 6 digits)\n");
454
568
  otp = void 0;
455
- continue;
569
+ } else {
570
+ otp = entered.trim();
456
571
  }
457
- loader.update({ text: `Publishing ${pkg.name}@${pkg.version} with OTP...` });
458
- loader.start();
459
- logger?.debug("Retrying with OTP");
460
572
  } catch (e) {
461
573
  rl.close();
462
574
  throw e;
463
575
  }
464
- } else {
465
- loader.fail("Max OTP attempts reached");
466
- logger?.error("Max OTP attempts reached", {
467
- name: pkg.name,
468
- attempts: maxAttempts
469
- });
470
- results.push({
471
- name: pkg.name,
472
- version: pkg.version,
473
- success: false,
474
- error: "Max OTP attempts reached"
475
- });
476
- break;
477
- }
478
- } else if (errorMessage.includes("E429") || errorMessage.includes("429 Too Many Requests")) {
479
- if (attempts < maxAttempts) {
480
- const delay = RETRY_429_DELAYS[Math.min(attempts - 1, RETRY_429_DELAYS.length - 1)];
481
- loader.update({ text: `429 rate limit \u2014 waiting ${delay / 1e3}s before retry...` });
576
+ loader.update({ text: `Publishing ${pkg.name}@${pkg.version} with OTP...` });
482
577
  loader.start();
483
- logger?.warn(`429 rate limit for ${pkg.name}, retrying in ${delay / 1e3}s`, { attempt: attempts });
484
- await new Promise((r) => setTimeout(r, delay));
485
578
  } else {
486
- loader.fail("Max retries exceeded (429 rate limit)");
487
- results.push({
488
- name: pkg.name,
489
- version: pkg.version,
490
- success: false,
491
- error: "429 rate limit: max retries exceeded"
492
- });
579
+ loader.fail("Max OTP attempts reached");
580
+ results.push({ name: pkg.name, version: pkg.version, success: false, error: "Max OTP attempts reached" });
493
581
  break;
494
582
  }
583
+ } else if (isRetryable2(msg)) {
584
+ const delay = retryDelay2(MAX_RETRIES2 - 1);
585
+ loader.update({ text: `Rate limited \u2014 waiting ${(delay / 1e3).toFixed(0)}s\u2026` });
586
+ loader.start();
587
+ await new Promise((r) => setTimeout(r, delay));
588
+ otpAttempts--;
495
589
  } else {
496
- loader.fail(`Failed: ${errorMessage}`);
497
- logger?.error("Package publish failed", {
498
- name: pkg.name,
499
- version: pkg.version,
500
- error: errorMessage
501
- });
502
- results.push({
503
- name: pkg.name,
504
- version: pkg.version,
505
- success: false,
506
- error: errorMessage
507
- });
590
+ loader.fail(`Failed: ${msg.split("\n")[0]}`);
591
+ results.push({ name: pkg.name, version: pkg.version, success: false, error: msg });
508
592
  break;
509
593
  }
510
594
  }
511
595
  }
512
596
  } finally {
513
- if (!restored) {
514
- writeFileSync(pkgJsonPath, originalPkgJson);
515
- restored = true;
516
- }
597
+ restore();
517
598
  }
518
599
  }
519
- const published = results.filter((r) => r.success).map((r) => `${r.name}@${r.version}`);
520
- const failed = results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`);
521
- const errors = results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`);
600
+ return buildResult(results, packages, dryRun);
601
+ }
602
+ function buildResult(results, packages, dryRun) {
522
603
  return {
523
604
  results,
524
- published,
525
- failed,
605
+ published: results.filter((r) => r.success && !r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
606
+ alreadyPublished: results.filter((r) => r.alreadyPublished).map((r) => `${r.name}@${r.version}`),
607
+ failed: results.filter((r) => !r.success).map((r) => `${r.name}@${r.version}`),
526
608
  skipped: dryRun ? packages.map((p) => `${p.name}@${p.version} (dry-run)`) : [],
527
- errors
609
+ errors: results.filter((r) => !r.success && r.error).map((r) => `${r.name}: ${r.error}`)
528
610
  };
529
611
  }
530
- function publishSinglePackage2(options) {
531
- const { packagePath, packageManager = "pnpm", otp, dryRun, tag, access } = options;
532
- return new Promise((resolve, reject) => {
533
- const args = ["publish"];
534
- if (packageManager === "pnpm") {
535
- args.push("--no-git-checks");
536
- }
537
- if (dryRun) {
538
- args.push("--dry-run");
539
- }
540
- if (otp) {
541
- args.push(`--otp=${otp}`);
542
- }
543
- if (tag) {
544
- args.push(`--tag=${tag}`);
545
- }
546
- if (access) {
547
- args.push(`--access=${access}`);
548
- }
549
- const child = spawn(packageManager, args, {
550
- cwd: packagePath,
551
- stdio: ["inherit", "pipe", "pipe"],
552
- shell: true
553
- });
554
- let stdout = "";
555
- let stderr = "";
556
- child.stdout?.on("data", (data) => {
557
- stdout += data.toString();
558
- });
559
- child.stderr?.on("data", (data) => {
560
- stderr += data.toString();
561
- });
562
- child.on("close", (code) => {
563
- if (code === 0) {
564
- resolve();
565
- } else {
566
- const errorOutput = stderr || stdout;
567
- reject(new Error(errorOutput));
568
- }
569
- });
570
- child.on("error", (err) => {
571
- reject(err);
572
- });
573
- });
574
- }
575
612
 
576
613
  // src/cli/commands/run.ts
577
614
  var BUMP_SYMBOL = {
@@ -711,6 +748,12 @@ var run_default = defineCommand({
711
748
  items: report.result.published.map((p) => `${ctx.ui.symbols.success} ${p}`)
712
749
  });
713
750
  }
751
+ if (!dryRun && report.result.alreadyPublished?.length) {
752
+ sections.push({
753
+ header: "Already published (skipped)",
754
+ items: report.result.alreadyPublished.map((p) => `${ctx.ui.symbols.info} ${p}`)
755
+ });
756
+ }
714
757
  if (dryRun && report.result.skipped?.length) {
715
758
  sections.push({
716
759
  header: "Would publish (dry-run)",