Skip to content

Commit 02ba932

Browse files
doble196claude
andcommitted
CLI v1.0.4 — fullstack project scaffolding
Add fullstack mode (Turborepo monorepo): - projectType prompt: frontend vs fullstack - backendFramework prompt: hono/express/fastify - Fullstack templates: root (turbo.json, githat.yaml), apps/web, apps/api - Hono, Express, and Fastify API templates - Next.js web app template for monorepo - Updated scaffold logic for monorepo structure - Updated success message for fullstack mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 40f7646 commit 02ba932

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1018
-40
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "create-githat-app",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "GitHat CLI — scaffold apps and manage the skills marketplace",
55
"type": "module",
66
"bin": {

src/constants.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const VERSION = '1.0.3';
1+
export const VERSION = '1.0.4';
22
export const DEFAULT_API_URL = 'https://api.githat.io';
33
export const DOCS_URL = 'https://githat.io/docs/sdk';
44
export const DASHBOARD_URL = 'https://githat.io/dashboard/apps';
@@ -64,4 +64,49 @@ export const DEPS = {
6464
dependencies: { 'drizzle-orm': '^0.39.0', 'better-sqlite3': '^11.0.0' },
6565
devDependencies: { 'drizzle-kit': '^0.30.0', '@types/better-sqlite3': '^7.6.0' },
6666
},
67+
// Backend frameworks for fullstack
68+
hono: {
69+
dependencies: {
70+
hono: '^4.6.0',
71+
'@hono/node-server': '^1.13.0',
72+
},
73+
devDependencies: {
74+
typescript: '^5.9.0',
75+
'@types/node': '^22.0.0',
76+
tsx: '^4.19.0',
77+
},
78+
},
79+
express: {
80+
dependencies: {
81+
express: '^5.0.0',
82+
cors: '^2.8.5',
83+
},
84+
devDependencies: {
85+
typescript: '^5.9.0',
86+
'@types/node': '^22.0.0',
87+
'@types/express': '^5.0.0',
88+
'@types/cors': '^2.8.17',
89+
tsx: '^4.19.0',
90+
},
91+
},
92+
fastify: {
93+
dependencies: {
94+
fastify: '^5.2.0',
95+
'@fastify/cors': '^10.0.0',
96+
},
97+
devDependencies: {
98+
typescript: '^5.9.0',
99+
'@types/node': '^22.0.0',
100+
tsx: '^4.19.0',
101+
},
102+
},
103+
// Turborepo for fullstack monorepo
104+
turbo: {
105+
devDependencies: {
106+
turbo: '^2.3.0',
107+
},
108+
},
67109
} as const;
110+
111+
export type ProjectType = 'frontend' | 'fullstack';
112+
export type BackendFramework = 'hono' | 'express' | 'fastify';

src/prompts/backend.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as p from '@clack/prompts';
2+
import type { BackendFramework } from '../constants.js';
3+
4+
export interface BackendAnswers {
5+
backendFramework: BackendFramework;
6+
}
7+
8+
export async function promptBackend(): Promise<BackendAnswers> {
9+
const answers = await p.group(
10+
{
11+
backendFramework: () =>
12+
p.select({
13+
message: 'Backend framework',
14+
options: [
15+
{
16+
value: 'hono',
17+
label: 'Hono',
18+
hint: 'recommended · serverless-native · 14KB',
19+
},
20+
{
21+
value: 'express',
22+
label: 'Express',
23+
hint: 'classic Node.js · large ecosystem',
24+
},
25+
{
26+
value: 'fastify',
27+
label: 'Fastify',
28+
hint: 'high performance · schema validation',
29+
},
30+
],
31+
}),
32+
},
33+
{
34+
onCancel: () => {
35+
p.cancel('Setup cancelled.');
36+
process.exit(0);
37+
},
38+
},
39+
);
40+
41+
return {
42+
backendFramework: answers.backendFramework as BackendFramework,
43+
};
44+
}

src/prompts/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import * as p from '@clack/prompts';
22
import { promptProject, type ProjectAnswers } from './project.js';
3+
import { promptProjectType, type ProjectTypeAnswers } from './project-type.js';
34
import { promptFramework, type FrameworkAnswers } from './framework.js';
5+
import { promptBackend, type BackendAnswers } from './backend.js';
46
import { promptGitHat, type GitHatAnswers } from './githat.js';
57
import { promptFeatures, type FeatureAnswers } from './features.js';
68
import { promptFinalize, type FinalizeAnswers } from './finalize.js';
79
import { sectionHeader } from '../utils/ascii.js';
810
import { detectPackageManager } from '../utils/package-manager.js';
9-
import { DEFAULT_API_URL } from '../constants.js';
11+
import { DEFAULT_API_URL, type ProjectType, type BackendFramework } from '../constants.js';
1012
import type { TemplateContext } from '../utils/template-engine.js';
1113

1214
export interface AllAnswers
1315
extends ProjectAnswers,
16+
ProjectTypeAnswers,
1417
FrameworkAnswers,
18+
Partial<BackendAnswers>,
1519
GitHatAnswers,
1620
FeatureAnswers,
1721
FinalizeAnswers {}
@@ -28,6 +32,7 @@ function getDefaults(projectName: string, publishableKey?: string, typescript?:
2832
projectName,
2933
businessName: displayName,
3034
description: `${displayName} — Built with GitHat`,
35+
projectType: 'frontend',
3136
framework: 'nextjs',
3237
typescript: typescript ?? true,
3338
packageManager: detectPackageManager(),
@@ -55,14 +60,21 @@ export async function runPrompts(args: {
5560
return getDefaults(args.initialName, args.publishableKey, args.typescript);
5661
}
5762

58-
p.intro('Let\u2019s set up your GitHat app');
63+
p.intro("Let's set up your GitHat app");
5964

6065
sectionHeader('Project');
6166
const project = await promptProject(args.initialName);
67+
const projectType = await promptProjectType();
6268

6369
sectionHeader('Stack');
6470
const framework = await promptFramework(args.typescript);
6571

72+
// Only prompt for backend if fullstack
73+
let backend: Partial<BackendAnswers> = {};
74+
if (projectType.projectType === 'fullstack') {
75+
backend = await promptBackend();
76+
}
77+
6678
sectionHeader('Connect');
6779
const githat = await promptGitHat(args.publishableKey);
6880

@@ -72,7 +84,7 @@ export async function runPrompts(args: {
7284
sectionHeader('Finish');
7385
const finalize = await promptFinalize();
7486

75-
return { ...project, ...framework, ...githat, ...features, ...finalize };
87+
return { ...project, ...projectType, ...framework, ...backend, ...githat, ...features, ...finalize };
7688
}
7789

7890
export function answersToContext(answers: AllAnswers): TemplateContext {
@@ -86,6 +98,10 @@ export function answersToContext(answers: AllAnswers): TemplateContext {
8698
publishableKey: answers.publishableKey,
8799
apiUrl: answers.apiUrl,
88100

101+
// Project type
102+
projectType: answers.projectType,
103+
backendFramework: answers.backendFramework,
104+
89105
useDatabase: answers.databaseChoice !== 'none',
90106
databaseChoice: answers.databaseChoice,
91107
useTailwind: answers.useTailwind,

src/prompts/project-type.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as p from '@clack/prompts';
2+
import type { ProjectType } from '../constants.js';
3+
4+
export interface ProjectTypeAnswers {
5+
projectType: ProjectType;
6+
}
7+
8+
export async function promptProjectType(): Promise<ProjectTypeAnswers> {
9+
const answers = await p.group(
10+
{
11+
projectType: () =>
12+
p.select({
13+
message: 'Project type',
14+
options: [
15+
{
16+
value: 'frontend',
17+
label: 'Frontend only',
18+
hint: 'Next.js with API routes',
19+
},
20+
{
21+
value: 'fullstack',
22+
label: 'Fullstack',
23+
hint: 'Next.js + separate API (Turborepo)',
24+
},
25+
],
26+
}),
27+
},
28+
{
29+
onCancel: () => {
30+
p.cancel('Setup cancelled.');
31+
process.exit(0);
32+
},
33+
},
34+
);
35+
36+
return {
37+
projectType: answers.projectType as ProjectType,
38+
};
39+
}

src/scaffold/index.ts

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,39 @@ export async function scaffold(
2323
process.exit(1);
2424
}
2525

26+
const isFullstack = context.projectType === 'fullstack';
27+
2628
// 1. Create project structure
2729
await withSpinner('Creating project structure...', async () => {
2830
fs.ensureDirSync(root);
2931
const templatesRoot = getTemplatesRoot();
3032

31-
// Render framework-specific templates
32-
const frameworkDir = path.join(templatesRoot, context.framework);
33-
if (!fs.existsSync(frameworkDir)) {
34-
throw new Error(`Templates not found at ${frameworkDir}. This is a bug — please report it.`);
35-
}
36-
renderTemplateDirectory(frameworkDir, root, context);
33+
if (isFullstack) {
34+
// Fullstack: Turborepo monorepo structure
35+
scaffoldFullstack(templatesRoot, root, context);
36+
} else {
37+
// Frontend only: single app
38+
const frameworkDir = path.join(templatesRoot, context.framework);
39+
if (!fs.existsSync(frameworkDir)) {
40+
throw new Error(`Templates not found at ${frameworkDir}. This is a bug — please report it.`);
41+
}
42+
renderTemplateDirectory(frameworkDir, root, context);
3743

38-
// Render base templates (githat/ folder, env, gitignore)
39-
const baseDir = path.join(templatesRoot, 'base');
40-
if (fs.existsSync(baseDir)) {
41-
renderTemplateDirectory(baseDir, root, context);
44+
// Render base templates (githat/ folder, env, gitignore)
45+
const baseDir = path.join(templatesRoot, 'base');
46+
if (fs.existsSync(baseDir)) {
47+
renderTemplateDirectory(baseDir, root, context);
48+
}
4249
}
4350
}, 'Project structure created');
4451

45-
// 2. Write package.json (built programmatically for accuracy)
46-
await withSpinner('Generating package.json...', async () => {
47-
const pkg = buildPackageJson(context);
48-
writeJson(root, 'package.json', pkg);
49-
}, 'package.json generated');
52+
// 2. Write package.json (built programmatically for accuracy) — skip for fullstack
53+
if (!isFullstack) {
54+
await withSpinner('Generating package.json...', async () => {
55+
const pkg = buildPackageJson(context);
56+
writeJson(root, 'package.json', pkg);
57+
}, 'package.json generated');
58+
}
5059

5160
// 3. Git init
5261
if (options.initGit) {
@@ -82,7 +91,7 @@ export async function scaffold(
8291
}
8392

8493
p.outro('Setup complete!');
85-
displaySuccess(context.projectName, context.packageManager, context.framework, !!context.publishableKey);
94+
displaySuccess(context.projectName, context.packageManager, context.framework, !!context.publishableKey, isFullstack);
8695

8796
// Offer to star the repo (skip if --yes flag)
8897
if (!options.skipPrompts) {
@@ -100,3 +109,48 @@ export async function scaffold(
100109
}
101110
}
102111
}
112+
113+
/**
114+
* Scaffold a fullstack monorepo (Turborepo)
115+
*/
116+
function scaffoldFullstack(templatesRoot: string, root: string, context: TemplateContext): void {
117+
const fullstackDir = path.join(templatesRoot, 'fullstack');
118+
119+
// 1. Render root templates (package.json, turbo.json, githat.yaml, .gitignore)
120+
const rootDir = path.join(fullstackDir, 'root');
121+
if (fs.existsSync(rootDir)) {
122+
renderTemplateDirectory(rootDir, root, context);
123+
}
124+
125+
// 2. Create apps directory
126+
const appsDir = path.join(root, 'apps');
127+
fs.ensureDirSync(appsDir);
128+
129+
// 3. Render web app (Next.js or React+Vite)
130+
const webDir = path.join(appsDir, 'web');
131+
fs.ensureDirSync(webDir);
132+
const webTemplateDir = path.join(fullstackDir, `apps-web-${context.framework}`);
133+
if (fs.existsSync(webTemplateDir)) {
134+
renderTemplateDirectory(webTemplateDir, webDir, context);
135+
} else {
136+
throw new Error(`Web app templates not found at ${webTemplateDir}. This is a bug — please report it.`);
137+
}
138+
139+
// 4. Render API app (hono, express, or fastify)
140+
const apiDir = path.join(appsDir, 'api');
141+
fs.ensureDirSync(apiDir);
142+
const backendFramework = context.backendFramework || 'hono';
143+
const apiTemplateDir = path.join(fullstackDir, `apps-api-${backendFramework}`);
144+
if (fs.existsSync(apiTemplateDir)) {
145+
renderTemplateDirectory(apiTemplateDir, apiDir, context);
146+
} else {
147+
throw new Error(`API templates not found at ${apiTemplateDir}. This is a bug — please report it.`);
148+
}
149+
150+
// 5. Create packages directory (for shared code)
151+
const packagesDir = path.join(root, 'packages');
152+
fs.ensureDirSync(packagesDir);
153+
154+
// Create a placeholder .gitkeep to preserve the directory
155+
fs.writeFileSync(path.join(packagesDir, '.gitkeep'), '');
156+
}

0 commit comments

Comments
 (0)