feat(cli): add alchemy and improve cli tooling and structure (#520)

This commit is contained in:
Aman Varshney
2025-08-20 23:43:58 +05:30
committed by GitHub
parent c5430ae4fd
commit 5788876c47
152 changed files with 5804 additions and 2264 deletions

94
scripts/bump-version.ts Normal file
View File

@@ -0,0 +1,94 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { select, text } from "@clack/prompts";
import { $ } from "bun";
const CLI_PACKAGE_JSON_PATH = join(process.cwd(), "apps/cli/package.json");
async function main(): Promise<void> {
const args = process.argv.slice(2);
const isDryRun = args.includes("--dry-run");
let versionInput = args.find((arg) => !arg.startsWith("--"));
if (!versionInput) {
const bumpType = await select({
message: "What type of release do you want to create?",
options: [
{ value: "patch", label: "Patch (bug fixes) - 2.33.9 → 2.33.10" },
{ value: "minor", label: "Minor (new features) - 2.33.9 → 2.34.0" },
{ value: "major", label: "Major (breaking changes) - 2.33.9 → 3.0.0" },
{ value: "custom", label: "Custom version" },
],
});
if (bumpType === "custom") {
const customVersion = await text({
message: "Enter the version (e.g., 2.34.0):",
placeholder: "2.34.0",
});
versionInput =
typeof customVersion === "string" ? customVersion : undefined;
} else if (typeof bumpType === "string") {
versionInput = bumpType;
}
if (!versionInput) {
console.log("❌ No version selected");
process.exit(1);
}
}
const packageJson = JSON.parse(
await readFile(CLI_PACKAGE_JSON_PATH, "utf-8"),
);
const currentVersion = packageJson.version;
console.log(`Current version: ${currentVersion}`);
let newVersion = "";
if (["major", "minor", "patch"].includes(versionInput)) {
const [major, minor, patch] = currentVersion.split(".").map(Number);
switch (versionInput) {
case "major":
newVersion = `${major + 1}.0.0`;
break;
case "minor":
newVersion = `${major}.${minor + 1}.0`;
break;
case "patch":
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}
console.log(`Bumping ${versionInput}: ${currentVersion}${newVersion}`);
} else {
if (!/^\d+\.\d+\.\d+$/.test(versionInput)) {
console.error("Version must be x.y.z format");
process.exit(1);
}
newVersion = versionInput;
}
if (isDryRun) {
console.log(`✅ Would release v${newVersion} (dry run)`);
return;
}
packageJson.version = newVersion;
await writeFile(
CLI_PACKAGE_JSON_PATH,
`${JSON.stringify(packageJson, null, 2)}\n`,
);
await $`bun install`;
await $`bun run build:cli`;
await $`git add apps/cli/package.json bun.lock`;
await $`git commit -m "chore(release): ${newVersion}"`;
await $`git tag v${newVersion}`;
await $`git push origin v${newVersion}`;
console.log(`✅ Released v${newVersion}`);
}
main().catch(console.error);

239
scripts/canary-release.ts Normal file
View File

@@ -0,0 +1,239 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { confirm, isCancel, multiselect, spinner } from "@clack/prompts";
import { $ } from "bun";
const CLI_PACKAGE_JSON_PATH = join(process.cwd(), "apps/cli/package.json");
async function main(): Promise<void> {
const args = process.argv.slice(2);
const isDryRun = args.includes("--dry-run");
const deprecateOld =
args.includes("--deprecate-old") || args.includes("--prune-old");
const autoYes = args.includes("--yes");
const packageJson = JSON.parse(
await readFile(CLI_PACKAGE_JSON_PATH, "utf-8"),
);
const currentVersion = packageJson.version;
const packageName: string = packageJson.name || "create-better-t-stack";
const strictSemver = /^\d+\.\d+\.\d+$/;
let baseVersion = currentVersion;
if (strictSemver.test(currentVersion)) {
baseVersion = currentVersion;
} else {
const m = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
baseVersion = m ? m[0] : currentVersion;
}
console.log(`Current version: ${currentVersion}`);
if (baseVersion !== currentVersion) {
console.log(`Sanitized base version: ${baseVersion}`);
}
const commitHash = (await $`git rev-parse --short HEAD`.text()).trim();
const canaryVersion = `${baseVersion}-canary.${commitHash}`;
console.log(`Canary version: ${canaryVersion}`);
console.log(`Commit: ${commitHash}`);
if (isDryRun) {
console.log(`✅ Would release canary v${canaryVersion} (dry run)`);
return;
}
if (deprecateOld) {
try {
const versionsJson =
await $`npm view ${packageName} versions --json`.text();
const versions = JSON.parse(versionsJson) as string[];
const isCanary = (v: string) =>
v.includes("-canary.") || v.includes("+canary.");
const canaryVersions = (Array.isArray(versions) ? versions : []).filter(
isCanary,
);
if (!canaryVersions.length) {
console.log(" No canary versions found to deprecate.");
return;
}
const nonDeprecated: string[] = [];
for (const v of canaryVersions) {
try {
const deprecatedJson =
await $`npm view ${`${packageName}@${v}`} deprecated --json`.text();
const deprecatedMsg = deprecatedJson
? JSON.parse(deprecatedJson)
: null;
if (
!deprecatedMsg ||
(typeof deprecatedMsg === "string" && deprecatedMsg.length === 0)
) {
nonDeprecated.push(v);
}
} catch {
nonDeprecated.push(v);
}
}
if (autoYes) {
const depSpin = spinner();
depSpin.start(
`Deprecating ${nonDeprecated.length} canary version(s)...`,
);
let count = 0;
for (const v of nonDeprecated) {
try {
await $`npm deprecate -f ${`${packageName}@${v}`} "Deprecated canary; use ${packageName}@canary (currently ${canaryVersion})"`;
count++;
} catch {}
}
depSpin.stop(`Deprecated ${count} version(s).`);
return;
}
const selected = (await multiselect({
message: "Select canary versions to deprecate:",
options: nonDeprecated
.sort()
.reverse()
.map((v) => ({ value: v, label: v })),
})) as unknown as string[] | symbol;
if (
isCancel(selected) ||
!Array.isArray(selected) ||
selected.length === 0
) {
console.log("❌ No selections made. Aborting.");
return;
}
const depSpin = spinner();
depSpin.start(`Deprecating ${selected.length} canary version(s)...`);
let count = 0;
for (const v of selected) {
try {
await $`npm deprecate -f ${`${packageName}@${v}`} "Deprecated canary; use ${packageName}@canary (currently ${canaryVersion})"`;
count++;
} catch {}
}
depSpin.stop(`Deprecated ${count} version(s).`);
return;
} catch (err) {
console.error("❌ Failed to fetch versions from npm:", err);
return;
}
}
try {
const versionsJson =
await $`npm view ${packageName} versions --json`.text();
const versions = JSON.parse(versionsJson) as string[];
if (Array.isArray(versions) && versions.includes(canaryVersion)) {
if (deprecateOld) {
const depSpin = spinner();
depSpin.start("Deprecating older canary versions (no publish)...");
try {
const isCanary = (v: string) =>
v.includes("-canary.") || v.includes("+canary.");
let count = 0;
for (const v of versions) {
if (!isCanary(v) || v === canaryVersion) continue;
await $`npm deprecate -f ${`${packageName}@${v}`} "Deprecated canary; use ${packageName}@canary (currently ${canaryVersion})"`;
count++;
}
depSpin.stop(`Deprecated ${count} older canary versions`);
} catch (err) {
depSpin.stop("Failed to deprecate older canaries");
console.warn("⚠️ Failed to deprecate older canaries:", err);
}
console.error(
`${packageName}@${canaryVersion} is already published on npm. Skipped publish after deprecating older canaries.`,
);
return;
}
console.error(
`${packageName}@${canaryVersion} is already published on npm. Make a new commit (or clean your workspace) and try again.`,
);
return;
}
} catch {}
if (!autoYes) {
const proceed = await confirm({
message: `Publish ${packageName}@${canaryVersion} with dist-tag "canary"${deprecateOld ? ", then deprecate older canaries" : ""}?`,
});
if (isCancel(proceed) || proceed === false) {
console.log("❌ Canceled by user.");
return;
}
}
const originalPackageJsonString = await readFile(
CLI_PACKAGE_JSON_PATH,
"utf-8",
);
let restored = false;
try {
packageJson.version = canaryVersion;
await writeFile(
CLI_PACKAGE_JSON_PATH,
`${JSON.stringify(packageJson, null, 2)}\n`,
);
const buildSpin = spinner();
buildSpin.start("Building CLI...");
try {
await $`bun run build:cli`;
buildSpin.stop("Build complete");
} catch (err) {
buildSpin.stop("Build failed");
throw err;
}
const pubSpin = spinner();
pubSpin.start(`Publishing ${packageName}@${canaryVersion} (canary)...`);
try {
await $`cd apps/cli && bun publish --access public --tag canary`;
pubSpin.stop("Publish complete");
} catch (err) {
pubSpin.stop("Publish failed");
throw err;
}
if (deprecateOld) {
console.log("🔎 Cleaning up older canary versions (deprecating)...");
try {
const versionsJson =
await $`npm view ${packageName} versions --json`.text();
const versions = JSON.parse(versionsJson) as string[];
const isCanary = (v: string) =>
v.includes("-canary.") || v.includes("+canary.");
for (const v of versions) {
if (!isCanary(v) || v === canaryVersion) continue;
console.log(`➡️ Deprecating ${packageName}@${v}`);
await $`npm deprecate -f ${`${packageName}@${v}`} "Deprecated canary; use ${packageName}@canary (currently ${canaryVersion})"`;
}
console.log("🧹 Older canaries deprecated.");
} catch (err) {
console.warn("⚠️ Failed to deprecate older canaries:", err);
}
}
await writeFile(CLI_PACKAGE_JSON_PATH, originalPackageJsonString);
restored = true;
console.log(`✅ Published canary v${canaryVersion}`);
console.log(
`📦 NPM: https://www.npmjs.com/package/${packageName}/v/${canaryVersion}`,
);
} finally {
if (!restored) {
await writeFile(CLI_PACKAGE_JSON_PATH, originalPackageJsonString);
}
}
}
main().catch(console.error);

38
scripts/release.ts Normal file
View File

@@ -0,0 +1,38 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { $ } from "bun";
import { generate } from "changelogithub";
import config from "../changelogithub.config";
async function main(): Promise<void> {
const tag = process.env.GITHUB_REF?.replace("refs/tags/", "");
if (!tag) {
console.error("No git tag found");
process.exit(1);
}
console.log(`Generating changelog for ${tag}`);
const changelog = await generate({
to: tag,
...config,
});
const changelogPath = join(process.cwd(), "CHANGELOG.md");
let existingContent = "";
try {
existingContent = await readFile(changelogPath, "utf-8");
} catch {}
const newChangelog = `## ${tag}\n\n${changelog.md}\n\n---\n\n${existingContent}`;
await writeFile(changelogPath, newChangelog);
await $`git add CHANGELOG.md`;
await $`git commit -m "chore: update changelog for ${tag}"`;
await $`git push`;
console.log(`✅ Generated changelog for ${tag}`);
}
main().catch(console.error);