pubz 0.7.3 → 0.7.5

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 (2) hide show
  1. package/dist/cli.js +175 -111
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -47,24 +47,92 @@ function frameLine(text = "") {
47
47
 
48
48
  // src/discovery.ts
49
49
  import { readFile, readdir as readdir2, stat as stat2 } from "node:fs/promises";
50
- import { join as join2, resolve } from "node:path";
50
+ import { join as join3, resolve } from "node:path";
51
+
52
+ // src/config.ts
53
+ import { existsSync, readFileSync } from "node:fs";
54
+ import { join } from "node:path";
55
+
56
+ // src/log.ts
57
+ var verboseEnabled = false;
58
+ function setVerbose(enabled) {
59
+ verboseEnabled = enabled;
60
+ }
61
+ function debug(...args) {
62
+ if (verboseEnabled) {
63
+ console.error("[debug]", ...args);
64
+ }
65
+ }
66
+
67
+ // src/config.ts
68
+ var CONFIG_FILENAME = ".pubz";
69
+ var VALID_KEYS = new Set(["skip-build", "skip-publish", "always-publish", "registry"]);
70
+ function loadConfig(cwd) {
71
+ const configPath = join(cwd, CONFIG_FILENAME);
72
+ if (!existsSync(configPath)) {
73
+ debug(`No ${CONFIG_FILENAME} found at ${configPath}`);
74
+ return {};
75
+ }
76
+ const content = readFileSync(configPath, "utf-8");
77
+ const config = {};
78
+ for (const rawLine of content.split(`
79
+ `)) {
80
+ const line = rawLine.trim();
81
+ if (!line || line.startsWith("#"))
82
+ continue;
83
+ const eqIndex = line.indexOf("=");
84
+ let key;
85
+ let value;
86
+ if (eqIndex === -1) {
87
+ key = line;
88
+ } else {
89
+ key = line.slice(0, eqIndex).trim();
90
+ value = line.slice(eqIndex + 1).trim();
91
+ }
92
+ if (!VALID_KEYS.has(key)) {
93
+ debug(`Ignoring unknown config key: ${key}`);
94
+ continue;
95
+ }
96
+ if (key === "registry") {
97
+ config.registry = value ?? "";
98
+ } else {
99
+ const boolKey = key;
100
+ if (value === undefined || value === "true") {
101
+ config[boolKey] = true;
102
+ } else if (value === "false") {
103
+ config[boolKey] = false;
104
+ } else {
105
+ debug(`Invalid boolean value for ${key}: ${value}`);
106
+ }
107
+ }
108
+ }
109
+ debug(`Loaded ${CONFIG_FILENAME}: ${JSON.stringify(config)}`);
110
+ return config;
111
+ }
51
112
 
52
113
  // src/glob.ts
53
114
  import { readdir, stat } from "node:fs/promises";
54
- import { join } from "node:path";
115
+ import { join as join2 } from "node:path";
55
116
  async function glob(pattern, cwd) {
56
117
  const results = [];
118
+ const isGlob = /\/\*\*?$/.test(pattern);
57
119
  const basePattern = pattern.replace(/\/\*\*?$/, "");
58
120
  const isRecursive = pattern.endsWith("/**");
59
- const basePath = join(cwd, basePattern);
121
+ const basePath = join2(cwd, basePattern);
122
+ if (!isGlob) {
123
+ try {
124
+ await stat(join2(basePath, "package.json"));
125
+ return [basePattern];
126
+ } catch {}
127
+ }
60
128
  try {
61
129
  const entries = await readdir(basePath, { withFileTypes: true });
62
130
  for (const entry of entries) {
63
131
  if (entry.isDirectory()) {
64
- const entryPath = join(basePattern, entry.name);
65
- const fullPath = join(cwd, entryPath);
132
+ const entryPath = join2(basePattern, entry.name);
133
+ const fullPath = join2(cwd, entryPath);
66
134
  try {
67
- await stat(join(fullPath, "package.json"));
135
+ await stat(join2(fullPath, "package.json"));
68
136
  results.push(entryPath);
69
137
  } catch {
70
138
  if (isRecursive) {
@@ -80,7 +148,7 @@ async function glob(pattern, cwd) {
80
148
 
81
149
  // src/discovery.ts
82
150
  async function findRootPackageJson(cwd) {
83
- const packageJsonPath = join2(cwd, "package.json");
151
+ const packageJsonPath = join3(cwd, "package.json");
84
152
  try {
85
153
  await stat2(packageJsonPath);
86
154
  return packageJsonPath;
@@ -116,10 +184,10 @@ async function discoverPackages(cwd) {
116
184
  packageDirs.push(...matches);
117
185
  }
118
186
  } else {
119
- const packagesDir = join2(cwd, "packages");
187
+ const packagesDir = join3(cwd, "packages");
120
188
  try {
121
189
  const entries = await readdir2(packagesDir, { withFileTypes: true });
122
- packageDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join2("packages", entry.name));
190
+ packageDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join3("packages", entry.name));
123
191
  } catch {
124
192
  if (!rootPackageJson.private) {
125
193
  return [
@@ -137,7 +205,7 @@ async function discoverPackages(cwd) {
137
205
  }
138
206
  for (const dir of packageDirs) {
139
207
  const pkgPath = resolve(cwd, dir);
140
- const pkgJsonPath = join2(pkgPath, "package.json");
208
+ const pkgJsonPath = join3(pkgPath, "package.json");
141
209
  try {
142
210
  const pkgJson = await readPackageJson(pkgJsonPath);
143
211
  packageNames.add(pkgJson.name);
@@ -150,13 +218,16 @@ async function discoverPackages(cwd) {
150
218
  return packages;
151
219
  }
152
220
  async function packageFromPath(path, packageJsonPath, packageJson, localDependencies) {
221
+ const pkgConfig = loadConfig(path);
153
222
  return {
154
223
  name: packageJson.name,
155
224
  version: packageJson.version,
156
225
  path,
157
226
  packageJsonPath,
158
227
  isPrivate: packageJson.private === true,
159
- localDependencies
228
+ localDependencies,
229
+ skipPublish: pkgConfig["skip-publish"],
230
+ alwaysPublish: pkgConfig["always-publish"]
160
231
  };
161
232
  }
162
233
  function findLocalDependencies(pkg, packageNames) {
@@ -338,19 +409,6 @@ async function multiSelect(message, options, allSelectedByDefault = true) {
338
409
  // src/auth.ts
339
410
  import { spawn } from "node:child_process";
340
411
  import { homedir } from "node:os";
341
-
342
- // src/log.ts
343
- var verboseEnabled = false;
344
- function setVerbose(enabled) {
345
- verboseEnabled = enabled;
346
- }
347
- function debug(...args) {
348
- if (verboseEnabled) {
349
- console.error("[debug]", ...args);
350
- }
351
- }
352
-
353
- // src/auth.ts
354
412
  async function checkNpmAuth(registry) {
355
413
  return new Promise((resolve2) => {
356
414
  const proc = spawn("npm", ["whoami", "--registry", registry], {
@@ -397,10 +455,10 @@ async function npmLogin(registry) {
397
455
  import { spawn as spawn2 } from "node:child_process";
398
456
  import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
399
457
  import { homedir as homedir2 } from "node:os";
400
- import { join as join3 } from "node:path";
458
+ import { join as join4 } from "node:path";
401
459
  var DIM_ON = "\x1B[90m";
402
460
  var DIM_OFF = "\x1B[39m";
403
- function run(command, args, cwd) {
461
+ function run(command, args, cwd, options) {
404
462
  return new Promise((resolve2) => {
405
463
  const proc = spawn2(command, args, {
406
464
  cwd,
@@ -409,17 +467,29 @@ function run(command, args, cwd) {
409
467
  let output = "";
410
468
  proc.stdout?.on("data", (data) => {
411
469
  output += data.toString();
412
- process.stdout.write(DIM_ON + data.toString() + DIM_OFF);
470
+ if (!options?.silent)
471
+ process.stdout.write(DIM_ON + data.toString() + DIM_OFF);
413
472
  });
414
473
  proc.stderr?.on("data", (data) => {
415
474
  output += data.toString();
416
- process.stderr.write(DIM_ON + data.toString() + DIM_OFF);
475
+ if (!options?.silent)
476
+ process.stderr.write(DIM_ON + data.toString() + DIM_OFF);
417
477
  });
418
478
  proc.on("close", (code) => {
419
479
  resolve2({ code: code ?? 1, output });
420
480
  });
421
481
  });
422
482
  }
483
+ function runStdout(command, args, cwd) {
484
+ return new Promise((resolve2) => {
485
+ const proc = spawn2(command, args, { cwd, stdio: ["inherit", "pipe", "ignore"] });
486
+ let stdout = "";
487
+ proc.stdout?.on("data", (data) => {
488
+ stdout += data.toString();
489
+ });
490
+ proc.on("close", () => resolve2(stdout));
491
+ });
492
+ }
423
493
  function runInteractive(command, args, cwd) {
424
494
  return new Promise((resolve2) => {
425
495
  const proc = spawn2(command, args, {
@@ -466,7 +536,7 @@ async function verifyBuild(pkg) {
466
536
  filesToCheck.push("./dist/index.js");
467
537
  }
468
538
  for (const file of filesToCheck) {
469
- const filePath = join3(pkg.path, file);
539
+ const filePath = join4(pkg.path, file);
470
540
  try {
471
541
  await stat3(filePath);
472
542
  } catch {
@@ -479,6 +549,10 @@ function isOtpError(output) {
479
549
  return output.includes("EOTP") || output.includes("one-time password");
480
550
  }
481
551
  var NPM_COMMAND = process.env.PUBZ_NPM_COMMAND ?? "npm";
552
+ async function isVersionPublished(packageName, version, registry) {
553
+ const result = await runStdout(NPM_COMMAND, ["view", `${packageName}@${version}`, "version", "--registry", registry], homedir2());
554
+ return result.trim() === version;
555
+ }
482
556
  async function publishPackage(pkg, registry, context, dryRun) {
483
557
  if (dryRun) {
484
558
  return { success: true };
@@ -510,8 +584,8 @@ async function publishPackage(pkg, registry, context, dryRun) {
510
584
  return { success: true };
511
585
  }
512
586
  async function hasUncommittedChanges(cwd) {
513
- const result = await run("git", ["status", "--porcelain"], cwd);
514
- const output = result.output.trim();
587
+ const stdout = await runStdout("git", ["status", "--porcelain"], cwd);
588
+ const output = stdout.trim();
515
589
  if (!output) {
516
590
  return { hasChanges: false, files: [] };
517
591
  }
@@ -524,8 +598,8 @@ async function commitVersionBump(version, cwd, dryRun) {
524
598
  if (dryRun) {
525
599
  return { success: true };
526
600
  }
527
- const statusResult = await run("git", ["status", "--porcelain"], cwd);
528
- if (!statusResult.output.trim()) {
601
+ const statusOutput = await runStdout("git", ["status", "--porcelain"], cwd);
602
+ if (!statusOutput.trim()) {
529
603
  return { success: true };
530
604
  }
531
605
  const addResult = await run("git", ["add", "-A"], cwd);
@@ -543,11 +617,15 @@ async function createGitTag(version, cwd, dryRun) {
543
617
  if (dryRun) {
544
618
  return { success: true };
545
619
  }
620
+ const existing = await runStdout("git", ["tag", "-l", tagName], cwd);
621
+ if (existing.trim() === tagName) {
622
+ return { success: true };
623
+ }
546
624
  const tagResult = await run("git", ["tag", tagName], cwd);
547
625
  if (tagResult.code !== 0) {
548
626
  return {
549
627
  success: false,
550
- error: `Failed to create tag ${tagName} (may already exist)`
628
+ error: `Failed to create tag ${tagName}`
551
629
  };
552
630
  }
553
631
  return { success: true };
@@ -574,12 +652,12 @@ import { spawn as spawn4 } from "node:child_process";
574
652
  // src/claude.ts
575
653
  import { spawn as spawn3 } from "node:child_process";
576
654
  import { readFile as readFile3 } from "node:fs/promises";
577
- import { join as join4 } from "node:path";
655
+ import { join as join5 } from "node:path";
578
656
  async function resolveCredentials() {
579
657
  const home = process.env.HOME ?? "";
580
658
  if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
581
659
  try {
582
- const tokenFile = join4(home, ".claude", "agent-oauth-token");
660
+ const tokenFile = join5(home, ".claude", "agent-oauth-token");
583
661
  const token = (await readFile3(tokenFile, "utf-8")).trim();
584
662
  if (token) {
585
663
  process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
@@ -619,7 +697,7 @@ async function resolveCredentials() {
619
697
  }
620
698
  if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
621
699
  try {
622
- const raw = await readFile3(join4(home, ".claude.json"), "utf-8");
700
+ const raw = await readFile3(join5(home, ".claude.json"), "utf-8");
623
701
  const creds = JSON.parse(raw);
624
702
  const accessToken = creds?.claudeAiOauth?.accessToken;
625
703
  if (typeof accessToken === "string" && accessToken.length > 0) {
@@ -794,6 +872,12 @@ async function createGitHubRelease(version, body, cwd, dryRun) {
794
872
  console.log(`[DRY RUN] Would create GitHub release for ${tagName}`);
795
873
  return { success: true };
796
874
  }
875
+ const existing = await runSilent(GH_COMMAND, ["release", "view", tagName], cwd);
876
+ if (existing.code === 0) {
877
+ const url2 = existing.output.trim().split(`
878
+ `).find((l) => l.startsWith("https://"))?.trim();
879
+ return { success: true, url: url2 };
880
+ }
797
881
  const result = await runSilent(GH_COMMAND, ["release", "create", tagName, "--title", tagName, "--notes", body], cwd);
798
882
  if (result.code !== 0) {
799
883
  return {
@@ -805,54 +889,6 @@ async function createGitHubRelease(version, body, cwd, dryRun) {
805
889
  return { success: true, url };
806
890
  }
807
891
 
808
- // src/config.ts
809
- import { existsSync, readFileSync } from "node:fs";
810
- import { join as join5 } from "node:path";
811
- var CONFIG_FILENAME = ".pubz";
812
- var VALID_KEYS = new Set(["skip-build", "skip-publish", "registry"]);
813
- function loadConfig(cwd) {
814
- const configPath = join5(cwd, CONFIG_FILENAME);
815
- if (!existsSync(configPath)) {
816
- debug(`No ${CONFIG_FILENAME} found at ${configPath}`);
817
- return {};
818
- }
819
- const content = readFileSync(configPath, "utf-8");
820
- const config = {};
821
- for (const rawLine of content.split(`
822
- `)) {
823
- const line = rawLine.trim();
824
- if (!line || line.startsWith("#"))
825
- continue;
826
- const eqIndex = line.indexOf("=");
827
- let key;
828
- let value;
829
- if (eqIndex === -1) {
830
- key = line;
831
- } else {
832
- key = line.slice(0, eqIndex).trim();
833
- value = line.slice(eqIndex + 1).trim();
834
- }
835
- if (!VALID_KEYS.has(key)) {
836
- debug(`Ignoring unknown config key: ${key}`);
837
- continue;
838
- }
839
- if (key === "registry") {
840
- config.registry = value ?? "";
841
- } else {
842
- const boolKey = key;
843
- if (value === undefined || value === "true") {
844
- config[boolKey] = true;
845
- } else if (value === "false") {
846
- config[boolKey] = false;
847
- } else {
848
- debug(`Invalid boolean value for ${key}: ${value}`);
849
- }
850
- }
851
- }
852
- debug(`Loaded ${CONFIG_FILENAME}: ${JSON.stringify(config)}`);
853
- return config;
854
- }
855
-
856
892
  // src/version.ts
857
893
  import { readFile as readFile4, writeFile } from "node:fs/promises";
858
894
  async function transformWorkspaceProtocolForPublish(packages, newVersion, dryRun) {
@@ -1149,13 +1185,20 @@ async function main() {
1149
1185
  process.exit(1);
1150
1186
  }
1151
1187
  let packages = await discoverPackages(cwd);
1152
- const publishablePackages = packages.filter((p) => !p.isPrivate || options.skipPublish);
1188
+ const publishablePackages = packages.filter((p) => {
1189
+ if (p.alwaysPublish)
1190
+ return true;
1191
+ if (p.isPrivate)
1192
+ return p.skipPublish || options.skipPublish;
1193
+ return true;
1194
+ });
1153
1195
  if (publishablePackages.length === 0) {
1154
1196
  console.log(yellow("No publishable packages found."));
1155
1197
  console.log("");
1156
1198
  console.log(muted("Make sure your packages:"));
1157
1199
  console.log(muted(' - Have a package.json with a "name" field'));
1158
1200
  console.log(muted(' - Do not have "private": true (or use --skip-publish for private packages)'));
1201
+ console.log(muted(' - Do not have "skip-publish" in a per-package .pubz file'));
1159
1202
  console.log("");
1160
1203
  process.exit(1);
1161
1204
  }
@@ -1230,29 +1273,38 @@ async function main() {
1230
1273
  console.log("");
1231
1274
  }
1232
1275
  if (didBump) {
1233
- frameHeader("\uD83D\uDD16 Version");
1234
- if (options.version && ["patch", "minor", "major"].includes(options.version)) {
1235
- frameLine(`Bumping (${options.version}): ${yellow(currentVersion)} ${green(newVersion)}`);
1276
+ const onDiskVersion = JSON.parse(readFileSync2(packages[0].packageJsonPath, "utf-8")).version;
1277
+ if (onDiskVersion === newVersion) {
1278
+ console.log(dim(`⏭ Version already set to ${newVersion}, skipping bump`));
1279
+ console.log("");
1280
+ for (const pkg of packages) {
1281
+ pkg.version = newVersion;
1282
+ }
1236
1283
  } else {
1237
- frameLine(`${yellow(currentVersion)} → ${green(newVersion)}`);
1238
- }
1239
- frameLine(dim("Updating all packages..."));
1240
- for (const pkg of packages) {
1241
- await updatePackageVersion(pkg, newVersion, options.dryRun);
1242
- }
1243
- await updateLocalDependencyVersions(packages, newVersion, options.dryRun);
1244
- for (const pkg of packages) {
1245
- pkg.version = newVersion;
1246
- }
1247
- const commitResult = await commitVersionBump(newVersion, cwd, options.dryRun);
1248
- if (!commitResult.success) {
1284
+ frameHeader("\uD83D\uDD16 Version");
1285
+ if (options.version && ["patch", "minor", "major"].includes(options.version)) {
1286
+ frameLine(`Bumping (${options.version}): ${yellow(currentVersion)} → ${green(newVersion)}`);
1287
+ } else {
1288
+ frameLine(`${yellow(currentVersion)} → ${green(newVersion)}`);
1289
+ }
1290
+ frameLine(dim("Updating all packages..."));
1291
+ for (const pkg of packages) {
1292
+ await updatePackageVersion(pkg, newVersion, options.dryRun);
1293
+ }
1294
+ await updateLocalDependencyVersions(packages, newVersion, options.dryRun);
1295
+ for (const pkg of packages) {
1296
+ pkg.version = newVersion;
1297
+ }
1298
+ const commitResult = await commitVersionBump(newVersion, cwd, options.dryRun);
1299
+ if (!commitResult.success) {
1300
+ frameFooter();
1301
+ console.error(red(bold("Failed to commit version bump:")) + ` ${commitResult.error}`);
1302
+ closePrompt();
1303
+ process.exit(1);
1304
+ }
1249
1305
  frameFooter();
1250
- console.error(red(bold("Failed to commit version bump:")) + ` ${commitResult.error}`);
1251
- closePrompt();
1252
- process.exit(1);
1306
+ console.log("");
1253
1307
  }
1254
- frameFooter();
1255
- console.log("");
1256
1308
  }
1257
1309
  if (!options.skipBuild) {
1258
1310
  frameHeader("\uD83C\uDFD7️ Build");
@@ -1283,7 +1335,14 @@ async function main() {
1283
1335
  process.exit(1);
1284
1336
  }
1285
1337
  }
1286
- if (options.skipPublish) {
1338
+ const packagesToPublish = packages.filter((p) => {
1339
+ if (p.alwaysPublish)
1340
+ return true;
1341
+ if (p.skipPublish || options.skipPublish)
1342
+ return false;
1343
+ return true;
1344
+ });
1345
+ if (packagesToPublish.length === 0) {
1287
1346
  console.log(yellow(bold("⏭️ Skipping npm publish")) + dim(" — use without --skip-publish to publish to npm"));
1288
1347
  console.log("");
1289
1348
  } else {
@@ -1308,7 +1367,7 @@ async function main() {
1308
1367
  console.log(`Publishing to ${cyan(registry)}:`);
1309
1368
  }
1310
1369
  console.log("");
1311
- for (const pkg of packages) {
1370
+ for (const pkg of packagesToPublish) {
1312
1371
  console.log(` ${dim("•")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)}`);
1313
1372
  }
1314
1373
  console.log("");
@@ -1352,7 +1411,7 @@ async function main() {
1352
1411
  }
1353
1412
  }
1354
1413
  frameLine(dim("Preparing packages..."));
1355
- const workspaceTransforms = await transformWorkspaceProtocolForPublish(packages, newVersion, options.dryRun);
1414
+ const workspaceTransforms = await transformWorkspaceProtocolForPublish(packagesToPublish, newVersion, options.dryRun);
1356
1415
  const publishContext = {
1357
1416
  otp: options.otp,
1358
1417
  useBrowserAuth: !options.ci,
@@ -1363,10 +1422,15 @@ async function main() {
1363
1422
  let failedPackageName = "";
1364
1423
  let failedError = "";
1365
1424
  try {
1366
- for (const pkg of packages) {
1425
+ for (const pkg of packagesToPublish) {
1367
1426
  if (options.dryRun) {
1368
1427
  frameLine(` ${dim("[dry run]")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)}`);
1369
1428
  } else {
1429
+ const alreadyPublished = await isVersionPublished(pkg.name, newVersion, registry);
1430
+ if (alreadyPublished) {
1431
+ frameLine(` ${dim("⏭")} ${cyan(pkg.name)}${dim("@")}${yellow(newVersion)} ${dim("already published, skipping")}`);
1432
+ continue;
1433
+ }
1370
1434
  frameLine(dim(` Publishing ${pkg.name}...`));
1371
1435
  }
1372
1436
  const result = await publishPackage(pkg, registry, publishContext, options.dryRun);
@@ -1400,7 +1464,7 @@ async function main() {
1400
1464
  console.log("");
1401
1465
  }
1402
1466
  }
1403
- if (!options.skipPublish) {
1467
+ if (packagesToPublish.length > 0) {
1404
1468
  console.log("✅ " + green(bold(`Published v${newVersion}!`)));
1405
1469
  console.log("");
1406
1470
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubz",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Interactive CLI for publishing npm packages (single or monorepo)",
5
5
  "type": "module",
6
6
  "bin": {