pubz 0.3.0 → 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 +174 -7
  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) => {
@@ -455,7 +478,9 @@ async function publishPackage(pkg, registry, context, dryRun) {
455
478
  let result;
456
479
  if (context.useBrowserAuth) {
457
480
  args.push("--auth-type", "web");
458
- const interactiveResult = await runInteractive(NPM_COMMAND, args, pkg.path);
481
+ args.push(pkg.path);
482
+ context.onInteractiveStart?.();
483
+ const interactiveResult = await runInteractive(NPM_COMMAND, args, homedir2());
459
484
  result = { code: interactiveResult.code, output: "" };
460
485
  context.onInteractiveComplete?.();
461
486
  } else {
@@ -535,6 +560,114 @@ async function pushGitTag(version, cwd, dryRun) {
535
560
  return { success: true };
536
561
  }
537
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
+
538
671
  // src/version.ts
539
672
  import { readFile as readFile3, writeFile } from "node:fs/promises";
540
673
  async function transformWorkspaceProtocolForPublish(packages, newVersion, dryRun) {
@@ -702,6 +835,7 @@ Options:
702
835
  --yes, -y Skip yes/no confirmation prompts (still asks for choices)
703
836
  --ci CI mode: skip all prompts, auto-accept everything
704
837
  --version <value> Version bump type (patch|minor|major) or explicit version (required with --ci)
838
+ --verbose Show debug logging
705
839
  -h, --help Show this help message
706
840
 
707
841
  Examples:
@@ -721,6 +855,7 @@ function parseArgs(args) {
721
855
  skipConfirms: false,
722
856
  ci: false,
723
857
  version: "",
858
+ verbose: false,
724
859
  help: false
725
860
  };
726
861
  for (let i = 0;i < args.length; i++) {
@@ -748,6 +883,9 @@ function parseArgs(args) {
748
883
  case "--version":
749
884
  options.version = args[++i] || "";
750
885
  break;
886
+ case "--verbose":
887
+ options.verbose = true;
888
+ break;
751
889
  case "-h":
752
890
  case "--help":
753
891
  options.help = true;
@@ -762,6 +900,7 @@ async function main() {
762
900
  process.exit(0);
763
901
  }
764
902
  const options = parseArgs(process.argv.slice(2));
903
+ setVerbose(options.verbose);
765
904
  if (options.help) {
766
905
  printUsage();
767
906
  process.exit(0);
@@ -930,7 +1069,9 @@ async function main() {
930
1069
  console.log("");
931
1070
  console.log(yellow("Not logged in to npm.") + " Starting login...");
932
1071
  console.log("");
1072
+ pausePrompt();
933
1073
  const loginResult = await npmLogin(registry);
1074
+ resetPrompt();
934
1075
  if (!loginResult.success) {
935
1076
  console.error(red(bold("Login failed:")) + ` ${loginResult.error}`);
936
1077
  closePrompt();
@@ -1014,6 +1155,7 @@ async function main() {
1014
1155
  const publishContext = {
1015
1156
  otp: options.otp,
1016
1157
  useBrowserAuth: !options.ci,
1158
+ onInteractiveStart: pausePrompt,
1017
1159
  onInteractiveComplete: resetPrompt
1018
1160
  };
1019
1161
  let publishFailed = false;
@@ -1052,27 +1194,52 @@ async function main() {
1052
1194
  console.log("");
1053
1195
  console.log(`Published version: ${green(bold(newVersion))}`);
1054
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
+ }
1055
1203
  if (!options.dryRun) {
1056
1204
  if (options.ci) {
1057
- console.log("");
1058
1205
  console.log(cyan("Creating git tag..."));
1059
1206
  const tagResult = await createGitTag(newVersion, cwd, options.dryRun);
1060
1207
  if (tagResult.success) {
1061
1208
  console.log(cyan("Pushing tag to origin..."));
1062
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
+ }
1063
1219
  } else {
1064
1220
  console.error(red(tagResult.error ?? "Failed to create git tag"));
1065
1221
  }
1066
1222
  console.log("");
1067
- } else if (!skipConfirms) {
1068
- 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}`)}?`);
1069
1225
  if (shouldTag) {
1070
1226
  console.log("");
1071
1227
  const tagResult = await createGitTag(newVersion, cwd, options.dryRun);
1072
1228
  if (tagResult.success) {
1073
- const shouldPush = await confirm("Push tag to origin?");
1229
+ const shouldPush = skipConfirms || await confirm("Push tag to origin?");
1074
1230
  if (shouldPush) {
1075
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
+ }
1076
1243
  } else {
1077
1244
  console.log(`Tag created locally. Push manually with: ${dim(`git push origin v${newVersion}`)}`);
1078
1245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubz",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Interactive CLI for publishing npm packages (single or monorepo)",
5
5
  "type": "module",
6
6
  "bin": {