@bensandee/tooling 0.22.0 → 0.23.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
@@ -7,9 +7,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, w
7
7
  import JSON5 from "json5";
8
8
  import { parse } from "jsonc-parser";
9
9
  import { z } from "zod";
10
+ import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
10
11
  import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
11
12
  import { execSync } from "node:child_process";
12
- import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
13
13
  import { tmpdir } from "node:os";
14
14
  //#region src/types.ts
15
15
  const LEGACY_TOOLS = [
@@ -201,7 +201,7 @@ function computeDefaults(targetDir) {
201
201
  setupVitest: !isMonorepo && !detected.hasVitestConfig,
202
202
  ci: detectCiPlatform(targetDir),
203
203
  setupRenovate: true,
204
- releaseStrategy: isMonorepo ? "changesets" : "simple",
204
+ releaseStrategy: "none",
205
205
  projectType: isMonorepo ? "default" : detectProjectType(targetDir),
206
206
  detectPackageTypes: true
207
207
  };
@@ -493,7 +493,8 @@ const DockerCheckConfigSchema = z.object({
493
493
  timeoutMs: z.number().int().positive().optional(),
494
494
  pollIntervalMs: z.number().int().positive().optional()
495
495
  });
496
- const ToolingConfigSchema = z.object({
496
+ const ToolingConfigSchema = z.strictObject({
497
+ $schema: z.string().optional(),
497
498
  structure: z.enum(["single", "monorepo"]).optional(),
498
499
  useEslintPlugin: z.boolean().optional(),
499
500
  formatter: z.enum(["oxfmt", "prettier"]).optional(),
@@ -524,17 +525,14 @@ const ToolingConfigSchema = z.object({
524
525
  })).optional(),
525
526
  dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional()
526
527
  });
527
- /** Load saved tooling config from the target directory. Returns undefined if missing or invalid. */
528
+ /** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
528
529
  function loadToolingConfig(targetDir) {
529
530
  const fullPath = path.join(targetDir, CONFIG_FILE);
530
531
  if (!existsSync(fullPath)) return void 0;
531
- try {
532
- const raw = readFileSync(fullPath, "utf-8");
533
- const result = ToolingConfigSchema.safeParse(JSON.parse(raw));
534
- return result.success ? result.data : void 0;
535
- } catch {
536
- return;
537
- }
532
+ const raw = readFileSync(fullPath, "utf-8");
533
+ const result = ToolingConfigSchema.safeParse(JSON.parse(raw));
534
+ if (!result.success) throw new FatalError(`Invalid .tooling.json:\n${result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`);
535
+ return result.data;
538
536
  }
539
537
  /** Config fields that can be overridden in .tooling.json. */
540
538
  const OVERRIDE_KEYS = [
@@ -558,7 +556,7 @@ const MONOREPO_IGNORED_KEYS = new Set(["setupVitest", "projectType"]);
558
556
  function saveToolingConfig(ctx, config) {
559
557
  const defaults = computeDefaults(config.targetDir);
560
558
  const isMonorepo = config.structure === "monorepo";
561
- const overrides = {};
559
+ const overrides = { $schema: "node_modules/@bensandee/tooling/tooling.schema.json" };
562
560
  for (const key of OVERRIDE_KEYS) {
563
561
  if (isMonorepo && MONOREPO_IGNORED_KEYS.has(key)) continue;
564
562
  if (config[key] !== defaults[key]) overrides[key] = config[key];
@@ -983,7 +981,7 @@ function getAddedDevDepNames(config) {
983
981
  const deps = { ...ROOT_DEV_DEPS };
984
982
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
985
983
  deps["@bensandee/config"] = "0.8.2";
986
- deps["@bensandee/tooling"] = "0.22.0";
984
+ deps["@bensandee/tooling"] = "0.23.0";
987
985
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
988
986
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
989
987
  addReleaseDeps(deps, config);
@@ -1008,7 +1006,7 @@ async function generatePackageJson(ctx) {
1008
1006
  const devDeps = { ...ROOT_DEV_DEPS };
1009
1007
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1010
1008
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.2";
1011
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.22.0";
1009
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.23.0";
1012
1010
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1013
1011
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
1014
1012
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -1494,25 +1492,26 @@ function actionsExpr$1(expr) {
1494
1492
  }
1495
1493
  const CI_CONCURRENCY = {
1496
1494
  group: `ci-${actionsExpr$1("github.ref")}`,
1497
- "cancel-in-progress": true
1495
+ "cancel-in-progress": actionsExpr$1("github.ref != 'refs/heads/main'")
1498
1496
  };
1499
1497
  function hasEnginesNode$1(ctx) {
1500
1498
  const raw = ctx.read("package.json");
1501
1499
  if (!raw) return false;
1502
1500
  return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
1503
1501
  }
1504
- function ciWorkflow(nodeVersionYaml, isForgejo) {
1502
+ function ciWorkflow(nodeVersionYaml, isForgejo, isChangesets) {
1505
1503
  const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
1504
+ const concurrencyBlock = isChangesets ? `
1505
+ concurrency:
1506
+ group: ci-${actionsExpr$1("github.ref")}
1507
+ cancel-in-progress: ${actionsExpr$1("github.ref != 'refs/heads/main'")}
1508
+ ` : "";
1506
1509
  return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
1507
1510
  ${emailNotifications}on:
1508
1511
  push:
1509
1512
  branches: [main]
1510
1513
  pull_request:
1511
-
1512
- concurrency:
1513
- group: ci-${actionsExpr$1("github.ref")}
1514
- cancel-in-progress: true
1515
-
1514
+ ${concurrencyBlock}
1516
1515
  jobs:
1517
1516
  check:
1518
1517
  runs-on: ubuntu-latest
@@ -1561,6 +1560,10 @@ function requiredCheckSteps(nodeVersionYaml) {
1561
1560
  }
1562
1561
  ];
1563
1562
  }
1563
+ /** Resolve the CI workflow filename based on release strategy. */
1564
+ function ciWorkflowPath(ci, releaseStrategy) {
1565
+ return `${ci === "github" ? ".github/workflows" : ".forgejo/workflows"}/${releaseStrategy === "changesets" ? "ci.yml" : "check.yml"}`;
1566
+ }
1564
1567
  async function generateCi(ctx) {
1565
1568
  if (ctx.config.ci === "none") return {
1566
1569
  filePath: "ci",
@@ -1568,16 +1571,23 @@ async function generateCi(ctx) {
1568
1571
  description: "CI workflow not requested"
1569
1572
  };
1570
1573
  const isGitHub = ctx.config.ci === "github";
1574
+ const isChangesets = ctx.config.releaseStrategy === "changesets";
1571
1575
  const nodeVersionYaml = hasEnginesNode$1(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
1572
- const filePath = isGitHub ? ".github/workflows/ci.yml" : ".forgejo/workflows/ci.yml";
1573
- const content = ciWorkflow(nodeVersionYaml, !isGitHub);
1576
+ const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
1577
+ const content = ciWorkflow(nodeVersionYaml, !isGitHub, isChangesets);
1574
1578
  if (ctx.exists(filePath)) {
1575
1579
  const existing = ctx.read(filePath);
1576
1580
  if (existing) {
1577
- const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
1578
- const withConcurrency = ensureWorkflowConcurrency(merged.content, CI_CONCURRENCY);
1579
- const withComment = ensureSchemaComment(withConcurrency.content, isGitHub ? "github" : "forgejo");
1580
- if (merged.changed || withConcurrency.changed || withComment !== withConcurrency.content) {
1581
+ let result = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
1582
+ if (isChangesets) {
1583
+ const withConcurrency = ensureWorkflowConcurrency(result.content, CI_CONCURRENCY);
1584
+ result = {
1585
+ content: withConcurrency.content,
1586
+ changed: result.changed || withConcurrency.changed
1587
+ };
1588
+ }
1589
+ const withComment = ensureSchemaComment(result.content, isGitHub ? "github" : "forgejo");
1590
+ if (result.changed || withComment !== result.content) {
1581
1591
  ctx.write(filePath, withComment);
1582
1592
  return {
1583
1593
  filePath,
@@ -2215,7 +2225,7 @@ function buildWorkflow(strategy, ci, nodeVersionYaml, publishesNpm) {
2215
2225
  }
2216
2226
  }
2217
2227
  function generateChangesetsReleaseCi(ctx, publishesNpm) {
2218
- const ciPath = ctx.config.ci === "github" ? ".github/workflows/ci.yml" : ".forgejo/workflows/ci.yml";
2228
+ const ciPath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
2219
2229
  const existing = ctx.read(ciPath);
2220
2230
  if (!existing) return {
2221
2231
  filePath: ciPath,
@@ -4577,7 +4587,7 @@ const dockerCheckCommand = defineCommand({
4577
4587
  const main = defineCommand({
4578
4588
  meta: {
4579
4589
  name: "tooling",
4580
- version: "0.22.0",
4590
+ version: "0.23.0",
4581
4591
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4582
4592
  },
4583
4593
  subCommands: {
@@ -4593,7 +4603,7 @@ const main = defineCommand({
4593
4603
  "docker:check": dockerCheckCommand
4594
4604
  }
4595
4605
  });
4596
- console.log(`@bensandee/tooling v0.22.0`);
4606
+ console.log(`@bensandee/tooling v0.23.0`);
4597
4607
  runMain(main);
4598
4608
  //#endregion
4599
4609
  export {};
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "tooling.schema.json"
10
11
  ],
11
12
  "type": "module",
12
13
  "imports": {
@@ -22,7 +23,8 @@
22
23
  "types": "./dist/docker-check/index.d.mts",
23
24
  "default": "./dist/docker-check/index.mjs"
24
25
  },
25
- "./package.json": "./package.json"
26
+ "./package.json": "./package.json",
27
+ "./tooling.schema.json": "./tooling.schema.json"
26
28
  },
27
29
  "publishConfig": {
28
30
  "access": "public"
@@ -0,0 +1,148 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "@bensandee/tooling configuration",
4
+ "description": "Override convention-detected defaults for repo:sync. Only fields that differ from detected defaults need to be specified.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "JSON Schema reference (ignored by tooling)"
11
+ },
12
+ "structure": {
13
+ "type": "string",
14
+ "enum": ["single", "monorepo"],
15
+ "description": "Project structure"
16
+ },
17
+ "useEslintPlugin": {
18
+ "type": "boolean",
19
+ "description": "Include @bensandee/eslint-plugin oxlint plugin"
20
+ },
21
+ "formatter": {
22
+ "type": "string",
23
+ "enum": ["oxfmt", "prettier"],
24
+ "description": "Formatter choice"
25
+ },
26
+ "setupVitest": {
27
+ "type": "boolean",
28
+ "description": "Generate vitest config and example test"
29
+ },
30
+ "ci": {
31
+ "type": "string",
32
+ "enum": ["github", "forgejo", "none"],
33
+ "description": "CI platform"
34
+ },
35
+ "setupRenovate": {
36
+ "type": "boolean",
37
+ "description": "Generate Renovate config"
38
+ },
39
+ "releaseStrategy": {
40
+ "type": "string",
41
+ "enum": ["release-it", "simple", "changesets", "none"],
42
+ "description": "Release management strategy"
43
+ },
44
+ "projectType": {
45
+ "type": "string",
46
+ "enum": ["default", "node", "react", "library"],
47
+ "description": "Project type (determines tsconfig base)"
48
+ },
49
+ "detectPackageTypes": {
50
+ "type": "boolean",
51
+ "description": "Auto-detect project types for monorepo packages"
52
+ },
53
+ "setupDocker": {
54
+ "type": "boolean",
55
+ "description": "Generate Docker build/check scripts"
56
+ },
57
+ "docker": {
58
+ "type": "object",
59
+ "description": "Docker package overrides (package name → config)",
60
+ "additionalProperties": {
61
+ "type": "object",
62
+ "required": ["dockerfile"],
63
+ "properties": {
64
+ "dockerfile": {
65
+ "type": "string",
66
+ "description": "Path to Dockerfile relative to package"
67
+ },
68
+ "context": {
69
+ "type": "string",
70
+ "default": ".",
71
+ "description": "Docker build context relative to package"
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ }
76
+ },
77
+ "dockerCheck": {
78
+ "oneOf": [
79
+ {
80
+ "const": false,
81
+ "description": "Disable Docker health checks"
82
+ },
83
+ {
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "composeFiles": {
88
+ "type": "array",
89
+ "items": { "type": "string" },
90
+ "description": "Compose files to use"
91
+ },
92
+ "envFile": {
93
+ "type": "string",
94
+ "description": "Environment file for compose"
95
+ },
96
+ "services": {
97
+ "type": "array",
98
+ "items": { "type": "string" },
99
+ "description": "Services to check (default: all)"
100
+ },
101
+ "healthChecks": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "object",
105
+ "required": ["name", "url"],
106
+ "additionalProperties": false,
107
+ "properties": {
108
+ "name": {
109
+ "type": "string",
110
+ "description": "Service name"
111
+ },
112
+ "url": {
113
+ "type": "string",
114
+ "description": "Health check URL"
115
+ },
116
+ "status": {
117
+ "type": "integer",
118
+ "description": "Expected HTTP status code"
119
+ }
120
+ }
121
+ },
122
+ "description": "Health check definitions"
123
+ },
124
+ "buildCommand": {
125
+ "type": "string",
126
+ "description": "Command to build images before checking"
127
+ },
128
+ "buildCwd": {
129
+ "type": "string",
130
+ "description": "Working directory for build command"
131
+ },
132
+ "timeoutMs": {
133
+ "type": "integer",
134
+ "minimum": 1,
135
+ "description": "Overall timeout in milliseconds"
136
+ },
137
+ "pollIntervalMs": {
138
+ "type": "integer",
139
+ "minimum": 1,
140
+ "description": "Poll interval in milliseconds"
141
+ }
142
+ }
143
+ }
144
+ ],
145
+ "description": "Docker health check configuration or false to disable"
146
+ }
147
+ }
148
+ }