create-githat-app 1.8.12 → 1.9.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.
Files changed (26) hide show
  1. package/dist/cli.js +222 -18
  2. package/package.json +2 -1
  3. package/templates/agent/app/(auth)/forgot-password/page.tsx.hbs +2 -0
  4. package/templates/agent/app/(auth)/sign-in/page.tsx.hbs +2 -0
  5. package/templates/agent/app/(auth)/sign-up/page.tsx.hbs +2 -0
  6. package/templates/classroom/app/(auth)/sign-in/page.tsx.hbs +2 -0
  7. package/templates/classroom/app/(auth)/sign-up/page.tsx.hbs +2 -0
  8. package/templates/content/app/(auth)/sign-in/page.tsx.hbs +2 -0
  9. package/templates/content/app/(auth)/sign-up/page.tsx.hbs +2 -0
  10. package/templates/dashboard/app/(auth)/sign-in/page.tsx.hbs +2 -0
  11. package/templates/dashboard/app/(auth)/sign-up/page.tsx.hbs +2 -0
  12. package/templates/fullstack/apps-web-nextjs/app/(auth)/forgot-password/page.tsx.hbs +2 -0
  13. package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-in/page.tsx.hbs +2 -0
  14. package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-up/page.tsx.hbs +2 -0
  15. package/templates/marketplace/app/(auth)/sign-in/page.tsx.hbs +2 -0
  16. package/templates/marketplace/app/(auth)/sign-up/page.tsx.hbs +2 -0
  17. package/templates/nextjs/app/(auth)/forgot-password/page.tsx.hbs +2 -0
  18. package/templates/nextjs/app/(auth)/sign-in/page.tsx.hbs +2 -0
  19. package/templates/nextjs/app/(auth)/sign-up/page.tsx.hbs +2 -0
  20. package/templates/nextjs/app/(auth)/verify-email/page.tsx.hbs +35 -45
  21. package/templates/plain/app/(auth)/sign-in/page.tsx.hbs +2 -0
  22. package/templates/plain/app/(auth)/sign-up/page.tsx.hbs +2 -0
  23. package/templates/portfolio/app/(auth)/sign-in/page.tsx.hbs +2 -0
  24. package/templates/portfolio/app/(auth)/sign-up/page.tsx.hbs +2 -0
  25. package/templates/saas/app/(auth)/sign-in/page.tsx.hbs +2 -0
  26. package/templates/saas/app/(auth)/sign-up/page.tsx.hbs +2 -0
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command7 } from "commander";
5
- import * as p12 from "@clack/prompts";
6
- import chalk10 from "chalk";
4
+ import { Command as Command8 } from "commander";
5
+ import * as p13 from "@clack/prompts";
6
+ import chalk11 from "chalk";
7
7
 
8
8
  // src/utils/ascii.ts
9
9
  import figlet from "figlet";
@@ -11,7 +11,7 @@ import gradient from "gradient-string";
11
11
  import chalk from "chalk";
12
12
 
13
13
  // src/constants.ts
14
- var VERSION = "1.0.18";
14
+ var VERSION = "1.9.0";
15
15
  var DEFAULT_API_URL = "https://api.githat.io";
16
16
  var DASHBOARD_URL = "https://githat.io/dashboard/apps";
17
17
  var BRAND_COLORS = ["#7c3aed", "#6366f1", "#8b5cf6"];
@@ -527,11 +527,12 @@ async function promptGitHat(existingKey) {
527
527
  options: [
528
528
  { value: "forgot-password", label: "Forgot password", hint: "reset via email" },
529
529
  { value: "email-verification", label: "Email verification" },
530
+ { value: "magic-link", label: "Magic link sign-in", hint: "passwordless email link" },
530
531
  { value: "org-management", label: "Organizations", hint: "teams & roles" },
531
532
  { value: "mcp-servers", label: "MCP servers", hint: "Model Context Protocol" },
532
533
  { value: "ai-agents", label: "AI agents", hint: "wallet-based identity" }
533
534
  ],
534
- initialValues: ["forgot-password"],
535
+ initialValues: ["forgot-password", "email-verification", "magic-link"],
535
536
  required: false
536
537
  });
537
538
  if (p5.isCancel(authFeatures)) {
@@ -616,7 +617,7 @@ function getDefaults(projectName, publishableKey, typescript, fullstack, backend
616
617
  packageManager: detectPackageManager(),
617
618
  publishableKey: publishableKey || "",
618
619
  apiUrl: DEFAULT_API_URL,
619
- authFeatures: template === "agent" ? ["forgot-password", "email-verification", "mcp-servers", "ai-agents"] : isMinimal ? [] : ["forgot-password"],
620
+ authFeatures: template === "agent" ? ["forgot-password", "email-verification", "magic-link", "mcp-servers", "ai-agents"] : isMinimal ? [] : ["forgot-password", "email-verification", "magic-link"],
620
621
  databaseChoice: "none",
621
622
  useTailwind: true,
622
623
  includeDashboard: template === "agent" ? true : !isMinimal,
@@ -672,6 +673,7 @@ function answersToContext(answers) {
672
673
  includeGithatFolder: answers.includeGithatFolder,
673
674
  includeForgotPassword: answers.authFeatures.includes("forgot-password"),
674
675
  includeEmailVerification: answers.authFeatures.includes("email-verification"),
676
+ includeMagicLink: answers.authFeatures.includes("magic-link"),
675
677
  includeOrgManagement: answers.authFeatures.includes("org-management"),
676
678
  includeMcpModule: answers.authFeatures.includes("mcp-servers"),
677
679
  includeAgentModule: answers.authFeatures.includes("ai-agents"),
@@ -1648,35 +1650,236 @@ skillsCommand.action(() => {
1648
1650
  console.log("");
1649
1651
  });
1650
1652
 
1653
+ // src/commands/deploy/index.ts
1654
+ import { Command as Command7 } from "commander";
1655
+ import * as p12 from "@clack/prompts";
1656
+ import chalk10 from "chalk";
1657
+ import ora2 from "ora";
1658
+ import { execSync as execSync4, spawn } from "child_process";
1659
+ import { promises as fs8, existsSync as existsSync4 } from "fs";
1660
+ import path8 from "path";
1661
+ import * as tar from "tar";
1662
+ var DEFAULT_API_URL2 = "https://api.githat.io";
1663
+ async function readConfig(cwd) {
1664
+ const configPath = path8.join(cwd, ".githat", "config.json");
1665
+ let appId;
1666
+ let apiUrl = DEFAULT_API_URL2;
1667
+ if (existsSync4(configPath)) {
1668
+ try {
1669
+ const cfg = JSON.parse(await fs8.readFile(configPath, "utf-8"));
1670
+ appId = cfg.appId;
1671
+ if (cfg.apiUrl) apiUrl = cfg.apiUrl;
1672
+ } catch (err) {
1673
+ throw new Error(
1674
+ `Could not read .githat/config.json: ${err.message}`
1675
+ );
1676
+ }
1677
+ }
1678
+ if (!appId) appId = process.env.GITHAT_APP_ID;
1679
+ if (process.env.NEXT_PUBLIC_GITHAT_API_URL) apiUrl = process.env.NEXT_PUBLIC_GITHAT_API_URL;
1680
+ if (!appId) {
1681
+ throw new Error(
1682
+ [
1683
+ "No appId found. Either:",
1684
+ ` - Run ${chalk10.cyan("githat init")} to bind this directory to a GitHat app, or`,
1685
+ ` - Set ${chalk10.cyan("GITHAT_APP_ID")} in your environment, or`,
1686
+ ` - Create ${chalk10.cyan(".githat/config.json")} with { "appId": "..." }`
1687
+ ].join("\n")
1688
+ );
1689
+ }
1690
+ let secretKey = process.env.GITHAT_SECRET_KEY || "";
1691
+ if (!secretKey) {
1692
+ const envPath = path8.join(cwd, ".env.local");
1693
+ if (existsSync4(envPath)) {
1694
+ const envText = await fs8.readFile(envPath, "utf-8");
1695
+ const m = envText.match(/^GITHAT_SECRET_KEY=(.*)$/m);
1696
+ if (m) secretKey = m[1].replace(/^["']|["']$/g, "").trim();
1697
+ }
1698
+ }
1699
+ if (!secretKey || !secretKey.startsWith("sk_live_")) {
1700
+ throw new Error(
1701
+ [
1702
+ "No GITHAT_SECRET_KEY found. Either:",
1703
+ ` - Add ${chalk10.cyan("GITHAT_SECRET_KEY=sk_live_...")} to ${chalk10.cyan(".env.local")}, or`,
1704
+ ` - Set the ${chalk10.cyan("GITHAT_SECRET_KEY")} env var (CI flow)`,
1705
+ "",
1706
+ chalk10.dim("Get yours at https://githat.io/dashboard/apps \u2192 your app \u2192 Keys \u2192 New secret key")
1707
+ ].join("\n")
1708
+ );
1709
+ }
1710
+ return { appId, apiUrl, secretKey };
1711
+ }
1712
+ async function packArtifact(cwd) {
1713
+ const outDir = path8.join(cwd, "out");
1714
+ if (!existsSync4(outDir)) {
1715
+ throw new Error(
1716
+ `No \`out/\` directory at ${outDir}. Run \`npm run build\` first.`
1717
+ );
1718
+ }
1719
+ const entries = await fs8.readdir(outDir);
1720
+ if (entries.length === 0) {
1721
+ throw new Error("`out/` is empty. Build did not produce any files.");
1722
+ }
1723
+ const stream = tar.create(
1724
+ {
1725
+ gzip: true,
1726
+ cwd: outDir,
1727
+ // 'portable' strips uid/gid/atime/ctime so the bundle hashes
1728
+ // identically across machines (helpful if you later add deploy
1729
+ // dedup on content hash).
1730
+ portable: true
1731
+ },
1732
+ entries
1733
+ );
1734
+ const chunks = [];
1735
+ for await (const chunk of stream) chunks.push(chunk);
1736
+ return Buffer.concat(chunks);
1737
+ }
1738
+ async function uploadArtifact(cfg, artifact, commit, commitMessage) {
1739
+ const url = `${cfg.apiUrl.replace(/\/$/, "")}/apps/${cfg.appId}/deploy`;
1740
+ const headers = {
1741
+ "Content-Type": "application/gzip",
1742
+ "Authorization": `Bearer ${cfg.secretKey}`
1743
+ };
1744
+ if (commit) headers["X-GitHat-Commit"] = commit;
1745
+ if (commitMessage) {
1746
+ const subject = commitMessage.split(/\r?\n/, 1)[0] || "";
1747
+ const sanitized = subject.replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, 200);
1748
+ if (sanitized) headers["X-GitHat-Commit-Message"] = sanitized;
1749
+ }
1750
+ const res = await fetch(url, { method: "POST", headers, body: artifact });
1751
+ const text4 = await res.text();
1752
+ let body;
1753
+ try {
1754
+ body = JSON.parse(text4);
1755
+ } catch {
1756
+ body = { error: text4 };
1757
+ }
1758
+ if (!res.ok) {
1759
+ throw new Error(
1760
+ `Deploy failed (${res.status}): ${body?.error || res.statusText}`
1761
+ );
1762
+ }
1763
+ return body;
1764
+ }
1765
+ function maybeReadGitInfo(cwd) {
1766
+ try {
1767
+ const commit = execSync4("git rev-parse --short HEAD", {
1768
+ cwd,
1769
+ stdio: ["ignore", "pipe", "ignore"]
1770
+ }).toString().trim();
1771
+ const message = execSync4("git log -1 --pretty=%B", {
1772
+ cwd,
1773
+ stdio: ["ignore", "pipe", "ignore"]
1774
+ }).toString().trim();
1775
+ return { commit, message };
1776
+ } catch {
1777
+ return { commit: null, message: null };
1778
+ }
1779
+ }
1780
+ async function ensureBuilt(cwd, force) {
1781
+ const outDir = path8.join(cwd, "out");
1782
+ if (existsSync4(outDir) && !force) {
1783
+ return;
1784
+ }
1785
+ const spinner2 = ora2("Building static export (npm run build)...").start();
1786
+ await new Promise((resolve4, reject) => {
1787
+ const child = spawn("npm", ["run", "build"], {
1788
+ cwd,
1789
+ stdio: ["ignore", "pipe", "pipe"],
1790
+ env: { ...process.env }
1791
+ });
1792
+ let stderr = "";
1793
+ child.stderr.on("data", (d) => {
1794
+ stderr += d.toString();
1795
+ });
1796
+ child.on("close", (code) => {
1797
+ if (code === 0) resolve4();
1798
+ else reject(new Error(`Build exited with code ${code}.
1799
+ ${stderr.slice(-2e3)}`));
1800
+ });
1801
+ });
1802
+ spinner2.succeed("Build complete");
1803
+ }
1804
+ function formatBytes(n) {
1805
+ if (n < 1024) return `${n} B`;
1806
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1807
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
1808
+ }
1809
+ var deployCommand = new Command7("deploy").description("Deploy this app to GitHat (no AWS credentials required)").option("--build", "Force a fresh build even if out/ exists", false).option("--cwd <dir>", "Run from this directory instead of the current one").action(async (opts) => {
1810
+ const cwd = path8.resolve(opts.cwd ?? process.cwd());
1811
+ try {
1812
+ const cfg = await readConfig(cwd);
1813
+ p12.note(
1814
+ [
1815
+ `${chalk10.dim("app: ")}${cfg.appId}`,
1816
+ `${chalk10.dim("api: ")}${cfg.apiUrl}`,
1817
+ `${chalk10.dim("cwd: ")}${cwd}`
1818
+ ].join("\n"),
1819
+ "githat deploy"
1820
+ );
1821
+ await ensureBuilt(cwd, !!opts.build);
1822
+ const packSpin = ora2("Packing artifact...").start();
1823
+ const artifact = await packArtifact(cwd);
1824
+ packSpin.succeed(`Packed ${formatBytes(artifact.length)}`);
1825
+ if (artifact.length > 10 * 1024 * 1024) {
1826
+ throw new Error(
1827
+ `Artifact is ${formatBytes(artifact.length)} \u2014 over the 10 MB API limit. Reduce out/ size or wait for the chunked-upload flow.`
1828
+ );
1829
+ }
1830
+ const upSpin = ora2("Uploading to GitHat...").start();
1831
+ const git = maybeReadGitInfo(cwd);
1832
+ const result = await uploadArtifact(cfg, artifact, git.commit, git.message);
1833
+ upSpin.succeed(
1834
+ `Uploaded ${result.filesUploaded} files (${formatBytes(result.bytesUploaded)})`
1835
+ );
1836
+ const lines = [
1837
+ chalk10.green.bold("\u{1F680} Deployment succeeded"),
1838
+ "",
1839
+ `${chalk10.dim("deployment id: ")}${result.deploymentId}`,
1840
+ `${chalk10.dim("cf invalidation:")} ${result.cloudfrontInvalidationId ?? chalk10.dim("(none)")}`,
1841
+ `${chalk10.dim("files: ")}${result.filesUploaded}`,
1842
+ `${chalk10.dim("size: ")}${formatBytes(result.bytesUploaded)}`
1843
+ ];
1844
+ if (result.url) {
1845
+ lines.push("", `${chalk10.cyan("Live at:")} ${result.url}`);
1846
+ }
1847
+ p12.note(lines.join("\n"));
1848
+ } catch (err) {
1849
+ p12.cancel(chalk10.red(err.message));
1850
+ process.exit(1);
1851
+ }
1852
+ });
1853
+
1651
1854
  // src/cli.ts
1652
- var program = new Command7();
1855
+ var program = new Command8();
1653
1856
  program.name("githat").description("GitHat CLI - Scaffold apps and manage skills").version(VERSION);
1654
1857
  program.command("create [project-name]", { isDefault: true }).description("Scaffold a new GitHat app").option("--key <key>", "CI only: bake key into .env.local. Default flow is paste into .env.local after scaffold.").option("--ts", "Use TypeScript (default)").option("--js", "Use JavaScript").option("--plain", "Smallest scaffold: auth + a homepage. No dashboard.").option("--saas", "B2B starter: orgs, teams, RBAC, subscription billing.").option("--marketplace", "Multi-vendor commerce: anonymous-first browsing, Sebastn Connect.").option("--agent", "Web4 wallet-bound autonomous agent + MCP server registration.").option("--content", "Paywalled posts, newsletter, one-time purchases via Sebastn.").option("--dashboard", "Admin UI over your existing database, auth-gated.").option("--portfolio", "Personal portfolio: public projects, auth-gated editor.").option("--classroom", "Live student presentations with real-time audience feedback.").option("--fullstack", "Create fullstack project (Turborepo)").option("--backend <framework>", "Backend framework (hono, express, fastify)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, opts) => {
1655
1858
  try {
1656
1859
  displayBanner();
1657
1860
  if (!opts.yes && !opts.key) {
1658
- p12.note(
1861
+ p13.note(
1659
1862
  [
1660
- chalk10.bold("How the GitHat key flow works:"),
1863
+ chalk11.bold("How the GitHat key flow works:"),
1661
1864
  "",
1662
- ` ${chalk10.cyan("1.")} We'll scaffold your project with a placeholder`,
1663
- ` in ${chalk10.bold(".env.local")} (gitignored \u2014 safe to keep secrets here)`,
1865
+ ` ${chalk11.cyan("1.")} We'll scaffold your project with a placeholder`,
1866
+ ` in ${chalk11.bold(".env.local")} (gitignored \u2014 safe to keep secrets here)`,
1664
1867
  "",
1665
- ` ${chalk10.cyan("2.")} You open ${chalk10.cyan("https://githat.io/dashboard/apps")}`,
1868
+ ` ${chalk11.cyan("2.")} You open ${chalk11.cyan("https://githat.io/dashboard/apps")}`,
1666
1869
  ` and copy your publishable key`,
1667
1870
  "",
1668
- ` ${chalk10.cyan("3.")} You paste it into ${chalk10.bold(".env.local")} and run`,
1669
- ` ${chalk10.dim("npm run dev")}`,
1871
+ ` ${chalk11.cyan("3.")} You paste it into ${chalk11.bold(".env.local")} and run`,
1872
+ ` ${chalk11.dim("npm run dev")}`,
1670
1873
  "",
1671
- chalk10.dim("Why not pass the key on the command line? Shell"),
1672
- chalk10.dim("history is forever. .env.local is more secure.")
1874
+ chalk11.dim("Why not pass the key on the command line? Shell"),
1875
+ chalk11.dim("history is forever. .env.local is more secure.")
1673
1876
  ].join("\n"),
1674
1877
  "First time with GitHat?"
1675
1878
  );
1676
1879
  }
1677
1880
  const typescript = opts.js ? false : opts.ts ? true : void 0;
1678
1881
  if (opts.yes && !projectName) {
1679
- p12.cancel(chalk10.red("Project name is required when using --yes flag"));
1882
+ p13.cancel(chalk11.red("Project name is required when using --yes flag"));
1680
1883
  process.exit(1);
1681
1884
  }
1682
1885
  const template = opts.marketplace ? "marketplace" : opts.classroom ? "classroom" : opts.portfolio ? "portfolio" : opts.agent ? "agent" : opts.saas ? "saas" : opts.content ? "content" : opts.dashboard ? "dashboard" : opts.plain ? "plain" : void 0;
@@ -1696,9 +1899,10 @@ program.command("create [project-name]", { isDefault: true }).description("Scaff
1696
1899
  skipPrompts: opts.yes
1697
1900
  });
1698
1901
  } catch (err) {
1699
- p12.cancel(chalk10.red(err.message || "Something went wrong."));
1902
+ p13.cancel(chalk11.red(err.message || "Something went wrong."));
1700
1903
  process.exit(1);
1701
1904
  }
1702
1905
  });
1703
1906
  program.addCommand(skillsCommand);
1907
+ program.addCommand(deployCommand);
1704
1908
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githat-app",
3
- "version": "1.8.12",
3
+ "version": "1.9.0",
4
4
  "description": "GitHat CLI — scaffold apps and manage the skills marketplace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "gradient-string": "^3.0.0",
27
27
  "handlebars": "^4.7.9",
28
28
  "ora": "^9.3.0",
29
+ "tar": "^7.4.3",
29
30
  "unzipper": "^0.12.3"
30
31
  },
31
32
  "devDependencies": {
@@ -1,6 +1,8 @@
1
1
  {{#if includeForgotPassword}}
2
2
  import { ForgotPasswordForm } from '@githat/nextjs';
3
3
 
4
+ export const metadata = { title: 'Reset password — {{businessName}}' };
5
+
4
6
  export default function ForgotPasswordPage() {
5
7
  return (
6
8
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,6 +1,8 @@
1
1
  {{#if includeForgotPassword}}
2
2
  import { ForgotPasswordForm } from '@githat/nextjs';
3
3
 
4
+ export const metadata = { title: 'Reset password — {{businessName}}' };
5
+
4
6
  export default function ForgotPasswordPage() {
5
7
  return (
6
8
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,6 +1,8 @@
1
1
  {{#if includeForgotPassword}}
2
2
  import { ForgotPasswordForm } from '@githat/nextjs';
3
3
 
4
+ export const metadata = { title: 'Reset password — {{businessName}}' };
5
+
4
6
  export default function ForgotPasswordPage() {
5
7
  return (
6
8
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,57 +1,47 @@
1
1
  {{#if includeEmailVerification}}
2
2
  'use client';
3
3
 
4
- import { useEffect, useState } from 'react';
5
- {{#if includeGithatFolder}}
6
- import { authApi } from '../../../githat/api/auth{{#unless typescript}}.js{{/unless}}';
7
- {{/if}}
4
+ import { Suspense } from 'react';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import Link from 'next/link';
7
+ import { VerifyEmailStatus } from '@githat/nextjs';
8
8
 
9
- export default function VerifyEmailPage() {
10
- const [status, setStatus] = useState{{#if typescript}}<'loading' | 'success' | 'error'>{{/if}}('loading');
9
+ export const metadata = { title: 'Verify email — {{businessName}}' };
11
10
 
12
- useEffect(() => {
13
- const params = new URLSearchParams(window.location.search);
14
- const token = params.get('token');
15
- if (!token) {
16
- setStatus('error');
17
- return;
18
- }
11
+ /**
12
+ * Verify-email landing — handles the `/verify-email?token=…` link from
13
+ * the GitHat Auth verification email. Uses the SDK's VerifyEmailStatus
14
+ * component (consistent with how SignInForm / SignUpForm /
15
+ * ForgotPasswordForm / ResetPasswordForm / MagicLinkVerify wrap their
16
+ * respective flows in this scaffold). Every auth page is a thin
17
+ * wrapper around the SDK component.
18
+ */
19
+ function VerifyEmailContent() {
20
+ const searchParams = useSearchParams();
21
+ const token = searchParams.get('token');
19
22
 
20
- (async () => {
21
- try {
22
- {{#if includeGithatFolder}}
23
- await authApi.verifyEmail(token);
24
- {{else}}
25
- await fetch('{{apiUrl}}/auth/verify-email', {
26
- method: 'POST',
27
- headers: { 'Content-Type': 'application/json' },
28
- body: JSON.stringify({ token }),
29
- });
30
- {{/if}}
31
- setStatus('success');
32
- } catch {
33
- setStatus('error');
34
- }
35
- })();
36
- }, []);
23
+ if (!token) {
24
+ return (
25
+ <div {{#if useTailwind}}className="text-center max-w-sm p-8"{{else}}style=\{{ textAlign: 'center', maxWidth: '24rem', padding: '2rem' }}{{/if}}>
26
+ <h1 {{#if useTailwind}}className="text-xl font-semibold text-white mb-2"{{else}}style=\{{ fontSize: '1.25rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}{{/if}}>
27
+ Verification link missing
28
+ </h1>
29
+ <p {{#if useTailwind}}className="text-zinc-400"{{else}}style=\{{ color: '#a1a1aa' }}{{/if}}>
30
+ <Link href="/sign-in" {{#if useTailwind}}className="text-violet-500 hover:underline"{{else}}style=\{{ color: '#7c3aed' }}{{/if}}>Back to sign in</Link>
31
+ </p>
32
+ </div>
33
+ );
34
+ }
37
35
 
36
+ return <VerifyEmailStatus token={token} signInUrl="/sign-in" />;
37
+ }
38
+
39
+ export default function VerifyEmailPage() {
38
40
  return (
39
41
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
40
- <div style=\{{ textAlign: 'center' }}>
41
- {status === 'loading' && <p style=\{{ color: '#a1a1aa' }}>Verifying your email...</p>}
42
- {status === 'success' && (
43
- <>
44
- <h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Email verified!</h1>
45
- <p style=\{{ color: '#a1a1aa' }}>You can now <a href="/sign-in" style=\{{ color: '#7c3aed' }}>sign in</a>.</p>
46
- </>
47
- )}
48
- {status === 'error' && (
49
- <>
50
- <h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Verification failed</h1>
51
- <p style=\{{ color: '#a1a1aa' }}>The link may have expired. <a href="/sign-up" style=\{{ color: '#7c3aed' }}>Try again</a>.</p>
52
- </>
53
- )}
54
- </div>
42
+ <Suspense fallback={null}>
43
+ <VerifyEmailContent />
44
+ </Suspense>
55
45
  </main>
56
46
  );
57
47
  }
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignInForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign in — {{businessName}}' };
4
+
3
5
  export default function SignInPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
@@ -1,5 +1,7 @@
1
1
  import { SignUpForm } from '@githat/nextjs';
2
2
 
3
+ export const metadata = { title: 'Sign up — {{businessName}}' };
4
+
3
5
  export default function SignUpPage() {
4
6
  return (
5
7
  <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>