mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
94
scripts/bump-version.ts
Normal file
94
scripts/bump-version.ts
Normal 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
239
scripts/canary-release.ts
Normal 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
38
scripts/release.ts
Normal 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);
|
||||
Reference in New Issue
Block a user