@bensandee/tooling 0.53.1 → 0.54.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/dist/bin.mjs +92 -33
- package/package.json +3 -3
- package/tooling.schema.json +4 -6
package/dist/bin.mjs
CHANGED
|
@@ -623,23 +623,44 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
623
623
|
]).optional().meta({ description: "Project type (determines tsconfig base)" }),
|
|
624
624
|
detectPackageTypes: z.boolean().optional().meta({ description: "Auto-detect project types for monorepo packages" }),
|
|
625
625
|
docker: z.record(z.string(), z.object({
|
|
626
|
-
dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
|
|
627
|
-
context: z.string().
|
|
626
|
+
dockerfile: z.string().optional().meta({ description: "Path to Dockerfile relative to package (falls back to convention when omitted)" }),
|
|
627
|
+
context: z.string().optional().meta({ description: "Docker build context relative to package (falls back to convention, default '.')" }),
|
|
628
628
|
buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." })
|
|
629
|
-
})).optional().meta({ description: "Docker package overrides (package name → config)" }),
|
|
629
|
+
})).optional().meta({ description: "Docker package overrides (package name → partial config; any field may be omitted to fall back to convention)" }),
|
|
630
630
|
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
|
|
631
631
|
registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
|
|
632
632
|
registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" }),
|
|
633
633
|
postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)" }),
|
|
634
634
|
postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" })
|
|
635
635
|
});
|
|
636
|
+
/** Format a Zod issue as a human-readable bullet: location + plain-English explanation. */
|
|
637
|
+
function formatIssue(issue) {
|
|
638
|
+
const loc = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
639
|
+
if (issue.code === "invalid_type") {
|
|
640
|
+
if (issue.input === void 0) return ` ${loc}: missing (expected ${issue.expected})`;
|
|
641
|
+
return ` ${loc}: expected ${issue.expected}, got ${typeof issue.input}`;
|
|
642
|
+
}
|
|
643
|
+
if (issue.code === "unrecognized_keys") return ` ${loc}: ${issue.keys.length > 1 ? "unknown fields" : "unknown field"} ${issue.keys.map((k) => `"${k}"`).join(", ")}`;
|
|
644
|
+
return ` ${loc}: ${issue.message}`;
|
|
645
|
+
}
|
|
636
646
|
/** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
|
|
637
647
|
function loadToolingConfig(targetDir) {
|
|
638
648
|
const fullPath = path.join(targetDir, CONFIG_FILE);
|
|
639
649
|
if (!existsSync(fullPath)) return void 0;
|
|
640
650
|
const raw = readFileSync(fullPath, "utf-8");
|
|
641
|
-
const
|
|
642
|
-
|
|
651
|
+
const displayPath = path.relative(process.cwd(), fullPath) || fullPath;
|
|
652
|
+
let parsed;
|
|
653
|
+
try {
|
|
654
|
+
parsed = JSON.parse(raw);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
throw new FatalError(`Could not parse ${displayPath} as JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
657
|
+
}
|
|
658
|
+
const result = ToolingConfigSchema.safeParse(parsed);
|
|
659
|
+
if (!result.success) {
|
|
660
|
+
const issues = result.error.issues.map(formatIssue).join("\n");
|
|
661
|
+
const count = result.error.issues.length;
|
|
662
|
+
throw new FatalError(`Invalid .tooling.json (${displayPath}) — ${count === 1 ? "1 problem" : `${count} problems`}:\n${issues}`);
|
|
663
|
+
}
|
|
643
664
|
return result.data;
|
|
644
665
|
}
|
|
645
666
|
/** Config fields that can be overridden in .tooling.json. */
|
|
@@ -658,30 +679,42 @@ const OVERRIDE_KEYS = [
|
|
|
658
679
|
];
|
|
659
680
|
/** Keys that have no effect for monorepos (generators ignore them). */
|
|
660
681
|
const MONOREPO_IGNORED_KEYS = new Set(["setupVitest", "projectType"]);
|
|
682
|
+
const SCHEMA_REF = "node_modules/@bensandee/tooling/tooling.schema.json";
|
|
683
|
+
const RawJsonObjectSchema = z.record(z.string(), z.unknown());
|
|
684
|
+
/** Parse the raw .tooling.json contents into a plain object, or {} if invalid/missing. */
|
|
685
|
+
function parseExistingConfig(raw) {
|
|
686
|
+
if (raw === void 0) return {};
|
|
687
|
+
try {
|
|
688
|
+
const result = RawJsonObjectSchema.safeParse(JSON.parse(raw));
|
|
689
|
+
if (result.success) return result.data;
|
|
690
|
+
} catch (_error) {}
|
|
691
|
+
return {};
|
|
692
|
+
}
|
|
661
693
|
/**
|
|
662
694
|
* Save only the fields that differ from detected defaults to .tooling.json.
|
|
663
695
|
* A fully conventional project produces `{}` (or a minimal set of overrides).
|
|
664
696
|
* Keys that have no effect for the current structure are omitted.
|
|
697
|
+
*
|
|
698
|
+
* Non-override fields (e.g. `docker`, `dockerCheck`) are copied verbatim from the
|
|
699
|
+
* raw JSON — they are not re-parsed through Zod, so schema defaults like
|
|
700
|
+
* `docker.<pkg>.context: "."` are never injected into the user's file.
|
|
665
701
|
*/
|
|
666
702
|
function saveToolingConfig(ctx, config) {
|
|
667
703
|
const defaults = computeDefaults(config.targetDir);
|
|
668
704
|
const isMonorepo = config.structure === "monorepo";
|
|
669
705
|
const existing = ctx.exists(CONFIG_FILE) ? ctx.read(CONFIG_FILE) : void 0;
|
|
706
|
+
const existingJson = parseExistingConfig(existing);
|
|
670
707
|
const overrideKeySet = new Set(OVERRIDE_KEYS);
|
|
671
|
-
const
|
|
672
|
-
if (existing) {
|
|
673
|
-
const result = ToolingConfigSchema.safeParse(JSON.parse(existing));
|
|
674
|
-
if (result.success) {
|
|
675
|
-
for (const [key, value] of Object.entries(result.data)) if (key !== "$schema" && !overrideKeySet.has(key)) preserved[key] = value;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
const overrides = { $schema: "node_modules/@bensandee/tooling/tooling.schema.json" };
|
|
708
|
+
const result = { $schema: SCHEMA_REF };
|
|
679
709
|
for (const key of OVERRIDE_KEYS) {
|
|
680
710
|
if (isMonorepo && MONOREPO_IGNORED_KEYS.has(key)) continue;
|
|
681
|
-
if (config[key] !== defaults[key])
|
|
711
|
+
if (config[key] !== defaults[key]) result[key] = config[key];
|
|
682
712
|
}
|
|
683
|
-
Object.
|
|
684
|
-
|
|
713
|
+
for (const [key, value] of Object.entries(existingJson)) {
|
|
714
|
+
if (key === "$schema" || overrideKeySet.has(key)) continue;
|
|
715
|
+
result[key] = value;
|
|
716
|
+
}
|
|
717
|
+
const content = JSON.stringify(result, null, 2) + "\n";
|
|
685
718
|
if (existing !== void 0 && contentEqual(CONFIG_FILE, existing, content)) return {
|
|
686
719
|
filePath: CONFIG_FILE,
|
|
687
720
|
action: "skipped",
|
|
@@ -715,8 +748,8 @@ function mergeWithSavedConfig(detected, saved) {
|
|
|
715
748
|
//#endregion
|
|
716
749
|
//#region src/release/docker.ts
|
|
717
750
|
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
718
|
-
dockerfile: z.string(),
|
|
719
|
-
context: z.string().
|
|
751
|
+
dockerfile: z.string().optional(),
|
|
752
|
+
context: z.string().optional(),
|
|
720
753
|
buildArgs: z.record(z.string(), z.string()).optional()
|
|
721
754
|
}));
|
|
722
755
|
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
@@ -737,6 +770,21 @@ function loadDockerMap(executor, cwd) {
|
|
|
737
770
|
return {};
|
|
738
771
|
}
|
|
739
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Merge a convention-discovered config with a user override. Missing override fields
|
|
775
|
+
* fall back to convention. Returns undefined if no dockerfile can be resolved from
|
|
776
|
+
* either source.
|
|
777
|
+
*/
|
|
778
|
+
function mergeDockerConfig(convention, override) {
|
|
779
|
+
if (!override) return convention;
|
|
780
|
+
const dockerfile = override.dockerfile ?? convention?.dockerfile;
|
|
781
|
+
if (!dockerfile) return void 0;
|
|
782
|
+
return {
|
|
783
|
+
dockerfile,
|
|
784
|
+
context: override.context ?? convention?.context ?? ".",
|
|
785
|
+
buildArgs: override.buildArgs ?? convention?.buildArgs
|
|
786
|
+
};
|
|
787
|
+
}
|
|
740
788
|
/** Read name and version from a package's package.json. */
|
|
741
789
|
function readPackageInfo(executor, packageJsonPath) {
|
|
742
790
|
const raw = executor.readFile(packageJsonPath);
|
|
@@ -821,16 +869,23 @@ function detectDockerPackages(executor, cwd, repoName) {
|
|
|
821
869
|
if (packageDirs.length > 0) {
|
|
822
870
|
for (const dir of packageDirs) {
|
|
823
871
|
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
824
|
-
const
|
|
872
|
+
const override = overrides[dir];
|
|
873
|
+
const docker = mergeDockerConfig(convention, override);
|
|
825
874
|
if (docker) {
|
|
826
875
|
packages.push(createDockerPackage(executor, cwd, dir, repoName, docker));
|
|
827
876
|
seen.add(dir);
|
|
828
877
|
}
|
|
829
878
|
}
|
|
830
|
-
for (const [dir,
|
|
879
|
+
for (const [dir, override] of Object.entries(overrides)) {
|
|
880
|
+
if (seen.has(dir)) continue;
|
|
881
|
+
const docker = mergeDockerConfig(void 0, override);
|
|
882
|
+
if (!docker) throw new FatalError(`Docker override for "${dir}" has no dockerfile and no convention Dockerfile was found`);
|
|
883
|
+
packages.push(createDockerPackage(executor, cwd, dir, repoName, docker));
|
|
884
|
+
}
|
|
831
885
|
} else {
|
|
832
886
|
const convention = findRootDockerfile(executor, cwd);
|
|
833
|
-
const
|
|
887
|
+
const override = overrides["."];
|
|
888
|
+
const docker = mergeDockerConfig(convention, override);
|
|
834
889
|
if (docker) packages.push(createDockerPackage(executor, cwd, ".", repoName, docker));
|
|
835
890
|
}
|
|
836
891
|
return packages;
|
|
@@ -842,7 +897,8 @@ function detectDockerPackages(executor, cwd, repoName) {
|
|
|
842
897
|
function readSinglePackageDocker(executor, cwd, packageDir, repoName) {
|
|
843
898
|
const dir = path.basename(path.resolve(cwd, packageDir));
|
|
844
899
|
const convention = findConventionDockerfile(executor, cwd, dir);
|
|
845
|
-
const
|
|
900
|
+
const override = loadDockerMap(executor, cwd)[dir];
|
|
901
|
+
const docker = mergeDockerConfig(convention, override);
|
|
846
902
|
if (!docker) throw new FatalError(`No Dockerfile found for package "${dir}" (checked convention paths and .tooling.json)`);
|
|
847
903
|
return createDockerPackage(executor, cwd, dir, repoName, docker);
|
|
848
904
|
}
|
|
@@ -896,36 +952,39 @@ function interpolate(value, env, onMissing) {
|
|
|
896
952
|
});
|
|
897
953
|
}
|
|
898
954
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
955
|
+
* Resolve a package's configured buildArgs map into `K=V` pairs, expanding
|
|
956
|
+
* env-var references in values. Warns once per unset variable per build.
|
|
901
957
|
*/
|
|
902
958
|
function resolveBuildArgs(pkg, env) {
|
|
903
959
|
const entries = Object.entries(pkg.docker.buildArgs ?? {});
|
|
904
960
|
if (entries.length === 0) return [];
|
|
905
961
|
const warned = /* @__PURE__ */ new Set();
|
|
906
|
-
const
|
|
962
|
+
const pairs = [];
|
|
907
963
|
for (const [k, rawValue] of entries) {
|
|
908
964
|
const resolved = interpolate(rawValue, env, (missing) => {
|
|
909
965
|
if (warned.has(missing)) return;
|
|
910
966
|
warned.add(missing);
|
|
911
967
|
log.warn(`docker build arg "${k}" for ${pkg.dir}: env var ${missing} is not set; using empty string`);
|
|
912
968
|
});
|
|
913
|
-
|
|
969
|
+
pairs.push(`${k}=${resolved}`);
|
|
914
970
|
}
|
|
915
|
-
return
|
|
971
|
+
return pairs;
|
|
916
972
|
}
|
|
917
973
|
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
918
974
|
function buildImage(executor, pkg, cwd, extraArgs) {
|
|
919
975
|
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
920
976
|
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
921
|
-
const
|
|
977
|
+
const configuredPairs = resolveBuildArgs(pkg, process.env);
|
|
978
|
+
log.info(` build args: ${configuredPairs.length > 0 ? configuredPairs.join(" ") : "(none)"}`);
|
|
979
|
+
log.info(` extra args: ${extraArgs.length > 0 ? extraArgs.join(" ") : "(none)"}`);
|
|
980
|
+
const buildArgFlags = configuredPairs.flatMap((pair) => ["--build-arg", pair]);
|
|
922
981
|
executor.execInherit("docker", [
|
|
923
982
|
"build",
|
|
924
983
|
"-f",
|
|
925
984
|
dockerfilePath,
|
|
926
985
|
"-t",
|
|
927
986
|
`${pkg.imageName}:latest`,
|
|
928
|
-
...
|
|
987
|
+
...buildArgFlags,
|
|
929
988
|
...extraArgs,
|
|
930
989
|
contextPath
|
|
931
990
|
]);
|
|
@@ -1674,7 +1733,7 @@ async function generatePackageJson(ctx) {
|
|
|
1674
1733
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1675
1734
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1676
1735
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
|
|
1677
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1736
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.54.1";
|
|
1678
1737
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1679
1738
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1680
1739
|
"@changesets/cli": "2.30.0",
|
|
@@ -3450,7 +3509,7 @@ async function runGenerators(ctx) {
|
|
|
3450
3509
|
function generateMigratePrompt(results, config, detected) {
|
|
3451
3510
|
const sections = [];
|
|
3452
3511
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3453
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3512
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.54.1 repo:sync\` on ${timestamp}_\n`);
|
|
3454
3513
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3455
3514
|
if (changed.length > 0) {
|
|
3456
3515
|
const items = changed.map((r) => {
|
|
@@ -6753,7 +6812,7 @@ async function sendWebhookWithLogging(url, secret, payload) {
|
|
|
6753
6812
|
const main = defineCommand({
|
|
6754
6813
|
meta: {
|
|
6755
6814
|
name: "bst",
|
|
6756
|
-
version: "0.
|
|
6815
|
+
version: "0.54.1",
|
|
6757
6816
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
6758
6817
|
},
|
|
6759
6818
|
subCommands: {
|
|
@@ -6821,7 +6880,7 @@ const main = defineCommand({
|
|
|
6821
6880
|
})
|
|
6822
6881
|
}
|
|
6823
6882
|
});
|
|
6824
|
-
console.log(`@bensandee/tooling v0.
|
|
6883
|
+
console.log(`@bensandee/tooling v0.54.1`);
|
|
6825
6884
|
async function run() {
|
|
6826
6885
|
await runMain(main);
|
|
6827
6886
|
process.exit(process.exitCode ?? 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.1",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"prettier": "3.8.3",
|
|
55
55
|
"release-it": "20.0.0",
|
|
56
56
|
"tsdown": "0.21.9",
|
|
57
|
-
"@bensandee/
|
|
58
|
-
"@bensandee/
|
|
57
|
+
"@bensandee/config": "0.10.2",
|
|
58
|
+
"@bensandee/claude-hooks": "0.7.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|
package/tooling.schema.json
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"type": "boolean"
|
|
59
59
|
},
|
|
60
60
|
"docker": {
|
|
61
|
-
"description": "Docker package overrides (package name → config)",
|
|
61
|
+
"description": "Docker package overrides (package name → partial config; any field may be omitted to fall back to convention)",
|
|
62
62
|
"type": "object",
|
|
63
63
|
"propertyNames": {
|
|
64
64
|
"type": "string"
|
|
@@ -67,12 +67,11 @@
|
|
|
67
67
|
"type": "object",
|
|
68
68
|
"properties": {
|
|
69
69
|
"dockerfile": {
|
|
70
|
-
"
|
|
71
|
-
"
|
|
70
|
+
"description": "Path to Dockerfile relative to package (falls back to convention when omitted)",
|
|
71
|
+
"type": "string"
|
|
72
72
|
},
|
|
73
73
|
"context": {
|
|
74
|
-
"
|
|
75
|
-
"description": "Docker build context relative to package",
|
|
74
|
+
"description": "Docker build context relative to package (falls back to convention, default '.')",
|
|
76
75
|
"type": "string"
|
|
77
76
|
},
|
|
78
77
|
"buildArgs": {
|
|
@@ -86,7 +85,6 @@
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
},
|
|
89
|
-
"required": ["dockerfile", "context"],
|
|
90
88
|
"additionalProperties": false
|
|
91
89
|
}
|
|
92
90
|
},
|