@podosoft/podokit 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/add.d.ts +13 -2
  2. package/dist/add.js +135 -20
  3. package/dist/api-paths.d.ts +34 -0
  4. package/dist/api-paths.js +103 -0
  5. package/dist/deploy-compose-profile.d.ts +4 -0
  6. package/dist/deploy-compose-profile.js +13 -2
  7. package/dist/deploy-compose-render.d.ts +1 -1
  8. package/dist/deploy-compose-render.js +23 -7
  9. package/dist/deploy-compose.js +2 -1
  10. package/dist/deploy-profile.d.ts +3 -0
  11. package/dist/deploy-profile.js +12 -4
  12. package/dist/deploy-render.js +54 -8
  13. package/dist/deploy.js +21 -1
  14. package/dist/dev.d.ts +14 -0
  15. package/dist/dev.js +152 -10
  16. package/dist/index.js +23 -2
  17. package/dist/lib.d.ts +4 -2
  18. package/dist/lib.js +5 -2
  19. package/dist/lockfile.d.ts +6 -1
  20. package/dist/lockfile.js +26 -6
  21. package/dist/module-api-paths.d.ts +52 -0
  22. package/dist/module-api-paths.js +189 -0
  23. package/dist/remove.d.ts +3 -0
  24. package/dist/remove.js +12 -3
  25. package/dist/templates/fullstack/AGENTS.md +17 -2
  26. package/dist/templates/fullstack/README.md +19 -5
  27. package/dist/templates/fullstack/apps/api/package.json +1 -1
  28. package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
  29. package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -0
  30. package/dist/templates/fullstack/apps/api/src/app.spec.ts +20 -1
  31. package/dist/templates/fullstack/apps/api/src/app.ts +11 -0
  32. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +3 -0
  33. package/dist/templates/fullstack/apps/api/src/config/redis.connection.spec.ts +48 -2
  34. package/dist/templates/fullstack/apps/api/src/config/redis.connection.ts +45 -0
  35. package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +46 -0
  36. package/dist/templates/fullstack/apps/api/src/config/server-options.ts +48 -0
  37. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +1 -0
  38. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
  39. package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +196 -0
  40. package/dist/templates/fullstack/apps/api/src/core/services.ts +181 -2
  41. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
  42. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
  43. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +8 -1
  44. package/dist/templates/fullstack/apps/api/src/database/database.ts +29 -10
  45. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
  46. package/dist/templates/fullstack/apps/api/src/database/pool-guard.spec.ts +188 -0
  47. package/dist/templates/fullstack/apps/api/src/database/pool-guard.ts +151 -0
  48. package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +28 -4
  49. package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +10 -2
  50. package/dist/templates/fullstack/apps/api/src/main.ts +2 -1
  51. package/dist/templates/fullstack/apps/web/components.json +3 -1
  52. package/dist/templates/fullstack/apps/web/package.json +1 -1
  53. package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/index.ts +7 -0
  54. package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/textarea.svelte +22 -0
  55. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +276 -1
  56. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +194 -9
  57. package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +15 -0
  58. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-headers.ts +12 -0
  59. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
  60. package/dist/templates/fullstack/dot-claude/skills/podokit-elysia-endpoint/SKILL.md +5 -0
  61. package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +14 -1
  62. package/dist/templates/fullstack/dot-devcontainer/devcontainer.json +1 -0
  63. package/dist/templates/fullstack/{dot-env.docker → dot-env.docker.example} +7 -4
  64. package/dist/templates/fullstack/dot-env.example +10 -0
  65. package/dist/templates/fullstack/dot-gitignore +1 -0
  66. package/dist/templates/fullstack/dot-podokit/dev.json +2 -1
  67. package/dist/templates/fullstack/infra/k3s/README.md +3 -1
  68. package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
  69. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
  70. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
  71. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
  72. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
  73. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
  74. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
  75. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
  76. package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
  77. package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
  78. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +27 -9
  79. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
  80. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
  81. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/asset-paths.ts +11 -0
  82. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
  83. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
  84. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
  85. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts +125 -0
  86. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.ts +107 -0
  87. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
  88. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
  89. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
  90. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
  91. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +2 -1
  92. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -1
  93. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +15 -12
  94. package/dist/templates/modules/admin-dashboard/files/tests/helpers/object-response.ts +12 -0
  95. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
  96. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
  97. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
  98. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
  99. package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
  100. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
  101. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
  102. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +14 -3
  103. package/dist/templates/modules/admin-dashboard/module.manifest.json +4 -2
  104. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
  105. package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
  106. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.spec.ts +26 -0
  107. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +17 -2
  108. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +8 -4
  109. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +2 -2
  110. package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.spec.ts +114 -0
  111. package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.ts +50 -0
  112. package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +4 -1
  113. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +11 -2
  114. package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance-command.ts +5 -0
  115. package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance.ts +14 -0
  116. package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.spec.ts +136 -0
  117. package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.ts +40 -0
  118. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
  119. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
  120. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
  121. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
  122. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
  123. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -2
  124. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
  125. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +121 -1
  126. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +108 -10
  127. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +3 -1
  128. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +31 -3
  129. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  130. package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +2 -4
  131. package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.spec.ts +224 -0
  132. package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +124 -35
  133. package/dist/templates/modules/events-redis/module.manifest.json +4 -0
  134. package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.routes.ts +136 -0
  135. package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.spec.ts +636 -0
  136. package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.ts +673 -0
  137. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +10 -4
  138. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +37 -8
  139. package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-config.ts +99 -0
  140. package/dist/templates/modules/file-upload/files/apps/api/src/migrations/1792100000000-InitFileUploadSessions.ts +49 -0
  141. package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.test.ts +456 -0
  142. package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.ts +356 -0
  143. package/dist/templates/modules/file-upload/files/tests/api/chunked-uploads.api.spec.ts +147 -0
  144. package/dist/templates/modules/file-upload/module.manifest.json +34 -6
  145. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +1 -1
  146. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +23 -0
  147. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +30 -20
  148. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  149. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +20 -0
  150. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +79 -12
  151. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +16 -1
  152. package/dist/templates/modules/object-storage-local/module.manifest.json +11 -2
  153. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.spec.ts +115 -0
  154. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.ts +171 -0
  155. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +222 -9
  156. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +55 -19
  157. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.spec.ts +71 -0
  158. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.ts +98 -0
  159. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +6 -3
  160. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +7 -2
  161. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +187 -44
  162. package/dist/templates/modules/object-storage-s3/module.manifest.json +10 -2
  163. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +11 -22
  164. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +113 -0
  165. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +122 -0
  166. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +24 -0
  167. package/dist/templates/modules/rate-limit/module.manifest.json +7 -0
  168. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +14 -1
  169. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +9 -12
  170. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +16 -4
  171. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +233 -5
  172. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +134 -32
  173. package/dist/templates/modules/redis/module.manifest.json +4 -0
  174. package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +11 -4
  175. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
  176. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
  177. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +68 -0
  178. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +25 -1
  179. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
  180. package/dist/templates/modules/sse/module.manifest.json +3 -1
  181. package/dist/templates/todo/AGENTS.md +9 -1
  182. package/dist/templates/todo/README.md +18 -5
  183. package/dist/update.d.ts +22 -0
  184. package/dist/update.js +68 -8
  185. package/package.json +2 -2
  186. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-validation.ts +0 -13
  187. package/dist/templates/modules/auth/files/apps/api/src/auth/skip-schema-validation.ts +0 -5
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.profilePath = exports.profileDirectory = void 0;
4
+ exports.parseDeploymentProfile = parseDeploymentProfile;
4
5
  exports.loadDeploymentProfile = loadDeploymentProfile;
5
6
  exports.listDeploymentProfiles = listDeploymentProfiles;
6
7
  exports.initializeDeploymentProfile = initializeDeploymentProfile;
@@ -11,6 +12,7 @@ const node_fs_1 = require("node:fs");
11
12
  const node_path_1 = require("node:path");
12
13
  const lockfile_1 = require("./lockfile");
13
14
  const deploy_schema_1 = require("./deploy-schema");
15
+ const api_paths_1 = require("./api-paths");
14
16
  const websocket_paths_1 = require("./websocket-paths");
15
17
  var deploy_schema_2 = require("./deploy-schema");
16
18
  Object.defineProperty(exports, "profileDirectory", { enumerable: true, get: function () { return deploy_schema_2.profileDirectory; } });
@@ -84,7 +86,7 @@ function parseStatefulDependency(value, field, extraKeys = []) {
84
86
  function parseRedisDependency(value, field) {
85
87
  return parseStatefulDependency(value, field);
86
88
  }
87
- function parseProfile(value) {
89
+ function parseDeploymentProfile(value) {
88
90
  if (!(0, deploy_schema_1.isRecord)(value))
89
91
  throw new Error("Deployment profile must be a JSON object.");
90
92
  (0, deploy_schema_1.assertOnlyKeys)(value, [
@@ -117,7 +119,7 @@ function parseProfile(value) {
117
119
  const runtimeConfig = (0, deploy_schema_1.requiredRecord)(value, "runtimeConfig");
118
120
  (0, deploy_schema_1.assertOnlyKeys)(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
119
121
  (0, deploy_schema_1.assertOnlyKeys)(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
120
- (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths"], "exposure");
122
+ (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths", "apiPaths"], "exposure");
121
123
  (0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
122
124
  (0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
123
125
  (0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "imagePull"], "secrets");
@@ -167,6 +169,10 @@ function parseProfile(value) {
167
169
  if (exposureMode === "nodePort" && webSocketPaths.length > 0) {
168
170
  throw new Error("Deployment exposure.webSocketPaths requires exposure.mode ingress.");
169
171
  }
172
+ const apiPaths = (0, api_paths_1.parseApiPaths)(exposure.apiPaths ?? [], "exposure.apiPaths", webSocketPaths);
173
+ if (exposureMode === "nodePort" && apiPaths.length > 0) {
174
+ throw new Error("Deployment exposure.apiPaths requires exposure.mode ingress.");
175
+ }
170
176
  const postgres = parseStatefulDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres");
171
177
  const redis = parseRedisDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis");
172
178
  const objectStorageRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "objectStorage");
@@ -210,6 +216,7 @@ function parseProfile(value) {
210
216
  ingressClassName,
211
217
  tlsSecretName,
212
218
  webSocketPaths,
219
+ apiPaths,
213
220
  },
214
221
  workloads: {
215
222
  api: parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
@@ -246,7 +253,7 @@ function loadDeploymentProfile(projectRoot, name) {
246
253
  catch (error) {
247
254
  throw new Error(`Cannot read deployment profile "${name}": ${error.message}`);
248
255
  }
249
- return parseProfile(parsed);
256
+ return parseDeploymentProfile(parsed);
250
257
  }
251
258
  function listDeploymentProfiles(projectRoot) {
252
259
  const directory = (0, deploy_schema_1.profileDirectory)(projectRoot);
@@ -319,6 +326,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
319
326
  ingressClassName: "traefik",
320
327
  tlsSecretName: null,
321
328
  webSocketPaths: [],
329
+ apiPaths: (0, api_paths_1.installedApiPaths)(manifest.modules),
322
330
  },
323
331
  workloads: {
324
332
  api: {
@@ -418,7 +426,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
418
426
  ],
419
427
  },
420
428
  };
421
- const validated = parseProfile(defaultProfile);
429
+ const validated = parseDeploymentProfile(defaultProfile);
422
430
  (0, node_fs_1.mkdirSync)((0, deploy_schema_1.profileDirectory)(projectRoot), { recursive: true });
423
431
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
424
432
  return { name, path, profile: validated };
@@ -5,6 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_crypto_1 = require("node:crypto");
6
6
  const node_path_1 = require("node:path");
7
7
  const deploy_profile_1 = require("./deploy-profile");
8
+ const api_paths_1 = require("./api-paths");
8
9
  const lockfile_1 = require("./lockfile");
9
10
  const toolchain_1 = require("./toolchain");
10
11
  function quote(value) {
@@ -525,6 +526,57 @@ spec:
525
526
  app.kubernetes.io/name: ${name}
526
527
  `;
527
528
  }
529
+ function ingressPath(path, pathType, service) {
530
+ return ` - path: ${quote(path)}
531
+ pathType: ${pathType}
532
+ backend:
533
+ service:
534
+ name: ${service}
535
+ port: { number: 3000 }
536
+ `;
537
+ }
538
+ /**
539
+ * Direct API paths need /api removed on the way to the API, which a standard
540
+ * Ingress cannot express, so they get their own Ingress with a Traefik
541
+ * stripPrefix middleware. ImplementationSpecific makes Traefik match the
542
+ * trailing-slash prefix as a plain PathPrefix, and the Exact path adds the bare
543
+ * path, so every controller version routes the same set of requests.
544
+ */
545
+ function apiPathIngress(profile, tls) {
546
+ if (profile.exposure.apiPaths.length === 0)
547
+ return "";
548
+ const apiService = `${profile.target.release}-api`;
549
+ const middleware = `${profile.target.release}-strip-api-prefix`;
550
+ const paths = profile.exposure.apiPaths
551
+ .map((path) => {
552
+ const base = (0, api_paths_1.apiPathBase)(path);
553
+ return (ingressPath(base, "Exact", apiService) +
554
+ ingressPath(`${base}/`, "ImplementationSpecific", apiService));
555
+ })
556
+ .join("");
557
+ return `---
558
+ apiVersion: traefik.io/v1alpha1
559
+ kind: Middleware
560
+ metadata:
561
+ name: ${middleware}
562
+ spec:
563
+ stripPrefix:
564
+ prefixes: [${quote(api_paths_1.API_PATH_PREFIX)}]
565
+ ---
566
+ apiVersion: networking.k8s.io/v1
567
+ kind: Ingress
568
+ metadata:
569
+ name: ${profile.target.release}-api-paths
570
+ annotations:
571
+ traefik.ingress.kubernetes.io/router.middlewares: ${quote(`${profile.target.namespace}-${middleware}@kubernetescrd`)}
572
+ spec:
573
+ ingressClassName: ${quote(profile.exposure.ingressClassName)}
574
+ ${tls} rules:
575
+ - host: ${quote(profile.exposure.host)}
576
+ http:
577
+ paths:
578
+ ${paths}`;
579
+ }
528
580
  function exposure(profile) {
529
581
  const webService = `${profile.target.release}-web`;
530
582
  const apiService = `${profile.target.release}-api`;
@@ -548,13 +600,7 @@ spec:
548
600
  `
549
601
  : "";
550
602
  const webSocketPaths = profile.exposure.webSocketPaths
551
- .map((path) => ` - path: ${quote(path)}
552
- pathType: Exact
553
- backend:
554
- service:
555
- name: ${apiService}
556
- port: { number: 3000 }
557
- `)
603
+ .map((path) => ingressPath(path, "Exact", apiService))
558
604
  .join("");
559
605
  return `apiVersion: networking.k8s.io/v1
560
606
  kind: Ingress
@@ -572,7 +618,7 @@ ${webSocketPaths} - path: /
572
618
  service:
573
619
  name: ${webService}
574
620
  port: { number: 3000 }
575
- `;
621
+ ${apiPathIngress(profile, tls)}`;
576
622
  }
577
623
  function applicationManifest(profile, images, rolloutStateDigest, workerCommand) {
578
624
  const runtimeConfigDigest = derivedRuntimeConfigDigest(profile);
package/dist/deploy.js CHANGED
@@ -13,6 +13,7 @@ const node_crypto_1 = require("node:crypto");
13
13
  const node_fs_1 = require("node:fs");
14
14
  const node_os_1 = require("node:os");
15
15
  const node_path_1 = require("node:path");
16
+ const api_paths_1 = require("./api-paths");
16
17
  const deploy_profile_1 = require("./deploy-profile");
17
18
  const deploy_render_1 = require("./deploy-render");
18
19
  function defaultRunner(command, args, options) {
@@ -168,6 +169,25 @@ function doctorDeployment(projectRoot, profileName, runner = defaultRunner) {
168
169
  ? `IngressClass ${profile.exposure.ingressClassName} exists.`
169
170
  : `IngressClass ${profile.exposure.ingressClassName} is missing or inaccessible.`,
170
171
  });
172
+ if (profile.exposure.apiPaths.length > 0) {
173
+ // Direct API paths remove /api with a Traefik Middleware resource.
174
+ const middlewareCrd = runner("kubectl", [
175
+ "--context",
176
+ profile.target.context,
177
+ "get",
178
+ "crd",
179
+ "middlewares.traefik.io",
180
+ "-o",
181
+ "name",
182
+ ], { capture: true });
183
+ findings.push({
184
+ code: "crd:middlewares.traefik.io",
185
+ ok: middlewareCrd.status === 0,
186
+ message: middlewareCrd.status === 0
187
+ ? "Traefik Middleware resources are available for exposure.apiPaths."
188
+ : "exposure.apiPaths needs the Traefik Middleware CRD (middlewares.traefik.io); serve the IngressClass with Traefik or remove exposure.apiPaths.",
189
+ });
190
+ }
171
191
  if (profile.exposure.tlsSecretName) {
172
192
  const tlsType = runner("kubectl", kubectlArgs(profile, [
173
193
  "get",
@@ -465,7 +485,7 @@ function planDeployment(projectRoot, profileName, release, runner = defaultRunne
465
485
  {
466
486
  order: 3,
467
487
  kind: "application",
468
- description: `Roll out API, web${profile.workloads.worker ? ", and worker" : ""} workloads with zero unavailable replicas.`,
488
+ description: `Roll out API, web${profile.workloads.worker ? ", and worker" : ""} workloads with zero unavailable replicas.${(0, api_paths_1.directApiRoutes)(profile.exposure.apiPaths)}`,
469
489
  },
470
490
  {
471
491
  order: 4,
package/dist/dev.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface DevConfig {
3
3
  hostname: string;
4
4
  publicUrl?: string;
5
5
  webSocketPaths: string[];
6
+ /** Path prefixes under /api/ that skip the web proxy and reach the API directly. */
7
+ apiPaths: string[];
6
8
  }
7
9
  export interface DevRuntime {
8
10
  alias: string;
@@ -24,5 +26,17 @@ export declare function resolveDevRuntime(projectRoot: string): DevRuntime;
24
26
  export declare function installedDevProfiles(projectRoot: string): string[];
25
27
  export declare function renderRuntimeCompose(runtime: DevRuntime): string;
26
28
  export declare function renderRoute(runtime: DevRuntime): string;
29
+ /**
30
+ * Create the untracked `.env.docker` from the committed `.env.docker.example`
31
+ * when it is missing, filling every empty `*_SECRET` value with a random one.
32
+ * An existing file is never changed. Returns true when the file was created.
33
+ */
34
+ export declare function ensureContainerEnv(projectRoot: string): boolean;
35
+ /**
36
+ * Process IDs of the Compose Watch that `podo dev watch` started for this
37
+ * project. The project-specific runtime overlay path in the command line
38
+ * identifies it, so watches of other projects never match.
39
+ */
40
+ export declare function findWatchProcesses(runtime: DevRuntime, psOutput: string): number[];
27
41
  export declare function runDevCommand(projectRoot: string, action: string | undefined, args: string[], runner?: CommandRunner): void;
28
42
  export {};
package/dist/dev.js CHANGED
@@ -5,12 +5,15 @@ exports.resolveDevRuntime = resolveDevRuntime;
5
5
  exports.installedDevProfiles = installedDevProfiles;
6
6
  exports.renderRuntimeCompose = renderRuntimeCompose;
7
7
  exports.renderRoute = renderRoute;
8
+ exports.ensureContainerEnv = ensureContainerEnv;
9
+ exports.findWatchProcesses = findWatchProcesses;
8
10
  exports.runDevCommand = runDevCommand;
9
11
  const node_child_process_1 = require("node:child_process");
10
12
  const node_crypto_1 = require("node:crypto");
11
13
  const node_fs_1 = require("node:fs");
12
14
  const node_os_1 = require("node:os");
13
15
  const node_path_1 = require("node:path");
16
+ const api_paths_1 = require("./api-paths");
14
17
  const lockfile_1 = require("./lockfile");
15
18
  const websocket_paths_1 = require("./websocket-paths");
16
19
  const GATEWAY_CONTAINER = "podokit-dev-gateway";
@@ -21,6 +24,9 @@ const GATEWAY_VERSION_LABEL = "io.podosoft.podokit.dev-gateway.version";
21
24
  const GATEWAY_NETWORK = "podokit-dev-gateway";
22
25
  const RUNTIME_COMPOSE = ".podokit/runtime/dev.compose.yaml";
23
26
  const DEV_CONFIG = ".podokit/dev.json";
27
+ const CONTAINER_ENV = ".env.docker";
28
+ const CONTAINER_ENV_EXAMPLE = ".env.docker.example";
29
+ const GENERATED_SECRET = /^([A-Z][A-Z0-9_]*_SECRET)=[ \t]*(\r?)$/gm;
24
30
  const DEV_PROFILE_BY_MODULE = new Map([
25
31
  ["redis", "cache"],
26
32
  ["object-storage-s3", "storage"],
@@ -81,16 +87,23 @@ function readDevConfig(projectRoot) {
81
87
  throw new Error("PodoKit manifest is missing answers.projectName.");
82
88
  const config = (0, node_fs_1.existsSync)(path)
83
89
  ? parseJson(path)
84
- : { schemaVersion: 1, hostname: `${fallbackName}.localhost`, webSocketPaths: [] };
90
+ : {
91
+ schemaVersion: 1,
92
+ hostname: `${fallbackName}.localhost`,
93
+ webSocketPaths: [],
94
+ apiPaths: [],
95
+ };
85
96
  if (config.schemaVersion !== 1) {
86
97
  throw new Error(`Unsupported ${DEV_CONFIG} schemaVersion: ${String(config.schemaVersion)}`);
87
98
  }
88
99
  validateHostname(config.hostname);
89
100
  if (config.publicUrl)
90
101
  validatePublicUrl(config.publicUrl);
102
+ const webSocketPaths = (0, websocket_paths_1.parseExactWebSocketPaths)(config.webSocketPaths ?? [], `${DEV_CONFIG}.webSocketPaths`);
91
103
  return {
92
104
  ...config,
93
- webSocketPaths: (0, websocket_paths_1.parseExactWebSocketPaths)(config.webSocketPaths ?? [], `${DEV_CONFIG}.webSocketPaths`),
105
+ webSocketPaths,
106
+ apiPaths: (0, api_paths_1.parseApiPaths)(config.apiPaths ?? [], `${DEV_CONFIG}.apiPaths`, webSocketPaths),
94
107
  };
95
108
  }
96
109
  function stableId(projectRoot) {
@@ -123,8 +136,15 @@ function installedDevProfiles(projectRoot) {
123
136
  .filter((profile) => profile !== undefined)),
124
137
  ];
125
138
  }
139
+ function routesToApi(config) {
140
+ return config.webSocketPaths.length > 0 || config.apiPaths.length > 0;
141
+ }
142
+ /** Traefik v3 matchers take one value each, so several paths are joined with ||. */
143
+ function anyOf(matchers) {
144
+ return matchers.length > 1 ? `(${matchers.join(" || ")})` : (matchers[0] ?? "");
145
+ }
126
146
  function renderRuntimeCompose(runtime) {
127
- const api = runtime.config.webSocketPaths.length
147
+ const api = routesToApi(runtime.config)
128
148
  ? ` api:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}-api]\n`
129
149
  : "";
130
150
  return `# Generated by podo dev. Do not edit.\nservices:\n traefik:\n profiles: [podokit-legacy-proxy]\n web:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}]\n${api}networks:\n ${GATEWAY_NETWORK}:\n external: true\n name: ${GATEWAY_NETWORK}\n`;
@@ -133,14 +153,24 @@ function renderRoute(runtime) {
133
153
  const { alias, config, routeId } = runtime;
134
154
  const hosts = routeHosts(config);
135
155
  const hostRule = hosts.map((host) => `Host(\`${host}\`)`).join(" || ");
136
- const webSocketHostRule = hosts.length > 1 ? `(${hostRule})` : hostRule;
156
+ const scopedHostRule = hosts.length > 1 ? `(${hostRule})` : hostRule;
137
157
  const webSocketRouter = config.webSocketPaths.length
138
- ? ` ${routeId}-websocket:\n entryPoints: [web]\n rule: ${webSocketHostRule} && Path(${config.webSocketPaths.map((path) => `\`${path}\``).join(", ")})\n priority: 100\n service: ${routeId}-api\n`
158
+ ? ` ${routeId}-websocket:\n entryPoints: [web]\n rule: ${scopedHostRule} && ${anyOf(config.webSocketPaths.map((path) => `Path(\`${path}\`)`))}\n priority: 100\n service: ${routeId}-api\n`
159
+ : "";
160
+ const apiPathMatchers = config.apiPaths.flatMap((path) => {
161
+ const base = (0, api_paths_1.apiPathBase)(path);
162
+ return [`Path(\`${base}\`)`, `PathPrefix(\`${base}/\`)`];
163
+ });
164
+ const apiPathRouter = config.apiPaths.length
165
+ ? ` ${routeId}-api-paths:\n entryPoints: [web]\n rule: ${scopedHostRule} && ${anyOf(apiPathMatchers)}\n priority: 100\n middlewares: [${routeId}-api-prefix]\n service: ${routeId}-api\n`
139
166
  : "";
140
- const apiService = config.webSocketPaths.length
167
+ const apiService = routesToApi(config)
141
168
  ? ` ${routeId}-api:\n loadBalancer:\n servers:\n - url: http://${alias}-api:5002\n`
142
169
  : "";
143
- return `http:\n routers:\n${webSocketRouter} ${routeId}:\n entryPoints: [web]\n rule: ${hostRule}\n service: ${routeId}\n middlewares: [${routeId}-compression]\n services:\n${apiService} ${routeId}:\n loadBalancer:\n servers:\n - url: http://${alias}:5001\n middlewares:\n ${routeId}-compression:\n compress: {}\n`;
170
+ const apiPrefixMiddleware = config.apiPaths.length
171
+ ? ` ${routeId}-api-prefix:\n stripPrefix:\n prefixes: ["${api_paths_1.API_PATH_PREFIX}"]\n`
172
+ : "";
173
+ return `http:\n routers:\n${webSocketRouter}${apiPathRouter} ${routeId}:\n entryPoints: [web]\n rule: ${hostRule}\n service: ${routeId}\n middlewares: [${routeId}-compression]\n services:\n${apiService} ${routeId}:\n loadBalancer:\n servers:\n - url: http://${alias}:5001\n middlewares:\n ${routeId}-compression:\n compress: {}\n${apiPrefixMiddleware}`;
144
174
  }
145
175
  function devHome() {
146
176
  return process.env.PODOKIT_DEV_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".podokit", "dev-gateway");
@@ -182,6 +212,28 @@ function activeEntries() {
182
212
  }
183
213
  return entries;
184
214
  }
215
+ /**
216
+ * Create the untracked `.env.docker` from the committed `.env.docker.example`
217
+ * when it is missing, filling every empty `*_SECRET` value with a random one.
218
+ * An existing file is never changed. Returns true when the file was created.
219
+ */
220
+ function ensureContainerEnv(projectRoot) {
221
+ const target = (0, node_path_1.join)(projectRoot, CONTAINER_ENV);
222
+ if ((0, node_fs_1.existsSync)(target))
223
+ return false;
224
+ const example = (0, node_path_1.join)(projectRoot, CONTAINER_ENV_EXAMPLE);
225
+ if (!(0, node_fs_1.existsSync)(example)) {
226
+ const compose = (0, node_path_1.join)(projectRoot, "compose.dev.yaml");
227
+ if ((0, node_fs_1.existsSync)(compose) && (0, node_fs_1.readFileSync)(compose, "utf8").includes(CONTAINER_ENV)) {
228
+ throw new Error(`${CONTAINER_ENV} is missing and there is no ${CONTAINER_ENV_EXAMPLE} to create it from. Run podo update --apply to add the example, or create ${CONTAINER_ENV} by hand.`);
229
+ }
230
+ return false;
231
+ }
232
+ const content = (0, node_fs_1.readFileSync)(example, "utf8").replace(GENERATED_SECRET, (_line, name, carriageReturn) => `${name}=${(0, node_crypto_1.randomBytes)(32).toString("base64url")}${carriageReturn}`);
233
+ (0, node_fs_1.writeFileSync)(target, content, { mode: 0o600 });
234
+ process.stdout.write(`Created ${CONTAINER_ENV} from ${CONTAINER_ENV_EXAMPLE} with generated secrets.\n`);
235
+ return true;
236
+ }
185
237
  function writeRuntimeFiles(runtime) {
186
238
  (0, node_fs_1.mkdirSync)((0, node_path_1.join)(runtime.projectRoot, ".podokit", "runtime"), { recursive: true });
187
239
  (0, node_fs_1.writeFileSync)(runtime.runtimeComposePath, renderRuntimeCompose(runtime));
@@ -296,6 +348,69 @@ function runCompose(runtime, args, runner) {
296
348
  throw new Error(`docker compose ${args.join(" ")} exited with status ${String(result.status)}.`);
297
349
  }
298
350
  }
351
+ // Exit statuses of a Compose process stopped by SIGINT or SIGTERM.
352
+ const STOPPED_STATUSES = new Set([130, 143]);
353
+ const WATCH_STOP_TIMEOUT_MS = 10_000;
354
+ function pause(milliseconds) {
355
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
356
+ }
357
+ /**
358
+ * Process IDs of the Compose Watch that `podo dev watch` started for this
359
+ * project. The project-specific runtime overlay path in the command line
360
+ * identifies it, so watches of other projects never match.
361
+ */
362
+ function findWatchProcesses(runtime, psOutput) {
363
+ const prefix = `compose ${composeArgs(runtime, []).slice(1).join(" ")} `;
364
+ const pids = [];
365
+ for (const line of psOutput.split("\n")) {
366
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
367
+ if (!match?.[1] || !match[2])
368
+ continue;
369
+ const start = match[2].indexOf(prefix);
370
+ if (start === -1)
371
+ continue;
372
+ const tokens = match[2].slice(start + prefix.length).split(/\s+/);
373
+ let index = 0;
374
+ while (tokens[index] === "--profile" || tokens[index]?.startsWith("--profile=")) {
375
+ index += tokens[index] === "--profile" ? 2 : 1;
376
+ }
377
+ if (tokens[index] === "watch")
378
+ pids.push(Number(match[1]));
379
+ }
380
+ return pids;
381
+ }
382
+ function anyAlive(pids, runner) {
383
+ return pids.some((pid) => commandSucceeded(runner("kill", ["-0", String(pid)], { capture: true })));
384
+ }
385
+ /**
386
+ * Stop this project's Compose Watch. Compose holds a per-project lock for as
387
+ * long as it watches, so a watch left behind by `down` made the next
388
+ * `podo dev watch` fail with "cannot take exclusive lock".
389
+ */
390
+ function stopWatch(runtime, runner) {
391
+ let listing;
392
+ try {
393
+ listing = runner("ps", ["-A", "-ww", "-o", "pid=,args="], { capture: true });
394
+ }
395
+ catch {
396
+ return;
397
+ }
398
+ if (!commandSucceeded(listing))
399
+ return;
400
+ const pids = findWatchProcesses(runtime, listing.stdout);
401
+ if (pids.length === 0)
402
+ return;
403
+ runner("kill", ["-TERM", ...pids.map(String)], { capture: true });
404
+ const deadline = Date.now() + WATCH_STOP_TIMEOUT_MS;
405
+ while (anyAlive(pids, runner)) {
406
+ if (Date.now() >= deadline) {
407
+ runner("kill", ["-KILL", ...pids.map(String)], { capture: true });
408
+ break;
409
+ }
410
+ pause(100);
411
+ }
412
+ process.stdout.write(`Stopped podo dev watch for this project (pid ${pids.join(", ")}).\n`);
413
+ }
299
414
  function composeCommand(action, args, installedProfiles) {
300
415
  const global = [];
301
416
  const command = [];
@@ -320,6 +435,12 @@ function composeCommand(action, args, installedProfiles) {
320
435
  command.push(value);
321
436
  }
322
437
  }
438
+ // Compose Watch syncs edits into the running containers, not into the image.
439
+ // A plain `up` that recreates a container would start it from the last
440
+ // built image and drop those edits, so `up` rebuilds unless told otherwise.
441
+ if (action === "up" && !command.includes("--build") && !command.includes("--no-build")) {
442
+ command.unshift("--build");
443
+ }
323
444
  const activeProfiles = action === "down"
324
445
  ? ["--profile", "*"]
325
446
  : installedProfiles
@@ -333,6 +454,7 @@ function stopGatewayWhenUnused(runner) {
333
454
  runner("docker", ["rm", "--force", GATEWAY_CONTAINER], { capture: true });
334
455
  runner("docker", ["network", "rm", GATEWAY_NETWORK], { capture: true });
335
456
  }
457
+ const DOCKER_ACTIONS = new Set(["watch", "up", "exec", "logs", "ps", "down"]);
336
458
  function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
337
459
  const runtime = resolveDevRuntime(projectRoot);
338
460
  const profiles = installedDevProfiles(projectRoot);
@@ -343,6 +465,16 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
343
465
  process.stdout.write(`${runtime.config.publicUrl}\n`);
344
466
  return;
345
467
  }
468
+ if (!DOCKER_ACTIONS.has(action ?? "") && action !== "env") {
469
+ throw new Error("Usage: podo dev <watch|up|exec|logs|ps|down|url|env> [docker compose options]");
470
+ }
471
+ // Git ignores .env.docker, so a fresh clone gets it from the committed example.
472
+ const createdEnv = ensureContainerEnv(projectRoot);
473
+ if (action === "env") {
474
+ if (!createdEnv)
475
+ process.stdout.write(`${CONTAINER_ENV} already exists; left unchanged.\n`);
476
+ return;
477
+ }
346
478
  ensureDocker(runner);
347
479
  if (action === "watch") {
348
480
  ensureNetwork(runner);
@@ -352,7 +484,18 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
352
484
  process.stdout.write(`Development URL: http://${runtime.config.hostname}\n`);
353
485
  if (runtime.config.publicUrl)
354
486
  process.stdout.write(`Public OAuth URL: ${runtime.config.publicUrl}\n`);
355
- runCompose(runtime, composeCommand("watch", args, profiles), runner);
487
+ const watchArgs = composeCommand("watch", args, profiles);
488
+ const result = runner("docker", composeArgs(runtime, watchArgs), { capture: false });
489
+ if (!commandSucceeded(result)) {
490
+ // `podo dev down` or Ctrl+C ends the watch with SIGTERM or SIGINT.
491
+ if (result.status !== null && STOPPED_STATUSES.has(result.status)) {
492
+ unregisterRoute(runtime);
493
+ stopGatewayWhenUnused(runner);
494
+ process.stdout.write("Stopped watching.\n");
495
+ return;
496
+ }
497
+ throw new Error(`docker compose ${watchArgs.join(" ")} exited with status ${String(result.status)}.`);
498
+ }
356
499
  }
357
500
  catch (error) {
358
501
  unregisterRoute(runtime);
@@ -379,6 +522,7 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
379
522
  return;
380
523
  }
381
524
  if (action === "down") {
525
+ stopWatch(runtime, runner);
382
526
  runCompose(runtime, composeCommand("down", args, profiles), runner);
383
527
  unregisterRoute(runtime);
384
528
  stopGatewayWhenUnused(runner);
@@ -386,7 +530,5 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
386
530
  }
387
531
  if (action === "exec" || action === "logs" || action === "ps") {
388
532
  runCompose(runtime, composeCommand(action, args, profiles), runner);
389
- return;
390
533
  }
391
- throw new Error("Usage: podo dev <watch|up|exec|logs|ps|down|url> [docker compose options]");
392
534
  }
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ const deploy_compose_render_1 = require("./deploy-compose-render");
23
23
  const deploy_compose_1 = require("./deploy-compose");
24
24
  const deploy_compose_sync_1 = require("./deploy-compose-sync");
25
25
  const provider_1 = require("./provider");
26
+ const module_api_paths_1 = require("./module-api-paths");
26
27
  const HELP = `podo — PodoKit project generator
27
28
 
28
29
  Usage:
@@ -219,6 +220,11 @@ function parseArgs(argv) {
219
220
  parsed.positionals = positionals;
220
221
  return parsed;
221
222
  }
223
+ function writeApiPaths(heading, result, verb) {
224
+ const lines = (0, module_api_paths_1.describeApiPaths)(result, verb);
225
+ if (lines.length)
226
+ process.stdout.write(`\n${heading}\n${lines.join("\n")}\n`);
227
+ }
222
228
  function fail(message) {
223
229
  process.stderr.write(`error: ${message}\n`);
224
230
  process.exit(1);
@@ -402,6 +408,7 @@ async function main(argv) {
402
408
  if (result.adopted.length) {
403
409
  process.stdout.write(`\nAdopted as module-managed file(s):\n${result.adopted.map((file) => ` ${file}`).join("\n")}\n`);
404
410
  }
411
+ writeApiPaths("Direct API paths (the gateway sends them to the API without the web proxy):", result.apiPaths, "add");
405
412
  if (result.instructions.length) {
406
413
  process.stdout.write(`\nNext steps:\n${result.instructions.map((i) => ` ${i}`).join("\n")}\n`);
407
414
  }
@@ -428,6 +435,7 @@ async function main(argv) {
428
435
  result.keptEdited.map((f) => ` ${f}`).join("\n") +
429
436
  "\n");
430
437
  }
438
+ writeApiPaths("Direct API paths:", result.apiPaths, "remove");
431
439
  }
432
440
  catch (err) {
433
441
  fail(err.message);
@@ -652,9 +660,13 @@ async function main(argv) {
652
660
  try {
653
661
  if (args.apply) {
654
662
  const result = (0, update_1.applyUpdate)(process.cwd(), templatesDir, { oldTemplatesDir: args.from });
663
+ if (result.addedModules.length) {
664
+ process.stdout.write(`Added required module(s): ${result.addedModules.join(", ")}\n`);
665
+ }
655
666
  process.stdout.write(`Applied: ${result.written.length} written, ${result.moved.length} moved, ` +
656
667
  `${result.removed.length} removed, ` +
657
668
  `${result.merged.length} merged, ${result.conflicts.length} conflict.\n`);
669
+ writeApiPaths("Direct API paths:", result.apiPaths, "add");
658
670
  if (result.conflicts.length) {
659
671
  process.stdout.write("\nResolve the following, then commit:\n" +
660
672
  result.conflicts.map((f) => ` ${f}`).join("\n") +
@@ -666,7 +678,16 @@ async function main(argv) {
666
678
  const counts = (0, update_1.summarize)(plan);
667
679
  const shown = plan.changes.filter((c) => c.action !== "up-to-date" && c.action !== "skip");
668
680
  process.stdout.write(`podo update ${plan.fromVersion} -> ${plan.toVersion} (template: ${plan.template}; modules: ${plan.modules.join(", ") || "none"})\n\n`);
669
- if (!shown.length) {
681
+ if (plan.addedModules.length) {
682
+ process.stdout.write("Modules to add:\n" +
683
+ plan.addedModules.map((module) => ` ${module.name} (${(0, update_1.addedModuleReason)(module)})`).join("\n") +
684
+ "\n\n");
685
+ }
686
+ const apiPathLines = (0, module_api_paths_1.describeApiPaths)(plan.apiPaths, "add");
687
+ if (apiPathLines.length) {
688
+ process.stdout.write(`Direct API paths to add:\n${apiPathLines.join("\n")}\n\n`);
689
+ }
690
+ if (!shown.length && !plan.apiPaths.changed.length) {
670
691
  process.stdout.write("Everything is up to date.\n");
671
692
  }
672
693
  else {
@@ -674,7 +695,7 @@ async function main(argv) {
674
695
  const path = c.fromPath ? `${c.fromPath} -> ${c.path}` : c.path;
675
696
  process.stdout.write(` ${c.action.padEnd(9)} ${path} (${c.note})\n`);
676
697
  }
677
- process.stdout.write(`\n${counts.update} update, ${counts.add} add, ${counts.move} move, ` +
698
+ process.stdout.write(`${shown.length ? "\n" : ""}${counts.update} update, ${counts.add} add, ${counts.move} move, ` +
678
699
  `${counts.remove} remove, ${counts.conflict} conflict. ` +
679
700
  `Dry-run — nothing was written. Re-run with --apply to write (use --from <dir> for a 3-way merge).\n`);
680
701
  }
package/dist/lib.d.ts CHANGED
@@ -6,10 +6,12 @@ export { TEMPLATES, DEFAULT_TEMPLATE, TEMPLATE_NAMES, isKnownTemplate } from "./
6
6
  export type { TemplateInfo } from "./templates";
7
7
  export { listModules, addModule } from "./add";
8
8
  export type { ModuleManifest, ModuleMigration, ModulePathMove, ModuleTextReplacement, AddOptions, AddResult, } from "./add";
9
+ export type { ApiPathChange, ApiPathResult, ApiPathSkip, ModuleApiPath, } from "./module-api-paths";
10
+ export { describeApiPaths } from "./module-api-paths";
9
11
  export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
10
12
  export type { StatusReport, DoctorFinding } from "./inspect";
11
- export { planUpdate, summarize } from "./update";
12
- export type { UpdatePlan, FileChange } from "./update";
13
+ export { addedModuleReason, planUpdate, summarize } from "./update";
14
+ export type { AddedModule, UpdatePlan, FileChange } from "./update";
13
15
  export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
14
16
  export type { CommandRunner, DevConfig, DevRuntime } from "./dev";
15
17
  export { applyDeployment, doctorDeployment, getDeploymentStatus, inspectClusterFingerprint, planDeployment, planRollback, rollbackDeployment, verifyDeployment, } from "./deploy";
package/dist/lib.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getComposeStatus = exports.doctorComposeDeployment = exports.applyComposeDeployment = exports.renderComposeDeployment = exports.parseComposeProfile = exports.initializeComposeProfile = exports.readDeploymentDriver = exports.loadKubernetesProfile = exports.loadComposeProfile = exports.loadAnyDeploymentProfile = exports.listAnyDeploymentProfiles = exports.assertAnyReleaseTag = exports.DEPLOY_DRIVERS = exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.PROVIDERS_SOURCE = exports.providerSource = exports.planProviderChange = exports.listProviders = exports.applyProviderChange = exports.create = void 0;
4
- exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = void 0;
3
+ exports.applyComposeDeployment = exports.renderComposeDeployment = exports.parseComposeProfile = exports.initializeComposeProfile = exports.readDeploymentDriver = exports.loadKubernetesProfile = exports.loadComposeProfile = exports.loadAnyDeploymentProfile = exports.listAnyDeploymentProfiles = exports.assertAnyReleaseTag = exports.DEPLOY_DRIVERS = exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.addedModuleReason = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.describeApiPaths = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.PROVIDERS_SOURCE = exports.providerSource = exports.planProviderChange = exports.listProviders = exports.applyProviderChange = exports.create = void 0;
4
+ exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = exports.getComposeStatus = exports.doctorComposeDeployment = void 0;
5
5
  exports.builtinTemplatesDir = builtinTemplatesDir;
6
6
  exports.builtinModulesDir = builtinModulesDir;
7
7
  // Library entry point: the reusable functions behind the `podo` CLI, for other
@@ -23,12 +23,15 @@ Object.defineProperty(exports, "isKnownTemplate", { enumerable: true, get: funct
23
23
  var add_1 = require("./add");
24
24
  Object.defineProperty(exports, "listModules", { enumerable: true, get: function () { return add_1.listModules; } });
25
25
  Object.defineProperty(exports, "addModule", { enumerable: true, get: function () { return add_1.addModule; } });
26
+ var module_api_paths_1 = require("./module-api-paths");
27
+ Object.defineProperty(exports, "describeApiPaths", { enumerable: true, get: function () { return module_api_paths_1.describeApiPaths; } });
26
28
  var inspect_1 = require("./inspect");
27
29
  Object.defineProperty(exports, "status", { enumerable: true, get: function () { return inspect_1.status; } });
28
30
  Object.defineProperty(exports, "diff", { enumerable: true, get: function () { return inspect_1.diff; } });
29
31
  Object.defineProperty(exports, "doctor", { enumerable: true, get: function () { return inspect_1.doctor; } });
30
32
  Object.defineProperty(exports, "SUPPORTED_FRAMEWORKS", { enumerable: true, get: function () { return inspect_1.SUPPORTED_FRAMEWORKS; } });
31
33
  var update_1 = require("./update");
34
+ Object.defineProperty(exports, "addedModuleReason", { enumerable: true, get: function () { return update_1.addedModuleReason; } });
32
35
  Object.defineProperty(exports, "planUpdate", { enumerable: true, get: function () { return update_1.planUpdate; } });
33
36
  Object.defineProperty(exports, "summarize", { enumerable: true, get: function () { return update_1.summarize; } });
34
37
  var dev_1 = require("./dev");
@@ -21,12 +21,15 @@ export interface ManifestModule {
21
21
  moduleVersion?: string;
22
22
  /** Module-local update migrations already applied to this project. */
23
23
  appliedMigrations?: string[];
24
+ /** Direct API paths of the module already merged into the app's gateway configuration. */
25
+ appliedApiPaths?: string[];
24
26
  }
25
27
  export interface ManifestModuleInput {
26
28
  name: string;
27
29
  packageName?: string;
28
30
  moduleVersion?: string;
29
31
  appliedMigrations?: string[];
32
+ appliedApiPaths?: string[];
30
33
  }
31
34
  export interface PodokitManifest {
32
35
  schemaVersion: number;
@@ -116,4 +119,6 @@ export declare function mergeOwnedGlobs(existing: string[], add: string[]): stri
116
119
  * `ownedGlobs` a module declares are merged into the manifest so its owned paths
117
120
  * survive every future recompute. No-op on projects without a manifest.
118
121
  */
119
- export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[]): void;
122
+ export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[],
123
+ /** Installed module that newly recorded modules must configure before. */
124
+ insertBefore?: string): void;