@bensandee/tooling 0.26.0 → 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";
@@ -12,6 +13,22 @@ import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } fr
12
13
  import { execSync } from "node:child_process";
13
14
  import picomatch from "picomatch";
14
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
15
32
  //#region src/types.ts
16
33
  const LEGACY_TOOLS = [
17
34
  "eslint",
@@ -205,6 +222,8 @@ function computeDefaults(targetDir) {
205
222
  ci: detectCiPlatform(targetDir),
206
223
  setupRenovate: true,
207
224
  releaseStrategy: "none",
225
+ publishNpm: false,
226
+ publishDocker: false,
208
227
  projectType: isMonorepo ? "default" : detectProjectType(targetDir),
209
228
  detectPackageTypes: true
210
229
  };
@@ -257,10 +276,10 @@ function getMonorepoPackages(targetDir) {
257
276
  //#endregion
258
277
  //#region src/prompts/init-prompts.ts
259
278
  function isCancelled(value) {
260
- return p.isCancel(value);
279
+ return clack.isCancel(value);
261
280
  }
262
281
  async function runInitPrompts(targetDir, saved) {
263
- p.intro("@bensandee/tooling repo:sync");
282
+ clack.intro("@bensandee/tooling repo:sync");
264
283
  const existingPkg = readPackageJson(targetDir);
265
284
  const detected = detectProject(targetDir);
266
285
  const defaults = computeDefaults(targetDir);
@@ -277,7 +296,7 @@ async function runInitPrompts(targetDir, saved) {
277
296
  const projectType = saved?.projectType ?? defaults.projectType;
278
297
  const detectPackageTypes = saved?.detectPackageTypes ?? defaults.detectPackageTypes;
279
298
  if (detected.legacyConfigs.some((l) => l.tool === "prettier") && isFirstInit) {
280
- const formatterAnswer = await p.select({
299
+ const formatterAnswer = await clack.select({
281
300
  message: "Existing Prettier config found. Keep Prettier or migrate to oxfmt?",
282
301
  initialValue: "prettier",
283
302
  options: [{
@@ -290,14 +309,14 @@ async function runInitPrompts(targetDir, saved) {
290
309
  }]
291
310
  });
292
311
  if (isCancelled(formatterAnswer)) {
293
- p.cancel("Cancelled.");
312
+ clack.cancel("Cancelled.");
294
313
  process.exit(0);
295
314
  }
296
315
  formatter = formatterAnswer;
297
316
  }
298
317
  const detectedCi = detectCiPlatform(targetDir);
299
318
  if (isFirstInit && detectedCi === "none") {
300
- const ciAnswer = await p.select({
319
+ const ciAnswer = await clack.select({
301
320
  message: "CI workflow",
302
321
  initialValue: "forgejo",
303
322
  options: [
@@ -316,14 +335,14 @@ async function runInitPrompts(targetDir, saved) {
316
335
  ]
317
336
  });
318
337
  if (isCancelled(ciAnswer)) {
319
- p.cancel("Cancelled.");
338
+ clack.cancel("Cancelled.");
320
339
  process.exit(0);
321
340
  }
322
341
  ci = ciAnswer;
323
342
  }
324
343
  const hasExistingRelease = detected.hasReleaseItConfig || detected.hasSimpleReleaseConfig || detected.hasChangesetsConfig;
325
344
  if (isFirstInit && !hasExistingRelease) {
326
- const releaseAnswer = await p.select({
345
+ const releaseAnswer = await clack.select({
327
346
  message: "Release management",
328
347
  initialValue: defaults.releaseStrategy,
329
348
  options: [
@@ -349,12 +368,40 @@ async function runInitPrompts(targetDir, saved) {
349
368
  ]
350
369
  });
351
370
  if (isCancelled(releaseAnswer)) {
352
- p.cancel("Cancelled.");
371
+ clack.cancel("Cancelled.");
353
372
  process.exit(0);
354
373
  }
355
374
  releaseStrategy = releaseAnswer;
356
375
  }
357
- 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!");
358
405
  return {
359
406
  name,
360
407
  isNew: !isExisting,
@@ -365,6 +412,8 @@ async function runInitPrompts(targetDir, saved) {
365
412
  ci,
366
413
  setupRenovate,
367
414
  releaseStrategy,
415
+ publishNpm,
416
+ publishDocker,
368
417
  projectType,
369
418
  detectPackageTypes,
370
419
  targetDir
@@ -482,51 +531,53 @@ function createDryRunContext(config) {
482
531
  //#region src/utils/tooling-config.ts
483
532
  const CONFIG_FILE = ".tooling.json";
484
533
  const DeclarativeHealthCheckSchema = z.object({
485
- name: z.string(),
486
- url: z.string(),
487
- 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" })
488
537
  });
489
538
  const DockerCheckConfigSchema = z.object({
490
- composeFiles: z.array(z.string()).optional(),
491
- envFile: z.string().optional(),
492
- services: z.array(z.string()).optional(),
493
- healthChecks: z.array(DeclarativeHealthCheckSchema).optional(),
494
- buildCommand: z.string().optional(),
495
- buildCwd: z.string().optional(),
496
- timeoutMs: z.number().int().positive().optional(),
497
- 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" })
498
547
  });
499
548
  const ToolingConfigSchema = z.strictObject({
500
- $schema: z.string().optional(),
501
- structure: z.enum(["single", "monorepo"]).optional(),
502
- useEslintPlugin: z.boolean().optional(),
503
- formatter: z.enum(["oxfmt", "prettier"]).optional(),
504
- 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" }),
505
554
  ci: z.enum([
506
555
  "github",
507
556
  "forgejo",
508
557
  "none"
509
- ]).optional(),
510
- setupRenovate: z.boolean().optional(),
558
+ ]).optional().meta({ description: "CI platform" }),
559
+ setupRenovate: z.boolean().optional().meta({ description: "Generate Renovate config" }),
511
560
  releaseStrategy: z.enum([
512
561
  "release-it",
513
562
  "simple",
514
563
  "changesets",
515
564
  "none"
516
- ]).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)" }),
517
568
  projectType: z.enum([
518
569
  "default",
519
570
  "node",
520
571
  "react",
521
572
  "library"
522
- ]).optional(),
523
- detectPackageTypes: z.boolean().optional(),
524
- 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" }),
525
576
  docker: z.record(z.string(), z.object({
526
- dockerfile: z.string(),
527
- context: z.string().default(".")
528
- })).optional(),
529
- 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" })
530
581
  });
531
582
  /** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
532
583
  function loadToolingConfig(targetDir) {
@@ -546,6 +597,8 @@ const OVERRIDE_KEYS = [
546
597
  "ci",
547
598
  "setupRenovate",
548
599
  "releaseStrategy",
600
+ "publishNpm",
601
+ "publishDocker",
549
602
  "projectType",
550
603
  "detectPackageTypes"
551
604
  ];
@@ -591,6 +644,8 @@ function mergeWithSavedConfig(detected, saved) {
591
644
  ci: saved.ci ?? detected.ci,
592
645
  setupRenovate: saved.setupRenovate ?? detected.setupRenovate,
593
646
  releaseStrategy: saved.releaseStrategy ?? detected.releaseStrategy,
647
+ publishNpm: saved.publishNpm ?? detected.publishNpm,
648
+ publishDocker: saved.publishDocker ?? detected.publishDocker,
594
649
  projectType: saved.projectType ?? detected.projectType,
595
650
  detectPackageTypes: saved.detectPackageTypes ?? detected.detectPackageTypes
596
651
  };
@@ -773,7 +828,7 @@ jobs:
773
828
  DOCKER_REGISTRY_NAMESPACE: ${actionsExpr$2("vars.DOCKER_REGISTRY_NAMESPACE")}
774
829
  DOCKER_USERNAME: ${actionsExpr$2("secrets.DOCKER_USERNAME")}
775
830
  DOCKER_PASSWORD: ${actionsExpr$2("secrets.DOCKER_PASSWORD")}
776
- run: pnpm exec tooling docker:publish
831
+ run: pnpm exec bst docker:publish
777
832
  `;
778
833
  }
779
834
  function requiredDeploySteps() {
@@ -796,7 +851,7 @@ function requiredDeploySteps() {
796
851
  },
797
852
  {
798
853
  match: { run: "docker:publish" },
799
- step: { run: "pnpm exec tooling docker:publish" }
854
+ step: { run: "pnpm exec bst docker:publish" }
800
855
  }
801
856
  ];
802
857
  }
@@ -833,7 +888,7 @@ function hasDockerPackages(ctx) {
833
888
  }
834
889
  async function generateDeployCi(ctx) {
835
890
  const filePath = "deploy-ci";
836
- if (!hasDockerPackages(ctx) || ctx.config.ci === "none") return {
891
+ if (!ctx.config.publishDocker || ctx.config.ci === "none") return {
837
892
  filePath,
838
893
  action: "skipped",
839
894
  description: "Deploy CI workflow not applicable"
@@ -852,21 +907,13 @@ async function generateDeployCi(ctx) {
852
907
  };
853
908
  const merged = mergeWorkflowSteps(existing, "deploy", requiredDeploySteps());
854
909
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
855
- if (withComment === content) {
856
- ctx.write(workflowPath, content);
857
- return {
858
- filePath: workflowPath,
859
- action: "updated",
860
- description: "Added missing steps to deploy workflow"
861
- };
862
- }
863
- if (await ctx.confirmOverwrite(workflowPath) === "skip") {
864
- if (merged.changed || withComment !== merged.content) {
910
+ if (!merged.changed) {
911
+ if (withComment !== existing) {
865
912
  ctx.write(workflowPath, withComment);
866
913
  return {
867
914
  filePath: workflowPath,
868
915
  action: "updated",
869
- description: "Added missing steps to deploy workflow"
916
+ description: "Added schema comment to deploy workflow"
870
917
  };
871
918
  }
872
919
  return {
@@ -875,11 +922,11 @@ async function generateDeployCi(ctx) {
875
922
  description: "Existing deploy workflow preserved"
876
923
  };
877
924
  }
878
- ctx.write(workflowPath, content);
925
+ ctx.write(workflowPath, withComment);
879
926
  return {
880
927
  filePath: workflowPath,
881
928
  action: "updated",
882
- description: "Replaced deploy workflow with updated template"
929
+ description: "Added missing steps to deploy workflow"
883
930
  };
884
931
  }
885
932
  return {
@@ -904,10 +951,10 @@ const STANDARD_SCRIPTS_SINGLE = {
904
951
  test: "vitest run",
905
952
  lint: "oxlint",
906
953
  knip: "knip",
907
- check: "pnpm exec tooling checks:run",
954
+ check: "bst checks:run",
908
955
  "ci:check": "pnpm check --skip 'docker:*'",
909
- "tooling:check": "pnpm exec tooling repo:sync --check",
910
- "tooling:sync": "pnpm exec tooling repo:sync"
956
+ "tooling:check": "bst repo:sync --check",
957
+ "tooling:sync": "bst repo:sync"
911
958
  };
912
959
  const STANDARD_SCRIPTS_MONOREPO = {
913
960
  build: "pnpm -r build",
@@ -915,10 +962,10 @@ const STANDARD_SCRIPTS_MONOREPO = {
915
962
  typecheck: "pnpm -r --parallel run typecheck",
916
963
  lint: "oxlint",
917
964
  knip: "knip",
918
- check: "pnpm exec tooling checks:run",
965
+ check: "bst checks:run",
919
966
  "ci:check": "pnpm check --skip 'docker:*'",
920
- "tooling:check": "pnpm exec tooling repo:sync --check",
921
- "tooling:sync": "pnpm exec tooling repo:sync"
967
+ "tooling:check": "bst repo:sync --check",
968
+ "tooling:sync": "bst repo:sync"
922
969
  };
923
970
  /** Scripts that tooling owns — map from script name to keyword that must appear in the value. */
924
971
  const MANAGED_SCRIPTS = {
@@ -983,8 +1030,8 @@ function addReleaseDeps(deps, config) {
983
1030
  function getAddedDevDepNames(config) {
984
1031
  const deps = { ...ROOT_DEV_DEPS };
985
1032
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
986
- deps["@bensandee/config"] = "0.9.0";
987
- deps["@bensandee/tooling"] = "0.26.0";
1033
+ deps["@bensandee/config"] = "0.9.1";
1034
+ deps["@bensandee/tooling"] = "0.27.0";
988
1035
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
989
1036
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
990
1037
  addReleaseDeps(deps, config);
@@ -1001,15 +1048,15 @@ async function generatePackageJson(ctx) {
1001
1048
  format: formatScript
1002
1049
  };
1003
1050
  if (ctx.config.releaseStrategy === "changesets") allScripts["changeset"] = "changeset";
1004
- 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";
1005
1052
  if (hasDockerPackages(ctx)) {
1006
- allScripts["docker:build"] = "pnpm exec tooling docker:build";
1007
- allScripts["docker:check"] = "pnpm exec tooling docker:check";
1053
+ allScripts["docker:build"] = "bst docker:build";
1054
+ allScripts["docker:check"] = "bst docker:check";
1008
1055
  }
1009
1056
  const devDeps = { ...ROOT_DEV_DEPS };
1010
1057
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1011
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.0";
1012
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.26.0";
1058
+ devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1059
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.27.0";
1013
1060
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1014
1061
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
1015
1062
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -1770,8 +1817,14 @@ const ClaudeSettingsSchema = z.object({
1770
1817
  });
1771
1818
  function parseClaudeSettings(raw) {
1772
1819
  try {
1773
- const result = ClaudeSettingsSchema.safeParse(JSON.parse(raw));
1774
- 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
+ };
1775
1828
  } catch {
1776
1829
  return;
1777
1830
  }
@@ -1830,7 +1883,6 @@ function buildSettings(ctx) {
1830
1883
  "Bash(wc *)",
1831
1884
  "Bash(test *)",
1832
1885
  "Bash([ *)",
1833
- "Bash(find *)",
1834
1886
  "Bash(grep *)",
1835
1887
  "Bash(which *)",
1836
1888
  "Bash(node -e *)",
@@ -1897,7 +1949,7 @@ function buildSettings(ctx) {
1897
1949
  instructions: [
1898
1950
  "Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
1899
1951
  "No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
1900
- "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.",
1901
1953
  "Prefer extensionless imports; if an extension is required, use .ts over .js."
1902
1954
  ],
1903
1955
  enabledPlugins,
@@ -1921,27 +1973,44 @@ function writeOrMergeSettings(ctx, filePath, generated) {
1921
1973
  action: "skipped",
1922
1974
  description: "Could not parse existing settings"
1923
1975
  };
1924
- const missingAllow = generated.permissions.allow.filter((rule) => !parsed.permissions.allow.includes(rule));
1925
- const missingDeny = generated.permissions.deny.filter((rule) => !parsed.permissions.deny.includes(rule));
1926
- const missingInstructions = generated.instructions.filter((inst) => !parsed.instructions.includes(inst));
1927
- const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in parsed.enabledPlugins));
1928
- const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in parsed.extraKnownMarketplaces));
1929
- const added = missingAllow.length + missingDeny.length + missingInstructions.length + missingPlugins.length + missingMarketplaces.length;
1930
- 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 {
1931
1992
  filePath,
1932
1993
  action: "skipped",
1933
1994
  description: "Already has all rules and instructions"
1934
1995
  };
1935
- parsed.permissions.allow = [...parsed.permissions.allow, ...missingAllow];
1936
- parsed.permissions.deny = [...parsed.permissions.deny, ...missingDeny];
1937
- parsed.instructions = [...parsed.instructions, ...missingInstructions];
1938
- for (const [key, value] of missingPlugins) parsed.enabledPlugins[key] = value;
1939
- for (const [key, value] of missingMarketplaces) parsed.extraKnownMarketplaces[key] = value;
1940
- 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");
1941
2010
  return {
1942
2011
  filePath,
1943
2012
  action: "updated",
1944
- description: `Added ${String(added)} rules/instructions`
2013
+ description: `Updated ${String(changed)} rules/instructions`
1945
2014
  };
1946
2015
  }
1947
2016
  ctx.write(filePath, serializeSettings$1(generated));
@@ -2115,13 +2184,13 @@ permissions:
2115
2184
  - name: Release
2116
2185
  env:
2117
2186
  GITHUB_TOKEN: \${{ github.token }}
2118
- run: pnpm exec tooling release:simple` : `
2187
+ run: pnpm exec bst release:simple` : `
2119
2188
  - name: Release
2120
2189
  env:
2121
2190
  FORGEJO_SERVER_URL: \${{ github.server_url }}
2122
2191
  FORGEJO_REPOSITORY: \${{ github.repository }}
2123
2192
  FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}
2124
- run: pnpm exec tooling release:simple`;
2193
+ run: pnpm exec bst release:simple`;
2125
2194
  return `${workflowSchemaComment(ci)}name: Release
2126
2195
  on:
2127
2196
  workflow_dispatch:
@@ -2161,7 +2230,7 @@ function changesetsReleaseStep(ci, publishesNpm) {
2161
2230
  FORGEJO_TOKEN: actionsExpr("secrets.FORGEJO_TOKEN"),
2162
2231
  ...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
2163
2232
  },
2164
- run: "pnpm exec tooling release:changesets"
2233
+ run: "pnpm exec bst release:changesets"
2165
2234
  }
2166
2235
  };
2167
2236
  }
@@ -2209,13 +2278,13 @@ function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
2209
2278
  case "simple":
2210
2279
  steps.push({
2211
2280
  match: { run: "release:simple" },
2212
- step: { run: "pnpm exec tooling release:simple" }
2281
+ step: { run: "pnpm exec bst release:simple" }
2213
2282
  });
2214
2283
  break;
2215
2284
  case "changesets":
2216
2285
  steps.push({
2217
2286
  match: { run: "changeset" },
2218
- step: { run: "pnpm exec tooling release:changesets" }
2287
+ step: { run: "pnpm exec bst release:changesets" }
2219
2288
  });
2220
2289
  break;
2221
2290
  }
@@ -2257,7 +2326,7 @@ async function generateReleaseCi(ctx) {
2257
2326
  action: "skipped",
2258
2327
  description: "Release CI workflow not applicable"
2259
2328
  };
2260
- const publishesNpm = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson).length > 0;
2329
+ const publishesNpm = ctx.config.publishNpm === true;
2261
2330
  if (ctx.config.releaseStrategy === "changesets") return generateChangesetsReleaseCi(ctx, publishesNpm);
2262
2331
  const isGitHub = ctx.config.ci === "github";
2263
2332
  const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
@@ -2278,21 +2347,13 @@ async function generateReleaseCi(ctx) {
2278
2347
  };
2279
2348
  const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm));
2280
2349
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2281
- if (withComment === content) {
2282
- ctx.write(workflowPath, content);
2283
- return {
2284
- filePath: workflowPath,
2285
- action: "updated",
2286
- description: "Added missing steps to release workflow"
2287
- };
2288
- }
2289
- if (await ctx.confirmOverwrite(workflowPath) === "skip") {
2290
- if (merged.changed || withComment !== merged.content) {
2350
+ if (!merged.changed) {
2351
+ if (withComment !== existing) {
2291
2352
  ctx.write(workflowPath, withComment);
2292
2353
  return {
2293
2354
  filePath: workflowPath,
2294
2355
  action: "updated",
2295
- description: "Added missing steps to release workflow"
2356
+ description: "Added schema comment to release workflow"
2296
2357
  };
2297
2358
  }
2298
2359
  return {
@@ -2301,11 +2362,11 @@ async function generateReleaseCi(ctx) {
2301
2362
  description: "Existing release workflow preserved"
2302
2363
  };
2303
2364
  }
2304
- ctx.write(workflowPath, content);
2365
+ ctx.write(workflowPath, withComment);
2305
2366
  return {
2306
2367
  filePath: workflowPath,
2307
2368
  action: "updated",
2308
- description: "Replaced release workflow with updated template"
2369
+ description: "Added missing steps to release workflow"
2309
2370
  };
2310
2371
  }
2311
2372
  return {
@@ -3085,15 +3146,19 @@ function contextAsDockerReader(ctx) {
3085
3146
  }
3086
3147
  /** Log what was detected so the user understands generator decisions. */
3087
3148
  function logDetectionSummary(ctx) {
3088
- const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
3089
- if (dockerPackages.length > 0) p.log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
3090
- const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
3091
- 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
+ }
3092
3157
  }
3093
3158
  async function runInit(config, options = {}) {
3094
3159
  const detected = detectProject(config.targetDir);
3095
3160
  const { ctx, archivedFiles } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
3096
- const result = await p.select({
3161
+ const result = await select({
3097
3162
  message: `${relativePath} already exists. What do you want to do?`,
3098
3163
  options: [{
3099
3164
  value: "overwrite",
@@ -3103,10 +3168,9 @@ async function runInit(config, options = {}) {
3103
3168
  label: "Skip"
3104
3169
  }]
3105
3170
  });
3106
- if (p.isCancel(result)) return "skip";
3171
+ if (isCancel(result)) return "skip";
3107
3172
  return result;
3108
3173
  }));
3109
- if (config.releaseStrategy !== "none" && !ctx.packageJson?.repository) p.log.warn(`package.json is missing a "repository" field — required for release strategy "${config.releaseStrategy}"`);
3110
3174
  logDetectionSummary(ctx);
3111
3175
  const results = await runGenerators(ctx);
3112
3176
  const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
@@ -3117,8 +3181,12 @@ async function runInit(config, options = {}) {
3117
3181
  });
3118
3182
  const created = results.filter((r) => r.action === "created");
3119
3183
  const updated = results.filter((r) => r.action === "updated");
3120
- if (!(created.length > 0 || updated.length > 0 || archivedFiles.length > 0) && options.noPrompt) {
3121
- 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.");
3122
3190
  return results;
3123
3191
  }
3124
3192
  if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
@@ -3131,18 +3199,15 @@ async function runInit(config, options = {}) {
3131
3199
  const summaryLines = [];
3132
3200
  if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
3133
3201
  if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
3134
- p.note(summaryLines.join("\n"), "Summary");
3202
+ note(summaryLines.join("\n"), "Summary");
3135
3203
  if (!options.noPrompt) {
3136
- const prompt = generateMigratePrompt(results, config, detected);
3137
- const promptPath = ".tooling-migrate.md";
3138
- ctx.write(promptPath, prompt);
3139
- p.log.info(`Migration prompt written to ${promptPath}`);
3140
- 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\"");
3141
3206
  }
3142
3207
  const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
3143
3208
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
3144
3209
  if (bensandeeDeps.length > 0 && hasLockfile) {
3145
- p.log.info("Updating @bensandee/* packages...");
3210
+ log$2.info("Updating @bensandee/* packages...");
3146
3211
  try {
3147
3212
  execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
3148
3213
  cwd: config.targetDir,
@@ -3150,10 +3215,17 @@ async function runInit(config, options = {}) {
3150
3215
  timeout: 6e4
3151
3216
  });
3152
3217
  } catch (_error) {
3153
- 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");
3154
3219
  }
3155
3220
  }
3156
- 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([
3157
3229
  "1. Run: pnpm install",
3158
3230
  "2. Run: pnpm check",
3159
3231
  ...options.noPrompt ? [] : ["3. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
@@ -3235,22 +3307,22 @@ async function runCheck(targetDir) {
3235
3307
  return true;
3236
3308
  });
3237
3309
  if (actionable.length === 0) {
3238
- p.log.success("Repository is up to date.");
3310
+ log$2.success("Repository is up to date.");
3239
3311
  return 0;
3240
3312
  }
3241
- 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`);
3242
3314
  for (const r of actionable) {
3243
- p.log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
3315
+ log$2.info(` ${r.action}: ${r.filePath} — ${r.description}`);
3244
3316
  const newContent = pendingWrites.get(r.filePath);
3245
3317
  if (!newContent) continue;
3246
3318
  const existingPath = path.join(targetDir, r.filePath);
3247
3319
  const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
3248
3320
  if (!existing) {
3249
3321
  const lineCount = newContent.split("\n").length - 1;
3250
- p.log.info(` + ${lineCount} new lines`);
3322
+ log$2.info(` + ${lineCount} new lines`);
3251
3323
  } else {
3252
3324
  const diff = lineDiff(existing, newContent);
3253
- for (const line of diff) p.log.info(` ${line}`);
3325
+ for (const line of diff) log$2.info(` ${line}`);
3254
3326
  }
3255
3327
  }
3256
3328
  return 1;
@@ -3497,7 +3569,7 @@ async function createRelease(executor, conn, tag) {
3497
3569
  //#region src/release/log.ts
3498
3570
  /** Log a debug message when verbose mode is enabled. */
3499
3571
  function debug(config, message) {
3500
- if (config.verbose) p.log.info(`[debug] ${message}`);
3572
+ if (config.verbose) log$2.info(`[debug] ${message}`);
3501
3573
  }
3502
3574
  /** Log the result of an exec call when verbose mode is enabled. */
3503
3575
  function debugExec(config, label, result) {
@@ -3505,7 +3577,7 @@ function debugExec(config, label, result) {
3505
3577
  const lines = [`[debug] ${label} (exit code ${String(result.exitCode)})`];
3506
3578
  if (result.stdout.trim()) lines.push(` stdout: ${result.stdout.trim()}`);
3507
3579
  if (result.stderr.trim()) lines.push(` stderr: ${result.stderr.trim()}`);
3508
- p.log.info(lines.join("\n"));
3580
+ log$2.info(lines.join("\n"));
3509
3581
  }
3510
3582
  //#endregion
3511
3583
  //#region src/release/version.ts
@@ -3577,7 +3649,7 @@ function buildPrContent(executor, cwd, packagesBefore) {
3577
3649
  }
3578
3650
  /** Mode 1: version packages and create/update a PR. */
3579
3651
  async function runVersionMode(executor, config) {
3580
- p.log.info("Changesets detected — versioning packages");
3652
+ log$2.info("Changesets detected — versioning packages");
3581
3653
  const packagesBefore = executor.listWorkspacePackages(config.cwd);
3582
3654
  debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
3583
3655
  const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
@@ -3603,19 +3675,19 @@ async function runVersionMode(executor, config) {
3603
3675
  const addResult = executor.exec("git add -A", { cwd: config.cwd });
3604
3676
  if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
3605
3677
  const remainingChangesets = executor.listChangesetFiles(config.cwd);
3606
- 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(", ")}`);
3607
3679
  debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
3608
3680
  const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
3609
3681
  debugExec(config, "git commit", commitResult);
3610
3682
  if (commitResult.exitCode !== 0) {
3611
- p.log.info("Nothing to commit after versioning");
3683
+ log$2.info("Nothing to commit after versioning");
3612
3684
  return {
3613
3685
  mode: "version",
3614
3686
  pr: "none"
3615
3687
  };
3616
3688
  }
3617
3689
  if (config.dryRun) {
3618
- p.log.info("[dry-run] Would push and create/update PR");
3690
+ log$2.info("[dry-run] Would push and create/update PR");
3619
3691
  return {
3620
3692
  mode: "version",
3621
3693
  pr: "none"
@@ -3638,7 +3710,7 @@ async function runVersionMode(executor, config) {
3638
3710
  base: "main",
3639
3711
  body
3640
3712
  });
3641
- p.log.info("Created version PR");
3713
+ log$2.info("Created version PR");
3642
3714
  return {
3643
3715
  mode: "version",
3644
3716
  pr: "created"
@@ -3648,7 +3720,7 @@ async function runVersionMode(executor, config) {
3648
3720
  title,
3649
3721
  body
3650
3722
  });
3651
- p.log.info(`Updated version PR #${String(existingPr)}`);
3723
+ log$2.info(`Updated version PR #${String(existingPr)}`);
3652
3724
  return {
3653
3725
  mode: "version",
3654
3726
  pr: "updated"
@@ -3673,7 +3745,7 @@ async function retryAsync(fn) {
3673
3745
  }
3674
3746
  /** Mode 2: publish to npm, push tags, and create Forgejo releases. */
3675
3747
  async function runPublishMode(executor, config) {
3676
- p.log.info("No changesets — publishing packages");
3748
+ log$2.info("No changesets — publishing packages");
3677
3749
  const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
3678
3750
  debugExec(config, "pnpm changeset publish", publishResult);
3679
3751
  if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
@@ -3688,11 +3760,11 @@ async function runPublishMode(executor, config) {
3688
3760
  debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
3689
3761
  if (config.dryRun) {
3690
3762
  if (tagsToPush.length === 0) {
3691
- p.log.info("No packages were published");
3763
+ log$2.info("No packages were published");
3692
3764
  return { mode: "none" };
3693
3765
  }
3694
- p.log.info(`Tags to process: ${tagsToPush.join(", ")}`);
3695
- 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");
3696
3768
  return {
3697
3769
  mode: "publish",
3698
3770
  tags: tagsToPush
@@ -3708,10 +3780,10 @@ async function runPublishMode(executor, config) {
3708
3780
  for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
3709
3781
  const allTags = [...tagsToPush, ...tagsWithMissingReleases];
3710
3782
  if (allTags.length === 0) {
3711
- p.log.info("No packages were published");
3783
+ log$2.info("No packages were published");
3712
3784
  return { mode: "none" };
3713
3785
  }
3714
- p.log.info(`Tags to process: ${allTags.join(", ")}`);
3786
+ log$2.info(`Tags to process: ${allTags.join(", ")}`);
3715
3787
  const errors = [];
3716
3788
  for (const tag of allTags) try {
3717
3789
  if (!remoteSet.has(tag)) {
@@ -3722,7 +3794,7 @@ async function runPublishMode(executor, config) {
3722
3794
  const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
3723
3795
  if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
3724
3796
  }
3725
- 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`);
3726
3798
  else {
3727
3799
  await retryAsync(async () => {
3728
3800
  try {
@@ -3732,14 +3804,14 @@ async function runPublishMode(executor, config) {
3732
3804
  throw error;
3733
3805
  }
3734
3806
  });
3735
- p.log.info(`Created release for ${tag}`);
3807
+ log$2.info(`Created release for ${tag}`);
3736
3808
  }
3737
3809
  } catch (error) {
3738
3810
  errors.push({
3739
3811
  tag,
3740
3812
  error
3741
3813
  });
3742
- 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)}`);
3743
3815
  }
3744
3816
  if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
3745
3817
  return {
@@ -3909,12 +3981,12 @@ async function triggerForgejo(conn, ref) {
3909
3981
  body: JSON.stringify({ ref })
3910
3982
  });
3911
3983
  if (!res.ok) throw new FatalError(`Failed to trigger Forgejo workflow: ${res.status} ${res.statusText}`);
3912
- p.log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
3984
+ log$2.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
3913
3985
  }
3914
3986
  function triggerGitHub(ref) {
3915
3987
  const result = createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
3916
3988
  if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
3917
- p.log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
3989
+ log$2.info(`Triggered release workflow on GitHub (ref: ${ref})`);
3918
3990
  }
3919
3991
  //#endregion
3920
3992
  //#region src/commands/forgejo-create-release.ts
@@ -3934,11 +4006,11 @@ const createForgejoReleaseCommand = defineCommand({
3934
4006
  const executor = createRealExecutor();
3935
4007
  const conn = resolved.conn;
3936
4008
  if (await findRelease(executor, conn, args.tag)) {
3937
- p.log.info(`Release for ${args.tag} already exists — skipping`);
4009
+ log$2.info(`Release for ${args.tag} already exists — skipping`);
3938
4010
  return;
3939
4011
  }
3940
4012
  await createRelease(executor, conn, args.tag);
3941
- p.log.info(`Created Forgejo release for ${args.tag}`);
4013
+ log$2.info(`Created Forgejo release for ${args.tag}`);
3942
4014
  }
3943
4015
  });
3944
4016
  //#endregion
@@ -3965,26 +4037,26 @@ async function mergeForgejo(conn, dryRun) {
3965
4037
  const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
3966
4038
  if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
3967
4039
  if (dryRun) {
3968
- 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}`);
3969
4041
  return;
3970
4042
  }
3971
4043
  await mergePr(executor, conn, prNumber, {
3972
4044
  method: "merge",
3973
4045
  deleteBranch: true
3974
4046
  });
3975
- 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}`);
3976
4048
  }
3977
4049
  function mergeGitHub(dryRun) {
3978
4050
  const executor = createRealExecutor();
3979
4051
  if (dryRun) {
3980
4052
  const prNum = executor.exec(`gh pr view ${HEAD_BRANCH} --json number --jq .number`, { cwd: process.cwd() }).stdout.trim();
3981
4053
  if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
3982
- 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}`);
3983
4055
  return;
3984
4056
  }
3985
4057
  const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
3986
4058
  if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
3987
- p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
4059
+ log$2.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
3988
4060
  }
3989
4061
  //#endregion
3990
4062
  //#region src/release/simple.ts
@@ -4017,7 +4089,7 @@ function readVersion(executor, cwd) {
4017
4089
  /** Run the full commit-and-tag-version release flow. */
4018
4090
  async function runSimpleRelease(executor, config) {
4019
4091
  const command = buildCommand(config);
4020
- p.log.info(`Running: ${command}`);
4092
+ log$2.info(`Running: ${command}`);
4021
4093
  const versionResult = executor.exec(command, { cwd: config.cwd });
4022
4094
  debugExec(config, "commit-and-tag-version", versionResult);
4023
4095
  if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
@@ -4027,12 +4099,12 @@ async function runSimpleRelease(executor, config) {
4027
4099
  debugExec(config, "git describe", tagResult);
4028
4100
  const tag = tagResult.stdout.trim();
4029
4101
  if (!tag) throw new FatalError("Could not determine the new tag from git describe");
4030
- p.log.info(`Version ${version} tagged as ${tag}`);
4102
+ log$2.info(`Version ${version} tagged as ${tag}`);
4031
4103
  if (config.dryRun) {
4032
4104
  const slidingTags = config.noSlidingTags ? [] : computeSlidingTags(version);
4033
- p.log.info(`[dry-run] Would push to origin with --follow-tags`);
4034
- if (slidingTags.length > 0) p.log.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
4035
- 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}`);
4036
4108
  return {
4037
4109
  version,
4038
4110
  tag,
@@ -4049,7 +4121,7 @@ async function runSimpleRelease(executor, config) {
4049
4121
  debugExec(config, "git push", pushResult);
4050
4122
  if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
4051
4123
  pushed = true;
4052
- p.log.info("Pushed to origin");
4124
+ log$2.info("Pushed to origin");
4053
4125
  }
4054
4126
  let slidingTags = [];
4055
4127
  if (!config.noSlidingTags && pushed) {
@@ -4060,8 +4132,8 @@ async function runSimpleRelease(executor, config) {
4060
4132
  }
4061
4133
  const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
4062
4134
  debugExec(config, "force-push sliding tags", forcePushResult);
4063
- if (forcePushResult.exitCode !== 0) p.log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
4064
- 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(", ")}`);
4065
4137
  }
4066
4138
  let releaseCreated = false;
4067
4139
  if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
@@ -4081,16 +4153,16 @@ async function createPlatformRelease(executor, config, tag) {
4081
4153
  return false;
4082
4154
  }
4083
4155
  await createRelease(executor, config.platform.conn, tag);
4084
- p.log.info(`Created Forgejo release for ${tag}`);
4156
+ log$2.info(`Created Forgejo release for ${tag}`);
4085
4157
  return true;
4086
4158
  }
4087
4159
  const ghResult = executor.exec(`gh release create ${tag} --generate-notes`, { cwd: config.cwd });
4088
4160
  debugExec(config, "gh release create", ghResult);
4089
4161
  if (ghResult.exitCode !== 0) {
4090
- 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}`);
4091
4163
  return false;
4092
4164
  }
4093
- p.log.info(`Created GitHub release for ${tag}`);
4165
+ log$2.info(`Created GitHub release for ${tag}`);
4094
4166
  return true;
4095
4167
  }
4096
4168
  //#endregion
@@ -4166,17 +4238,17 @@ const releaseSimpleCommand = defineCommand({
4166
4238
  //#region src/commands/repo-run-checks.ts
4167
4239
  const CHECKS = [
4168
4240
  { name: "build" },
4169
- { name: "docker:build" },
4170
4241
  { name: "typecheck" },
4171
4242
  { name: "lint" },
4172
4243
  { name: "test" },
4244
+ { name: "docker:build" },
4245
+ { name: "tooling:check" },
4246
+ { name: "docker:check" },
4173
4247
  {
4174
4248
  name: "format",
4175
4249
  args: "--check"
4176
4250
  },
4177
- { name: "knip" },
4178
- { name: "tooling:check" },
4179
- { name: "docker:check" }
4251
+ { name: "knip" }
4180
4252
  ];
4181
4253
  /** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
4182
4254
  function shouldSkip(name, patterns) {
@@ -4203,7 +4275,30 @@ function defaultExecCommand(cmd, cwd) {
4203
4275
  return 1;
4204
4276
  }
4205
4277
  }
4206
- 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
+ };
4207
4302
  function runRunChecks(targetDir, options = {}) {
4208
4303
  const exec = options.execCommand ?? defaultExecCommand;
4209
4304
  const getScripts = options.getScripts ?? defaultGetScripts;
@@ -4211,6 +4306,7 @@ function runRunChecks(targetDir, options = {}) {
4211
4306
  const add = options.add ?? [];
4212
4307
  const isCI = Boolean(process.env["CI"]);
4213
4308
  const failFast = options.failFast ?? !isCI;
4309
+ const reporter = isCI ? ciReporter : localReporter;
4214
4310
  const definedScripts = getScripts(targetDir);
4215
4311
  const addedNames = new Set(add);
4216
4312
  const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
@@ -4220,29 +4316,30 @@ function runRunChecks(targetDir, options = {}) {
4220
4316
  if (shouldSkip(check.name, skip)) continue;
4221
4317
  if (!definedScripts.has(check.name)) {
4222
4318
  if (addedNames.has(check.name)) {
4223
- p.log.error(`${check.name} not defined in package.json`);
4319
+ reporter.undefinedCheck(check.name);
4224
4320
  failures.push(check.name);
4225
4321
  } else notDefined.push(check.name);
4226
4322
  continue;
4227
4323
  }
4228
4324
  const cmd = check.args ? `pnpm run ${check.name} ${check.args}` : `pnpm run ${check.name}`;
4229
- if (isCI) ciLog(`::group::${check.name}`);
4325
+ reporter.groupStart(check.name);
4326
+ const start = Date.now();
4230
4327
  const exitCode = exec(cmd, targetDir);
4231
- if (isCI) ciLog("::endgroup::");
4232
- 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);
4233
4331
  else {
4234
- if (isCI) ciLog(`::error::${check.name} failed`);
4235
- p.log.error(`${check.name} failed`);
4332
+ reporter.failed(check.name, elapsed);
4236
4333
  failures.push(check.name);
4237
4334
  if (failFast) return 1;
4238
4335
  }
4239
4336
  }
4240
- if (notDefined.length > 0) p.log.info(`Skipped (not defined): ${notDefined.join(", ")}`);
4337
+ if (notDefined.length > 0) reporter.skippedNotDefined(notDefined);
4241
4338
  if (failures.length > 0) {
4242
- p.log.error(`Failed checks: ${failures.join(", ")}`);
4339
+ reporter.anyFailed(failures);
4243
4340
  return 1;
4244
4341
  }
4245
- p.log.success("All checks passed");
4342
+ reporter.allPassed();
4246
4343
  return 0;
4247
4344
  }
4248
4345
  const runChecksCommand = defineCommand({
@@ -4276,7 +4373,7 @@ const runChecksCommand = defineCommand({
4276
4373
  const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
4277
4374
  skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
4278
4375
  add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
4279
- failFast: args["fail-fast"] === true ? true : args["fail-fast"] === false ? false : void 0
4376
+ failFast: args["fail-fast"] ? true : void 0
4280
4377
  });
4281
4378
  process.exitCode = exitCode;
4282
4379
  }
@@ -4656,8 +4753,8 @@ const dockerCheckCommand = defineCommand({
4656
4753
  //#region src/bin.ts
4657
4754
  const main = defineCommand({
4658
4755
  meta: {
4659
- name: "tooling",
4660
- version: "0.26.0",
4756
+ name: "bst",
4757
+ version: "0.27.0",
4661
4758
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4662
4759
  },
4663
4760
  subCommands: {
@@ -4673,7 +4770,7 @@ const main = defineCommand({
4673
4770
  "docker:check": dockerCheckCommand
4674
4771
  }
4675
4772
  });
4676
- console.log(`@bensandee/tooling v0.26.0`);
4773
+ console.log(`@bensandee/tooling v0.27.0`);
4677
4774
  async function run() {
4678
4775
  await runMain(main);
4679
4776
  process.exit(process.exitCode ?? 0);
package/dist/index.d.mts CHANGED
@@ -44,6 +44,10 @@ interface ProjectConfig {
44
44
  setupRenovate: boolean;
45
45
  /** Release management strategy */
46
46
  releaseStrategy: ReleaseStrategy;
47
+ /** Opt-in: publish packages to npm registry */
48
+ publishNpm: boolean;
49
+ /** Opt-in: publish Docker images to a registry */
50
+ publishDocker: boolean;
47
51
  /** Project type determines tsconfig base configuration */
48
52
  projectType: "default" | "node" | "react" | "library";
49
53
  /** Auto-detect and configure tsconfig bases for monorepo packages */
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
- "tooling": "./dist/bin.mjs"
6
+ "bst": "./dist/bin.mjs"
7
7
  },
8
8
  "files": [
9
9
  "dist",
@@ -41,20 +41,21 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.12.0",
44
- "@types/picomatch": "^4.0.2",
44
+ "@types/picomatch": "4.0.2",
45
45
  "tsdown": "0.21.2",
46
46
  "typescript": "5.9.3",
47
- "vitest": "4.0.18",
48
- "@bensandee/config": "0.9.0"
47
+ "vitest": "4.1.0",
48
+ "@bensandee/config": "0.9.1"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@changesets/cli": "^2.29.4",
52
- "commit-and-tag-version": "^12.5.0"
51
+ "@changesets/cli": "^2.30.0",
52
+ "commit-and-tag-version": "^12.6.1"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsdown",
56
56
  "dev": "tsdown --watch",
57
57
  "typecheck": "tsc --noEmit",
58
- "test": "vitest run"
58
+ "test": "vitest run",
59
+ "generate:schema": "node scripts/generate-schema.ts"
59
60
  }
60
61
  }
@@ -3,107 +3,120 @@
3
3
  "title": "@bensandee/tooling configuration",
4
4
  "description": "Override convention-detected defaults for repo:sync. Only fields that differ from detected defaults need to be specified.",
5
5
  "type": "object",
6
- "additionalProperties": false,
7
6
  "properties": {
8
7
  "$schema": {
9
- "type": "string",
10
- "description": "JSON Schema reference (ignored by tooling)"
8
+ "description": "JSON Schema reference (ignored by tooling)",
9
+ "type": "string"
11
10
  },
12
11
  "structure": {
12
+ "description": "Project structure",
13
13
  "type": "string",
14
- "enum": ["single", "monorepo"],
15
- "description": "Project structure"
14
+ "enum": ["single", "monorepo"]
16
15
  },
17
16
  "useEslintPlugin": {
18
- "type": "boolean",
19
- "description": "Include @bensandee/eslint-plugin oxlint plugin"
17
+ "description": "Include @bensandee/eslint-plugin oxlint plugin",
18
+ "type": "boolean"
20
19
  },
21
20
  "formatter": {
21
+ "description": "Formatter choice",
22
22
  "type": "string",
23
- "enum": ["oxfmt", "prettier"],
24
- "description": "Formatter choice"
23
+ "enum": ["oxfmt", "prettier"]
25
24
  },
26
25
  "setupVitest": {
27
- "type": "boolean",
28
- "description": "Generate vitest config and example test"
26
+ "description": "Generate vitest config and example test",
27
+ "type": "boolean"
29
28
  },
30
29
  "ci": {
30
+ "description": "CI platform",
31
31
  "type": "string",
32
- "enum": ["github", "forgejo", "none"],
33
- "description": "CI platform"
32
+ "enum": ["github", "forgejo", "none"]
34
33
  },
35
34
  "setupRenovate": {
36
- "type": "boolean",
37
- "description": "Generate Renovate config"
35
+ "description": "Generate Renovate config",
36
+ "type": "boolean"
38
37
  },
39
38
  "releaseStrategy": {
39
+ "description": "Release management strategy",
40
40
  "type": "string",
41
- "enum": ["release-it", "simple", "changesets", "none"],
42
- "description": "Release management strategy"
41
+ "enum": ["release-it", "simple", "changesets", "none"]
42
+ },
43
+ "publishNpm": {
44
+ "description": "Publish packages to npm (opt-in)",
45
+ "type": "boolean"
46
+ },
47
+ "publishDocker": {
48
+ "description": "Publish Docker images to a registry (opt-in)",
49
+ "type": "boolean"
43
50
  },
44
51
  "projectType": {
52
+ "description": "Project type (determines tsconfig base)",
45
53
  "type": "string",
46
- "enum": ["default", "node", "react", "library"],
47
- "description": "Project type (determines tsconfig base)"
54
+ "enum": ["default", "node", "react", "library"]
48
55
  },
49
56
  "detectPackageTypes": {
50
- "type": "boolean",
51
- "description": "Auto-detect project types for monorepo packages"
57
+ "description": "Auto-detect project types for monorepo packages",
58
+ "type": "boolean"
52
59
  },
53
60
  "setupDocker": {
54
- "type": "boolean",
55
- "description": "Generate Docker build/check scripts"
61
+ "description": "Generate Docker build/check scripts",
62
+ "type": "boolean"
56
63
  },
57
64
  "docker": {
58
- "type": "object",
59
65
  "description": "Docker package overrides (package name → config)",
66
+ "type": "object",
67
+ "propertyNames": {
68
+ "type": "string"
69
+ },
60
70
  "additionalProperties": {
61
71
  "type": "object",
62
- "required": ["dockerfile"],
63
72
  "properties": {
64
73
  "dockerfile": {
65
74
  "type": "string",
66
75
  "description": "Path to Dockerfile relative to package"
67
76
  },
68
77
  "context": {
69
- "type": "string",
70
78
  "default": ".",
71
- "description": "Docker build context relative to package"
79
+ "description": "Docker build context relative to package",
80
+ "type": "string"
72
81
  }
73
82
  },
83
+ "required": ["dockerfile", "context"],
74
84
  "additionalProperties": false
75
85
  }
76
86
  },
77
87
  "dockerCheck": {
78
- "oneOf": [
88
+ "description": "Docker health check configuration or false to disable",
89
+ "anyOf": [
79
90
  {
80
- "const": false,
81
- "description": "Disable Docker health checks"
91
+ "type": "boolean",
92
+ "const": false
82
93
  },
83
94
  {
84
95
  "type": "object",
85
- "additionalProperties": false,
86
96
  "properties": {
87
97
  "composeFiles": {
98
+ "description": "Compose files to use",
88
99
  "type": "array",
89
- "items": { "type": "string" },
90
- "description": "Compose files to use"
100
+ "items": {
101
+ "type": "string"
102
+ }
91
103
  },
92
104
  "envFile": {
93
- "type": "string",
94
- "description": "Environment file for compose"
105
+ "description": "Environment file for compose",
106
+ "type": "string"
95
107
  },
96
108
  "services": {
109
+ "description": "Services to check (default: all)",
97
110
  "type": "array",
98
- "items": { "type": "string" },
99
- "description": "Services to check (default: all)"
111
+ "items": {
112
+ "type": "string"
113
+ }
100
114
  },
101
115
  "healthChecks": {
116
+ "description": "Health check definitions",
102
117
  "type": "array",
103
118
  "items": {
104
119
  "type": "object",
105
- "required": ["name", "url"],
106
- "additionalProperties": false,
107
120
  "properties": {
108
121
  "name": {
109
122
  "type": "string",
@@ -114,35 +127,41 @@
114
127
  "description": "Health check URL"
115
128
  },
116
129
  "status": {
130
+ "description": "Expected HTTP status code",
117
131
  "type": "integer",
118
- "description": "Expected HTTP status code"
132
+ "minimum": -9007199254740991,
133
+ "maximum": 9007199254740991
119
134
  }
120
- }
121
- },
122
- "description": "Health check definitions"
135
+ },
136
+ "required": ["name", "url"],
137
+ "additionalProperties": false
138
+ }
123
139
  },
124
140
  "buildCommand": {
125
- "type": "string",
126
- "description": "Command to build images before checking"
141
+ "description": "Command to build images before checking",
142
+ "type": "string"
127
143
  },
128
144
  "buildCwd": {
129
- "type": "string",
130
- "description": "Working directory for build command"
145
+ "description": "Working directory for build command",
146
+ "type": "string"
131
147
  },
132
148
  "timeoutMs": {
149
+ "description": "Overall timeout in milliseconds",
133
150
  "type": "integer",
134
- "minimum": 1,
135
- "description": "Overall timeout in milliseconds"
151
+ "exclusiveMinimum": 0,
152
+ "maximum": 9007199254740991
136
153
  },
137
154
  "pollIntervalMs": {
155
+ "description": "Poll interval in milliseconds",
138
156
  "type": "integer",
139
- "minimum": 1,
140
- "description": "Poll interval in milliseconds"
157
+ "exclusiveMinimum": 0,
158
+ "maximum": 9007199254740991
141
159
  }
142
- }
160
+ },
161
+ "additionalProperties": false
143
162
  }
144
- ],
145
- "description": "Docker health check configuration or false to disable"
163
+ ]
146
164
  }
147
- }
165
+ },
166
+ "additionalProperties": false
148
167
  }