@podosoft/podokit 0.14.0 → 0.16.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 (122) hide show
  1. package/README.md +20 -0
  2. package/dist/add.d.ts +4 -0
  3. package/dist/add.js +59 -1
  4. package/dist/create.js +8 -1
  5. package/dist/deploy-profile.d.ts +92 -0
  6. package/dist/deploy-profile.js +603 -0
  7. package/dist/deploy-render.d.ts +18 -0
  8. package/dist/deploy-render.js +651 -0
  9. package/dist/deploy.d.ts +65 -0
  10. package/dist/deploy.js +851 -0
  11. package/dist/dev.d.ts +1 -0
  12. package/dist/dev.js +29 -5
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.js +147 -1
  15. package/dist/lib.d.ts +6 -0
  16. package/dist/lib.js +18 -1
  17. package/dist/lockfile.js +3 -0
  18. package/dist/remove.js +7 -0
  19. package/dist/templates/fullstack-nest-svelte/AGENTS.md +1 -0
  20. package/dist/templates/fullstack-nest-svelte/README.md +19 -8
  21. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +2 -2
  22. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.spec.ts +53 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.ts +69 -0
  25. package/dist/templates/fullstack-nest-svelte/apps/api/src/database/migrate.ts +16 -0
  26. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +19 -4
  27. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.module.ts +5 -1
  28. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.spec.ts +22 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.ts +33 -0
  30. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +2 -2
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
  32. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +3 -5
  33. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
  34. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
  35. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +37 -0
  36. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/rollback.md +18 -0
  37. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
  38. package/dist/templates/fullstack-nest-svelte/dot-env.example +6 -0
  39. package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +1 -1
  40. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +0 -4
  41. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +6 -4
  42. package/dist/templates/fullstack-nest-svelte/tests/playwright.extensions.ts +44 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +35 -4
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +33 -4
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +6 -0
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +18 -10
  47. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +11 -6
  48. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +18 -8
  49. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +15 -9
  50. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +1 -0
  51. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +83 -0
  52. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +13 -10
  53. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +69 -3
  54. package/dist/templates/modules/admin-dashboard/files/tests/seed.teardown.ts +46 -0
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +13 -0
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +3 -6
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +34 -28
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +29 -39
  59. package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
  60. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.spec.ts +36 -0
  61. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.ts +20 -0
  62. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.spec.ts +49 -0
  63. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.ts +3 -11
  64. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.module.ts +3 -1
  65. package/dist/templates/modules/api-key-auth/module.manifest.json +1 -1
  66. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +32 -0
  67. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +35 -0
  68. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +3 -2
  69. package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +19 -4
  70. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +4 -5
  71. package/dist/templates/modules/bullmq/module.manifest.json +4 -1
  72. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +36 -0
  73. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +26 -5
  74. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.spec.ts +35 -0
  75. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +21 -2
  76. package/dist/templates/modules/object-storage-s3/module.manifest.json +3 -0
  77. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +85 -0
  78. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +110 -0
  79. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +132 -0
  80. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +100 -0
  81. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.integration.spec.ts +91 -0
  82. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +228 -0
  83. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +97 -38
  84. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +11 -6
  85. package/dist/templates/modules/rate-limit/module.manifest.json +23 -9
  86. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +28 -0
  87. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +52 -11
  88. package/dist/templates/modules/redis/module.manifest.json +16 -1
  89. package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +17 -3
  90. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +9 -2
  91. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +61 -0
  92. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +44 -4
  93. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.redis.spec.ts +42 -0
  94. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.spec.ts +97 -0
  95. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +165 -0
  96. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +41 -0
  97. package/dist/templates/modules/sse/module.manifest.json +16 -3
  98. package/dist/templates/todo/AGENTS.md +1 -0
  99. package/dist/templates/todo/README.md +12 -8
  100. package/dist/templates/todo/apps/api/Dockerfile +2 -2
  101. package/dist/templates/todo/apps/api/package.json +1 -0
  102. package/dist/templates/todo/apps/api/src/config/redis.connection.spec.ts +53 -0
  103. package/dist/templates/todo/apps/api/src/config/redis.connection.ts +69 -0
  104. package/dist/templates/todo/apps/api/src/database/migrate.ts +16 -0
  105. package/dist/templates/todo/apps/api/src/health/health.controller.ts +19 -4
  106. package/dist/templates/todo/apps/api/src/health/health.module.ts +5 -1
  107. package/dist/templates/todo/apps/api/src/health/readiness.service.spec.ts +22 -0
  108. package/dist/templates/todo/apps/api/src/health/readiness.service.ts +33 -0
  109. package/dist/templates/todo/apps/web/Dockerfile +2 -2
  110. package/dist/templates/todo/compose.dev.yaml +3 -5
  111. package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
  112. package/dist/templates/todo/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
  113. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +28 -0
  114. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/rollback.md +5 -0
  115. package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
  116. package/dist/templates/todo/dot-env.example +6 -0
  117. package/dist/templates/todo/infra/k3s/api-deployment.yaml +1 -1
  118. package/dist/templates/todo/infra/k3s/ingress.yaml +0 -4
  119. package/dist/templates/todo/tests/playwright.config.ts +6 -4
  120. package/dist/templates/todo/tests/playwright.extensions.ts +44 -0
  121. package/dist/update.js +17 -13
  122. package/package.json +2 -2
package/README.md CHANGED
@@ -38,6 +38,7 @@ The `todo` template (`--template todo`) generates a working todo app (SvelteKit
38
38
  | `podo update [--apply]` | Preview (or apply) what a version update would change |
39
39
  | `podo eject <path…>` | Take ownership of a managed file so updates skip it |
40
40
  | `podo dev <action>` | Watch, inspect, execute in, or stop a container stack behind the shared `*.localhost` gateway |
41
+ | `podo deploy <action>` | Plan, apply, verify, or roll back an exact-image Helm release |
41
42
 
42
43
  ## Usage
43
44
 
@@ -61,6 +62,9 @@ Generated projects include agent guidance for AI coding tools — an
61
62
  it, `.cursor`/`.github` pointers, and Claude Code skills under `.claude/skills/`.
62
63
  Modules extend `AGENTS.md` as you add them. Use `--no-ai` to skip.
63
64
 
65
+ Fullstack projects also include a deployment skill under `.agents/skills/`
66
+ with a Claude pointer under `.claude/skills/`.
67
+
64
68
  Examples:
65
69
 
66
70
  ```bash
@@ -101,6 +105,22 @@ podo update --apply # apply it: clean updates are written, your edits are 3-way
101
105
 
102
106
  Files you own (routes, your components, shadcn UI) are never touched. See the
103
107
  [updating guide](https://github.com/podosoft-dev/podokit/blob/main/docs/updating.md).
108
+ An exact path handed over with `podo eject` remains owned even if a module also
109
+ declares it managed; if you later move or delete that file, update reports the
110
+ missing owned path instead of recreating it.
111
+
112
+ ## Deploy an immutable release
113
+
114
+ ```bash
115
+ podo deploy init --profile production --context production --host app.example.com
116
+ podo deploy doctor --profile production
117
+ podo deploy plan --profile production --release v1.2.3 --json
118
+ ```
119
+
120
+ Apply and rollback require the exact confirmation hash from a fresh plan.
121
+ PodoKit consumes an existing namespace and Secrets, runs migrations before the
122
+ application rollout, and exposes only the SvelteKit web proxy. See the
123
+ [deployment guide](https://github.com/podosoft-dev/podokit/blob/main/docs/deployment.md).
104
124
 
105
125
  ## What you get (`fullstack-nest-svelte`)
106
126
 
package/dist/add.d.ts CHANGED
@@ -61,6 +61,10 @@ export interface ModuleManifest {
61
61
  managedOverrides?: string[];
62
62
  /** Idempotent path migrations applied by `podo update --apply`. */
63
63
  migrations?: ModuleMigration[];
64
+ /** Baseline files that must already exist before this module can be applied. */
65
+ requiredProjectFiles?: string[];
66
+ /** Stable source contracts that must be present in generated baseline files. */
67
+ requiredProjectContents?: Record<string, string[]>;
64
68
  }
65
69
  export interface PackageOverlay {
66
70
  dependencies?: Record<string, string>;
package/dist/add.js CHANGED
@@ -88,6 +88,57 @@ function assertManifestVersion(name, manifest) {
88
88
  throw new Error(`Module "${name}" needs a newer PodoKit (manifest v${manifest.manifestVersion}, this CLI supports v${exports.SUPPORTED_MANIFEST_VERSION}). Update @podosoft/podokit.`);
89
89
  }
90
90
  }
91
+ function assertRequiredProjectFiles(projectRoot, name, manifest) {
92
+ const contents = manifest.requiredProjectContents ?? {};
93
+ const required = [
94
+ ...(manifest.requiredProjectFiles ?? []),
95
+ ...Object.keys(contents),
96
+ ];
97
+ const invalid = required.find((path) => path.startsWith("/") ||
98
+ path.split("/").includes("..") ||
99
+ path.trim() === "");
100
+ if (invalid) {
101
+ throw new Error(`Module "${name}" declares an invalid required project file: ${invalid}.`);
102
+ }
103
+ const missing = required.filter((path) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, path)));
104
+ if (missing.length) {
105
+ throw new Error(`Module "${name}" requires a newer PodoKit project baseline. Run podo update --apply, then retry. Missing: ${missing.join(", ")}.`);
106
+ }
107
+ const invalidContract = Object.entries(contents).find(([, snippets]) => !Array.isArray(snippets) ||
108
+ snippets.length === 0 ||
109
+ snippets.some((snippet) => typeof snippet !== "string" || snippet.length === 0));
110
+ if (invalidContract) {
111
+ throw new Error(`Module "${name}" declares an invalid required project content contract: ${invalidContract[0]}.`);
112
+ }
113
+ const incompatible = Object.entries(contents)
114
+ .filter(([path, snippets]) => {
115
+ const source = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, path), "utf8");
116
+ return snippets.some((snippet) => !source.includes(snippet));
117
+ })
118
+ .map(([path]) => path);
119
+ if (incompatible.length) {
120
+ throw new Error(`Module "${name}" requires a newer PodoKit project baseline. Run podo update --apply, then retry. Incompatible: ${incompatible.join(", ")}.`);
121
+ }
122
+ }
123
+ function assertModuleGraphPrerequisites(projectRoot, module, modulesDir, visited = new Set(), requiredBy) {
124
+ if (visited.has(module))
125
+ return;
126
+ visited.add(module);
127
+ const resolved = resolveModule(module, modulesDir, projectRoot);
128
+ if (!resolved) {
129
+ if (requiredBy) {
130
+ throw new Error(`Module "${requiredBy}" requires unknown module "${module}".`);
131
+ }
132
+ const available = listModules(modulesDir, projectRoot).map((entry) => entry.name);
133
+ throw new Error(`Unknown module "${module}".${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
134
+ }
135
+ const manifest = readModuleManifest(resolved.dir);
136
+ assertManifestVersion(module, manifest);
137
+ assertRequiredProjectFiles(projectRoot, module, manifest);
138
+ for (const required of manifest.requires ?? []) {
139
+ assertModuleGraphPrerequisites(projectRoot, required, modulesDir, visited, module);
140
+ }
141
+ }
91
142
  /** List modules available to a project: the bundled ones under `modulesDir` plus,
92
143
  * when `projectRoot` is given, any `@podosoft/podokit-module-*` packages installed
93
144
  * in it. Bundled names take precedence on a clash. */
@@ -138,8 +189,13 @@ function appendEnv(projectRoot, lines) {
138
189
  const separator = current.endsWith("\n") ? "" : "\n";
139
190
  (0, node_fs_1.writeFileSync)(file, `${current}${separator}\n${missing.join("\n")}\n`);
140
191
  }
141
- /** Heuristic: is `module` already applied to the project? */
192
+ /** Prefer the generation manifest as the authoritative installed-module list.
193
+ * Fall back to the legacy wiring heuristic only for projects without one. */
142
194
  function isApplied(projectRoot, modulesDir, module) {
195
+ const projectManifest = (0, lockfile_1.readManifest)(projectRoot);
196
+ if (projectManifest) {
197
+ return projectManifest.modules.some((entry) => entry.name === module);
198
+ }
143
199
  const moduleDir = resolveModuleDir(module, modulesDir, projectRoot);
144
200
  if (!moduleDir)
145
201
  return false;
@@ -157,6 +213,7 @@ function isApplied(projectRoot, modulesDir, module) {
157
213
  * and inject wiring at markers. Missing required modules are added first.
158
214
  */
159
215
  function addModule(options) {
216
+ assertModuleGraphPrerequisites(options.projectRoot, options.module, options.modulesDir);
160
217
  const previousLock = (0, lockfile_1.readFilesLock)(options.projectRoot);
161
218
  const cleanPaths = previousLock
162
219
  ? Object.entries(previousLock.files)
@@ -195,6 +252,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
195
252
  const moduleDir = resolved.dir;
196
253
  const manifest = readModuleManifest(moduleDir);
197
254
  assertManifestVersion(module, manifest);
255
+ assertRequiredProjectFiles(projectRoot, module, manifest);
198
256
  const requiredApps = new Set([manifest.targetApp, ...Object.keys(manifest.packageOverlays ?? {})]);
199
257
  const missingApp = [...requiredApps].find((app) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "apps", app, "package.json")));
200
258
  if (missingApp) {
package/dist/create.js CHANGED
@@ -10,7 +10,14 @@ const templates_1 = require("./templates");
10
10
  Object.defineProperty(exports, "DEFAULT_TEMPLATE", { enumerable: true, get: function () { return templates_1.DEFAULT_TEMPLATE; } });
11
11
  const lockfile_1 = require("./lockfile");
12
12
  /** AI agent guidance files, removed when scaffolding with `--no-ai`. */
13
- const AI_ARTIFACTS = ["AGENTS.md", "CLAUDE.md", ".claude", ".cursor", ".github/copilot-instructions.md"];
13
+ const AI_ARTIFACTS = [
14
+ "AGENTS.md",
15
+ "CLAUDE.md",
16
+ ".agents",
17
+ ".claude",
18
+ ".cursor",
19
+ ".github/copilot-instructions.md",
20
+ ];
14
21
  const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?$/i;
15
22
  /** Validate a project name: no path separators, npm-friendly characters. */
16
23
  function assertValidName(name) {
@@ -0,0 +1,92 @@
1
+ export type DependencyMode = "inCluster" | "external" | "disabled";
2
+ export interface ResourceProfile {
3
+ cpuRequest: string;
4
+ cpuLimit: string;
5
+ memoryRequest: string;
6
+ memoryLimit: string;
7
+ }
8
+ export interface WorkloadProfile {
9
+ replicas: number;
10
+ resources: ResourceProfile;
11
+ }
12
+ export interface StatefulDependencyProfile {
13
+ mode: DependencyMode;
14
+ image: string;
15
+ secretName: string;
16
+ storageClassName: string;
17
+ storageSize: string;
18
+ }
19
+ export type RedisDependencyProfile = StatefulDependencyProfile;
20
+ export interface ObjectStorageDependencyProfile extends StatefulDependencyProfile {
21
+ bucket: string;
22
+ clientImage: string;
23
+ }
24
+ export interface NamedSecretProfile {
25
+ name: string;
26
+ requiredKeys: string[];
27
+ }
28
+ export interface VerificationCheckProfile {
29
+ path: string;
30
+ expectedStatus: number;
31
+ expectedJson?: Record<string, string | number | boolean | null>;
32
+ }
33
+ export interface DeployProfileV1 {
34
+ schemaVersion: 1;
35
+ driver: "kubernetes-helm";
36
+ target: {
37
+ context: string;
38
+ clusterFingerprint: string;
39
+ namespace: string;
40
+ release: string;
41
+ createNamespace: false;
42
+ };
43
+ release: {
44
+ strategy: "shared-tag";
45
+ tagPattern: string;
46
+ apiRepository: string;
47
+ webRepository: string;
48
+ };
49
+ exposure: {
50
+ mode: "ingress" | "nodePort";
51
+ host: string;
52
+ ingressClassName: string;
53
+ tlsSecretName: string | null;
54
+ };
55
+ workloads: {
56
+ api: WorkloadProfile;
57
+ web: WorkloadProfile;
58
+ worker: WorkloadProfile | null;
59
+ };
60
+ dependencies: {
61
+ postgres: StatefulDependencyProfile;
62
+ redis: RedisDependencyProfile;
63
+ objectStorage: ObjectStorageDependencyProfile;
64
+ };
65
+ secrets: {
66
+ api: NamedSecretProfile;
67
+ web: NamedSecretProfile | null;
68
+ imagePull: string;
69
+ };
70
+ runtimeConfig: Record<string, string>;
71
+ verification: {
72
+ baseUrl: string;
73
+ checks: VerificationCheckProfile[];
74
+ };
75
+ }
76
+ export interface DeploymentProfileSummary {
77
+ name: string;
78
+ path: string;
79
+ profile: DeployProfileV1;
80
+ }
81
+ export declare function profileDirectory(projectRoot: string): string;
82
+ export declare function profilePath(projectRoot: string, name: string): string;
83
+ export declare function loadDeploymentProfile(projectRoot: string, name: string): DeployProfileV1;
84
+ export declare function listDeploymentProfiles(projectRoot: string): DeploymentProfileSummary[];
85
+ export interface InitializeDeploymentProfileOptions {
86
+ context: string;
87
+ clusterFingerprint: string;
88
+ host?: string;
89
+ }
90
+ export declare function initializeDeploymentProfile(projectRoot: string, name: string, options: InitializeDeploymentProfileOptions): DeploymentProfileSummary;
91
+ export declare function assertReleaseTag(profile: DeployProfileV1, release: string): void;
92
+ export declare function deploymentProfileDigest(profile: DeployProfileV1): string;