@podosoft/podokit 0.1.0 → 0.2.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.
Files changed (153) hide show
  1. package/README.md +98 -0
  2. package/dist/add.d.ts +40 -0
  3. package/dist/add.js +114 -0
  4. package/dist/create.d.ts +2 -1
  5. package/dist/create.js +3 -2
  6. package/dist/index.js +39 -1
  7. package/dist/prompt.d.ts +0 -1
  8. package/dist/prompt.js +6 -7
  9. package/dist/templates/fullstack-nest-svelte/README.md +20 -8
  10. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +14 -2
  11. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +11 -1
  12. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +20 -15
  13. package/dist/templates/fullstack-nest-svelte/apps/api/src/database/data-source.ts +19 -0
  14. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +15 -5
  15. package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +8 -0
  16. package/dist/templates/fullstack-nest-svelte/apps/web/components.json +1 -1
  17. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +9 -2
  18. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +72 -8
  19. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/button.svelte +82 -0
  20. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/index.ts +17 -0
  21. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
  22. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card.svelte +22 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/index.ts +25 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
  30. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/index.ts +7 -0
  32. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +48 -0
  33. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/index.ts +7 -0
  34. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/label.svelte +20 -0
  35. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/utils.ts +11 -0
  36. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +19 -12
  37. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.controller.ts +31 -0
  38. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.module.ts +22 -0
  39. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.service.ts +44 -0
  40. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/login.dto.ts +12 -0
  41. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/register.dto.ts +13 -0
  42. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt-auth.guard.ts +5 -0
  43. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt.strategy.ts +23 -0
  44. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/user.entity.ts +16 -0
  45. package/dist/templates/modules/auth-jwt/files/apps/api/src/migrations/1720200000000-InitUsers.ts +23 -0
  46. package/dist/templates/modules/auth-jwt/module.manifest.json +31 -0
  47. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/demo.processor.ts +12 -0
  48. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/dto/create-job.dto.ts +9 -0
  49. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.controller.ts +29 -0
  50. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +15 -0
  51. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +8 -0
  52. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +20 -0
  53. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +14 -0
  54. package/dist/templates/modules/bullmq/files/infra/docker/worker.compose.example.yml +18 -0
  55. package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +22 -0
  56. package/dist/templates/modules/bullmq/module.manifest.json +28 -0
  57. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.controller.ts +29 -0
  58. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -0
  59. package/dist/templates/modules/file-upload/module.manifest.json +19 -0
  60. package/dist/templates/modules/job-progress/files/apps/api/src/progress/dto/start-job.dto.ts +11 -0
  61. package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +13 -0
  62. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +19 -0
  63. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.controller.ts +17 -0
  64. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +25 -0
  65. package/dist/templates/modules/job-progress/module.manifest.json +23 -0
  66. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/dto/put-object.dto.ts +8 -0
  67. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +31 -0
  68. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.controller.ts +29 -0
  69. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +11 -0
  70. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +37 -0
  71. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +33 -0
  72. package/dist/templates/modules/object-storage-s3/module.manifest.json +31 -0
  73. package/dist/templates/modules/redis/files/apps/api/src/redis/cache.controller.ts +21 -0
  74. package/dist/templates/modules/redis/files/apps/api/src/redis/dto/set-cache.dto.ts +14 -0
  75. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +12 -0
  76. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +43 -0
  77. package/dist/templates/modules/redis/module.manifest.json +21 -0
  78. package/dist/templates/modules/sse/files/apps/api/src/events/dto/publish-event.dto.ts +8 -0
  79. package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +25 -0
  80. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +12 -0
  81. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +17 -0
  82. package/dist/templates/modules/sse/module.manifest.json +16 -0
  83. package/dist/templates/todo/README.md +40 -0
  84. package/dist/templates/todo/apps/api/Dockerfile +22 -0
  85. package/dist/templates/todo/apps/api/nest-cli.json +5 -0
  86. package/dist/templates/todo/apps/api/package.json +44 -0
  87. package/dist/templates/todo/apps/api/src/app.module.ts +19 -0
  88. package/dist/templates/todo/apps/api/src/common/all-exceptions.filter.ts +43 -0
  89. package/dist/templates/todo/apps/api/src/common/app-exception.ts +12 -0
  90. package/dist/templates/todo/apps/api/src/config/env.validation.ts +23 -0
  91. package/dist/templates/todo/apps/api/src/database/data-source.ts +19 -0
  92. package/dist/templates/todo/apps/api/src/health/health.controller.ts +23 -0
  93. package/dist/templates/todo/apps/api/src/health/health.module.ts +7 -0
  94. package/dist/templates/todo/apps/api/src/main.ts +29 -0
  95. package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +22 -0
  96. package/dist/templates/todo/apps/api/src/todos/dto/create-todo.dto.ts +10 -0
  97. package/dist/templates/todo/apps/api/src/todos/dto/update-todo.dto.ts +10 -0
  98. package/dist/templates/todo/apps/api/src/todos/todo.entity.ts +16 -0
  99. package/dist/templates/todo/apps/api/src/todos/todos.controller.ts +38 -0
  100. package/dist/templates/todo/apps/api/src/todos/todos.module.ts +12 -0
  101. package/dist/templates/todo/apps/api/src/todos/todos.service.ts +41 -0
  102. package/dist/templates/todo/apps/api/test/health.e2e-spec.ts +23 -0
  103. package/dist/templates/todo/apps/api/tsconfig.json +21 -0
  104. package/dist/templates/todo/apps/web/Dockerfile +22 -0
  105. package/dist/templates/todo/apps/web/components.json +15 -0
  106. package/dist/templates/todo/apps/web/package.json +35 -0
  107. package/dist/templates/todo/apps/web/src/app.css +81 -0
  108. package/dist/templates/todo/apps/web/src/app.d.ts +11 -0
  109. package/dist/templates/todo/apps/web/src/app.html +11 -0
  110. package/dist/templates/todo/apps/web/src/lib/components/ui/button/button.svelte +82 -0
  111. package/dist/templates/todo/apps/web/src/lib/components/ui/button/index.ts +17 -0
  112. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
  113. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
  114. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
  115. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
  116. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
  117. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
  118. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card.svelte +22 -0
  119. package/dist/templates/todo/apps/web/src/lib/components/ui/card/index.ts +25 -0
  120. package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
  121. package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
  122. package/dist/templates/todo/apps/web/src/lib/components/ui/input/index.ts +7 -0
  123. package/dist/templates/todo/apps/web/src/lib/components/ui/input/input.svelte +48 -0
  124. package/dist/templates/todo/apps/web/src/lib/components/ui/label/index.ts +7 -0
  125. package/dist/templates/todo/apps/web/src/lib/components/ui/label/label.svelte +20 -0
  126. package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -0
  127. package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +10 -0
  128. package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +8 -0
  129. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +16 -0
  130. package/dist/templates/todo/apps/web/src/lib/utils.ts +11 -0
  131. package/dist/templates/todo/apps/web/src/routes/+layout.svelte +9 -0
  132. package/dist/templates/todo/apps/web/src/routes/+page.svelte +95 -0
  133. package/dist/templates/todo/apps/web/src/routes/api/health/+server.ts +12 -0
  134. package/dist/templates/todo/apps/web/src/routes/api/todos/+server.ts +24 -0
  135. package/dist/templates/todo/apps/web/src/routes/api/todos/[id]/+server.ts +24 -0
  136. package/dist/templates/todo/apps/web/static/.gitkeep +0 -0
  137. package/dist/templates/todo/apps/web/svelte.config.js +15 -0
  138. package/dist/templates/todo/apps/web/tsconfig.json +9 -0
  139. package/dist/templates/todo/apps/web/vite.config.ts +7 -0
  140. package/dist/templates/todo/dot-env.example +16 -0
  141. package/dist/templates/todo/dot-gitignore +9 -0
  142. package/dist/templates/todo/infra/docker/docker-compose.yml +29 -0
  143. package/dist/templates/todo/infra/k3s/api-deployment.yaml +24 -0
  144. package/dist/templates/todo/infra/k3s/configmap.yaml +10 -0
  145. package/dist/templates/todo/infra/k3s/ingress.yaml +18 -0
  146. package/dist/templates/todo/infra/k3s/namespace.yaml +4 -0
  147. package/dist/templates/todo/infra/k3s/secret.example.yaml +10 -0
  148. package/dist/templates/todo/infra/k3s/services.yaml +21 -0
  149. package/dist/templates/todo/infra/k3s/web-deployment.yaml +20 -0
  150. package/dist/templates/todo/package.json +13 -0
  151. package/dist/templates.d.ts +10 -0
  152. package/dist/templates.js +33 -0
  153. package/package.json +14 -4
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @podosoft/podokit
2
+
3
+ **PodoKit** is an opinionated but extensible starter toolkit and CLI for building full-stack TypeScript applications with **NestJS**, **SvelteKit**, **TailwindCSS**, **shadcn-svelte**, **Docker**, and **k3s**.
4
+
5
+ Stop rewriting the same backend bootstrap, frontend setup, environment config, health checks, Docker Compose, and CI every time you start a project. `podo create` gives you a consistent, production-minded foundation in seconds.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npx @podosoft/podokit create my-app
11
+ cd my-app
12
+ npm install
13
+ cp .env.example .env
14
+ npm run dev
15
+ ```
16
+
17
+ - API: http://localhost:3000 (health at `/health`)
18
+ - Web: http://localhost:5173
19
+
20
+ When run in a terminal, `podo create` lists the templates with descriptions and asks which one (and which package manager) to use. Pass flags (or `--yes`) to skip the prompts.
21
+
22
+ The `todo` template (`--template todo`) generates a working todo app (SvelteKit UI + NestJS API + PostgreSQL) with Swagger docs:
23
+
24
+ ![Generated todo app](https://raw.githubusercontent.com/podosoft-dev/podokit/main/docs/images/todo-app.png)
25
+
26
+ ## Usage
27
+
28
+ ```
29
+ podo create <name> [options]
30
+
31
+ Options:
32
+ --template <t> Template to scaffold (default: fullstack-nest-svelte)
33
+ - fullstack-nest-svelte : clean NestJS + SvelteKit starter
34
+ - todo : fullstack + a Todo CRUD example
35
+ - base : minimal npm workspace
36
+ --dir <path> Target directory (default: ./<name>)
37
+ --pm <name> Package manager: npm | pnpm | yarn (default: npm)
38
+ -y, --yes Skip prompts and accept defaults
39
+ -h, --help Show help
40
+ ```
41
+
42
+ Examples:
43
+
44
+ ```bash
45
+ # Interactive
46
+ npx @podosoft/podokit create my-app
47
+
48
+ # Non-interactive, explicit choices
49
+ npx @podosoft/podokit create my-app --template fullstack-nest-svelte --pm pnpm --yes
50
+
51
+ # Minimal workspace
52
+ npx @podosoft/podokit create my-lib --template base --yes
53
+ ```
54
+
55
+ ## Add features with modules
56
+
57
+ ```bash
58
+ cd my-app
59
+ npx @podosoft/podokit add auth-jwt # JWT auth: register, login, guard, /auth/me
60
+ ```
61
+
62
+ `podo add <module>` overlays files, merges dependencies, appends env vars, and wires the module into the NestJS app. Run `podo add` with no argument to list available modules.
63
+
64
+ ## What you get (`fullstack-nest-svelte`)
65
+
66
+ ```
67
+ my-app/
68
+ ├── apps/
69
+ │ ├── api/ # NestJS: config validation, /health, global
70
+ │ │ └── src/ # ValidationPipe, standard error envelope
71
+ │ └── web/ # SvelteKit: Tailwind v4, shadcn-svelte,
72
+ │ └── src/ # typesafe-i18n, server-side API proxy
73
+ ├── infra/
74
+ │ ├── docker/ # docker-compose (PostgreSQL, Redis)
75
+ │ └── k3s/ # namespace, deployments, service, ingress, secret example
76
+ ├── .env.example
77
+ ├── package.json # npm workspace
78
+ └── README.md
79
+ ```
80
+
81
+ Highlights of the generated app:
82
+
83
+ - **Backend (NestJS)** — bootstrap with a global `ValidationPipe` and exception filter, typed environment validation, a `/health` endpoint, and a stable `{ success, error: { code, ... } }` response envelope.
84
+ - **Frontend (SvelteKit)** — TailwindCSS v4 (config-less), **shadcn-svelte components preinstalled** (button, input, card, checkbox, label), a typesafe-i18n scaffold, and a **server-side proxy** so the browser never calls the API directly.
85
+ - **Infra** — Docker Compose for local PostgreSQL and Redis, plus example k3s manifests (standard `Ingress`, `secret.example.yaml`).
86
+
87
+ ## Status
88
+
89
+ PodoKit is early (`0.x`). The CLI and templates work end-to-end, but APIs and templates may change before `1.0`. Feedback and issues are welcome.
90
+
91
+ ## Links
92
+
93
+ - Repository & issues: https://github.com/podosoft-dev/podokit
94
+ - Changelog: https://github.com/podosoft-dev/podokit/blob/main/CHANGELOG.md
95
+
96
+ ## License
97
+
98
+ [Apache-2.0](https://github.com/podosoft-dev/podokit/blob/main/LICENSE)
package/dist/add.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ interface Injection {
2
+ file: string;
3
+ marker: string;
4
+ text: string;
5
+ }
6
+ export interface ModuleManifest {
7
+ name: string;
8
+ description: string;
9
+ requires?: string[];
10
+ targetApp: string;
11
+ dependencies?: Record<string, string>;
12
+ devDependencies?: Record<string, string>;
13
+ scripts?: Record<string, string>;
14
+ env?: string[];
15
+ inject?: Injection[];
16
+ instructions?: string[];
17
+ }
18
+ export interface AddOptions {
19
+ projectRoot: string;
20
+ module: string;
21
+ modulesDir: string;
22
+ }
23
+ export interface AddResult {
24
+ module: string;
25
+ instructions: string[];
26
+ /** Required modules that were auto-added because they were missing. */
27
+ added: string[];
28
+ }
29
+ /** List modules available under `modulesDir` (each has a module.manifest.json). */
30
+ export declare function listModules(modulesDir: string): {
31
+ name: string;
32
+ description: string;
33
+ }[];
34
+ /**
35
+ * Apply a module to an existing generated project: overlay files, merge the
36
+ * target app's package.json dependencies and scripts, append env example lines,
37
+ * and inject wiring at markers. Missing required modules are added first.
38
+ */
39
+ export declare function addModule(options: AddOptions): AddResult;
40
+ export {};
package/dist/add.js ADDED
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listModules = listModules;
4
+ exports.addModule = addModule;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
8
+ /** List modules available under `modulesDir` (each has a module.manifest.json). */
9
+ function listModules(modulesDir) {
10
+ if (!(0, node_fs_1.existsSync)(modulesDir))
11
+ return [];
12
+ return (0, node_fs_1.readdirSync)(modulesDir)
13
+ .filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(modulesDir, name, "module.manifest.json")))
14
+ .map((name) => {
15
+ const manifest = readManifest((0, node_path_1.join)(modulesDir, name));
16
+ return { name, description: manifest.description };
17
+ });
18
+ }
19
+ function readManifest(moduleDir) {
20
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8"));
21
+ }
22
+ function readJson(path) {
23
+ return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
24
+ }
25
+ function projectName(projectRoot) {
26
+ const pkg = readJson((0, node_path_1.join)(projectRoot, "package.json"));
27
+ return typeof pkg.name === "string" ? pkg.name : "app";
28
+ }
29
+ function appendEnv(projectRoot, lines) {
30
+ const file = (0, node_path_1.join)(projectRoot, ".env.example");
31
+ if (!(0, node_fs_1.existsSync)(file))
32
+ return;
33
+ const current = (0, node_fs_1.readFileSync)(file, "utf8");
34
+ const missing = lines.filter((line) => !current.split("\n").includes(line));
35
+ if (missing.length === 0)
36
+ return;
37
+ const separator = current.endsWith("\n") ? "" : "\n";
38
+ (0, node_fs_1.writeFileSync)(file, `${current}${separator}\n${missing.join("\n")}\n`);
39
+ }
40
+ /** Heuristic: is `module` already applied to the project? */
41
+ function isApplied(projectRoot, modulesDir, module) {
42
+ const manifestPath = (0, node_path_1.join)(modulesDir, module, "module.manifest.json");
43
+ if (!(0, node_fs_1.existsSync)(manifestPath))
44
+ return false;
45
+ const manifest = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf8"));
46
+ const firstInject = manifest.inject?.[0];
47
+ if (firstInject) {
48
+ const target = (0, node_path_1.join)(projectRoot, firstInject.file);
49
+ return (0, node_fs_1.existsSync)(target) && (0, node_fs_1.readFileSync)(target, "utf8").includes(firstInject.text);
50
+ }
51
+ return false;
52
+ }
53
+ /**
54
+ * Apply a module to an existing generated project: overlay files, merge the
55
+ * target app's package.json dependencies and scripts, append env example lines,
56
+ * and inject wiring at markers. Missing required modules are added first.
57
+ */
58
+ function addModule(options) {
59
+ return applyModule(options.projectRoot, options.module, options.modulesDir, new Set());
60
+ }
61
+ function applyModule(projectRoot, module, modulesDir, applied) {
62
+ const moduleDir = (0, node_path_1.join)(modulesDir, module);
63
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"))) {
64
+ const available = listModules(modulesDir).map((m) => m.name);
65
+ throw new Error(`Unknown module "${module}".${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
66
+ }
67
+ const manifest = readManifest(moduleDir);
68
+ const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", manifest.targetApp, "package.json");
69
+ if (!(0, node_fs_1.existsSync)(appPkgPath)) {
70
+ throw new Error(`This does not look like a PodoKit project: ${(0, node_path_1.join)("apps", manifest.targetApp, "package.json")} not found. Run inside a generated project.`);
71
+ }
72
+ applied.add(module);
73
+ // 0) apply required modules first (auto-add if missing)
74
+ const added = [];
75
+ for (const required of manifest.requires ?? []) {
76
+ if (applied.has(required) || isApplied(projectRoot, modulesDir, required))
77
+ continue;
78
+ const result = applyModule(projectRoot, required, modulesDir, applied);
79
+ added.push(required, ...result.added);
80
+ }
81
+ const appName = projectName(projectRoot);
82
+ const vars = { projectName: appName };
83
+ // 1) overlay files
84
+ const filesDir = (0, node_path_1.join)(moduleDir, "files");
85
+ if ((0, node_fs_1.existsSync)(filesDir)) {
86
+ (0, podokit_template_engine_1.copyTemplate)(filesDir, projectRoot, vars);
87
+ }
88
+ // 2) merge dependencies and scripts into the target app
89
+ if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
90
+ const overlay = {};
91
+ if (manifest.dependencies)
92
+ overlay.dependencies = manifest.dependencies;
93
+ if (manifest.devDependencies)
94
+ overlay.devDependencies = manifest.devDependencies;
95
+ if (manifest.scripts)
96
+ overlay.scripts = manifest.scripts;
97
+ const merged = (0, podokit_template_engine_1.mergePackageJson)(readJson(appPkgPath), overlay);
98
+ (0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(merged, null, 2)}\n`);
99
+ }
100
+ // 3) append env example lines
101
+ if (manifest.env?.length) {
102
+ appendEnv(projectRoot, manifest.env);
103
+ }
104
+ // 4) inject wiring at markers
105
+ for (const injection of manifest.inject ?? []) {
106
+ const target = (0, node_path_1.join)(projectRoot, injection.file);
107
+ if (!(0, node_fs_1.existsSync)(target)) {
108
+ throw new Error(`Cannot wire module: ${injection.file} not found.`);
109
+ }
110
+ (0, node_fs_1.writeFileSync)(target, (0, podokit_template_engine_1.insertAtMarker)((0, node_fs_1.readFileSync)(target, "utf8"), injection.marker, injection.text));
111
+ }
112
+ const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
113
+ return { module, instructions, added };
114
+ }
package/dist/create.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { DEFAULT_TEMPLATE } from "./templates";
1
2
  export type PackageManager = "npm" | "pnpm" | "yarn";
2
- export declare const DEFAULT_TEMPLATE = "fullstack-nest-svelte";
3
+ export { DEFAULT_TEMPLATE };
3
4
  export interface CreateOptions {
4
5
  /** Project name; also the default directory name. */
5
6
  name: string;
package/dist/create.js CHANGED
@@ -6,7 +6,8 @@ exports.create = create;
6
6
  const node_fs_1 = require("node:fs");
7
7
  const node_path_1 = require("node:path");
8
8
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
9
- exports.DEFAULT_TEMPLATE = "fullstack-nest-svelte";
9
+ const templates_1 = require("./templates");
10
+ Object.defineProperty(exports, "DEFAULT_TEMPLATE", { enumerable: true, get: function () { return templates_1.DEFAULT_TEMPLATE; } });
10
11
  const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?$/i;
11
12
  /** Validate a project name: no path separators, npm-friendly characters. */
12
13
  function assertValidName(name) {
@@ -24,7 +25,7 @@ function isEmptyDir(dir) {
24
25
  function create(options) {
25
26
  const { name, templatesDir } = options;
26
27
  assertValidName(name);
27
- const template = options.template ?? exports.DEFAULT_TEMPLATE;
28
+ const template = options.template ?? templates_1.DEFAULT_TEMPLATE;
28
29
  const packageManager = options.packageManager ?? "npm";
29
30
  const projectDir = options.targetDir
30
31
  ? (0, node_path_1.isAbsolute)(options.targetDir)
package/dist/index.js CHANGED
@@ -6,20 +6,28 @@ const node_path_1 = require("node:path");
6
6
  const promises_1 = require("node:readline/promises");
7
7
  const create_1 = require("./create");
8
8
  const prompt_1 = require("./prompt");
9
+ const templates_1 = require("./templates");
10
+ const add_1 = require("./add");
9
11
  const HELP = `podo — PodoKit project generator
10
12
 
11
13
  Usage:
12
14
  podo create <name> [options]
15
+ podo add <module>
13
16
 
14
17
  Options:
15
- --template <t> Template: fullstack-nest-svelte | base (default: fullstack-nest-svelte)
18
+ --template <t> Template to scaffold (see below)
16
19
  --dir <path> Target directory (default: ./<name>)
17
20
  --pm <name> Package manager: npm | pnpm | yarn (default: npm)
18
21
  -y, --yes Skip prompts and accept defaults
19
22
  -h, --help Show this help
20
23
 
24
+ Templates:
25
+ ${(0, templates_1.templateListText)()}
26
+
21
27
  Example:
22
28
  npx @podosoft/podokit create my-app
29
+ npx @podosoft/podokit create my-app --template todo
30
+ cd my-app && npx @podosoft/podokit add auth-jwt
23
31
  `;
24
32
  function parseArgs(argv) {
25
33
  const parsed = { help: false, yes: false };
@@ -59,6 +67,32 @@ async function main(argv) {
59
67
  process.stdout.write(HELP);
60
68
  return;
61
69
  }
70
+ const modulesDir = (0, node_path_1.join)(__dirname, "templates", "modules");
71
+ if (args.command === "add") {
72
+ const moduleName = args.name;
73
+ if (!moduleName) {
74
+ const available = (0, add_1.listModules)(modulesDir);
75
+ const list = available.length
76
+ ? available.map((m) => ` ${m.name} ${m.description}`).join("\n")
77
+ : " (none available)";
78
+ process.stdout.write(`Usage: podo add <module>\n\nModules:\n${list}\n`);
79
+ return;
80
+ }
81
+ try {
82
+ const result = (0, add_1.addModule)({ projectRoot: process.cwd(), module: moduleName, modulesDir });
83
+ if (result.added.length) {
84
+ process.stdout.write(`\nAlso added required module(s): ${result.added.join(", ")}\n`);
85
+ }
86
+ process.stdout.write(`\nAdded ${result.module}.\n`);
87
+ if (result.instructions.length) {
88
+ process.stdout.write(`\nNext steps:\n${result.instructions.map((i) => ` ${i}`).join("\n")}\n`);
89
+ }
90
+ }
91
+ catch (err) {
92
+ fail(err.message);
93
+ }
94
+ return;
95
+ }
62
96
  if (args.command !== "create") {
63
97
  fail(`Unknown command "${args.command}". Run "podo --help".`);
64
98
  }
@@ -74,6 +108,10 @@ async function main(argv) {
74
108
  const interactive = Boolean(process.stdin.isTTY) && !args.yes;
75
109
  const rl = interactive ? (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout }) : undefined;
76
110
  const ask = async (question) => (rl ? (await rl.question(question)).trim() : "");
111
+ // Show the template menu with descriptions before prompting for one.
112
+ if (interactive && !args.template) {
113
+ process.stdout.write(`\nTemplates:\n${(0, templates_1.templateListText)()}\n\n`);
114
+ }
77
115
  const templatesDir = (0, node_path_1.join)(__dirname, "templates");
78
116
  try {
79
117
  const resolved = await (0, prompt_1.resolveCreateOptions)({ template: args.template, pm: args.pm }, ask, interactive);
package/dist/prompt.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { type PackageManager } from "./create";
2
- export declare const TEMPLATES: readonly ["fullstack-nest-svelte", "base"];
3
2
  export declare const PACKAGE_MANAGERS: PackageManager[];
4
3
  /** Asks a single question and resolves to the trimmed answer (empty if skipped). */
5
4
  export type Ask = (question: string) => Promise<string>;
package/dist/prompt.js CHANGED
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PACKAGE_MANAGERS = exports.TEMPLATES = void 0;
3
+ exports.PACKAGE_MANAGERS = void 0;
4
4
  exports.resolveCreateOptions = resolveCreateOptions;
5
- const create_1 = require("./create");
6
- exports.TEMPLATES = ["fullstack-nest-svelte", "base"];
5
+ const templates_1 = require("./templates");
7
6
  exports.PACKAGE_MANAGERS = ["npm", "pnpm", "yarn"];
8
7
  function isPackageManager(value) {
9
8
  return exports.PACKAGE_MANAGERS.includes(value);
@@ -18,12 +17,12 @@ function isPackageManager(value) {
18
17
  async function resolveCreateOptions(args, ask, interactive) {
19
18
  let template = args.template;
20
19
  if (!template && interactive) {
21
- const answer = await ask(`Template (${exports.TEMPLATES.join(" / ")}) [${create_1.DEFAULT_TEMPLATE}]: `);
20
+ const answer = await ask(`Template [${templates_1.DEFAULT_TEMPLATE}]: `);
22
21
  template = answer || undefined;
23
22
  }
24
- template = template ?? create_1.DEFAULT_TEMPLATE;
25
- if (!exports.TEMPLATES.includes(template)) {
26
- throw new Error(`Unknown template "${template}". Choose one of: ${exports.TEMPLATES.join(", ")}.`);
23
+ template = template ?? templates_1.DEFAULT_TEMPLATE;
24
+ if (!(0, templates_1.isKnownTemplate)(template)) {
25
+ throw new Error(`Unknown template "${template}". Choose one of: ${templates_1.TEMPLATE_NAMES.join(", ")}.`);
27
26
  }
28
27
  let pm = args.pm;
29
28
  if (!pm && interactive) {
@@ -1,29 +1,41 @@
1
1
  # {{projectName}}
2
2
 
3
- Full-stack TypeScript app generated with [PodoKit](https://github.com/podosoft-dev/podokit).
3
+ Full-stack TypeScript starter generated with [PodoKit](https://github.com/podosoft-dev/podokit).
4
4
 
5
- - `apps/api` — NestJS API (config validation, health checks, standard error envelope)
6
- - `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) that talks to the API through a server-side proxy
7
- - `infra/` — Docker Compose and k3s manifests
5
+ - `apps/api` — NestJS API: schema-validated env (zod), `/health` + `/health/ready`, Swagger docs at `/api-docs`, a standard error envelope, and TypeORM + PostgreSQL wired up (no domain entities yet — add your own).
6
+ - `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) that talks to the API through a server-side proxy.
7
+ - `infra/` — Docker Compose (PostgreSQL, Redis) and k3s manifests.
8
8
 
9
9
  ## Getting started
10
10
 
11
11
  ```bash
12
12
  {{packageManager}} install
13
13
  cp .env.example .env
14
+
15
+ # start local PostgreSQL + Redis
16
+ docker compose -f infra/docker/docker-compose.yml up -d
17
+
14
18
  {{packageManager}} run dev
15
19
  ```
16
20
 
17
- - API: http://localhost:3000 (health at `/health`)
21
+ - API: http://localhost:3000 health at `/health`, docs at `/api-docs`
18
22
  - Web: http://localhost:5173
19
23
 
20
- ## Local services
24
+ ## Database & migrations
25
+
26
+ The API uses TypeORM with PostgreSQL and ships no domain entities yet. Add an
27
+ entity under `apps/api/src`, register it in `src/database/data-source.ts`, then
28
+ generate and run a migration:
21
29
 
22
30
  ```bash
23
- docker compose -f infra/docker/docker-compose.yml up -d
31
+ {{packageManager}} run migration:generate -w {{projectName}}-api -- src/migrations/Init
32
+ {{packageManager}} run migration:run -w {{projectName}}-api
24
33
  ```
25
34
 
35
+ Want a worked example? Generate the `todo` template instead:
36
+ `npx @podosoft/podokit create my-app --template todo`.
37
+
26
38
  ## Deploy
27
39
 
28
- Docker Compose manifests live in `infra/docker`; example k3s manifests in `infra/k3s`
40
+ Docker Compose in `infra/docker`; example k3s manifests in `infra/k3s`
29
41
  (use `secret.example.yaml` as a template — never commit real secrets).
@@ -7,16 +7,27 @@
7
7
  "build": "nest build",
8
8
  "start": "node dist/main",
9
9
  "lint": "tsc -p tsconfig.json --noEmit",
10
- "test": "jest"
10
+ "test": "jest",
11
+ "typeorm": "typeorm-ts-node-commonjs -d src/database/data-source.ts",
12
+ "migration:run": "npm run typeorm -- migration:run",
13
+ "migration:revert": "npm run typeorm -- migration:revert",
14
+ "migration:generate": "npm run typeorm -- migration:generate"
11
15
  },
12
16
  "dependencies": {
13
17
  "@nestjs/common": "^10.4.0",
18
+ "@nestjs/config": "^3.2.3",
14
19
  "@nestjs/core": "^10.4.0",
15
20
  "@nestjs/platform-express": "^10.4.0",
21
+ "@nestjs/swagger": "^7.4.0",
22
+ "@nestjs/typeorm": "^10.0.2",
16
23
  "class-transformer": "^0.5.1",
17
24
  "class-validator": "^0.14.1",
25
+ "dotenv": "^16.4.5",
26
+ "pg": "^8.12.0",
18
27
  "reflect-metadata": "^0.2.2",
19
- "rxjs": "^7.8.1"
28
+ "rxjs": "^7.8.1",
29
+ "typeorm": "^0.3.20",
30
+ "zod": "^3.23.8"
20
31
  },
21
32
  "devDependencies": {
22
33
  "@nestjs/cli": "^10.4.0",
@@ -27,6 +38,7 @@
27
38
  "jest": "^29.7.0",
28
39
  "supertest": "^7.0.0",
29
40
  "ts-jest": "^29.2.5",
41
+ "ts-node": "^10.9.2",
30
42
  "typescript": "^5.6.3"
31
43
  }
32
44
  }
@@ -1,7 +1,17 @@
1
1
  import { Module } from "@nestjs/common";
2
+ import { ConfigModule } from "@nestjs/config";
3
+ import { TypeOrmModule } from "@nestjs/typeorm";
4
+ import { validateEnv } from "./config/env.validation";
5
+ import { dataSourceOptions } from "./database/data-source";
2
6
  import { HealthModule } from "./health/health.module";
7
+ // podokit:imports
3
8
 
4
9
  @Module({
5
- imports: [HealthModule],
10
+ imports: [
11
+ ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }),
12
+ TypeOrmModule.forRoot(dataSourceOptions),
13
+ HealthModule,
14
+ // podokit:module-imports
15
+ ],
6
16
  })
7
17
  export class AppModule {}
@@ -1,18 +1,23 @@
1
- // Minimal typed environment validation. Extend as the app grows.
2
- export interface AppEnv {
3
- nodeEnv: string;
4
- port: number;
5
- corsOrigin: string | undefined;
6
- }
1
+ import { z } from "zod";
2
+
3
+ // Schema-validated environment. Fails fast at boot if something is wrong.
4
+ const schema = z.object({
5
+ NODE_ENV: z.string().default("development"),
6
+ PORT: z.coerce.number().default(3000),
7
+ CORS_ORIGIN: z.string().optional(),
8
+ POSTGRES_HOST: z.string().default("localhost"),
9
+ POSTGRES_PORT: z.coerce.number().default(5432),
10
+ POSTGRES_USER: z.string().default("podokit"),
11
+ POSTGRES_PASSWORD: z.string().default("podokit"),
12
+ POSTGRES_DB: z.string().default("podokit"),
13
+ });
14
+
15
+ export type AppEnv = z.infer<typeof schema>;
7
16
 
8
- export function validateEnv(env: NodeJS.ProcessEnv = process.env): AppEnv {
9
- const port = Number(env.PORT ?? 3000);
10
- if (Number.isNaN(port)) {
11
- throw new Error(`Invalid PORT: ${env.PORT}`);
17
+ export function validateEnv(config: Record<string, unknown>): AppEnv {
18
+ const parsed = schema.safeParse(config);
19
+ if (!parsed.success) {
20
+ throw new Error(`Invalid environment:\n${parsed.error.toString()}`);
12
21
  }
13
- return {
14
- nodeEnv: env.NODE_ENV ?? "development",
15
- port,
16
- corsOrigin: env.CORS_ORIGIN,
17
- };
22
+ return parsed.data;
18
23
  }
@@ -0,0 +1,19 @@
1
+ import "dotenv/config";
2
+ import { join } from "node:path";
3
+ import { DataSource, type DataSourceOptions } from "typeorm";
4
+
5
+ export const dataSourceOptions: DataSourceOptions = {
6
+ type: "postgres",
7
+ host: process.env.POSTGRES_HOST ?? "localhost",
8
+ port: Number(process.env.POSTGRES_PORT ?? 5432),
9
+ username: process.env.POSTGRES_USER ?? "podokit",
10
+ password: process.env.POSTGRES_PASSWORD ?? "podokit",
11
+ database: process.env.POSTGRES_DB ?? "podokit",
12
+ // Entities are auto-discovered by file name (*.entity.ts / .js).
13
+ entities: [join(__dirname, "..", "**", "*.entity{.ts,.js}")],
14
+ migrations: [join(__dirname, "..", "migrations", "*{.ts,.js}")],
15
+ synchronize: false,
16
+ };
17
+
18
+ // Used by the TypeORM CLI for migrations (see package.json scripts).
19
+ export default new DataSource(dataSourceOptions);
@@ -1,13 +1,23 @@
1
1
  import { Controller, Get } from "@nestjs/common";
2
+ import { InjectDataSource } from "@nestjs/typeorm";
3
+ import { DataSource } from "typeorm";
2
4
 
3
5
  @Controller("health")
4
6
  export class HealthController {
7
+ constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
8
+
5
9
  @Get()
6
10
  liveness(): { status: string; uptime: number; timestamp: string } {
7
- return {
8
- status: "ok",
9
- uptime: process.uptime(),
10
- timestamp: new Date().toISOString(),
11
- };
11
+ return { status: "ok", uptime: process.uptime(), timestamp: new Date().toISOString() };
12
+ }
13
+
14
+ @Get("ready")
15
+ async readiness(): Promise<{ status: string; db: string }> {
16
+ try {
17
+ await this.dataSource.query("SELECT 1");
18
+ return { status: "ready", db: "up" };
19
+ } catch {
20
+ return { status: "degraded", db: "down" };
21
+ }
12
22
  }
13
23
  }
@@ -1,5 +1,6 @@
1
1
  import { NestFactory } from "@nestjs/core";
2
2
  import { ValidationPipe } from "@nestjs/common";
3
+ import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
3
4
  import { AppModule } from "./app.module";
4
5
  import { AllExceptionsFilter } from "./common/all-exceptions.filter";
5
6
 
@@ -14,6 +15,13 @@ async function bootstrap(): Promise<void> {
14
15
  const corsOrigin = process.env.CORS_ORIGIN?.split(",").map((o) => o.trim());
15
16
  app.enableCors({ origin: corsOrigin ?? true, credentials: true });
16
17
 
18
+ const config = new DocumentBuilder()
19
+ .setTitle("{{projectName}} API")
20
+ .setDescription("Generated with PodoKit")
21
+ .setVersion("0.0.0")
22
+ .build();
23
+ SwaggerModule.setup("api-docs", app, SwaggerModule.createDocument(app, config));
24
+
17
25
  const port = Number(process.env.PORT ?? 3000);
18
26
  await app.listen(port);
19
27
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://shadcn-svelte.com/schema.json",
3
- "style": "new-york",
3
+ "style": "nova",
4
4
  "tailwind": {
5
5
  "css": "src/app.css",
6
6
  "baseColor": "zinc"
@@ -11,7 +11,13 @@
11
11
  "test": "echo \"add Playwright tests here\" && exit 0"
12
12
  },
13
13
  "dependencies": {
14
- "mode-watcher": "^0.5.0"
14
+ "mode-watcher": "^0.5.0",
15
+ "bits-ui": "^2.18.1",
16
+ "clsx": "^2.1.1",
17
+ "tailwind-merge": "^3.6.0",
18
+ "tailwind-variants": "^3.2.2",
19
+ "@lucide/svelte": "^1.23.0",
20
+ "@internationalized/date": "^3.12.2"
15
21
  },
16
22
  "devDependencies": {
17
23
  "@sveltejs/adapter-node": "^5.2.0",
@@ -23,6 +29,7 @@
23
29
  "tailwindcss": "^4.0.0",
24
30
  "typesafe-i18n": "^5.26.2",
25
31
  "typescript": "^5.6.3",
26
- "vite": "^5.4.0"
32
+ "vite": "^5.4.0",
33
+ "tw-animate-css": "^1.4.0"
27
34
  }
28
35
  }