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