create-sdd-ai-stack 0.1.17
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/AGENTS.md +164 -0
- package/APP-STACK.md +31 -0
- package/APP.md +46 -0
- package/ARCHITECTURE.md +115 -0
- package/DESIGN.md +219 -0
- package/LICENSE +21 -0
- package/NEXT.md +345 -0
- package/NODE.md +107 -0
- package/REACT.md +92 -0
- package/README.md +260 -0
- package/SKILLS/check-docs/SKILL.md +24 -0
- package/SKILLS/check-docs/check-docs.mjs +56 -0
- package/SKILLS/create-feature/SKILL.md +40 -0
- package/SKILLS/create-feature/create-feature.mjs +128 -0
- package/SKILLS/create-feature.sh +112 -0
- package/SKILLS/install-submodule/SKILL.md +42 -0
- package/SKILLS/install-submodule/install-submodule.mjs +119 -0
- package/bin/create-sdd-ai-stack.mjs +93 -0
- package/docs/CHANGELOG.md +209 -0
- package/docs/PLANNING.md +68 -0
- package/docs/PRODUCT.md +76 -0
- package/docs/RELEASE.md +332 -0
- package/lib/check-links.mjs +42 -0
- package/lib/constants.mjs +60 -0
- package/lib/scaffold.mjs +253 -0
- package/package.json +80 -0
- package/specs/BACKLOG.md +24 -0
- package/specs/PLAN.md +57 -0
- package/specs/ROADMAP.md +35 -0
- package/specs/history/phases/phase-0-bootstrap.md +67 -0
- package/specs/tasks/TASK_TEMPLATE.md +46 -0
- package/src/cli.mjs +119 -0
- package/stacks/README.md +33 -0
- package/stacks/ai.md +52 -0
- package/stacks/ci.md +59 -0
- package/stacks/database.md +56 -0
- package/stacks/git.md +51 -0
- package/stacks/shadcn.md +52 -0
- package/stacks/tailwind.md +65 -0
- package/stacks/testing.md +74 -0
- package/stacks/typescript.md +58 -0
- package/template/next/.env.example +4 -0
- package/template/next/README.md +47 -0
- package/template/next/biome.json +36 -0
- package/template/next/gitignore +32 -0
- package/template/next/next.config.ts +12 -0
- package/template/next/package.json +47 -0
- package/template/next/playwright.config.ts +21 -0
- package/template/next/postcss.config.mjs +7 -0
- package/template/next/src/app/(app)/app/page.tsx +18 -0
- package/template/next/src/app/(app)/error.tsx +30 -0
- package/template/next/src/app/(app)/layout.tsx +25 -0
- package/template/next/src/app/(marketing)/page.tsx +73 -0
- package/template/next/src/app/globals.css +437 -0
- package/template/next/src/app/layout.tsx +41 -0
- package/template/next/src/app/not-found.tsx +11 -0
- package/template/next/src/features/example/actions.ts +43 -0
- package/template/next/src/features/example/application/create-example.usecase.ts +26 -0
- package/template/next/src/features/example/container.ts +16 -0
- package/template/next/src/features/example/domain/IExampleRepository.ts +11 -0
- package/template/next/src/features/example/domain/example.schema.ts +18 -0
- package/template/next/src/features/example/infrastructure/example.repository.ts +26 -0
- package/template/next/src/features/example/ui/create-example-form.tsx +56 -0
- package/template/next/src/proxy.ts +23 -0
- package/template/next/src/shared/lib/cn.ts +6 -0
- package/template/next/src/shared/server/auth.ts +32 -0
- package/template/next/src/shared/server/env.ts +14 -0
- package/template/next/src/shared/ui/index.ts +3 -0
- package/template/next/tests/e2e/routes.spec.ts +34 -0
- package/template/next/tests/unit/create-example.test.ts +55 -0
- package/template/next/tsconfig.json +37 -0
- package/template/next/vitest.config.ts +19 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network boundary. NÃO é fronteira de segurança (ver SDD/NEXT.md §7).
|
|
5
|
+
* Autorização real vive na camada de dado (actions/queries).
|
|
6
|
+
*/
|
|
7
|
+
export function proxy(request: NextRequest) {
|
|
8
|
+
const headers = new Headers(request.headers);
|
|
9
|
+
|
|
10
|
+
// defense-in-depth: remove o header interno que o CVE-2025-29927 explorava
|
|
11
|
+
headers.delete("x-middleware-subrequest");
|
|
12
|
+
headers.set("x-request-id", crypto.randomUUID());
|
|
13
|
+
|
|
14
|
+
const response = NextResponse.next({ request: { headers } });
|
|
15
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
16
|
+
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
17
|
+
response.headers.set("X-Frame-Options", "DENY");
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const config = {
|
|
22
|
+
matcher: ["/((?!_next|api|favicon.ico|.*\\..*).*)"],
|
|
23
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
|
|
4
|
+
export interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
role: "admin" | "member" | "banned";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Substitua pela sessão real (Auth.js, Clerk, etc).
|
|
12
|
+
* Autorização NUNCA sai daqui — ver SDD/NEXT.md §7.
|
|
13
|
+
*/
|
|
14
|
+
export const getSession = cache(async (): Promise<AuthUser | null> => {
|
|
15
|
+
return null;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function requireUser(): Promise<AuthUser> {
|
|
19
|
+
const user = await getSession();
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw new Error("UNAUTHORIZED");
|
|
22
|
+
}
|
|
23
|
+
return user;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function requireRole(...roles: AuthUser["role"][]) {
|
|
27
|
+
const user = await requireUser();
|
|
28
|
+
if (!roles.includes(user.role)) {
|
|
29
|
+
throw new Error("FORBIDDEN");
|
|
30
|
+
}
|
|
31
|
+
return user;
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const EnvSchema = z.object({
|
|
5
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
6
|
+
DATABASE_URL: z.url().optional(),
|
|
7
|
+
AUTH_SECRET: z.string().min(16).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const env = EnvSchema.parse({
|
|
11
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
12
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
13
|
+
AUTH_SECRET: process.env.AUTH_SECRET,
|
|
14
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.describe("Landing pública", () => {
|
|
4
|
+
test("renderiza hero, KPIs e chips de stack", async ({ page }) => {
|
|
5
|
+
await page.goto("/");
|
|
6
|
+
|
|
7
|
+
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
|
8
|
+
await expect(page.getByText("R$ 24M")).toBeVisible();
|
|
9
|
+
await expect(page.getByText("Next.js 16")).toBeVisible();
|
|
10
|
+
await expect(page.getByText("Tecnologia em produção")).toBeVisible();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("responde 200 na raiz", async ({ page }) => {
|
|
14
|
+
const response = await page.goto("/");
|
|
15
|
+
expect(response?.status()).toBe(200);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test.describe("Área logada", () => {
|
|
20
|
+
test("renderiza o shell da aplicação", async ({ page }) => {
|
|
21
|
+
await page.goto("/app");
|
|
22
|
+
|
|
23
|
+
await expect(page.getByRole("heading", { name: "Área logada" })).toBeVisible();
|
|
24
|
+
await expect(page.getByRole("link", { name: "App" })).toBeVisible();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test.describe("Erros", () => {
|
|
29
|
+
test("404 devolve a página not-found", async ({ page }) => {
|
|
30
|
+
const response = await page.goto("/rota-que-nao-existe");
|
|
31
|
+
expect(response?.status()).toBe(404);
|
|
32
|
+
await expect(page.getByText("Recurso não encontrado")).toBeVisible();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AppError,
|
|
4
|
+
CreateExampleUseCase,
|
|
5
|
+
} from "@/features/example/application/create-example.usecase";
|
|
6
|
+
import type { IExampleRepository } from "@/features/example/domain/IExampleRepository";
|
|
7
|
+
|
|
8
|
+
function makeRepo() {
|
|
9
|
+
const created: { title: string; body: string }[] = [];
|
|
10
|
+
const repo: IExampleRepository = {
|
|
11
|
+
create: async (input) => {
|
|
12
|
+
created.push(input);
|
|
13
|
+
return { id: "id-1", createdAt: new Date(), ...input };
|
|
14
|
+
},
|
|
15
|
+
findById: async () => null,
|
|
16
|
+
};
|
|
17
|
+
return { repo, created };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("CreateExampleUseCase", () => {
|
|
21
|
+
let repo: IExampleRepository;
|
|
22
|
+
let created: { title: string; body: string }[];
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
const made = makeRepo();
|
|
26
|
+
repo = made.repo;
|
|
27
|
+
created = made.created;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("deve_criar_quando_input_valido", async () => {
|
|
31
|
+
const useCase = new CreateExampleUseCase(repo);
|
|
32
|
+
|
|
33
|
+
const result = await useCase.execute({ title: "Ledger", body: "conteúdo ok" });
|
|
34
|
+
|
|
35
|
+
expect(result.title).toBe("Ledger");
|
|
36
|
+
expect(created).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("deve_remover_espacos_dos_lados", async () => {
|
|
40
|
+
const useCase = new CreateExampleUseCase(repo);
|
|
41
|
+
|
|
42
|
+
await useCase.execute({ title: " Ledger ", body: " corpo " });
|
|
43
|
+
|
|
44
|
+
expect(created[0]).toEqual({ title: "Ledger", body: "corpo" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("deve_lancar_erro_quando_titulo_curto_apos_trim", async () => {
|
|
48
|
+
const useCase = new CreateExampleUseCase(repo);
|
|
49
|
+
|
|
50
|
+
await expect(useCase.execute({ title: " ab ", body: "corpo" })).rejects.toThrow(
|
|
51
|
+
AppError,
|
|
52
|
+
);
|
|
53
|
+
expect(created).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"module": "esnext",
|
|
14
|
+
"moduleResolution": "bundler",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"verbatimModuleSyntax": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"incremental": true,
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": ["./src/*"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"include": [
|
|
30
|
+
"next-env.d.ts",
|
|
31
|
+
"**/*.ts",
|
|
32
|
+
"**/*.tsx",
|
|
33
|
+
".next/types/**/*.ts",
|
|
34
|
+
".next/dev/types/**/*.ts"
|
|
35
|
+
],
|
|
36
|
+
"exclude": ["node_modules", "SDD"]
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: ["tests/unit/**/*.test.ts", "tests/integration/**/*.test.ts"],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: "v8",
|
|
10
|
+
reporter: ["text", "html"],
|
|
11
|
+
include: ["src/features/**/application/**", "src/features/**/domain/**"],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|