@bensandee/tooling 0.54.0 → 0.54.1

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
@@ -623,23 +623,44 @@ const ToolingConfigSchema = z.strictObject({
623
623
  ]).optional().meta({ description: "Project type (determines tsconfig base)" }),
624
624
  detectPackageTypes: z.boolean().optional().meta({ description: "Auto-detect project types for monorepo packages" }),
625
625
  docker: z.record(z.string(), z.object({
626
- dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
627
- context: z.string().default(".").meta({ description: "Docker build context relative to package" }),
626
+ dockerfile: z.string().optional().meta({ description: "Path to Dockerfile relative to package (falls back to convention when omitted)" }),
627
+ context: z.string().optional().meta({ description: "Docker build context relative to package (falls back to convention, default '.')" }),
628
628
  buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." })
629
- })).optional().meta({ description: "Docker package overrides (package name → config)" }),
629
+ })).optional().meta({ description: "Docker package overrides (package name → partial config; any field may be omitted to fall back to convention)" }),
630
630
  dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
631
631
  registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
632
632
  registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" }),
633
633
  postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)" }),
634
634
  postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" })
635
635
  });
636
+ /** Format a Zod issue as a human-readable bullet: location + plain-English explanation. */
637
+ function formatIssue(issue) {
638
+ const loc = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
639
+ if (issue.code === "invalid_type") {
640
+ if (issue.input === void 0) return ` ${loc}: missing (expected ${issue.expected})`;
641
+ return ` ${loc}: expected ${issue.expected}, got ${typeof issue.input}`;
642
+ }
643
+ if (issue.code === "unrecognized_keys") return ` ${loc}: ${issue.keys.length > 1 ? "unknown fields" : "unknown field"} ${issue.keys.map((k) => `"${k}"`).join(", ")}`;
644
+ return ` ${loc}: ${issue.message}`;
645
+ }
636
646
  /** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
637
647
  function loadToolingConfig(targetDir) {
638
648
  const fullPath = path.join(targetDir, CONFIG_FILE);
639
649
  if (!existsSync(fullPath)) return void 0;
640
650
  const raw = readFileSync(fullPath, "utf-8");
641
- const result = ToolingConfigSchema.safeParse(JSON.parse(raw));
642
- if (!result.success) throw new FatalError(`Invalid .tooling.json:\n${result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`);
651
+ const displayPath = path.relative(process.cwd(), fullPath) || fullPath;
652
+ let parsed;
653
+ try {
654
+ parsed = JSON.parse(raw);
655
+ } catch (error) {
656
+ throw new FatalError(`Could not parse ${displayPath} as JSON: ${error instanceof Error ? error.message : String(error)}`);
657
+ }
658
+ const result = ToolingConfigSchema.safeParse(parsed);
659
+ if (!result.success) {
660
+ const issues = result.error.issues.map(formatIssue).join("\n");
661
+ const count = result.error.issues.length;
662
+ throw new FatalError(`Invalid .tooling.json (${displayPath}) — ${count === 1 ? "1 problem" : `${count} problems`}:\n${issues}`);
663
+ }
643
664
  return result.data;
644
665
  }
645
666
  /** Config fields that can be overridden in .tooling.json. */
@@ -658,30 +679,42 @@ const OVERRIDE_KEYS = [
658
679
  ];
659
680
  /** Keys that have no effect for monorepos (generators ignore them). */
660
681
  const MONOREPO_IGNORED_KEYS = new Set(["setupVitest", "projectType"]);
682
+ const SCHEMA_REF = "node_modules/@bensandee/tooling/tooling.schema.json";
683
+ const RawJsonObjectSchema = z.record(z.string(), z.unknown());
684
+ /** Parse the raw .tooling.json contents into a plain object, or {} if invalid/missing. */
685
+ function parseExistingConfig(raw) {
686
+ if (raw === void 0) return {};
687
+ try {
688
+ const result = RawJsonObjectSchema.safeParse(JSON.parse(raw));
689
+ if (result.success) return result.data;
690
+ } catch (_error) {}
691
+ return {};
692
+ }
661
693
  /**
662
694
  * Save only the fields that differ from detected defaults to .tooling.json.
663
695
  * A fully conventional project produces `{}` (or a minimal set of overrides).
664
696
  * Keys that have no effect for the current structure are omitted.
697
+ *
698
+ * Non-override fields (e.g. `docker`, `dockerCheck`) are copied verbatim from the
699
+ * raw JSON — they are not re-parsed through Zod, so schema defaults like
700
+ * `docker.<pkg>.context: "."` are never injected into the user's file.
665
701
  */
666
702
  function saveToolingConfig(ctx, config) {
667
703
  const defaults = computeDefaults(config.targetDir);
668
704
  const isMonorepo = config.structure === "monorepo";
669
705
  const existing = ctx.exists(CONFIG_FILE) ? ctx.read(CONFIG_FILE) : void 0;
706
+ const existingJson = parseExistingConfig(existing);
670
707
  const overrideKeySet = new Set(OVERRIDE_KEYS);
671
- const preserved = {};
672
- if (existing) {
673
- const result = ToolingConfigSchema.safeParse(JSON.parse(existing));
674
- if (result.success) {
675
- for (const [key, value] of Object.entries(result.data)) if (key !== "$schema" && !overrideKeySet.has(key)) preserved[key] = value;
676
- }
677
- }
678
- const overrides = { $schema: "node_modules/@bensandee/tooling/tooling.schema.json" };
708
+ const result = { $schema: SCHEMA_REF };
679
709
  for (const key of OVERRIDE_KEYS) {
680
710
  if (isMonorepo && MONOREPO_IGNORED_KEYS.has(key)) continue;
681
- if (config[key] !== defaults[key]) overrides[key] = config[key];
711
+ if (config[key] !== defaults[key]) result[key] = config[key];
712
+ }
713
+ for (const [key, value] of Object.entries(existingJson)) {
714
+ if (key === "$schema" || overrideKeySet.has(key)) continue;
715
+ result[key] = value;
682
716
  }
683
- Object.assign(overrides, preserved);
684
- const content = JSON.stringify(overrides, null, 2) + "\n";
717
+ const content = JSON.stringify(result, null, 2) + "\n";
685
718
  if (existing !== void 0 && contentEqual(CONFIG_FILE, existing, content)) return {
686
719
  filePath: CONFIG_FILE,
687
720
  action: "skipped",
@@ -1700,7 +1733,7 @@ async function generatePackageJson(ctx) {
1700
1733
  const devDeps = { ...ROOT_DEV_DEPS };
1701
1734
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1702
1735
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
1703
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.54.0";
1736
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.54.1";
1704
1737
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
1705
1738
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1706
1739
  "@changesets/cli": "2.30.0",
@@ -3476,7 +3509,7 @@ async function runGenerators(ctx) {
3476
3509
  function generateMigratePrompt(results, config, detected) {
3477
3510
  const sections = [];
3478
3511
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3479
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.54.0 repo:sync\` on ${timestamp}_\n`);
3512
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.54.1 repo:sync\` on ${timestamp}_\n`);
3480
3513
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3481
3514
  if (changed.length > 0) {
3482
3515
  const items = changed.map((r) => {
@@ -6779,7 +6812,7 @@ async function sendWebhookWithLogging(url, secret, payload) {
6779
6812
  const main = defineCommand({
6780
6813
  meta: {
6781
6814
  name: "bst",
6782
- version: "0.54.0",
6815
+ version: "0.54.1",
6783
6816
  description: "Bootstrap and maintain standardized TypeScript project tooling"
6784
6817
  },
6785
6818
  subCommands: {
@@ -6847,7 +6880,7 @@ const main = defineCommand({
6847
6880
  })
6848
6881
  }
6849
6882
  });
6850
- console.log(`@bensandee/tooling v0.54.0`);
6883
+ console.log(`@bensandee/tooling v0.54.1`);
6851
6884
  async function run() {
6852
6885
  await runMain(main);
6853
6886
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.54.0",
3
+ "version": "0.54.1",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,8 +54,8 @@
54
54
  "prettier": "3.8.3",
55
55
  "release-it": "20.0.0",
56
56
  "tsdown": "0.21.9",
57
- "@bensandee/claude-hooks": "0.7.0",
58
- "@bensandee/config": "0.10.2"
57
+ "@bensandee/config": "0.10.2",
58
+ "@bensandee/claude-hooks": "0.7.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",
@@ -58,7 +58,7 @@
58
58
  "type": "boolean"
59
59
  },
60
60
  "docker": {
61
- "description": "Docker package overrides (package name → config)",
61
+ "description": "Docker package overrides (package name → partial config; any field may be omitted to fall back to convention)",
62
62
  "type": "object",
63
63
  "propertyNames": {
64
64
  "type": "string"
@@ -67,12 +67,11 @@
67
67
  "type": "object",
68
68
  "properties": {
69
69
  "dockerfile": {
70
- "type": "string",
71
- "description": "Path to Dockerfile relative to package"
70
+ "description": "Path to Dockerfile relative to package (falls back to convention when omitted)",
71
+ "type": "string"
72
72
  },
73
73
  "context": {
74
- "default": ".",
75
- "description": "Docker build context relative to package",
74
+ "description": "Docker build context relative to package (falls back to convention, default '.')",
76
75
  "type": "string"
77
76
  },
78
77
  "buildArgs": {
@@ -86,7 +85,6 @@
86
85
  }
87
86
  }
88
87
  },
89
- "required": ["dockerfile", "context"],
90
88
  "additionalProperties": false
91
89
  }
92
90
  },