@bensandee/tooling 0.50.2 → 0.51.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
@@ -126,10 +126,11 @@ The generated release workflow exposes these as optional `workflow_dispatch` inp
126
126
 
127
127
  ### Docker
128
128
 
129
- | Command | Description |
130
- | ------------------------ | --------------------------------------------------- |
131
- | `tooling docker:build` | Build Docker images for discovered Docker packages. |
132
- | `tooling docker:publish` | Build, tag, and push Docker images to a registry. |
129
+ | Command | Description |
130
+ | ------------------------ | ------------------------------------------------------------------- |
131
+ | `tooling docker:build` | Build Docker images for discovered Docker packages. |
132
+ | `tooling docker:publish` | Build, tag, and push Docker images to a registry. |
133
+ | `tooling docker:check` | Start a Compose stack, run health checks, run smoketest, tear down. |
133
134
 
134
135
  Docker packages are discovered automatically. Any package with a `Dockerfile` or `docker/Dockerfile` is a Docker package. Image names are derived as `{root-package-name}-{package-name}`, build context defaults to `.` (project root). For single-package repos, `Dockerfile` or `docker/Dockerfile` at the project root is checked.
135
136
 
@@ -255,9 +256,32 @@ import type {
255
256
 
256
257
  ## Docker check
257
258
 
258
- The `@bensandee/tooling/docker-check` export provides utilities for checking Docker Compose stacks via health checks.
259
+ `docker:check` verifies that a Docker Compose stack starts correctly, passes health checks, and (optionally) survives a smoketest command. The lifecycle is: build images, compose up, poll health checks, run smoketest, compose down.
259
260
 
260
- ### Quick start
261
+ ### Smoketests
262
+
263
+ A smoketest is a command that runs against the live Docker stack after all containers are healthy and HTTP health checks pass. This is useful for running integration tests, API contract checks, or any validation that requires the full stack to be running.
264
+
265
+ **Auto-detection:** If the root `package.json` has a `"test:smoke"` script, it is automatically used as the smoketest command — no configuration needed.
266
+
267
+ **Manual configuration:** For non-conventional command names, configure via `.tooling.json`:
268
+
269
+ ```json
270
+ {
271
+ "dockerCheck": {
272
+ "smoketest": ["pnpm", "test:integration"],
273
+ "smoketestCwd": "."
274
+ }
275
+ }
276
+ ```
277
+
278
+ If the smoketest command exits non-zero, `docker:check` dumps container logs and fails with reason `"smoketest-failed"`.
279
+
280
+ ### Library API
281
+
282
+ The `@bensandee/tooling/docker-check` export provides utilities for checking Docker Compose stacks programmatically.
283
+
284
+ #### Quick start
261
285
 
262
286
  ```ts
263
287
  import { createRealExecutor, runDockerCheck } from "@bensandee/tooling/docker-check";
@@ -269,7 +293,7 @@ const config: CheckConfig = {
269
293
  composeFiles: ["docker-compose.yaml"],
270
294
  services: ["api", "db"],
271
295
  },
272
- buildCommand: "pnpm image:build",
296
+ buildCommand: ["pnpm", "image:build"],
273
297
  healthChecks: [
274
298
  {
275
299
  name: "API",
@@ -277,6 +301,7 @@ const config: CheckConfig = {
277
301
  validate: async (res) => res.ok,
278
302
  },
279
303
  ],
304
+ smoketest: ["pnpm", "test:smoke"],
280
305
  timeoutMs: 120_000,
281
306
  pollIntervalMs: 5_000,
282
307
  };
@@ -287,24 +312,24 @@ if (!result.success) {
287
312
  }
288
313
  ```
289
314
 
290
- ### Exports
315
+ #### Exports
291
316
 
292
- | Export | Description |
293
- | -------------------------------------- | ----------------------------------------------------------------- |
294
- | `runDockerCheck(executor, config)` | Full lifecycle: build, compose up, health check polling, teardown |
295
- | `createRealExecutor()` | Production executor (real shell, fetch, timers) |
296
- | `composeUp(executor, config)` | Start compose services |
297
- | `composeDown(executor, config)` | Stop and remove compose services |
298
- | `composeLogs(executor, config)` | Stream compose logs |
299
- | `composePs(executor, config)` | List running containers |
300
- | `checkHttpHealth(executor, check)` | Run a single HTTP health check |
301
- | `getContainerHealth(executor, config)` | Check container-level health status |
317
+ | Export | Description |
318
+ | -------------------------------------- | ---------------------------------------------------------------------------- |
319
+ | `runDockerCheck(executor, config)` | Full lifecycle: build, compose up, health check polling, smoketest, teardown |
320
+ | `createRealExecutor()` | Production executor (real shell, fetch, timers) |
321
+ | `composeUp(executor, config)` | Start compose services |
322
+ | `composeDown(executor, config)` | Stop and remove compose services |
323
+ | `composeLogs(executor, config)` | Stream compose logs |
324
+ | `composePs(executor, config)` | List running containers |
325
+ | `checkHttpHealth(executor, check)` | Run a single HTTP health check |
326
+ | `getContainerHealth(executor, config)` | Check container-level health status |
302
327
 
303
- ### Types
328
+ #### Types
304
329
 
305
330
  | Type | Description |
306
331
  | --------------------- | ------------------------------------------------------------------------------------------ |
307
- | `CheckConfig` | Full check config (compose settings, build command, health checks, timeouts) |
332
+ | `CheckConfig` | Full check config (compose settings, build command, health checks, smoketest, timeouts) |
308
333
  | `ComposeConfig` | Docker Compose settings (cwd, compose files, env file, services) |
309
334
  | `HttpHealthCheck` | Health check definition (name, URL, validate function) |
310
335
  | `CheckResult` | Result: `{ success: true, elapsedMs }` or `{ success: false, reason, message, elapsedMs }` |
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as debug, f as debugExec, g as note, h as log, l as createRealExecutor$1, m as redact, p as isEnvVerbose, t as runDockerCheck, u as isExecSyncError } from "./check-hutCTIXe.mjs";
2
+ import { d as debug, f as debugExec, g as note, h as log, l as createRealExecutor$1, m as redact, p as isEnvVerbose, t as runDockerCheck, u as isExecSyncError } from "./check-COzp4S_Q.mjs";
3
3
  import { defineCommand, runMain } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  import { isCancel, select } from "@clack/prompts";
@@ -590,6 +590,8 @@ const DockerCheckConfigSchema = z.object({
590
590
  healthChecks: z.array(DeclarativeHealthCheckSchema).optional().meta({ description: "Health check definitions" }),
591
591
  buildCommand: z.array(z.string()).optional().meta({ description: "Command to build images before checking (e.g. [\"pnpm\", \"image:build\"])" }),
592
592
  buildCwd: z.string().optional().meta({ description: "Working directory for build command" }),
593
+ smoketest: z.array(z.string()).optional().meta({ description: "Command to run as a smoketest after health checks pass (e.g. [\"pnpm\", \"test:smoke\"])" }),
594
+ smoketestCwd: z.string().optional().meta({ description: "Working directory for smoketest command" }),
593
595
  timeoutMs: z.number().int().positive().optional().meta({ description: "Overall timeout in milliseconds" }),
594
596
  pollIntervalMs: z.number().int().positive().optional().meta({ description: "Poll interval in milliseconds" })
595
597
  });
@@ -1369,15 +1371,15 @@ const PER_PACKAGE_DEV_DEPS = {
1369
1371
  "@release-it/bumper": "7.0.5",
1370
1372
  "@types/node": "24.12.0",
1371
1373
  "commit-and-tag-version": "12.7.1",
1372
- "knip": "6.3.0",
1373
- "lefthook": "2.1.4",
1374
- "oxfmt": "0.43.0",
1375
- "oxlint": "1.58.0",
1376
- "prettier": "3.8.1",
1374
+ "knip": "6.3.1",
1375
+ "lefthook": "2.1.5",
1376
+ "oxfmt": "0.44.0",
1377
+ "oxlint": "1.59.0",
1378
+ "prettier": "3.8.2",
1377
1379
  "release-it": "19.2.4",
1378
1380
  "tsdown": "0.21.7",
1379
1381
  "typescript": "6.0.2",
1380
- "vitest": "4.1.2",
1382
+ "vitest": "4.1.4",
1381
1383
  "pnpm": "10.33.0"
1382
1384
  }["@types/node"],
1383
1385
  tsdown: {
@@ -1385,15 +1387,15 @@ const PER_PACKAGE_DEV_DEPS = {
1385
1387
  "@release-it/bumper": "7.0.5",
1386
1388
  "@types/node": "24.12.0",
1387
1389
  "commit-and-tag-version": "12.7.1",
1388
- "knip": "6.3.0",
1389
- "lefthook": "2.1.4",
1390
- "oxfmt": "0.43.0",
1391
- "oxlint": "1.58.0",
1392
- "prettier": "3.8.1",
1390
+ "knip": "6.3.1",
1391
+ "lefthook": "2.1.5",
1392
+ "oxfmt": "0.44.0",
1393
+ "oxlint": "1.59.0",
1394
+ "prettier": "3.8.2",
1393
1395
  "release-it": "19.2.4",
1394
1396
  "tsdown": "0.21.7",
1395
1397
  "typescript": "6.0.2",
1396
- "vitest": "4.1.2",
1398
+ "vitest": "4.1.4",
1397
1399
  "pnpm": "10.33.0"
1398
1400
  }["tsdown"],
1399
1401
  typescript: {
@@ -1401,15 +1403,15 @@ const PER_PACKAGE_DEV_DEPS = {
1401
1403
  "@release-it/bumper": "7.0.5",
1402
1404
  "@types/node": "24.12.0",
1403
1405
  "commit-and-tag-version": "12.7.1",
1404
- "knip": "6.3.0",
1405
- "lefthook": "2.1.4",
1406
- "oxfmt": "0.43.0",
1407
- "oxlint": "1.58.0",
1408
- "prettier": "3.8.1",
1406
+ "knip": "6.3.1",
1407
+ "lefthook": "2.1.5",
1408
+ "oxfmt": "0.44.0",
1409
+ "oxlint": "1.59.0",
1410
+ "prettier": "3.8.2",
1409
1411
  "release-it": "19.2.4",
1410
1412
  "tsdown": "0.21.7",
1411
1413
  "typescript": "6.0.2",
1412
- "vitest": "4.1.2",
1414
+ "vitest": "4.1.4",
1413
1415
  "pnpm": "10.33.0"
1414
1416
  }["typescript"],
1415
1417
  vitest: {
@@ -1417,15 +1419,15 @@ const PER_PACKAGE_DEV_DEPS = {
1417
1419
  "@release-it/bumper": "7.0.5",
1418
1420
  "@types/node": "24.12.0",
1419
1421
  "commit-and-tag-version": "12.7.1",
1420
- "knip": "6.3.0",
1421
- "lefthook": "2.1.4",
1422
- "oxfmt": "0.43.0",
1423
- "oxlint": "1.58.0",
1424
- "prettier": "3.8.1",
1422
+ "knip": "6.3.1",
1423
+ "lefthook": "2.1.5",
1424
+ "oxfmt": "0.44.0",
1425
+ "oxlint": "1.59.0",
1426
+ "prettier": "3.8.2",
1425
1427
  "release-it": "19.2.4",
1426
1428
  "tsdown": "0.21.7",
1427
1429
  "typescript": "6.0.2",
1428
- "vitest": "4.1.2",
1430
+ "vitest": "4.1.4",
1429
1431
  "pnpm": "10.33.0"
1430
1432
  }["vitest"]
1431
1433
  };
@@ -1436,15 +1438,15 @@ const ROOT_DEV_DEPS = {
1436
1438
  "@release-it/bumper": "7.0.5",
1437
1439
  "@types/node": "24.12.0",
1438
1440
  "commit-and-tag-version": "12.7.1",
1439
- "knip": "6.3.0",
1440
- "lefthook": "2.1.4",
1441
- "oxfmt": "0.43.0",
1442
- "oxlint": "1.58.0",
1443
- "prettier": "3.8.1",
1441
+ "knip": "6.3.1",
1442
+ "lefthook": "2.1.5",
1443
+ "oxfmt": "0.44.0",
1444
+ "oxlint": "1.59.0",
1445
+ "prettier": "3.8.2",
1444
1446
  "release-it": "19.2.4",
1445
1447
  "tsdown": "0.21.7",
1446
1448
  "typescript": "6.0.2",
1447
- "vitest": "4.1.2",
1449
+ "vitest": "4.1.4",
1448
1450
  "pnpm": "10.33.0"
1449
1451
  }["knip"],
1450
1452
  lefthook: {
@@ -1452,15 +1454,15 @@ const ROOT_DEV_DEPS = {
1452
1454
  "@release-it/bumper": "7.0.5",
1453
1455
  "@types/node": "24.12.0",
1454
1456
  "commit-and-tag-version": "12.7.1",
1455
- "knip": "6.3.0",
1456
- "lefthook": "2.1.4",
1457
- "oxfmt": "0.43.0",
1458
- "oxlint": "1.58.0",
1459
- "prettier": "3.8.1",
1457
+ "knip": "6.3.1",
1458
+ "lefthook": "2.1.5",
1459
+ "oxfmt": "0.44.0",
1460
+ "oxlint": "1.59.0",
1461
+ "prettier": "3.8.2",
1460
1462
  "release-it": "19.2.4",
1461
1463
  "tsdown": "0.21.7",
1462
1464
  "typescript": "6.0.2",
1463
- "vitest": "4.1.2",
1465
+ "vitest": "4.1.4",
1464
1466
  "pnpm": "10.33.0"
1465
1467
  }["lefthook"],
1466
1468
  oxlint: {
@@ -1468,15 +1470,15 @@ const ROOT_DEV_DEPS = {
1468
1470
  "@release-it/bumper": "7.0.5",
1469
1471
  "@types/node": "24.12.0",
1470
1472
  "commit-and-tag-version": "12.7.1",
1471
- "knip": "6.3.0",
1472
- "lefthook": "2.1.4",
1473
- "oxfmt": "0.43.0",
1474
- "oxlint": "1.58.0",
1475
- "prettier": "3.8.1",
1473
+ "knip": "6.3.1",
1474
+ "lefthook": "2.1.5",
1475
+ "oxfmt": "0.44.0",
1476
+ "oxlint": "1.59.0",
1477
+ "prettier": "3.8.2",
1476
1478
  "release-it": "19.2.4",
1477
1479
  "tsdown": "0.21.7",
1478
1480
  "typescript": "6.0.2",
1479
- "vitest": "4.1.2",
1481
+ "vitest": "4.1.4",
1480
1482
  "pnpm": "10.33.0"
1481
1483
  }["oxlint"]
1482
1484
  };
@@ -1509,15 +1511,15 @@ function addReleaseDeps(deps, config) {
1509
1511
  "@release-it/bumper": "7.0.5",
1510
1512
  "@types/node": "24.12.0",
1511
1513
  "commit-and-tag-version": "12.7.1",
1512
- "knip": "6.3.0",
1513
- "lefthook": "2.1.4",
1514
- "oxfmt": "0.43.0",
1515
- "oxlint": "1.58.0",
1516
- "prettier": "3.8.1",
1514
+ "knip": "6.3.1",
1515
+ "lefthook": "2.1.5",
1516
+ "oxfmt": "0.44.0",
1517
+ "oxlint": "1.59.0",
1518
+ "prettier": "3.8.2",
1517
1519
  "release-it": "19.2.4",
1518
1520
  "tsdown": "0.21.7",
1519
1521
  "typescript": "6.0.2",
1520
- "vitest": "4.1.2",
1522
+ "vitest": "4.1.4",
1521
1523
  "pnpm": "10.33.0"
1522
1524
  }["release-it"];
1523
1525
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
@@ -1525,15 +1527,15 @@ function addReleaseDeps(deps, config) {
1525
1527
  "@release-it/bumper": "7.0.5",
1526
1528
  "@types/node": "24.12.0",
1527
1529
  "commit-and-tag-version": "12.7.1",
1528
- "knip": "6.3.0",
1529
- "lefthook": "2.1.4",
1530
- "oxfmt": "0.43.0",
1531
- "oxlint": "1.58.0",
1532
- "prettier": "3.8.1",
1530
+ "knip": "6.3.1",
1531
+ "lefthook": "2.1.5",
1532
+ "oxfmt": "0.44.0",
1533
+ "oxlint": "1.59.0",
1534
+ "prettier": "3.8.2",
1533
1535
  "release-it": "19.2.4",
1534
1536
  "tsdown": "0.21.7",
1535
1537
  "typescript": "6.0.2",
1536
- "vitest": "4.1.2",
1538
+ "vitest": "4.1.4",
1537
1539
  "pnpm": "10.33.0"
1538
1540
  }["@release-it/bumper"];
1539
1541
  break;
@@ -1543,15 +1545,15 @@ function addReleaseDeps(deps, config) {
1543
1545
  "@release-it/bumper": "7.0.5",
1544
1546
  "@types/node": "24.12.0",
1545
1547
  "commit-and-tag-version": "12.7.1",
1546
- "knip": "6.3.0",
1547
- "lefthook": "2.1.4",
1548
- "oxfmt": "0.43.0",
1549
- "oxlint": "1.58.0",
1550
- "prettier": "3.8.1",
1548
+ "knip": "6.3.1",
1549
+ "lefthook": "2.1.5",
1550
+ "oxfmt": "0.44.0",
1551
+ "oxlint": "1.59.0",
1552
+ "prettier": "3.8.2",
1551
1553
  "release-it": "19.2.4",
1552
1554
  "tsdown": "0.21.7",
1553
1555
  "typescript": "6.0.2",
1554
- "vitest": "4.1.2",
1556
+ "vitest": "4.1.4",
1555
1557
  "pnpm": "10.33.0"
1556
1558
  }["commit-and-tag-version"];
1557
1559
  break;
@@ -1561,15 +1563,15 @@ function addReleaseDeps(deps, config) {
1561
1563
  "@release-it/bumper": "7.0.5",
1562
1564
  "@types/node": "24.12.0",
1563
1565
  "commit-and-tag-version": "12.7.1",
1564
- "knip": "6.3.0",
1565
- "lefthook": "2.1.4",
1566
- "oxfmt": "0.43.0",
1567
- "oxlint": "1.58.0",
1568
- "prettier": "3.8.1",
1566
+ "knip": "6.3.1",
1567
+ "lefthook": "2.1.5",
1568
+ "oxfmt": "0.44.0",
1569
+ "oxlint": "1.59.0",
1570
+ "prettier": "3.8.2",
1569
1571
  "release-it": "19.2.4",
1570
1572
  "tsdown": "0.21.7",
1571
1573
  "typescript": "6.0.2",
1572
- "vitest": "4.1.2",
1574
+ "vitest": "4.1.4",
1573
1575
  "pnpm": "10.33.0"
1574
1576
  }["@changesets/cli"];
1575
1577
  break;
@@ -1594,22 +1596,22 @@ async function generatePackageJson(ctx) {
1594
1596
  const devDeps = { ...ROOT_DEV_DEPS };
1595
1597
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1596
1598
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
1597
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.50.2";
1599
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.51.0";
1598
1600
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
1599
1601
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1600
1602
  "@changesets/cli": "2.30.0",
1601
1603
  "@release-it/bumper": "7.0.5",
1602
1604
  "@types/node": "24.12.0",
1603
1605
  "commit-and-tag-version": "12.7.1",
1604
- "knip": "6.3.0",
1605
- "lefthook": "2.1.4",
1606
- "oxfmt": "0.43.0",
1607
- "oxlint": "1.58.0",
1608
- "prettier": "3.8.1",
1606
+ "knip": "6.3.1",
1607
+ "lefthook": "2.1.5",
1608
+ "oxfmt": "0.44.0",
1609
+ "oxlint": "1.59.0",
1610
+ "prettier": "3.8.2",
1609
1611
  "release-it": "19.2.4",
1610
1612
  "tsdown": "0.21.7",
1611
1613
  "typescript": "6.0.2",
1612
- "vitest": "4.1.2",
1614
+ "vitest": "4.1.4",
1613
1615
  "pnpm": "10.33.0"
1614
1616
  }["oxfmt"];
1615
1617
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
@@ -1617,15 +1619,15 @@ async function generatePackageJson(ctx) {
1617
1619
  "@release-it/bumper": "7.0.5",
1618
1620
  "@types/node": "24.12.0",
1619
1621
  "commit-and-tag-version": "12.7.1",
1620
- "knip": "6.3.0",
1621
- "lefthook": "2.1.4",
1622
- "oxfmt": "0.43.0",
1623
- "oxlint": "1.58.0",
1624
- "prettier": "3.8.1",
1622
+ "knip": "6.3.1",
1623
+ "lefthook": "2.1.5",
1624
+ "oxfmt": "0.44.0",
1625
+ "oxlint": "1.59.0",
1626
+ "prettier": "3.8.2",
1625
1627
  "release-it": "19.2.4",
1626
1628
  "tsdown": "0.21.7",
1627
1629
  "typescript": "6.0.2",
1628
- "vitest": "4.1.2",
1630
+ "vitest": "4.1.4",
1629
1631
  "pnpm": "10.33.0"
1630
1632
  }["prettier"];
1631
1633
  addReleaseDeps(devDeps, ctx.config);
@@ -1711,15 +1713,15 @@ async function generatePackageJson(ctx) {
1711
1713
  "@release-it/bumper": "7.0.5",
1712
1714
  "@types/node": "24.12.0",
1713
1715
  "commit-and-tag-version": "12.7.1",
1714
- "knip": "6.3.0",
1715
- "lefthook": "2.1.4",
1716
- "oxfmt": "0.43.0",
1717
- "oxlint": "1.58.0",
1718
- "prettier": "3.8.1",
1716
+ "knip": "6.3.1",
1717
+ "lefthook": "2.1.5",
1718
+ "oxfmt": "0.44.0",
1719
+ "oxlint": "1.59.0",
1720
+ "prettier": "3.8.2",
1719
1721
  "release-it": "19.2.4",
1720
1722
  "tsdown": "0.21.7",
1721
1723
  "typescript": "6.0.2",
1722
- "vitest": "4.1.2",
1724
+ "vitest": "4.1.4",
1723
1725
  "pnpm": "10.33.0"
1724
1726
  }["pnpm"]}`
1725
1727
  };
@@ -2168,7 +2170,7 @@ const REQUIRED_ENTRIES = [
2168
2170
  const OPTIONAL_ENTRIES = [
2169
2171
  ".tooling-migrate.md",
2170
2172
  ".tooling-archived/",
2171
- ".claude/hooks/bst-hook.audit.local.jsonl"
2173
+ ".claude/hooks/bst-hook.audit.local.jsonl*"
2172
2174
  ];
2173
2175
  const ALL_ENTRIES = [...REQUIRED_ENTRIES, ...OPTIONAL_ENTRIES];
2174
2176
  /** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
@@ -3368,7 +3370,7 @@ async function runGenerators(ctx) {
3368
3370
  function generateMigratePrompt(results, config, detected) {
3369
3371
  const sections = [];
3370
3372
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3371
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.50.2 repo:sync\` on ${timestamp}_\n`);
3373
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.51.0 repo:sync\` on ${timestamp}_\n`);
3372
3374
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
3373
3375
  if (changed.length > 0) {
3374
3376
  const items = changed.map((r) => {
@@ -5529,6 +5531,12 @@ function generateCheckOverlay(services) {
5529
5531
  }
5530
5532
  return stringify({ services: serviceOverrides });
5531
5533
  }
5534
+ /** Detect a smoketest command from a `test:smoke` script in the root package.json. */
5535
+ function detectSmoketest(cwd) {
5536
+ try {
5537
+ if (parsePackageJson(readFileSync(path.join(cwd, "package.json"), "utf-8"))?.scripts?.["test:smoke"]) return ["pnpm", "test:smoke"];
5538
+ } catch (_error) {}
5539
+ }
5532
5540
  /** Auto-detect compose config from conventional file locations. */
5533
5541
  function computeCheckDefaults(cwd) {
5534
5542
  const { dir, files } = detectComposeFiles(cwd);
@@ -5538,13 +5546,15 @@ function computeCheckDefaults(cwd) {
5538
5546
  const envFile = detectCheckEnvFile(dir);
5539
5547
  const services = parseComposeServices(dir, files);
5540
5548
  const healthChecks = deriveHealthChecks(services);
5549
+ const smoketest = detectSmoketest(cwd);
5541
5550
  return {
5542
5551
  composeCwd: dir !== cwd ? dir : void 0,
5543
5552
  composeFiles: files,
5544
5553
  checkOverlay,
5545
5554
  envFile,
5546
5555
  services: services.map((s) => s.name),
5547
- healthChecks: healthChecks.length > 0 ? healthChecks : void 0
5556
+ healthChecks: healthChecks.length > 0 ? healthChecks : void 0,
5557
+ smoketest
5548
5558
  };
5549
5559
  }
5550
5560
  /** Create a DockerFileReader backed by the real filesystem. */
@@ -5605,11 +5615,15 @@ const dockerCheckCommand = defineCommand({
5605
5615
  },
5606
5616
  async run({ args }) {
5607
5617
  const cwd = process.cwd();
5608
- if (loadToolingConfig(cwd)?.dockerCheck === false) {
5618
+ const toolingConfig = loadToolingConfig(cwd);
5619
+ if (toolingConfig?.dockerCheck === false) {
5609
5620
  log.info("Docker check is disabled in .tooling.json");
5610
5621
  return;
5611
5622
  }
5612
- const defaults = computeCheckDefaults(cwd);
5623
+ const defaults = {
5624
+ ...computeCheckDefaults(cwd),
5625
+ ...toolingConfig?.dockerCheck
5626
+ };
5613
5627
  if (!defaults.composeFiles || defaults.composeFiles.length === 0) throw new FatalError("No compose files found. Expected docker-compose.yaml or compose.yaml in ./ or docker/.");
5614
5628
  if (!defaults.checkOverlay) {
5615
5629
  const composeCwd = defaults.composeCwd ?? cwd;
@@ -5648,6 +5662,8 @@ const dockerCheckCommand = defineCommand({
5648
5662
  buildCommand: defaults.buildCommand,
5649
5663
  buildCwd: defaults.buildCwd,
5650
5664
  healthChecks: defaults.healthChecks ? toHttpHealthChecks(defaults.healthChecks) : [],
5665
+ smoketest: defaults.smoketest,
5666
+ smoketestCwd: defaults.smoketestCwd,
5651
5667
  timeoutMs: args.timeout ? Number.parseInt(args.timeout, 10) : defaults.timeoutMs,
5652
5668
  pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs,
5653
5669
  verbose: args.verbose === true || isEnvVerbose()
@@ -6459,7 +6475,7 @@ const webhookSendCommand = defineCommand({
6459
6475
  const main = defineCommand({
6460
6476
  meta: {
6461
6477
  name: "bst",
6462
- version: "0.50.2",
6478
+ version: "0.51.0",
6463
6479
  description: "Bootstrap and maintain standardized TypeScript project tooling"
6464
6480
  },
6465
6481
  subCommands: {
@@ -6480,7 +6496,7 @@ const main = defineCommand({
6480
6496
  "webhook:send": webhookSendCommand
6481
6497
  }
6482
6498
  });
6483
- console.log(`@bensandee/tooling v0.50.2`);
6499
+ console.log(`@bensandee/tooling v0.51.0`);
6484
6500
  async function run() {
6485
6501
  await runMain(main);
6486
6502
  process.exit(process.exitCode ?? 0);
@@ -128,6 +128,13 @@ function buildImages(executor, config) {
128
128
  if (!file) return;
129
129
  executor.execInherit(file, args, { cwd: config.buildCwd ?? config.compose.cwd });
130
130
  }
131
+ /** Run the smoketest command against the running stack. */
132
+ function runSmoketest(executor, config) {
133
+ if (!config.smoketest) return;
134
+ const [file, ...args] = config.smoketest;
135
+ if (!file) return;
136
+ executor.execInherit(file, args, { cwd: config.smoketestCwd ?? config.compose.cwd });
137
+ }
131
138
  /** Start the compose stack in detached mode. */
132
139
  function composeUp(executor, config) {
133
140
  executor.execInherit("docker", [
@@ -263,6 +270,24 @@ async function runDockerCheck(executor, config) {
263
270
  }
264
271
  }
265
272
  if (allContainersHealthy && [...healthStatus.values()].every(Boolean)) {
273
+ if (config.smoketest) {
274
+ executor.log(`Running smoketest: ${config.smoketest.join(" ")}`);
275
+ try {
276
+ runSmoketest(executor, config);
277
+ executor.log("Smoketest passed!");
278
+ } catch (error) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ executor.logError(`Smoketest failed: ${message}`);
281
+ for (const service of compose.services) composeLogs(executor, compose, service);
282
+ cleanup();
283
+ return {
284
+ success: false,
285
+ reason: "smoketest-failed",
286
+ message,
287
+ elapsedMs: executor.now() - startTime
288
+ };
289
+ }
290
+ }
266
291
  executor.log("Check successful! All systems operational.");
267
292
  cleanup();
268
293
  return {
@@ -68,6 +68,10 @@ interface CheckConfig {
68
68
  buildCwd?: string;
69
69
  /** HTTP health checks to poll. All must pass for success. */
70
70
  healthChecks: HttpHealthCheck[];
71
+ /** Optional smoketest command to run after all health checks pass (e.g. ["pnpm", "test:smoke"]). */
72
+ smoketest?: string[];
73
+ /** Working directory for the smoketest command (defaults to compose.cwd). */
74
+ smoketestCwd?: string;
71
75
  /** Maximum time to wait for all checks to pass, in ms. Default: 120000. */
72
76
  timeoutMs?: number;
73
77
  /** Interval between polling attempts, in ms. Default: 5000. */
@@ -81,7 +85,7 @@ type CheckResult = {
81
85
  elapsedMs: number;
82
86
  } | {
83
87
  success: false;
84
- reason: "timeout" | "unhealthy-container" | "error";
88
+ reason: "timeout" | "unhealthy-container" | "smoketest-failed" | "error";
85
89
  message: string;
86
90
  elapsedMs: number;
87
91
  };
@@ -1,2 +1,2 @@
1
- import { a as composeDown, c as composeUp, i as composeArgs, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-hutCTIXe.mjs";
1
+ import { a as composeDown, c as composeUp, i as composeArgs, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-COzp4S_Q.mjs";
2
2
  export { checkHttpHealth, composeArgs, 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.50.2",
3
+ "version": "0.51.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,11 +51,11 @@
51
51
  "@types/node": "24.12.0",
52
52
  "@types/picomatch": "4.0.3",
53
53
  "commit-and-tag-version": "12.7.1",
54
- "prettier": "3.8.1",
54
+ "prettier": "3.8.2",
55
55
  "release-it": "19.2.4",
56
56
  "tsdown": "0.21.7",
57
- "@bensandee/config": "0.10.2",
58
- "@bensandee/claude-hooks": "0.7.0"
57
+ "@bensandee/claude-hooks": "0.7.0",
58
+ "@bensandee/config": "0.10.2"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",
@@ -144,6 +144,17 @@
144
144
  "description": "Working directory for build command",
145
145
  "type": "string"
146
146
  },
147
+ "smoketest": {
148
+ "description": "Command to run as a smoketest after health checks pass (e.g. [\"pnpm\", \"test:smoke\"])",
149
+ "type": "array",
150
+ "items": {
151
+ "type": "string"
152
+ }
153
+ },
154
+ "smoketestCwd": {
155
+ "description": "Working directory for smoketest command",
156
+ "type": "string"
157
+ },
147
158
  "timeoutMs": {
148
159
  "description": "Overall timeout in milliseconds",
149
160
  "type": "integer",