teamshare-bridge 0.3.0 → 0.4.0
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/bridge/daemon.js +36 -0
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/local-server.d.ts +1 -1
- package/dist/bridge/spawn.d.ts +2 -0
- package/dist/bridge/spawn.js +17 -0
- package/dist/bridge/spawn.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.js +75 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/build.js +9 -6
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/doc.js +1 -1
- package/dist/cli/commands/doc.js.map +1 -1
- package/dist/cli/commands/run.js +3 -2
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +1 -0
- package/dist/cli/commands/scaffold.js +111 -0
- package/dist/cli/commands/scaffold.js.map +1 -0
- package/dist/cli/commands/worker.d.ts +2 -0
- package/dist/cli/commands/worker.js +110 -0
- package/dist/cli/commands/worker.js.map +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/api.d.ts +22 -3
- package/dist/lib/api.js +9 -2
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/audit/docs.d.ts +5 -0
- package/dist/lib/audit/docs.js +52 -0
- package/dist/lib/audit/docs.js.map +1 -0
- package/dist/lib/audit/errors.d.ts +15 -0
- package/dist/lib/audit/errors.js +154 -0
- package/dist/lib/audit/errors.js.map +1 -0
- package/dist/lib/audit/gates.d.ts +2 -0
- package/dist/lib/audit/gates.js +57 -0
- package/dist/lib/audit/gates.js.map +1 -0
- package/dist/lib/audit/modularity.d.ts +13 -0
- package/dist/lib/audit/modularity.js +200 -0
- package/dist/lib/audit/modularity.js.map +1 -0
- package/dist/lib/audit/quality.d.ts +9 -0
- package/dist/lib/audit/quality.js +128 -0
- package/dist/lib/audit/quality.js.map +1 -0
- package/dist/lib/audit/rbac.d.ts +14 -0
- package/dist/lib/audit/rbac.js +139 -0
- package/dist/lib/audit/rbac.js.map +1 -0
- package/dist/lib/audit/reporter.d.ts +5 -0
- package/dist/lib/audit/reporter.js +107 -0
- package/dist/lib/audit/reporter.js.map +1 -0
- package/dist/lib/audit/runner.d.ts +2 -0
- package/dist/lib/audit/runner.js +150 -0
- package/dist/lib/audit/runner.js.map +1 -0
- package/dist/lib/audit/security.d.ts +9 -0
- package/dist/lib/audit/security.js +151 -0
- package/dist/lib/audit/security.js.map +1 -0
- package/dist/lib/audit/types.d.ts +67 -0
- package/dist/lib/audit/types.js +26 -0
- package/dist/lib/audit/types.js.map +1 -0
- package/dist/lib/audit/ux.d.ts +9 -0
- package/dist/lib/audit/ux.js +112 -0
- package/dist/lib/audit/ux.js.map +1 -0
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/llm.js +1 -0
- package/dist/lib/llm.js.map +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/scaffold/renderer.d.ts +7 -0
- package/dist/lib/scaffold/renderer.js +37 -0
- package/dist/lib/scaffold/renderer.js.map +1 -0
- package/dist/lib/scaffold/structure.d.ts +3 -0
- package/dist/lib/scaffold/structure.js +73 -0
- package/dist/lib/scaffold/structure.js.map +1 -0
- package/dist/lib/scaffold/types.d.ts +34 -0
- package/dist/lib/scaffold/types.js +6 -0
- package/dist/lib/scaffold/types.js.map +1 -0
- package/dist/lib/scaffold/verify.d.ts +9 -0
- package/dist/lib/scaffold/verify.js +48 -0
- package/dist/lib/scaffold/verify.js.map +1 -0
- package/dist/lib/skills.d.ts +2 -2
- package/dist/lib/skills.js +2 -1
- package/dist/lib/skills.js.map +1 -1
- package/package.json +3 -2
- package/skills/teamshare-orchestrator-ask/SKILL.md +14 -0
- package/skills/teamshare-scaffold/SKILL.md +133 -0
- package/templates/backend/.env.example.tpl +13 -0
- package/templates/backend/nest-cli.json.tpl +8 -0
- package/templates/backend/package.json.tpl +41 -0
- package/templates/backend/prisma/schema.prisma.tpl +39 -0
- package/templates/backend/src/app.module.ts.tpl +24 -0
- package/templates/backend/src/common/filters/all-exceptions.filter.ts.tpl +59 -0
- package/templates/backend/src/common/guards/jwt-auth.guard.ts.tpl +27 -0
- package/templates/backend/src/common/guards/rbac.guard.ts.tpl +48 -0
- package/templates/backend/src/common/interceptors/transform.interceptor.ts.tpl +44 -0
- package/templates/backend/src/common/pipes/zod-validation.pipe.ts.tpl +29 -0
- package/templates/backend/src/common/swagger/envelope.schemas.ts.tpl +53 -0
- package/templates/backend/src/common/swagger/error.schemas.ts.tpl +62 -0
- package/templates/backend/src/main.ts.tpl +27 -0
- package/templates/backend/src/modules/example/example.controller.ts.tpl +102 -0
- package/templates/backend/src/modules/example/example.dto.ts.tpl +14 -0
- package/templates/backend/src/modules/example/example.module.ts.tpl +10 -0
- package/templates/backend/src/modules/example/example.service.ts.tpl +45 -0
- package/templates/backend/tsconfig.json.tpl +26 -0
- package/templates/common/.gitignore.tpl +42 -0
- package/templates/common/AGENTS.md.tpl +58 -0
- package/templates/common/docs/adr/README.md +25 -0
- package/templates/common/docs/api-contract.md.tpl +72 -0
- package/templates/web/next.config.ts.tpl +7 -0
- package/templates/web/package.json.tpl +37 -0
- package/templates/web/src/app/globals.css.tpl +48 -0
- package/templates/web/src/app/layout.tsx.tpl +25 -0
- package/templates/web/src/components/shared/app-button.tsx.tpl +59 -0
- package/templates/web/src/components/shared/app-empty-state.tsx.tpl +39 -0
- package/templates/web/src/components/shared/app-list-toolbar.tsx.tpl +63 -0
- package/templates/web/src/components/shared/app-skeleton.tsx.tpl +14 -0
- package/templates/web/src/components/shared/app-table.tsx.tpl +128 -0
- package/templates/web/src/components/shared/index.ts.tpl +5 -0
- package/templates/web/src/hooks/use-list-state.ts.tpl +72 -0
- package/templates/web/src/hooks/use-url-tab.ts.tpl +25 -0
- package/templates/web/src/lib/api/client.ts.tpl +123 -0
- package/templates/web/src/lib/providers.tsx.tpl +35 -0
- package/templates/web/src/lib/query-keys.ts.tpl +21 -0
- package/templates/web/src/lib/utils.ts.tpl +7 -0
- package/templates/web/src/lib/validation/schemas.ts.tpl +15 -0
- package/templates/web/src/modules/example/views/example-list-view.tsx.tpl +98 -0
- package/templates/web/tsconfig.json.tpl +23 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { applyDecorators } from "@nestjs/common";
|
|
2
|
+
import { ApiResponse } from "@nestjs/swagger";
|
|
3
|
+
|
|
4
|
+
export const INTERNAL_ERROR_CODES = [
|
|
5
|
+
"INTERNAL_ERROR",
|
|
6
|
+
"BAD_REQUEST",
|
|
7
|
+
"UNAUTHORIZED",
|
|
8
|
+
"FORBIDDEN",
|
|
9
|
+
"NOT_FOUND",
|
|
10
|
+
"CONFLICT",
|
|
11
|
+
"UNPROCESSABLE_ENTITY",
|
|
12
|
+
"TOO_MANY_REQUESTS",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export function errs(...codes: (typeof INTERNAL_ERROR_CODES)[number][]) {
|
|
16
|
+
const descriptions: Record<string, string> = {
|
|
17
|
+
INTERNAL_ERROR: "Internal server error",
|
|
18
|
+
BAD_REQUEST: "Invalid request parameters",
|
|
19
|
+
UNAUTHORIZED: "Authentication required",
|
|
20
|
+
FORBIDDEN: "Insufficient permissions",
|
|
21
|
+
NOT_FOUND: "Resource not found",
|
|
22
|
+
CONFLICT: "Resource already exists",
|
|
23
|
+
UNPROCESSABLE_ENTITY: "Validation failed",
|
|
24
|
+
TOO_MANY_REQUESTS: "Rate limit exceeded",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return applyDecorators(
|
|
28
|
+
...codes.map((code) =>
|
|
29
|
+
ApiResponse({
|
|
30
|
+
status: codeToStatus(code),
|
|
31
|
+
description: descriptions[code] ?? code,
|
|
32
|
+
schema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
success: { type: "boolean", example: false },
|
|
36
|
+
error: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
code: { type: "string", example: code },
|
|
40
|
+
message: { type: "string", example: descriptions[code] },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function codeToStatus(code: string): number {
|
|
51
|
+
const map: Record<string, number> = {
|
|
52
|
+
BAD_REQUEST: 400,
|
|
53
|
+
UNAUTHORIZED: 401,
|
|
54
|
+
FORBIDDEN: 403,
|
|
55
|
+
NOT_FOUND: 404,
|
|
56
|
+
CONFLICT: 409,
|
|
57
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
58
|
+
TOO_MANY_REQUESTS: 429,
|
|
59
|
+
INTERNAL_ERROR: 500,
|
|
60
|
+
};
|
|
61
|
+
return map[code] ?? 500;
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NestFactory } from "@nestjs/core";
|
|
2
|
+
import { ValidationPipe } from "@nestjs/common";
|
|
3
|
+
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
|
4
|
+
import { AppModule } from "./app.module";
|
|
5
|
+
|
|
6
|
+
async function bootstrap() {
|
|
7
|
+
const app = await NestFactory.create(AppModule);
|
|
8
|
+
|
|
9
|
+
app.setGlobalPrefix("api");
|
|
10
|
+
app.enableCors();
|
|
11
|
+
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
|
12
|
+
|
|
13
|
+
const config = new DocumentBuilder()
|
|
14
|
+
.setTitle("{{projectName}} API")
|
|
15
|
+
.setDescription("{{description}}")
|
|
16
|
+
.setVersion("0.1.0")
|
|
17
|
+
.addBearerAuth()
|
|
18
|
+
.build();
|
|
19
|
+
|
|
20
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
21
|
+
SwaggerModule.setup("docs", app, document);
|
|
22
|
+
|
|
23
|
+
await app.listen({{apiPort}});
|
|
24
|
+
console.log(`Application running on: http://localhost:{{apiPort}}`);
|
|
25
|
+
console.log(`Swagger docs: http://localhost:{{apiPort}}/docs`);
|
|
26
|
+
}
|
|
27
|
+
bootstrap();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Controller,
|
|
3
|
+
Get,
|
|
4
|
+
Post,
|
|
5
|
+
Param,
|
|
6
|
+
Query,
|
|
7
|
+
Body,
|
|
8
|
+
ParseUUIDPipe,
|
|
9
|
+
} from "@nestjs/common";
|
|
10
|
+
import {
|
|
11
|
+
ApiTags,
|
|
12
|
+
ApiOperation,
|
|
13
|
+
ApiParam,
|
|
14
|
+
ApiQuery,
|
|
15
|
+
ApiBody,
|
|
16
|
+
ApiOkResponse,
|
|
17
|
+
ApiBearerAuth,
|
|
18
|
+
} from "@nestjs/swagger";
|
|
19
|
+
import { Permissions } from "../../common/decorators/permissions.decorator";
|
|
20
|
+
import { Public } from "../../common/decorators/public.decorator";
|
|
21
|
+
import { ZodValidationPipe } from "../../common/pipes/zod-validation.pipe";
|
|
22
|
+
import { successSchema, paginatedSchema } from "../../common/swagger/envelope.schemas";
|
|
23
|
+
import { errs } from "../../common/swagger/error.schemas";
|
|
24
|
+
import { ExampleService } from "./example.service";
|
|
25
|
+
import { CreateExampleSchema, ExampleQuerySchema } from "./example.dto";
|
|
26
|
+
|
|
27
|
+
@ApiTags("examples")
|
|
28
|
+
@ApiBearerAuth()
|
|
29
|
+
@Controller("examples")
|
|
30
|
+
export class ExampleController {
|
|
31
|
+
constructor(private readonly exampleService: ExampleService) {}
|
|
32
|
+
|
|
33
|
+
@Get()
|
|
34
|
+
@Public()
|
|
35
|
+
@ApiOperation({ summary: "List examples", description: "Retrieve a paginated list of examples" })
|
|
36
|
+
@ApiQuery({ name: "page", required: false, example: 1 })
|
|
37
|
+
@ApiQuery({ name: "limit", required: false, example: 20 })
|
|
38
|
+
@ApiQuery({ name: "search", required: false })
|
|
39
|
+
@ApiOkResponse({
|
|
40
|
+
description: "Paginated list of examples",
|
|
41
|
+
schema: paginatedSchema({
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: "string", format: "uuid" },
|
|
45
|
+
name: { type: "string" },
|
|
46
|
+
createdAt: { type: "string", format: "date-time" },
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
})
|
|
50
|
+
@errs("INTERNAL_ERROR")
|
|
51
|
+
async list(
|
|
52
|
+
@Query(new ZodValidationPipe(ExampleQuerySchema, "query")) query: {
|
|
53
|
+
page: number;
|
|
54
|
+
limit: number;
|
|
55
|
+
search?: string;
|
|
56
|
+
}
|
|
57
|
+
) {
|
|
58
|
+
return this.exampleService.list(query);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Post()
|
|
62
|
+
@Permissions("example:create")
|
|
63
|
+
@ApiOperation({ summary: "Create example", description: "Create a new example" })
|
|
64
|
+
@ApiBody({ schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } })
|
|
65
|
+
@ApiOkResponse({
|
|
66
|
+
description: "Created example",
|
|
67
|
+
schema: successSchema({
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
id: { type: "string", format: "uuid" },
|
|
71
|
+
name: { type: "string" },
|
|
72
|
+
createdAt: { type: "string", format: "date-time" },
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
@errs("BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "INTERNAL_ERROR")
|
|
77
|
+
async create(
|
|
78
|
+
@Body(new ZodValidationPipe(CreateExampleSchema)) body: { name: string }
|
|
79
|
+
) {
|
|
80
|
+
return this.exampleService.create(body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Get(":id")
|
|
84
|
+
@Permissions("example:read")
|
|
85
|
+
@ApiOperation({ summary: "Get example", description: "Get an example by ID" })
|
|
86
|
+
@ApiParam({ name: "id", required: true })
|
|
87
|
+
@ApiOkResponse({
|
|
88
|
+
description: "Example details",
|
|
89
|
+
schema: successSchema({
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
id: { type: "string", format: "uuid" },
|
|
93
|
+
name: { type: "string" },
|
|
94
|
+
createdAt: { type: "string", format: "date-time" },
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
@errs("NOT_FOUND", "UNAUTHORIZED", "FORBIDDEN", "INTERNAL_ERROR")
|
|
99
|
+
async getOne(@Param("id", ParseUUIDPipe) id: string) {
|
|
100
|
+
return this.exampleService.getOne(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const CreateExampleSchema = z.object({
|
|
4
|
+
name: z.string().min(1).max(200),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const ExampleQuerySchema = z.object({
|
|
8
|
+
page: z.coerce.number().int().positive().default(1),
|
|
9
|
+
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
10
|
+
search: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type CreateExampleInput = z.infer<typeof CreateExampleSchema>;
|
|
14
|
+
export type ExampleQueryInput = z.infer<typeof ExampleQuerySchema>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { ExampleController } from "./example.controller";
|
|
3
|
+
import { ExampleService } from "./example.service";
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [ExampleController],
|
|
7
|
+
providers: [ExampleService],
|
|
8
|
+
exports: [ExampleService],
|
|
9
|
+
})
|
|
10
|
+
export class ExampleModule {}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
2
|
+
import { PrismaService } from "../../prisma/prisma.service";
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class ExampleService {
|
|
6
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
7
|
+
|
|
8
|
+
async list(query: { page: number; limit: number; search?: string }) {
|
|
9
|
+
const where = query.search
|
|
10
|
+
? { name: { contains: query.search, mode: "insensitive" as const } }
|
|
11
|
+
: {};
|
|
12
|
+
|
|
13
|
+
const [data, total] = await Promise.all([
|
|
14
|
+
this.prisma.example.findMany({
|
|
15
|
+
where,
|
|
16
|
+
skip: (query.page - 1) * query.limit,
|
|
17
|
+
take: query.limit,
|
|
18
|
+
orderBy: { createdAt: "desc" },
|
|
19
|
+
}),
|
|
20
|
+
this.prisma.example.count({ where }),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
data,
|
|
25
|
+
pagination: {
|
|
26
|
+
page: query.page,
|
|
27
|
+
limit: query.limit,
|
|
28
|
+
total,
|
|
29
|
+
totalPages: Math.ceil(total / query.limit),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async create(data: { name: string }) {
|
|
35
|
+
return this.prisma.example.create({ data });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getOne(id: string) {
|
|
39
|
+
const example = await this.prisma.example.findUnique({ where: { id } });
|
|
40
|
+
if (!example) {
|
|
41
|
+
throw new NotFoundException("Example not found");
|
|
42
|
+
}
|
|
43
|
+
return example;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"removeComments": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"target": "ES2021",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"baseUrl": "./",
|
|
13
|
+
"incremental": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strictNullChecks": true,
|
|
16
|
+
"noImplicitAny": false,
|
|
17
|
+
"strictBindCallApply": false,
|
|
18
|
+
"forceConsistentCasingInFileNames": false,
|
|
19
|
+
"noFallthroughCasesInSwitch": false,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*"],
|
|
25
|
+
"exclude": ["node_modules", "dist"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
.next/
|
|
7
|
+
out/
|
|
8
|
+
|
|
9
|
+
# Environment
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# IDE
|
|
15
|
+
.vscode/
|
|
16
|
+
.idea/
|
|
17
|
+
*.swp
|
|
18
|
+
*.swo
|
|
19
|
+
*~
|
|
20
|
+
|
|
21
|
+
# OS
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
24
|
+
|
|
25
|
+
# Debug
|
|
26
|
+
npm-debug.log*
|
|
27
|
+
yarn-debug.log*
|
|
28
|
+
yarn-error.log*
|
|
29
|
+
|
|
30
|
+
# Prisma
|
|
31
|
+
prisma/migrations/
|
|
32
|
+
|
|
33
|
+
# TypeScript
|
|
34
|
+
*.tsbuildinfo
|
|
35
|
+
next-env.d.ts
|
|
36
|
+
|
|
37
|
+
# Logs
|
|
38
|
+
*.log
|
|
39
|
+
|
|
40
|
+
# Temp
|
|
41
|
+
tmp/
|
|
42
|
+
temp/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# AGENTS.md - {{projectName}}
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
{{description}}
|
|
6
|
+
|
|
7
|
+
## Tech stack
|
|
8
|
+
|
|
9
|
+
- **Frontend:** Next.js 16, React 19, TypeScript, Tailwind v4, shadcn, react-query v5, TanStack Table v8, react-hook-form, zod, next-auth v4, next-themes, socket.io-client
|
|
10
|
+
- **Backend:** NestJS 11, Prisma 6, zod, Swagger/OpenAPI, passport-jwt, argon2, socket.io
|
|
11
|
+
- **Database:** PostgreSQL
|
|
12
|
+
|
|
13
|
+
## Folder structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{{slug}}/
|
|
17
|
+
├── frontend/ # Next.js web app
|
|
18
|
+
│ ├── src/
|
|
19
|
+
│ │ ├── app/ # App Router routes
|
|
20
|
+
│ │ ├── components/
|
|
21
|
+
│ │ │ ├── ui/ # Vendor primitives (shadcn) — do not edit
|
|
22
|
+
│ │ │ └── shared/# Shared wrappers (AppButton, AppTable, etc.)
|
|
23
|
+
│ │ ├── hooks/ # Custom React hooks
|
|
24
|
+
│ │ ├── lib/ # Utilities, API client, query keys, validation
|
|
25
|
+
│ │ └── modules/ # Feature modules (views, components, columns)
|
|
26
|
+
│ └── package.json
|
|
27
|
+
├── backend/ # NestJS API
|
|
28
|
+
│ ├── prisma/ # Schema + migrations
|
|
29
|
+
│ ├── src/
|
|
30
|
+
│ │ ├── common/ # Guards, filters, interceptors, pipes, swagger
|
|
31
|
+
│ │ ├── modules/ # Feature modules (controller, service, dto, module)
|
|
32
|
+
│ │ ├── generated/ # Prisma + zod generated output
|
|
33
|
+
│ │ └── main.ts
|
|
34
|
+
│ └── package.json
|
|
35
|
+
└── docs/ # Project documentation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Build commands
|
|
39
|
+
|
|
40
|
+
| Command | Location | Description |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `npm run dev` | frontend/ | Start Next.js dev server |
|
|
43
|
+
| `npm run build` | frontend/ | Build Next.js app |
|
|
44
|
+
| `npm run dev` | backend/ | Start NestJS dev server |
|
|
45
|
+
| `npm run build` | backend/ | Build NestJS app |
|
|
46
|
+
| `npx prisma generate` | backend/ | Regenerate Prisma client |
|
|
47
|
+
| `npx prisma migrate dev` | backend/ | Run migrations |
|
|
48
|
+
|
|
49
|
+
## Conventions
|
|
50
|
+
|
|
51
|
+
- Shared components use a consistent prefix (e.g., App*, or your own choice)
|
|
52
|
+
- Use `data-slot="app-*"` attribute on shared components for styling hooks
|
|
53
|
+
- Backend uses ZodValidationPipe for request validation
|
|
54
|
+
- All endpoints return `{ success: true, data }` envelope
|
|
55
|
+
- RBAC via `@Permissions()` decorator, `@Public()` for open routes
|
|
56
|
+
- Query keys live in `src/lib/query-keys.ts`
|
|
57
|
+
- Validation schemas live in `src/lib/validation/schemas.ts`
|
|
58
|
+
- Loading states: every query view has skeleton + error + empty state
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Architecture Decision Records
|
|
2
|
+
|
|
3
|
+
One file per decision, named `NNN-short-title.md` (e.g. `001-orm-choice.md`).
|
|
4
|
+
|
|
5
|
+
Format for each ADR:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# ADR-NNN: <Title>
|
|
9
|
+
|
|
10
|
+
**Status:** proposed | accepted | superseded by ADR-MMM
|
|
11
|
+
**Date:** YYYY-MM-DD
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
What problem or force drove this decision?
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
What was decided, stated plainly.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
What becomes easier, what becomes harder, what must now be true.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Write an ADR whenever a choice is expensive to reverse (ORM, validation
|
|
24
|
+
strategy, folder layout, auth model). Agents working in this repo should
|
|
25
|
+
read existing ADRs before proposing conflicting changes.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# API Contract - {{projectName}}
|
|
2
|
+
|
|
3
|
+
## Response envelope
|
|
4
|
+
|
|
5
|
+
Every successful response returns:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"success": true,
|
|
10
|
+
"data": { ... }
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Paginated responses include a `pagination` field:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"success": true,
|
|
19
|
+
"data": [ ... ],
|
|
20
|
+
"pagination": {
|
|
21
|
+
"page": 1,
|
|
22
|
+
"limit": 20,
|
|
23
|
+
"total": 100,
|
|
24
|
+
"totalPages": 5
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Error envelope
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"success": false,
|
|
34
|
+
"error": {
|
|
35
|
+
"code": "NOT_FOUND",
|
|
36
|
+
"message": "Resource not found",
|
|
37
|
+
"details": null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Common error codes: `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `UNPROCESSABLE_ENTITY`, `TOO_MANY_REQUESTS`, `INTERNAL_ERROR`.
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
- Bearer token in `Authorization` header
|
|
47
|
+
- JWT issued by `/api/auth/login`
|
|
48
|
+
- Public routes marked with `@Public()` decorator
|
|
49
|
+
|
|
50
|
+
## RBAC
|
|
51
|
+
|
|
52
|
+
Route permissions declared via `@Permissions("resource:action")` decorator.
|
|
53
|
+
Roles resolve against the addressed company scope.
|
|
54
|
+
|
|
55
|
+
| Role | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| owner | Full access |
|
|
58
|
+
| admin | Manage members, settings |
|
|
59
|
+
| member | Standard access |
|
|
60
|
+
| viewer | Read-only |
|
|
61
|
+
|
|
62
|
+
## Pagination
|
|
63
|
+
|
|
64
|
+
Query params: `page` (default 1), `limit` (default 20, max 100).
|
|
65
|
+
|
|
66
|
+
## Search
|
|
67
|
+
|
|
68
|
+
`search` query param performs case-insensitive text matching.
|
|
69
|
+
|
|
70
|
+
## Sorting
|
|
71
|
+
|
|
72
|
+
`sort` and `order` query params (e.g. `?sort=createdAt&order=desc`).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "{{description}}",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev --turbopack",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "next lint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^16.0.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0",
|
|
16
|
+
"next-auth": "^4.24.0",
|
|
17
|
+
"next-themes": "^0.4.0",
|
|
18
|
+
"@tanstack/react-query": "^5.60.0",
|
|
19
|
+
"@tanstack/react-table": "^8.20.0",
|
|
20
|
+
"react-hook-form": "^7.53.0",
|
|
21
|
+
"@hookform/resolvers": "^3.9.0",
|
|
22
|
+
"zod": "^3.23.0",
|
|
23
|
+
"socket.io-client": "^4.8.0",
|
|
24
|
+
"iconsax-react": "^0.0.8",
|
|
25
|
+
"clsx": "^2.1.0",
|
|
26
|
+
"tailwind-merge": "^2.5.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"typescript": "^5.6.0",
|
|
33
|
+
"tailwindcss": "^4.0.0",
|
|
34
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
35
|
+
"postcss": "^8.4.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme inline {
|
|
4
|
+
--app-primary-50: #eff6ff;
|
|
5
|
+
--app-primary-100: #dbeafe;
|
|
6
|
+
--app-primary-200: #bfdbfe;
|
|
7
|
+
--app-primary-300: #93c5fd;
|
|
8
|
+
--app-primary-400: #60a5fa;
|
|
9
|
+
--app-primary-500: #3b82f6;
|
|
10
|
+
--app-primary-600: #2563eb;
|
|
11
|
+
--app-primary-700: #1d4ed8;
|
|
12
|
+
--app-primary-800: #1e40af;
|
|
13
|
+
--app-primary-900: #1e3a8a;
|
|
14
|
+
|
|
15
|
+
--app-bg-primary: #ffffff;
|
|
16
|
+
--app-bg-secondary: #f8fafc;
|
|
17
|
+
--app-bg-tertiary: #f1f5f9;
|
|
18
|
+
--app-bg-inverse: #0f172a;
|
|
19
|
+
|
|
20
|
+
--app-text-primary: #0f172a;
|
|
21
|
+
--app-text-secondary: #475569;
|
|
22
|
+
--app-text-tertiary: #94a3b8;
|
|
23
|
+
--app-text-inverse: #f8fafc;
|
|
24
|
+
|
|
25
|
+
--app-border-default: #e2e8f0;
|
|
26
|
+
--app-border-strong: #cbd5e1;
|
|
27
|
+
|
|
28
|
+
--app-radius: 0.5rem;
|
|
29
|
+
|
|
30
|
+
--color-background: var(--app-bg-primary);
|
|
31
|
+
--color-foreground: var(--app-text-primary);
|
|
32
|
+
--color-muted: var(--app-bg-secondary);
|
|
33
|
+
--color-muted-foreground: var(--app-text-secondary);
|
|
34
|
+
--color-border: var(--app-border-default);
|
|
35
|
+
--color-primary: var(--app-primary-600);
|
|
36
|
+
--color-primary-foreground: var(--app-text-inverse);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@custom-variant dark (&:is(.dark *));
|
|
40
|
+
|
|
41
|
+
@layer base {
|
|
42
|
+
* {
|
|
43
|
+
@apply border-border;
|
|
44
|
+
}
|
|
45
|
+
body {
|
|
46
|
+
@apply bg-background text-foreground;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import { Providers } from "@/lib/providers";
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
|
|
6
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: "{{projectName}}",
|
|
10
|
+
description: "{{description}}",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function RootLayout({
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<html lang="en" suppressHydrationWarning>
|
|
20
|
+
<body className={inter.className}>
|
|
21
|
+
<Providers>{children}</Providers>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Loader2 } from "lucide-react";
|
|
2
|
+
import { type ButtonHTMLAttributes, forwardRef } from "react";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: "h-10 px-4 py-2",
|
|
23
|
+
sm: "h-9 rounded-md px-3",
|
|
24
|
+
lg: "h-11 rounded-md px-8",
|
|
25
|
+
icon: "h-10 w-10",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export interface AppButtonProps
|
|
36
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AppButton = forwardRef<HTMLButtonElement, AppButtonProps>(
|
|
42
|
+
({ className, variant, size, loading, disabled, children, ...props }, ref) => {
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
ref={ref}
|
|
46
|
+
data-slot="app-button"
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
disabled={disabled || loading}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{loading && <Loader2 className="h-4 w-4 animate-spin" variant="Outline" color="currentColor" />}
|
|
52
|
+
{children}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
AppButton.displayName = "AppButton";
|
|
58
|
+
|
|
59
|
+
export { AppButton, buttonVariants };
|