From fd86d51d8ecada84d2f28ccac5dcf2c34c767673 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 12 Jun 2025 12:27:51 +0530 Subject: [PATCH] add sponsors, builder, docs command in cli --- .changeset/cold-cups-fetch.md | 5 ++ apps/cli/src/index.ts | 37 ++++++++++ apps/cli/src/utils/open-url.ts | 25 +++++++ apps/cli/src/utils/sponsors.ts | 68 +++++++++++++++++++ .../(home)/_components/sponsors-section.tsx | 2 +- 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 .changeset/cold-cups-fetch.md create mode 100644 apps/cli/src/utils/open-url.ts create mode 100644 apps/cli/src/utils/sponsors.ts diff --git a/.changeset/cold-cups-fetch.md b/.changeset/cold-cups-fetch.md new file mode 100644 index 0000000..9477b8b --- /dev/null +++ b/.changeset/cold-cups-fetch.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +add sponsors, builder, docs command diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index dcdf577..1eb09de 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -34,7 +34,9 @@ import { trackProjectCreation } from "./utils/analytics"; import { displayConfig } from "./utils/display-config"; import { generateReproducibleCommand } from "./utils/generate-reproducible-command"; import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; +import { openUrl } from "./utils/open-url"; import { renderTitle } from "./utils/render-title"; +import { displaySponsors, fetchSponsors } from "./utils/sponsors"; import { getProvidedFlags, processAndValidateFlags } from "./validation"; const t = trpcServer.initTRPC.create(); @@ -299,6 +301,41 @@ const router = t.router({ }; await createProjectHandler(combinedInput); }), + sponsors: t.procedure + .meta({ description: "Show Better-T Stack sponsors" }) + .mutation(async () => { + try { + renderTitle(); + intro(pc.magenta("Better-T Stack Sponsors")); + const sponsors = await fetchSponsors(); + displaySponsors(sponsors); + } catch (error) { + consola.error(error); + process.exit(1); + } + }), + docs: t.procedure + .meta({ description: "Open Better-T Stack documentation" }) + .mutation(async () => { + const DOCS_URL = "https://better-t-stack.amanv.dev/docs"; + try { + await openUrl(DOCS_URL); + log.success(pc.blue("Opened docs in your default browser.")); + } catch { + log.message(`Please visit ${DOCS_URL}`); + } + }), + builder: t.procedure + .meta({ description: "Open the web-based stack builder" }) + .mutation(async () => { + const BUILDER_URL = "https://better-t-stack.amanv.dev/new"; + try { + await openUrl(BUILDER_URL); + log.success(pc.blue("Opened builder in your default browser.")); + } catch { + log.message(`Please visit ${BUILDER_URL}`); + } + }), }); createCli({ diff --git a/apps/cli/src/utils/open-url.ts b/apps/cli/src/utils/open-url.ts new file mode 100644 index 0000000..b05d5fc --- /dev/null +++ b/apps/cli/src/utils/open-url.ts @@ -0,0 +1,25 @@ +import { log } from "@clack/prompts"; +import { execa } from "execa"; + +export async function openUrl(url: string): Promise { + const platform = process.platform; + let command: string; + let args: string[] = []; + + if (platform === "darwin") { + command = "open"; + args = [url]; + } else if (platform === "win32") { + command = "cmd"; + args = ["/c", "start", "", url.replace(/&/g, "^&")]; + } else { + command = "xdg-open"; + args = [url]; + } + + try { + await execa(command, args, { stdio: "ignore" }); + } catch { + log.message(`Please open ${url} in your browser.`); + } +} diff --git a/apps/cli/src/utils/sponsors.ts b/apps/cli/src/utils/sponsors.ts new file mode 100644 index 0000000..1f91b9a --- /dev/null +++ b/apps/cli/src/utils/sponsors.ts @@ -0,0 +1,68 @@ +import { log, outro, spinner } from "@clack/prompts"; +import pc from "picocolors"; + +export interface SponsorEntry { + readonly sponsor: { + readonly login: string; + readonly name?: string | null; + readonly avatarUrl?: string | null; + readonly websiteUrl?: string | null; + readonly linkUrl?: string | null; + readonly type?: string; + }; + readonly isOneTime: boolean; + readonly monthlyDollars?: number; + readonly tierName?: string; +} + +export const SPONSORS_JSON_URL = "https://sponsors.amanv.dev/sponsors.json"; + +export async function fetchSponsors( + url: string = SPONSORS_JSON_URL, +): Promise { + const s = spinner(); + s.start("Fetching sponsors…"); + + const response = await fetch(url); + if (!response.ok) { + s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`)); + throw new Error(`Failed to fetch sponsors: ${response.statusText}`); + } + + const sponsors = (await response.json()) as SponsorEntry[]; + s.stop("Sponsors fetched successfully!"); + return sponsors; +} + +export function displaySponsors(sponsors: SponsorEntry[]): void { + if (sponsors.length === 0) { + log.info("No sponsors found. You can be the first one! ✨"); + outro( + pc.cyan( + "Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor.", + ), + ); + return; + } + + sponsors.forEach((entry: SponsorEntry, idx: number) => { + const sponsor = entry.sponsor; + const displayName = sponsor.name ?? sponsor.login; + const tier = entry.tierName ? ` (${entry.tierName})` : ""; + + log.step(`${idx + 1}. ${pc.green(displayName)}${pc.yellow(tier)}`); + log.message(` ${pc.dim("GitHub:")} https://github.com/${sponsor.login}`); + + const website = sponsor.websiteUrl ?? sponsor.linkUrl; + if (website) { + log.message(` ${pc.dim("Website:")} ${website}`); + } + }); + + log.message(""); + outro( + pc.magenta( + "Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor.", + ), + ); +} diff --git a/apps/web/src/app/(home)/_components/sponsors-section.tsx b/apps/web/src/app/(home)/_components/sponsors-section.tsx index a6c27c1..e3184da 100644 --- a/apps/web/src/app/(home)/_components/sponsors-section.tsx +++ b/apps/web/src/app/(home)/_components/sponsors-section.tsx @@ -158,7 +158,7 @@ export default function SponsorsSection() { > - github.com/{entry.sponsor.login} + {entry.sponsor.login} {(entry.sponsor.websiteUrl ||