@bensandee/tooling 0.10.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +143 -100
  2. package/package.json +1 -1
package/dist/bin.mjs CHANGED
@@ -516,7 +516,8 @@ const STANDARD_SCRIPTS_SINGLE = {
516
516
  test: "vitest run",
517
517
  lint: "oxlint",
518
518
  knip: "knip",
519
- check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip"
519
+ check: "pnpm exec tooling repo:run-checks",
520
+ "tooling:check": "pnpm exec tooling repo:check"
520
521
  };
521
522
  const STANDARD_SCRIPTS_MONOREPO = {
522
523
  build: "pnpm -r build",
@@ -524,7 +525,8 @@ const STANDARD_SCRIPTS_MONOREPO = {
524
525
  typecheck: "pnpm -r --parallel run typecheck",
525
526
  lint: "oxlint",
526
527
  knip: "knip",
527
- check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip"
528
+ check: "pnpm exec tooling repo:run-checks",
529
+ "tooling:check": "pnpm exec tooling repo:check"
528
530
  };
529
531
  /** DevDeps that belong in every project (single repo) or per-package (monorepo). */
530
532
  const PER_PACKAGE_DEV_DEPS = {
@@ -581,7 +583,7 @@ function getAddedDevDepNames(config) {
581
583
  const deps = { ...ROOT_DEV_DEPS };
582
584
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
583
585
  deps["@bensandee/config"] = "0.7.1";
584
- deps["@bensandee/tooling"] = "0.10.0";
586
+ deps["@bensandee/tooling"] = "0.11.0";
585
587
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
586
588
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
587
589
  addReleaseDeps(deps, config);
@@ -598,11 +600,11 @@ async function generatePackageJson(ctx) {
598
600
  format: formatScript
599
601
  };
600
602
  if (ctx.config.releaseStrategy === "changesets") allScripts["changeset"] = "changeset";
601
- if (ctx.config.releaseStrategy !== "none") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
603
+ if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
602
604
  const devDeps = { ...ROOT_DEV_DEPS };
603
605
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
604
606
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.7.1";
605
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.10.0";
607
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.11.0";
606
608
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.0";
607
609
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
608
610
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -1193,6 +1195,7 @@ async function generateTsdown(ctx) {
1193
1195
  /** Entries that every project should have — repo:check flags these as missing. */
1194
1196
  const REQUIRED_ENTRIES = [
1195
1197
  "node_modules/",
1198
+ ".pnpm-store/",
1196
1199
  "dist/",
1197
1200
  "*.tsbuildinfo",
1198
1201
  ".env",
@@ -1244,6 +1247,17 @@ async function generateGitignore(ctx) {
1244
1247
  //#endregion
1245
1248
  //#region src/utils/yaml-merge.ts
1246
1249
  const IGNORE_PATTERN = "@bensandee/tooling:ignore";
1250
+ const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
1251
+ /** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
1252
+ function workflowSchemaComment(ci) {
1253
+ return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
1254
+ }
1255
+ /** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
1256
+ function ensureSchemaComment(content, ci) {
1257
+ if (ci !== "forgejo") return content;
1258
+ if (content.includes("yaml-language-server")) return content;
1259
+ return FORGEJO_SCHEMA_COMMENT + content;
1260
+ }
1247
1261
  /** Check if a YAML file has an opt-out comment in the first 10 lines. */
1248
1262
  function isToolingIgnored(content) {
1249
1263
  return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
@@ -1343,9 +1357,10 @@ function hasEnginesNode$1(ctx) {
1343
1357
  if (!raw) return false;
1344
1358
  return typeof parsePackageJson(raw)?.engines?.["node"] === "string";
1345
1359
  }
1346
- function ciWorkflow(isMonorepo, nodeVersionYaml, isForgejo) {
1347
- return `name: CI
1348
- ${isForgejo ? "\nenable-email-notifications: true\n" : ""}on:
1360
+ function ciWorkflow(nodeVersionYaml, isForgejo) {
1361
+ const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
1362
+ return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
1363
+ ${emailNotifications}on:
1349
1364
  push:
1350
1365
  branches: [main]
1351
1366
  pull_request:
@@ -1362,19 +1377,11 @@ jobs:
1362
1377
  ${nodeVersionYaml}
1363
1378
  cache: pnpm
1364
1379
  - run: pnpm install --frozen-lockfile
1365
- - run: ${isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck"}
1366
- - run: pnpm lint
1367
- - run: ${isMonorepo ? "pnpm -r build" : "pnpm build"}
1368
- - run: ${isMonorepo ? "pnpm -r test" : "pnpm test"}
1369
- - run: pnpm format --check
1370
- - run: pnpm knip
1371
- - run: pnpm exec tooling repo:check
1380
+ - name: Run all checks
1381
+ run: pnpm check
1372
1382
  `;
1373
1383
  }
1374
- function requiredCheckSteps(isMonorepo, nodeVersionYaml) {
1375
- const buildCmd = isMonorepo ? "pnpm -r build" : "pnpm build";
1376
- const testCmd = isMonorepo ? "pnpm -r test" : "pnpm test";
1377
- const typecheckCmd = isMonorepo ? "pnpm -r --parallel run typecheck" : "pnpm typecheck";
1384
+ function requiredCheckSteps(nodeVersionYaml) {
1378
1385
  return [
1379
1386
  {
1380
1387
  match: { uses: "actions/checkout" },
@@ -1399,32 +1406,11 @@ function requiredCheckSteps(isMonorepo, nodeVersionYaml) {
1399
1406
  step: { run: "pnpm install --frozen-lockfile" }
1400
1407
  },
1401
1408
  {
1402
- match: { run: "typecheck" },
1403
- step: { run: typecheckCmd }
1404
- },
1405
- {
1406
- match: { run: "lint" },
1407
- step: { run: "pnpm lint" }
1408
- },
1409
- {
1410
- match: { run: "build" },
1411
- step: { run: buildCmd }
1412
- },
1413
- {
1414
- match: { run: "test" },
1415
- step: { run: testCmd }
1416
- },
1417
- {
1418
- match: { run: "format" },
1419
- step: { run: "pnpm format --check" }
1420
- },
1421
- {
1422
- match: { run: "knip" },
1423
- step: { run: "pnpm knip" }
1424
- },
1425
- {
1426
- match: { run: "repo:check" },
1427
- step: { run: "pnpm exec tooling repo:check" }
1409
+ match: { run: "check" },
1410
+ step: {
1411
+ name: "Run all checks",
1412
+ run: "pnpm check"
1413
+ }
1428
1414
  }
1429
1415
  ];
1430
1416
  }
@@ -1434,17 +1420,17 @@ async function generateCi(ctx) {
1434
1420
  action: "skipped",
1435
1421
  description: "CI workflow not requested"
1436
1422
  };
1437
- const isMonorepo = ctx.config.structure === "monorepo";
1438
1423
  const isGitHub = ctx.config.ci === "github";
1439
1424
  const nodeVersionYaml = hasEnginesNode$1(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
1440
1425
  const filePath = isGitHub ? ".github/workflows/check.yml" : ".forgejo/workflows/check.yml";
1441
- const content = ciWorkflow(isMonorepo, nodeVersionYaml, !isGitHub);
1426
+ const content = ciWorkflow(nodeVersionYaml, !isGitHub);
1442
1427
  if (ctx.exists(filePath)) {
1443
1428
  const existing = ctx.read(filePath);
1444
1429
  if (existing) {
1445
- const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(isMonorepo, nodeVersionYaml));
1446
- if (merged.changed) {
1447
- ctx.write(filePath, merged.content);
1430
+ const merged = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
1431
+ const withComment = ensureSchemaComment(merged.content, isGitHub ? "github" : "forgejo");
1432
+ if (merged.changed || withComment !== merged.content) {
1433
+ ctx.write(filePath, withComment);
1448
1434
  return {
1449
1435
  filePath,
1450
1436
  action: "updated",
@@ -1637,6 +1623,7 @@ function buildSettings(ctx) {
1637
1623
  `Bash(${pm} add *)`,
1638
1624
  `Bash(${pm} update *)`,
1639
1625
  `Bash(${pm} view *)`,
1626
+ `Bash(${pm} why *)`,
1640
1627
  `Bash(${pm} list)`,
1641
1628
  `Bash(${pm} list *)`,
1642
1629
  `Bash(${pm} ls)`,
@@ -1933,7 +1920,7 @@ permissions:
1933
1920
  contents: write
1934
1921
  ` : "";
1935
1922
  const tokenEnv = isGitHub ? `GITHUB_TOKEN: \${{ github.token }}` : `FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}`;
1936
- return `name: Release
1923
+ return `${workflowSchemaComment(ci)}name: Release
1937
1924
  on:
1938
1925
  workflow_dispatch:
1939
1926
  ${permissions}
@@ -1982,7 +1969,7 @@ permissions:
1982
1969
  TAG=$(git describe --tags --abbrev=0)
1983
1970
  pnpm publish --no-git-checks
1984
1971
  pnpm exec tooling release:create-forgejo-release --tag "$TAG"`;
1985
- return `name: Release
1972
+ return `${workflowSchemaComment(ci)}name: Release
1986
1973
  on:
1987
1974
  workflow_dispatch:
1988
1975
  ${permissions}
@@ -1994,7 +1981,7 @@ ${commonSteps(nodeVersionYaml)}${gitConfigStep}${releaseStep}
1994
1981
  `;
1995
1982
  }
1996
1983
  function changesetsWorkflow(ci, nodeVersionYaml) {
1997
- if (ci === "github") return `name: Release
1984
+ if (ci === "github") return `${workflowSchemaComment(ci)}name: Release
1998
1985
  on:
1999
1986
  push:
2000
1987
  branches:
@@ -2017,7 +2004,7 @@ ${commonSteps(nodeVersionYaml)}
2017
2004
  GITHUB_TOKEN: \${{ github.token }}
2018
2005
  NPM_TOKEN: \${{ secrets.NPM_TOKEN }}
2019
2006
  `;
2020
- return `name: Release
2007
+ return `${workflowSchemaComment(ci)}name: Release
2021
2008
  on:
2022
2009
  push:
2023
2010
  branches:
@@ -2125,8 +2112,9 @@ async function generateReleaseCi(ctx) {
2125
2112
  const existing = ctx.read(workflowPath);
2126
2113
  if (existing) {
2127
2114
  const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml));
2128
- if (merged.changed) {
2129
- ctx.write(workflowPath, merged.content);
2115
+ const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2116
+ if (merged.changed || withComment !== merged.content) {
2117
+ ctx.write(workflowPath, withComment);
2130
2118
  return {
2131
2119
  filePath: workflowPath,
2132
2120
  action: "updated",
@@ -2304,22 +2292,13 @@ async function generateLefthook(ctx) {
2304
2292
  const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json";
2305
2293
  const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
2306
2294
  const SETTINGS_PATH = ".vscode/settings.json";
2307
- const SCHEMA_GLOB = ".forgejo/workflows/*.yml";
2295
+ const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
2308
2296
  const VscodeSettingsSchema = z.object({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) }).passthrough();
2309
- const FullWorkspaceFileSchema = z.object({ settings: z.record(z.string(), z.unknown()).default({}) }).passthrough();
2310
2297
  function readSchemaFromNodeModules(targetDir) {
2311
2298
  const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
2312
2299
  if (!existsSync(candidate)) return void 0;
2313
2300
  return readFileSync(candidate, "utf-8");
2314
2301
  }
2315
- /** Find a *.code-workspace file in the target directory. */
2316
- function findWorkspaceFile(targetDir) {
2317
- try {
2318
- return readdirSync(targetDir).find((e) => e.endsWith(".code-workspace"));
2319
- } catch {
2320
- return;
2321
- }
2322
- }
2323
2302
  function serializeSettings(settings) {
2324
2303
  return JSON.stringify(settings, null, 2) + "\n";
2325
2304
  }
@@ -2341,36 +2320,6 @@ function mergeYamlSchemas(settings) {
2341
2320
  changed: true
2342
2321
  };
2343
2322
  }
2344
- function writeSchemaToWorkspaceFile(ctx, wsFileName) {
2345
- const raw = ctx.read(wsFileName);
2346
- if (!raw) return {
2347
- filePath: wsFileName,
2348
- action: "skipped",
2349
- description: "Could not read workspace file"
2350
- };
2351
- const fullParsed = FullWorkspaceFileSchema.safeParse(parse(raw));
2352
- if (!fullParsed.success) return {
2353
- filePath: wsFileName,
2354
- action: "skipped",
2355
- description: "Could not parse workspace file"
2356
- };
2357
- const { merged, changed } = mergeYamlSchemas(fullParsed.data.settings);
2358
- if (!changed) return {
2359
- filePath: wsFileName,
2360
- action: "skipped",
2361
- description: "Already has Forgejo schema mapping"
2362
- };
2363
- const updated = {
2364
- ...fullParsed.data,
2365
- settings: merged
2366
- };
2367
- ctx.write(wsFileName, JSON.stringify(updated, null, 2) + "\n");
2368
- return {
2369
- filePath: wsFileName,
2370
- action: "updated",
2371
- description: "Added Forgejo workflow schema mapping to workspace settings"
2372
- };
2373
- }
2374
2323
  function writeSchemaToSettings(ctx) {
2375
2324
  if (ctx.exists(SETTINGS_PATH)) {
2376
2325
  const raw = ctx.read(SETTINGS_PATH);
@@ -2438,8 +2387,7 @@ async function generateVscodeSettings(ctx) {
2438
2387
  description: "Copied Forgejo workflow schema from @bensandee/config"
2439
2388
  });
2440
2389
  }
2441
- const wsFile = findWorkspaceFile(ctx.targetDir);
2442
- results.push(wsFile ? writeSchemaToWorkspaceFile(ctx, wsFile) : writeSchemaToSettings(ctx));
2390
+ results.push(writeSchemaToSettings(ctx));
2443
2391
  return results;
2444
2392
  }
2445
2393
  //#endregion
@@ -3454,24 +3402,119 @@ function mergeGitHub(dryRun) {
3454
3402
  p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
3455
3403
  }
3456
3404
  //#endregion
3405
+ //#region src/commands/repo-run-checks.ts
3406
+ const CHECKS = [
3407
+ {
3408
+ name: "build",
3409
+ cmd: "pnpm run --if-present build"
3410
+ },
3411
+ {
3412
+ name: "typecheck",
3413
+ cmd: "pnpm run --if-present typecheck"
3414
+ },
3415
+ {
3416
+ name: "lint",
3417
+ cmd: "pnpm run --if-present lint"
3418
+ },
3419
+ {
3420
+ name: "test",
3421
+ cmd: "pnpm run --if-present test"
3422
+ },
3423
+ {
3424
+ name: "format",
3425
+ cmd: "pnpm run --if-present format -- --check"
3426
+ },
3427
+ {
3428
+ name: "knip",
3429
+ cmd: "pnpm run --if-present knip"
3430
+ },
3431
+ {
3432
+ name: "tooling:check",
3433
+ cmd: "pnpm run --if-present tooling:check"
3434
+ }
3435
+ ];
3436
+ function defaultExecCommand(cmd, cwd) {
3437
+ try {
3438
+ execSync(cmd, {
3439
+ cwd,
3440
+ stdio: "inherit"
3441
+ });
3442
+ return 0;
3443
+ } catch (err) {
3444
+ if (isExecSyncError(err)) return err.status;
3445
+ return 1;
3446
+ }
3447
+ }
3448
+ const ciLog = (msg) => console.log(msg);
3449
+ function runRunChecks(targetDir, options = {}) {
3450
+ const exec = options.execCommand ?? defaultExecCommand;
3451
+ const skip = options.skip ?? /* @__PURE__ */ new Set();
3452
+ const isCI = Boolean(process.env["CI"]);
3453
+ const failures = [];
3454
+ for (const check of CHECKS) {
3455
+ if (skip.has(check.name)) {
3456
+ p.log.info(`${check.name} (skipped)`);
3457
+ continue;
3458
+ }
3459
+ if (isCI) ciLog(`::group::${check.name}`);
3460
+ const exitCode = exec(check.cmd, targetDir);
3461
+ if (isCI) ciLog("::endgroup::");
3462
+ if (exitCode === 0) p.log.success(check.name);
3463
+ else {
3464
+ if (isCI) ciLog(`::error::${check.name} failed`);
3465
+ p.log.error(`${check.name} failed`);
3466
+ failures.push(check.name);
3467
+ }
3468
+ }
3469
+ if (failures.length > 0) {
3470
+ p.log.error(`Failed checks: ${failures.join(", ")}`);
3471
+ return 1;
3472
+ }
3473
+ p.log.success("All checks passed");
3474
+ return 0;
3475
+ }
3476
+ const runChecksCommand = defineCommand({
3477
+ meta: {
3478
+ name: "repo:run-checks",
3479
+ description: "Run all standard checks (build, typecheck, lint, test, format, knip, tooling:check)"
3480
+ },
3481
+ args: {
3482
+ dir: {
3483
+ type: "positional",
3484
+ description: "Target directory (default: current directory)",
3485
+ required: false
3486
+ },
3487
+ skip: {
3488
+ type: "string",
3489
+ description: "Comma-separated list of checks to skip (build, typecheck, lint, test, format, knip, tooling:check)",
3490
+ required: false
3491
+ }
3492
+ },
3493
+ run({ args }) {
3494
+ const exitCode = runRunChecks(path.resolve(args.dir ?? "."), { skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0 });
3495
+ process.exitCode = exitCode;
3496
+ }
3497
+ });
3498
+ //#endregion
3457
3499
  //#region src/bin.ts
3458
3500
  const main = defineCommand({
3459
3501
  meta: {
3460
3502
  name: "tooling",
3461
- version: "0.10.0",
3503
+ version: "0.11.0",
3462
3504
  description: "Bootstrap and maintain standardized TypeScript project tooling"
3463
3505
  },
3464
3506
  subCommands: {
3465
3507
  "repo:init": initCommand,
3466
3508
  "repo:update": updateCommand,
3467
3509
  "repo:check": checkCommand,
3510
+ "repo:run-checks": runChecksCommand,
3468
3511
  "release:changesets": releaseForgejoCommand,
3469
3512
  "release:trigger": releaseTriggerCommand,
3470
3513
  "release:create-forgejo-release": createForgejoReleaseCommand,
3471
3514
  "release:merge": releaseMergeCommand
3472
3515
  }
3473
3516
  });
3474
- console.log(`@bensandee/tooling v0.10.0`);
3517
+ console.log(`@bensandee/tooling v0.11.0`);
3475
3518
  runMain(main);
3476
3519
  //#endregion
3477
3520
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"