@bensandee/tooling 0.51.0 → 0.52.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 (2) hide show
  1. package/dist/bin.mjs +243 -24
  2. package/package.json +2 -2
package/dist/bin.mjs CHANGED
@@ -1369,7 +1369,7 @@ const PER_PACKAGE_DEV_DEPS = {
1369
1369
  "@types/node": {
1370
1370
  "@changesets/cli": "2.30.0",
1371
1371
  "@release-it/bumper": "7.0.5",
1372
- "@types/node": "24.12.0",
1372
+ "@types/node": "24.12.2",
1373
1373
  "commit-and-tag-version": "12.7.1",
1374
1374
  "knip": "6.3.1",
1375
1375
  "lefthook": "2.1.5",
@@ -1385,7 +1385,7 @@ const PER_PACKAGE_DEV_DEPS = {
1385
1385
  tsdown: {
1386
1386
  "@changesets/cli": "2.30.0",
1387
1387
  "@release-it/bumper": "7.0.5",
1388
- "@types/node": "24.12.0",
1388
+ "@types/node": "24.12.2",
1389
1389
  "commit-and-tag-version": "12.7.1",
1390
1390
  "knip": "6.3.1",
1391
1391
  "lefthook": "2.1.5",
@@ -1401,7 +1401,7 @@ const PER_PACKAGE_DEV_DEPS = {
1401
1401
  typescript: {
1402
1402
  "@changesets/cli": "2.30.0",
1403
1403
  "@release-it/bumper": "7.0.5",
1404
- "@types/node": "24.12.0",
1404
+ "@types/node": "24.12.2",
1405
1405
  "commit-and-tag-version": "12.7.1",
1406
1406
  "knip": "6.3.1",
1407
1407
  "lefthook": "2.1.5",
@@ -1417,7 +1417,7 @@ const PER_PACKAGE_DEV_DEPS = {
1417
1417
  vitest: {
1418
1418
  "@changesets/cli": "2.30.0",
1419
1419
  "@release-it/bumper": "7.0.5",
1420
- "@types/node": "24.12.0",
1420
+ "@types/node": "24.12.2",
1421
1421
  "commit-and-tag-version": "12.7.1",
1422
1422
  "knip": "6.3.1",
1423
1423
  "lefthook": "2.1.5",
@@ -1436,7 +1436,7 @@ const ROOT_DEV_DEPS = {
1436
1436
  knip: {
1437
1437
  "@changesets/cli": "2.30.0",
1438
1438
  "@release-it/bumper": "7.0.5",
1439
- "@types/node": "24.12.0",
1439
+ "@types/node": "24.12.2",
1440
1440
  "commit-and-tag-version": "12.7.1",
1441
1441
  "knip": "6.3.1",
1442
1442
  "lefthook": "2.1.5",
@@ -1452,7 +1452,7 @@ const ROOT_DEV_DEPS = {
1452
1452
  lefthook: {
1453
1453
  "@changesets/cli": "2.30.0",
1454
1454
  "@release-it/bumper": "7.0.5",
1455
- "@types/node": "24.12.0",
1455
+ "@types/node": "24.12.2",
1456
1456
  "commit-and-tag-version": "12.7.1",
1457
1457
  "knip": "6.3.1",
1458
1458
  "lefthook": "2.1.5",
@@ -1468,7 +1468,7 @@ const ROOT_DEV_DEPS = {
1468
1468
  oxlint: {
1469
1469
  "@changesets/cli": "2.30.0",
1470
1470
  "@release-it/bumper": "7.0.5",
1471
- "@types/node": "24.12.0",
1471
+ "@types/node": "24.12.2",
1472
1472
  "commit-and-tag-version": "12.7.1",
1473
1473
  "knip": "6.3.1",
1474
1474
  "lefthook": "2.1.5",
@@ -1509,7 +1509,7 @@ function addReleaseDeps(deps, config) {
1509
1509
  deps["release-it"] = {
1510
1510
  "@changesets/cli": "2.30.0",
1511
1511
  "@release-it/bumper": "7.0.5",
1512
- "@types/node": "24.12.0",
1512
+ "@types/node": "24.12.2",
1513
1513
  "commit-and-tag-version": "12.7.1",
1514
1514
  "knip": "6.3.1",
1515
1515
  "lefthook": "2.1.5",
@@ -1525,7 +1525,7 @@ function addReleaseDeps(deps, config) {
1525
1525
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
1526
1526
  "@changesets/cli": "2.30.0",
1527
1527
  "@release-it/bumper": "7.0.5",
1528
- "@types/node": "24.12.0",
1528
+ "@types/node": "24.12.2",
1529
1529
  "commit-and-tag-version": "12.7.1",
1530
1530
  "knip": "6.3.1",
1531
1531
  "lefthook": "2.1.5",
@@ -1543,7 +1543,7 @@ function addReleaseDeps(deps, config) {
1543
1543
  deps["commit-and-tag-version"] = {
1544
1544
  "@changesets/cli": "2.30.0",
1545
1545
  "@release-it/bumper": "7.0.5",
1546
- "@types/node": "24.12.0",
1546
+ "@types/node": "24.12.2",
1547
1547
  "commit-and-tag-version": "12.7.1",
1548
1548
  "knip": "6.3.1",
1549
1549
  "lefthook": "2.1.5",
@@ -1561,7 +1561,7 @@ function addReleaseDeps(deps, config) {
1561
1561
  deps["@changesets/cli"] = {
1562
1562
  "@changesets/cli": "2.30.0",
1563
1563
  "@release-it/bumper": "7.0.5",
1564
- "@types/node": "24.12.0",
1564
+ "@types/node": "24.12.2",
1565
1565
  "commit-and-tag-version": "12.7.1",
1566
1566
  "knip": "6.3.1",
1567
1567
  "lefthook": "2.1.5",
@@ -1596,12 +1596,12 @@ async function generatePackageJson(ctx) {
1596
1596
  const devDeps = { ...ROOT_DEV_DEPS };
1597
1597
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1598
1598
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
1599
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.51.0";
1599
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.52.0";
1600
1600
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
1601
1601
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1602
1602
  "@changesets/cli": "2.30.0",
1603
1603
  "@release-it/bumper": "7.0.5",
1604
- "@types/node": "24.12.0",
1604
+ "@types/node": "24.12.2",
1605
1605
  "commit-and-tag-version": "12.7.1",
1606
1606
  "knip": "6.3.1",
1607
1607
  "lefthook": "2.1.5",
@@ -1617,7 +1617,7 @@ async function generatePackageJson(ctx) {
1617
1617
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
1618
1618
  "@changesets/cli": "2.30.0",
1619
1619
  "@release-it/bumper": "7.0.5",
1620
- "@types/node": "24.12.0",
1620
+ "@types/node": "24.12.2",
1621
1621
  "commit-and-tag-version": "12.7.1",
1622
1622
  "knip": "6.3.1",
1623
1623
  "lefthook": "2.1.5",
@@ -1711,7 +1711,7 @@ async function generatePackageJson(ctx) {
1711
1711
  packageManager: `pnpm@${{
1712
1712
  "@changesets/cli": "2.30.0",
1713
1713
  "@release-it/bumper": "7.0.5",
1714
- "@types/node": "24.12.0",
1714
+ "@types/node": "24.12.2",
1715
1715
  "commit-and-tag-version": "12.7.1",
1716
1716
  "knip": "6.3.1",
1717
1717
  "lefthook": "2.1.5",
@@ -3370,7 +3370,7 @@ async function runGenerators(ctx) {
3370
3370
  function generateMigratePrompt(results, config, detected) {
3371
3371
  const sections = [];
3372
3372
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3373
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.51.0 repo:sync\` on ${timestamp}_\n`);
3373
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.52.0 repo:sync\` on ${timestamp}_\n`);
3374
3374
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3375
3375
  if (changed.length > 0) {
3376
3376
  const items = changed.map((r) => {
@@ -4125,6 +4125,116 @@ async function deleteActionSecret(executor, conn, secretName) {
4125
4125
  throw new TransientError(`Failed to delete secret ${secretName}: ${String(res.status)} ${res.statusText}\n${body}`);
4126
4126
  }
4127
4127
  }
4128
+ const WebhookSchema = z.object({
4129
+ id: z.number(),
4130
+ type: z.string(),
4131
+ active: z.boolean(),
4132
+ config: z.record(z.string(), z.string()).optional()
4133
+ });
4134
+ const WebhookListSchema = z.array(WebhookSchema);
4135
+ async function fetchHooks(executor, conn, endpoint) {
4136
+ const res = await executor.fetch(`${conn.serverUrl}/api/v1${endpoint}`, { headers: { Authorization: `token ${conn.token}` } });
4137
+ if (res.status === 401 || res.status === 403) return {
4138
+ status: "unauthorized",
4139
+ httpStatus: res.status
4140
+ };
4141
+ if (!res.ok) {
4142
+ const body = await responseBodyText(res);
4143
+ return {
4144
+ status: "error",
4145
+ httpStatus: res.status,
4146
+ message: `${res.statusText}\n${body}`
4147
+ };
4148
+ }
4149
+ const parsed = WebhookListSchema.safeParse(await res.json());
4150
+ if (!parsed.success) throw new UnexpectedError(`Unexpected webhook list response for ${endpoint}: ${parsed.error.message}`);
4151
+ return {
4152
+ status: "ok",
4153
+ hooks: parsed.data
4154
+ };
4155
+ }
4156
+ /** List system-level webhooks. Requires admin token (read:admin scope). */
4157
+ async function listSystemHooks(executor, conn) {
4158
+ return fetchHooks(executor, conn, "/admin/hooks");
4159
+ }
4160
+ /** List webhooks owned by the authenticated user. Requires read:user scope. */
4161
+ async function listUserHooks(executor, conn) {
4162
+ return fetchHooks(executor, conn, "/user/hooks");
4163
+ }
4164
+ /** List webhooks for an organization. Requires read:organization scope. */
4165
+ async function listOrgHooks(executor, conn, org) {
4166
+ return fetchHooks(executor, conn, `/orgs/${encodeURIComponent(org)}/hooks`);
4167
+ }
4168
+ /** List webhooks for a repository. Requires read:repository + repo admin. */
4169
+ async function listRepoHooks(executor, conn, repository) {
4170
+ return fetchHooks(executor, conn, `/repos/${repository}/hooks`);
4171
+ }
4172
+ const AuthenticatedUserSchema = z.object({ login: z.string() });
4173
+ /** Return the login of the authenticated user, or null if the token is invalid. */
4174
+ async function getAuthenticatedUser(executor, conn) {
4175
+ const res = await executor.fetch(`${conn.serverUrl}/api/v1/user`, { headers: { Authorization: `token ${conn.token}` } });
4176
+ if (!res.ok) return {
4177
+ ok: false,
4178
+ httpStatus: res.status
4179
+ };
4180
+ const parsed = AuthenticatedUserSchema.safeParse(await res.json());
4181
+ if (!parsed.success) throw new UnexpectedError(`Unexpected /user response: ${parsed.error.message}`);
4182
+ return {
4183
+ ok: true,
4184
+ login: parsed.data.login
4185
+ };
4186
+ }
4187
+ /**
4188
+ * Probe whether the token has admin access by requesting `/admin/users?limit=1`.
4189
+ *
4190
+ * Returns true on 2xx, false on 401/403. Any other status is treated as not-admin
4191
+ * to keep enumeration conservative.
4192
+ */
4193
+ async function hasAdminAccess(executor, conn) {
4194
+ return (await executor.fetch(`${conn.serverUrl}/api/v1/admin/users?limit=1`, { headers: { Authorization: `token ${conn.token}` } })).ok;
4195
+ }
4196
+ const OrgSchema = z.object({ username: z.string() });
4197
+ const OrgListSchema = z.array(OrgSchema);
4198
+ const PAGE_LIMIT = 50;
4199
+ async function paginate(executor, conn, endpoint, extract) {
4200
+ const all = [];
4201
+ let page = 1;
4202
+ const separator = endpoint.includes("?") ? "&" : "?";
4203
+ for (;;) {
4204
+ const url = `${conn.serverUrl}/api/v1${endpoint}${separator}page=${String(page)}&limit=${String(PAGE_LIMIT)}`;
4205
+ const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
4206
+ if (!res.ok) {
4207
+ const body = await responseBodyText(res);
4208
+ throw new TransientError(`Failed to fetch ${endpoint} (page ${String(page)}): ${String(res.status)} ${res.statusText}\n${body}`);
4209
+ }
4210
+ const items = extract(await res.json());
4211
+ all.push(...items);
4212
+ if (items.length < PAGE_LIMIT) break;
4213
+ page++;
4214
+ }
4215
+ return all;
4216
+ }
4217
+ /**
4218
+ * List organizations. When `mode` is "admin", uses `/admin/orgs` (all orgs on
4219
+ * the instance). When "user", uses `/user/orgs` (only the token user's orgs).
4220
+ */
4221
+ async function listOrgs(executor, conn, mode) {
4222
+ const endpoint = mode === "admin" ? "/admin/orgs" : "/user/orgs";
4223
+ return (await paginate(executor, conn, endpoint, (raw) => {
4224
+ const parsed = OrgListSchema.safeParse(raw);
4225
+ if (!parsed.success) throw new UnexpectedError(`Unexpected ${endpoint} response: ${parsed.error.message}`);
4226
+ return parsed.data;
4227
+ })).map((o) => o.username);
4228
+ }
4229
+ const RepoSearchSchema = z.object({ data: z.array(z.object({ full_name: z.string() })) });
4230
+ /** List repositories visible to the token via `/repos/search`. */
4231
+ async function searchRepos(executor, conn) {
4232
+ return (await paginate(executor, conn, "/repos/search", (raw) => {
4233
+ const parsed = RepoSearchSchema.safeParse(raw);
4234
+ if (!parsed.success) throw new UnexpectedError(`Unexpected /repos/search response: ${parsed.error.message}`);
4235
+ return parsed.data.data;
4236
+ })).map((r) => r.full_name);
4237
+ }
4128
4238
  //#endregion
4129
4239
  //#region src/release/version.ts
4130
4240
  const BRANCH = "changeset-release/main";
@@ -5480,15 +5590,33 @@ function parseComposeServices(cwd, composeFiles) {
5480
5590
  function stripComposeVariables(image) {
5481
5591
  return image.replace(/\$\{[^}]*\}/g, "");
5482
5592
  }
5593
+ /** Resolve a compose service image string to its bare name (variables and :tag stripped). */
5594
+ function bareImageName(image) {
5595
+ return stripComposeVariables(image).split(":")[0] || void 0;
5596
+ }
5483
5597
  /** Extract deduplicated bare image names (without tags) from parsed services. */
5484
5598
  function extractComposeImageNames(services) {
5485
5599
  const names = /* @__PURE__ */ new Set();
5486
5600
  for (const service of services) if (service.image) {
5487
- const bare = stripComposeVariables(service.image).split(":")[0];
5601
+ const bare = bareImageName(service.image);
5488
5602
  if (bare) names.add(bare);
5489
5603
  }
5490
5604
  return [...names];
5491
5605
  }
5606
+ /**
5607
+ * Whether a service's image refers to an image this repo builds locally.
5608
+ * Matches the bare image name (variables/tag stripped) against the list of
5609
+ * detected Docker package image names, using the same exact-or-suffix match
5610
+ * as the cross-reference warning in docker:check.
5611
+ */
5612
+ function isLocallyBuiltService(service, locallyBuiltImageNames) {
5613
+ if (!service.image) return false;
5614
+ const bare = bareImageName(service.image);
5615
+ if (!bare) return false;
5616
+ if (locallyBuiltImageNames.has(bare)) return true;
5617
+ for (const name of locallyBuiltImageNames) if (bare.endsWith(`/${name}`)) return true;
5618
+ return false;
5619
+ }
5492
5620
  /** Generate health checks from parsed services: services with exposed ports get HTTP checks, unless they define a compose-level healthcheck. */
5493
5621
  function deriveHealthChecks(services) {
5494
5622
  return services.filter((s) => s.hostPort !== void 0 && !s.hasHealthcheck).map((s) => ({
@@ -5519,14 +5647,21 @@ const CHECK_HEALTHCHECK = {
5519
5647
  };
5520
5648
  /**
5521
5649
  * Generate a check overlay YAML string from parsed services.
5522
- * Sets `restart: "no"` on all services, and overrides healthcheck intervals
5523
- * for services that define a healthcheck in the base compose file.
5650
+ * Sets `restart: "no"` on all services, overrides healthcheck intervals
5651
+ * for services that define a healthcheck in the base compose file, and
5652
+ * forces `pull_policy: never` on services whose image is locally built by
5653
+ * this repo (matched against `locallyBuiltImageNames`). The override
5654
+ * prevents pull failures when the consumer's base compose sets
5655
+ * `pull_policy: always` for prod deploys against images that only exist
5656
+ * in the local Docker cache after `docker:build`.
5524
5657
  */
5525
- function generateCheckOverlay(services) {
5658
+ function generateCheckOverlay(services, locallyBuiltImageNames = []) {
5659
+ const localSet = new Set(locallyBuiltImageNames);
5526
5660
  const serviceOverrides = {};
5527
5661
  for (const service of services) {
5528
5662
  const override = { restart: "no" };
5529
5663
  if (service.hasHealthcheck) override["healthcheck"] = { ...CHECK_HEALTHCHECK };
5664
+ if (isLocallyBuiltService(service, localSet)) override["pull_policy"] = "never";
5530
5665
  serviceOverrides[service.name] = override;
5531
5666
  }
5532
5667
  return stringify({ services: serviceOverrides });
@@ -5637,15 +5772,19 @@ const dockerCheckCommand = defineCommand({
5637
5772
  const services = parseComposeServices(composeCwd, defaults.composeFiles);
5638
5773
  const fileReader = createFileReader();
5639
5774
  const rootPkgRaw = fileReader.readFile(path.join(cwd, "package.json"));
5775
+ const locallyBuiltImageNames = [];
5640
5776
  if (rootPkgRaw) {
5641
5777
  const rootPkg = parsePackageJson(rootPkgRaw);
5642
5778
  if (rootPkg?.name) {
5643
5779
  const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
5644
5780
  const composeImages = extractComposeImageNames(services);
5645
- for (const pkg of dockerPackages) if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
5781
+ for (const pkg of dockerPackages) {
5782
+ locallyBuiltImageNames.push(pkg.imageName);
5783
+ if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
5784
+ }
5646
5785
  }
5647
5786
  }
5648
- const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services));
5787
+ const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services, locallyBuiltImageNames));
5649
5788
  const composeFiles = [
5650
5789
  ...defaults.composeFiles,
5651
5790
  tempOverlayPath,
@@ -6317,6 +6456,85 @@ const forgejoSecretsCommand = defineCommand({
6317
6456
  }
6318
6457
  });
6319
6458
  //#endregion
6459
+ //#region src/commands/forgejo-webhooks.ts
6460
+ /** Render a WebhookFetchStatus, logging warnings for unauthorized/error cases. */
6461
+ function collectGroup(label, scopeHint, status) {
6462
+ if (status.status === "ok") return status.hooks.length > 0 ? {
6463
+ label,
6464
+ hooks: status.hooks
6465
+ } : null;
6466
+ if (status.status === "unauthorized") {
6467
+ log.warn(`${label}: HTTP ${String(status.httpStatus)} — token likely missing scope: ${scopeHint}`);
6468
+ return null;
6469
+ }
6470
+ log.warn(`${label}: HTTP ${String(status.httpStatus)} ${status.message}`);
6471
+ return null;
6472
+ }
6473
+ async function runListWebhooks(executor, conn) {
6474
+ const userResult = await getAuthenticatedUser(executor, conn);
6475
+ if (!userResult.ok) throw new FatalError(`Token is invalid or lacks read:user scope (HTTP ${String(userResult.httpStatus)})`);
6476
+ const user = userResult.login;
6477
+ const isAdmin = await hasAdminAccess(executor, conn);
6478
+ const groups = [];
6479
+ const systemGroup = collectGroup("System", "read:admin", await listSystemHooks(executor, conn));
6480
+ if (systemGroup) groups.push(systemGroup);
6481
+ const userGroup = collectGroup(`User: ${user}`, "read:user", await listUserHooks(executor, conn));
6482
+ if (userGroup) groups.push(userGroup);
6483
+ const orgs = await listOrgs(executor, conn, isAdmin ? "admin" : "user");
6484
+ for (const org of orgs) {
6485
+ const orgGroup = collectGroup(`Org: ${org}`, "read:organization", await listOrgHooks(executor, conn, org));
6486
+ if (orgGroup) groups.push(orgGroup);
6487
+ }
6488
+ const repos = await searchRepos(executor, conn);
6489
+ for (const repo of repos) {
6490
+ const repoGroup = collectGroup(`Repo: ${repo}`, "read:repository + repo admin", await listRepoHooks(executor, conn, repo));
6491
+ if (repoGroup) groups.push(repoGroup);
6492
+ }
6493
+ return {
6494
+ user,
6495
+ isAdmin,
6496
+ groups,
6497
+ totalCount: groups.reduce((sum, g) => sum + g.hooks.length, 0)
6498
+ };
6499
+ }
6500
+ function renderText(inventory, serverUrl) {
6501
+ log.info(`Forgejo Webhook Inventory — ${serverUrl}`);
6502
+ log.info(`Token user: ${inventory.user}${inventory.isAdmin ? " (admin)" : ""}`);
6503
+ if (inventory.groups.length === 0) {
6504
+ log.info("No webhooks found.");
6505
+ return;
6506
+ }
6507
+ for (const group of inventory.groups) {
6508
+ log.info(`${group.label} (${String(group.hooks.length)} webhook(s))`);
6509
+ for (const hook of group.hooks) {
6510
+ const url = hook.config?.["url"] ?? "n/a";
6511
+ log.info(` id=${String(hook.id)} type=${hook.type} active=${String(hook.active)} url=${url}`);
6512
+ }
6513
+ }
6514
+ log.info(`Total: ${String(inventory.totalCount)} webhook(s)`);
6515
+ if (inventory.isAdmin) log.info("Note: User-level webhooks for other accounts cannot be enumerated via the API. Check the admin UI or run this command with tokens from individual user accounts.");
6516
+ }
6517
+ const forgejoWebhooksCommand = defineCommand({
6518
+ meta: {
6519
+ name: "forgejo:webhooks",
6520
+ description: "Enumerate webhooks across a Forgejo instance (system, user, org, and repo levels)"
6521
+ },
6522
+ args: { json: {
6523
+ type: "boolean",
6524
+ description: "Emit results as JSON instead of a human-readable summary"
6525
+ } },
6526
+ async run({ args }) {
6527
+ const resolved = resolveConnection(process.cwd());
6528
+ if (resolved.type !== "forgejo") throw new FatalError("forgejo:webhooks requires a Forgejo repository");
6529
+ const inventory = await runListWebhooks(createRealExecutor(), resolved.conn);
6530
+ if (args.json) {
6531
+ console.log(JSON.stringify(inventory, null, 2));
6532
+ return;
6533
+ }
6534
+ renderText(inventory, resolved.conn.serverUrl);
6535
+ }
6536
+ });
6537
+ //#endregion
6320
6538
  //#region src/release/webhook.ts
6321
6539
  /** Build a webhook payload from simple release context. */
6322
6540
  function buildSimplePayload(options) {
@@ -6475,7 +6693,7 @@ const webhookSendCommand = defineCommand({
6475
6693
  const main = defineCommand({
6476
6694
  meta: {
6477
6695
  name: "bst",
6478
- version: "0.51.0",
6696
+ version: "0.52.0",
6479
6697
  description: "Bootstrap and maintain standardized TypeScript project tooling"
6480
6698
  },
6481
6699
  subCommands: {
@@ -6493,10 +6711,11 @@ const main = defineCommand({
6493
6711
  "hooks:sync": hooksSyncCommand,
6494
6712
  "setup:secrets": setupSecretsCommand,
6495
6713
  "forgejo:secrets": forgejoSecretsCommand,
6714
+ "forgejo:webhooks": forgejoWebhooksCommand,
6496
6715
  "webhook:send": webhookSendCommand
6497
6716
  }
6498
6717
  });
6499
- console.log(`@bensandee/tooling v0.51.0`);
6718
+ console.log(`@bensandee/tooling v0.52.0`);
6500
6719
  async function run() {
6501
6720
  await runMain(main);
6502
6721
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,7 +48,7 @@
48
48
  "devDependencies": {
49
49
  "@changesets/cli": "2.30.0",
50
50
  "@release-it/bumper": "7.0.5",
51
- "@types/node": "24.12.0",
51
+ "@types/node": "24.12.2",
52
52
  "@types/picomatch": "4.0.3",
53
53
  "commit-and-tag-version": "12.7.1",
54
54
  "prettier": "3.8.2",