@bensandee/tooling 0.7.2 → 0.8.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 +158 -66
  2. package/package.json +2 -2
package/dist/bin.mjs CHANGED
@@ -160,6 +160,32 @@ function detectProjectType(targetDir) {
160
160
  if (pkg.exports || pkg.main || pkg.types || pkg.typings) return "library";
161
161
  return "default";
162
162
  }
163
+ const WEB_UI_DEPS = new Set([
164
+ "react",
165
+ "react-dom",
166
+ "vue",
167
+ "svelte",
168
+ "solid-js",
169
+ "next",
170
+ "nuxt",
171
+ "@angular/core",
172
+ "preact"
173
+ ]);
174
+ /** Check whether a package.json depends on a web UI framework. */
175
+ function packageHasWebUIDeps(pkg) {
176
+ const deps = {
177
+ ...pkg.dependencies,
178
+ ...pkg.devDependencies
179
+ };
180
+ for (const dep of WEB_UI_DEPS) if (dep in deps) return true;
181
+ return false;
182
+ }
183
+ /** Check whether a package.json in the given directory depends on a web UI framework. */
184
+ function hasWebUIDeps(targetDir) {
185
+ const pkg = readPackageJson(targetDir);
186
+ if (!pkg) return false;
187
+ return packageHasWebUIDeps(pkg);
188
+ }
163
189
  /** List packages in a monorepo's packages/ directory. */
164
190
  function getMonorepoPackages(targetDir) {
165
191
  const packagesDir = path.join(targetDir, "packages");
@@ -772,6 +798,8 @@ function generateMigratePrompt(results, config, detected) {
772
798
  }
773
799
  sections.push("## Ground rules");
774
800
  sections.push("");
801
+ sections.push("It is OK to add new packages (e.g. `zod`, `@bensandee/common`) if they are needed to resolve errors.");
802
+ sections.push("");
775
803
  sections.push("When resolving errors from the checklist below, prefer fixing the root cause over suppressing the issue. For example:");
776
804
  sections.push("");
777
805
  sections.push("- **Lint errors**: fix the code rather than adding disable comments or rule exceptions");
@@ -803,13 +831,12 @@ function generateMigratePrompt(results, config, detected) {
803
831
  async function generateTsconfig(ctx) {
804
832
  const filePath = "tsconfig.json";
805
833
  const existing = ctx.read(filePath);
806
- if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx, existing), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
834
+ if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
807
835
  const extendsValue = `@bensandee/config/tsconfig/${ctx.config.projectType}`;
808
836
  if (!existing) {
809
837
  const config = {
810
838
  extends: extendsValue,
811
- include: ["src"],
812
- exclude: ["node_modules", "dist"]
839
+ ...ctx.exists("src") ? { include: ["src"] } : {}
813
840
  };
814
841
  ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
815
842
  return [{
@@ -863,12 +890,14 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
863
890
  parsed.extends = extendsValue;
864
891
  changes.push(`added extends: ${extendsValue}`);
865
892
  }
866
- const existingInclude = parsed.include ?? [];
867
- if (!existingInclude.includes("src")) {
868
- existingInclude.push("src");
869
- changes.push("added \"src\" to include");
893
+ if (!parsed.include) {
894
+ const tsconfigDir = path.dirname(filePath);
895
+ const srcDir = tsconfigDir === "." ? "src" : path.join(tsconfigDir, "src");
896
+ if (ctx.exists(srcDir)) {
897
+ parsed.include = ["src"];
898
+ changes.push("added include: [\"src\"]");
899
+ }
870
900
  }
871
- parsed.include = existingInclude;
872
901
  if (changes.length === 0) return {
873
902
  filePath,
874
903
  action: "skipped",
@@ -881,31 +910,18 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
881
910
  description: changes.join(", ")
882
911
  };
883
912
  }
884
- function generateMonorepoRootTsconfig(ctx, existing) {
913
+ function generateMonorepoRootTsconfig(ctx) {
885
914
  const filePath = "tsconfig.json";
886
- if (existing) {
887
- const parsed = parseTsconfig(existing);
888
- if (!parsed.references) {
889
- parsed.files = [];
890
- parsed.references = [];
891
- ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
892
- return {
893
- filePath,
894
- action: "updated",
895
- description: "Added project references structure for monorepo"
896
- };
897
- }
898
- return {
899
- filePath,
900
- action: "skipped",
901
- description: "Already has project references"
902
- };
903
- }
904
- return {
915
+ if (!ctx.read(filePath)) return {
905
916
  filePath,
906
917
  action: "skipped",
907
918
  description: "No tsconfig.json found"
908
919
  };
920
+ return {
921
+ filePath,
922
+ action: "skipped",
923
+ description: "Root tsconfig left as-is"
924
+ };
909
925
  }
910
926
  function generateMonorepoPackageTsconfigs(ctx) {
911
927
  const packages = getMonorepoPackages(ctx.targetDir);
@@ -944,12 +960,10 @@ function generateMonorepoPackageTsconfigs(ctx) {
944
960
  parsed.extends = extendsValue;
945
961
  changes.push(prev ? `changed extends: ${String(prev)} → ${extendsValue}` : `added extends: ${extendsValue}`);
946
962
  }
947
- const existingInclude = parsed.include ?? [];
948
- if (!existingInclude.includes("src")) {
949
- existingInclude.push("src");
950
- changes.push("added \"src\" to include");
963
+ if (!parsed.include && ctx.exists(path.join(relDir, "src"))) {
964
+ parsed.include = ["src"];
965
+ changes.push("added include: [\"src\"]");
951
966
  }
952
- parsed.include = existingInclude;
953
967
  if (changes.length === 0) {
954
968
  results.push({
955
969
  filePath,
@@ -967,8 +981,7 @@ function generateMonorepoPackageTsconfigs(ctx) {
967
981
  } else {
968
982
  const config = {
969
983
  extends: extendsValue,
970
- include: ["src"],
971
- exclude: ["node_modules", "dist"]
984
+ ...ctx.exists(path.join(relDir, "src")) ? { include: ["src"] } : {}
972
985
  };
973
986
  ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
974
987
  results.push({
@@ -1107,23 +1120,15 @@ async function generateFormatter(ctx) {
1107
1120
  }
1108
1121
  async function generateOxfmt(ctx) {
1109
1122
  const filePath = ".oxfmtrc.json";
1110
- const existing = ctx.read(filePath);
1111
- if (existing) {
1112
- if (existing === OXFMT_CONFIG) return {
1113
- filePath,
1114
- action: "skipped",
1115
- description: "Already configured"
1116
- };
1117
- if (await ctx.confirmOverwrite(filePath) === "skip") return {
1118
- filePath,
1119
- action: "skipped",
1120
- description: "Existing oxfmt config preserved"
1121
- };
1122
- }
1123
+ if (ctx.exists(filePath)) return {
1124
+ filePath,
1125
+ action: "skipped",
1126
+ description: "Existing oxfmt config preserved"
1127
+ };
1123
1128
  ctx.write(filePath, OXFMT_CONFIG);
1124
1129
  return {
1125
1130
  filePath,
1126
- action: existing ? "updated" : "created",
1131
+ action: "created",
1127
1132
  description: "Generated .oxfmtrc.json"
1128
1133
  };
1129
1134
  }
@@ -1267,6 +1272,24 @@ jobs:
1267
1272
  - run: pnpm exec tooling repo:check
1268
1273
  `;
1269
1274
  }
1275
+ /**
1276
+ * Insert a step at the end of the `check` job's steps, even if other jobs
1277
+ * follow. Returns null if we can't find the right insertion point.
1278
+ */
1279
+ function insertStepIntoCheckJob(yaml, step) {
1280
+ const lines = yaml.split("\n");
1281
+ const checkJobIdx = lines.findIndex((l) => /^ {2}check:\s*$/.test(l));
1282
+ if (checkJobIdx === -1) return null;
1283
+ let lastStepIdx = -1;
1284
+ for (const [i, line] of lines.entries()) {
1285
+ if (i <= checkJobIdx) continue;
1286
+ if (/^ {2}\S/.test(line)) break;
1287
+ if (/^ {6}/.test(line)) lastStepIdx = i;
1288
+ }
1289
+ if (lastStepIdx === -1) return null;
1290
+ lines.splice(lastStepIdx + 1, 0, step.trimEnd());
1291
+ return lines.join("\n");
1292
+ }
1270
1293
  async function generateCi(ctx) {
1271
1294
  if (ctx.config.ci === "none") return {
1272
1295
  filePath: "ci",
@@ -1281,13 +1304,15 @@ async function generateCi(ctx) {
1281
1304
  if (ctx.exists(filePath)) {
1282
1305
  const existing = ctx.read(filePath);
1283
1306
  if (existing && !existing.includes("repo:check")) {
1284
- const patched = existing.trimEnd() + "\n - run: pnpm exec tooling repo:check\n";
1285
- ctx.write(filePath, patched);
1286
- return {
1287
- filePath,
1288
- action: "updated",
1289
- description: "Added `pnpm exec tooling repo:check` step to CI workflow"
1290
- };
1307
+ const patched = insertStepIntoCheckJob(existing, " - run: pnpm exec tooling repo:check\n");
1308
+ if (patched) {
1309
+ ctx.write(filePath, patched);
1310
+ return {
1311
+ filePath,
1312
+ action: "updated",
1313
+ description: "Added `pnpm exec tooling repo:check` step to CI workflow"
1314
+ };
1315
+ }
1291
1316
  }
1292
1317
  return {
1293
1318
  filePath,
@@ -1458,7 +1483,9 @@ const ClaudeSettingsSchema = z.object({
1458
1483
  allow: [],
1459
1484
  deny: []
1460
1485
  }),
1461
- instructions: z.array(z.string()).default([])
1486
+ instructions: z.array(z.string()).default([]),
1487
+ enabledPlugins: z.record(z.string(), z.boolean()).default({}),
1488
+ extraKnownMarketplaces: z.record(z.string(), z.record(z.string(), z.unknown())).default({})
1462
1489
  });
1463
1490
  function parseClaudeSettings(raw) {
1464
1491
  try {
@@ -1522,11 +1549,21 @@ function buildSettings(ctx) {
1522
1549
  "Bash(test *)",
1523
1550
  "Bash([ *)",
1524
1551
  "Bash(find *)",
1552
+ "Bash(grep *)",
1525
1553
  "Bash(which *)",
1526
1554
  "Bash(node -e *)",
1527
1555
  "Bash(node -p *)"
1528
1556
  ];
1529
1557
  if (ctx.config.structure === "monorepo") allow.push(`Bash(${pm} --filter *)`, `Bash(${pm} -r *)`);
1558
+ const enabledPlugins = { "code-simplifier@claude-plugins-official": true };
1559
+ const extraKnownMarketplaces = {};
1560
+ if (ctx.config.structure === "monorepo" ? getMonorepoPackages(ctx.targetDir).some((p) => hasWebUIDeps(p.dir)) : ctx.packageJson ? packageHasWebUIDeps(ctx.packageJson) : false) {
1561
+ enabledPlugins["example-skills@anthropic-agent-skills"] = true;
1562
+ extraKnownMarketplaces["anthropic-agent-skills"] = { source: {
1563
+ source: "github",
1564
+ repo: "anthropics/skills"
1565
+ } };
1566
+ }
1530
1567
  return {
1531
1568
  permissions: {
1532
1569
  allow,
@@ -1534,17 +1571,68 @@ function buildSettings(ctx) {
1534
1571
  "Bash(npx *)",
1535
1572
  "Bash(git push *)",
1536
1573
  "Bash(git push)",
1537
- "Bash(git reset --hard *)",
1538
- "Bash(rm -rf *)"
1574
+ "Bash(git add *)",
1575
+ "Bash(git add)",
1576
+ "Bash(git commit *)",
1577
+ "Bash(git commit)",
1578
+ "Bash(git reset *)",
1579
+ "Bash(git merge *)",
1580
+ "Bash(git rebase *)",
1581
+ "Bash(git cherry-pick *)",
1582
+ "Bash(git checkout *)",
1583
+ "Bash(git switch *)",
1584
+ "Bash(git stash *)",
1585
+ "Bash(git tag *)",
1586
+ "Bash(git revert *)",
1587
+ "Bash(git clean *)",
1588
+ "Bash(git rm *)",
1589
+ "Bash(git mv *)",
1590
+ "Bash(rm -rf *)",
1591
+ "Bash(cat *.env)",
1592
+ "Bash(cat *.env.*)",
1593
+ "Bash(cat .env)",
1594
+ "Bash(cat .env.*)",
1595
+ "Bash(head *.env)",
1596
+ "Bash(head *.env.*)",
1597
+ "Bash(head .env)",
1598
+ "Bash(head .env.*)",
1599
+ "Bash(tail *.env)",
1600
+ "Bash(tail *.env.*)",
1601
+ "Bash(tail .env)",
1602
+ "Bash(tail .env.*)",
1603
+ "Bash(less *.env)",
1604
+ "Bash(less *.env.*)",
1605
+ "Bash(less .env)",
1606
+ "Bash(less .env.*)",
1607
+ "Bash(more *.env)",
1608
+ "Bash(more *.env.*)",
1609
+ "Bash(more .env)",
1610
+ "Bash(more .env.*)",
1611
+ "Bash(grep * .env)",
1612
+ "Bash(grep * .env.*)",
1613
+ "Read(.env)",
1614
+ "Read(.env.*)",
1615
+ "Read(*.env)",
1616
+ "Read(*.env.*)"
1539
1617
  ]
1540
1618
  },
1541
1619
  instructions: [
1542
1620
  "Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
1543
1621
  "No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
1544
1622
  "Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option."
1545
- ]
1623
+ ],
1624
+ enabledPlugins,
1625
+ extraKnownMarketplaces
1546
1626
  };
1547
1627
  }
1628
+ /** Remove enabledPlugins/extraKnownMarketplaces when empty to keep the file clean. */
1629
+ function serializeSettings(settings) {
1630
+ const { enabledPlugins, extraKnownMarketplaces, ...rest } = settings;
1631
+ const out = { ...rest };
1632
+ if (Object.keys(enabledPlugins).length > 0) out["enabledPlugins"] = enabledPlugins;
1633
+ if (Object.keys(extraKnownMarketplaces).length > 0) out["extraKnownMarketplaces"] = extraKnownMarketplaces;
1634
+ return JSON.stringify(out, null, 2) + "\n";
1635
+ }
1548
1636
  async function generateClaudeSettings(ctx) {
1549
1637
  const filePath = ".claude/settings.json";
1550
1638
  const existing = ctx.read(filePath);
@@ -1559,7 +1647,10 @@ async function generateClaudeSettings(ctx) {
1559
1647
  const missingAllow = generated.permissions.allow.filter((rule) => !parsed.permissions.allow.includes(rule));
1560
1648
  const missingDeny = generated.permissions.deny.filter((rule) => !parsed.permissions.deny.includes(rule));
1561
1649
  const missingInstructions = generated.instructions.filter((inst) => !parsed.instructions.includes(inst));
1562
- if (missingAllow.length === 0 && missingDeny.length === 0 && missingInstructions.length === 0) return {
1650
+ const missingPlugins = Object.entries(generated.enabledPlugins).filter(([key]) => !(key in parsed.enabledPlugins));
1651
+ const missingMarketplaces = Object.entries(generated.extraKnownMarketplaces).filter(([key]) => !(key in parsed.extraKnownMarketplaces));
1652
+ const added = missingAllow.length + missingDeny.length + missingInstructions.length + missingPlugins.length + missingMarketplaces.length;
1653
+ if (added === 0) return {
1563
1654
  filePath,
1564
1655
  action: "skipped",
1565
1656
  description: "Already has all rules and instructions"
@@ -1567,15 +1658,16 @@ async function generateClaudeSettings(ctx) {
1567
1658
  parsed.permissions.allow = [...parsed.permissions.allow, ...missingAllow];
1568
1659
  parsed.permissions.deny = [...parsed.permissions.deny, ...missingDeny];
1569
1660
  parsed.instructions = [...parsed.instructions, ...missingInstructions];
1570
- const added = missingAllow.length + missingDeny.length + missingInstructions.length;
1571
- ctx.write(filePath, JSON.stringify(parsed, null, 2) + "\n");
1661
+ for (const [key, value] of missingPlugins) parsed.enabledPlugins[key] = value;
1662
+ for (const [key, value] of missingMarketplaces) parsed.extraKnownMarketplaces[key] = value;
1663
+ ctx.write(filePath, serializeSettings(parsed));
1572
1664
  return {
1573
1665
  filePath,
1574
1666
  action: "updated",
1575
1667
  description: `Added ${String(added)} rules/instructions`
1576
1668
  };
1577
1669
  }
1578
- ctx.write(filePath, JSON.stringify(generated, null, 2) + "\n");
1670
+ ctx.write(filePath, serializeSettings(generated));
1579
1671
  return {
1580
1672
  filePath,
1581
1673
  action: "created",
@@ -2971,7 +3063,7 @@ function mergeGitHub(dryRun) {
2971
3063
  const main = defineCommand({
2972
3064
  meta: {
2973
3065
  name: "tooling",
2974
- version: "0.7.2",
3066
+ version: "0.8.0",
2975
3067
  description: "Bootstrap and maintain standardized TypeScript project tooling"
2976
3068
  },
2977
3069
  subCommands: {
@@ -2984,7 +3076,7 @@ const main = defineCommand({
2984
3076
  "release:merge": releaseMergeCommand
2985
3077
  }
2986
3078
  });
2987
- console.log(`@bensandee/tooling v0.7.2`);
3079
+ console.log(`@bensandee/tooling v0.8.0`);
2988
3080
  runMain(main);
2989
3081
 
2990
3082
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
@@ -33,7 +33,7 @@
33
33
  "tsdown": "0.20.3",
34
34
  "typescript": "5.9.3",
35
35
  "vitest": "4.0.18",
36
- "@bensandee/config": "0.6.3"
36
+ "@bensandee/config": "0.6.5"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsdown",