pubz 0.2.12 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +7 -2
  2. package/dist/cli.js +295 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # `pubz`
2
2
 
3
+ <img width="1024" height="1024" alt="image" src="https://github.com/user-attachments/assets/11ffa33c-e895-4a7d-b2c3-dfadde8dd124" />
4
+
5
+ ---
6
+
3
7
  ```bash
4
8
  bunx pubz
5
9
  ```
@@ -14,8 +18,9 @@ bunx pubz
14
18
  6. Commits version changes
15
19
  7. Prompts you for where you want to publish (e.g. `npm` or private registry)
16
20
  8. Builds packages
17
- 9. Publishes to npm
18
- 10. Prompts you to create a `git tag` and push it
21
+ 9. Transforms `workspace:` definitions to hard version numbers (so `npm` can be used for publishing with OIDC support).
22
+ 10. Publishes to npm
23
+ 11. Prompts you to create a `git tag` and push it
19
24
 
20
25
  ## Options
21
26
 
package/dist/cli.js CHANGED
@@ -195,6 +195,9 @@ function prompt(question) {
195
195
  function closePrompt() {
196
196
  rl.close();
197
197
  }
198
+ function pausePrompt() {
199
+ rl.close();
200
+ }
198
201
  function resetPrompt() {
199
202
  rl.close();
200
203
  rl = readline.createInterface({
@@ -317,10 +320,25 @@ async function multiSelect(message, options, allSelectedByDefault = true) {
317
320
 
318
321
  // src/auth.ts
319
322
  import { spawn } from "node:child_process";
323
+ import { homedir } from "node:os";
324
+
325
+ // src/log.ts
326
+ var verboseEnabled = false;
327
+ function setVerbose(enabled) {
328
+ verboseEnabled = enabled;
329
+ }
330
+ function debug(...args) {
331
+ if (verboseEnabled) {
332
+ console.error("[debug]", ...args);
333
+ }
334
+ }
335
+
336
+ // src/auth.ts
320
337
  async function checkNpmAuth(registry) {
321
338
  return new Promise((resolve2) => {
322
339
  const proc = spawn("npm", ["whoami", "--registry", registry], {
323
- stdio: ["inherit", "pipe", "pipe"]
340
+ stdio: ["ignore", "pipe", "pipe"],
341
+ cwd: homedir()
324
342
  });
325
343
  let stdout = "";
326
344
  let stderr = "";
@@ -334,6 +352,7 @@ async function checkNpmAuth(registry) {
334
352
  if (code === 0 && stdout.trim()) {
335
353
  resolve2({ authenticated: true, username: stdout.trim() });
336
354
  } else {
355
+ debug(`npm whoami failed: code=${code}, stdout=${JSON.stringify(stdout)}, stderr=${JSON.stringify(stderr)}`);
337
356
  resolve2({ authenticated: false });
338
357
  }
339
358
  });
@@ -341,10 +360,13 @@ async function checkNpmAuth(registry) {
341
360
  }
342
361
  async function npmLogin(registry) {
343
362
  return new Promise((resolve2) => {
363
+ debug(`spawning: npm login --registry ${registry}`);
344
364
  const proc = spawn("npm", ["login", "--registry", registry], {
345
- stdio: "inherit"
365
+ stdio: "inherit",
366
+ cwd: homedir()
346
367
  });
347
368
  proc.on("close", (code) => {
369
+ debug(`npm login exited: code=${code}`);
348
370
  if (code === 0) {
349
371
  resolve2({ success: true });
350
372
  } else {
@@ -357,6 +379,7 @@ async function npmLogin(registry) {
357
379
  // src/publish.ts
358
380
  import { spawn as spawn2 } from "node:child_process";
359
381
  import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
382
+ import { homedir as homedir2 } from "node:os";
360
383
  import { join as join3 } from "node:path";
361
384
  function run(command, args, cwd) {
362
385
  return new Promise((resolve2) => {
@@ -441,6 +464,7 @@ async function verifyBuild(pkg) {
441
464
  function isOtpError(output) {
442
465
  return output.includes("EOTP") || output.includes("one-time password");
443
466
  }
467
+ var NPM_COMMAND = process.env.PUBZ_NPM_COMMAND ?? "npm";
444
468
  async function publishPackage(pkg, registry, context, dryRun) {
445
469
  if (dryRun) {
446
470
  console.log(` [DRY RUN] Would publish ${pkg.name}@${pkg.version} to ${registry}`);
@@ -454,11 +478,13 @@ async function publishPackage(pkg, registry, context, dryRun) {
454
478
  let result;
455
479
  if (context.useBrowserAuth) {
456
480
  args.push("--auth-type", "web");
457
- const interactiveResult = await runInteractive("npm", args, pkg.path);
481
+ args.push(pkg.path);
482
+ context.onInteractiveStart?.();
483
+ const interactiveResult = await runInteractive(NPM_COMMAND, args, homedir2());
458
484
  result = { code: interactiveResult.code, output: "" };
459
485
  context.onInteractiveComplete?.();
460
486
  } else {
461
- result = await run("npm", args, pkg.path);
487
+ result = await run(NPM_COMMAND, args, pkg.path);
462
488
  }
463
489
  if (result.code !== 0) {
464
490
  if (isOtpError(result.output)) {
@@ -534,8 +560,184 @@ async function pushGitTag(version, cwd, dryRun) {
534
560
  return { success: true };
535
561
  }
536
562
 
563
+ // src/changelog.ts
564
+ import { spawn as spawn3 } from "node:child_process";
565
+ function parseGitRemoteUrl(remoteUrl) {
566
+ const sshMatch = remoteUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
567
+ if (sshMatch) {
568
+ return `https://${sshMatch[1]}/${sshMatch[2]}`;
569
+ }
570
+ const httpsMatch = remoteUrl.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
571
+ if (httpsMatch) {
572
+ return `https://${httpsMatch[1]}/${httpsMatch[2]}`;
573
+ }
574
+ return null;
575
+ }
576
+ function runSilent(command, args, cwd) {
577
+ return new Promise((resolve2) => {
578
+ const proc = spawn3(command, args, {
579
+ cwd,
580
+ stdio: ["ignore", "pipe", "pipe"]
581
+ });
582
+ let output = "";
583
+ proc.stdout?.on("data", (data) => {
584
+ output += data.toString();
585
+ });
586
+ proc.stderr?.on("data", (data) => {
587
+ output += data.toString();
588
+ });
589
+ proc.on("close", (code) => {
590
+ resolve2({ code: code ?? 1, output });
591
+ });
592
+ });
593
+ }
594
+ async function getPreviousTag(cwd) {
595
+ const result = await runSilent("git", ["tag", "--sort=-version:refname"], cwd);
596
+ if (result.code !== 0)
597
+ return null;
598
+ const tags = result.output.trim().split(`
599
+ `).filter((t) => t.length > 0);
600
+ return tags[0] ?? null;
601
+ }
602
+ async function getRepoUrl(cwd) {
603
+ const result = await runSilent("git", ["remote", "get-url", "origin"], cwd);
604
+ if (result.code !== 0)
605
+ return null;
606
+ return parseGitRemoteUrl(result.output.trim());
607
+ }
608
+ async function getCommitsSince(ref, cwd) {
609
+ const result = await runSilent("git", ["log", `${ref}..HEAD`, "--oneline", "--no-decorate"], cwd);
610
+ if (result.code !== 0)
611
+ return [];
612
+ return result.output.trim().split(`
613
+ `).filter((line) => line.length > 0).map((line) => {
614
+ const spaceIdx = line.indexOf(" ");
615
+ return {
616
+ sha: line.slice(0, spaceIdx),
617
+ message: line.slice(spaceIdx + 1)
618
+ };
619
+ });
620
+ }
621
+ function isReleaseCommit(message) {
622
+ return /^chore: release v/.test(message);
623
+ }
624
+ function formatChangelogTerminal(commits) {
625
+ const filtered = commits.filter((c) => !isReleaseCommit(c.message));
626
+ if (filtered.length === 0)
627
+ return "";
628
+ return filtered.map((c) => ` ${dim(c.sha)} ${c.message}`).join(`
629
+ `);
630
+ }
631
+ function formatChangelogMarkdown(commits, repoUrl) {
632
+ const filtered = commits.filter((c) => !isReleaseCommit(c.message));
633
+ if (filtered.length === 0)
634
+ return "";
635
+ return filtered.map((c) => {
636
+ const shaRef = repoUrl ? `[\`${c.sha}\`](${repoUrl}/commit/${c.sha})` : `\`${c.sha}\``;
637
+ return `- ${shaRef} ${c.message}`;
638
+ }).join(`
639
+ `);
640
+ }
641
+ async function generateChangelog(cwd) {
642
+ const [previousTag, repoUrl] = await Promise.all([
643
+ getPreviousTag(cwd),
644
+ getRepoUrl(cwd)
645
+ ]);
646
+ if (!previousTag) {
647
+ return { commits: [], terminal: "", markdown: "", previousTag: null, repoUrl };
648
+ }
649
+ const commits = await getCommitsSince(previousTag, cwd);
650
+ const terminal = formatChangelogTerminal(commits);
651
+ const markdown = formatChangelogMarkdown(commits, repoUrl);
652
+ return { commits, terminal, markdown, previousTag, repoUrl };
653
+ }
654
+ async function createGitHubRelease(version, body, cwd, dryRun) {
655
+ const tagName = `v${version}`;
656
+ if (dryRun) {
657
+ console.log(`[DRY RUN] Would create GitHub release for ${tagName}`);
658
+ return { success: true };
659
+ }
660
+ const result = await runSilent("gh", ["release", "create", tagName, "--title", tagName, "--notes", body], cwd);
661
+ if (result.code !== 0) {
662
+ return {
663
+ success: false,
664
+ error: result.output.trim() || `Failed to create GitHub release for ${tagName}`
665
+ };
666
+ }
667
+ const url = result.output.trim();
668
+ return { success: true, url };
669
+ }
670
+
537
671
  // src/version.ts
538
672
  import { readFile as readFile3, writeFile } from "node:fs/promises";
673
+ async function transformWorkspaceProtocolForPublish(packages, newVersion, dryRun) {
674
+ const packageNames = new Set(packages.map((p) => p.name));
675
+ const transforms = [];
676
+ for (const pkg of packages) {
677
+ const content = await readFile3(pkg.packageJsonPath, "utf-8");
678
+ const packageJson = JSON.parse(content);
679
+ let modified = false;
680
+ for (const depType of [
681
+ "dependencies",
682
+ "devDependencies",
683
+ "peerDependencies"
684
+ ]) {
685
+ const deps = packageJson[depType];
686
+ if (!deps)
687
+ continue;
688
+ for (const depName of Object.keys(deps)) {
689
+ if (packageNames.has(depName)) {
690
+ const oldVersion = deps[depName];
691
+ if (oldVersion.startsWith("workspace:")) {
692
+ const modifier = oldVersion.replace("workspace:", "");
693
+ const newVersionSpec = modifier === "*" || modifier === "" ? newVersion : `${modifier}${newVersion}`;
694
+ if (dryRun) {
695
+ console.log(` [DRY RUN] Would temporarily transform ${pkg.name} ${depType}.${depName}: ${oldVersion} -> ${newVersionSpec}`);
696
+ } else {
697
+ transforms.push({
698
+ packageJsonPath: pkg.packageJsonPath,
699
+ depType,
700
+ depName,
701
+ originalValue: oldVersion
702
+ });
703
+ deps[depName] = newVersionSpec;
704
+ modified = true;
705
+ }
706
+ }
707
+ }
708
+ }
709
+ }
710
+ if (modified && !dryRun) {
711
+ await writeFile(pkg.packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
712
+ `);
713
+ console.log(` Transformed workspace references in ${pkg.name}`);
714
+ }
715
+ }
716
+ return transforms;
717
+ }
718
+ async function restoreWorkspaceProtocol(transforms) {
719
+ const byPath = new Map;
720
+ for (const transform of transforms) {
721
+ const existing = byPath.get(transform.packageJsonPath) ?? [];
722
+ existing.push(transform);
723
+ byPath.set(transform.packageJsonPath, existing);
724
+ }
725
+ for (const [packageJsonPath, pathTransforms] of byPath) {
726
+ const content = await readFile3(packageJsonPath, "utf-8");
727
+ const packageJson = JSON.parse(content);
728
+ for (const transform of pathTransforms) {
729
+ const deps = packageJson[transform.depType];
730
+ if (deps) {
731
+ deps[transform.depName] = transform.originalValue;
732
+ }
733
+ }
734
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
735
+ `);
736
+ }
737
+ if (transforms.length > 0) {
738
+ console.log(` Restored workspace references in ${byPath.size} package(s)`);
739
+ }
740
+ }
539
741
  function bumpVersion(version, type) {
540
742
  if (type === "none")
541
743
  return version;
@@ -633,6 +835,7 @@ Options:
633
835
  --yes, -y Skip yes/no confirmation prompts (still asks for choices)
634
836
  --ci CI mode: skip all prompts, auto-accept everything
635
837
  --version <value> Version bump type (patch|minor|major) or explicit version (required with --ci)
838
+ --verbose Show debug logging
636
839
  -h, --help Show this help message
637
840
 
638
841
  Examples:
@@ -652,6 +855,7 @@ function parseArgs(args) {
652
855
  skipConfirms: false,
653
856
  ci: false,
654
857
  version: "",
858
+ verbose: false,
655
859
  help: false
656
860
  };
657
861
  for (let i = 0;i < args.length; i++) {
@@ -679,6 +883,9 @@ function parseArgs(args) {
679
883
  case "--version":
680
884
  options.version = args[++i] || "";
681
885
  break;
886
+ case "--verbose":
887
+ options.verbose = true;
888
+ break;
682
889
  case "-h":
683
890
  case "--help":
684
891
  options.help = true;
@@ -693,6 +900,7 @@ async function main() {
693
900
  process.exit(0);
694
901
  }
695
902
  const options = parseArgs(process.argv.slice(2));
903
+ setVerbose(options.verbose);
696
904
  if (options.help) {
697
905
  printUsage();
698
906
  process.exit(0);
@@ -861,7 +1069,9 @@ async function main() {
861
1069
  console.log("");
862
1070
  console.log(yellow("Not logged in to npm.") + " Starting login...");
863
1071
  console.log("");
1072
+ pausePrompt();
864
1073
  const loginResult = await npmLogin(registry);
1074
+ resetPrompt();
865
1075
  if (!loginResult.success) {
866
1076
  console.error(red(bold("Login failed:")) + ` ${loginResult.error}`);
867
1077
  closePrompt();
@@ -915,47 +1125,68 @@ async function main() {
915
1125
  console.log("");
916
1126
  if (options.dryRun) {
917
1127
  console.log(yellow("[DRY RUN]") + ` Would publish the following packages to ${cyan(registry)}:`);
918
- console.log("");
919
- for (const pkg of packages) {
920
- console.log(` ${dim("•")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)}`);
921
- }
922
- console.log("");
923
- console.log(muted("Run without --dry-run to actually publish."));
924
1128
  } else {
925
1129
  console.log("About to publish the following packages:");
926
- console.log("");
927
- for (const pkg of packages) {
928
- console.log(` ${dim("•")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)}`);
929
- }
930
- console.log("");
931
- console.log(`Registry: ${cyan(registry)}`);
932
- console.log("");
933
- if (!skipConfirms) {
934
- const shouldContinue = await confirm("Continue?");
935
- if (!shouldContinue) {
936
- console.log(yellow("Publish cancelled."));
937
- closePrompt();
938
- process.exit(0);
939
- }
1130
+ }
1131
+ console.log("");
1132
+ for (const pkg of packages) {
1133
+ console.log(` ${dim("•")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)}`);
1134
+ }
1135
+ console.log("");
1136
+ console.log(`Registry: ${cyan(registry)}`);
1137
+ console.log("");
1138
+ if (!options.dryRun && !skipConfirms) {
1139
+ const shouldContinue = await confirm("Continue?");
1140
+ if (!shouldContinue) {
1141
+ console.log(yellow("Publish cancelled."));
1142
+ closePrompt();
1143
+ process.exit(0);
940
1144
  }
941
1145
  console.log("");
942
- console.log(cyan("Publishing packages..."));
1146
+ }
1147
+ console.log(cyan("Preparing packages for publish..."));
1148
+ console.log("");
1149
+ const workspaceTransforms = await transformWorkspaceProtocolForPublish(packages, newVersion, options.dryRun);
1150
+ if (workspaceTransforms.length > 0 || options.dryRun) {
943
1151
  console.log("");
944
- const publishContext = {
945
- otp: options.otp,
946
- useBrowserAuth: !options.ci,
947
- onInteractiveComplete: resetPrompt
948
- };
1152
+ }
1153
+ console.log(cyan("Publishing packages..."));
1154
+ console.log("");
1155
+ const publishContext = {
1156
+ otp: options.otp,
1157
+ useBrowserAuth: !options.ci,
1158
+ onInteractiveStart: pausePrompt,
1159
+ onInteractiveComplete: resetPrompt
1160
+ };
1161
+ let publishFailed = false;
1162
+ let failedPackageName = "";
1163
+ let failedError = "";
1164
+ try {
949
1165
  for (const pkg of packages) {
950
1166
  const result = await publishPackage(pkg, registry, publishContext, options.dryRun);
951
1167
  if (!result.success) {
952
- console.error(red(bold("Failed to publish")) + ` ${cyan(pkg.name)}: ${result.error}`);
953
- console.log("");
954
- console.log(red("Stopping publish process."));
955
- closePrompt();
956
- process.exit(1);
1168
+ publishFailed = true;
1169
+ failedPackageName = pkg.name;
1170
+ failedError = result.error ?? "Unknown error";
1171
+ break;
957
1172
  }
958
1173
  }
1174
+ } finally {
1175
+ if (workspaceTransforms.length > 0) {
1176
+ console.log("");
1177
+ await restoreWorkspaceProtocol(workspaceTransforms);
1178
+ }
1179
+ }
1180
+ if (publishFailed) {
1181
+ console.error(red(bold("Failed to publish")) + ` ${cyan(failedPackageName)}: ${failedError}`);
1182
+ console.log("");
1183
+ console.log(red("Stopping publish process."));
1184
+ closePrompt();
1185
+ process.exit(1);
1186
+ }
1187
+ if (options.dryRun) {
1188
+ console.log("");
1189
+ console.log(muted("Run without --dry-run to actually publish."));
959
1190
  }
960
1191
  console.log("");
961
1192
  console.log(dim("═".repeat(30)));
@@ -963,27 +1194,52 @@ async function main() {
963
1194
  console.log("");
964
1195
  console.log(`Published version: ${green(bold(newVersion))}`);
965
1196
  console.log("");
1197
+ const changelog = await generateChangelog(cwd);
1198
+ if (changelog.terminal) {
1199
+ console.log(bold("Changes since ") + cyan(changelog.previousTag ?? "initial") + bold(":"));
1200
+ console.log(changelog.terminal);
1201
+ console.log("");
1202
+ }
966
1203
  if (!options.dryRun) {
967
1204
  if (options.ci) {
968
- console.log("");
969
1205
  console.log(cyan("Creating git tag..."));
970
1206
  const tagResult = await createGitTag(newVersion, cwd, options.dryRun);
971
1207
  if (tagResult.success) {
972
1208
  console.log(cyan("Pushing tag to origin..."));
973
1209
  await pushGitTag(newVersion, cwd, options.dryRun);
1210
+ if (changelog.markdown) {
1211
+ console.log(cyan("Creating GitHub release..."));
1212
+ const releaseResult = await createGitHubRelease(newVersion, changelog.markdown, cwd, options.dryRun);
1213
+ if (releaseResult.success && releaseResult.url) {
1214
+ console.log(` Release created: ${cyan(releaseResult.url)}`);
1215
+ } else if (!releaseResult.success) {
1216
+ console.error(yellow(releaseResult.error ?? "Failed to create GitHub release"));
1217
+ }
1218
+ }
974
1219
  } else {
975
1220
  console.error(red(tagResult.error ?? "Failed to create git tag"));
976
1221
  }
977
1222
  console.log("");
978
- } else if (!skipConfirms) {
979
- const shouldTag = await confirm(`Create a git tag for ${cyan(`v${newVersion}`)}?`);
1223
+ } else {
1224
+ const shouldTag = skipConfirms || await confirm(`Create a git tag for ${cyan(`v${newVersion}`)}?`);
980
1225
  if (shouldTag) {
981
1226
  console.log("");
982
1227
  const tagResult = await createGitTag(newVersion, cwd, options.dryRun);
983
1228
  if (tagResult.success) {
984
- const shouldPush = await confirm("Push tag to origin?");
1229
+ const shouldPush = skipConfirms || await confirm("Push tag to origin?");
985
1230
  if (shouldPush) {
986
1231
  await pushGitTag(newVersion, cwd, options.dryRun);
1232
+ if (changelog.markdown) {
1233
+ const shouldRelease = skipConfirms || await confirm("Create a GitHub release?");
1234
+ if (shouldRelease) {
1235
+ const releaseResult = await createGitHubRelease(newVersion, changelog.markdown, cwd, options.dryRun);
1236
+ if (releaseResult.success && releaseResult.url) {
1237
+ console.log(` Release created: ${cyan(releaseResult.url)}`);
1238
+ } else if (!releaseResult.success) {
1239
+ console.error(yellow(releaseResult.error ?? "Failed to create GitHub release"));
1240
+ }
1241
+ }
1242
+ }
987
1243
  } else {
988
1244
  console.log(`Tag created locally. Push manually with: ${dim(`git push origin v${newVersion}`)}`);
989
1245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubz",
3
- "version": "0.2.12",
3
+ "version": "0.4.0",
4
4
  "description": "Interactive CLI for publishing npm packages (single or monorepo)",
5
5
  "type": "module",
6
6
  "bin": {