@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.
- package/dist/bin.mjs +158 -66
- 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
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
913
|
+
function generateMonorepoRootTsconfig(ctx) {
|
|
885
914
|
const filePath = "tsconfig.json";
|
|
886
|
-
if (
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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:
|
|
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
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
1538
|
-
"Bash(
|
|
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
|
-
|
|
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
|
|
1571
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
+
"@bensandee/config": "0.6.5"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsdown",
|