@bensandee/tooling 0.25.3 → 0.27.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.
package/dist/bin.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-D41R218h.mjs";
3
3
  import { defineCommand, runMain } from "citty";
4
- import * as p from "@clack/prompts";
4
+ import * as clack from "@clack/prompts";
5
+ import { isCancel, select } from "@clack/prompts";
5
6
  import path from "node:path";
6
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
7
8
  import JSON5 from "json5";
@@ -10,7 +11,24 @@ import { z } from "zod";
10
11
  import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
11
12
  import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
12
13
  import { execSync } from "node:child_process";
14
+ import picomatch from "picomatch";
13
15
  import { tmpdir } from "node:os";
16
+ //#region src/utils/log.ts
17
+ const out = (msg) => console.log(msg);
18
+ const isCI = Boolean(process.env["CI"]);
19
+ const log$2 = isCI ? {
20
+ info: out,
21
+ warn: (msg) => out(`[warn] ${msg}`),
22
+ error: (msg) => out(`[error] ${msg}`),
23
+ success: (msg) => out(`✓ ${msg}`)
24
+ } : clack.log;
25
+ function note(body, title) {
26
+ if (isCI) {
27
+ if (title) out(`--- ${title} ---`);
28
+ out(body);
29
+ } else clack.note(body, title);
30
+ }
31
+ //#endregion
14
32
  //#region src/types.ts
15
33
  const LEGACY_TOOLS = [
16
34
  "eslint",
@@ -204,6 +222,8 @@ function computeDefaults(targetDir) {
204
222
  ci: detectCiPlatform(targetDir),
205
223
  setupRenovate: true,
206
224
  releaseStrategy: "none",
225
+ publishNpm: false,
226
+ publishDocker: false,
207
227
  projectType: isMonorepo ? "default" : detectProjectType(targetDir),
208
228
  detectPackageTypes: true
209
229
  };
@@ -256,10 +276,10 @@ function getMonorepoPackages(targetDir) {
256
276
  //#endregion
257
277
  //#region src/prompts/init-prompts.ts
258
278
  function isCancelled(value) {
259
- return p.isCancel(value);
279
+ return clack.isCancel(value);
260
280
  }
261
281
  async function runInitPrompts(targetDir, saved) {
262
- p.intro("@bensandee/tooling repo:sync");
282
+ clack.intro("@bensandee/tooling repo:sync");
263
283
  const existingPkg = readPackageJson(targetDir);
264
284
  const detected = detectProject(targetDir);
265
285
  const defaults = computeDefaults(targetDir);
@@ -276,7 +296,7 @@ async function runInitPrompts(targetDir, saved) {
276
296
  const projectType = saved?.projectType ?? defaults.projectType;
277
297
  const detectPackageTypes = saved?.detectPackageTypes ?? defaults.detectPackageTypes;
278
298
  if (detected.legacyConfigs.some((l) => l.tool === "prettier") && isFirstInit) {
279
- const formatterAnswer = await p.select({
299
+ const formatterAnswer = await clack.select({
280
300
  message: "Existing Prettier config found. Keep Prettier or migrate to oxfmt?",
281
301
  initialValue: "prettier",
282
302
  options: [{
@@ -289,14 +309,14 @@ async function runInitPrompts(targetDir, saved) {
289
309
  }]
290
310
  });
291
311
  if (isCancelled(formatterAnswer)) {
292
- p.cancel("Cancelled.");
312
+ clack.cancel("Cancelled.");
293
313
  process.exit(0);
294
314
  }
295
315
  formatter = formatterAnswer;
296
316
  }
297
317
  const detectedCi = detectCiPlatform(targetDir);
298
318
  if (isFirstInit && detectedCi === "none") {
299
- const ciAnswer = await p.select({
319
+ const ciAnswer = await clack.select({
300
320
  message: "CI workflow",
301
321
  initialValue: "forgejo",
302
322
  options: [
@@ -315,14 +335,14 @@ async function runInitPrompts(targetDir, saved) {
315
335
  ]
316
336
  });
317
337
  if (isCancelled(ciAnswer)) {
318
- p.cancel("Cancelled.");
338
+ clack.cancel("Cancelled.");
319
339
  process.exit(0);
320
340
  }
321
341
  ci = ciAnswer;
322
342
  }
323
343
  const hasExistingRelease = detected.hasReleaseItConfig || detected.hasSimpleReleaseConfig || detected.hasChangesetsConfig;
324
344
  if (isFirstInit && !hasExistingRelease) {
325
- const releaseAnswer = await p.select({
345
+ const releaseAnswer = await clack.select({
326
346
  message: "Release management",
327
347
  initialValue: defaults.releaseStrategy,
328
348
  options: [
@@ -348,12 +368,40 @@ async function runInitPrompts(targetDir, saved) {
348
368
  ]
349
369
  });
350
370
  if (isCancelled(releaseAnswer)) {
351
- p.cancel("Cancelled.");
371
+ clack.cancel("Cancelled.");
352
372
  process.exit(0);
353
373
  }
354
374
  releaseStrategy = releaseAnswer;
355
375
  }
356
- p.outro("Configuration complete!");
376
+ let publishNpm = saved?.publishNpm ?? false;
377
+ if (isFirstInit && releaseStrategy !== "none") {
378
+ if (getPublishablePackages(targetDir, structure).length > 0) {
379
+ const answer = await clack.confirm({
380
+ message: "Publish packages to npm?",
381
+ initialValue: false
382
+ });
383
+ if (isCancelled(answer)) {
384
+ clack.cancel("Cancelled.");
385
+ process.exit(0);
386
+ }
387
+ publishNpm = answer;
388
+ }
389
+ }
390
+ let publishDocker = saved?.publishDocker ?? false;
391
+ if (isFirstInit) {
392
+ if (existsSync(path.join(targetDir, "Dockerfile")) || existsSync(path.join(targetDir, "docker/Dockerfile"))) {
393
+ const answer = await clack.confirm({
394
+ message: "Publish Docker images to a registry?",
395
+ initialValue: false
396
+ });
397
+ if (isCancelled(answer)) {
398
+ clack.cancel("Cancelled.");
399
+ process.exit(0);
400
+ }
401
+ publishDocker = answer;
402
+ }
403
+ }
404
+ clack.outro("Configuration complete!");
357
405
  return {
358
406
  name,
359
407
  isNew: !isExisting,
@@ -364,6 +412,8 @@ async function runInitPrompts(targetDir, saved) {
364
412
  ci,
365
413
  setupRenovate,
366
414
  releaseStrategy,
415
+ publishNpm,
416
+ publishDocker,
367
417
  projectType,
368
418
  detectPackageTypes,
369
419
  targetDir
@@ -481,51 +531,53 @@ function createDryRunContext(config) {
481
531
  //#region src/utils/tooling-config.ts
482
532
  const CONFIG_FILE = ".tooling.json";
483
533
  const DeclarativeHealthCheckSchema = z.object({
484
- name: z.string(),
485
- url: z.string(),
486
- status: z.number().int().optional()
534
+ name: z.string().meta({ description: "Service name" }),
535
+ url: z.string().meta({ description: "Health check URL" }),
536
+ status: z.number().int().optional().meta({ description: "Expected HTTP status code" })
487
537
  });
488
538
  const DockerCheckConfigSchema = z.object({
489
- composeFiles: z.array(z.string()).optional(),
490
- envFile: z.string().optional(),
491
- services: z.array(z.string()).optional(),
492
- healthChecks: z.array(DeclarativeHealthCheckSchema).optional(),
493
- buildCommand: z.string().optional(),
494
- buildCwd: z.string().optional(),
495
- timeoutMs: z.number().int().positive().optional(),
496
- pollIntervalMs: z.number().int().positive().optional()
539
+ composeFiles: z.array(z.string()).optional().meta({ description: "Compose files to use" }),
540
+ envFile: z.string().optional().meta({ description: "Environment file for compose" }),
541
+ services: z.array(z.string()).optional().meta({ description: "Services to check (default: all)" }),
542
+ healthChecks: z.array(DeclarativeHealthCheckSchema).optional().meta({ description: "Health check definitions" }),
543
+ buildCommand: z.string().optional().meta({ description: "Command to build images before checking" }),
544
+ buildCwd: z.string().optional().meta({ description: "Working directory for build command" }),
545
+ timeoutMs: z.number().int().positive().optional().meta({ description: "Overall timeout in milliseconds" }),
546
+ pollIntervalMs: z.number().int().positive().optional().meta({ description: "Poll interval in milliseconds" })
497
547
  });
498
548
  const ToolingConfigSchema = z.strictObject({
499
- $schema: z.string().optional(),
500
- structure: z.enum(["single", "monorepo"]).optional(),
501
- useEslintPlugin: z.boolean().optional(),
502
- formatter: z.enum(["oxfmt", "prettier"]).optional(),
503
- setupVitest: z.boolean().optional(),
549
+ $schema: z.string().optional().meta({ description: "JSON Schema reference (ignored by tooling)" }),
550
+ structure: z.enum(["single", "monorepo"]).optional().meta({ description: "Project structure" }),
551
+ useEslintPlugin: z.boolean().optional().meta({ description: "Include @bensandee/eslint-plugin oxlint plugin" }),
552
+ formatter: z.enum(["oxfmt", "prettier"]).optional().meta({ description: "Formatter choice" }),
553
+ setupVitest: z.boolean().optional().meta({ description: "Generate vitest config and example test" }),
504
554
  ci: z.enum([
505
555
  "github",
506
556
  "forgejo",
507
557
  "none"
508
- ]).optional(),
509
- setupRenovate: z.boolean().optional(),
558
+ ]).optional().meta({ description: "CI platform" }),
559
+ setupRenovate: z.boolean().optional().meta({ description: "Generate Renovate config" }),
510
560
  releaseStrategy: z.enum([
511
561
  "release-it",
512
562
  "simple",
513
563
  "changesets",
514
564
  "none"
515
- ]).optional(),
565
+ ]).optional().meta({ description: "Release management strategy" }),
566
+ publishNpm: z.boolean().optional().meta({ description: "Publish packages to npm (opt-in)" }),
567
+ publishDocker: z.boolean().optional().meta({ description: "Publish Docker images to a registry (opt-in)" }),
516
568
  projectType: z.enum([
517
569
  "default",
518
570
  "node",
519
571
  "react",
520
572
  "library"
521
- ]).optional(),
522
- detectPackageTypes: z.boolean().optional(),
523
- setupDocker: z.boolean().optional(),
573
+ ]).optional().meta({ description: "Project type (determines tsconfig base)" }),
574
+ detectPackageTypes: z.boolean().optional().meta({ description: "Auto-detect project types for monorepo packages" }),
575
+ setupDocker: z.boolean().optional().meta({ description: "Generate Docker build/check scripts" }),
524
576
  docker: z.record(z.string(), z.object({
525
- dockerfile: z.string(),
526
- context: z.string().default(".")
527
- })).optional(),
528
- dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional()
577
+ dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
578
+ context: z.string().default(".").meta({ description: "Docker build context relative to package" })
579
+ })).optional().meta({ description: "Docker package overrides (package name → config)" }),
580
+ dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" })
529
581
  });
530
582
  /** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
531
583
  function loadToolingConfig(targetDir) {
@@ -545,6 +597,8 @@ const OVERRIDE_KEYS = [
545
597
  "ci",
546
598
  "setupRenovate",
547
599
  "releaseStrategy",
600
+ "publishNpm",
601
+ "publishDocker",
548
602
  "projectType",
549
603
  "detectPackageTypes"
550
604
  ];
@@ -590,6 +644,8 @@ function mergeWithSavedConfig(detected, saved) {
590
644
  ci: saved.ci ?? detected.ci,
591
645
  setupRenovate: saved.setupRenovate ?? detected.setupRenovate,
592
646
  releaseStrategy: saved.releaseStrategy ?? detected.releaseStrategy,
647
+ publishNpm: saved.publishNpm ?? detected.publishNpm,
648
+ publishDocker: saved.publishDocker ?? detected.publishDocker,
593
649
  projectType: saved.projectType ?? detected.projectType,
594
650
  detectPackageTypes: saved.detectPackageTypes ?? detected.detectPackageTypes
595
651
  };
@@ -772,7 +828,7 @@ jobs:
772
828
  DOCKER_REGISTRY_NAMESPACE: ${actionsExpr$2("vars.DOCKER_REGISTRY_NAMESPACE")}
773
829
  DOCKER_USERNAME: ${actionsExpr$2("secrets.DOCKER_USERNAME")}
774
830
  DOCKER_PASSWORD: ${actionsExpr$2("secrets.DOCKER_PASSWORD")}
775
- run: pnpm exec tooling docker:publish
831
+ run: pnpm exec bst docker:publish
776
832
  `;
777
833
  }
778
834
  function requiredDeploySteps() {
@@ -795,7 +851,7 @@ function requiredDeploySteps() {
795
851
  },
796
852
  {
797
853
  match: { run: "docker:publish" },
798
- step: { run: "pnpm exec tooling docker:publish" }
854
+ step: { run: "pnpm exec bst docker:publish" }
799
855
  }
800
856
  ];
801
857
  }
@@ -832,7 +888,7 @@ function hasDockerPackages(ctx) {
832
888
  }
833
889
  async function generateDeployCi(ctx) {
834
890
  const filePath = "deploy-ci";
835
- if (!hasDockerPackages(ctx) || ctx.config.ci === "none") return {
891
+ if (!ctx.config.publishDocker || ctx.config.ci === "none") return {
836
892
  filePath,
837
893
  action: "skipped",
838
894
  description: "Deploy CI workflow not applicable"
@@ -851,21 +907,13 @@ async function generateDeployCi(ctx) {
851
907
  };
852
908
  const merged = mergeWorkflowSteps(existing, "deploy", requiredDeploySteps());
853
909
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
854
- if (withComment === content) {
855
- ctx.write(workflowPath, content);
856
- return {
857
- filePath: workflowPath,
858
- action: "updated",
859
- description: "Added missing steps to deploy workflow"
860
- };
861
- }
862
- if (await ctx.confirmOverwrite(workflowPath) === "skip") {
863
- if (merged.changed || withComment !== merged.content) {
910
+ if (!merged.changed) {
911
+ if (withComment !== existing) {
864
912
  ctx.write(workflowPath, withComment);
865
913
  return {
866
914
  filePath: workflowPath,
867
915
  action: "updated",
868
- description: "Added missing steps to deploy workflow"
916
+ description: "Added schema comment to deploy workflow"
869
917
  };
870
918
  }
871
919
  return {
@@ -874,11 +922,11 @@ async function generateDeployCi(ctx) {
874
922
  description: "Existing deploy workflow preserved"
875
923
  };
876
924
  }
877
- ctx.write(workflowPath, content);
925
+ ctx.write(workflowPath, withComment);
878
926
  return {
879
927
  filePath: workflowPath,
880
928
  action: "updated",
881
- description: "Replaced deploy workflow with updated template"
929
+ description: "Added missing steps to deploy workflow"
882
930
  };
883
931
  }
884
932
  return {
@@ -903,10 +951,10 @@ const STANDARD_SCRIPTS_SINGLE = {
903
951
  test: "vitest run",
904
952
  lint: "oxlint",
905
953
  knip: "knip",
906
- check: "pnpm exec tooling checks:run",
907
- "ci:check": "pnpm check",
908
- "tooling:check": "pnpm exec tooling repo:sync --check",
909
- "tooling:sync": "pnpm exec tooling repo:sync"
954
+ check: "bst checks:run",
955
+ "ci:check": "pnpm check --skip 'docker:*'",
956
+ "tooling:check": "bst repo:sync --check",
957
+ "tooling:sync": "bst repo:sync"
910
958
  };
911
959
  const STANDARD_SCRIPTS_MONOREPO = {
912
960
  build: "pnpm -r build",
@@ -914,10 +962,10 @@ const STANDARD_SCRIPTS_MONOREPO = {
914
962
  typecheck: "pnpm -r --parallel run typecheck",
915
963
  lint: "oxlint",
916
964
  knip: "knip",
917
- check: "pnpm exec tooling checks:run",
918
- "ci:check": "pnpm check",
919
- "tooling:check": "pnpm exec tooling repo:sync --check",
920
- "tooling:sync": "pnpm exec tooling repo:sync"
965
+ check: "bst checks:run",
966
+ "ci:check": "pnpm check --skip 'docker:*'",
967
+ "tooling:check": "bst repo:sync --check",
968
+ "tooling:sync": "bst repo:sync"
921
969
  };
922
970
  /** Scripts that tooling owns — map from script name to keyword that must appear in the value. */
923
971
  const MANAGED_SCRIPTS = {
@@ -982,8 +1030,8 @@ function addReleaseDeps(deps, config) {
982
1030
  function getAddedDevDepNames(config) {
983
1031
  const deps = { ...ROOT_DEV_DEPS };
984
1032
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
985
- deps["@bensandee/config"] = "0.9.0";
986
- deps["@bensandee/tooling"] = "0.25.3";
1033
+ deps["@bensandee/config"] = "0.9.1";
1034
+ deps["@bensandee/tooling"] = "0.27.0";
987
1035
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
988
1036
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
989
1037
  addReleaseDeps(deps, config);
@@ -1000,15 +1048,15 @@ async function generatePackageJson(ctx) {
1000
1048
  format: formatScript
1001
1049
  };
1002
1050
  if (ctx.config.releaseStrategy === "changesets") allScripts["changeset"] = "changeset";
1003
- if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
1051
+ if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "bst release:trigger";
1004
1052
  if (hasDockerPackages(ctx)) {
1005
- allScripts["docker:build"] = "pnpm exec tooling docker:build";
1006
- allScripts["docker:check"] = "pnpm exec tooling docker:check";
1053
+ allScripts["docker:build"] = "bst docker:build";
1054
+ allScripts["docker:check"] = "bst docker:check";
1007
1055
  }
1008
1056
  const devDeps = { ...ROOT_DEV_DEPS };
1009
1057
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1010
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.0";
1011
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.25.3";
1058
+ devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1059
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.27.0";
1012
1060
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1013
1061
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
1014
1062
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -1769,8 +1817,14 @@ const ClaudeSettingsSchema = z.object({
1769
1817
  });
1770
1818
  function parseClaudeSettings(raw) {
1771
1819
  try {
1772
- const result = ClaudeSettingsSchema.safeParse(JSON.parse(raw));
1773
- return result.success ? result.data : void 0;
1820
+ const json = JSON.parse(raw);
1821
+ const rawResult = z.record(z.string(), z.unknown()).safeParse(json);
1822
+ const settingsResult = ClaudeSettingsSchema.safeParse(json);
1823
+ if (!rawResult.success || !settingsResult.success) return void 0;
1824
+ return {
1825
+ settings: settingsResult.data,
1826
+ rawJson: rawResult.data
1827
+ };
1774
1828
  } catch {
1775
1829
  return;
1776
1830
  }
@@ -1829,7 +1883,6 @@ function buildSettings(ctx) {
1829
1883
  "Bash(wc *)",
1830
1884
  "Bash(test *)",
1831
1885
  "Bash([ *)",
1832
- "Bash(find *)",
1833
1886
  "Bash(grep *)",
1834
1887
  "Bash(which *)",
1835
1888
  "Bash(node -e *)",
@@ -1896,7 +1949,7 @@ function buildSettings(ctx) {
1896
1949
  instructions: [
1897
1950
  "Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
1898
1951
  "No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
1899
- "Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option.",
1952
+ "Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option. For no-empty-function: add a `{ /* no-op */ }` comment body instead of a disable comment.",
1900
1953
  "Prefer extensionless imports; if an extension is required, use .ts over .js."
1901
1954
  ],
1902
1955
  enabledPlugins,
@@ -1920,27 +1973,44 @@ function writeOrMergeSettings(ctx, filePath, generated) {
1920
1973
  action: "skipped",
1921
1974
  description: "Could not parse existing settings"
1922
1975
  };
1923
- const missingAllow = generated.permissions.allow.filter((rule) => !parsed.permissions.allow.includes(rule));
1924
- const missingDeny = generated.permissions.deny.filter((rule) => !parsed.permissions.deny.includes(rule));
1925
- const missingInstructions = generated.instructions.filter((inst) => !parsed.instructions.includes(inst));
1926
- const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in parsed.enabledPlugins));
1927
- const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in parsed.extraKnownMarketplaces));
1928
- const added = missingAllow.length + missingDeny.length + missingInstructions.length + missingPlugins.length + missingMarketplaces.length;
1929
- if (added === 0) return {
1976
+ const { settings, rawJson } = parsed;
1977
+ const missingAllow = generated.permissions.allow.filter((rule) => !settings.permissions.allow.includes(rule));
1978
+ const missingDeny = generated.permissions.deny.filter((rule) => !settings.permissions.deny.includes(rule));
1979
+ const mergedInstructions = [...settings.instructions];
1980
+ let instructionChanges = 0;
1981
+ for (const inst of generated.instructions) {
1982
+ if (mergedInstructions.includes(inst)) continue;
1983
+ const prefixIdx = mergedInstructions.findIndex((e) => inst.startsWith(e) || e.startsWith(inst));
1984
+ if (prefixIdx !== -1) mergedInstructions[prefixIdx] = inst;
1985
+ else mergedInstructions.push(inst);
1986
+ instructionChanges++;
1987
+ }
1988
+ const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in settings.enabledPlugins));
1989
+ const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in settings.extraKnownMarketplaces));
1990
+ const changed = missingAllow.length + missingDeny.length + instructionChanges + missingPlugins.length + missingMarketplaces.length;
1991
+ if (changed === 0) return {
1930
1992
  filePath,
1931
1993
  action: "skipped",
1932
1994
  description: "Already has all rules and instructions"
1933
1995
  };
1934
- parsed.permissions.allow = [...parsed.permissions.allow, ...missingAllow];
1935
- parsed.permissions.deny = [...parsed.permissions.deny, ...missingDeny];
1936
- parsed.instructions = [...parsed.instructions, ...missingInstructions];
1937
- for (const [key, value] of missingPlugins) parsed.enabledPlugins[key] = value;
1938
- for (const [key, value] of missingMarketplaces) parsed.extraKnownMarketplaces[key] = value;
1939
- ctx.write(filePath, serializeSettings$1(parsed));
1996
+ rawJson["permissions"] = {
1997
+ allow: [...settings.permissions.allow, ...missingAllow],
1998
+ deny: [...settings.permissions.deny, ...missingDeny]
1999
+ };
2000
+ rawJson["instructions"] = mergedInstructions;
2001
+ const updatedPlugins = { ...settings.enabledPlugins };
2002
+ for (const [key, value] of missingPlugins) updatedPlugins[key] = value;
2003
+ const updatedMarketplaces = { ...settings.extraKnownMarketplaces };
2004
+ for (const [key, value] of missingMarketplaces) updatedMarketplaces[key] = value;
2005
+ if (Object.keys(updatedPlugins).length > 0) rawJson["enabledPlugins"] = updatedPlugins;
2006
+ else delete rawJson["enabledPlugins"];
2007
+ if (Object.keys(updatedMarketplaces).length > 0) rawJson["extraKnownMarketplaces"] = updatedMarketplaces;
2008
+ else delete rawJson["extraKnownMarketplaces"];
2009
+ ctx.write(filePath, JSON.stringify(rawJson, null, 2) + "\n");
1940
2010
  return {
1941
2011
  filePath,
1942
2012
  action: "updated",
1943
- description: `Added ${String(added)} rules/instructions`
2013
+ description: `Updated ${String(changed)} rules/instructions`
1944
2014
  };
1945
2015
  }
1946
2016
  ctx.write(filePath, serializeSettings$1(generated));
@@ -2114,13 +2184,13 @@ permissions:
2114
2184
  - name: Release
2115
2185
  env:
2116
2186
  GITHUB_TOKEN: \${{ github.token }}
2117
- run: pnpm exec tooling release:simple` : `
2187
+ run: pnpm exec bst release:simple` : `
2118
2188
  - name: Release
2119
2189
  env:
2120
2190
  FORGEJO_SERVER_URL: \${{ github.server_url }}
2121
2191
  FORGEJO_REPOSITORY: \${{ github.repository }}
2122
2192
  FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}
2123
- run: pnpm exec tooling release:simple`;
2193
+ run: pnpm exec bst release:simple`;
2124
2194
  return `${workflowSchemaComment(ci)}name: Release
2125
2195
  on:
2126
2196
  workflow_dispatch:
@@ -2160,7 +2230,7 @@ function changesetsReleaseStep(ci, publishesNpm) {
2160
2230
  FORGEJO_TOKEN: actionsExpr("secrets.FORGEJO_TOKEN"),
2161
2231
  ...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
2162
2232
  },
2163
- run: "pnpm exec tooling release:changesets"
2233
+ run: "pnpm exec bst release:changesets"
2164
2234
  }
2165
2235
  };
2166
2236
  }
@@ -2208,13 +2278,13 @@ function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
2208
2278
  case "simple":
2209
2279
  steps.push({
2210
2280
  match: { run: "release:simple" },
2211
- step: { run: "pnpm exec tooling release:simple" }
2281
+ step: { run: "pnpm exec bst release:simple" }
2212
2282
  });
2213
2283
  break;
2214
2284
  case "changesets":
2215
2285
  steps.push({
2216
2286
  match: { run: "changeset" },
2217
- step: { run: "pnpm exec tooling release:changesets" }
2287
+ step: { run: "pnpm exec bst release:changesets" }
2218
2288
  });
2219
2289
  break;
2220
2290
  }
@@ -2256,7 +2326,7 @@ async function generateReleaseCi(ctx) {
2256
2326
  action: "skipped",
2257
2327
  description: "Release CI workflow not applicable"
2258
2328
  };
2259
- const publishesNpm = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson).length > 0;
2329
+ const publishesNpm = ctx.config.publishNpm === true;
2260
2330
  if (ctx.config.releaseStrategy === "changesets") return generateChangesetsReleaseCi(ctx, publishesNpm);
2261
2331
  const isGitHub = ctx.config.ci === "github";
2262
2332
  const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
@@ -2277,21 +2347,13 @@ async function generateReleaseCi(ctx) {
2277
2347
  };
2278
2348
  const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm));
2279
2349
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2280
- if (withComment === content) {
2281
- ctx.write(workflowPath, content);
2282
- return {
2283
- filePath: workflowPath,
2284
- action: "updated",
2285
- description: "Added missing steps to release workflow"
2286
- };
2287
- }
2288
- if (await ctx.confirmOverwrite(workflowPath) === "skip") {
2289
- if (merged.changed || withComment !== merged.content) {
2350
+ if (!merged.changed) {
2351
+ if (withComment !== existing) {
2290
2352
  ctx.write(workflowPath, withComment);
2291
2353
  return {
2292
2354
  filePath: workflowPath,
2293
2355
  action: "updated",
2294
- description: "Added missing steps to release workflow"
2356
+ description: "Added schema comment to release workflow"
2295
2357
  };
2296
2358
  }
2297
2359
  return {
@@ -2300,11 +2362,11 @@ async function generateReleaseCi(ctx) {
2300
2362
  description: "Existing release workflow preserved"
2301
2363
  };
2302
2364
  }
2303
- ctx.write(workflowPath, content);
2365
+ ctx.write(workflowPath, withComment);
2304
2366
  return {
2305
2367
  filePath: workflowPath,
2306
2368
  action: "updated",
2307
- description: "Replaced release workflow with updated template"
2369
+ description: "Added missing steps to release workflow"
2308
2370
  };
2309
2371
  }
2310
2372
  return {
@@ -2779,9 +2841,6 @@ function imageRef(namespace, imageName, tag) {
2779
2841
  function log$1(message) {
2780
2842
  console.log(message);
2781
2843
  }
2782
- function debug$1(verbose, message) {
2783
- if (verbose) console.log(`[debug] ${message}`);
2784
- }
2785
2844
  /** Read the repo name from root package.json. */
2786
2845
  function readRepoName(executor, cwd) {
2787
2846
  const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
@@ -2791,7 +2850,7 @@ function readRepoName(executor, cwd) {
2791
2850
  return repoName;
2792
2851
  }
2793
2852
  /** Build a single docker image from its config. Paths are resolved relative to cwd. */
2794
- function buildImage(executor, pkg, cwd, verbose, extraArgs) {
2853
+ function buildImage(executor, pkg, cwd, extraArgs) {
2795
2854
  const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
2796
2855
  const contextPath = path.resolve(cwd, pkg.docker.context);
2797
2856
  const command = [
@@ -2801,10 +2860,7 @@ function buildImage(executor, pkg, cwd, verbose, extraArgs) {
2801
2860
  ...extraArgs,
2802
2861
  contextPath
2803
2862
  ].join(" ");
2804
- debug$1(verbose, `Running: ${command}`);
2805
- const buildResult = executor.exec(command);
2806
- debug$1(verbose, `Build stdout: ${buildResult.stdout}`);
2807
- if (buildResult.exitCode !== 0) throw new FatalError(`docker build failed for ${pkg.dir} (exit ${buildResult.exitCode}): ${buildResult.stderr}`);
2863
+ executor.execInherit(command);
2808
2864
  }
2809
2865
  /**
2810
2866
  * Detect packages with docker config in .tooling.json and build each one.
@@ -2818,7 +2874,7 @@ function runDockerBuild(executor, config) {
2818
2874
  if (config.packageDir) {
2819
2875
  const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
2820
2876
  log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
2821
- buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
2877
+ buildImage(executor, pkg, config.cwd, config.extraArgs);
2822
2878
  log$1(`Built ${pkg.imageName}:latest`);
2823
2879
  return { packages: [pkg] };
2824
2880
  }
@@ -2830,7 +2886,7 @@ function runDockerBuild(executor, config) {
2830
2886
  log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
2831
2887
  for (const pkg of packages) {
2832
2888
  log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
2833
- buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
2889
+ buildImage(executor, pkg, config.cwd, config.extraArgs);
2834
2890
  }
2835
2891
  log$1(`Built ${packages.length} image(s)`);
2836
2892
  return { packages };
@@ -2847,7 +2903,6 @@ function runDockerPublish(executor, config) {
2847
2903
  const { packages } = runDockerBuild(executor, {
2848
2904
  cwd: config.cwd,
2849
2905
  packageDir: void 0,
2850
- verbose: config.verbose,
2851
2906
  extraArgs: []
2852
2907
  });
2853
2908
  if (packages.length === 0) return {
@@ -3091,15 +3146,19 @@ function contextAsDockerReader(ctx) {
3091
3146
  }
3092
3147
  /** Log what was detected so the user understands generator decisions. */
3093
3148
  function logDetectionSummary(ctx) {
3094
- const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
3095
- if (dockerPackages.length > 0) p.log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
3096
- const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
3097
- if (publishable.length > 0) p.log.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
3149
+ if (ctx.config.publishDocker) {
3150
+ const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
3151
+ if (dockerPackages.length > 0) log$2.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
3152
+ }
3153
+ if (ctx.config.publishNpm) {
3154
+ const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
3155
+ if (publishable.length > 0) log$2.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
3156
+ }
3098
3157
  }
3099
3158
  async function runInit(config, options = {}) {
3100
3159
  const detected = detectProject(config.targetDir);
3101
3160
  const { ctx, archivedFiles } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
3102
- const result = await p.select({
3161
+ const result = await select({
3103
3162
  message: `${relativePath} already exists. What do you want to do?`,
3104
3163
  options: [{
3105
3164
  value: "overwrite",
@@ -3109,10 +3168,9 @@ async function runInit(config, options = {}) {
3109
3168
  label: "Skip"
3110
3169
  }]
3111
3170
  });
3112
- if (p.isCancel(result)) return "skip";
3171
+ if (isCancel(result)) return "skip";
3113
3172
  return result;
3114
3173
  }));
3115
- if (config.releaseStrategy !== "none" && !ctx.packageJson?.repository) p.log.warn(`package.json is missing a "repository" field — required for release strategy "${config.releaseStrategy}"`);
3116
3174
  logDetectionSummary(ctx);
3117
3175
  const results = await runGenerators(ctx);
3118
3176
  const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
@@ -3123,8 +3181,12 @@ async function runInit(config, options = {}) {
3123
3181
  });
3124
3182
  const created = results.filter((r) => r.action === "created");
3125
3183
  const updated = results.filter((r) => r.action === "updated");
3126
- if (!(created.length > 0 || updated.length > 0 || archivedFiles.length > 0) && options.noPrompt) {
3127
- p.log.success("Repository is up to date.");
3184
+ const hasChanges = created.length > 0 || updated.length > 0 || archivedFiles.length > 0;
3185
+ const prompt = generateMigratePrompt(results, config, detected);
3186
+ const promptPath = ".tooling-migrate.md";
3187
+ ctx.write(promptPath, prompt);
3188
+ if (!hasChanges && options.noPrompt) {
3189
+ log$2.success("Repository is up to date.");
3128
3190
  return results;
3129
3191
  }
3130
3192
  if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
@@ -3137,18 +3199,15 @@ async function runInit(config, options = {}) {
3137
3199
  const summaryLines = [];
3138
3200
  if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
3139
3201
  if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
3140
- p.note(summaryLines.join("\n"), "Summary");
3202
+ note(summaryLines.join("\n"), "Summary");
3141
3203
  if (!options.noPrompt) {
3142
- const prompt = generateMigratePrompt(results, config, detected);
3143
- const promptPath = ".tooling-migrate.md";
3144
- ctx.write(promptPath, prompt);
3145
- p.log.info(`Migration prompt written to ${promptPath}`);
3146
- p.log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
3204
+ log$2.info(`Migration prompt written to ${promptPath}`);
3205
+ log$2.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
3147
3206
  }
3148
3207
  const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
3149
3208
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
3150
3209
  if (bensandeeDeps.length > 0 && hasLockfile) {
3151
- p.log.info("Updating @bensandee/* packages...");
3210
+ log$2.info("Updating @bensandee/* packages...");
3152
3211
  try {
3153
3212
  execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
3154
3213
  cwd: config.targetDir,
@@ -3156,10 +3215,17 @@ async function runInit(config, options = {}) {
3156
3215
  timeout: 6e4
3157
3216
  });
3158
3217
  } catch (_error) {
3159
- p.log.warn("Could not update @bensandee/* packages — run pnpm install manually");
3218
+ log$2.warn("Could not update @bensandee/* packages — run pnpm install manually");
3160
3219
  }
3161
3220
  }
3162
- p.note([
3221
+ if (hasChanges && ctx.exists("package.json")) try {
3222
+ execSync("pnpm format", {
3223
+ cwd: config.targetDir,
3224
+ stdio: "ignore",
3225
+ timeout: 3e4
3226
+ });
3227
+ } catch (_error) {}
3228
+ note([
3163
3229
  "1. Run: pnpm install",
3164
3230
  "2. Run: pnpm check",
3165
3231
  ...options.noPrompt ? [] : ["3. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
@@ -3241,22 +3307,22 @@ async function runCheck(targetDir) {
3241
3307
  return true;
3242
3308
  });
3243
3309
  if (actionable.length === 0) {
3244
- p.log.success("Repository is up to date.");
3310
+ log$2.success("Repository is up to date.");
3245
3311
  return 0;
3246
3312
  }
3247
- p.log.warn(`${actionable.length} file(s) would be changed by repo:sync`);
3313
+ log$2.warn(`${actionable.length} file(s) would be changed by repo:sync`);
3248
3314
  for (const r of actionable) {
3249
- p.log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
3315
+ log$2.info(` ${r.action}: ${r.filePath} — ${r.description}`);
3250
3316
  const newContent = pendingWrites.get(r.filePath);
3251
3317
  if (!newContent) continue;
3252
3318
  const existingPath = path.join(targetDir, r.filePath);
3253
3319
  const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
3254
3320
  if (!existing) {
3255
3321
  const lineCount = newContent.split("\n").length - 1;
3256
- p.log.info(` + ${lineCount} new lines`);
3322
+ log$2.info(` + ${lineCount} new lines`);
3257
3323
  } else {
3258
3324
  const diff = lineDiff(existing, newContent);
3259
- for (const line of diff) p.log.info(` ${line}`);
3325
+ for (const line of diff) log$2.info(` ${line}`);
3260
3326
  }
3261
3327
  }
3262
3328
  return 1;
@@ -3311,6 +3377,16 @@ function createRealExecutor() {
3311
3377
  };
3312
3378
  }
3313
3379
  },
3380
+ execInherit(command, options) {
3381
+ execSync(command, {
3382
+ cwd: options?.cwd,
3383
+ env: options?.env ? {
3384
+ ...process.env,
3385
+ ...options.env
3386
+ } : void 0,
3387
+ stdio: "inherit"
3388
+ });
3389
+ },
3314
3390
  fetch: globalThis.fetch,
3315
3391
  listChangesetFiles(cwd) {
3316
3392
  const dir = path.join(cwd, ".changeset");
@@ -3493,7 +3569,7 @@ async function createRelease(executor, conn, tag) {
3493
3569
  //#region src/release/log.ts
3494
3570
  /** Log a debug message when verbose mode is enabled. */
3495
3571
  function debug(config, message) {
3496
- if (config.verbose) p.log.info(`[debug] ${message}`);
3572
+ if (config.verbose) log$2.info(`[debug] ${message}`);
3497
3573
  }
3498
3574
  /** Log the result of an exec call when verbose mode is enabled. */
3499
3575
  function debugExec(config, label, result) {
@@ -3501,7 +3577,7 @@ function debugExec(config, label, result) {
3501
3577
  const lines = [`[debug] ${label} (exit code ${String(result.exitCode)})`];
3502
3578
  if (result.stdout.trim()) lines.push(` stdout: ${result.stdout.trim()}`);
3503
3579
  if (result.stderr.trim()) lines.push(` stderr: ${result.stderr.trim()}`);
3504
- p.log.info(lines.join("\n"));
3580
+ log$2.info(lines.join("\n"));
3505
3581
  }
3506
3582
  //#endregion
3507
3583
  //#region src/release/version.ts
@@ -3573,7 +3649,7 @@ function buildPrContent(executor, cwd, packagesBefore) {
3573
3649
  }
3574
3650
  /** Mode 1: version packages and create/update a PR. */
3575
3651
  async function runVersionMode(executor, config) {
3576
- p.log.info("Changesets detected — versioning packages");
3652
+ log$2.info("Changesets detected — versioning packages");
3577
3653
  const packagesBefore = executor.listWorkspacePackages(config.cwd);
3578
3654
  debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
3579
3655
  const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
@@ -3599,19 +3675,19 @@ async function runVersionMode(executor, config) {
3599
3675
  const addResult = executor.exec("git add -A", { cwd: config.cwd });
3600
3676
  if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
3601
3677
  const remainingChangesets = executor.listChangesetFiles(config.cwd);
3602
- if (remainingChangesets.length > 0) p.log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
3678
+ if (remainingChangesets.length > 0) log$2.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
3603
3679
  debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
3604
3680
  const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
3605
3681
  debugExec(config, "git commit", commitResult);
3606
3682
  if (commitResult.exitCode !== 0) {
3607
- p.log.info("Nothing to commit after versioning");
3683
+ log$2.info("Nothing to commit after versioning");
3608
3684
  return {
3609
3685
  mode: "version",
3610
3686
  pr: "none"
3611
3687
  };
3612
3688
  }
3613
3689
  if (config.dryRun) {
3614
- p.log.info("[dry-run] Would push and create/update PR");
3690
+ log$2.info("[dry-run] Would push and create/update PR");
3615
3691
  return {
3616
3692
  mode: "version",
3617
3693
  pr: "none"
@@ -3634,7 +3710,7 @@ async function runVersionMode(executor, config) {
3634
3710
  base: "main",
3635
3711
  body
3636
3712
  });
3637
- p.log.info("Created version PR");
3713
+ log$2.info("Created version PR");
3638
3714
  return {
3639
3715
  mode: "version",
3640
3716
  pr: "created"
@@ -3644,7 +3720,7 @@ async function runVersionMode(executor, config) {
3644
3720
  title,
3645
3721
  body
3646
3722
  });
3647
- p.log.info(`Updated version PR #${String(existingPr)}`);
3723
+ log$2.info(`Updated version PR #${String(existingPr)}`);
3648
3724
  return {
3649
3725
  mode: "version",
3650
3726
  pr: "updated"
@@ -3669,7 +3745,7 @@ async function retryAsync(fn) {
3669
3745
  }
3670
3746
  /** Mode 2: publish to npm, push tags, and create Forgejo releases. */
3671
3747
  async function runPublishMode(executor, config) {
3672
- p.log.info("No changesets — publishing packages");
3748
+ log$2.info("No changesets — publishing packages");
3673
3749
  const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
3674
3750
  debugExec(config, "pnpm changeset publish", publishResult);
3675
3751
  if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
@@ -3684,11 +3760,11 @@ async function runPublishMode(executor, config) {
3684
3760
  debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
3685
3761
  if (config.dryRun) {
3686
3762
  if (tagsToPush.length === 0) {
3687
- p.log.info("No packages were published");
3763
+ log$2.info("No packages were published");
3688
3764
  return { mode: "none" };
3689
3765
  }
3690
- p.log.info(`Tags to process: ${tagsToPush.join(", ")}`);
3691
- p.log.info("[dry-run] Would push tags and create releases");
3766
+ log$2.info(`Tags to process: ${tagsToPush.join(", ")}`);
3767
+ log$2.info("[dry-run] Would push tags and create releases");
3692
3768
  return {
3693
3769
  mode: "publish",
3694
3770
  tags: tagsToPush
@@ -3704,10 +3780,10 @@ async function runPublishMode(executor, config) {
3704
3780
  for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
3705
3781
  const allTags = [...tagsToPush, ...tagsWithMissingReleases];
3706
3782
  if (allTags.length === 0) {
3707
- p.log.info("No packages were published");
3783
+ log$2.info("No packages were published");
3708
3784
  return { mode: "none" };
3709
3785
  }
3710
- p.log.info(`Tags to process: ${allTags.join(", ")}`);
3786
+ log$2.info(`Tags to process: ${allTags.join(", ")}`);
3711
3787
  const errors = [];
3712
3788
  for (const tag of allTags) try {
3713
3789
  if (!remoteSet.has(tag)) {
@@ -3718,7 +3794,7 @@ async function runPublishMode(executor, config) {
3718
3794
  const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
3719
3795
  if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
3720
3796
  }
3721
- if (await findRelease(executor, conn, tag)) p.log.warn(`Release for ${tag} already exists — skipping`);
3797
+ if (await findRelease(executor, conn, tag)) log$2.warn(`Release for ${tag} already exists — skipping`);
3722
3798
  else {
3723
3799
  await retryAsync(async () => {
3724
3800
  try {
@@ -3728,14 +3804,14 @@ async function runPublishMode(executor, config) {
3728
3804
  throw error;
3729
3805
  }
3730
3806
  });
3731
- p.log.info(`Created release for ${tag}`);
3807
+ log$2.info(`Created release for ${tag}`);
3732
3808
  }
3733
3809
  } catch (error) {
3734
3810
  errors.push({
3735
3811
  tag,
3736
3812
  error
3737
3813
  });
3738
- p.log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
3814
+ log$2.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
3739
3815
  }
3740
3816
  if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
3741
3817
  return {
@@ -3905,12 +3981,12 @@ async function triggerForgejo(conn, ref) {
3905
3981
  body: JSON.stringify({ ref })
3906
3982
  });
3907
3983
  if (!res.ok) throw new FatalError(`Failed to trigger Forgejo workflow: ${res.status} ${res.statusText}`);
3908
- p.log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
3984
+ log$2.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
3909
3985
  }
3910
3986
  function triggerGitHub(ref) {
3911
3987
  const result = createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
3912
3988
  if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
3913
- p.log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
3989
+ log$2.info(`Triggered release workflow on GitHub (ref: ${ref})`);
3914
3990
  }
3915
3991
  //#endregion
3916
3992
  //#region src/commands/forgejo-create-release.ts
@@ -3930,11 +4006,11 @@ const createForgejoReleaseCommand = defineCommand({
3930
4006
  const executor = createRealExecutor();
3931
4007
  const conn = resolved.conn;
3932
4008
  if (await findRelease(executor, conn, args.tag)) {
3933
- p.log.info(`Release for ${args.tag} already exists — skipping`);
4009
+ log$2.info(`Release for ${args.tag} already exists — skipping`);
3934
4010
  return;
3935
4011
  }
3936
4012
  await createRelease(executor, conn, args.tag);
3937
- p.log.info(`Created Forgejo release for ${args.tag}`);
4013
+ log$2.info(`Created Forgejo release for ${args.tag}`);
3938
4014
  }
3939
4015
  });
3940
4016
  //#endregion
@@ -3961,26 +4037,26 @@ async function mergeForgejo(conn, dryRun) {
3961
4037
  const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
3962
4038
  if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
3963
4039
  if (dryRun) {
3964
- p.log.info(`[dry-run] Would merge PR #${String(prNumber)} and delete branch ${HEAD_BRANCH}`);
4040
+ log$2.info(`[dry-run] Would merge PR #${String(prNumber)} and delete branch ${HEAD_BRANCH}`);
3965
4041
  return;
3966
4042
  }
3967
4043
  await mergePr(executor, conn, prNumber, {
3968
4044
  method: "merge",
3969
4045
  deleteBranch: true
3970
4046
  });
3971
- p.log.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
4047
+ log$2.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
3972
4048
  }
3973
4049
  function mergeGitHub(dryRun) {
3974
4050
  const executor = createRealExecutor();
3975
4051
  if (dryRun) {
3976
4052
  const prNum = executor.exec(`gh pr view ${HEAD_BRANCH} --json number --jq .number`, { cwd: process.cwd() }).stdout.trim();
3977
4053
  if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
3978
- p.log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
4054
+ log$2.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
3979
4055
  return;
3980
4056
  }
3981
4057
  const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
3982
4058
  if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
3983
- p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
4059
+ log$2.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
3984
4060
  }
3985
4061
  //#endregion
3986
4062
  //#region src/release/simple.ts
@@ -4013,7 +4089,7 @@ function readVersion(executor, cwd) {
4013
4089
  /** Run the full commit-and-tag-version release flow. */
4014
4090
  async function runSimpleRelease(executor, config) {
4015
4091
  const command = buildCommand(config);
4016
- p.log.info(`Running: ${command}`);
4092
+ log$2.info(`Running: ${command}`);
4017
4093
  const versionResult = executor.exec(command, { cwd: config.cwd });
4018
4094
  debugExec(config, "commit-and-tag-version", versionResult);
4019
4095
  if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
@@ -4023,12 +4099,12 @@ async function runSimpleRelease(executor, config) {
4023
4099
  debugExec(config, "git describe", tagResult);
4024
4100
  const tag = tagResult.stdout.trim();
4025
4101
  if (!tag) throw new FatalError("Could not determine the new tag from git describe");
4026
- p.log.info(`Version ${version} tagged as ${tag}`);
4102
+ log$2.info(`Version ${version} tagged as ${tag}`);
4027
4103
  if (config.dryRun) {
4028
4104
  const slidingTags = config.noSlidingTags ? [] : computeSlidingTags(version);
4029
- p.log.info(`[dry-run] Would push to origin with --follow-tags`);
4030
- if (slidingTags.length > 0) p.log.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
4031
- if (!config.noRelease && config.platform) p.log.info(`[dry-run] Would create ${config.platform.type} release for ${tag}`);
4105
+ log$2.info(`[dry-run] Would push to origin with --follow-tags`);
4106
+ if (slidingTags.length > 0) log$2.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
4107
+ if (!config.noRelease && config.platform) log$2.info(`[dry-run] Would create ${config.platform.type} release for ${tag}`);
4032
4108
  return {
4033
4109
  version,
4034
4110
  tag,
@@ -4045,7 +4121,7 @@ async function runSimpleRelease(executor, config) {
4045
4121
  debugExec(config, "git push", pushResult);
4046
4122
  if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
4047
4123
  pushed = true;
4048
- p.log.info("Pushed to origin");
4124
+ log$2.info("Pushed to origin");
4049
4125
  }
4050
4126
  let slidingTags = [];
4051
4127
  if (!config.noSlidingTags && pushed) {
@@ -4056,8 +4132,8 @@ async function runSimpleRelease(executor, config) {
4056
4132
  }
4057
4133
  const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
4058
4134
  debugExec(config, "force-push sliding tags", forcePushResult);
4059
- if (forcePushResult.exitCode !== 0) p.log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4060
- else p.log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
4135
+ if (forcePushResult.exitCode !== 0) log$2.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4136
+ else log$2.info(`Created sliding tags: ${slidingTags.join(", ")}`);
4061
4137
  }
4062
4138
  let releaseCreated = false;
4063
4139
  if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
@@ -4077,16 +4153,16 @@ async function createPlatformRelease(executor, config, tag) {
4077
4153
  return false;
4078
4154
  }
4079
4155
  await createRelease(executor, config.platform.conn, tag);
4080
- p.log.info(`Created Forgejo release for ${tag}`);
4156
+ log$2.info(`Created Forgejo release for ${tag}`);
4081
4157
  return true;
4082
4158
  }
4083
4159
  const ghResult = executor.exec(`gh release create ${tag} --generate-notes`, { cwd: config.cwd });
4084
4160
  debugExec(config, "gh release create", ghResult);
4085
4161
  if (ghResult.exitCode !== 0) {
4086
- p.log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
4162
+ log$2.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
4087
4163
  return false;
4088
4164
  }
4089
- p.log.info(`Created GitHub release for ${tag}`);
4165
+ log$2.info(`Created GitHub release for ${tag}`);
4090
4166
  return true;
4091
4167
  }
4092
4168
  //#endregion
@@ -4165,14 +4241,20 @@ const CHECKS = [
4165
4241
  { name: "typecheck" },
4166
4242
  { name: "lint" },
4167
4243
  { name: "test" },
4244
+ { name: "docker:build" },
4245
+ { name: "tooling:check" },
4246
+ { name: "docker:check" },
4168
4247
  {
4169
4248
  name: "format",
4170
4249
  args: "--check"
4171
4250
  },
4172
- { name: "knip" },
4173
- { name: "tooling:check" },
4174
- { name: "docker:check" }
4251
+ { name: "knip" }
4175
4252
  ];
4253
+ /** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
4254
+ function shouldSkip(name, patterns) {
4255
+ if (patterns.size === 0) return false;
4256
+ return picomatch.isMatch(name, [...patterns]);
4257
+ }
4176
4258
  function defaultGetScripts(targetDir) {
4177
4259
  try {
4178
4260
  const pkg = parsePackageJson(readFileSync(path.join(targetDir, "package.json"), "utf-8"));
@@ -4193,7 +4275,30 @@ function defaultExecCommand(cmd, cwd) {
4193
4275
  return 1;
4194
4276
  }
4195
4277
  }
4196
- const ciLog = (msg) => console.log(msg);
4278
+ const rawLog = (msg) => console.log(msg);
4279
+ const ciReporter = {
4280
+ groupStart: (name) => rawLog(`::group::${name}`),
4281
+ groupEnd: () => rawLog("::endgroup::"),
4282
+ passed: (name, elapsedS) => rawLog(`✓ ${name} (${elapsedS}s)`),
4283
+ failed: (name, elapsedS) => {
4284
+ rawLog(`✗ ${name} (${elapsedS}s)`);
4285
+ rawLog(`::error::${name} failed`);
4286
+ },
4287
+ undefinedCheck: (name) => rawLog(`::error::${name} not defined in package.json`),
4288
+ skippedNotDefined: (names) => rawLog(`Skipped (not defined): ${names.join(", ")}`),
4289
+ allPassed: () => rawLog("✓ All checks passed"),
4290
+ anyFailed: (names) => rawLog(`::error::Failed checks: ${names.join(", ")}`)
4291
+ };
4292
+ const localReporter = {
4293
+ groupStart: (_name) => {},
4294
+ groupEnd: () => {},
4295
+ passed: (name) => log$2.success(name),
4296
+ failed: (name) => log$2.error(`${name} failed`),
4297
+ undefinedCheck: (name) => log$2.error(`${name} not defined in package.json`),
4298
+ skippedNotDefined: (names) => log$2.info(`Skipped (not defined): ${names.join(", ")}`),
4299
+ allPassed: () => log$2.success("All checks passed"),
4300
+ anyFailed: (names) => log$2.error(`Failed checks: ${names.join(", ")}`)
4301
+ };
4197
4302
  function runRunChecks(targetDir, options = {}) {
4198
4303
  const exec = options.execCommand ?? defaultExecCommand;
4199
4304
  const getScripts = options.getScripts ?? defaultGetScripts;
@@ -4201,38 +4306,40 @@ function runRunChecks(targetDir, options = {}) {
4201
4306
  const add = options.add ?? [];
4202
4307
  const isCI = Boolean(process.env["CI"]);
4203
4308
  const failFast = options.failFast ?? !isCI;
4309
+ const reporter = isCI ? ciReporter : localReporter;
4204
4310
  const definedScripts = getScripts(targetDir);
4205
4311
  const addedNames = new Set(add);
4206
4312
  const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
4207
4313
  const failures = [];
4208
4314
  const notDefined = [];
4209
4315
  for (const check of allChecks) {
4210
- if (skip.has(check.name)) continue;
4316
+ if (shouldSkip(check.name, skip)) continue;
4211
4317
  if (!definedScripts.has(check.name)) {
4212
4318
  if (addedNames.has(check.name)) {
4213
- p.log.error(`${check.name} not defined in package.json`);
4319
+ reporter.undefinedCheck(check.name);
4214
4320
  failures.push(check.name);
4215
4321
  } else notDefined.push(check.name);
4216
4322
  continue;
4217
4323
  }
4218
4324
  const cmd = check.args ? `pnpm run ${check.name} ${check.args}` : `pnpm run ${check.name}`;
4219
- if (isCI) ciLog(`::group::${check.name}`);
4325
+ reporter.groupStart(check.name);
4326
+ const start = Date.now();
4220
4327
  const exitCode = exec(cmd, targetDir);
4221
- if (isCI) ciLog("::endgroup::");
4222
- if (exitCode === 0) p.log.success(check.name);
4328
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
4329
+ reporter.groupEnd();
4330
+ if (exitCode === 0) reporter.passed(check.name, elapsed);
4223
4331
  else {
4224
- if (isCI) ciLog(`::error::${check.name} failed`);
4225
- p.log.error(`${check.name} failed`);
4332
+ reporter.failed(check.name, elapsed);
4226
4333
  failures.push(check.name);
4227
4334
  if (failFast) return 1;
4228
4335
  }
4229
4336
  }
4230
- if (notDefined.length > 0) p.log.info(`Skipped (not defined): ${notDefined.join(", ")}`);
4337
+ if (notDefined.length > 0) reporter.skippedNotDefined(notDefined);
4231
4338
  if (failures.length > 0) {
4232
- p.log.error(`Failed checks: ${failures.join(", ")}`);
4339
+ reporter.anyFailed(failures);
4233
4340
  return 1;
4234
4341
  }
4235
- p.log.success("All checks passed");
4342
+ reporter.allPassed();
4236
4343
  return 0;
4237
4344
  }
4238
4345
  const runChecksCommand = defineCommand({
@@ -4266,7 +4373,7 @@ const runChecksCommand = defineCommand({
4266
4373
  const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
4267
4374
  skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
4268
4375
  add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
4269
- failFast: args["fail-fast"] === true ? true : args["fail-fast"] === false ? false : void 0
4376
+ failFast: args["fail-fast"] ? true : void 0
4270
4377
  });
4271
4378
  process.exitCode = exitCode;
4272
4379
  }
@@ -4283,16 +4390,10 @@ const publishDockerCommand = defineCommand({
4283
4390
  name: "docker:publish",
4284
4391
  description: "Build, tag, and push Docker images for packages with an image:build script"
4285
4392
  },
4286
- args: {
4287
- "dry-run": {
4288
- type: "boolean",
4289
- description: "Build and tag images but skip login, push, and logout"
4290
- },
4291
- verbose: {
4292
- type: "boolean",
4293
- description: "Enable detailed debug logging"
4294
- }
4295
- },
4393
+ args: { "dry-run": {
4394
+ type: "boolean",
4395
+ description: "Build and tag images but skip login, push, and logout"
4396
+ } },
4296
4397
  async run({ args }) {
4297
4398
  const config = {
4298
4399
  cwd: process.cwd(),
@@ -4300,8 +4401,7 @@ const publishDockerCommand = defineCommand({
4300
4401
  registryNamespace: requireEnv("DOCKER_REGISTRY_NAMESPACE"),
4301
4402
  username: requireEnv("DOCKER_USERNAME"),
4302
4403
  password: requireEnv("DOCKER_PASSWORD"),
4303
- dryRun: args["dry-run"] === true,
4304
- verbose: args.verbose === true
4404
+ dryRun: args["dry-run"] === true
4305
4405
  };
4306
4406
  runDockerPublish(createRealExecutor(), config);
4307
4407
  }
@@ -4334,10 +4434,6 @@ const dockerBuildCommand = defineCommand({
4334
4434
  type: "string",
4335
4435
  description: "Build a single package by directory path (e.g. packages/server). Useful as an image:build script."
4336
4436
  },
4337
- verbose: {
4338
- type: "boolean",
4339
- description: "Enable detailed debug logging"
4340
- },
4341
4437
  _: {
4342
4438
  type: "positional",
4343
4439
  required: false,
@@ -4358,7 +4454,6 @@ const dockerBuildCommand = defineCommand({
4358
4454
  runDockerBuild(executor, {
4359
4455
  cwd,
4360
4456
  packageDir,
4361
- verbose: args.verbose === true,
4362
4457
  extraArgs: extraArgs.filter((a) => a.length > 0)
4363
4458
  });
4364
4459
  }
@@ -4658,8 +4753,8 @@ const dockerCheckCommand = defineCommand({
4658
4753
  //#region src/bin.ts
4659
4754
  const main = defineCommand({
4660
4755
  meta: {
4661
- name: "tooling",
4662
- version: "0.25.3",
4756
+ name: "bst",
4757
+ version: "0.27.0",
4663
4758
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4664
4759
  },
4665
4760
  subCommands: {
@@ -4675,7 +4770,7 @@ const main = defineCommand({
4675
4770
  "docker:check": dockerCheckCommand
4676
4771
  }
4677
4772
  });
4678
- console.log(`@bensandee/tooling v0.25.3`);
4773
+ console.log(`@bensandee/tooling v0.27.0`);
4679
4774
  async function run() {
4680
4775
  await runMain(main);
4681
4776
  process.exit(process.exitCode ?? 0);