pm-ops 2026.9.13 → 2026.9.23

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.
package/dist/index.js CHANGED
@@ -97,6 +97,16 @@ function reposFlag(description) {
97
97
  list: true,
98
98
  };
99
99
  }
100
+ function opsExamples(command, examples) {
101
+ return [`pm ops ${command}`, ...examples];
102
+ }
103
+ /** Build the shared output-format and output-file flag definitions. */
104
+ function formatOutputFlags(outputDescription) {
105
+ return [
106
+ { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
107
+ { long: "--output", value_name: "file", description: outputDescription },
108
+ ];
109
+ }
100
110
  /** Extract every --repos value that follows a command path in the raw argv array. */
101
111
  function cliRepoFlagValues(commandPath, argv = process.argv.slice(2)) {
102
112
  const commandTokens = commandPath.split(" ");
@@ -154,7 +164,7 @@ function expandHome(path) {
154
164
  return path.startsWith("~/") || path.startsWith("~\\") ? join(homedir(), path.slice(2)) : path;
155
165
  }
156
166
  function hasGlob(path) {
157
- return /[*?\[]/.test(path);
167
+ return /[*?[]/.test(path);
158
168
  }
159
169
  function escapeRegexChar(char) {
160
170
  return /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
@@ -217,6 +227,16 @@ function expandSimpleGlob(pattern) {
217
227
  // guaranteed — important for stable, diff-friendly agent output.
218
228
  return candidates.length > 0 ? [...candidates].sort() : [absolute];
219
229
  }
230
+ /** Normalize command context into the repository paths and output routing inputs. */
231
+ function resolveCommandInputs(ctx) {
232
+ const options = ctx.options;
233
+ return {
234
+ options,
235
+ repos: resolveRepos(options, ctx.args),
236
+ format: resolveFormat(options, ctx.global),
237
+ outputPath: readString(options, "output"),
238
+ };
239
+ }
220
240
  function resolveRepos(options, args = []) {
221
241
  const repos = [...asArray(options["repos"]), ...asArray(args)];
222
242
  if (repos.length > 0)
@@ -643,6 +663,21 @@ function ghOpenCount(repoPath, kind) {
643
663
  const parsed = parseJsonSafe(r.stdout);
644
664
  return Array.isArray(parsed) ? parsed.length : null;
645
665
  }
666
+ function readRepoPackageInfo(repoPath) {
667
+ const pkg = readPackageJson(repoPath);
668
+ return { pkg, name: pkg?.name ?? null, version: pkg?.version ?? null };
669
+ }
670
+ /** Read npm audit severity counts and report unavailable audits to the caller. */
671
+ function readAuditCounts(repoPath, onError) {
672
+ try {
673
+ const audit = readAudit(repoPath);
674
+ return { critical: audit.critical, high: audit.high };
675
+ }
676
+ catch (error) {
677
+ onError(auditUnavailable(error));
678
+ return { critical: null, high: null };
679
+ }
680
+ }
646
681
  /** Gather the full release-readiness snapshot for a single repository path. */
647
682
  function scanRepo(repoPath) {
648
683
  const errors = [];
@@ -670,9 +705,7 @@ function scanRepo(repoPath) {
670
705
  errors,
671
706
  };
672
707
  }
673
- const pkg = readPackageJson(repoPath);
674
- const name = pkg?.name ?? null;
675
- const version = pkg?.version ?? null;
708
+ const { pkg, name, version } = readRepoPackageInfo(repoPath);
676
709
  const strict_ts = readTsConfigStrict(repoPath);
677
710
  const has_changelog = existsSync(join(repoPath, "CHANGELOG.md"));
678
711
  const has_release_workflow = existsSync(join(repoPath, ".github", "workflows", "release.yml"));
@@ -684,16 +717,9 @@ function scanRepo(repoPath) {
684
717
  const pm_open_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "open").length : null;
685
718
  const pm_inprogress_items = items ? items.filter((i) => (i.status ?? "").toLowerCase() === "in_progress").length : null;
686
719
  const outdated_count = countOutdated(repoPath);
687
- let audit_critical = null;
688
- let audit_high = null;
689
- try {
690
- const a = readAudit(repoPath);
691
- audit_critical = a.critical;
692
- audit_high = a.high;
693
- }
694
- catch (err) {
695
- errors.push(auditUnavailable(err));
696
- }
720
+ const audit = readAuditCounts(repoPath, (message) => errors.push(message));
721
+ const audit_critical = audit.critical;
722
+ const audit_high = audit.high;
697
723
  const open_prs = ghOpenCount(repoPath, "pr");
698
724
  const open_issues = ghOpenCount(repoPath, "issue");
699
725
  const has_pkg = Boolean(pkg);
@@ -1065,6 +1091,10 @@ function verifyRelease(repos, progress) {
1065
1091
  summary: { total: results.length, passed: results.filter((r) => r.failed === 0).length, failed: results.filter((r) => r.failed > 0).length },
1066
1092
  };
1067
1093
  }
1094
+ function throwVerifyReleaseFailure(result) {
1095
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1096
+ throw new CommandError(`verify-release: ${result.summary.failed} repo(s) failed`, EXIT_CODE.GENERIC_FAILURE);
1097
+ }
1068
1098
  /** Render the verify-release result as a markdown table of checks per repo. */
1069
1099
  function renderVerifyReleaseMarkdown(result) {
1070
1100
  const lines = [
@@ -1105,9 +1135,7 @@ async function collectStatus(repoPath) {
1105
1135
  pending_receipts: null,
1106
1136
  };
1107
1137
  }
1108
- const pkg = readPackageJson(repoPath);
1109
- const name = pkg?.name ?? null;
1110
- const version = pkg?.version ?? null;
1138
+ const { pkg, name, version } = readRepoPackageInfo(repoPath);
1111
1139
  const issues = [];
1112
1140
  const strict_ts = readTsConfigStrict(repoPath);
1113
1141
  if (!strict_ts)
@@ -1126,16 +1154,9 @@ async function collectStatus(repoPath) {
1126
1154
  if (!has_pm_changelog && !self_hosts_pm_changelog)
1127
1155
  issues.push("pm-changelog not wired");
1128
1156
  const outdated_count = countOutdated(repoPath);
1129
- let audit_critical = null;
1130
- let audit_high = null;
1131
- try {
1132
- const a = readAudit(repoPath);
1133
- audit_critical = a.critical;
1134
- audit_high = a.high;
1135
- }
1136
- catch (err) {
1137
- issues.push(auditUnavailable(err));
1138
- }
1157
+ const audit = readAuditCounts(repoPath, (message) => issues.push(message));
1158
+ const audit_critical = audit.critical;
1159
+ const audit_high = audit.high;
1139
1160
  // Critical vulnerabilities gate readiness (matching scanRepo's
1140
1161
  // audit gate). High-severity findings are still pushed to issues for
1141
1162
  // textual visibility but do not block ready, so fleet-health reports
@@ -2061,7 +2082,7 @@ function renderDocstringsMarkdown(result) {
2061
2082
  const defineExtension = (module) => module;
2062
2083
  export default defineExtension({
2063
2084
  name: "pm-ops",
2064
- version: "2026.9.13",
2085
+ version: "2026.9.23",
2065
2086
  activate(api) {
2066
2087
  if (typeof api.registerRenderer === "function") {
2067
2088
  // Derived from OPS_COMMAND_PATHS rather than restated. A literal copy is
@@ -2094,23 +2115,18 @@ export default defineExtension({
2094
2115
  "(comma-separated or repeatable). --json emits clean JSON; --format markdown emits a table.",
2095
2116
  intent: "audit release readiness across many pm repositories",
2096
2117
  arguments: additionalRepoArguments(),
2097
- examples: [
2098
- "pm ops scan",
2118
+ examples: opsExamples("scan", [
2099
2119
  "pm ops scan --repos ./pm-csv ./pm-github",
2100
2120
  "pm ops scan --repos ./pm-csv,./pm-github --json",
2101
2121
  "pm ops scan --format markdown",
2102
2122
  "pm ops scan --repos ~/container/pm-* --format markdown --output FLEET.md",
2103
- ],
2123
+ ]),
2104
2124
  flags: [
2105
2125
  reposFlag("Repo paths to scan (comma-separated or repeatable; default: current dir)"),
2106
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2107
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2126
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2108
2127
  ],
2109
2128
  async run(ctx) {
2110
- const options = ctx.options;
2111
- const repos = resolveRepos(options, ctx.args);
2112
- const format = resolveFormat(options, ctx.global);
2113
- const outputPath = readString(options, "output");
2129
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2114
2130
  console.error(`pm-ops scan: ${repos.length} repo(s)`);
2115
2131
  const result = scanRepos(repos, (m) => console.error(` ${m}`));
2116
2132
  console.error(`scan: ${result.summary.ready}/${result.summary.total} ready`);
@@ -2126,25 +2142,20 @@ export default defineExtension({
2126
2142
  "({ checks: [{ id, severity, repo_filter?, params? }] }). --strict exits non-zero on any failure.",
2127
2143
  intent: "enforce naming/workflow/pm policies across many pm repositories",
2128
2144
  arguments: additionalRepoArguments(),
2129
- examples: [
2130
- "pm ops policy",
2145
+ examples: opsExamples("policy", [
2131
2146
  "pm ops policy --repos ./pm-csv ./pm-github",
2132
2147
  "pm ops policy --policy ./fleet-policy.json --strict",
2133
2148
  "pm ops policy --format markdown",
2134
- ],
2149
+ ]),
2135
2150
  flags: [
2136
2151
  reposFlag("Repo paths to check (comma-separated or repeatable; default: current dir)"),
2137
2152
  { long: "--policy", value_name: "file", description: "JSON policy bundle overriding the default checks" },
2138
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2153
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2139
2154
  { long: "--strict", description: "Exit non-zero on any failed check (any severity)" },
2140
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2141
2155
  ],
2142
2156
  async run(ctx) {
2143
- const options = ctx.options;
2144
- const repos = resolveRepos(options, ctx.args);
2145
- const format = resolveFormat(options, ctx.global);
2157
+ const { options, repos, format, outputPath } = resolveCommandInputs(ctx);
2146
2158
  const strict = readBool(options, "strict");
2147
- const outputPath = readString(options, "output");
2148
2159
  let bundle = DEFAULT_POLICY;
2149
2160
  const policyFile = readString(options, "policy");
2150
2161
  if (policyFile) {
@@ -2181,22 +2192,17 @@ export default defineExtension({
2181
2192
  "Exits non-zero if any repo fails. --output writes the report to a file.",
2182
2193
  intent: "run a release gate matrix across many pm repositories",
2183
2194
  arguments: additionalRepoArguments(),
2184
- examples: [
2185
- "pm ops verify-release",
2195
+ examples: opsExamples("verify-release", [
2186
2196
  "pm ops verify-release --repos ./pm-csv ./pm-github",
2187
2197
  "pm ops verify-release --json",
2188
2198
  "pm ops verify-release --format markdown --output RELEASE.md",
2189
- ],
2199
+ ]),
2190
2200
  flags: [
2191
2201
  reposFlag("Repo paths to verify (comma-separated or repeatable; default: current dir)"),
2192
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2193
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2202
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2194
2203
  ],
2195
2204
  async run(ctx) {
2196
- const options = ctx.options;
2197
- const repos = resolveRepos(options, ctx.args);
2198
- const format = resolveFormat(options, ctx.global);
2199
- const outputPath = readString(options, "output");
2205
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2200
2206
  console.error(`pm-ops verify-release: ${repos.length} repo(s)`);
2201
2207
  const result = verifyRelease(repos, (m) => console.error(` ${m}`));
2202
2208
  console.error(`verify-release: ${result.summary.passed}/${result.summary.total} repos passed`);
@@ -2220,10 +2226,8 @@ export default defineExtension({
2220
2226
  return { written_to: outputPath, format };
2221
2227
  }
2222
2228
  if (format === "json") {
2223
- if (failed) {
2224
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
2225
- throw new CommandError(`verify-release: ${result.summary.failed} repo(s) failed`, EXIT_CODE.GENERIC_FAILURE);
2226
- }
2229
+ if (failed)
2230
+ throwVerifyReleaseFailure(result);
2227
2231
  return renderedCommandResult(`${JSON.stringify(result, null, 2)}\n`);
2228
2232
  }
2229
2233
  if (format === "markdown") {
@@ -2234,10 +2238,8 @@ export default defineExtension({
2234
2238
  }
2235
2239
  return renderedCommandResult(md);
2236
2240
  }
2237
- if (failed) {
2238
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
2239
- throw new CommandError(`verify-release: ${result.summary.failed} repo(s) failed`, EXIT_CODE.GENERIC_FAILURE);
2240
- }
2241
+ if (failed)
2242
+ throwVerifyReleaseFailure(result);
2241
2243
  return result;
2242
2244
  },
2243
2245
  });
@@ -2249,25 +2251,20 @@ export default defineExtension({
2249
2251
  "--output writes the report to a file. Default stdout TOON.",
2250
2252
  intent: "produce a concise fleet report across many pm repositories",
2251
2253
  arguments: additionalRepoArguments(),
2252
- examples: [
2253
- "pm ops report",
2254
+ examples: opsExamples("report", [
2254
2255
  "pm ops report --repos ./pm-csv ./pm-github --format markdown",
2255
2256
  "pm ops report --format markdown --output FLEET.md",
2256
2257
  "pm ops report --format markdown --include-release --output FLEET.md",
2257
2258
  "pm ops report --json",
2258
- ],
2259
+ ]),
2259
2260
  flags: [
2260
2261
  reposFlag("Repo paths to report on (comma-separated or repeatable; default: current dir)"),
2261
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2262
- { long: "--output", value_name: "file", description: "Write the rendered report to a file instead of stdout" },
2262
+ ...formatOutputFlags("Write the rendered report to a file instead of stdout"),
2263
2263
  { long: "--include-release", description: "Also run verify-release and include results in the report" },
2264
2264
  ],
2265
2265
  async run(ctx) {
2266
- const options = ctx.options;
2267
- const repos = resolveRepos(options, ctx.args);
2268
- const format = resolveFormat(options, ctx.global);
2269
- const outputPath = readString(options, "output");
2270
- const includeRelease = readBool(options, "includeRelease", "include-release");
2266
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2267
+ const includeRelease = readBool(ctx.options, "includeRelease", "include-release");
2271
2268
  console.error(`pm-ops report: ${repos.length} repo(s)${includeRelease ? " (+release)" : ""}`);
2272
2269
  const result = buildReport(repos, (m) => console.error(` ${m}`), includeRelease);
2273
2270
  console.error(`report: scan ${result.scan.summary.ready}/${result.scan.summary.total} ready; policy ${result.policy.summary.failed} failed${result.release ? `; release ${result.release.summary.passed}/${result.release.summary.total} passed` : ""}`);
@@ -2283,21 +2280,16 @@ export default defineExtension({
2283
2280
  "emits a compact table.",
2284
2281
  intent: "get a quick fleet health overview across many pm repositories",
2285
2282
  arguments: additionalRepoArguments(),
2286
- examples: [
2287
- "pm ops status",
2283
+ examples: opsExamples("status", [
2288
2284
  "pm ops status --repos ./pm-csv ./pm-github",
2289
2285
  "pm ops status --format markdown",
2290
- ],
2286
+ ]),
2291
2287
  flags: [
2292
2288
  reposFlag("Repo paths (comma-separated or repeatable; default: current dir)"),
2293
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2294
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2289
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2295
2290
  ],
2296
2291
  async run(ctx) {
2297
- const options = ctx.options;
2298
- const repos = resolveRepos(options, ctx.args);
2299
- const format = resolveFormat(options, ctx.global);
2300
- const outputPath = readString(options, "output");
2292
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2301
2293
  console.error(`pm-ops status: ${repos.length} repo(s)`);
2302
2294
  const result = await collectStatusAll(repos, (m) => console.error(` ${m}`));
2303
2295
  console.error(`status: ${result.summary.ready}/${result.summary.total} ready, ${result.summary.total_issues} issue(s)`);
@@ -2311,21 +2303,16 @@ export default defineExtension({
2311
2303
  "by repo with per-package current/wanted/latest columns.",
2312
2304
  intent: "check dependency freshness across many pm repositories",
2313
2305
  arguments: additionalRepoArguments(),
2314
- examples: [
2315
- "pm ops outdated",
2306
+ examples: opsExamples("outdated", [
2316
2307
  "pm ops outdated --repos ./pm-csv ./pm-github",
2317
2308
  "pm ops outdated --format markdown",
2318
- ],
2309
+ ]),
2319
2310
  flags: [
2320
2311
  reposFlag("Repo paths (comma-separated or repeatable; default: current dir)"),
2321
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2322
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2312
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2323
2313
  ],
2324
2314
  async run(ctx) {
2325
- const options = ctx.options;
2326
- const repos = resolveRepos(options, ctx.args);
2327
- const format = resolveFormat(options, ctx.global);
2328
- const outputPath = readString(options, "output");
2315
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2329
2316
  console.error(`pm-ops outdated: ${repos.length} repo(s)`);
2330
2317
  const result = collectOutdatedAll(repos, (m) => console.error(` ${m}`));
2331
2318
  console.error(`outdated: ${result.summary.repos_with_outdated}/${result.summary.total} repos with outdated, ${result.summary.total_outdated} total`);
@@ -2339,21 +2326,16 @@ export default defineExtension({
2339
2326
  "fleet-wide vulnerability table.",
2340
2327
  intent: "audit security vulnerabilities across many pm repositories",
2341
2328
  arguments: additionalRepoArguments(),
2342
- examples: [
2343
- "pm ops audit",
2329
+ examples: opsExamples("audit", [
2344
2330
  "pm ops audit --repos ./pm-csv ./pm-github",
2345
2331
  "pm ops audit --format markdown",
2346
- ],
2332
+ ]),
2347
2333
  flags: [
2348
2334
  reposFlag("Repo paths (comma-separated or repeatable; default: current dir)"),
2349
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2350
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2335
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2351
2336
  ],
2352
2337
  async run(ctx) {
2353
- const options = ctx.options;
2354
- const repos = resolveRepos(options, ctx.args);
2355
- const format = resolveFormat(options, ctx.global);
2356
- const outputPath = readString(options, "output");
2338
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2357
2339
  console.error(`pm-ops audit: ${repos.length} repo(s)`);
2358
2340
  const result = collectAuditAll(repos, (m) => console.error(` ${m}`));
2359
2341
  console.error(`audit: ${result.summary.clean}/${result.summary.total} clean, ${result.summary.total_critical} critical, ${result.summary.total_high} high`);
@@ -2370,12 +2352,11 @@ export default defineExtension({
2370
2352
  "exposition format; --output writes a .prom file for the node_exporter textfile collector.",
2371
2353
  intent: "expose pm workspace metrics to Prometheus/Grafana across many pm repositories",
2372
2354
  arguments: additionalRepoArguments(),
2373
- examples: [
2374
- "pm ops metrics",
2355
+ examples: opsExamples("metrics", [
2375
2356
  "pm ops metrics --repos ./pm-csv ./pm-github",
2376
2357
  "pm ops metrics --output /var/lib/node_exporter/pm.prom",
2377
2358
  "pm ops metrics --stale-days 7 --format json",
2378
- ],
2359
+ ]),
2379
2360
  flags: [
2380
2361
  reposFlag("Repo paths (comma-separated or repeatable; default: current dir)"),
2381
2362
  { long: "--stale-days", value_name: "days", description: "Age (days) after which an active item counts as stale (default: 14)" },
@@ -2446,26 +2427,21 @@ export default defineExtension({
2446
2427
  "--warn-only reports identically but always exits 0.",
2447
2428
  intent: "gate fleet merges on clone-local decision receipts and merge-driver safety",
2448
2429
  arguments: additionalRepoArguments(),
2449
- examples: [
2450
- "pm ops merge-receipts",
2430
+ examples: opsExamples("merge-receipts", [
2451
2431
  "pm ops merge-receipts --repos ./pm-csv ./pm-github",
2452
2432
  "pm ops merge-receipts --json",
2453
2433
  "pm ops merge-receipts --warn-only",
2454
2434
  "pm ops merge-receipts --include-reconciled --format markdown",
2455
2435
  "pm ops merge-receipts --repos ~/container/pm-* --format markdown --output MERGE.md",
2456
- ],
2436
+ ]),
2457
2437
  flags: [
2458
2438
  reposFlag("Repo paths to audit (comma-separated or repeatable; default: current dir)"),
2459
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2460
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2439
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2461
2440
  { long: "--warn-only", description: "Report identically but always exit 0 (never fail the gate)" },
2462
2441
  { long: "--include-reconciled", description: "Include receipts already represented in committed history (default: pending only)" },
2463
2442
  ],
2464
2443
  async run(ctx) {
2465
- const options = ctx.options;
2466
- const repos = resolveRepos(options, ctx.args);
2467
- const format = resolveFormat(options, ctx.global);
2468
- const outputPath = readString(options, "output");
2444
+ const { options, repos, format, outputPath } = resolveCommandInputs(ctx);
2469
2445
  const warnOnly = readBool(options, "warnOnly", "warn-only");
2470
2446
  const includeReconciled = readBool(options, "includeReconciled", "include-reconciled");
2471
2447
  console.error(`pm-ops merge-receipts: ${repos.length} repo(s)${includeReconciled ? " (+reconciled)" : ""}${warnOnly ? " [warn-only]" : ""}`);
@@ -2520,22 +2496,17 @@ export default defineExtension({
2520
2496
  "commented-out lines cannot satisfy it. Exits non-zero when any repo has violations.",
2521
2497
  intent: "enforce docstring coverage across many pm repositories",
2522
2498
  arguments: additionalRepoArguments(),
2523
- examples: [
2524
- "pm ops docstrings",
2499
+ examples: opsExamples("docstrings", [
2525
2500
  "pm ops docstrings --repos ./pm-csv ./pm-github",
2526
2501
  "pm ops docstrings --format markdown",
2527
2502
  "pm ops docstrings --repos ./pm-csv --json",
2528
- ],
2503
+ ]),
2529
2504
  flags: [
2530
2505
  reposFlag("Repo paths to audit (comma-separated or repeatable; default: current dir)"),
2531
- { long: "--format", value_name: "toon|json|markdown", description: "Output format (default: toon)" },
2532
- { long: "--output", value_name: "file", description: "Write the rendered output to a file instead of stdout" },
2506
+ ...formatOutputFlags("Write the rendered output to a file instead of stdout"),
2533
2507
  ],
2534
2508
  async run(ctx) {
2535
- const options = ctx.options;
2536
- const repos = resolveRepos(options, ctx.args);
2537
- const format = resolveFormat(options, ctx.global);
2538
- const outputPath = readString(options, "output");
2509
+ const { repos, format, outputPath } = resolveCommandInputs(ctx);
2539
2510
  console.error(`pm-ops docstrings: ${repos.length} repo(s)`);
2540
2511
  const result = collectDocstringsAll(repos, (m) => console.error(` ${m}`));
2541
2512
  console.error(`docstrings: ${result.summary.total_violations} violation(s) across ${result.summary.with_violations}/${result.summary.total} repo(s)`);