@bensandee/tooling 0.24.0 → 0.25.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 +89 -30
- package/dist/index.d.mts +5 -0
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const LEGACY_TOOLS = [
|
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/utils/json.ts
|
|
24
24
|
const StringRecord = z.record(z.string(), z.string());
|
|
25
|
-
const PackageJsonSchema = z.
|
|
25
|
+
const PackageJsonSchema = z.looseObject({
|
|
26
26
|
name: z.string().optional(),
|
|
27
27
|
version: z.string().optional(),
|
|
28
28
|
private: z.boolean().optional(),
|
|
@@ -35,20 +35,21 @@ const PackageJsonSchema = z.object({
|
|
|
35
35
|
main: z.string().optional(),
|
|
36
36
|
types: z.string().optional(),
|
|
37
37
|
typings: z.string().optional(),
|
|
38
|
-
engines: StringRecord.optional()
|
|
39
|
-
}).
|
|
40
|
-
|
|
38
|
+
engines: StringRecord.optional(),
|
|
39
|
+
repository: z.union([z.string(), z.looseObject({ url: z.string() })]).optional()
|
|
40
|
+
});
|
|
41
|
+
const TsconfigSchema = z.looseObject({
|
|
41
42
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
42
43
|
include: z.array(z.string()).optional(),
|
|
43
44
|
exclude: z.array(z.string()).optional(),
|
|
44
45
|
files: z.array(z.string()).optional(),
|
|
45
|
-
references: z.array(z.
|
|
46
|
+
references: z.array(z.looseObject({ path: z.string() })).optional(),
|
|
46
47
|
compilerOptions: z.record(z.string(), z.unknown()).optional()
|
|
47
|
-
})
|
|
48
|
-
const RenovateSchema = z.
|
|
48
|
+
});
|
|
49
|
+
const RenovateSchema = z.looseObject({
|
|
49
50
|
$schema: z.string().optional(),
|
|
50
51
|
extends: z.array(z.string()).optional()
|
|
51
|
-
})
|
|
52
|
+
});
|
|
52
53
|
/** Parse a JSONC string as a tsconfig.json. Returns a typed object with `{}` fallback on failure. */
|
|
53
54
|
function parseTsconfig(raw) {
|
|
54
55
|
const result = TsconfigSchema.safeParse(parse(raw));
|
|
@@ -63,7 +64,7 @@ function parseRenovateJson(raw) {
|
|
|
63
64
|
return {};
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
const ChangesetConfigSchema = z.
|
|
67
|
+
const ChangesetConfigSchema = z.looseObject({ commit: z.union([z.boolean(), z.string()]).optional() });
|
|
67
68
|
/** Parse a JSON string as a .changeset/config.json. Returns `undefined` on failure. */
|
|
68
69
|
function parseChangesetConfig(raw) {
|
|
69
70
|
try {
|
|
@@ -108,6 +109,7 @@ function detectProject(targetDir) {
|
|
|
108
109
|
hasReleaseItConfig: exists(".release-it.json") || exists(".release-it.yaml") || exists(".release-it.toml"),
|
|
109
110
|
hasSimpleReleaseConfig: exists(".versionrc") || exists(".versionrc.json") || exists(".versionrc.js"),
|
|
110
111
|
hasChangesetsConfig: exists(".changeset/config.json"),
|
|
112
|
+
hasRepositoryField: !!readPackageJson(targetDir)?.repository,
|
|
111
113
|
legacyConfigs: detectLegacyConfigs(targetDir)
|
|
112
114
|
};
|
|
113
115
|
}
|
|
@@ -981,7 +983,7 @@ function getAddedDevDepNames(config) {
|
|
|
981
983
|
const deps = { ...ROOT_DEV_DEPS };
|
|
982
984
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
983
985
|
deps["@bensandee/config"] = "0.8.2";
|
|
984
|
-
deps["@bensandee/tooling"] = "0.
|
|
986
|
+
deps["@bensandee/tooling"] = "0.25.0";
|
|
985
987
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
986
988
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
987
989
|
addReleaseDeps(deps, config);
|
|
@@ -1006,7 +1008,7 @@ async function generatePackageJson(ctx) {
|
|
|
1006
1008
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1007
1009
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1008
1010
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.2";
|
|
1009
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1011
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.25.0";
|
|
1010
1012
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1011
1013
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
1012
1014
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
@@ -1894,7 +1896,8 @@ function buildSettings(ctx) {
|
|
|
1894
1896
|
instructions: [
|
|
1895
1897
|
"Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
1896
1898
|
"No typecasts (as/any). Use zod schemas, type guards, or narrowing instead.",
|
|
1897
|
-
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option."
|
|
1899
|
+
"Fix lint violations instead of suppressing them. Only add disable comments when suppression is genuinely the best option.",
|
|
1900
|
+
"Prefer extensionless imports; if an extension is required, use .ts over .js."
|
|
1898
1901
|
],
|
|
1899
1902
|
enabledPlugins,
|
|
1900
1903
|
extraKnownMarketplaces
|
|
@@ -2475,7 +2478,7 @@ const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json"
|
|
|
2475
2478
|
const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
|
|
2476
2479
|
const SETTINGS_PATH = ".vscode/settings.json";
|
|
2477
2480
|
const SCHEMA_GLOB = ".forgejo/workflows/*.{yml,yaml}";
|
|
2478
|
-
const VscodeSettingsSchema = z.
|
|
2481
|
+
const VscodeSettingsSchema = z.looseObject({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) });
|
|
2479
2482
|
function readSchemaFromNodeModules(targetDir) {
|
|
2480
2483
|
const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
|
|
2481
2484
|
if (!existsSync(candidate)) return void 0;
|
|
@@ -2924,6 +2927,17 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
2924
2927
|
}
|
|
2925
2928
|
sections.push("## Migration tasks");
|
|
2926
2929
|
sections.push("");
|
|
2930
|
+
if (config.releaseStrategy !== "none" && !detected.hasRepositoryField) {
|
|
2931
|
+
sections.push("### Add repository field to package.json");
|
|
2932
|
+
sections.push("");
|
|
2933
|
+
sections.push(`The release strategy \`${config.releaseStrategy}\` requires a \`repository\` field in \`package.json\` so that \`release:trigger\` can determine the correct hosting platform (Forgejo vs GitHub).`);
|
|
2934
|
+
sections.push("");
|
|
2935
|
+
sections.push("Add the appropriate repository URL to `package.json`:");
|
|
2936
|
+
sections.push("");
|
|
2937
|
+
sections.push("- For Forgejo: `\"repository\": \"https://your-forgejo-instance.com/owner/repo\"`");
|
|
2938
|
+
sections.push("- For GitHub: `\"repository\": \"https://github.com/owner/repo\"`");
|
|
2939
|
+
sections.push("");
|
|
2940
|
+
}
|
|
2927
2941
|
const legacyToRemove = detected.legacyConfigs.filter((legacy) => !(legacy.tool === "prettier" && config.formatter === "prettier"));
|
|
2928
2942
|
if (legacyToRemove.length > 0) {
|
|
2929
2943
|
sections.push("### Remove legacy tooling");
|
|
@@ -3096,6 +3110,7 @@ async function runInit(config, options = {}) {
|
|
|
3096
3110
|
if (p.isCancel(result)) return "skip";
|
|
3097
3111
|
return result;
|
|
3098
3112
|
}));
|
|
3113
|
+
if (config.releaseStrategy !== "none" && !ctx.packageJson?.repository) p.log.warn(`package.json is missing a "repository" field — required for release strategy "${config.releaseStrategy}"`);
|
|
3099
3114
|
logDetectionSummary(ctx);
|
|
3100
3115
|
s.start("Generating configuration files...");
|
|
3101
3116
|
let results;
|
|
@@ -3588,7 +3603,8 @@ async function runVersionMode(executor, config) {
|
|
|
3588
3603
|
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm install --no-frozen-lockfile", { cwd: config.cwd }));
|
|
3589
3604
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
3590
3605
|
debug(config, `PR title: ${title}`);
|
|
3591
|
-
executor.exec("git add -A", { cwd: config.cwd });
|
|
3606
|
+
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
3607
|
+
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
3592
3608
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
3593
3609
|
if (remainingChangesets.length > 0) p.log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
3594
3610
|
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
@@ -3608,7 +3624,9 @@ async function runVersionMode(executor, config) {
|
|
|
3608
3624
|
pr: "none"
|
|
3609
3625
|
};
|
|
3610
3626
|
}
|
|
3611
|
-
|
|
3627
|
+
const pushResult = executor.exec(`git push origin "HEAD:refs/heads/${BRANCH}" --force`, { cwd: config.cwd });
|
|
3628
|
+
debugExec(config, "git push", pushResult);
|
|
3629
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
3612
3630
|
const conn = {
|
|
3613
3631
|
serverUrl: config.serverUrl,
|
|
3614
3632
|
repository: config.repository,
|
|
@@ -3700,8 +3718,12 @@ async function runPublishMode(executor, config) {
|
|
|
3700
3718
|
const errors = [];
|
|
3701
3719
|
for (const tag of allTags) try {
|
|
3702
3720
|
if (!remoteSet.has(tag)) {
|
|
3703
|
-
if (executor.exec(`git tag -l ${JSON.stringify(tag)}`, { cwd: config.cwd }).stdout.trim() === "")
|
|
3704
|
-
|
|
3721
|
+
if (executor.exec(`git tag -l ${JSON.stringify(tag)}`, { cwd: config.cwd }).stdout.trim() === "") {
|
|
3722
|
+
const tagResult = executor.exec(`git tag ${JSON.stringify(tag)}`, { cwd: config.cwd });
|
|
3723
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
3724
|
+
}
|
|
3725
|
+
const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
|
|
3726
|
+
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
3705
3727
|
}
|
|
3706
3728
|
if (await findRelease(executor, conn, tag)) p.log.warn(`Release for ${tag} already exists — skipping`);
|
|
3707
3729
|
else {
|
|
@@ -3893,7 +3915,8 @@ async function triggerForgejo(conn, ref) {
|
|
|
3893
3915
|
p.log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
|
|
3894
3916
|
}
|
|
3895
3917
|
function triggerGitHub(ref) {
|
|
3896
|
-
createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
|
|
3918
|
+
const result = createRealExecutor().exec(`gh workflow run release.yml --ref ${ref}`, { cwd: process.cwd() });
|
|
3919
|
+
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
3897
3920
|
p.log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
3898
3921
|
}
|
|
3899
3922
|
//#endregion
|
|
@@ -3962,7 +3985,8 @@ function mergeGitHub(dryRun) {
|
|
|
3962
3985
|
p.log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
3963
3986
|
return;
|
|
3964
3987
|
}
|
|
3965
|
-
executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
3988
|
+
const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
3989
|
+
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
3966
3990
|
p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
3967
3991
|
}
|
|
3968
3992
|
//#endregion
|
|
@@ -4033,7 +4057,10 @@ async function runSimpleRelease(executor, config) {
|
|
|
4033
4057
|
let slidingTags = [];
|
|
4034
4058
|
if (!config.noSlidingTags && pushed) {
|
|
4035
4059
|
slidingTags = computeSlidingTags(version);
|
|
4036
|
-
for (const slidingTag of slidingTags)
|
|
4060
|
+
for (const slidingTag of slidingTags) {
|
|
4061
|
+
const slidingTagResult = executor.exec(`git tag -f ${slidingTag}`, { cwd: config.cwd });
|
|
4062
|
+
if (slidingTagResult.exitCode !== 0) throw new FatalError(`Failed to create sliding tag ${slidingTag}: ${slidingTagResult.stderr || slidingTagResult.stdout}`);
|
|
4063
|
+
}
|
|
4037
4064
|
const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
|
|
4038
4065
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4039
4066
|
if (forcePushResult.exitCode !== 0) p.log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
@@ -4288,6 +4315,22 @@ const publishDockerCommand = defineCommand({
|
|
|
4288
4315
|
});
|
|
4289
4316
|
//#endregion
|
|
4290
4317
|
//#region src/commands/docker-build.ts
|
|
4318
|
+
/**
|
|
4319
|
+
* Detect if cwd is inside a packages/ subdirectory.
|
|
4320
|
+
* If so, return the project root and the relative package dir.
|
|
4321
|
+
* Otherwise return cwd as-is with no packageDir.
|
|
4322
|
+
*/
|
|
4323
|
+
function detectSubpackage(cwd) {
|
|
4324
|
+
const parent = path.dirname(cwd);
|
|
4325
|
+
if (path.basename(parent) === "packages" && existsSync(path.join(parent, "..", "package.json"))) return {
|
|
4326
|
+
root: path.dirname(parent),
|
|
4327
|
+
packageDir: `packages/${path.basename(cwd)}`
|
|
4328
|
+
};
|
|
4329
|
+
return {
|
|
4330
|
+
root: cwd,
|
|
4331
|
+
packageDir: void 0
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4291
4334
|
const dockerBuildCommand = defineCommand({
|
|
4292
4335
|
meta: {
|
|
4293
4336
|
name: "docker:build",
|
|
@@ -4312,9 +4355,16 @@ const dockerBuildCommand = defineCommand({
|
|
|
4312
4355
|
const executor = createRealExecutor();
|
|
4313
4356
|
const rawExtra = args._ ?? [];
|
|
4314
4357
|
const extraArgs = Array.isArray(rawExtra) ? rawExtra.map(String) : [String(rawExtra)];
|
|
4358
|
+
let cwd = process.cwd();
|
|
4359
|
+
let packageDir = args.package;
|
|
4360
|
+
if (!packageDir) {
|
|
4361
|
+
const detected = detectSubpackage(cwd);
|
|
4362
|
+
cwd = detected.root;
|
|
4363
|
+
packageDir = detected.packageDir;
|
|
4364
|
+
}
|
|
4315
4365
|
runDockerBuild(executor, {
|
|
4316
|
-
cwd
|
|
4317
|
-
packageDir
|
|
4366
|
+
cwd,
|
|
4367
|
+
packageDir,
|
|
4318
4368
|
verbose: args.verbose === true,
|
|
4319
4369
|
extraArgs: extraArgs.filter((a) => a.length > 0)
|
|
4320
4370
|
});
|
|
@@ -4330,16 +4380,16 @@ const COMPOSE_FILE_CANDIDATES = [
|
|
|
4330
4380
|
"compose.yml"
|
|
4331
4381
|
];
|
|
4332
4382
|
/** Zod schema for the subset of compose YAML we care about. */
|
|
4333
|
-
const ComposePortSchema = z.union([z.string(), z.
|
|
4383
|
+
const ComposePortSchema = z.union([z.string(), z.looseObject({
|
|
4334
4384
|
published: z.union([z.string(), z.number()]),
|
|
4335
4385
|
target: z.union([z.string(), z.number()]).optional()
|
|
4336
|
-
})
|
|
4337
|
-
const ComposeServiceSchema = z.
|
|
4386
|
+
})]);
|
|
4387
|
+
const ComposeServiceSchema = z.looseObject({
|
|
4338
4388
|
image: z.string().optional(),
|
|
4339
4389
|
ports: z.array(ComposePortSchema).optional(),
|
|
4340
4390
|
healthcheck: z.unknown().optional()
|
|
4341
|
-
})
|
|
4342
|
-
const ComposeFileSchema = z.
|
|
4391
|
+
});
|
|
4392
|
+
const ComposeFileSchema = z.looseObject({ services: z.record(z.string(), ComposeServiceSchema).optional() });
|
|
4343
4393
|
/** Directories to scan for compose files, in priority order. */
|
|
4344
4394
|
const COMPOSE_DIR_CANDIDATES = [".", "docker"];
|
|
4345
4395
|
/** Detect which compose files exist at conventional paths.
|
|
@@ -4418,11 +4468,20 @@ function parseComposeServices(cwd, composeFiles) {
|
|
|
4418
4468
|
}
|
|
4419
4469
|
return [...serviceMap.values()];
|
|
4420
4470
|
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Strip Docker Compose variable substitutions from an image string.
|
|
4473
|
+
* Handles `${VAR:-default}`, `${VAR-default}`, `${VAR:+alt}`, `${VAR+alt}`,
|
|
4474
|
+
* `${VAR:?err}`, `${VAR?err}`, and plain `${VAR}`.
|
|
4475
|
+
* Nested braces are not supported.
|
|
4476
|
+
*/
|
|
4477
|
+
function stripComposeVariables(image) {
|
|
4478
|
+
return image.replace(/\$\{[^}]*\}/g, "");
|
|
4479
|
+
}
|
|
4421
4480
|
/** Extract deduplicated bare image names (without tags) from parsed services. */
|
|
4422
4481
|
function extractComposeImageNames(services) {
|
|
4423
4482
|
const names = /* @__PURE__ */ new Set();
|
|
4424
4483
|
for (const service of services) if (service.image) {
|
|
4425
|
-
const bare = service.image.split(":")[0];
|
|
4484
|
+
const bare = stripComposeVariables(service.image).split(":")[0];
|
|
4426
4485
|
if (bare) names.add(bare);
|
|
4427
4486
|
}
|
|
4428
4487
|
return [...names];
|
|
@@ -4607,7 +4666,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
4607
4666
|
const main = defineCommand({
|
|
4608
4667
|
meta: {
|
|
4609
4668
|
name: "tooling",
|
|
4610
|
-
version: "0.
|
|
4669
|
+
version: "0.25.0",
|
|
4611
4670
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
4612
4671
|
},
|
|
4613
4672
|
subCommands: {
|
|
@@ -4623,7 +4682,7 @@ const main = defineCommand({
|
|
|
4623
4682
|
"docker:check": dockerCheckCommand
|
|
4624
4683
|
}
|
|
4625
4684
|
});
|
|
4626
|
-
console.log(`@bensandee/tooling v0.
|
|
4685
|
+
console.log(`@bensandee/tooling v0.25.0`);
|
|
4627
4686
|
async function run() {
|
|
4628
4687
|
await runMain(main);
|
|
4629
4688
|
process.exit(process.exitCode ?? 0);
|
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,9 @@ declare const PackageJsonSchema: z.ZodObject<{
|
|
|
15
15
|
types: z.ZodOptional<z.ZodString>;
|
|
16
16
|
typings: z.ZodOptional<z.ZodString>;
|
|
17
17
|
engines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
18
|
+
repository: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
19
|
+
url: z.ZodString;
|
|
20
|
+
}, z.core.$loose>]>>;
|
|
18
21
|
}, z.core.$loose>;
|
|
19
22
|
type PackageJson = z.infer<typeof PackageJsonSchema>;
|
|
20
23
|
//#endregion
|
|
@@ -91,6 +94,8 @@ interface DetectedProjectState {
|
|
|
91
94
|
hasReleaseItConfig: boolean;
|
|
92
95
|
hasSimpleReleaseConfig: boolean;
|
|
93
96
|
hasChangesetsConfig: boolean;
|
|
97
|
+
/** Whether package.json has a repository field (needed for release workflows) */
|
|
98
|
+
hasRepositoryField: boolean;
|
|
94
99
|
/** Legacy tooling configs found */
|
|
95
100
|
legacyConfigs: LegacyConfig[];
|
|
96
101
|
}
|