@quanticjs/create-app 0.1.1
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/deps.d.ts +4 -0
- package/dist/deps.js +97 -0
- package/dist/deps.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.js +20 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/bff.d.ts +2 -0
- package/dist/generators/bff.js +9 -0
- package/dist/generators/bff.js.map +1 -0
- package/dist/generators/claude.d.ts +2 -0
- package/dist/generators/claude.js +45 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.js +10 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/e2e.d.ts +2 -0
- package/dist/generators/e2e.js +8 -0
- package/dist/generators/e2e.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.js +28 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/module.d.ts +2 -0
- package/dist/generators/module.js +35 -0
- package/dist/generators/module.js.map +1 -0
- package/dist/generators/root.d.ts +2 -0
- package/dist/generators/root.js +7 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/scripts.d.ts +2 -0
- package/dist/generators/scripts.js +10 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +8 -0
- package/dist/prompts.js +53 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +79 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/utils/exec.d.ts +2 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/template.d.ts +10 -0
- package/dist/utils/template.js +20 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validate.d.ts +4 -0
- package/dist/utils/validate.js +27 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +50 -0
- package/templates/backend/app.module.ts.ejs +61 -0
- package/templates/backend/bff.controller.ts.ejs +60 -0
- package/templates/backend/bff.module.ts.ejs +10 -0
- package/templates/backend/bff.service.ts.ejs +6 -0
- package/templates/backend/create-schema.migration.ts.ejs +15 -0
- package/templates/backend/data-source.ts.ejs +13 -0
- package/templates/backend/env.example.ejs +30 -0
- package/templates/backend/main.ts.ejs +43 -0
- package/templates/backend/module.ts.ejs +14 -0
- package/templates/backend/nest-cli.json.ejs +8 -0
- package/templates/backend/package.json.ejs +23 -0
- package/templates/backend/tsconfig.build.json.ejs +4 -0
- package/templates/backend/tsconfig.json.ejs +24 -0
- package/templates/claude/CLAUDE.md.ejs +86 -0
- package/templates/claude/hooks/auto-format.sh +22 -0
- package/templates/claude/hooks/check-secrets.sh +49 -0
- package/templates/claude/hooks/guard-destructive.sh +42 -0
- package/templates/claude/hooks/on-compaction.sh +29 -0
- package/templates/claude/mcp.json +10 -0
- package/templates/claude/rules/api-patterns.md +86 -0
- package/templates/claude/rules/auth-patterns.md +109 -0
- package/templates/claude/rules/backend-patterns.md +421 -0
- package/templates/claude/rules/database-patterns.md +96 -0
- package/templates/claude/rules/docker-patterns.md +86 -0
- package/templates/claude/rules/frontend-patterns.md +262 -0
- package/templates/claude/rules/observability-backend.md +132 -0
- package/templates/claude/rules/observability-frontend.md +49 -0
- package/templates/claude/rules/playwright-mcp.md +80 -0
- package/templates/claude/rules/resilience-ops.md +103 -0
- package/templates/claude/rules/testing-e2e-ui.md +190 -0
- package/templates/claude/rules/testing-patterns.md +94 -0
- package/templates/claude/rules/workflow-backend.md +64 -0
- package/templates/claude/rules/workflow-frontend.md +60 -0
- package/templates/claude/settings.json +68 -0
- package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
- package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
- package/templates/claude/skills/add-entity/SKILL.md +56 -0
- package/templates/claude/skills/add-event/SKILL.md +127 -0
- package/templates/claude/skills/add-feature/SKILL.md +20 -0
- package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
- package/templates/claude/skills/add-handler/SKILL.md +105 -0
- package/templates/claude/skills/add-integration/SKILL.md +176 -0
- package/templates/claude/skills/add-migration/SKILL.md +20 -0
- package/templates/claude/skills/add-module/SKILL.md +89 -0
- package/templates/claude/skills/add-realtime/SKILL.md +119 -0
- package/templates/claude/skills/audit-rules/SKILL.md +120 -0
- package/templates/claude/skills/debugging/SKILL.md +105 -0
- package/templates/claude/skills/docker-dev/SKILL.md +86 -0
- package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
- package/templates/claude/skills/e2e-full/SKILL.md +132 -0
- package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
- package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
- package/templates/claude/skills/fix-bug/SKILL.md +33 -0
- package/templates/claude/skills/implement-spec/SKILL.md +98 -0
- package/templates/claude/skills/review-code/SKILL.md +109 -0
- package/templates/claude/skills/review-spec/SKILL.md +216 -0
- package/templates/claude/skills/run-tests/SKILL.md +37 -0
- package/templates/claude/skills/specify/SKILL.md +87 -0
- package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
- package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
- package/templates/docker/Dockerfile.client.ejs +14 -0
- package/templates/docker/Dockerfile.ejs +28 -0
- package/templates/docker/docker-compose.test.yml.ejs +54 -0
- package/templates/docker/docker-compose.yml.ejs +76 -0
- package/templates/docker/nginx.conf.ejs +21 -0
- package/templates/frontend/App.tsx.ejs +64 -0
- package/templates/frontend/DashboardPage.tsx.ejs +37 -0
- package/templates/frontend/LoginPage.tsx.ejs +20 -0
- package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
- package/templates/frontend/api-client.ts.ejs +15 -0
- package/templates/frontend/index.css.ejs +57 -0
- package/templates/frontend/index.html.ejs +13 -0
- package/templates/frontend/main.tsx.ejs +10 -0
- package/templates/frontend/package.json.ejs +16 -0
- package/templates/frontend/playwright.config.ts.ejs +20 -0
- package/templates/frontend/postcss.config.js.ejs +3 -0
- package/templates/frontend/smoke.spec.ts.ejs +37 -0
- package/templates/frontend/tailwind.config.ts.ejs +56 -0
- package/templates/frontend/tsconfig.json.ejs +25 -0
- package/templates/frontend/tsconfig.node.json.ejs +15 -0
- package/templates/frontend/utils.ts.ejs +6 -0
- package/templates/frontend/vite-env.d.ts.ejs +1 -0
- package/templates/frontend/vite.config.ts.ejs +20 -0
- package/templates/root/gitignore.ejs +9 -0
- package/templates/root/prettierrc.ejs +7 -0
- package/templates/scripts/init-db.sh.ejs +8 -0
- package/templates/scripts/save-auth-state.ts.ejs +24 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface TemplateContext {
|
|
2
|
+
projectName: string;
|
|
3
|
+
projectDescription: string;
|
|
4
|
+
projectNameUnderscored: string;
|
|
5
|
+
projectNamePascal: string;
|
|
6
|
+
modules: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function renderTemplate(templatePath: string, ctx: TemplateContext): string;
|
|
9
|
+
export declare function writeFile(filePath: string, content: string): void;
|
|
10
|
+
export declare function renderAndWrite(templatePath: string, outputPath: string, ctx: TemplateContext): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
|
|
7
|
+
export function renderTemplate(templatePath, ctx) {
|
|
8
|
+
const fullPath = join(TEMPLATES_DIR, templatePath);
|
|
9
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
10
|
+
return ejs.render(content, ctx);
|
|
11
|
+
}
|
|
12
|
+
export function writeFile(filePath, content) {
|
|
13
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
14
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
export function renderAndWrite(templatePath, outputPath, ctx) {
|
|
17
|
+
const content = renderTemplate(templatePath, ctx);
|
|
18
|
+
writeFile(outputPath, content);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,GAAG,MAAM,KAAK,CAAC;AAEtB,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AAU/D,MAAM,UAAU,cAAc,CAAC,YAAoB,EAAE,GAAoB;IACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,OAAe;IACzD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,YAAoB,EACpB,UAAkB,EAClB,GAAoB;IAEpB,MAAM,OAAO,GAAG,cAAc,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IAClD,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function validateProjectName(name: string): string | true;
|
|
2
|
+
export declare function validateModuleName(name: string): string | true;
|
|
3
|
+
export declare function toUnderscored(kebab: string): string;
|
|
4
|
+
export declare function toPascalCase(kebab: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const KEBAB_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2
|
+
export function validateProjectName(name) {
|
|
3
|
+
if (!name)
|
|
4
|
+
return 'Project name is required';
|
|
5
|
+
if (!KEBAB_RE.test(name))
|
|
6
|
+
return 'Must be kebab-case (e.g. cr-workflow)';
|
|
7
|
+
if (name.length > 50)
|
|
8
|
+
return 'Must be 50 characters or fewer';
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
export function validateModuleName(name) {
|
|
12
|
+
if (!name)
|
|
13
|
+
return 'Module name is required';
|
|
14
|
+
if (!KEBAB_RE.test(name))
|
|
15
|
+
return 'Must be kebab-case (e.g. change-request)';
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export function toUnderscored(kebab) {
|
|
19
|
+
return kebab.replace(/-/g, '_');
|
|
20
|
+
}
|
|
21
|
+
export function toPascalCase(kebab) {
|
|
22
|
+
return kebab
|
|
23
|
+
.split('-')
|
|
24
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
25
|
+
.join('');
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/utils/validate.ts"],"names":[],"mappings":"AAAA,MAAM,QAAQ,GAAG,+BAA+B,CAAC;AAEjD,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI;QAAE,OAAO,0BAA0B,CAAC;IAC7C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,uCAAuC,CAAC;IACzE,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,gCAAgC,CAAC;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC,IAAI;QAAE,OAAO,yBAAyB,CAAC;IAC5C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,0CAA0C,CAAC;IAC5E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quanticjs/create-app",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold a QuanticJS project — NestJS modular monolith + React frontend",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-quanticjs-app": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"lint": "eslint src --ext .ts",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"quanticjs",
|
|
23
|
+
"nestjs",
|
|
24
|
+
"react",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"cli",
|
|
27
|
+
"cqrs"
|
|
28
|
+
],
|
|
29
|
+
"author": "QuanticJS",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"ejs": "^3.1.10",
|
|
35
|
+
"inquirer": "^9.3.0",
|
|
36
|
+
"ora": "^8.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/ejs": "^3.1.5",
|
|
40
|
+
"@types/inquirer": "^9.0.7",
|
|
41
|
+
"@types/node": "^20.14.0",
|
|
42
|
+
"typescript": "^5.5.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
3
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
4
|
+
import { CqrsModule } from '@nestjs/cqrs';
|
|
5
|
+
import { ScheduleModule } from '@nestjs/schedule';
|
|
6
|
+
import { LoggerModule } from 'nestjs-pino';
|
|
7
|
+
import { QuanticModule } from '@nestjs-cqrs/quanticjs';
|
|
8
|
+
import { QuanticHealthModule } from '@quanticjs/core';
|
|
9
|
+
import { BffModule } from './bff/bff.module';
|
|
10
|
+
<% modules.forEach(function(mod) { -%>
|
|
11
|
+
import { <%= toPascalCase(mod) %>Module } from './<%= mod %>/<%= mod %>.module';
|
|
12
|
+
<% }); -%>
|
|
13
|
+
|
|
14
|
+
@Module({
|
|
15
|
+
imports: [
|
|
16
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
17
|
+
|
|
18
|
+
LoggerModule.forRoot({
|
|
19
|
+
pinoHttp: {
|
|
20
|
+
transport:
|
|
21
|
+
process.env.NODE_ENV !== 'production'
|
|
22
|
+
? { target: 'pino-pretty', options: { colorize: true } }
|
|
23
|
+
: undefined,
|
|
24
|
+
serializers: {
|
|
25
|
+
req: (req: any) => ({ id: req.id, method: req.method, url: req.url }),
|
|
26
|
+
res: (res: any) => ({ statusCode: res.statusCode }),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
TypeOrmModule.forRootAsync({
|
|
32
|
+
imports: [ConfigModule],
|
|
33
|
+
inject: [ConfigService],
|
|
34
|
+
useFactory: (config: ConfigService) => ({
|
|
35
|
+
type: 'postgres',
|
|
36
|
+
host: config.get('DATABASE_HOST', 'localhost'),
|
|
37
|
+
port: config.get<number>('DATABASE_PORT', 5432),
|
|
38
|
+
username: config.get('DATABASE_USER', 'postgres'),
|
|
39
|
+
password: config.get('DATABASE_PASSWORD', 'postgres'),
|
|
40
|
+
database: config.get('DATABASE_NAME', '<%= projectNameUnderscored %>'),
|
|
41
|
+
autoLoadEntities: true,
|
|
42
|
+
synchronize: false,
|
|
43
|
+
logging: config.get('NODE_ENV') !== 'production',
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
QuanticModule.forRoot({
|
|
48
|
+
redis: { url: process.env.REDIS_URL ?? 'redis://localhost:6379' },
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
QuanticHealthModule.forRoot(),
|
|
52
|
+
ScheduleModule.forRoot(),
|
|
53
|
+
CqrsModule.forRoot(),
|
|
54
|
+
|
|
55
|
+
BffModule,
|
|
56
|
+
<% modules.forEach(function(mod) { -%>
|
|
57
|
+
<%= toPascalCase(mod) %>Module,
|
|
58
|
+
<% }); -%>
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
export class AppModule {}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Controller, Get, Post, Req, Res, Query } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
|
3
|
+
import { Request, Response } from 'express';
|
|
4
|
+
import { Public } from '@nestjs-cqrs/quanticjs';
|
|
5
|
+
import { BffService } from './bff.service';
|
|
6
|
+
|
|
7
|
+
@ApiTags('auth')
|
|
8
|
+
@Controller('auth')
|
|
9
|
+
export class BffController {
|
|
10
|
+
constructor(private readonly bffService: BffService) {}
|
|
11
|
+
|
|
12
|
+
@Public()
|
|
13
|
+
@Get('login')
|
|
14
|
+
@ApiOperation({ summary: 'Redirect to Keycloak login' })
|
|
15
|
+
login(
|
|
16
|
+
@Query('provider') provider: string,
|
|
17
|
+
@Query('returnTo') returnTo: string,
|
|
18
|
+
@Res() res: Response,
|
|
19
|
+
) {
|
|
20
|
+
// TODO: implement PKCE + Keycloak redirect
|
|
21
|
+
res.redirect('/');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Public()
|
|
25
|
+
@Get('callback')
|
|
26
|
+
@ApiOperation({ summary: 'OIDC callback — exchange code for tokens' })
|
|
27
|
+
async callback(@Req() req: Request, @Res() res: Response) {
|
|
28
|
+
// TODO: exchange code, store tokens in Redis, set httpOnly cookie
|
|
29
|
+
res.redirect('/');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Post('refresh')
|
|
33
|
+
@ApiOperation({ summary: 'Refresh access token' })
|
|
34
|
+
async refresh(@Req() req: Request) {
|
|
35
|
+
// TODO: refresh via Redis-stored refresh token
|
|
36
|
+
return { success: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Post('logout')
|
|
40
|
+
@ApiOperation({ summary: 'Logout — clear session' })
|
|
41
|
+
async logout(@Req() req: Request, @Res() res: Response) {
|
|
42
|
+
// TODO: clear cookie, revoke Keycloak session, invalidate Redis
|
|
43
|
+
res.clearCookie('sid').json({ success: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Get('me')
|
|
47
|
+
@ApiOperation({ summary: 'Get current user info' })
|
|
48
|
+
async me(@Req() req: Request) {
|
|
49
|
+
// TODO: read session from Redis, return user info
|
|
50
|
+
return {
|
|
51
|
+
id: '',
|
|
52
|
+
keycloakId: '',
|
|
53
|
+
email: '',
|
|
54
|
+
displayName: '',
|
|
55
|
+
role: 'user',
|
|
56
|
+
roles: ['user'],
|
|
57
|
+
permissions: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { BffController } from './bff.controller';
|
|
3
|
+
import { BffService } from './bff.service';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [BffController],
|
|
7
|
+
providers: [BffService],
|
|
8
|
+
exports: [BffService],
|
|
9
|
+
})
|
|
10
|
+
export class BffModule {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export class CreateSchemas<%= timestamp %> implements MigrationInterface {
|
|
4
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
5
|
+
<% modules.forEach(function(mod) { -%>
|
|
6
|
+
await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS <%= mod.replace(/-/g, '_') %>`);
|
|
7
|
+
<% }); -%>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
11
|
+
<% modules.slice().reverse().forEach(function(mod) { -%>
|
|
12
|
+
await queryRunner.query(`DROP SCHEMA IF EXISTS <%= mod.replace(/-/g, '_') %> CASCADE`);
|
|
13
|
+
<% }); -%>
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DataSource } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export default new DataSource({
|
|
4
|
+
type: 'postgres',
|
|
5
|
+
host: process.env.DATABASE_HOST ?? 'localhost',
|
|
6
|
+
port: parseInt(process.env.DATABASE_PORT ?? '5432', 10),
|
|
7
|
+
username: process.env.DATABASE_USER ?? 'postgres',
|
|
8
|
+
password: process.env.DATABASE_PASSWORD ?? 'postgres',
|
|
9
|
+
database: process.env.DATABASE_NAME ?? '<%= projectNameUnderscored %>',
|
|
10
|
+
entities: ['src/**/entities/*.ts'],
|
|
11
|
+
migrations: ['src/migrations/*.ts'],
|
|
12
|
+
synchronize: false,
|
|
13
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Application
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=3000
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
DATABASE_HOST=postgres
|
|
7
|
+
DATABASE_PORT=5432
|
|
8
|
+
DATABASE_USER=postgres
|
|
9
|
+
DATABASE_PASSWORD=postgres
|
|
10
|
+
DATABASE_NAME=<%= projectNameUnderscored %>
|
|
11
|
+
|
|
12
|
+
# Redis
|
|
13
|
+
REDIS_URL=redis://redis:6379
|
|
14
|
+
|
|
15
|
+
# Keycloak
|
|
16
|
+
KEYCLOAK_URL=http://keycloak:8080
|
|
17
|
+
KEYCLOAK_REALM=<%= projectName %>
|
|
18
|
+
KEYCLOAK_CLIENT_ID=<%= projectName %>-backend
|
|
19
|
+
KEYCLOAK_CLIENT_SECRET=change-me
|
|
20
|
+
|
|
21
|
+
# Session
|
|
22
|
+
SESSION_SECRET=change-me-in-production
|
|
23
|
+
SESSION_COOKIE_NAME=sid
|
|
24
|
+
|
|
25
|
+
# Sentry (optional)
|
|
26
|
+
SENTRY_DSN=
|
|
27
|
+
|
|
28
|
+
# Unleash (optional — features enabled by default if not set)
|
|
29
|
+
UNLEASH_URL=
|
|
30
|
+
UNLEASH_API_KEY=
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
3
|
+
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
4
|
+
import { Logger } from 'nestjs-pino';
|
|
5
|
+
import * as cookieParser from 'cookie-parser';
|
|
6
|
+
import { GlobalExceptionFilter, ResultInterceptor } from '@nestjs-cqrs/quanticjs';
|
|
7
|
+
import { AppModule } from './app.module';
|
|
8
|
+
|
|
9
|
+
async function bootstrap() {
|
|
10
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
11
|
+
|
|
12
|
+
app.useLogger(app.get(Logger));
|
|
13
|
+
app.use(cookieParser());
|
|
14
|
+
app.setGlobalPrefix('api', { exclude: ['auth/(.*)'] });
|
|
15
|
+
|
|
16
|
+
app.useGlobalFilters(app.get(GlobalExceptionFilter));
|
|
17
|
+
app.useGlobalInterceptors(app.get(ResultInterceptor));
|
|
18
|
+
|
|
19
|
+
app.useGlobalPipes(
|
|
20
|
+
new ValidationPipe({
|
|
21
|
+
whitelist: true,
|
|
22
|
+
forbidNonWhitelisted: true,
|
|
23
|
+
transform: true,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
28
|
+
const config = new DocumentBuilder()
|
|
29
|
+
.setTitle('<%= projectNamePascal %>')
|
|
30
|
+
.setDescription('API documentation')
|
|
31
|
+
.setVersion('1.0')
|
|
32
|
+
.addBearerAuth()
|
|
33
|
+
.build();
|
|
34
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
35
|
+
SwaggerModule.setup('api/docs', app, document);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
app.enableShutdownHooks();
|
|
39
|
+
|
|
40
|
+
const port = process.env.PORT ?? 3000;
|
|
41
|
+
await app.listen(port);
|
|
42
|
+
}
|
|
43
|
+
bootstrap();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { CqrsModule } from '@nestjs/cqrs';
|
|
3
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
4
|
+
|
|
5
|
+
const CommandHandlers: never[] = [];
|
|
6
|
+
const QueryHandlers: never[] = [];
|
|
7
|
+
const Validators: never[] = [];
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [CqrsModule, TypeOrmModule.forFeature([])],
|
|
11
|
+
controllers: [],
|
|
12
|
+
providers: [...CommandHandlers, ...QueryHandlers, ...Validators],
|
|
13
|
+
})
|
|
14
|
+
export class <%= modulePascal %>Module {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "<%= projectDescription %>",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "nest build",
|
|
8
|
+
"start": "nest start",
|
|
9
|
+
"start:dev": "nest start --watch",
|
|
10
|
+
"start:debug": "nest start --debug --watch",
|
|
11
|
+
"start:prod": "node dist/main",
|
|
12
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
13
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:cov": "jest --coverage",
|
|
17
|
+
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
18
|
+
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d src/data-source.ts",
|
|
19
|
+
"migration:generate": "npm run typeorm -- migration:generate",
|
|
20
|
+
"migration:run": "npm run typeorm -- migration:run",
|
|
21
|
+
"migration:revert": "npm run typeorm -- migration:revert"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
"strict": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["src/*"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# <%= projectNamePascal %>
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
|
|
5
|
+
- **Backend:** NestJS (modular monolith), CQRS with pipeline behaviors, TypeORM, PostgreSQL, Redis, Keycloak BFF auth
|
|
6
|
+
- **Frontend:** React, Vite, TanStack Query, Zustand, React Hook Form + Zod, Tailwind CSS, shadcn/ui
|
|
7
|
+
- **Infrastructure:** Docker Compose (local dev), Kubernetes + Helm + ArgoCD (production)
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
**Modular monolith.** One backend Docker image, one frontend Docker image. Modules communicate via CommandBus/QueryBus. Each module owns its own PostgreSQL schema.
|
|
12
|
+
|
|
13
|
+
**CQRS.** Every operation is a Command/Query class + Handler. Controllers are thin — they only parse requests and dispatch to the bus.
|
|
14
|
+
|
|
15
|
+
**BFF Authentication.** Tokens stored server-side in Redis. Browser gets httpOnly cookies only. No tokens in localStorage/sessionStorage ever.
|
|
16
|
+
|
|
17
|
+
## Dev Workflow
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Start backend + infrastructure
|
|
21
|
+
docker compose up
|
|
22
|
+
|
|
23
|
+
# Start frontend (separate terminal)
|
|
24
|
+
cd client && npm run dev
|
|
25
|
+
|
|
26
|
+
# Browser
|
|
27
|
+
open http://localhost:5173
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Ports:** 5173 (frontend/Vite), 3000 (backend API), 8080 (Keycloak)
|
|
31
|
+
|
|
32
|
+
## Key Conventions
|
|
33
|
+
|
|
34
|
+
- Database columns are **camelCase** (TypeORM default naming strategy)
|
|
35
|
+
- Handlers return `Result<T>` — never throw for business errors
|
|
36
|
+
- Validation: DTOs use class-validator, Commands use Zod via `@Validate`
|
|
37
|
+
- Entities extend `BaseEntity` from the shared kernel
|
|
38
|
+
- Generate migrations from entities: `npx typeorm migration:generate src/migrations/Name`
|
|
39
|
+
|
|
40
|
+
## How to Build — Skill Routing
|
|
41
|
+
|
|
42
|
+
Use the appropriate skill for each task. Skills enforce architectural rules automatically.
|
|
43
|
+
|
|
44
|
+
| When you need to... | Use |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Add a full feature (backend + frontend) | `/add-feature` (orchestrates all sub-skills) |
|
|
47
|
+
| Add a command/query + validator + handler | `/add-handler` |
|
|
48
|
+
| Add a TypeORM entity | `/add-entity` |
|
|
49
|
+
| Generate a database migration | `/add-migration` |
|
|
50
|
+
| Wire a handler to an HTTP endpoint | `/add-api-endpoint` |
|
|
51
|
+
| Add an authenticated/role-gated endpoint | `/add-auth-endpoint` |
|
|
52
|
+
| Create a new bounded context module | `/add-module` |
|
|
53
|
+
| Add a React page with data fetching | `/add-frontend-page` |
|
|
54
|
+
| Integrate an external API (AI, payment, etc.) | `/add-integration` |
|
|
55
|
+
| Add inter-module async events | `/add-event` |
|
|
56
|
+
| Add live WebSocket updates | `/add-realtime` |
|
|
57
|
+
| Write backend tests | `/write-backend-tests` |
|
|
58
|
+
| Write Playwright E2E tests | `/write-ui-tests` |
|
|
59
|
+
| Find missing E2E coverage | `/e2e-scan` |
|
|
60
|
+
| Walk through journeys via MCP browser | `/e2e-verify` |
|
|
61
|
+
| Audit specs against real pages via MCP | `/e2e-audit` |
|
|
62
|
+
| Run full E2E suite (scan + audit + verify) | `/e2e-full` |
|
|
63
|
+
| Run the test suite | `/run-tests` |
|
|
64
|
+
| Fix a bug (TDD workflow) | `/fix-bug` |
|
|
65
|
+
| Review code before merge | `/review-code` |
|
|
66
|
+
| Debug a failing service | `/debugging` |
|
|
67
|
+
| Manage Docker dev environment | `/docker-dev` |
|
|
68
|
+
| Write a feature spec | `/specify` |
|
|
69
|
+
| Review a spec against rules | `/review-spec` (validates structure + rules compliance, writes corrected `-v2.md`) |
|
|
70
|
+
| Implement an existing spec | `/implement-spec` (spec-driven, picks only needed sub-skills) |
|
|
71
|
+
| Audit code against ADR rules | `/audit-rules` |
|
|
72
|
+
|
|
73
|
+
**When in doubt, start with `/add-feature`** — it walks through the full flow. If a spec already exists in `docs/specs/`, use `/implement-spec` instead.
|
|
74
|
+
|
|
75
|
+
## Test Commands
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Backend unit + integration tests
|
|
79
|
+
npm test
|
|
80
|
+
|
|
81
|
+
# Frontend tests
|
|
82
|
+
cd client && npm test
|
|
83
|
+
|
|
84
|
+
# E2E tests
|
|
85
|
+
cd client && npx playwright test
|
|
86
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: auto-format staged files before commit.
|
|
3
|
+
# Exit 0 = allow (formatting is best-effort, never blocks commit).
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
8
|
+
|
|
9
|
+
# Only run on git commit
|
|
10
|
+
if ! echo "$COMMAND" | grep -qE "git commit" 2>/dev/null; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Format staged .ts/.tsx/.js/.jsx files
|
|
15
|
+
FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(tsx?|jsx?)$' || true)
|
|
16
|
+
|
|
17
|
+
if [[ -n "$FILES" ]]; then
|
|
18
|
+
echo "$FILES" | xargs npx prettier --write >/dev/null 2>&1 || true
|
|
19
|
+
echo "$FILES" | xargs git add 2>/dev/null || true
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: prevent committing secrets.
|
|
3
|
+
# Exit 0 = allow, Exit 2 = block (stderr shown to Claude as feedback)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
8
|
+
|
|
9
|
+
# Only check git add and git commit commands
|
|
10
|
+
if ! echo "$COMMAND" | grep -qE "git (add|commit)" 2>/dev/null; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
ERRORS=""
|
|
15
|
+
|
|
16
|
+
# Check for .env files being staged
|
|
17
|
+
if echo "$COMMAND" | grep -qE "git add" 2>/dev/null; then
|
|
18
|
+
if echo "$COMMAND" | grep -qE "\\.env" 2>/dev/null && ! echo "$COMMAND" | grep -qE "\\.env\\.example" 2>/dev/null; then
|
|
19
|
+
ERRORS="$ERRORS\n❌ .env file being staged — contains secrets. Add to .gitignore instead."
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Check staged files for secrets patterns
|
|
24
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
25
|
+
if [[ -z "$STAGED" ]]; then
|
|
26
|
+
STAGED=$(git diff --name-only 2>/dev/null)
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
for file in $STAGED; do
|
|
30
|
+
[[ -f "$file" ]] || continue
|
|
31
|
+
[[ "$file" == *.ts || "$file" == *.tsx || "$file" == *.js || "$file" == *.json || "$file" == *.yaml || "$file" == *.yml ]] || continue
|
|
32
|
+
[[ "$file" == *.spec.* || "$file" == *.test.* ]] && continue
|
|
33
|
+
|
|
34
|
+
if grep -qE "(API_KEY|SECRET_KEY|PRIVATE_KEY|PASSWORD)\s*=\s*['\"][^'\"]+['\"]" "$file" 2>/dev/null; then
|
|
35
|
+
ERRORS="$ERRORS\n❌ $file: Hardcoded secret detected. Use environment variables."
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
# Check for private key files
|
|
40
|
+
if echo "$COMMAND" | grep -qE "\\.pem|\\.key|\\.p12" 2>/dev/null; then
|
|
41
|
+
ERRORS="$ERRORS\n❌ Private key file being staged. These must never be committed."
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [[ -n "$ERRORS" ]]; then
|
|
45
|
+
echo -e "$ERRORS" >&2
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: block irreversible commands.
|
|
3
|
+
# Exit 0 = allow, Exit 2 = block (stderr shown to Claude as feedback)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
8
|
+
|
|
9
|
+
if [[ -z "$COMMAND" ]]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Force push to main/master
|
|
14
|
+
if echo "$COMMAND" | grep -qE "git push.*--force.*(main|master)" 2>/dev/null; then
|
|
15
|
+
echo "❌ BLOCKED: Force push to main/master. This rewrites shared history permanently." >&2
|
|
16
|
+
exit 2
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if echo "$COMMAND" | grep -qE "git push.*-f.*(main|master)" 2>/dev/null; then
|
|
20
|
+
echo "❌ BLOCKED: Force push to main/master. This rewrites shared history permanently." >&2
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# git reset --hard
|
|
25
|
+
if echo "$COMMAND" | grep -qE "git reset --hard" 2>/dev/null; then
|
|
26
|
+
echo "❌ BLOCKED: git reset --hard discards uncommitted work permanently. Use git stash or create a backup branch first." >&2
|
|
27
|
+
exit 2
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# DROP TABLE / DROP DATABASE
|
|
31
|
+
if echo "$COMMAND" | grep -qiE "DROP (TABLE|DATABASE|SCHEMA)" 2>/dev/null; then
|
|
32
|
+
echo "❌ BLOCKED: DROP TABLE/DATABASE/SCHEMA is irreversible. Confirm with the user before proceeding." >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# rm -rf / or rm -rf .
|
|
37
|
+
if echo "$COMMAND" | grep -qE "rm -rf\s+(/|\.)\s*$" 2>/dev/null; then
|
|
38
|
+
echo "❌ BLOCKED: Catastrophic rm -rf target." >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
exit 0
|