create-githat-app 1.0.3 → 1.0.5

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 (44) hide show
  1. package/dist/cli.js +283 -123
  2. package/package.json +1 -1
  3. package/templates/fullstack/apps-api-express/.env.example.hbs +6 -0
  4. package/templates/fullstack/apps-api-express/.env.local.hbs +6 -0
  5. package/templates/fullstack/apps-api-express/package.json.hbs +24 -0
  6. package/templates/fullstack/apps-api-express/src/index.ts.hbs +41 -0
  7. package/templates/fullstack/apps-api-express/src/routes/health.ts.hbs +11 -0
  8. package/templates/fullstack/apps-api-express/src/routes/users.ts.hbs +43 -0
  9. package/templates/fullstack/apps-api-express/tsconfig.json.hbs +16 -0
  10. package/templates/fullstack/apps-api-fastify/.env.example.hbs +6 -0
  11. package/templates/fullstack/apps-api-fastify/.env.local.hbs +6 -0
  12. package/templates/fullstack/apps-api-fastify/package.json.hbs +22 -0
  13. package/templates/fullstack/apps-api-fastify/src/index.ts.hbs +28 -0
  14. package/templates/fullstack/apps-api-fastify/src/routes/health.ts.hbs +11 -0
  15. package/templates/fullstack/apps-api-fastify/src/routes/users.ts.hbs +43 -0
  16. package/templates/fullstack/apps-api-fastify/tsconfig.json.hbs +16 -0
  17. package/templates/fullstack/apps-api-hono/.env.example.hbs +6 -0
  18. package/templates/fullstack/apps-api-hono/.env.local.hbs +6 -0
  19. package/templates/fullstack/apps-api-hono/package.json.hbs +22 -0
  20. package/templates/fullstack/apps-api-hono/src/index.ts.hbs +35 -0
  21. package/templates/fullstack/apps-api-hono/src/routes/health.ts.hbs +11 -0
  22. package/templates/fullstack/apps-api-hono/src/routes/users.ts.hbs +43 -0
  23. package/templates/fullstack/apps-api-hono/tsconfig.json.hbs +16 -0
  24. package/templates/fullstack/apps-web-nextjs/.env.example.hbs +5 -0
  25. package/templates/fullstack/apps-web-nextjs/.env.local.hbs +5 -0
  26. package/templates/fullstack/apps-web-nextjs/app/(auth)/forgot-password/page.tsx.hbs +11 -0
  27. package/templates/fullstack/apps-web-nextjs/app/(auth)/reset-password/page.tsx.hbs +39 -0
  28. package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-in/page.tsx.hbs +9 -0
  29. package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-up/page.tsx.hbs +9 -0
  30. package/templates/fullstack/apps-web-nextjs/app/(auth)/verify-email/page.tsx.hbs +11 -0
  31. package/templates/fullstack/apps-web-nextjs/app/dashboard/layout.tsx.hbs +15 -0
  32. package/templates/fullstack/apps-web-nextjs/app/dashboard/page.tsx.hbs +27 -0
  33. package/templates/fullstack/apps-web-nextjs/app/globals.css.hbs +20 -0
  34. package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +26 -0
  35. package/templates/fullstack/apps-web-nextjs/app/page.tsx.hbs +18 -0
  36. package/templates/fullstack/apps-web-nextjs/next.config.ts.hbs +15 -0
  37. package/templates/fullstack/apps-web-nextjs/package.json.hbs +33 -0
  38. package/templates/fullstack/apps-web-nextjs/postcss.config.mjs.hbs +9 -0
  39. package/templates/fullstack/apps-web-nextjs/tsconfig.json.hbs +21 -0
  40. package/templates/fullstack/root/.gitignore.hbs +42 -0
  41. package/templates/fullstack/root/githat.yaml.hbs +17 -0
  42. package/templates/fullstack/root/package.json.hbs +15 -0
  43. package/templates/fullstack/root/turbo.json.hbs +20 -0
  44. package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +10 -2
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from 'module'; const require = createRequire(import.met
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command as Command7 } from "commander";
5
- import * as p9 from "@clack/prompts";
5
+ import * as p11 from "@clack/prompts";
6
6
  import chalk9 from "chalk";
7
7
 
8
8
  // src/utils/ascii.ts
@@ -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.3";
14
+ var VERSION = "1.0.5";
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"];
@@ -73,6 +73,48 @@ var DEPS = {
73
73
  "drizzle-sqlite": {
74
74
  dependencies: { "drizzle-orm": "^0.39.0", "better-sqlite3": "^11.0.0" },
75
75
  devDependencies: { "drizzle-kit": "^0.30.0", "@types/better-sqlite3": "^7.6.0" }
76
+ },
77
+ // Backend frameworks for fullstack
78
+ hono: {
79
+ dependencies: {
80
+ hono: "^4.6.0",
81
+ "@hono/node-server": "^1.13.0"
82
+ },
83
+ devDependencies: {
84
+ typescript: "^5.9.0",
85
+ "@types/node": "^22.0.0",
86
+ tsx: "^4.19.0"
87
+ }
88
+ },
89
+ express: {
90
+ dependencies: {
91
+ express: "^5.0.0",
92
+ cors: "^2.8.5"
93
+ },
94
+ devDependencies: {
95
+ typescript: "^5.9.0",
96
+ "@types/node": "^22.0.0",
97
+ "@types/express": "^5.0.0",
98
+ "@types/cors": "^2.8.17",
99
+ tsx: "^4.19.0"
100
+ }
101
+ },
102
+ fastify: {
103
+ dependencies: {
104
+ fastify: "^5.2.0",
105
+ "@fastify/cors": "^10.0.0"
106
+ },
107
+ devDependencies: {
108
+ typescript: "^5.9.0",
109
+ "@types/node": "^22.0.0",
110
+ tsx: "^4.19.0"
111
+ }
112
+ },
113
+ // Turborepo for fullstack monorepo
114
+ turbo: {
115
+ devDependencies: {
116
+ turbo: "^2.3.0"
117
+ }
76
118
  }
77
119
  };
78
120
 
@@ -123,35 +165,59 @@ function sectionHeader(title) {
123
165
  console.log(dim(` \u2500\u2500\u2500 ${title} ${"\u2500".repeat(lineLen)}`));
124
166
  console.log("");
125
167
  }
126
- function displaySuccess(projectName, packageManager, framework, hasPublishableKey = true) {
168
+ function displaySuccess(projectName, packageManager, framework, hasPublishableKey = true, isFullstack = false) {
127
169
  const devCmd = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
128
170
  const port = framework === "react-vite" ? "5173" : "3000";
129
171
  console.log("");
130
- drawBox([
131
- `${violet("\u2726")} Your GitHat app is ready!`,
132
- "",
133
- `${violet("$")} cd ${projectName}`,
134
- `${violet("$")} ${devCmd}`,
135
- "",
136
- dim(`\u2192 http://localhost:${port}`),
137
- "",
138
- chalk.bold("Routes"),
139
- `${violet("/sign-in")} Sign in`,
140
- `${violet("/sign-up")} Create account`,
141
- `${violet("/dashboard")} Protected dashboard`,
142
- "",
143
- ...hasPublishableKey ? [] : [
144
- chalk.yellow("No key configured \u2014 auth works on localhost."),
145
- `For production: ${violet("githat.io/dashboard/apps")}`,
146
- ""
147
- ],
148
- dim("Docs \u2192 https://githat.io/docs/sdk")
149
- ]);
172
+ if (isFullstack) {
173
+ drawBox([
174
+ `${violet("\u2726")} Your GitHat fullstack app is ready!`,
175
+ "",
176
+ `${violet("$")} cd ${projectName}`,
177
+ `${violet("$")} ${devCmd}`,
178
+ "",
179
+ dim(`\u2192 Web: http://localhost:3000`),
180
+ dim(`\u2192 API: http://localhost:3001`),
181
+ "",
182
+ chalk.bold("Structure"),
183
+ `${violet("apps/web")} Next.js frontend`,
184
+ `${violet("apps/api")} API backend`,
185
+ `${violet("packages/")} Shared code`,
186
+ "",
187
+ ...hasPublishableKey ? [] : [
188
+ chalk.yellow("No key configured \u2014 auth works on localhost."),
189
+ `For production: ${violet("githat.io/dashboard/apps")}`,
190
+ ""
191
+ ],
192
+ dim("Docs \u2192 https://githat.io/docs/sdk")
193
+ ]);
194
+ } else {
195
+ drawBox([
196
+ `${violet("\u2726")} Your GitHat app is ready!`,
197
+ "",
198
+ `${violet("$")} cd ${projectName}`,
199
+ `${violet("$")} ${devCmd}`,
200
+ "",
201
+ dim(`\u2192 http://localhost:${port}`),
202
+ "",
203
+ chalk.bold("Routes"),
204
+ `${violet("/sign-in")} Sign in`,
205
+ `${violet("/sign-up")} Create account`,
206
+ `${violet("/dashboard")} Protected dashboard`,
207
+ "",
208
+ ...hasPublishableKey ? [] : [
209
+ chalk.yellow("No key configured \u2014 auth works on localhost."),
210
+ `For production: ${violet("githat.io/dashboard/apps")}`,
211
+ ""
212
+ ],
213
+ dim("Docs \u2192 https://githat.io/docs/sdk")
214
+ ]);
215
+ }
150
216
  console.log("");
151
217
  }
152
218
 
153
219
  // src/prompts/index.ts
154
- import * as p6 from "@clack/prompts";
220
+ import * as p8 from "@clack/prompts";
155
221
 
156
222
  // src/prompts/project.ts
157
223
  import * as p from "@clack/prompts";
@@ -204,8 +270,41 @@ async function promptProject(initialName) {
204
270
  };
205
271
  }
206
272
 
207
- // src/prompts/framework.ts
273
+ // src/prompts/project-type.ts
208
274
  import * as p2 from "@clack/prompts";
275
+ async function promptProjectType() {
276
+ const answers = await p2.group(
277
+ {
278
+ projectType: () => p2.select({
279
+ message: "Project type",
280
+ options: [
281
+ {
282
+ value: "frontend",
283
+ label: "Frontend only",
284
+ hint: "Next.js with API routes"
285
+ },
286
+ {
287
+ value: "fullstack",
288
+ label: "Fullstack",
289
+ hint: "Next.js + separate API (Turborepo)"
290
+ }
291
+ ]
292
+ })
293
+ },
294
+ {
295
+ onCancel: () => {
296
+ p2.cancel("Setup cancelled.");
297
+ process.exit(0);
298
+ }
299
+ }
300
+ );
301
+ return {
302
+ projectType: answers.projectType
303
+ };
304
+ }
305
+
306
+ // src/prompts/framework.ts
307
+ import * as p3 from "@clack/prompts";
209
308
 
210
309
  // src/utils/package-manager.ts
211
310
  function detectPackageManager() {
@@ -228,16 +327,16 @@ function getInstallCommand(pm) {
228
327
  // src/prompts/framework.ts
229
328
  async function promptFramework(typescriptOverride) {
230
329
  const packageManager = detectPackageManager();
231
- const answers = await p2.group(
330
+ const answers = await p3.group(
232
331
  {
233
- framework: () => p2.select({
332
+ framework: () => p3.select({
234
333
  message: "Framework",
235
334
  options: [
236
335
  { value: "nextjs", label: "Next.js 16", hint: "App Router \xB7 SSR \xB7 middleware auth" },
237
336
  { value: "react-vite", label: "React 19 + Vite 7", hint: "SPA \xB7 client-side routing" }
238
337
  ]
239
338
  }),
240
- typescript: () => typescriptOverride !== void 0 ? Promise.resolve(typescriptOverride) : p2.select({
339
+ typescript: () => typescriptOverride !== void 0 ? Promise.resolve(typescriptOverride) : p3.select({
241
340
  message: "Language",
242
341
  options: [
243
342
  { value: true, label: "TypeScript", hint: "recommended" },
@@ -247,7 +346,7 @@ async function promptFramework(typescriptOverride) {
247
346
  },
248
347
  {
249
348
  onCancel: () => {
250
- p2.cancel("Setup cancelled.");
349
+ p3.cancel("Setup cancelled.");
251
350
  process.exit(0);
252
351
  }
253
352
  }
@@ -259,62 +358,74 @@ async function promptFramework(typescriptOverride) {
259
358
  };
260
359
  }
261
360
 
262
- // src/prompts/githat.ts
263
- import { execSync } from "child_process";
264
- import * as p3 from "@clack/prompts";
265
- function openBrowser(url) {
266
- try {
267
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
268
- execSync(`${cmd} "${url}"`, { stdio: "ignore" });
269
- } catch {
270
- }
361
+ // src/prompts/backend.ts
362
+ import * as p4 from "@clack/prompts";
363
+ async function promptBackend() {
364
+ const answers = await p4.group(
365
+ {
366
+ backendFramework: () => p4.select({
367
+ message: "Backend framework",
368
+ options: [
369
+ {
370
+ value: "hono",
371
+ label: "Hono",
372
+ hint: "recommended \xB7 serverless-native \xB7 14KB"
373
+ },
374
+ {
375
+ value: "express",
376
+ label: "Express",
377
+ hint: "classic Node.js \xB7 large ecosystem"
378
+ },
379
+ {
380
+ value: "fastify",
381
+ label: "Fastify",
382
+ hint: "high performance \xB7 schema validation"
383
+ }
384
+ ]
385
+ })
386
+ },
387
+ {
388
+ onCancel: () => {
389
+ p4.cancel("Setup cancelled.");
390
+ process.exit(0);
391
+ }
392
+ }
393
+ );
394
+ return {
395
+ backendFramework: answers.backendFramework
396
+ };
271
397
  }
398
+
399
+ // src/prompts/githat.ts
400
+ import * as p5 from "@clack/prompts";
272
401
  async function promptGitHat(existingKey) {
273
402
  let publishableKey = existingKey || "";
274
403
  if (!publishableKey) {
275
- const connectChoice = await p3.select({
404
+ const connectChoice = await p5.select({
276
405
  message: "Connect to GitHat",
277
406
  options: [
278
- { value: "browser", label: "Sign in with browser", hint: "opens githat.io \u2014 recommended" },
279
- { value: "paste", label: "I have a key", hint: "paste your pk_live_... key" },
280
- { value: "skip", label: "Skip for now", hint: "add key to .env later" }
407
+ { value: "skip", label: "Skip for now", hint: "auth works on localhost \u2014 add key later" },
408
+ { value: "paste", label: "I have a key", hint: "paste your pk_live_... key" }
281
409
  ]
282
410
  });
283
- if (p3.isCancel(connectChoice)) {
284
- p3.cancel("Setup cancelled.");
411
+ if (p5.isCancel(connectChoice)) {
412
+ p5.cancel("Setup cancelled.");
285
413
  process.exit(0);
286
414
  }
287
- if (connectChoice === "browser") {
288
- p3.log.step("Opening githat.io in your browser...");
289
- openBrowser("https://githat.io/sign-up");
290
- p3.log.info("Sign up (or sign in), then go to Dashboard \u2192 Apps to copy your key.");
291
- const pastedKey = await p3.text({
292
- message: "Paste your publishable key",
293
- placeholder: "pk_live_...",
294
- validate: validatePublishableKey
295
- });
296
- if (p3.isCancel(pastedKey)) {
297
- p3.cancel("Setup cancelled.");
298
- process.exit(0);
299
- }
300
- publishableKey = pastedKey || "";
301
- } else if (connectChoice === "paste") {
302
- const pastedKey = await p3.text({
415
+ if (connectChoice === "paste") {
416
+ const pastedKey = await p5.text({
303
417
  message: "Publishable key",
304
418
  placeholder: `pk_live_... (get one at ${DASHBOARD_URL})`,
305
419
  validate: validatePublishableKey
306
420
  });
307
- if (p3.isCancel(pastedKey)) {
308
- p3.cancel("Setup cancelled.");
421
+ if (p5.isCancel(pastedKey)) {
422
+ p5.cancel("Setup cancelled.");
309
423
  process.exit(0);
310
424
  }
311
425
  publishableKey = pastedKey || "";
312
- } else if (connectChoice === "skip") {
313
- p3.log.info("Auth works on localhost without a key (CORS bypass for development).");
314
- p3.log.info("Sign up at githat.io \u2014 a publishable key is auto-created for you.");
315
426
  }
316
427
  }
317
- const authFeatures = await p3.multiselect({
428
+ const authFeatures = await p5.multiselect({
318
429
  message: "Auth features",
319
430
  options: [
320
431
  { value: "forgot-password", label: "Forgot password", hint: "reset via email" },
@@ -326,8 +437,8 @@ async function promptGitHat(existingKey) {
326
437
  initialValues: ["forgot-password"],
327
438
  required: false
328
439
  });
329
- if (p3.isCancel(authFeatures)) {
330
- p3.cancel("Setup cancelled.");
440
+ if (p5.isCancel(authFeatures)) {
441
+ p5.cancel("Setup cancelled.");
331
442
  process.exit(0);
332
443
  }
333
444
  return {
@@ -338,11 +449,11 @@ async function promptGitHat(existingKey) {
338
449
  }
339
450
 
340
451
  // src/prompts/features.ts
341
- import * as p4 from "@clack/prompts";
452
+ import * as p6 from "@clack/prompts";
342
453
  async function promptFeatures() {
343
- const answers = await p4.group(
454
+ const answers = await p6.group(
344
455
  {
345
- databaseChoice: () => p4.select({
456
+ databaseChoice: () => p6.select({
346
457
  message: "Database",
347
458
  options: [
348
459
  { value: "none", label: "None", hint: "use GitHat backend directly" },
@@ -352,12 +463,12 @@ async function promptFeatures() {
352
463
  { value: "drizzle-sqlite", label: "Drizzle + SQLite" }
353
464
  ]
354
465
  }),
355
- useTailwind: () => p4.confirm({ message: "Tailwind CSS?", initialValue: true }),
356
- includeDashboard: () => p4.confirm({ message: "Include dashboard?", initialValue: true })
466
+ useTailwind: () => p6.confirm({ message: "Tailwind CSS?", initialValue: true }),
467
+ includeDashboard: () => p6.confirm({ message: "Include dashboard?", initialValue: true })
357
468
  },
358
469
  {
359
470
  onCancel: () => {
360
- p4.cancel("Setup cancelled.");
471
+ p6.cancel("Setup cancelled.");
361
472
  process.exit(0);
362
473
  }
363
474
  }
@@ -371,14 +482,14 @@ async function promptFeatures() {
371
482
  }
372
483
 
373
484
  // src/prompts/finalize.ts
374
- import * as p5 from "@clack/prompts";
485
+ import * as p7 from "@clack/prompts";
375
486
  async function promptFinalize() {
376
- const installDeps = await p5.confirm({
487
+ const installDeps = await p7.confirm({
377
488
  message: "Install dependencies?",
378
489
  initialValue: true
379
490
  });
380
- if (p5.isCancel(installDeps)) {
381
- p5.cancel("Setup cancelled.");
491
+ if (p7.isCancel(installDeps)) {
492
+ p7.cancel("Setup cancelled.");
382
493
  process.exit(0);
383
494
  }
384
495
  return {
@@ -391,12 +502,15 @@ async function promptFinalize() {
391
502
  function toDisplayName(projectName) {
392
503
  return projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
393
504
  }
394
- function getDefaults(projectName, publishableKey, typescript) {
505
+ function getDefaults(projectName, publishableKey, typescript, fullstack, backendFramework) {
395
506
  const displayName = toDisplayName(projectName);
507
+ const projectType = fullstack ? "fullstack" : "frontend";
396
508
  return {
397
509
  projectName,
398
510
  businessName: displayName,
399
511
  description: `${displayName} \u2014 Built with GitHat`,
512
+ projectType,
513
+ backendFramework: fullstack ? backendFramework || "hono" : void 0,
400
514
  framework: "nextjs",
401
515
  typescript: typescript ?? true,
402
516
  packageManager: detectPackageManager(),
@@ -406,28 +520,33 @@ function getDefaults(projectName, publishableKey, typescript) {
406
520
  databaseChoice: "none",
407
521
  useTailwind: true,
408
522
  includeDashboard: true,
409
- includeGithatFolder: true,
523
+ includeGithatFolder: projectType === "frontend",
410
524
  initGit: true,
411
525
  installDeps: true
412
526
  };
413
527
  }
414
528
  async function runPrompts(args) {
415
529
  if (args.yes && args.initialName) {
416
- p6.log.info("Using defaults (--yes flag)");
417
- return getDefaults(args.initialName, args.publishableKey, args.typescript);
530
+ p8.log.info("Using defaults (--yes flag)");
531
+ return getDefaults(args.initialName, args.publishableKey, args.typescript, args.fullstack, args.backendFramework);
418
532
  }
419
- p6.intro("Let\u2019s set up your GitHat app");
533
+ p8.intro("Let's set up your GitHat app");
420
534
  sectionHeader("Project");
421
535
  const project = await promptProject(args.initialName);
536
+ const projectType = await promptProjectType();
422
537
  sectionHeader("Stack");
423
538
  const framework = await promptFramework(args.typescript);
539
+ let backend = {};
540
+ if (projectType.projectType === "fullstack") {
541
+ backend = await promptBackend();
542
+ }
424
543
  sectionHeader("Connect");
425
544
  const githat = await promptGitHat(args.publishableKey);
426
545
  sectionHeader("Features");
427
546
  const features = await promptFeatures();
428
547
  sectionHeader("Finish");
429
548
  const finalize = await promptFinalize();
430
- return { ...project, ...framework, ...githat, ...features, ...finalize };
549
+ return { ...project, ...projectType, ...framework, ...backend, ...githat, ...features, ...finalize };
431
550
  }
432
551
  function answersToContext(answers) {
433
552
  return {
@@ -439,6 +558,9 @@ function answersToContext(answers) {
439
558
  packageManager: answers.packageManager,
440
559
  publishableKey: answers.publishableKey,
441
560
  apiUrl: answers.apiUrl,
561
+ // Project type
562
+ projectType: answers.projectType,
563
+ backendFramework: answers.backendFramework,
442
564
  useDatabase: answers.databaseChoice !== "none",
443
565
  databaseChoice: answers.databaseChoice,
444
566
  useTailwind: answers.useTailwind,
@@ -457,8 +579,8 @@ function answersToContext(answers) {
457
579
  // src/scaffold/index.ts
458
580
  import fs3 from "fs-extra";
459
581
  import path3 from "path";
460
- import { execSync as execSync3 } from "child_process";
461
- import * as p7 from "@clack/prompts";
582
+ import { execSync as execSync2 } from "child_process";
583
+ import * as p9 from "@clack/prompts";
462
584
  import chalk2 from "chalk";
463
585
 
464
586
  // src/utils/template-engine.ts
@@ -592,12 +714,12 @@ async function withSpinner(text4, fn, successText) {
592
714
  }
593
715
 
594
716
  // src/utils/git.ts
595
- import { execSync as execSync2 } from "child_process";
717
+ import { execSync } from "child_process";
596
718
  function initGit(cwd) {
597
719
  try {
598
- execSync2("git init", { cwd, stdio: "ignore" });
599
- execSync2("git add -A", { cwd, stdio: "ignore" });
600
- execSync2('git commit -m "Initial commit from create-githat-app"', {
720
+ execSync("git init", { cwd, stdio: "ignore" });
721
+ execSync("git add -A", { cwd, stdio: "ignore" });
722
+ execSync('git commit -m "Initial commit from create-githat-app"', {
601
723
  cwd,
602
724
  stdio: "ignore"
603
725
  });
@@ -611,26 +733,33 @@ function initGit(cwd) {
611
733
  async function scaffold(context, options) {
612
734
  const root = path3.resolve(process.cwd(), context.projectName);
613
735
  if (fs3.existsSync(root)) {
614
- p7.cancel(`Directory "${context.projectName}" already exists.`);
736
+ p9.cancel(`Directory "${context.projectName}" already exists.`);
615
737
  process.exit(1);
616
738
  }
739
+ const isFullstack = context.projectType === "fullstack";
617
740
  await withSpinner("Creating project structure...", async () => {
618
741
  fs3.ensureDirSync(root);
619
742
  const templatesRoot = getTemplatesRoot();
620
- const frameworkDir = path3.join(templatesRoot, context.framework);
621
- if (!fs3.existsSync(frameworkDir)) {
622
- throw new Error(`Templates not found at ${frameworkDir}. This is a bug \u2014 please report it.`);
623
- }
624
- renderTemplateDirectory(frameworkDir, root, context);
625
- const baseDir = path3.join(templatesRoot, "base");
626
- if (fs3.existsSync(baseDir)) {
627
- renderTemplateDirectory(baseDir, root, context);
743
+ if (isFullstack) {
744
+ scaffoldFullstack(templatesRoot, root, context);
745
+ } else {
746
+ const frameworkDir = path3.join(templatesRoot, context.framework);
747
+ if (!fs3.existsSync(frameworkDir)) {
748
+ throw new Error(`Templates not found at ${frameworkDir}. This is a bug \u2014 please report it.`);
749
+ }
750
+ renderTemplateDirectory(frameworkDir, root, context);
751
+ const baseDir = path3.join(templatesRoot, "base");
752
+ if (fs3.existsSync(baseDir)) {
753
+ renderTemplateDirectory(baseDir, root, context);
754
+ }
628
755
  }
629
756
  }, "Project structure created");
630
- await withSpinner("Generating package.json...", async () => {
631
- const pkg = buildPackageJson(context);
632
- writeJson(root, "package.json", pkg);
633
- }, "package.json generated");
757
+ if (!isFullstack) {
758
+ await withSpinner("Generating package.json...", async () => {
759
+ const pkg = buildPackageJson(context);
760
+ writeJson(root, "package.json", pkg);
761
+ }, "package.json generated");
762
+ }
634
763
  if (options.initGit) {
635
764
  const gitSpinner = createSpinner("Initializing git repository...");
636
765
  gitSpinner.start();
@@ -647,36 +776,65 @@ async function scaffold(context, options) {
647
776
  `Installing dependencies with ${context.packageManager}...`,
648
777
  async () => {
649
778
  try {
650
- execSync3(installCmd, { cwd: root, stdio: "ignore", timeout: 12e4 });
779
+ execSync2(installCmd, { cwd: root, stdio: "ignore", timeout: 12e4 });
651
780
  } catch (err) {
652
781
  const msg = err.message || "";
653
782
  if (msg.includes("TIMEOUT")) {
654
- p7.log.warn(`Install timed out. Run ${chalk2.cyan(installCmd)} manually.`);
783
+ p9.log.warn(`Install timed out. Run ${chalk2.cyan(installCmd)} manually.`);
655
784
  } else {
656
- p7.log.warn(`Could not auto-install. Run ${chalk2.cyan(installCmd)} manually.`);
785
+ p9.log.warn(`Could not auto-install. Run ${chalk2.cyan(installCmd)} manually.`);
657
786
  }
658
787
  }
659
788
  },
660
789
  "Dependencies installed"
661
790
  );
662
791
  }
663
- p7.outro("Setup complete!");
664
- displaySuccess(context.projectName, context.packageManager, context.framework, !!context.publishableKey);
792
+ p9.outro("Setup complete!");
793
+ displaySuccess(context.projectName, context.packageManager, context.framework, !!context.publishableKey, isFullstack);
665
794
  if (!options.skipPrompts) {
666
- const starPrompt = await p7.confirm({
795
+ const starPrompt = await p9.confirm({
667
796
  message: "Star GitHat on GitHub? (helps us grow!)",
668
797
  initialValue: false
669
798
  });
670
- if (!p7.isCancel(starPrompt) && starPrompt) {
799
+ if (!p9.isCancel(starPrompt) && starPrompt) {
671
800
  try {
672
801
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
673
- execSync3(`${cmd} "https://github.com/GitHat-IO/githat"`, { stdio: "ignore" });
802
+ execSync2(`${cmd} "https://github.com/GitHat-IO/githat"`, { stdio: "ignore" });
674
803
  } catch {
675
- p7.log.info("Visit https://github.com/GitHat-IO/githat to star us!");
804
+ p9.log.info("Visit https://github.com/GitHat-IO/githat to star us!");
676
805
  }
677
806
  }
678
807
  }
679
808
  }
809
+ function scaffoldFullstack(templatesRoot, root, context) {
810
+ const fullstackDir = path3.join(templatesRoot, "fullstack");
811
+ const rootDir = path3.join(fullstackDir, "root");
812
+ if (fs3.existsSync(rootDir)) {
813
+ renderTemplateDirectory(rootDir, root, context);
814
+ }
815
+ const appsDir = path3.join(root, "apps");
816
+ fs3.ensureDirSync(appsDir);
817
+ const webDir = path3.join(appsDir, "web");
818
+ fs3.ensureDirSync(webDir);
819
+ const webTemplateDir = path3.join(fullstackDir, `apps-web-${context.framework}`);
820
+ if (fs3.existsSync(webTemplateDir)) {
821
+ renderTemplateDirectory(webTemplateDir, webDir, context);
822
+ } else {
823
+ throw new Error(`Web app templates not found at ${webTemplateDir}. This is a bug \u2014 please report it.`);
824
+ }
825
+ const apiDir = path3.join(appsDir, "api");
826
+ fs3.ensureDirSync(apiDir);
827
+ const backendFramework = context.backendFramework || "hono";
828
+ const apiTemplateDir = path3.join(fullstackDir, `apps-api-${backendFramework}`);
829
+ if (fs3.existsSync(apiTemplateDir)) {
830
+ renderTemplateDirectory(apiTemplateDir, apiDir, context);
831
+ } else {
832
+ throw new Error(`API templates not found at ${apiTemplateDir}. This is a bug \u2014 please report it.`);
833
+ }
834
+ const packagesDir = path3.join(root, "packages");
835
+ fs3.ensureDirSync(packagesDir);
836
+ fs3.writeFileSync(path3.join(packagesDir, ".gitkeep"), "");
837
+ }
680
838
 
681
839
  // src/commands/skills/index.ts
682
840
  import { Command as Command6 } from "commander";
@@ -996,7 +1154,7 @@ import { Command as Command5 } from "commander";
996
1154
  import chalk7 from "chalk";
997
1155
  import * as fs6 from "fs";
998
1156
  import * as path6 from "path";
999
- import * as p8 from "@clack/prompts";
1157
+ import * as p10 from "@clack/prompts";
1000
1158
  var SKILL_TYPES = ["template", "integration", "ui", "ai", "workflow"];
1001
1159
  function generateReadme(manifest) {
1002
1160
  return `# ${manifest.name}
@@ -1144,7 +1302,7 @@ var initCommand = new Command5("init").description("Initialize a new skill packa
1144
1302
  if (options.type && SKILL_TYPES.includes(options.type)) {
1145
1303
  type = options.type;
1146
1304
  } else {
1147
- const result = await p8.select({
1305
+ const result = await p10.select({
1148
1306
  message: "What type of skill are you creating?",
1149
1307
  options: [
1150
1308
  { value: "integration", label: "Integration", hint: "Connect to external services (Stripe, SendGrid, etc.)" },
@@ -1154,19 +1312,19 @@ var initCommand = new Command5("init").description("Initialize a new skill packa
1154
1312
  { value: "workflow", label: "Workflow", hint: "Automation recipes" }
1155
1313
  ]
1156
1314
  });
1157
- if (p8.isCancel(result)) {
1158
- p8.cancel("Operation cancelled");
1315
+ if (p10.isCancel(result)) {
1316
+ p10.cancel("Operation cancelled");
1159
1317
  process.exit(0);
1160
1318
  }
1161
1319
  type = result;
1162
1320
  }
1163
- const description = await p8.text({
1321
+ const description = await p10.text({
1164
1322
  message: "Short description:",
1165
1323
  placeholder: `A ${type} skill for...`,
1166
1324
  validate: (v) => v.length < 10 ? "Description must be at least 10 characters" : void 0
1167
1325
  });
1168
- if (p8.isCancel(description)) {
1169
- p8.cancel("Operation cancelled");
1326
+ if (p10.isCancel(description)) {
1327
+ p10.cancel("Operation cancelled");
1170
1328
  process.exit(0);
1171
1329
  }
1172
1330
  const manifest = {
@@ -1254,19 +1412,21 @@ skillsCommand.action(() => {
1254
1412
  // src/cli.ts
1255
1413
  var program = new Command7();
1256
1414
  program.name("githat").description("GitHat CLI - Scaffold apps and manage skills").version(VERSION);
1257
- program.command("create [project-name]", { isDefault: true }).description("Scaffold a new GitHat app").option("--key <key>", "GitHat publishable key (pk_live_...)").option("--ts", "Use TypeScript (default)").option("--js", "Use JavaScript").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, opts) => {
1415
+ program.command("create [project-name]", { isDefault: true }).description("Scaffold a new GitHat app").option("--key <key>", "GitHat publishable key (pk_live_...)").option("--ts", "Use TypeScript (default)").option("--js", "Use JavaScript").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) => {
1258
1416
  try {
1259
1417
  displayBanner();
1260
1418
  const typescript = opts.js ? false : opts.ts ? true : void 0;
1261
1419
  if (opts.yes && !projectName) {
1262
- p9.cancel(chalk9.red("Project name is required when using --yes flag"));
1420
+ p11.cancel(chalk9.red("Project name is required when using --yes flag"));
1263
1421
  process.exit(1);
1264
1422
  }
1265
1423
  const answers = await runPrompts({
1266
1424
  initialName: projectName,
1267
1425
  publishableKey: opts.key,
1268
1426
  typescript,
1269
- yes: opts.yes
1427
+ yes: opts.yes,
1428
+ fullstack: opts.fullstack,
1429
+ backendFramework: opts.backend
1270
1430
  });
1271
1431
  const context = answersToContext(answers);
1272
1432
  await scaffold(context, {
@@ -1275,7 +1435,7 @@ program.command("create [project-name]", { isDefault: true }).description("Scaff
1275
1435
  skipPrompts: opts.yes
1276
1436
  });
1277
1437
  } catch (err) {
1278
- p9.cancel(chalk9.red(err.message || "Something went wrong."));
1438
+ p11.cancel(chalk9.red(err.message || "Something went wrong."));
1279
1439
  process.exit(1);
1280
1440
  }
1281
1441
  });