@bensandee/tooling 0.60.0 → 0.60.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/README.md +15 -1
- package/dist/bin.mjs +118 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ 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, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`.
|
|
35
|
+
| `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`, `--with-coverage`. |
|
|
36
36
|
|
|
37
37
|
**Flags:** `--yes` (accept all defaults), `--no-ci`, `--no-prompt`, `--eslint-plugin`
|
|
38
38
|
|
|
@@ -56,6 +56,20 @@ The `--add` flag appends extra checks (must be defined in `package.json`):
|
|
|
56
56
|
tooling checks:run --add e2e
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
The `--with-coverage` flag runs the `test` step via `pnpm run test:coverage` (when that script is defined), producing `coverage/coverage-summary.json` for the `coverage:*` commands.
|
|
60
|
+
|
|
61
|
+
**Extend the `check` script with `--add`, never a shell chain.** `repo:sync` generates `check` as a single rooted command (`bst checks:run`) and `ci:check` as `pnpm check --skip 'docker:*'`. When CI runs `pnpm ci:check --with-coverage`, pnpm appends the forwarded flag to the _end_ of the resolved script — so it reaches `bst checks:run` only if that is the last command. Adding extra checks via an `&&` chain silently drops `--with-coverage` and `--skip 'docker:*'`:
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
// ✓ forwarded flags reach bst checks:run
|
|
65
|
+
"check": "bst checks:run --add check:env-drift,check:custom"
|
|
66
|
+
|
|
67
|
+
// ✗ --with-coverage / --skip land on check:custom instead
|
|
68
|
+
"check": "bst checks:run && pnpm check:env-drift && pnpm check:custom"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`repo:sync` warns when it detects the broken shape, and `coverage:summary` / `coverage:check` name it as the likely cause if the summary file is missing.
|
|
72
|
+
|
|
59
73
|
The generated `ci:check` script defaults to `pnpm check --skip 'docker:*'` since CI environments typically lack Docker support.
|
|
60
74
|
|
|
61
75
|
### Custom blocks in workflows
|
package/dist/bin.mjs
CHANGED
|
@@ -1564,6 +1564,72 @@ async function generateDeployCi(ctx) {
|
|
|
1564
1564
|
};
|
|
1565
1565
|
}
|
|
1566
1566
|
//#endregion
|
|
1567
|
+
//#region src/utils/script-forwarding.ts
|
|
1568
|
+
/**
|
|
1569
|
+
* pnpm appends args forwarded via `pnpm <script> <args>` to the END of the
|
|
1570
|
+
* resolved script string. A forwarded flag therefore only reaches its intended
|
|
1571
|
+
* command when that command is the LAST command in the script. The generated
|
|
1572
|
+
* CI workflow depends on this two-hop forwarding:
|
|
1573
|
+
*
|
|
1574
|
+
* pnpm ci:check --with-coverage
|
|
1575
|
+
* → pnpm check --skip 'docker:*' --with-coverage
|
|
1576
|
+
* → bst checks:run --skip 'docker:*' --with-coverage
|
|
1577
|
+
*
|
|
1578
|
+
* If `check` or `ci:check` ends in a different command (e.g. an `&&` chain),
|
|
1579
|
+
* the forwarded flags silently land on the wrong command — tests run without
|
|
1580
|
+
* coverage and `--skip` becomes a no-op. This module detects that shape so
|
|
1581
|
+
* callers can surface a precise diagnosis instead of a confusing downstream
|
|
1582
|
+
* failure.
|
|
1583
|
+
*/
|
|
1584
|
+
/** The final command of a script, after splitting on `&&`, `||`, `|`, and `;`. */
|
|
1585
|
+
function lastCommandSegment(script) {
|
|
1586
|
+
return (script.split(/&&|\|\|?|;/).at(-1) ?? "").trim();
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* True when args forwarded to a `check` script would reach `bst checks:run`
|
|
1590
|
+
* — its last command must invoke `bst checks:run` (or the `bin.mjs checks:run`
|
|
1591
|
+
* variant used by the tooling repo itself). Trailing flags like `--add` are
|
|
1592
|
+
* fine; only the position of the invocation matters.
|
|
1593
|
+
*/
|
|
1594
|
+
function isCheckScriptForwardingSafe(script) {
|
|
1595
|
+
return /(?:^|[\s/])(?:bst|bin\.mjs)\s+checks:run(?=\s|$)/.test(lastCommandSegment(script));
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* True when args forwarded to a `ci:check` script would reach `pnpm check`
|
|
1599
|
+
* — its last command must invoke `pnpm check` (or `pnpm run check`).
|
|
1600
|
+
*/
|
|
1601
|
+
function isCiCheckScriptForwardingSafe(script) {
|
|
1602
|
+
return /(?:^|\s)pnpm\s+(?:run\s+)?check(?=\s|$)/.test(lastCommandSegment(script));
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Inspect a package.json `scripts` record for `check` / `ci:check` definitions
|
|
1606
|
+
* whose shape would swallow pnpm-forwarded args. Returns a human-readable hint
|
|
1607
|
+
* naming the problem and the fix, or undefined when forwarding is sound.
|
|
1608
|
+
*/
|
|
1609
|
+
function diagnoseCheckScriptForwarding(scripts) {
|
|
1610
|
+
if (!scripts) return void 0;
|
|
1611
|
+
const problems = [];
|
|
1612
|
+
const check = scripts["check"];
|
|
1613
|
+
if (check !== void 0 && !isCheckScriptForwardingSafe(check)) problems.push(`the \`check\` script does not end with \`bst checks:run\` (its last command is \`${lastCommandSegment(check)}\`)`);
|
|
1614
|
+
const ciCheck = scripts["ci:check"];
|
|
1615
|
+
if (ciCheck !== void 0 && !isCiCheckScriptForwardingSafe(ciCheck)) problems.push(`the \`ci:check\` script does not end with \`pnpm check\` (its last command is \`${lastCommandSegment(ciCheck)}\`)`);
|
|
1616
|
+
if (problems.length === 0) return void 0;
|
|
1617
|
+
return `Likely cause: ${problems.join(", and ")}. pnpm appends forwarded args (e.g. \`--with-coverage\`, \`--skip 'docker:*'\`) to the end of the resolved script, so they never reach \`bst checks:run\`. Make \`bst checks:run\` the last command — move extra checks into \`bst checks:run --add <name,...>\`.`;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Filesystem wrapper around {@link diagnoseCheckScriptForwarding}: reads
|
|
1621
|
+
* `package.json` from `targetDir` (best-effort) and diagnoses its scripts.
|
|
1622
|
+
*/
|
|
1623
|
+
function diagnoseCheckScriptForwardingAt(targetDir) {
|
|
1624
|
+
let scripts;
|
|
1625
|
+
try {
|
|
1626
|
+
scripts = parsePackageJson(readFileSync(path.join(targetDir, "package.json"), "utf-8"))?.scripts;
|
|
1627
|
+
} catch (_error) {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
return diagnoseCheckScriptForwarding(scripts);
|
|
1631
|
+
}
|
|
1632
|
+
//#endregion
|
|
1567
1633
|
//#region src/generators/package-json.ts
|
|
1568
1634
|
const STANDARD_SCRIPTS_SINGLE = {
|
|
1569
1635
|
build: "tsdown",
|
|
@@ -1841,7 +1907,7 @@ async function generatePackageJson(ctx) {
|
|
|
1841
1907
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1842
1908
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1843
1909
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.2";
|
|
1844
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.60.
|
|
1910
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.60.1";
|
|
1845
1911
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.0";
|
|
1846
1912
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1847
1913
|
"@changesets/cli": "2.31.0",
|
|
@@ -1901,6 +1967,8 @@ async function generatePackageJson(ctx) {
|
|
|
1901
1967
|
changes.push(`removed deprecated script: ${key}`);
|
|
1902
1968
|
}
|
|
1903
1969
|
pkg.scripts = existingScripts;
|
|
1970
|
+
const forwardingHint = diagnoseCheckScriptForwarding(existingScripts);
|
|
1971
|
+
if (forwardingHint) log.warn(`package.json: pnpm-forwarded check flags may be dropped. ${forwardingHint}`);
|
|
1904
1972
|
const existingDevDeps = pkg.devDependencies ?? {};
|
|
1905
1973
|
for (const [key, value] of Object.entries(devDeps)) if (!(key in existingDevDeps)) {
|
|
1906
1974
|
existingDevDeps[key] = value;
|
|
@@ -4903,7 +4971,7 @@ async function runGenerators(ctx) {
|
|
|
4903
4971
|
function generateMigratePrompt(results, config, detected) {
|
|
4904
4972
|
const sections = [];
|
|
4905
4973
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4906
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.60.
|
|
4974
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.60.1 repo:sync\` on ${timestamp}_\n`);
|
|
4907
4975
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
4908
4976
|
if (changed.length > 0) {
|
|
4909
4977
|
const items = changed.map((r) => {
|
|
@@ -5971,19 +6039,39 @@ async function runRelease(config, executor) {
|
|
|
5971
6039
|
}
|
|
5972
6040
|
//#endregion
|
|
5973
6041
|
//#region src/release/dispatch.ts
|
|
6042
|
+
const INPUT_FLAG = "--input";
|
|
6043
|
+
const INPUT_FLAG_EQ = "--input=";
|
|
5974
6044
|
/**
|
|
5975
|
-
*
|
|
6045
|
+
* Collect every `--input` value from a command's raw CLI args.
|
|
5976
6046
|
*
|
|
5977
|
-
*
|
|
5978
|
-
*
|
|
5979
|
-
*
|
|
6047
|
+
* citty@0.2.2 keeps only the LAST occurrence of a repeated named flag, so a
|
|
6048
|
+
* declared `input` arg cannot capture `--input a=1 --input b=2` — it silently
|
|
6049
|
+
* yields just `b=2`. Scanning the raw argv directly recovers all occurrences.
|
|
6050
|
+
* Both `--input k=v` (two tokens) and `--input=k=v` (one token) forms are
|
|
6051
|
+
* supported. Throws {@link FatalError} when `--input` has no following value.
|
|
6052
|
+
*/
|
|
6053
|
+
function collectInputArgs(rawArgs) {
|
|
6054
|
+
const values = [];
|
|
6055
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
6056
|
+
const arg = rawArgs[i];
|
|
6057
|
+
if (arg === void 0) continue;
|
|
6058
|
+
if (arg === INPUT_FLAG) {
|
|
6059
|
+
const next = rawArgs[i + 1];
|
|
6060
|
+
if (next === void 0) throw new FatalError("Missing value for --input: expected `--input key=value`");
|
|
6061
|
+
values.push(next);
|
|
6062
|
+
i += 1;
|
|
6063
|
+
} else if (arg.startsWith(INPUT_FLAG_EQ)) values.push(arg.slice(8));
|
|
6064
|
+
}
|
|
6065
|
+
return values;
|
|
6066
|
+
}
|
|
6067
|
+
/**
|
|
6068
|
+
* Parse `key=value` workflow-input strings (as collected by
|
|
6069
|
+
* {@link collectInputArgs}) into an inputs object. Throws {@link FatalError} on
|
|
6070
|
+
* a malformed entry — no `=`, or an empty key.
|
|
5980
6071
|
*/
|
|
5981
6072
|
function parseInputs(raw) {
|
|
5982
6073
|
const inputs = {};
|
|
5983
|
-
|
|
5984
|
-
const values = Array.isArray(raw) ? raw : [raw];
|
|
5985
|
-
for (const value of values) {
|
|
5986
|
-
if (typeof value !== "string") throw new FatalError(`Invalid --input value: expected key=value, got ${typeof value}`);
|
|
6074
|
+
for (const value of raw) {
|
|
5987
6075
|
const eq = value.indexOf("=");
|
|
5988
6076
|
if (eq <= 0) throw new FatalError(`Invalid --input "${value}": expected key=value`);
|
|
5989
6077
|
inputs[value.slice(0, eq)] = value.slice(eq + 1);
|
|
@@ -6101,10 +6189,10 @@ const workflowDispatchCommand = defineCommand({
|
|
|
6101
6189
|
required: false
|
|
6102
6190
|
}
|
|
6103
6191
|
},
|
|
6104
|
-
async run({ args }) {
|
|
6192
|
+
async run({ args, rawArgs }) {
|
|
6105
6193
|
if (!args.workflow) throw new FatalError("--workflow is required");
|
|
6106
6194
|
const ref = args.ref ?? "main";
|
|
6107
|
-
const inputs = parseInputs(
|
|
6195
|
+
const inputs = parseInputs(collectInputArgs(rawArgs));
|
|
6108
6196
|
await dispatchWorkflow(resolveConnection(process.cwd()), {
|
|
6109
6197
|
repo: args.repo,
|
|
6110
6198
|
workflow: args.workflow,
|
|
@@ -8405,12 +8493,26 @@ function formatTimestamp(iso) {
|
|
|
8405
8493
|
return `${String(date.getUTCFullYear())}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
8406
8494
|
}
|
|
8407
8495
|
//#endregion
|
|
8496
|
+
//#region src/commands/coverage-missing-summary.ts
|
|
8497
|
+
const COVERAGE_SUMMARY_FILE$3 = "coverage/coverage-summary.json";
|
|
8498
|
+
/**
|
|
8499
|
+
* Build the "coverage summary not found" error message. When the project's
|
|
8500
|
+
* `check` / `ci:check` scripts have a shape that drops pnpm-forwarded flags,
|
|
8501
|
+
* a precise diagnosis is appended — that mis-forwarding is the usual reason a
|
|
8502
|
+
* CI `--with-coverage` run produces no summary file.
|
|
8503
|
+
*/
|
|
8504
|
+
function missingSummaryMessage(targetDir) {
|
|
8505
|
+
const base = `${COVERAGE_SUMMARY_FILE$3} not found. Run \`pnpm test:coverage\` first, or add "json-summary" to your vitest reporters.`;
|
|
8506
|
+
const hint = diagnoseCheckScriptForwardingAt(targetDir);
|
|
8507
|
+
return hint ? `${base}\n\n${hint}` : base;
|
|
8508
|
+
}
|
|
8509
|
+
//#endregion
|
|
8408
8510
|
//#region src/commands/coverage-summary.ts
|
|
8409
8511
|
const COVERAGE_SUMMARY_FILE$2 = "coverage/coverage-summary.json";
|
|
8410
8512
|
const out$3 = (msg) => console.log(msg);
|
|
8411
8513
|
function readSummary(targetDir) {
|
|
8412
8514
|
const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE$2);
|
|
8413
|
-
if (!existsSync(summaryPath)) throw new FatalError(
|
|
8515
|
+
if (!existsSync(summaryPath)) throw new FatalError(missingSummaryMessage(targetDir));
|
|
8414
8516
|
return readFileSync(summaryPath, "utf-8");
|
|
8415
8517
|
}
|
|
8416
8518
|
function getHeadSha(targetDir) {
|
|
@@ -8493,7 +8595,7 @@ const coverageCheckCommand = defineCommand({
|
|
|
8493
8595
|
const targetDir = path.resolve(args.dir ?? ".");
|
|
8494
8596
|
const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE$1);
|
|
8495
8597
|
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
8496
|
-
if (!existsSync(summaryPath)) throw new FatalError(
|
|
8598
|
+
if (!existsSync(summaryPath)) throw new FatalError(missingSummaryMessage(targetDir));
|
|
8497
8599
|
const current = deriveBaseline(parseV8Summary(readFileSync(summaryPath, "utf-8")), { rootDir: targetDir });
|
|
8498
8600
|
if (!existsSync(baselinePath)) {
|
|
8499
8601
|
if (args["update-baseline"] === true) {
|
|
@@ -8580,7 +8682,7 @@ const out = (msg) => console.log(msg);
|
|
|
8580
8682
|
const main = defineCommand({
|
|
8581
8683
|
meta: {
|
|
8582
8684
|
name: "bst",
|
|
8583
|
-
version: "0.60.
|
|
8685
|
+
version: "0.60.1",
|
|
8584
8686
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
8585
8687
|
},
|
|
8586
8688
|
subCommands: {
|
|
@@ -8653,7 +8755,7 @@ const main = defineCommand({
|
|
|
8653
8755
|
})
|
|
8654
8756
|
}
|
|
8655
8757
|
});
|
|
8656
|
-
console.log(`@bensandee/tooling v0.60.
|
|
8758
|
+
console.log(`@bensandee/tooling v0.60.1`);
|
|
8657
8759
|
async function run() {
|
|
8658
8760
|
await runMain(main);
|
|
8659
8761
|
process.exit(process.exitCode ?? 0);
|