nextjs-hackathon-stack 0.1.1 → 0.1.3
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.
- package/dist/index.js +23 -18
- package/package.json +1 -1
- package/template/.cursor/agents/security-researcher.md +1 -1
- package/template/.cursor/agents/test-qa.md +2 -2
- package/template/.cursor/rules/security.mdc +1 -1
- package/template/.cursor/skills/create-api-route/SKILL.md +3 -3
- package/template/.cursor/skills/create-feature/SKILL.md +5 -5
- package/template/.cursor/skills/review-branch/SKILL.md +3 -3
- package/template/.cursor/skills/security-audit/SKILL.md +1 -1
- package/template/.env.example +1 -1
- package/template/README.md +12 -12
- package/template/playwright.config.ts +1 -1
- package/template/src/app/(auth)/login/page.tsx +8 -3
- package/template/src/features/auth/components/login-form.tsx +51 -44
package/dist/index.js
CHANGED
|
@@ -18,9 +18,6 @@ function printBanner() {
|
|
|
18
18
|
function success(msg) {
|
|
19
19
|
p.log.success(msg);
|
|
20
20
|
}
|
|
21
|
-
function info(msg) {
|
|
22
|
-
p.log.info(msg);
|
|
23
|
-
}
|
|
24
21
|
function outro2(msg) {
|
|
25
22
|
p.outro(pc.green(msg));
|
|
26
23
|
}
|
|
@@ -104,37 +101,45 @@ async function scaffold(projectName, skipInstall) {
|
|
|
104
101
|
if (!skipInstall) {
|
|
105
102
|
spinner2.start("Installing dependencies (this may take a minute)");
|
|
106
103
|
try {
|
|
107
|
-
execSync("
|
|
104
|
+
execSync("pnpm install", { cwd: targetDir, stdio: "pipe" });
|
|
108
105
|
spinner2.stop("Dependencies installed");
|
|
109
106
|
} catch (err) {
|
|
110
107
|
spinner2.stop("Failed to install dependencies");
|
|
111
108
|
const stderr = err instanceof Error && "stderr" in err ? err.stderr?.toString() : String(err);
|
|
112
|
-
p3.log.error(stderr || "
|
|
113
|
-
p3.log.info("You can install manually: cd " + projectName + " &&
|
|
109
|
+
p3.log.error(stderr || "pnpm install failed");
|
|
110
|
+
p3.log.info("You can install manually: cd " + projectName + " && pnpm install");
|
|
114
111
|
}
|
|
115
|
-
|
|
112
|
+
spinner2.start("Initializing shadcn/ui");
|
|
116
113
|
try {
|
|
117
|
-
execSync("npx shadcn@latest init --yes --defaults", {
|
|
114
|
+
execSync("npx shadcn@latest init --yes --defaults --force --silent", {
|
|
118
115
|
cwd: targetDir,
|
|
119
|
-
stdio: "
|
|
116
|
+
stdio: "pipe"
|
|
120
117
|
});
|
|
121
|
-
|
|
118
|
+
spinner2.stop("shadcn/ui initialized");
|
|
122
119
|
} catch {
|
|
123
|
-
|
|
120
|
+
spinner2.stop("shadcn/ui init failed \u2014 run manually: npx shadcn@latest init");
|
|
124
121
|
}
|
|
125
|
-
info("Adding shadcn/ui base components...");
|
|
126
122
|
const components = ["button", "input", "card", "form", "label"];
|
|
127
123
|
for (const component of components) {
|
|
124
|
+
spinner2.start(`Adding shadcn/ui component: ${component}`);
|
|
128
125
|
try {
|
|
129
|
-
execSync(`npx shadcn@latest add ${component} --yes`, {
|
|
126
|
+
execSync(`npx shadcn@latest add ${component} --yes --silent`, {
|
|
130
127
|
cwd: targetDir,
|
|
131
|
-
stdio: "
|
|
128
|
+
stdio: "pipe"
|
|
132
129
|
});
|
|
130
|
+
spinner2.stop(`Added shadcn/ui component: ${component}`);
|
|
133
131
|
} catch {
|
|
134
|
-
|
|
132
|
+
spinner2.stop(`Failed to add component: ${component}`);
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
|
-
|
|
135
|
+
}
|
|
136
|
+
spinner2.start("Creating initial commit");
|
|
137
|
+
try {
|
|
138
|
+
execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
139
|
+
execSync('git commit -m "chore: initial scaffold"', { cwd: targetDir, stdio: "ignore" });
|
|
140
|
+
spinner2.stop("Initial commit created");
|
|
141
|
+
} catch {
|
|
142
|
+
spinner2.stop("Could not create initial commit");
|
|
138
143
|
}
|
|
139
144
|
success(pc2.bold(`Project "${projectName}" created!`));
|
|
140
145
|
console.log(`
|
|
@@ -146,13 +151,13 @@ async function scaffold(projectName, skipInstall) {
|
|
|
146
151
|
console.log(` ${pc2.dim("DATABASE_URL")} \u2014 from supabase.com > Project Settings > Database`);
|
|
147
152
|
console.log(` ${pc2.dim("AI_GATEWAY_URL")} \u2014 Vercel AI Gateway URL`);
|
|
148
153
|
console.log(` ${pc2.dim("MINIMAX_API_KEY")} \u2014 from minimaxi.chat`);
|
|
149
|
-
console.log(` ${pc2.cyan("
|
|
154
|
+
console.log(` ${pc2.cyan("pnpm dev")}
|
|
150
155
|
`);
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
// src/index.ts
|
|
154
159
|
var program = new Command();
|
|
155
|
-
program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip
|
|
160
|
+
program.name("nextjs-hackathon-stack").description("Scaffold a full-stack Next.js 15 hackathon starter").version("0.1.0").argument("[project-name]", "Name of the project to create").option("--skip-install", "Skip pnpm install and shadcn/ui init", false).action(async (projectName, opts) => {
|
|
156
161
|
try {
|
|
157
162
|
const options = await runCli(projectName, opts.skipInstall);
|
|
158
163
|
await scaffold(options.projectName, options.skipInstall);
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@ readonly: true
|
|
|
27
27
|
- [ ] `.env.example` has no real values
|
|
28
28
|
|
|
29
29
|
### Dependencies
|
|
30
|
-
- [ ] Run `
|
|
30
|
+
- [ ] Run `pnpm audit` — report Critical/High findings
|
|
31
31
|
- [ ] Review new dependencies for malicious packages
|
|
32
32
|
|
|
33
33
|
### Headers & Cookies
|
|
@@ -11,7 +11,7 @@ readonly: false
|
|
|
11
11
|
1. **RED** — write failing test that describes the desired behavior
|
|
12
12
|
2. **GREEN** — write minimum code to make test pass
|
|
13
13
|
3. **REFACTOR** — clean up while keeping tests green
|
|
14
|
-
4. **VERIFY** — run `
|
|
14
|
+
4. **VERIFY** — run `pnpm test:coverage` and confirm 100%
|
|
15
15
|
|
|
16
16
|
## AAA Pattern (Arrange-Act-Assert)
|
|
17
17
|
|
|
@@ -56,7 +56,7 @@ statements: 100%
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
After every change:
|
|
59
|
-
1. Run `
|
|
59
|
+
1. Run `pnpm test:coverage`
|
|
60
60
|
2. If below 100%, add missing tests
|
|
61
61
|
3. Never mark work complete without full coverage
|
|
62
62
|
|
|
@@ -12,7 +12,7 @@ alwaysApply: true
|
|
|
12
12
|
4. **XSS** — never use `dangerouslySetInnerHTML`, escape user input
|
|
13
13
|
5. **Broken Access Control** — RLS on all tables, auth check in all protected routes
|
|
14
14
|
6. **Security Misconfiguration** — CSP, HSTS, X-Frame-Options in `next.config.ts`
|
|
15
|
-
7. **Insecure Dependencies** — run `
|
|
15
|
+
7. **Insecure Dependencies** — run `pnpm audit` regularly
|
|
16
16
|
8. **SSRF** — validate URLs before fetch
|
|
17
17
|
|
|
18
18
|
## Environment Variables
|
|
@@ -28,22 +28,22 @@ Write ALL test files first:
|
|
|
28
28
|
- `__tests__/use-<feature>.test.ts` — hook tests
|
|
29
29
|
- `__tests__/<action>.test.ts` — action tests
|
|
30
30
|
|
|
31
|
-
Run `
|
|
31
|
+
Run `pnpm test:unit` — all tests must FAIL (RED).
|
|
32
32
|
|
|
33
33
|
### 4. TDD: GREEN Phase
|
|
34
34
|
Implement minimum code to pass each test:
|
|
35
35
|
- Components → actions → hooks → API routes
|
|
36
36
|
|
|
37
|
-
Run `
|
|
37
|
+
Run `pnpm test:unit` — all tests must PASS (GREEN).
|
|
38
38
|
|
|
39
39
|
### 5. Refactor
|
|
40
40
|
Clean up while keeping tests green.
|
|
41
41
|
|
|
42
42
|
### 6. Verify
|
|
43
43
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
pnpm test:coverage # Must show 100%
|
|
45
|
+
pnpm lint # Must pass with 0 warnings
|
|
46
|
+
pnpm typecheck # Must pass with 0 errors
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
## Guardrails
|
package/template/.env.example
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# =============================================================================
|
|
2
|
-
# REQUIRED — fill these in before running
|
|
2
|
+
# REQUIRED — fill these in before running pnpm dev
|
|
3
3
|
# =============================================================================
|
|
4
4
|
|
|
5
5
|
# Supabase — https://supabase.com > Project Settings > API
|
package/template/README.md
CHANGED
|
@@ -39,28 +39,28 @@ AI_GATEWAY_URL=https://gateway.ai.vercel.app/v1/your-team-id/your-gateway-id
|
|
|
39
39
|
### 2. Run the dev server
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
42
|
+
pnpm dev
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### 3. Apply database migrations
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
pnpm db:generate
|
|
49
|
+
pnpm db:migrate
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Scripts
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
pnpm dev # Start dev server
|
|
56
|
+
pnpm build # Production build
|
|
57
|
+
pnpm lint # Lint (0 warnings allowed)
|
|
58
|
+
pnpm typecheck # TypeScript check
|
|
59
|
+
pnpm test # Unit tests
|
|
60
|
+
pnpm test:coverage # Tests with coverage (100% required)
|
|
61
|
+
pnpm test:e2e # Playwright e2e tests
|
|
62
|
+
pnpm db:generate # Generate Drizzle migrations
|
|
63
|
+
pnpm db:migrate # Apply migrations
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
## Environment Variables
|
|
@@ -2,9 +2,14 @@ import { LoginForm } from "@/features/auth/components/login-form";
|
|
|
2
2
|
|
|
3
3
|
export default function LoginPage() {
|
|
4
4
|
return (
|
|
5
|
-
<main className="flex min-h-screen items-center justify-center">
|
|
6
|
-
<div className="w-full max-w-
|
|
7
|
-
<
|
|
5
|
+
<main className="flex min-h-screen items-center justify-center bg-muted/40 px-4">
|
|
6
|
+
<div className="w-full max-w-sm space-y-6">
|
|
7
|
+
<div className="text-center">
|
|
8
|
+
<h1 className="text-3xl font-bold tracking-tight">Welcome back</h1>
|
|
9
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
10
|
+
Sign in to your account to continue
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
8
13
|
<LoginForm />
|
|
9
14
|
</div>
|
|
10
15
|
</main>
|
|
@@ -5,6 +5,10 @@ import { useForm } from "react-hook-form";
|
|
|
5
5
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { loginAction, type LoginActionState } from "../actions/login.action";
|
|
8
|
+
import { Button } from "@/shared/components/ui/button";
|
|
9
|
+
import { Input } from "@/shared/components/ui/input";
|
|
10
|
+
import { Label } from "@/shared/components/ui/label";
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card";
|
|
8
12
|
|
|
9
13
|
const schema = z.object({
|
|
10
14
|
email: z.string().email("Invalid email"),
|
|
@@ -32,49 +36,52 @@ export function LoginForm() {
|
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
39
|
+
<Card>
|
|
40
|
+
<CardHeader>
|
|
41
|
+
<CardTitle>Sign in</CardTitle>
|
|
42
|
+
<CardDescription>Enter your email and password below</CardDescription>
|
|
43
|
+
</CardHeader>
|
|
44
|
+
<CardContent>
|
|
45
|
+
<form onSubmit={onSubmit} data-testid="login-form" className="space-y-4">
|
|
46
|
+
{state.status === "error" && (
|
|
47
|
+
<p role="alert" className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
48
|
+
{state.message}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
<div className="space-y-1.5">
|
|
53
|
+
<Label htmlFor="email">Email</Label>
|
|
54
|
+
<Input
|
|
55
|
+
id="email"
|
|
56
|
+
type="email"
|
|
57
|
+
autoComplete="email"
|
|
58
|
+
placeholder="you@example.com"
|
|
59
|
+
{...register("email")}
|
|
60
|
+
/>
|
|
61
|
+
{errors.email && (
|
|
62
|
+
<p className="text-sm text-destructive">{errors.email.message}</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="space-y-1.5">
|
|
67
|
+
<Label htmlFor="password">Password</Label>
|
|
68
|
+
<Input
|
|
69
|
+
id="password"
|
|
70
|
+
type="password"
|
|
71
|
+
autoComplete="current-password"
|
|
72
|
+
placeholder="••••••••"
|
|
73
|
+
{...register("password")}
|
|
74
|
+
/>
|
|
75
|
+
{errors.password && (
|
|
76
|
+
<p className="text-sm text-destructive">{errors.password.message}</p>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<Button type="submit" className="w-full" disabled={isPending}>
|
|
81
|
+
{isPending ? "Signing in…" : "Sign in"}
|
|
82
|
+
</Button>
|
|
83
|
+
</form>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
79
86
|
);
|
|
80
87
|
}
|