@bensandee/tooling 0.25.2 → 0.26.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/README.md CHANGED
@@ -32,10 +32,32 @@ The tool auto-detects project structure, CI platform, project type, and Docker p
32
32
  | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
33
  | `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
34
34
  | `tooling repo:sync --check [dir]` | Dry-run drift detection. Exits 1 if files would change. CI-friendly. |
35
- | `tooling checks:run` | Run project checks (build, typecheck, lint, knip, test). Flag: `--fail-fast`. |
35
+ | `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`. |
36
36
 
37
37
  **Flags:** `--yes` (accept all defaults), `--no-ci`, `--no-prompt`, `--eslint-plugin`
38
38
 
39
+ #### `checks:run`
40
+
41
+ Runs checks in order: build, docker:build, typecheck, lint, test, format (--check), knip, tooling:check, docker:check. Checks without a matching script in `package.json` are silently skipped.
42
+
43
+ The `--skip` flag supports glob patterns via picomatch:
44
+
45
+ ```bash
46
+ # Skip all docker steps
47
+ tooling checks:run --skip 'docker:*'
48
+
49
+ # Skip specific checks
50
+ tooling checks:run --skip build,knip
51
+ ```
52
+
53
+ The `--add` flag appends extra checks (must be defined in `package.json`):
54
+
55
+ ```bash
56
+ tooling checks:run --add e2e
57
+ ```
58
+
59
+ The generated `ci:check` script defaults to `pnpm check --skip 'docker:*'` since CI environments typically lack Docker support.
60
+
39
61
  ### Release management
40
62
 
41
63
  | Command | Description |
@@ -99,7 +121,7 @@ To give individual packages a standalone `image:build` script for local testing:
99
121
  }
100
122
  ```
101
123
 
102
- **Flags:** `--package <dir>` (build a single package), `--verbose`
124
+ **Flags:** `--package <dir>` (build a single package)
103
125
 
104
126
  #### `docker:publish`
105
127
 
@@ -109,7 +131,7 @@ Tags generated per package: `latest`, `vX.Y.Z`, `vX.Y`, `vX`
109
131
 
110
132
  Each package is tagged independently using its own version, so packages in a monorepo can have different release cadences. Packages without a `version` field are rejected at publish time.
111
133
 
112
- **Flags:** `--dry-run` (build and tag only, skip login/push/logout), `--verbose`
134
+ **Flags:** `--dry-run` (build and tag only, skip login/push/logout)
113
135
 
114
136
  **Required environment variables:**
115
137
 
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-VAgrEX2D.mjs";
2
+ import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-D41R218h.mjs";
3
3
  import { defineCommand, runMain } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  import path from "node:path";
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
11
11
  import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } from "yaml";
12
12
  import { execSync } from "node:child_process";
13
+ import picomatch from "picomatch";
13
14
  import { tmpdir } from "node:os";
14
15
  //#region src/types.ts
15
16
  const LEGACY_TOOLS = [
@@ -904,7 +905,7 @@ const STANDARD_SCRIPTS_SINGLE = {
904
905
  lint: "oxlint",
905
906
  knip: "knip",
906
907
  check: "pnpm exec tooling checks:run",
907
- "ci:check": "pnpm check",
908
+ "ci:check": "pnpm check --skip 'docker:*'",
908
909
  "tooling:check": "pnpm exec tooling repo:sync --check",
909
910
  "tooling:sync": "pnpm exec tooling repo:sync"
910
911
  };
@@ -915,7 +916,7 @@ const STANDARD_SCRIPTS_MONOREPO = {
915
916
  lint: "oxlint",
916
917
  knip: "knip",
917
918
  check: "pnpm exec tooling checks:run",
918
- "ci:check": "pnpm check",
919
+ "ci:check": "pnpm check --skip 'docker:*'",
919
920
  "tooling:check": "pnpm exec tooling repo:sync --check",
920
921
  "tooling:sync": "pnpm exec tooling repo:sync"
921
922
  };
@@ -983,7 +984,7 @@ function getAddedDevDepNames(config) {
983
984
  const deps = { ...ROOT_DEV_DEPS };
984
985
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
985
986
  deps["@bensandee/config"] = "0.9.0";
986
- deps["@bensandee/tooling"] = "0.25.2";
987
+ deps["@bensandee/tooling"] = "0.26.0";
987
988
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
988
989
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
989
990
  addReleaseDeps(deps, config);
@@ -1008,7 +1009,7 @@ async function generatePackageJson(ctx) {
1008
1009
  const devDeps = { ...ROOT_DEV_DEPS };
1009
1010
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1010
1011
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.0";
1011
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.25.2";
1012
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.26.0";
1012
1013
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1013
1014
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
1014
1015
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -2779,9 +2780,6 @@ function imageRef(namespace, imageName, tag) {
2779
2780
  function log$1(message) {
2780
2781
  console.log(message);
2781
2782
  }
2782
- function debug$1(verbose, message) {
2783
- if (verbose) console.log(`[debug] ${message}`);
2784
- }
2785
2783
  /** Read the repo name from root package.json. */
2786
2784
  function readRepoName(executor, cwd) {
2787
2785
  const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
@@ -2791,7 +2789,7 @@ function readRepoName(executor, cwd) {
2791
2789
  return repoName;
2792
2790
  }
2793
2791
  /** Build a single docker image from its config. Paths are resolved relative to cwd. */
2794
- function buildImage(executor, pkg, cwd, verbose, extraArgs) {
2792
+ function buildImage(executor, pkg, cwd, extraArgs) {
2795
2793
  const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
2796
2794
  const contextPath = path.resolve(cwd, pkg.docker.context);
2797
2795
  const command = [
@@ -2801,10 +2799,7 @@ function buildImage(executor, pkg, cwd, verbose, extraArgs) {
2801
2799
  ...extraArgs,
2802
2800
  contextPath
2803
2801
  ].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}`);
2802
+ executor.execInherit(command);
2808
2803
  }
2809
2804
  /**
2810
2805
  * Detect packages with docker config in .tooling.json and build each one.
@@ -2818,7 +2813,7 @@ function runDockerBuild(executor, config) {
2818
2813
  if (config.packageDir) {
2819
2814
  const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
2820
2815
  log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
2821
- buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
2816
+ buildImage(executor, pkg, config.cwd, config.extraArgs);
2822
2817
  log$1(`Built ${pkg.imageName}:latest`);
2823
2818
  return { packages: [pkg] };
2824
2819
  }
@@ -2830,7 +2825,7 @@ function runDockerBuild(executor, config) {
2830
2825
  log$1(`Found ${packages.length} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
2831
2826
  for (const pkg of packages) {
2832
2827
  log$1(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
2833
- buildImage(executor, pkg, config.cwd, config.verbose, config.extraArgs);
2828
+ buildImage(executor, pkg, config.cwd, config.extraArgs);
2834
2829
  }
2835
2830
  log$1(`Built ${packages.length} image(s)`);
2836
2831
  return { packages };
@@ -2847,7 +2842,6 @@ function runDockerPublish(executor, config) {
2847
2842
  const { packages } = runDockerBuild(executor, {
2848
2843
  cwd: config.cwd,
2849
2844
  packageDir: void 0,
2850
- verbose: config.verbose,
2851
2845
  extraArgs: []
2852
2846
  });
2853
2847
  if (packages.length === 0) return {
@@ -3098,9 +3092,7 @@ function logDetectionSummary(ctx) {
3098
3092
  }
3099
3093
  async function runInit(config, options = {}) {
3100
3094
  const detected = detectProject(config.targetDir);
3101
- const s = p.spinner();
3102
3095
  const { ctx, archivedFiles } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
3103
- s.stop("Paused");
3104
3096
  const result = await p.select({
3105
3097
  message: `${relativePath} already exists. What do you want to do?`,
3106
3098
  options: [{
@@ -3111,20 +3103,12 @@ async function runInit(config, options = {}) {
3111
3103
  label: "Skip"
3112
3104
  }]
3113
3105
  });
3114
- s.start("Generating configuration files...");
3115
3106
  if (p.isCancel(result)) return "skip";
3116
3107
  return result;
3117
3108
  }));
3118
3109
  if (config.releaseStrategy !== "none" && !ctx.packageJson?.repository) p.log.warn(`package.json is missing a "repository" field — required for release strategy "${config.releaseStrategy}"`);
3119
3110
  logDetectionSummary(ctx);
3120
- s.start("Generating configuration files...");
3121
- let results;
3122
- try {
3123
- results = await runGenerators(ctx);
3124
- } catch (error) {
3125
- s.stop("Generation failed!");
3126
- throw error;
3127
- }
3111
+ const results = await runGenerators(ctx);
3128
3112
  const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
3129
3113
  for (const rel of archivedFiles) if (!alreadyArchived.has(rel)) results.push({
3130
3114
  filePath: rel,
@@ -3134,10 +3118,9 @@ async function runInit(config, options = {}) {
3134
3118
  const created = results.filter((r) => r.action === "created");
3135
3119
  const updated = results.filter((r) => r.action === "updated");
3136
3120
  if (!(created.length > 0 || updated.length > 0 || archivedFiles.length > 0) && options.noPrompt) {
3137
- s.stop("Repository is up to date.");
3121
+ p.log.success("Repository is up to date.");
3138
3122
  return results;
3139
3123
  }
3140
- s.stop("Done!");
3141
3124
  if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
3142
3125
  execSync("git config --unset core.hooksPath", {
3143
3126
  cwd: config.targetDir,
@@ -3159,16 +3142,15 @@ async function runInit(config, options = {}) {
3159
3142
  const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
3160
3143
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
3161
3144
  if (bensandeeDeps.length > 0 && hasLockfile) {
3162
- s.start("Updating @bensandee/* packages...");
3145
+ p.log.info("Updating @bensandee/* packages...");
3163
3146
  try {
3164
3147
  execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
3165
3148
  cwd: config.targetDir,
3166
- stdio: "ignore",
3149
+ stdio: "inherit",
3167
3150
  timeout: 6e4
3168
3151
  });
3169
- s.stop("Updated @bensandee/* packages");
3170
3152
  } catch (_error) {
3171
- s.stop("Could not update @bensandee/* packages — run pnpm install first");
3153
+ p.log.warn("Could not update @bensandee/* packages — run pnpm install manually");
3172
3154
  }
3173
3155
  }
3174
3156
  p.note([
@@ -3323,6 +3305,16 @@ function createRealExecutor() {
3323
3305
  };
3324
3306
  }
3325
3307
  },
3308
+ execInherit(command, options) {
3309
+ execSync(command, {
3310
+ cwd: options?.cwd,
3311
+ env: options?.env ? {
3312
+ ...process.env,
3313
+ ...options.env
3314
+ } : void 0,
3315
+ stdio: "inherit"
3316
+ });
3317
+ },
3326
3318
  fetch: globalThis.fetch,
3327
3319
  listChangesetFiles(cwd) {
3328
3320
  const dir = path.join(cwd, ".changeset");
@@ -4174,6 +4166,7 @@ const releaseSimpleCommand = defineCommand({
4174
4166
  //#region src/commands/repo-run-checks.ts
4175
4167
  const CHECKS = [
4176
4168
  { name: "build" },
4169
+ { name: "docker:build" },
4177
4170
  { name: "typecheck" },
4178
4171
  { name: "lint" },
4179
4172
  { name: "test" },
@@ -4185,6 +4178,11 @@ const CHECKS = [
4185
4178
  { name: "tooling:check" },
4186
4179
  { name: "docker:check" }
4187
4180
  ];
4181
+ /** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
4182
+ function shouldSkip(name, patterns) {
4183
+ if (patterns.size === 0) return false;
4184
+ return picomatch.isMatch(name, [...patterns]);
4185
+ }
4188
4186
  function defaultGetScripts(targetDir) {
4189
4187
  try {
4190
4188
  const pkg = parsePackageJson(readFileSync(path.join(targetDir, "package.json"), "utf-8"));
@@ -4219,7 +4217,7 @@ function runRunChecks(targetDir, options = {}) {
4219
4217
  const failures = [];
4220
4218
  const notDefined = [];
4221
4219
  for (const check of allChecks) {
4222
- if (skip.has(check.name)) continue;
4220
+ if (shouldSkip(check.name, skip)) continue;
4223
4221
  if (!definedScripts.has(check.name)) {
4224
4222
  if (addedNames.has(check.name)) {
4225
4223
  p.log.error(`${check.name} not defined in package.json`);
@@ -4295,16 +4293,10 @@ const publishDockerCommand = defineCommand({
4295
4293
  name: "docker:publish",
4296
4294
  description: "Build, tag, and push Docker images for packages with an image:build script"
4297
4295
  },
4298
- args: {
4299
- "dry-run": {
4300
- type: "boolean",
4301
- description: "Build and tag images but skip login, push, and logout"
4302
- },
4303
- verbose: {
4304
- type: "boolean",
4305
- description: "Enable detailed debug logging"
4306
- }
4307
- },
4296
+ args: { "dry-run": {
4297
+ type: "boolean",
4298
+ description: "Build and tag images but skip login, push, and logout"
4299
+ } },
4308
4300
  async run({ args }) {
4309
4301
  const config = {
4310
4302
  cwd: process.cwd(),
@@ -4312,8 +4304,7 @@ const publishDockerCommand = defineCommand({
4312
4304
  registryNamespace: requireEnv("DOCKER_REGISTRY_NAMESPACE"),
4313
4305
  username: requireEnv("DOCKER_USERNAME"),
4314
4306
  password: requireEnv("DOCKER_PASSWORD"),
4315
- dryRun: args["dry-run"] === true,
4316
- verbose: args.verbose === true
4307
+ dryRun: args["dry-run"] === true
4317
4308
  };
4318
4309
  runDockerPublish(createRealExecutor(), config);
4319
4310
  }
@@ -4346,10 +4337,6 @@ const dockerBuildCommand = defineCommand({
4346
4337
  type: "string",
4347
4338
  description: "Build a single package by directory path (e.g. packages/server). Useful as an image:build script."
4348
4339
  },
4349
- verbose: {
4350
- type: "boolean",
4351
- description: "Enable detailed debug logging"
4352
- },
4353
4340
  _: {
4354
4341
  type: "positional",
4355
4342
  required: false,
@@ -4370,7 +4357,6 @@ const dockerBuildCommand = defineCommand({
4370
4357
  runDockerBuild(executor, {
4371
4358
  cwd,
4372
4359
  packageDir,
4373
- verbose: args.verbose === true,
4374
4360
  extraArgs: extraArgs.filter((a) => a.length > 0)
4375
4361
  });
4376
4362
  }
@@ -4671,7 +4657,7 @@ const dockerCheckCommand = defineCommand({
4671
4657
  const main = defineCommand({
4672
4658
  meta: {
4673
4659
  name: "tooling",
4674
- version: "0.25.2",
4660
+ version: "0.26.0",
4675
4661
  description: "Bootstrap and maintain standardized TypeScript project tooling"
4676
4662
  },
4677
4663
  subCommands: {
@@ -4687,7 +4673,7 @@ const main = defineCommand({
4687
4673
  "docker:check": dockerCheckCommand
4688
4674
  }
4689
4675
  });
4690
- console.log(`@bensandee/tooling v0.25.2`);
4676
+ console.log(`@bensandee/tooling v0.26.0`);
4691
4677
  async function run() {
4692
4678
  await runMain(main);
4693
4679
  process.exit(process.exitCode ?? 0);
@@ -165,16 +165,21 @@ async function runDockerCheck(executor, config) {
165
165
  const healthStatus = new Map(config.healthChecks.map((c) => [c.name, false]));
166
166
  while (executor.now() - startTime < timeoutMs) {
167
167
  const containers = composePs(executor, compose);
168
- for (const service of compose.services) if (getContainerHealth(containers, service) === "unhealthy") {
169
- executor.logError(`Container ${service} is unhealthy`);
170
- composeLogs(executor, compose, service);
171
- cleanup();
172
- return {
173
- success: false,
174
- reason: "unhealthy-container",
175
- message: service,
176
- elapsedMs: executor.now() - startTime
177
- };
168
+ let allContainersHealthy = true;
169
+ for (const service of compose.services) {
170
+ const status = getContainerHealth(containers, service);
171
+ if (status === "unhealthy") {
172
+ executor.logError(`Container ${service} is unhealthy`);
173
+ composeLogs(executor, compose, service);
174
+ cleanup();
175
+ return {
176
+ success: false,
177
+ reason: "unhealthy-container",
178
+ message: service,
179
+ elapsedMs: executor.now() - startTime
180
+ };
181
+ }
182
+ if (status === "starting") allContainersHealthy = false;
178
183
  }
179
184
  for (const check of config.healthChecks) if (!healthStatus.get(check.name)) {
180
185
  if (await checkHttpHealth(executor, check)) {
@@ -182,7 +187,7 @@ async function runDockerCheck(executor, config) {
182
187
  executor.log(`${check.name} is healthy!`);
183
188
  }
184
189
  }
185
- if ([...healthStatus.values()].every(Boolean)) {
190
+ if (allContainersHealthy && [...healthStatus.values()].every(Boolean)) {
186
191
  executor.log("Check successful! All systems operational.");
187
192
  cleanup();
188
193
  return {
@@ -1,2 +1,2 @@
1
- import { a as composeDown, c as composeUp, i as composeCommand, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-VAgrEX2D.mjs";
1
+ import { a as composeDown, c as composeUp, i as composeCommand, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-D41R218h.mjs";
2
2
  export { checkHttpHealth, composeCommand, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runDockerCheck };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
@@ -34,12 +34,14 @@
34
34
  "citty": "^0.2.1",
35
35
  "json5": "^2.2.3",
36
36
  "jsonc-parser": "^3.3.1",
37
+ "picomatch": "^4.0.3",
37
38
  "yaml": "^2.8.2",
38
39
  "zod": "^4.3.6",
39
40
  "@bensandee/common": "0.1.2"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "24.12.0",
44
+ "@types/picomatch": "^4.0.2",
43
45
  "tsdown": "0.21.2",
44
46
  "typescript": "5.9.3",
45
47
  "vitest": "4.0.18",