@podosoft/podokit 0.5.1 → 0.7.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/README.md +6 -0
- package/dist/add.d.ts +24 -2
- package/dist/add.js +77 -20
- package/dist/assemble.js +4 -1
- package/dist/create.d.ts +2 -0
- package/dist/create.js +6 -0
- package/dist/eject.js +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -2
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +34 -0
- package/dist/lockfile.d.ts +7 -2
- package/dist/lockfile.js +37 -2
- package/dist/templates/base/AGENTS.md +30 -0
- package/dist/templates/base/CLAUDE.md +6 -0
- package/dist/templates/fullstack-nest-svelte/AGENTS.md +79 -0
- package/dist/templates/fullstack-nest-svelte/CLAUDE.md +6 -0
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +25 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +4 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.extensions.ts +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/index.ts +1 -3
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +5 -25
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +1 -4
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/static/favicon.svg +13 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +150 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-module/SKILL.md +27 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-nest-endpoint/SKILL.md +23 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-sveltekit-route/SKILL.md +23 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-update/SKILL.md +26 -0
- package/dist/templates/fullstack-nest-svelte/dot-cursor/rules/podokit.mdc +11 -0
- package/dist/templates/fullstack-nest-svelte/dot-devcontainer/devcontainer.json +20 -0
- package/dist/templates/fullstack-nest-svelte/dot-dockerignore +8 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.docker +26 -0
- package/dist/templates/fullstack-nest-svelte/dot-github/copilot-instructions.md +6 -0
- package/dist/templates/fullstack-nest-svelte/dot-mcp.json +8 -0
- package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +87 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.module.ts +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +84 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +12 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +37 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +25 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +32 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +16 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +52 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +38 -13
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +203 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/+layout.svelte +7 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +8 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +34 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/maintenance/+page.svelte +25 -0
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +43 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +42 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +22 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +27 -0
- package/dist/templates/modules/auth/module.manifest.json +9 -8
- package/dist/templates/modules/bullmq/module.manifest.json +1 -1
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.controller.ts +3 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/db.ts +13 -0
- package/dist/templates/modules/{auth → mailer}/files/apps/api/src/mail/mailer.ts +41 -7
- package/dist/templates/modules/mailer/module.manifest.json +38 -0
- package/dist/templates/todo/AGENTS.md +80 -0
- package/dist/templates/todo/CLAUDE.md +6 -0
- package/dist/templates/todo/Dockerfile.dev +25 -0
- package/dist/templates/todo/apps/api/package.json +4 -2
- package/dist/templates/todo/apps/api/src/app.extensions.ts +21 -0
- package/dist/templates/todo/apps/api/src/app.module.ts +3 -0
- package/dist/templates/todo/apps/web/src/app.html +2 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/index.ts +1 -3
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +1 -1
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +5 -25
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +1 -4
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs.svelte +1 -1
- package/dist/templates/todo/apps/web/static/favicon.svg +13 -0
- package/dist/templates/todo/apps/web/vite.config.ts +12 -0
- package/dist/templates/todo/compose.dev.yaml +150 -0
- package/dist/templates/todo/dot-claude/skills/podokit-add-module/SKILL.md +27 -0
- package/dist/templates/todo/dot-claude/skills/podokit-nest-endpoint/SKILL.md +23 -0
- package/dist/templates/todo/dot-claude/skills/podokit-sveltekit-route/SKILL.md +23 -0
- package/dist/templates/todo/dot-claude/skills/podokit-update/SKILL.md +26 -0
- package/dist/templates/todo/dot-cursor/rules/podokit.mdc +11 -0
- package/dist/templates/todo/dot-devcontainer/devcontainer.json +20 -0
- package/dist/templates/todo/dot-dockerignore +8 -0
- package/dist/templates/todo/dot-env.docker +26 -0
- package/dist/templates/todo/dot-github/copilot-instructions.md +6 -0
- package/dist/templates/todo/dot-mcp.json +8 -0
- package/dist/templates/todo/infra/traefik/dynamic.yml +13 -0
- package/package.json +9 -1
|
@@ -45,7 +45,34 @@ export function createFeatureGate(pool: Pool) {
|
|
|
45
45
|
return cache;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Registration can be closed from the admin Settings ("Allow sign-up"). It is a
|
|
49
|
+
// site setting (`site.allowSignup`), so the row is absent until an admin turns
|
|
50
|
+
// it off — a missing/any-non-"false" value means open, matching the shipped
|
|
51
|
+
// default. Cached the same way as the feature flags.
|
|
52
|
+
let signupOpen = true;
|
|
53
|
+
let signupFetchedAt = 0;
|
|
54
|
+
async function isSignupOpen(): Promise<boolean> {
|
|
55
|
+
if (Date.now() - signupFetchedAt < CACHE_TTL_MS) return signupOpen;
|
|
56
|
+
try {
|
|
57
|
+
const res = await pool.query<{ value: string }>(
|
|
58
|
+
'SELECT "value" FROM "app_setting" WHERE "key" = $1',
|
|
59
|
+
["site.allowSignup"],
|
|
60
|
+
);
|
|
61
|
+
signupOpen = res.rows[0]?.value !== "false";
|
|
62
|
+
} catch {
|
|
63
|
+
signupOpen = true;
|
|
64
|
+
}
|
|
65
|
+
signupFetchedAt = Date.now();
|
|
66
|
+
return signupOpen;
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
return createAuthMiddleware(async (ctx) => {
|
|
70
|
+
// Block email/password registration when sign-up is closed. Social/invite
|
|
71
|
+
// flows are unaffected; admins create users from the dashboard.
|
|
72
|
+
if (ctx.path === "/sign-up/email" || ctx.path.startsWith("/sign-up/")) {
|
|
73
|
+
if (!(await isSignupOpen())) throw new APIError("FORBIDDEN", { message: "Sign-up is disabled." });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
49
76
|
const feature = FEATURE_PATHS.find((f) =>
|
|
50
77
|
f.prefixes.some((p) => ctx.path === p || ctx.path.startsWith(p)),
|
|
51
78
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth",
|
|
3
3
|
"description": "Full authentication (better-auth): email/password, sessions, and \u2014 via config \u2014 OAuth and 2FA. Installs a global guard so the API is secure by default; health and /api/auth stay public.",
|
|
4
|
-
"requires": [],
|
|
4
|
+
"requires": ["mailer"],
|
|
5
5
|
"targetApp": "api",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@podosoft/podokit-auth": "^0.1.0",
|
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
"better-auth": "^1.6.23",
|
|
10
10
|
"@thallesp/nestjs-better-auth": "^2.7.0",
|
|
11
11
|
"pg": "^8.12.0",
|
|
12
|
-
"nodemailer": "^7.0.9",
|
|
13
12
|
"@better-auth/api-key": "^1.6.23",
|
|
14
13
|
"@better-auth/passkey": "^1.6.23",
|
|
15
14
|
"@better-auth/oauth-provider": "^1.6.23"
|
|
16
15
|
},
|
|
17
16
|
"devDependencies": {
|
|
18
|
-
"@types/pg": "^8.11.10"
|
|
19
|
-
"@types/nodemailer": "^7.0.4"
|
|
17
|
+
"@types/pg": "^8.11.10"
|
|
20
18
|
},
|
|
21
19
|
"env": [
|
|
22
20
|
"# \u2500\u2500 Auth (better-auth) \u2014 the only required secrets \u2500\u2500",
|
|
@@ -32,10 +30,7 @@
|
|
|
32
30
|
"# override (e.g. for secrets-manager injection in production); the DB takes",
|
|
33
31
|
"# precedence when set.",
|
|
34
32
|
"# --- Optional overrides / local development ---",
|
|
35
|
-
"# Email
|
|
36
|
-
"# SMTP_HOST=localhost",
|
|
37
|
-
"# SMTP_PORT=1025",
|
|
38
|
-
"# MAIL_FROM=PodoKit <no-reply@example.com>",
|
|
33
|
+
"# Email/SMTP is provided by the mailer module (see its .env block).",
|
|
39
34
|
"# Phone-number SMS webhook (dev: SMS sink on port 8095)",
|
|
40
35
|
"# SMS_WEBHOOK_URL=http://localhost:8095/sms",
|
|
41
36
|
"# AUTH_HIBP=false",
|
|
@@ -91,6 +86,12 @@
|
|
|
91
86
|
"file": "apps/api/src/app.module.ts",
|
|
92
87
|
"marker": "// podokit:end:providers",
|
|
93
88
|
"text": "{ provide: APP_GUARD, useClass: AuthGuard },"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"file": "AGENTS.md",
|
|
92
|
+
"marker": "<!-- podokit:end:agents-modules -->",
|
|
93
|
+
"optional": true,
|
|
94
|
+
"text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, and the server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Create the auth tables: `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`.\n"
|
|
94
95
|
}
|
|
95
96
|
],
|
|
96
97
|
"instructions": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start:worker": "node dist/main-worker",
|
|
12
|
-
"dev:worker": "nest start --watch --entryFile main-worker"
|
|
12
|
+
"dev:worker": "nest start --watch --builder swc --entryFile main-worker"
|
|
13
13
|
},
|
|
14
14
|
"env": [
|
|
15
15
|
"# bullmq uses REDIS_HOST / REDIS_PORT (already defined above)"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The API tsconfig restricts `types` to ["node"], which suppresses @types/multer's
|
|
2
|
+
// global augmentation of Express.Multer.File. Load it explicitly so the type resolves.
|
|
3
|
+
/// <reference types="multer" />
|
|
1
4
|
import { BadRequestException, Controller, Get, Param, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
|
|
2
5
|
import { FileInterceptor } from "@nestjs/platform-express";
|
|
3
6
|
import { ApiConsumes, ApiTags } from "@nestjs/swagger";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
|
|
3
|
+
// The mailer's own pg pool, in its own module so the mailer stays independent of
|
|
4
|
+
// any other module. Used only to read the optional admin-configured SMTP settings
|
|
5
|
+
// from `auth_config` when the auth module is present; with no such table the read
|
|
6
|
+
// fails softly and the mailer falls back to SMTP_* env (see mailer.ts).
|
|
7
|
+
export const pool = new Pool({
|
|
8
|
+
host: process.env.POSTGRES_HOST ?? "localhost",
|
|
9
|
+
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
10
|
+
user: process.env.POSTGRES_USER ?? "podokit",
|
|
11
|
+
password: process.env.POSTGRES_PASSWORD ?? "podokit",
|
|
12
|
+
database: process.env.POSTGRES_DB ?? "podokit",
|
|
13
|
+
});
|
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import nodemailer, { type Transporter } from "nodemailer";
|
|
2
|
-
import { pool } from "
|
|
2
|
+
import { pool } from "./db";
|
|
3
3
|
import { createConfigStore, type SmtpConfig } from "@podosoft/podokit-auth";
|
|
4
4
|
|
|
5
|
-
// SMTP is
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// Reusable email sending. SMTP is resolved in priority order, applied live (short
|
|
6
|
+
// TTL) so no restart is needed:
|
|
7
|
+
// 1. admin-configured DB settings (auth_config) — only when the auth module is
|
|
8
|
+
// installed; read via the config store, which decrypts the SMTP password;
|
|
9
|
+
// 2. SMTP_* env (fallback / production secrets-manager injection);
|
|
10
|
+
// 3. none → a JSON transport that logs the message so dev links are grabbable.
|
|
11
|
+
// The whole transport can also be overridden from app code (setMailTransport),
|
|
12
|
+
// e.g. from apps/api/src/app.extensions.ts, to plug in a provider SDK.
|
|
9
13
|
const store = createConfigStore(pool);
|
|
10
14
|
const TTL_MS = 3_000;
|
|
11
15
|
|
|
16
|
+
function envSmtp(): SmtpConfig | null {
|
|
17
|
+
if (!process.env.SMTP_HOST) return null;
|
|
18
|
+
return {
|
|
19
|
+
host: process.env.SMTP_HOST,
|
|
20
|
+
port: Number(process.env.SMTP_PORT ?? 1025),
|
|
21
|
+
secure: process.env.SMTP_SECURE === "true",
|
|
22
|
+
user: process.env.SMTP_USER,
|
|
23
|
+
pass: process.env.SMTP_PASS,
|
|
24
|
+
from: process.env.MAIL_FROM,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveSmtp(): Promise<SmtpConfig | null> {
|
|
29
|
+
try {
|
|
30
|
+
// Reads auth_config and falls back to env internally when auth is installed.
|
|
31
|
+
return await store.smtpConfig();
|
|
32
|
+
} catch {
|
|
33
|
+
// No auth_config table (auth not installed) — use env directly.
|
|
34
|
+
return envSmtp();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
function buildTransport(smtp: SmtpConfig | null): Transporter {
|
|
13
39
|
if (!smtp) return nodemailer.createTransport({ jsonTransport: true });
|
|
14
40
|
return nodemailer.createTransport({
|
|
@@ -24,12 +50,20 @@ let transportFrom = process.env.MAIL_FROM ?? "PodoKit <no-reply@example.com>";
|
|
|
24
50
|
let hasSmtp = false;
|
|
25
51
|
let checkedAt = 0;
|
|
26
52
|
let signature = "";
|
|
53
|
+
let override: Transporter | null = null;
|
|
54
|
+
|
|
55
|
+
/** Override the transport from app code (e.g. a provider SDK). Pass null to
|
|
56
|
+
* restore the config-driven transport. Set this from app.extensions.ts. */
|
|
57
|
+
export function setMailTransport(custom: Transporter | null): void {
|
|
58
|
+
override = custom;
|
|
59
|
+
}
|
|
27
60
|
|
|
28
61
|
async function getTransport(): Promise<Transporter> {
|
|
62
|
+
if (override) return override;
|
|
29
63
|
if (checkedAt !== 0 && Date.now() - checkedAt < TTL_MS) return transport;
|
|
30
64
|
checkedAt = Date.now();
|
|
31
65
|
try {
|
|
32
|
-
const smtp = await
|
|
66
|
+
const smtp = await resolveSmtp();
|
|
33
67
|
const next = JSON.stringify(smtp ?? null);
|
|
34
68
|
if (next !== signature) {
|
|
35
69
|
transport = buildTransport(smtp);
|
|
@@ -48,7 +82,7 @@ export type Mail = { to: string; subject: string; text: string; html?: string };
|
|
|
48
82
|
export async function sendMail(mail: Mail): Promise<void> {
|
|
49
83
|
const t = await getTransport();
|
|
50
84
|
const info = await t.sendMail({ from: transportFrom, ...mail });
|
|
51
|
-
if (!hasSmtp) {
|
|
85
|
+
if (!override && !hasSmtp) {
|
|
52
86
|
// JSON transport: surface the message so the link is grabbable from logs.
|
|
53
87
|
console.log(`[mailer] no SMTP configured; message not delivered:\n${info.message as string}`);
|
|
54
88
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mailer",
|
|
3
|
+
"description": "Reusable email sending (nodemailer). Resolves SMTP from the admin-configured DB settings when auth is installed, else SMTP_* env, else logs to the console. Used by auth and contact-form; the transport is overridable from app.extensions.ts.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"nodemailer": "^7.0.9",
|
|
8
|
+
"@podosoft/podokit-auth": "^0.1.0",
|
|
9
|
+
"pg": "^8.12.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/nodemailer": "^7.0.4",
|
|
13
|
+
"@types/pg": "^8.11.10"
|
|
14
|
+
},
|
|
15
|
+
"env": [
|
|
16
|
+
"# ── Mailer (SMTP) — optional ──",
|
|
17
|
+
"# With auth installed, configure SMTP on the admin Settings page (stored",
|
|
18
|
+
"# encrypted in the DB, applied live). These env vars are a fallback/override;",
|
|
19
|
+
"# with no SMTP configured at all the mailer logs messages to the console.",
|
|
20
|
+
"# Local dev uses Mailpit: docker compose --profile dev up → http://localhost:8025",
|
|
21
|
+
"# SMTP_HOST=localhost",
|
|
22
|
+
"# SMTP_PORT=1025",
|
|
23
|
+
"# MAIL_FROM=PodoKit <no-reply@example.com>"
|
|
24
|
+
],
|
|
25
|
+
"inject": [
|
|
26
|
+
{
|
|
27
|
+
"file": "AGENTS.md",
|
|
28
|
+
"marker": "<!-- podokit:end:agents-modules -->",
|
|
29
|
+
"optional": true,
|
|
30
|
+
"text": "### mailer\n- Send email with `import { sendMail, actionEmail } from \"../mail/mailer\"` (path relative to your file). `sendMail({ to, subject, text, html? })`.\n- SMTP comes from the admin Settings page (when auth is installed) or `SMTP_*` env; with neither, messages are logged to the console (dev links are grabbable from logs).\n- To use a provider SDK instead of SMTP, call `setMailTransport(...)` from `apps/api/src/app.extensions.ts`.\n"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"instructions": [
|
|
34
|
+
"Send mail from anywhere in the API: import { sendMail } from \"../mail/mailer\" then await sendMail({ to, subject, text }).",
|
|
35
|
+
"SMTP is optional: with auth installed, configure it on the admin Settings page (DB, applied live); otherwise set SMTP_HOST/SMTP_PORT/MAIL_FROM in .env. With none set, messages are logged to the console.",
|
|
36
|
+
"To send through a provider SDK, override the transport in apps/api/src/app.extensions.ts: import { setMailTransport } from \"./mail/mailer\"; setMailTransport(myTransport)."
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AGENTS.md — {{projectName}}
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents (Claude Code, Codex, Cursor, Copilot, …) working in
|
|
4
|
+
this repository. It was generated by [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
5
|
+
Follow it so your changes match the project's conventions.
|
|
6
|
+
|
|
7
|
+
## Project overview
|
|
8
|
+
|
|
9
|
+
A full-stack TypeScript monorepo (npm workspaces):
|
|
10
|
+
|
|
11
|
+
- `apps/api` — **NestJS** backend (`{{projectName}}-api`), TypeORM + PostgreSQL, Swagger at `/api-docs`.
|
|
12
|
+
- `apps/web` — **SvelteKit** frontend (`{{projectName}}-web`), Tailwind v4 + shadcn-svelte + typesafe-i18n.
|
|
13
|
+
- `infra/` — Docker Compose (PostgreSQL/Redis) and example k3s manifests.
|
|
14
|
+
- `.podokit/` — PodoKit's generation lockfile (see "PodoKit tooling" below). Do not edit by hand.
|
|
15
|
+
- **Todo example**: `apps/api/src/todos/` (controller/service/entity/DTO + migration) and the todo UI in `apps/web/src/routes/` show the end-to-end patterns to follow.
|
|
16
|
+
|
|
17
|
+
## Setup & commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
docker compose -f infra/docker/docker-compose.yml up -d # PostgreSQL (+ Redis)
|
|
23
|
+
npm run dev # api on http://localhost:5002, web on http://localhost:5001
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Ports: web = 5001, api = 5002.** The web app reaches the API only through its
|
|
27
|
+
SvelteKit server proxy (`/api/*`), never with a direct browser `fetch`.
|
|
28
|
+
- Build: `npm run build` · Type-check/lint: `npm run lint` · Unit tests: `npm test`
|
|
29
|
+
- Per workspace: `npm run build -w {{projectName}}-api` / `-w {{projectName}}-web`.
|
|
30
|
+
- DB migrations (TypeORM): `npm run migration:run -w {{projectName}}-api`.
|
|
31
|
+
- e2e (Playwright, ships in `tests/`): `npm run test:e2e`.
|
|
32
|
+
|
|
33
|
+
**Always** verify a change by building the affected workspace (and `svelte-check`
|
|
34
|
+
for web) before considering it done. UI changes need a Playwright test; API
|
|
35
|
+
changes need an e2e/unit test.
|
|
36
|
+
|
|
37
|
+
## Code style
|
|
38
|
+
|
|
39
|
+
### TypeScript (both apps)
|
|
40
|
+
- `strict` mode. **No `any`** — use `unknown` + narrowing. **No `@ts-ignore`/`@ts-expect-error`** and no `eslint-disable`.
|
|
41
|
+
- Explicit function return types. DTOs use `class-validator` decorators.
|
|
42
|
+
- Keep `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noUncheckedIndexedAccess`.
|
|
43
|
+
|
|
44
|
+
### Svelte 5 (web)
|
|
45
|
+
- **Runes only**: `$state()`, `$derived()`, `$effect()`, `$props()`. **Never** use `$:` (deprecated).
|
|
46
|
+
- Reactive stores go in `.svelte.ts` files; don't destructure a getter-exported rune (it loses reactivity) — wrap in `$derived`.
|
|
47
|
+
|
|
48
|
+
### UI — Tailwind + shadcn-svelte (web)
|
|
49
|
+
- Prefer **shadcn-svelte components** over raw HTML (`<button>`, `<input>`, `<select>`, …).
|
|
50
|
+
- **Do not edit `src/lib/components/ui/**`** (the shadcn primitives). Customize in a separate wrapper.
|
|
51
|
+
- Tailwind v4 (CSS-first, no `tailwind.config`). Use semantic classes: `bg-background`, `text-foreground`, `border-border`. Dark mode via `mode-watcher`. Responsive (`sm:`/`md:`/`lg:`).
|
|
52
|
+
|
|
53
|
+
### i18n & errors
|
|
54
|
+
- **Every user-facing string goes through i18n** (`src/lib/i18n/messages.ts`) — no hardcoded text. Add keys to **all** locales at once.
|
|
55
|
+
- The backend returns a stable **error envelope**: `{ success: false, error: { code, message, statusCode, path, timestamp, details? } }`. Throw `AppException("SOME_CODE", ...)` on the backend; **branch on `error.code`, not the message,** on the frontend.
|
|
56
|
+
- Logging is structured; no emojis/icons. Start messages with an action verb.
|
|
57
|
+
|
|
58
|
+
## PodoKit tooling & file ownership
|
|
59
|
+
|
|
60
|
+
This project is managed by the `podo` CLI. Files have an ownership tier recorded
|
|
61
|
+
in `.podokit/files.lock` — respect it:
|
|
62
|
+
|
|
63
|
+
- **owned** (your code: `apps/web/src/routes/**`, your components/services, `apps/api/src/app.extensions.ts`, and this `AGENTS.md`) — edit freely. To add or override Nest providers/modules, use `app.extensions.ts` (spread into `AppModule` after the module-wired providers, so a same-token override wins) rather than editing `app.module.ts`.
|
|
64
|
+
- **assembled** (e.g. `apps/api/src/app.module.ts`, `auth.ts`) — contain `// podokit:begin:… / …:end` fenced regions that the toolkit recomputes. Edit outside the fences only; **never hand-edit inside the fences**.
|
|
65
|
+
- **managed** (bootstrap/config like `main.ts`) — the toolkit may update these; avoid gratuitous edits.
|
|
66
|
+
|
|
67
|
+
Useful commands:
|
|
68
|
+
|
|
69
|
+
- `podo add <module>` — add a feature (auth, admin-dashboard, redis, bullmq, …). It wires itself into the app; run `podo add` with no argument to list modules.
|
|
70
|
+
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
71
|
+
- `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
|
|
72
|
+
|
|
73
|
+
## Commit conventions
|
|
74
|
+
|
|
75
|
+
Conventional Commits (`feat|fix|docs|refactor|test|chore`), imperative mood,
|
|
76
|
+
≤50-char subject, no trailing period, no emojis. One logical change per commit.
|
|
77
|
+
|
|
78
|
+
<!-- podokit:begin:agents-modules -->
|
|
79
|
+
<!-- Module-specific guidance is injected here by `podo add`. -->
|
|
80
|
+
<!-- podokit:end:agents-modules -->
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Development image for the PodoKit monorepo (web + api).
|
|
2
|
+
# Dependencies and source are baked in; `docker compose watch` syncs your
|
|
3
|
+
# edits into the running container so the native file watchers fire.
|
|
4
|
+
FROM node:22-bookworm-slim
|
|
5
|
+
|
|
6
|
+
# git for the toolchain (editors, AI agents); build tools for native modules.
|
|
7
|
+
RUN apt-get update \
|
|
8
|
+
&& apt-get install -y --no-install-recommends git python3 make g++ ca-certificates \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
|
|
13
|
+
# Install workspace dependencies (cached unless a package.json changes).
|
|
14
|
+
# NB: the host package-lock.json is intentionally NOT copied — a lockfile
|
|
15
|
+
# generated on macOS/Windows prunes platform-specific optional deps and can drop
|
|
16
|
+
# packages (e.g. @swc/cli) on linux. Resolving fresh in-container avoids that.
|
|
17
|
+
COPY package.json ./
|
|
18
|
+
COPY apps/api/package.json ./apps/api/package.json
|
|
19
|
+
COPY apps/web/package.json ./apps/web/package.json
|
|
20
|
+
RUN npm install
|
|
21
|
+
|
|
22
|
+
# Baked source (kept in sync during development by `docker compose watch`).
|
|
23
|
+
COPY . .
|
|
24
|
+
|
|
25
|
+
EXPOSE 5001 5002
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "nest start --watch",
|
|
6
|
+
"dev": "nest start --watch --builder swc",
|
|
7
7
|
"build": "nest build",
|
|
8
8
|
"start": "node dist/main",
|
|
9
9
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"supertest": "^7.0.0",
|
|
41
41
|
"ts-jest": "^29.2.5",
|
|
42
42
|
"ts-node": "^10.9.2",
|
|
43
|
-
"typescript": "^6.0.3"
|
|
43
|
+
"typescript": "^6.0.3",
|
|
44
|
+
"@swc/core": "^1.7.0",
|
|
45
|
+
"@swc/cli": "^0.4.0"
|
|
44
46
|
},
|
|
45
47
|
"jest": {
|
|
46
48
|
"moduleFileExtensions": [
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DynamicModule, Provider, Type } from "@nestjs/common";
|
|
2
|
+
|
|
3
|
+
// Owned extension slot for the Nest app. This file is yours — PodoKit never
|
|
4
|
+
// writes to it, so `podo update` leaves it alone. Use it to add your own modules
|
|
5
|
+
// or providers, or to override a PodoKit-provided provider without editing
|
|
6
|
+
// managed code. Because these are spread in *after* the module-wired providers,
|
|
7
|
+
// an override here takes precedence.
|
|
8
|
+
//
|
|
9
|
+
// Example — override a provider with your own implementation:
|
|
10
|
+
// import { SomeService } from "./some/some.service";
|
|
11
|
+
// import { MyService } from "./my-service";
|
|
12
|
+
// export const extensionProviders: Provider[] = [
|
|
13
|
+
// { provide: SomeService, useClass: MyService },
|
|
14
|
+
// ];
|
|
15
|
+
|
|
16
|
+
/** Extra modules to import into AppModule (or DynamicModule overrides). */
|
|
17
|
+
export const extensionImports: (Type | DynamicModule)[] = [];
|
|
18
|
+
|
|
19
|
+
/** Extra or overriding providers for AppModule. Same-token providers listed here
|
|
20
|
+
* win over the ones PodoKit modules registered. */
|
|
21
|
+
export const extensionProviders: Provider[] = [];
|
|
@@ -5,6 +5,7 @@ import { validateEnv } from "./config/env.validation";
|
|
|
5
5
|
import { dataSourceOptions } from "./database/data-source";
|
|
6
6
|
import { HealthModule } from "./health/health.module";
|
|
7
7
|
import { TodosModule } from "./todos/todos.module";
|
|
8
|
+
import { extensionImports, extensionProviders } from "./app.extensions";
|
|
8
9
|
// podokit:begin:imports
|
|
9
10
|
// podokit:end:imports
|
|
10
11
|
|
|
@@ -16,10 +17,12 @@ import { TodosModule } from "./todos/todos.module";
|
|
|
16
17
|
TodosModule,
|
|
17
18
|
// podokit:begin:module-imports
|
|
18
19
|
// podokit:end:module-imports
|
|
20
|
+
...extensionImports,
|
|
19
21
|
],
|
|
20
22
|
providers: [
|
|
21
23
|
// podokit:begin:providers
|
|
22
24
|
// podokit:end:providers
|
|
25
|
+
...extensionProviders,
|
|
23
26
|
],
|
|
24
27
|
})
|
|
25
28
|
export class AppModule {}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{projectName}}</title>
|
|
7
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
6
8
|
%sveltekit.head%
|
|
7
9
|
</head>
|
|
8
10
|
<body data-sveltekit-preload-data="hover" class="h-full">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Root from "./tabs.svelte";
|
|
2
2
|
import Content from "./tabs-content.svelte";
|
|
3
|
-
import List
|
|
3
|
+
import List from "./tabs-list.svelte";
|
|
4
4
|
import Trigger from "./tabs-trigger.svelte";
|
|
5
5
|
|
|
6
6
|
export {
|
|
@@ -8,8 +8,6 @@ export {
|
|
|
8
8
|
Content,
|
|
9
9
|
List,
|
|
10
10
|
Trigger,
|
|
11
|
-
tabsListVariants,
|
|
12
|
-
type TabsListVariant,
|
|
13
11
|
//
|
|
14
12
|
Root as Tabs,
|
|
15
13
|
Content as TabsContent,
|
|
@@ -1,40 +1,20 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
-
|
|
4
|
-
export const tabsListVariants = tv({
|
|
5
|
-
base: "rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
6
|
-
variants: {
|
|
7
|
-
variant: {
|
|
8
|
-
default: "cn-tabs-list-variant-default bg-muted",
|
|
9
|
-
line: "cn-tabs-list-variant-line gap-1 bg-transparent",
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
defaultVariants: {
|
|
13
|
-
variant: "default",
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export type TabsListVariant = VariantProps<typeof tabsListVariants>["variant"];
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
1
|
<script lang="ts">
|
|
21
2
|
import { Tabs as TabsPrimitive } from "bits-ui";
|
|
22
3
|
import { cn } from "$lib/utils.js";
|
|
23
4
|
|
|
24
5
|
let {
|
|
25
6
|
ref = $bindable(null),
|
|
26
|
-
variant = "default",
|
|
27
7
|
class: className,
|
|
28
8
|
...restProps
|
|
29
|
-
}: TabsPrimitive.ListProps
|
|
30
|
-
variant?: TabsListVariant;
|
|
31
|
-
} = $props();
|
|
9
|
+
}: TabsPrimitive.ListProps = $props();
|
|
32
10
|
</script>
|
|
33
11
|
|
|
34
12
|
<TabsPrimitive.List
|
|
35
13
|
bind:ref
|
|
36
14
|
data-slot="tabs-list"
|
|
37
|
-
|
|
38
|
-
|
|
15
|
+
class={cn(
|
|
16
|
+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
39
19
|
{...restProps}
|
|
40
20
|
/>
|
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="tabs-trigger"
|
|
15
15
|
class={cn(
|
|
16
|
-
"
|
|
17
|
-
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
18
|
-
"data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
|
|
19
|
-
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
16
|
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
20
17
|
className
|
|
21
18
|
)}
|
|
22
19
|
{...restProps}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="PodoKit">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="pk" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#8b5cf6"/>
|
|
5
|
+
<stop offset="1" stop-color="#4f46e5"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="64" height="64" rx="15" fill="url(#pk)"/>
|
|
9
|
+
<!-- layered stack: full-stack toolkit -->
|
|
10
|
+
<path d="M32 14 50 25 32 36 14 25Z" fill="#fff" fill-opacity="0.96"/>
|
|
11
|
+
<path d="M16 31 32 40.5 48 31" fill="none" stroke="#fff" stroke-opacity="0.78" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
12
|
+
<path d="M16 38 32 47.5 48 38" fill="none" stroke="#fff" stroke-opacity="0.5" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
13
|
+
</svg>
|
|
@@ -2,6 +2,18 @@ import { sveltekit } from "@sveltejs/kit/vite";
|
|
|
2
2
|
import tailwindcss from "@tailwindcss/vite";
|
|
3
3
|
import { defineConfig } from "vite";
|
|
4
4
|
|
|
5
|
+
// Inside the dev container (compose.dev.yaml sets VITE_DOCKER=1): bind to all
|
|
6
|
+
// interfaces, allow the Traefik hostname, and route HMR through Traefik on :80.
|
|
7
|
+
const inDocker = process.env.VITE_DOCKER === "1";
|
|
8
|
+
|
|
5
9
|
export default defineConfig({
|
|
6
10
|
plugins: [tailwindcss(), sveltekit()],
|
|
11
|
+
server: inDocker
|
|
12
|
+
? {
|
|
13
|
+
host: true,
|
|
14
|
+
port: 5001,
|
|
15
|
+
allowedHosts: true,
|
|
16
|
+
hmr: { clientPort: 80 },
|
|
17
|
+
}
|
|
18
|
+
: undefined,
|
|
7
19
|
});
|