create-githat-app 0.1.1

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.
@@ -0,0 +1,529 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const nextjsTemplates = require('./templates');
6
+ const reactTemplates = require('./templates/react');
7
+ const pageTemplates = require('./templates/pages');
8
+ const componentTemplates = require('./templates/components');
9
+
10
+ // ANSI helpers
11
+ const c = {
12
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
13
+ green: '\x1b[32m', cyan: '\x1b[36m', magenta: '\x1b[35m', red: '\x1b[31m',
14
+ };
15
+
16
+ const ok = (msg) => console.log(` ${c.green}✓${c.reset} ${msg}`);
17
+ const info = (msg) => console.log(` ${c.cyan}◐${c.reset} ${msg}`);
18
+
19
+ async function scaffold(options) {
20
+ const { projectName, framework, typescript, packageManager, publishableKey, features } = options;
21
+ const root = path.resolve(process.cwd(), projectName);
22
+
23
+ if (fs.existsSync(root)) {
24
+ console.error(`\n ${c.red}✗${c.reset} Directory "${projectName}" already exists.\n`);
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log();
29
+ fs.mkdirSync(root, { recursive: true });
30
+
31
+ if (framework === 'react') {
32
+ scaffoldReact(root, options);
33
+ } else {
34
+ scaffoldNextjs(root, options);
35
+ }
36
+
37
+ // Install dependencies
38
+ const installCmd = { npm: 'npm install', pnpm: 'pnpm install', yarn: 'yarn' }[packageManager] || 'npm install';
39
+ info(`Installing dependencies with ${packageManager}...`);
40
+ try {
41
+ execSync(installCmd, { cwd: root, stdio: 'ignore' });
42
+ ok('Installed dependencies');
43
+ } catch {
44
+ console.log(` ${c.red}⚠${c.reset} Could not auto-install. Run ${c.cyan}${installCmd}${c.reset} manually.`);
45
+ }
46
+
47
+ const devCmd = packageManager === 'npm' ? 'npm run dev' : `${packageManager} dev`;
48
+ const port = framework === 'react' ? '5173' : '3000';
49
+
50
+ // Build routes list for the banner
51
+ const routes = [
52
+ ' /sign-up Create account ',
53
+ ' /sign-in Sign in ',
54
+ ];
55
+ if (features.forgotPassword) routes.push(' /forgot-pw Reset password ');
56
+ if (features.dashboard) routes.push(' /dashboard Protected page ');
57
+ if (features.settings) routes.push(' /settings Profile & prefs ');
58
+ if (features.team) routes.push(' /team Org members ');
59
+ if (features.apps) routes.push(' /apps API keys ');
60
+
61
+ const routeLines = routes.map((r) =>
62
+ ` ${c.magenta}│${c.reset} ${c.dim}${r}${c.reset} ${c.magenta}│${c.reset}`
63
+ ).join('\n');
64
+
65
+ console.log(`
66
+ ${c.magenta}┌─────────────────────────────────────┐${c.reset}
67
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
68
+ ${c.magenta}│${c.reset} ${c.green}${c.bold}✓ Ready!${c.reset} ${c.magenta}│${c.reset}
69
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
70
+ ${c.magenta}│${c.reset} ${c.cyan}cd ${projectName}${c.reset}${pad(25 - projectName.length)}${c.magenta}│${c.reset}
71
+ ${c.magenta}│${c.reset} ${c.cyan}${devCmd}${c.reset}${pad(33 - devCmd.length)}${c.magenta}│${c.reset}
72
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
73
+ ${c.magenta}│${c.reset} ${c.dim}→ http://localhost:${port}${c.reset}${pad(port.length === 4 ? 8 : 9)}${c.magenta}│${c.reset}
74
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
75
+ ${routeLines}
76
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
77
+ ${c.magenta}│${c.reset} ${c.dim}Docs: https://githat.io/docs/sdk${c.reset} ${c.magenta}│${c.reset}
78
+ ${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
79
+ ${c.magenta}└─────────────────────────────────────┘${c.reset}
80
+
81
+ ${c.bold}Next steps:${c.reset}
82
+ ${c.dim}1.${c.reset} Get your publishable key at ${c.cyan}https://githat.io/dashboard/apps${c.reset}
83
+ ${c.dim}2.${c.reset} Add it to ${c.cyan}.env.local${c.reset} (or ${c.cyan}.env${c.reset} for Vite)
84
+ ${c.dim}3.${c.reset} Run ${c.cyan}${devCmd}${c.reset} and open ${c.cyan}http://localhost:${port}${c.reset}
85
+ `);
86
+ }
87
+
88
+ function pad(n) { return ' '.repeat(Math.max(0, n)); }
89
+
90
+ // ─── Next.js (App Router) ────────────────────────────────────────────
91
+
92
+ function scaffoldNextjs(root, options) {
93
+ const { projectName, typescript, publishableKey, features } = options;
94
+ const ext = typescript ? 'tsx' : 'jsx';
95
+ const hasSidebar = features.settings || features.team || features.apps;
96
+
97
+ const pkg = {
98
+ name: projectName, version: '0.1.0', private: true,
99
+ scripts: { dev: 'next dev', build: 'next build', start: 'next start', lint: 'next lint' },
100
+ dependencies: { next: '^14.0.0', react: '^18.0.0', 'react-dom': '^18.0.0', '@githat/nextjs': '^0.2.0' },
101
+ devDependencies: {},
102
+ };
103
+ if (typescript) {
104
+ Object.assign(pkg.devDependencies, {
105
+ typescript: '^5.0.0', '@types/react': '^18.0.0', '@types/react-dom': '^18.0.0', '@types/node': '^20.0.0',
106
+ });
107
+ }
108
+ writeFile(root, 'package.json', JSON.stringify(pkg, null, 2));
109
+
110
+ writeFile(root, 'next.config.mjs',
111
+ `/** @type {import('next').NextConfig} */\nconst nextConfig = {};\nexport default nextConfig;\n`);
112
+
113
+ if (typescript) {
114
+ writeFile(root, 'tsconfig.json', JSON.stringify({
115
+ compilerOptions: {
116
+ target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], allowJs: true, skipLibCheck: true,
117
+ strict: true, noEmit: true, esModuleInterop: true, module: 'esnext', moduleResolution: 'bundler',
118
+ resolveJsonModule: true, isolatedModules: true, jsx: 'preserve', incremental: true,
119
+ plugins: [{ name: 'next' }], paths: { '@/*': ['./*'] },
120
+ },
121
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
122
+ exclude: ['node_modules'],
123
+ }, null, 2));
124
+ }
125
+
126
+ // Public routes for middleware
127
+ const publicRoutes = ['/', '/sign-in', '/sign-up'];
128
+ if (features.forgotPassword) publicRoutes.push('/forgot-password', '/reset-password');
129
+
130
+ writeFile(root, '.env.local', `NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=${publishableKey}\n`);
131
+ writeFile(root, '.env.example', `# Get your publishable key at https://githat.io/dashboard/apps\nNEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here\n`);
132
+ writeFile(root, '.gitignore', 'node_modules\n.next\n.env*.local\nout\n');
133
+ writeFile(root, `middleware.${typescript ? 'ts' : 'js'}`, nextjsMiddleware(publicRoutes));
134
+
135
+ fs.mkdirSync(path.join(root, 'app'), { recursive: true });
136
+ writeFile(root, `app/layout.${ext}`, nextjsTemplates.layout(typescript));
137
+ writeFile(root, `app/page.${ext}`, nextjsTemplates.page());
138
+ writeFile(root, 'app/globals.css', nextjsTemplates.globalsCss());
139
+ ok('Created project structure');
140
+
141
+ // Auth pages
142
+ fs.mkdirSync(path.join(root, 'app', '(auth)', 'sign-in'), { recursive: true });
143
+ fs.mkdirSync(path.join(root, 'app', '(auth)', 'sign-up'), { recursive: true });
144
+ writeFile(root, `app/(auth)/sign-in/page.${ext}`, nextjsTemplates.signInPage());
145
+ writeFile(root, `app/(auth)/sign-up/page.${ext}`, nextjsTemplates.signUpPage());
146
+ ok('Generated auth pages (sign-in, sign-up)');
147
+
148
+ // Forgot / Reset password
149
+ if (features.forgotPassword) {
150
+ fs.mkdirSync(path.join(root, 'app', '(auth)', 'forgot-password'), { recursive: true });
151
+ fs.mkdirSync(path.join(root, 'app', '(auth)', 'reset-password'), { recursive: true });
152
+ writeFile(root, `app/(auth)/forgot-password/page.${ext}`, pageTemplates.forgotPasswordPage());
153
+ writeFile(root, `app/(auth)/reset-password/page.${ext}`, pageTemplates.resetPasswordPage());
154
+ ok('Added forgot/reset password flow');
155
+ }
156
+
157
+ // Dashboard + sub-pages
158
+ if (features.dashboard) {
159
+ fs.mkdirSync(path.join(root, 'app', 'dashboard'), { recursive: true });
160
+ writeFile(root, `app/dashboard/layout.${ext}`, nextjsDashboardLayout(features));
161
+ writeFile(root, `app/dashboard/page.${ext}`, nextjsTemplates.dashboardPage());
162
+
163
+ // Verify email banner component
164
+ fs.mkdirSync(path.join(root, 'app', 'components'), { recursive: true });
165
+ writeFile(root, `app/components/VerifyEmailBanner.${ext}`, componentTemplates.verifyEmailBanner());
166
+
167
+ if (hasSidebar) {
168
+ writeFile(root, `app/components/Sidebar.${ext}`, componentTemplates.sidebar(features));
169
+ }
170
+
171
+ ok('Added protected dashboard');
172
+
173
+ if (features.settings) {
174
+ fs.mkdirSync(path.join(root, 'app', 'dashboard', 'settings'), { recursive: true });
175
+ writeFile(root, `app/dashboard/settings/page.${ext}`, pageTemplates.settingsPage());
176
+ ok('Added settings page');
177
+ }
178
+ if (features.team) {
179
+ fs.mkdirSync(path.join(root, 'app', 'dashboard', 'team'), { recursive: true });
180
+ writeFile(root, `app/dashboard/team/page.${ext}`, pageTemplates.teamPage());
181
+ ok('Added team management page');
182
+ }
183
+ if (features.apps) {
184
+ fs.mkdirSync(path.join(root, 'app', 'dashboard', 'apps'), { recursive: true });
185
+ writeFile(root, `app/dashboard/apps/page.${ext}`, pageTemplates.appsPage());
186
+ ok('Added API keys page');
187
+ }
188
+ }
189
+
190
+ // README
191
+ writeFile(root, 'README.md', generateReadme(projectName, 'nextjs', features));
192
+ ok('Generated README.md');
193
+
194
+ ok('Configured middleware');
195
+ }
196
+
197
+ function nextjsMiddleware(publicRoutes) {
198
+ const routesStr = publicRoutes.map((r) => `'${r}'`).join(', ');
199
+ return `import { authMiddleware } from '@githat/nextjs/middleware';
200
+
201
+ export default authMiddleware({
202
+ publicRoutes: [${routesStr}],
203
+ signInUrl: '/sign-in',
204
+ });
205
+
206
+ export const config = {
207
+ matcher: ['/((?!_next|api|.*\\\\..*).*)'],
208
+ };
209
+ `;
210
+ }
211
+
212
+ function nextjsDashboardLayout(features) {
213
+ const hasSidebar = features.settings || features.team || features.apps;
214
+ const sidebarImport = hasSidebar ? `\nimport Sidebar from '../components/Sidebar';` : '';
215
+ const bannerImport = `\nimport VerifyEmailBanner from '../components/VerifyEmailBanner';`;
216
+ const sidebarJsx = hasSidebar
217
+ ? `\n <Sidebar />\n <main style={{ flex: 1, padding: '2rem', overflow: 'auto' }}>`
218
+ : `\n <main style={{ padding: '2rem' }}>`;
219
+
220
+ return `import { ProtectedRoute, UserButton, OrgSwitcher } from '@githat/nextjs';${sidebarImport}${bannerImport}
221
+
222
+ export default function DashboardLayout({ children }) {
223
+ return (
224
+ <ProtectedRoute>
225
+ <div style={{ minHeight: '100vh', background: '#09090b', display: 'flex', flexDirection: 'column' }}>
226
+ <VerifyEmailBanner />
227
+ <header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 2rem', borderBottom: '1px solid #1e1e2e' }}>
228
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
229
+ <h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: '#fafafa' }}>Dashboard</h2>
230
+ <OrgSwitcher />
231
+ </div>
232
+ <UserButton />
233
+ </header>
234
+ <div style={{ display: 'flex', flex: 1 }}>${sidebarJsx}
235
+ {children}
236
+ </main>
237
+ </div>
238
+ </div>
239
+ </ProtectedRoute>
240
+ );
241
+ }
242
+ `;
243
+ }
244
+
245
+ // ─── React (Vite) ────────────────────────────────────────────────────
246
+
247
+ function scaffoldReact(root, options) {
248
+ const { projectName, typescript, publishableKey, features } = options;
249
+ const ext = typescript ? 'tsx' : 'jsx';
250
+ const hasSidebar = features.settings || features.team || features.apps;
251
+
252
+ const pkg = {
253
+ name: projectName, version: '0.1.0', private: true, type: 'module',
254
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
255
+ dependencies: {
256
+ react: '^18.0.0', 'react-dom': '^18.0.0', 'react-router-dom': '^6.0.0', '@githat/nextjs': '^0.2.0',
257
+ },
258
+ devDependencies: { vite: '^5.0.0', '@vitejs/plugin-react': '^4.0.0' },
259
+ };
260
+ if (typescript) {
261
+ Object.assign(pkg.devDependencies, {
262
+ typescript: '^5.0.0', '@types/react': '^18.0.0', '@types/react-dom': '^18.0.0',
263
+ });
264
+ }
265
+ writeFile(root, 'package.json', JSON.stringify(pkg, null, 2));
266
+
267
+ writeFile(root, typescript ? 'vite.config.ts' : 'vite.config.js', reactTemplates.viteConfig());
268
+
269
+ if (typescript) {
270
+ writeFile(root, 'tsconfig.json', JSON.stringify({
271
+ compilerOptions: {
272
+ target: 'ES2020', lib: ['ES2020', 'DOM', 'DOM.Iterable'], module: 'ESNext', skipLibCheck: true,
273
+ moduleResolution: 'bundler', allowImportingTsExtensions: true, isolatedModules: true,
274
+ moduleDetection: 'force', noEmit: true, jsx: 'react-jsx', strict: true,
275
+ },
276
+ include: ['src'],
277
+ }, null, 2));
278
+ }
279
+
280
+ writeFile(root, '.env', `VITE_GITHAT_PUBLISHABLE_KEY=${publishableKey}\n`);
281
+ writeFile(root, '.env.example', `# Get your publishable key at https://githat.io/dashboard/apps\nVITE_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here\n`);
282
+ writeFile(root, '.gitignore', 'node_modules\ndist\n.env*.local\n');
283
+ writeFile(root, 'index.html', reactTemplates.indexHtml(projectName, typescript));
284
+ ok('Created project structure');
285
+
286
+ fs.mkdirSync(path.join(root, 'src', 'pages'), { recursive: true });
287
+ fs.mkdirSync(path.join(root, 'src', 'components'), { recursive: true });
288
+ writeFile(root, `src/main.${ext}`, reactTemplates.mainEntry());
289
+ writeFile(root, `src/App.${ext}`, reactAppRoutes(features));
290
+ writeFile(root, 'src/index.css', reactTemplates.indexCss());
291
+
292
+ // Core pages
293
+ writeFile(root, `src/pages/Home.${ext}`, reactTemplates.homePage());
294
+ writeFile(root, `src/pages/SignIn.${ext}`, reactTemplates.signInPage());
295
+ writeFile(root, `src/pages/SignUp.${ext}`, reactTemplates.signUpPage());
296
+ ok('Generated auth pages (sign-in, sign-up)');
297
+
298
+ // Forgot / Reset password
299
+ if (features.forgotPassword) {
300
+ writeFile(root, `src/pages/ForgotPassword.${ext}`, pageTemplates.forgotPasswordPage());
301
+ writeFile(root, `src/pages/ResetPassword.${ext}`, pageTemplates.resetPasswordPage());
302
+ ok('Added forgot/reset password flow');
303
+ }
304
+
305
+ // Dashboard + sub-pages
306
+ if (features.dashboard) {
307
+ writeFile(root, `src/pages/Dashboard.${ext}`, reactTemplates.dashboardPage());
308
+ writeFile(root, `src/components/VerifyEmailBanner.${ext}`, componentTemplates.verifyEmailBanner());
309
+
310
+ if (hasSidebar) {
311
+ writeFile(root, `src/components/Sidebar.${ext}`, componentTemplates.sidebar(features));
312
+ }
313
+
314
+ writeFile(root, `src/pages/DashboardLayout.${ext}`, reactDashboardLayout(features));
315
+ ok('Added protected dashboard');
316
+
317
+ if (features.settings) {
318
+ writeFile(root, `src/pages/Settings.${ext}`, pageTemplates.settingsPage());
319
+ ok('Added settings page');
320
+ }
321
+ if (features.team) {
322
+ writeFile(root, `src/pages/Team.${ext}`, pageTemplates.teamPage());
323
+ ok('Added team management page');
324
+ }
325
+ if (features.apps) {
326
+ writeFile(root, `src/pages/Apps.${ext}`, pageTemplates.appsPage());
327
+ ok('Added API keys page');
328
+ }
329
+ }
330
+
331
+ // README
332
+ writeFile(root, 'README.md', generateReadme(projectName, 'react', features));
333
+ ok('Generated README.md');
334
+ }
335
+
336
+ function reactAppRoutes(features) {
337
+ const imports = [
338
+ `import { Routes, Route } from 'react-router-dom';`,
339
+ `import Home from './pages/Home';`,
340
+ `import SignIn from './pages/SignIn';`,
341
+ `import SignUp from './pages/SignUp';`,
342
+ ];
343
+ const routes = [
344
+ ` <Route path="/" element={<Home />} />`,
345
+ ` <Route path="/sign-in" element={<SignIn />} />`,
346
+ ` <Route path="/sign-up" element={<SignUp />} />`,
347
+ ];
348
+
349
+ if (features.forgotPassword) {
350
+ imports.push(`import ForgotPassword from './pages/ForgotPassword';`);
351
+ imports.push(`import ResetPassword from './pages/ResetPassword';`);
352
+ routes.push(` <Route path="/forgot-password" element={<ForgotPassword />} />`);
353
+ routes.push(` <Route path="/reset-password" element={<ResetPassword />} />`);
354
+ }
355
+
356
+ if (features.dashboard) {
357
+ imports.push(`import DashboardLayout from './pages/DashboardLayout';`);
358
+ imports.push(`import Dashboard from './pages/Dashboard';`);
359
+
360
+ const subRoutes = [` <Route index element={<Dashboard />} />`];
361
+ if (features.settings) {
362
+ imports.push(`import Settings from './pages/Settings';`);
363
+ subRoutes.push(` <Route path="settings" element={<Settings />} />`);
364
+ }
365
+ if (features.team) {
366
+ imports.push(`import Team from './pages/Team';`);
367
+ subRoutes.push(` <Route path="team" element={<Team />} />`);
368
+ }
369
+ if (features.apps) {
370
+ imports.push(`import Apps from './pages/Apps';`);
371
+ subRoutes.push(` <Route path="apps" element={<Apps />} />`);
372
+ }
373
+
374
+ routes.push(` <Route path="/dashboard" element={<DashboardLayout />}>`);
375
+ routes.push(...subRoutes);
376
+ routes.push(` </Route>`);
377
+ }
378
+
379
+ return `${imports.join('\n')}
380
+
381
+ export default function App() {
382
+ return (
383
+ <Routes>
384
+ ${routes.join('\n')}
385
+ </Routes>
386
+ );
387
+ }
388
+ `;
389
+ }
390
+
391
+ function reactDashboardLayout(features) {
392
+ const hasSidebar = features.settings || features.team || features.apps;
393
+ const sidebarImport = hasSidebar ? `\nimport Sidebar from '../components/Sidebar';` : '';
394
+ const sidebarJsx = hasSidebar
395
+ ? `\n <Sidebar />\n <main style={{ flex: 1, padding: '2rem', overflow: 'auto' }}>`
396
+ : `\n <main style={{ padding: '2rem' }}>`;
397
+
398
+ return `import { Outlet } from 'react-router-dom';
399
+ import { ProtectedRoute, UserButton, OrgSwitcher } from '@githat/nextjs';
400
+ import VerifyEmailBanner from '../components/VerifyEmailBanner';${sidebarImport}
401
+
402
+ export default function DashboardLayout() {
403
+ return (
404
+ <ProtectedRoute>
405
+ <div style={{ minHeight: '100vh', background: '#09090b', display: 'flex', flexDirection: 'column' }}>
406
+ <VerifyEmailBanner />
407
+ <header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 2rem', borderBottom: '1px solid #1e1e2e' }}>
408
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
409
+ <h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: '#fafafa' }}>Dashboard</h2>
410
+ <OrgSwitcher />
411
+ </div>
412
+ <UserButton />
413
+ </header>
414
+ <div style={{ display: 'flex', flex: 1 }}>${sidebarJsx}
415
+ <Outlet />
416
+ </main>
417
+ </div>
418
+ </div>
419
+ </ProtectedRoute>
420
+ );
421
+ }
422
+ `;
423
+ }
424
+
425
+ // ─── README Generator ────────────────────────────────────────────────
426
+
427
+ function generateReadme(projectName, framework, features) {
428
+ const isNext = framework !== 'react';
429
+ const port = isNext ? '3000' : '5173';
430
+ const devCmd = isNext ? 'npm run dev' : 'npm run dev';
431
+ const envFile = isNext ? '.env.local' : '.env';
432
+ const envVar = isNext ? 'NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY' : 'VITE_GITHAT_PUBLISHABLE_KEY';
433
+
434
+ const routes = ['- `/sign-in` — Sign in', '- `/sign-up` — Create account'];
435
+ if (features.forgotPassword) routes.push('- `/forgot-password` — Password recovery');
436
+ if (features.dashboard) routes.push('- `/dashboard` — Protected dashboard');
437
+ if (features.settings) routes.push('- `/dashboard/settings` — Profile, avatar, password');
438
+ if (features.team) routes.push('- `/dashboard/team` — Invite members, manage roles');
439
+ if (features.apps) routes.push('- `/dashboard/apps` — Create apps, manage API keys');
440
+
441
+ return `# ${projectName}
442
+
443
+ Built with [GitHat](https://githat.io) — authentication, teams, and API keys out of the box.
444
+
445
+ ## Getting Started
446
+
447
+ 1. Install dependencies:
448
+
449
+ \`\`\`bash
450
+ npm install
451
+ \`\`\`
452
+
453
+ 2. Add your GitHat publishable key to \`${envFile}\`:
454
+
455
+ \`\`\`env
456
+ ${envVar}=pk_live_your_key_here
457
+ \`\`\`
458
+
459
+ Get your key at [https://githat.io/dashboard/apps](https://githat.io/dashboard/apps).
460
+
461
+ 3. Start the dev server:
462
+
463
+ \`\`\`bash
464
+ ${devCmd}
465
+ \`\`\`
466
+
467
+ 4. Open [http://localhost:${port}](http://localhost:${port})
468
+
469
+ ## Pages
470
+
471
+ ${routes.join('\n')}
472
+
473
+ ## GitHat SDK
474
+
475
+ This project uses [\`@githat/nextjs\`](https://www.npmjs.com/package/@githat/nextjs) for authentication. Key imports:
476
+
477
+ \`\`\`tsx
478
+ import { useAuth, UserButton, OrgSwitcher } from '@githat/nextjs';
479
+
480
+ const { user, org, signIn, signUp, signOut } = useAuth();
481
+ \`\`\`
482
+
483
+ See the [SDK docs](https://www.npmjs.com/package/@githat/nextjs) for the full API.
484
+
485
+ ## Deploy
486
+
487
+ ${isNext ? `### Vercel (recommended)
488
+
489
+ \`\`\`bash
490
+ npx vercel
491
+ \`\`\`
492
+
493
+ Add \`${envVar}\` to your Vercel project environment variables.` : `### Vercel
494
+
495
+ \`\`\`bash
496
+ npx vercel
497
+ \`\`\`
498
+
499
+ ### Netlify
500
+
501
+ \`\`\`bash
502
+ npx netlify deploy
503
+ \`\`\`
504
+
505
+ Add \`${envVar}\` to your hosting provider's environment variables.`}
506
+
507
+ ## Learn More
508
+
509
+ - [GitHat Documentation](https://githat.io/docs)
510
+ - [SDK Reference](https://www.npmjs.com/package/@githat/nextjs)
511
+ - [API Reference](https://githat.io/docs/api)
512
+ `;
513
+ }
514
+
515
+ // ─── Helpers ─────────────────────────────────────────────────────────
516
+
517
+ function writeFile(root, relativePath, content) {
518
+ const fullPath = path.join(root, relativePath);
519
+ const dir = path.dirname(fullPath);
520
+ try {
521
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
522
+ fs.writeFileSync(fullPath, content, 'utf-8');
523
+ } catch (err) {
524
+ console.error(`\n ${c.red}✗${c.reset} Failed to write ${relativePath}: ${err.message}\n`);
525
+ process.exit(1);
526
+ }
527
+ }
528
+
529
+ module.exports = { scaffold };
@@ -0,0 +1,97 @@
1
+ // Shared component templates used by both Next.js and React scaffolds.
2
+
3
+ function verifyEmailBanner() {
4
+ return `'use client';
5
+
6
+ import { useState } from 'react';
7
+ import { useAuth, useGitHat } from '@githat/nextjs';
8
+
9
+ export default function VerifyEmailBanner() {
10
+ const { user } = useAuth();
11
+ const { fetch: apiFetch } = useGitHat();
12
+ const [sending, setSending] = useState(false);
13
+ const [sent, setSent] = useState(false);
14
+ const [dismissed, setDismissed] = useState(false);
15
+
16
+ if (!user || user.emailVerified || dismissed) return null;
17
+
18
+ const resend = async () => {
19
+ setSending(true);
20
+ try {
21
+ await apiFetch('/auth/resend-verification', {
22
+ method: 'POST',
23
+ body: JSON.stringify({ email: user.email }),
24
+ });
25
+ setSent(true);
26
+ } catch {}
27
+ setSending(false);
28
+ };
29
+
30
+ return (
31
+ <div style={{
32
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
33
+ padding: '0.75rem 2rem', background: '#1e1b4b', borderBottom: '1px solid #312e81',
34
+ fontSize: '0.875rem', color: '#c7d2fe',
35
+ }}>
36
+ <span>
37
+ Please verify your email <strong style={{ color: '#fafafa' }}>{user.email}</strong>
38
+ {sent
39
+ ? <span style={{ marginLeft: '0.5rem', color: '#34d399' }}>— Check your inbox!</span>
40
+ : <button type="button" onClick={resend} disabled={sending} style={{
41
+ marginLeft: '0.5rem', background: 'none', border: 'none', color: '#818cf8',
42
+ cursor: 'pointer', textDecoration: 'underline', fontSize: '0.875rem',
43
+ }}>{sending ? 'Sending...' : 'Resend'}</button>
44
+ }
45
+ </span>
46
+ <button type="button" onClick={() => setDismissed(true)} style={{
47
+ background: 'none', border: 'none', color: '#6366f1', cursor: 'pointer', fontSize: '1rem',
48
+ }}>×</button>
49
+ </div>
50
+ );
51
+ }
52
+ `;
53
+ }
54
+
55
+ function sidebar(features) {
56
+ const links = [{ href: '/dashboard', label: 'Dashboard', icon: 'home' }];
57
+ if (features.settings) links.push({ href: '/dashboard/settings', label: 'Settings', icon: 'settings' });
58
+ if (features.team) links.push({ href: '/dashboard/team', label: 'Team', icon: 'team' });
59
+ if (features.apps) links.push({ href: '/dashboard/apps', label: 'Apps', icon: 'apps' });
60
+
61
+ const icons = {
62
+ home: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
63
+ settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
64
+ team: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
65
+ apps: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`,
66
+ };
67
+
68
+ const linkItems = links.map((l) => {
69
+ return ` <a href="${l.href}" style={{
70
+ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem',
71
+ borderRadius: '0.375rem', color: '#a1a1aa', fontSize: '0.875rem', textDecoration: 'none',
72
+ transition: 'background 0.15s',
73
+ }} onMouseEnter={(e) => { e.currentTarget.style.background = '#27272a'; e.currentTarget.style.color = '#fafafa'; }}
74
+ onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#a1a1aa'; }}>
75
+ ${icons[l.icon]}
76
+ ${l.label}
77
+ </a>`;
78
+ }).join('\n');
79
+
80
+ return `export default function Sidebar() {
81
+ return (
82
+ <nav style={{
83
+ width: '14rem', padding: '1rem 0.75rem',
84
+ borderRight: '1px solid #27272a', background: '#0f0f14',
85
+ display: 'flex', flexDirection: 'column', gap: '0.25rem',
86
+ }}>
87
+ ${linkItems}
88
+ </nav>
89
+ );
90
+ }
91
+ `;
92
+ }
93
+
94
+ module.exports = {
95
+ verifyEmailBanner,
96
+ sidebar,
97
+ };