Skip to content

Commit f1664d5

Browse files
gracefullightclaude
andcommitted
feat(root): add create-fullstack-starter cli package
Scaffolding CLI for `bun create fullstack-starter <project-name>`. Uses @clack/prompts for interactive output, shallow clones the template repo, cleans up template-specific files, and initializes git. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 33a06eb commit f1664d5

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

packages/create-fullstack-starter/bun.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "create-fullstack-starter",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"bin": {
6+
"create-fullstack-starter": "./dist/index.mjs"
7+
},
8+
"files": ["dist"],
9+
"scripts": {
10+
"build": "bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.mjs",
11+
"dev": "bun run src/index.ts"
12+
},
13+
"dependencies": {
14+
"@clack/prompts": "^0.10.0"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^24.10.7"
18+
},
19+
"engines": {
20+
"node": ">=18.0.0"
21+
}
22+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env node
2+
3+
import { execSync } from "node:child_process";
4+
import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5+
import { basename, join, resolve } from "node:path";
6+
import * as p from "@clack/prompts";
7+
8+
const REPO_URL = "https://github.com/first-fluke/fullstack-starter.git";
9+
const TITLE = "Fullstack Starter";
10+
const DESCRIPTION = "Production-ready fullstack monorepo template";
11+
12+
async function main() {
13+
console.log();
14+
p.intro(`${TITLE} - ${DESCRIPTION}`);
15+
16+
const args = process.argv.slice(2).filter((arg) => !arg.startsWith("-"));
17+
const flags = process.argv.slice(2).filter((arg) => arg.startsWith("-"));
18+
const noInstall = flags.includes("--no-install");
19+
const noGit = flags.includes("--no-git");
20+
21+
let projectDir = args[0];
22+
23+
if (!projectDir) {
24+
const result = await p.text({
25+
message: "Where should we create your project?",
26+
placeholder: "./my-app",
27+
validate: (value) => {
28+
if (!value) return "Please enter a directory.";
29+
},
30+
});
31+
32+
if (p.isCancel(result)) {
33+
p.cancel("Cancelled.");
34+
process.exit(0);
35+
}
36+
37+
projectDir = result;
38+
}
39+
40+
const targetDir = resolve(process.cwd(), projectDir);
41+
const projectName = basename(targetDir);
42+
43+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
44+
const overwrite = await p.confirm({
45+
message: `Directory "${projectName}" is not empty. Overwrite?`,
46+
initialValue: false,
47+
});
48+
49+
if (p.isCancel(overwrite) || !overwrite) {
50+
p.cancel("Cancelled.");
51+
process.exit(0);
52+
}
53+
54+
rmSync(targetDir, { recursive: true, force: true });
55+
}
56+
57+
const spinner = p.spinner();
58+
spinner.start("Cloning template from first-fluke/fullstack-starter");
59+
60+
try {
61+
execSync(`git clone --depth 1 ${REPO_URL} "${targetDir}"`, { stdio: "ignore" });
62+
// Remove .git from cloned template
63+
rmSync(join(targetDir, ".git"), { recursive: true, force: true });
64+
spinner.stop("Template cloned.");
65+
} catch {
66+
spinner.stop("Failed to clone template.");
67+
p.log.error("Check your network connection and try again.");
68+
process.exit(1);
69+
}
70+
71+
// Clean up template-specific files
72+
spinner.start("Cleaning up template files");
73+
const filesToRemove = ["CHANGELOG.md", "version.txt", ".github", ".agents", ".claude", ".serena"];
74+
75+
for (const file of filesToRemove) {
76+
const filePath = join(targetDir, file);
77+
if (existsSync(filePath)) {
78+
rmSync(filePath, { recursive: true, force: true });
79+
}
80+
}
81+
82+
writeFileSync(
83+
join(targetDir, "README.md"),
84+
`# ${projectName}\n\nCreated with [Fullstack Starter](https://github.com/first-fluke/fullstack-starter).\n`,
85+
);
86+
spinner.stop("Cleaned up.");
87+
88+
if (!noGit) {
89+
spinner.start("Initializing git repository");
90+
try {
91+
execSync("git init", { cwd: targetDir, stdio: "ignore" });
92+
execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
93+
execSync('git commit -m "init: scaffold from fullstack-starter"', {
94+
cwd: targetDir,
95+
stdio: "ignore",
96+
});
97+
spinner.stop("Git initialized.");
98+
} catch {
99+
spinner.stop("Git initialization skipped.");
100+
}
101+
}
102+
103+
if (!noInstall) {
104+
spinner.start("Installing dependencies (this may take a while)");
105+
try {
106+
execSync("mise install", { cwd: targetDir, stdio: "ignore", timeout: 300_000 });
107+
spinner.stop("Dependencies installed.");
108+
} catch {
109+
spinner.stop("Auto-install skipped. Run `mise install` manually.");
110+
}
111+
}
112+
113+
const steps = [`cd ${projectDir}`, ...(noInstall ? ["mise install"] : []), "mise dev"];
114+
115+
p.note(steps.join("\n"), "Next steps");
116+
p.outro("Happy hacking!");
117+
}
118+
119+
main().catch((error) => {
120+
console.error(error);
121+
process.exit(1);
122+
});

0 commit comments

Comments
 (0)