@podosoft/podokit 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +2 -1
  2. package/dist/app-proxy-timeouts.d.ts +27 -0
  3. package/dist/app-proxy-timeouts.js +180 -0
  4. package/dist/assemble.d.ts +5 -2
  5. package/dist/assemble.js +14 -4
  6. package/dist/deploy-checks.d.ts +44 -0
  7. package/dist/deploy-checks.js +267 -0
  8. package/dist/deploy-compose-profile.d.ts +6 -2
  9. package/dist/deploy-compose-profile.js +15 -6
  10. package/dist/deploy-compose-render.d.ts +12 -4
  11. package/dist/deploy-compose-render.js +47 -18
  12. package/dist/deploy-compose.d.ts +2 -5
  13. package/dist/deploy-compose.js +7 -2
  14. package/dist/deploy-profile.d.ts +6 -2
  15. package/dist/deploy-profile.js +11 -7
  16. package/dist/deploy-render.d.ts +4 -1
  17. package/dist/deploy-render.js +27 -21
  18. package/dist/deploy-schema.d.ts +60 -2
  19. package/dist/deploy-schema.js +141 -14
  20. package/dist/deploy.d.ts +2 -5
  21. package/dist/deploy.js +5 -2
  22. package/dist/dev.d.ts +21 -0
  23. package/dist/dev.js +78 -3
  24. package/dist/eject.js +2 -0
  25. package/dist/index.js +85 -5
  26. package/dist/lib.d.ts +2 -0
  27. package/dist/lib.js +5 -2
  28. package/dist/lockfile.d.ts +24 -0
  29. package/dist/lockfile.js +74 -4
  30. package/dist/provider.js +10 -5
  31. package/dist/remove.js +1 -1
  32. package/dist/template-files.d.ts +42 -0
  33. package/dist/template-files.js +102 -0
  34. package/dist/templates/fullstack/AGENTS.md +30 -4
  35. package/dist/templates/fullstack/README.md +8 -7
  36. package/dist/templates/fullstack/apps/api/package.json +4 -4
  37. package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
  38. package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
  39. package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
  40. package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +7 -2
  41. package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
  42. package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
  43. package/dist/templates/fullstack/apps/api/src/build.spec.ts +121 -0
  44. package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
  45. package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
  46. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
  47. package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
  48. package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
  49. package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
  50. package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
  51. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +72 -4
  52. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
  53. package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
  54. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
  55. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
  56. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
  57. package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
  58. package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
  59. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
  60. package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
  61. package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
  62. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
  63. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
  64. package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
  65. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
  66. package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
  67. package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
  68. package/dist/templates/fullstack/apps/web/package.json +1 -1
  69. package/dist/templates/fullstack/apps/web/src/app.css +3 -0
  70. package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
  71. package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
  72. package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
  73. package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
  74. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  75. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
  76. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
  77. package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
  78. package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
  79. package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
  80. package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
  81. package/dist/templates/fullstack/compose.dev.yaml +7 -4
  82. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
  83. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +13 -3
  84. package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
  85. package/dist/templates/fullstack/dot-env.docker.example +5 -2
  86. package/dist/templates/fullstack/dot-env.example +4 -0
  87. package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
  88. package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
  89. package/dist/templates/fullstack/tests/README.md +50 -1
  90. package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
  91. package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
  92. package/dist/templates/fullstack/tests/helpers/seeded-admin.ts +15 -0
  93. package/dist/templates/fullstack/tests/package.json +3 -3
  94. package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
  95. package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
  96. package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
  97. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  98. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  99. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  100. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  101. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  102. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  103. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  104. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +53 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  106. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  107. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
  108. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  109. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +1 -1
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  124. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  125. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  126. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  127. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  128. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  129. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  130. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  131. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  132. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  133. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  134. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
  135. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  136. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  137. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
  138. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  139. package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +13 -4
  140. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  141. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  142. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +14 -6
  143. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  144. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
  145. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  146. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  147. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  148. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  149. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  150. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  151. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  152. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  153. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  154. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  155. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  156. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  157. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  164. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
  165. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +35 -0
  166. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  167. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  168. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  169. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  170. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
  171. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
  172. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  173. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  174. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  175. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  176. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  177. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
  178. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  179. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
  180. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  181. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  182. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  183. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  184. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  185. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  186. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  187. package/dist/templates/modules/auth/module.manifest.json +8 -2
  188. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +107 -1
  189. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -9
  190. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  191. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  192. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  193. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  194. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  195. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  196. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +174 -1
  197. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -3
  198. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  199. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  200. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  201. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  202. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +3 -6
  203. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  204. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +3 -6
  205. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
  206. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +3 -6
  207. package/dist/templates/todo/AGENTS.md +5 -1
  208. package/dist/templates/todo/README.md +6 -4
  209. package/dist/text-diff.d.ts +12 -0
  210. package/dist/text-diff.js +111 -0
  211. package/dist/update.d.ts +61 -3
  212. package/dist/update.js +152 -27
  213. package/package.json +2 -2
  214. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  215. package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
package/dist/index.js CHANGED
@@ -12,12 +12,14 @@ const remove_1 = require("./remove");
12
12
  const inspect_1 = require("./inspect");
13
13
  const update_1 = require("./update");
14
14
  const eject_1 = require("./eject");
15
+ const template_files_1 = require("./template-files");
15
16
  const dev_1 = require("./dev");
16
17
  const locale_1 = require("./locale");
17
18
  const deploy_1 = require("./deploy");
18
19
  const deploy_profile_1 = require("./deploy-profile");
19
20
  const deploy_render_1 = require("./deploy-render");
20
21
  const deploy_driver_1 = require("./deploy-driver");
22
+ const deploy_checks_1 = require("./deploy-checks");
21
23
  const deploy_compose_profile_1 = require("./deploy-compose-profile");
22
24
  const deploy_compose_render_1 = require("./deploy-compose-render");
23
25
  const deploy_compose_1 = require("./deploy-compose");
@@ -33,7 +35,7 @@ Usage:
33
35
  podo provider set <capability> <provider> [--apply]
34
36
  podo remove <module> Un-apply a module (inverse of add)
35
37
  podo status Show version, modules, file tiers, and local edits
36
- podo diff List PodoKit-managed files you have edited
38
+ podo diff [path...] List edited managed files, or diff files against the template
37
39
  podo doctor Check framework versions against supported ranges
38
40
  podo dev <action> [...] Run the shared, portless container development gateway
39
41
  podo deploy <action> Plan, apply, verify, or roll back a deployment
@@ -41,6 +43,7 @@ Usage:
41
43
  podo locale <command> Add, validate, activate, or list JSON locales
42
44
  podo update [--apply] Preview (or apply) what a version update would change
43
45
  podo eject <path...> Take ownership of managed files (update skips them)
46
+ podo restore <path...> Rewrite managed or ejected files from the template
44
47
 
45
48
  Options:
46
49
  --template <t> Template to scaffold (see below)
@@ -220,6 +223,26 @@ function parseArgs(argv) {
220
223
  parsed.positionals = positionals;
221
224
  return parsed;
222
225
  }
226
+ /** Explain owned files whose template changed, with the upstream diff when the previous templates are given. */
227
+ function writeOwnedTemplateNotes(plan, templatesDir, from) {
228
+ if (plan.changes.some((change) => change.action === "review")) {
229
+ if (from) {
230
+ process.stdout.write("\nTemplate changes to files you own (not applied):\n");
231
+ for (const change of (0, update_1.upstreamChanges)(process.cwd(), templatesDir, plan, from)) {
232
+ process.stdout.write(change.diff || `${change.path}: new in this version's template.\n`);
233
+ }
234
+ }
235
+ else {
236
+ process.stdout.write("\nreview: the template changed for a file you own. podo update never writes it; " +
237
+ "podo diff <path> compares it with the new template, and --from <previous templates> shows what changed upstream.\n");
238
+ }
239
+ }
240
+ if (plan.ownedWithoutTemplate.length) {
241
+ process.stdout.write(`\n${plan.ownedWithoutTemplate.length} owned file(s) differ from this version's templates, and the lock does not record ` +
242
+ "the template they came from. podo diff <path> shows the difference; applying this update records the template " +
243
+ "so the next update reports only real template changes.\n");
244
+ }
245
+ }
223
246
  function writeApiPaths(heading, result, verb) {
224
247
  const lines = (0, module_api_paths_1.describeApiPaths)(result, verb);
225
248
  if (lines.length)
@@ -258,7 +281,7 @@ async function runComposeDeploy(action, args, profileName) {
258
281
  const findings = (0, deploy_compose_1.doctorComposeDeployment)(cwd, profileName, undefined, args.release);
259
282
  process.stdout.write(args.json
260
283
  ? `${JSON.stringify(findings, null, 2)}\n`
261
- : `${findings.map((f) => `${f.ok ? "ok " : "FAIL"} ${f.message}`).join("\n")}\n`);
284
+ : `${findings.map(deploy_checks_1.formatFinding).join("\n")}\n`);
262
285
  if (findings.some((finding) => !finding.ok))
263
286
  process.exitCode = 1;
264
287
  return;
@@ -461,7 +484,33 @@ async function main(argv) {
461
484
  return;
462
485
  }
463
486
  if (args.command === "diff") {
487
+ const targets = args.positionals.slice(1);
464
488
  try {
489
+ if (targets.length) {
490
+ const result = (0, template_files_1.templateDiff)(process.cwd(), (0, node_path_1.join)(__dirname, "templates"), targets);
491
+ if (result.projectVersion !== result.templateVersion) {
492
+ process.stdout.write(`Comparing with PodoKit ${result.templateVersion} templates; this project is on ${result.projectVersion}.\n`);
493
+ }
494
+ for (const file of result.files) {
495
+ const tier = file.tier ? ` (${file.tier})` : "";
496
+ if (file.status === "not-generated") {
497
+ process.stdout.write(`${file.path}: PodoKit does not generate this file.\n`);
498
+ }
499
+ else if (file.status === "missing") {
500
+ process.stdout.write(`${file.path}${tier}: missing; podo restore ${file.path} writes the template's version.\n`);
501
+ }
502
+ else if (file.status === "same") {
503
+ process.stdout.write(`${file.path}${tier}: same as the template.\n`);
504
+ }
505
+ else if (file.binary) {
506
+ process.stdout.write(`${file.path}${tier}: binary file differs from the template.\n`);
507
+ }
508
+ else {
509
+ process.stdout.write(file.diff);
510
+ }
511
+ }
512
+ return;
513
+ }
465
514
  const { drifted, missing } = (0, inspect_1.diff)(process.cwd());
466
515
  if (!drifted.length && !missing.length) {
467
516
  process.stdout.write("No local edits to PodoKit-managed files.\n");
@@ -539,7 +588,7 @@ async function main(argv) {
539
588
  const findings = (0, deploy_1.doctorDeployment)(process.cwd(), profileName);
540
589
  process.stdout.write(args.json
541
590
  ? `${JSON.stringify(findings, null, 2)}\n`
542
- : `${findings.map((finding) => `${finding.ok ? "ok " : "FAIL"} ${finding.message}`).join("\n")}\n`);
591
+ : `${findings.map(deploy_checks_1.formatFinding).join("\n")}\n`);
543
592
  if (findings.some((finding) => !finding.ok))
544
593
  process.exitCode = 1;
545
594
  return;
@@ -561,7 +610,9 @@ async function main(argv) {
561
610
  const plan = (0, deploy_1.planDeployment)(process.cwd(), profileName, args.release);
562
611
  process.stdout.write(args.json
563
612
  ? `${JSON.stringify(plan, null, 2)}\n`
564
- : `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\nPlan hash: ${plan.planHash}\n`);
613
+ : `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\n` +
614
+ `${plan.warnings.map((warning) => `warning: ${warning}`).join("\n")}\n` +
615
+ `Plan hash: ${plan.planHash}\n`);
565
616
  return;
566
617
  }
567
618
  if (action === "apply") {
@@ -672,12 +723,23 @@ async function main(argv) {
672
723
  result.conflicts.map((f) => ` ${f}`).join("\n") +
673
724
  "\n");
674
725
  }
726
+ if (result.review.length) {
727
+ process.stdout.write("\nThe template changed for these files you own; they were left as they are. " +
728
+ "Compare each with podo diff <path>:\n" +
729
+ result.review.map((f) => ` ${f}`).join("\n") +
730
+ "\n");
731
+ }
675
732
  return;
676
733
  }
677
734
  const plan = (0, update_1.planUpdate)(process.cwd(), templatesDir);
678
735
  const counts = (0, update_1.summarize)(plan);
679
736
  const shown = plan.changes.filter((c) => c.action !== "up-to-date" && c.action !== "skip");
680
737
  process.stdout.write(`podo update ${plan.fromVersion} -> ${plan.toVersion} (template: ${plan.template}; modules: ${plan.modules.join(", ") || "none"})\n\n`);
738
+ if (plan.moduleDowngrades.length) {
739
+ process.stdout.write("WARN older external modules are installed than this project applied:\n" +
740
+ `${(0, update_1.describeDowngrades)(plan.moduleDowngrades)}\n` +
741
+ "--apply refuses until you install the recorded versions; the plan below would downgrade them.\n\n");
742
+ }
681
743
  if (plan.addedModules.length) {
682
744
  process.stdout.write("Modules to add:\n" +
683
745
  plan.addedModules.map((module) => ` ${module.name} (${(0, update_1.addedModuleReason)(module)})`).join("\n") +
@@ -696,9 +758,10 @@ async function main(argv) {
696
758
  process.stdout.write(` ${c.action.padEnd(9)} ${path} (${c.note})\n`);
697
759
  }
698
760
  process.stdout.write(`${shown.length ? "\n" : ""}${counts.update} update, ${counts.add} add, ${counts.move} move, ` +
699
- `${counts.remove} remove, ${counts.conflict} conflict. ` +
761
+ `${counts.remove} remove, ${counts.conflict} conflict, ${counts.review} review. ` +
700
762
  `Dry-run — nothing was written. Re-run with --apply to write (use --from <dir> for a 3-way merge).\n`);
701
763
  }
764
+ writeOwnedTemplateNotes(plan, templatesDir, args.from);
702
765
  }
703
766
  catch (err) {
704
767
  fail(err.message);
@@ -727,6 +790,23 @@ async function main(argv) {
727
790
  }
728
791
  return;
729
792
  }
793
+ if (args.command === "restore") {
794
+ const targets = args.positionals.slice(1);
795
+ if (!targets.length) {
796
+ fail("Usage: podo restore <path...>");
797
+ }
798
+ try {
799
+ const result = (0, template_files_1.restore)(process.cwd(), (0, node_path_1.join)(__dirname, "templates"), targets);
800
+ process.stdout.write(`Restored from the template: ${result.restored.join(", ")}\n`);
801
+ if (result.remanaged.length) {
802
+ process.stdout.write(`Managed by PodoKit again: ${result.remanaged.join(", ")}\n`);
803
+ }
804
+ }
805
+ catch (err) {
806
+ fail(err.message);
807
+ }
808
+ return;
809
+ }
730
810
  if (args.command !== "create") {
731
811
  fail(`Unknown command "${args.command}". Run "podo --help".`);
732
812
  }
package/dist/lib.d.ts CHANGED
@@ -11,6 +11,8 @@ export { describeApiPaths } from "./module-api-paths";
11
11
  export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
12
12
  export type { StatusReport, DoctorFinding } from "./inspect";
13
13
  export { addedModuleReason, planUpdate, summarize } from "./update";
14
+ export { restore, templateDiff } from "./template-files";
15
+ export type { RestoreResult, TemplateDiffResult, TemplateFileDiff } from "./template-files";
14
16
  export type { AddedModule, UpdatePlan, FileChange } from "./update";
15
17
  export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
16
18
  export type { CommandRunner, DevConfig, DevRuntime } from "./dev";
package/dist/lib.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
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;
3
+ 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.templateDiff = exports.restore = 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 = exports.applyComposeDeployment = exports.renderComposeDeployment = 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
@@ -34,6 +34,9 @@ var update_1 = require("./update");
34
34
  Object.defineProperty(exports, "addedModuleReason", { enumerable: true, get: function () { return update_1.addedModuleReason; } });
35
35
  Object.defineProperty(exports, "planUpdate", { enumerable: true, get: function () { return update_1.planUpdate; } });
36
36
  Object.defineProperty(exports, "summarize", { enumerable: true, get: function () { return update_1.summarize; } });
37
+ var template_files_1 = require("./template-files");
38
+ Object.defineProperty(exports, "restore", { enumerable: true, get: function () { return template_files_1.restore; } });
39
+ Object.defineProperty(exports, "templateDiff", { enumerable: true, get: function () { return template_files_1.templateDiff; } });
37
40
  var dev_1 = require("./dev");
38
41
  Object.defineProperty(exports, "readDevConfig", { enumerable: true, get: function () { return dev_1.readDevConfig; } });
39
42
  Object.defineProperty(exports, "renderRoute", { enumerable: true, get: function () { return dev_1.renderRoute; } });
@@ -51,6 +51,12 @@ export interface FileEntry {
51
51
  tier: Tier;
52
52
  /** Hash of the exact bytes PodoKit last wrote; user-edit detector. */
53
53
  outHash: string;
54
+ /**
55
+ * Owned files only: hash of what the template generated for the path when
56
+ * PodoKit last rendered it, so `podo update` can tell the application that
57
+ * the template changed without touching the file.
58
+ */
59
+ templateHash?: string;
54
60
  }
55
61
  export interface FilesLock {
56
62
  schemaVersion: number;
@@ -68,6 +74,12 @@ export interface RecordModulesLockSnapshot {
68
74
  }
69
75
  /** Default user-owned globs seeded into a new project. See ADR-0010. */
70
76
  export declare const DEFAULT_OWNED_GLOBS: string[];
77
+ /**
78
+ * PodoKit's own agent skills stay managed inside the owned `.claude/**` and
79
+ * `.agents/**` trees, so updates keep their commands and APIs current. Skills an
80
+ * application adds under other names stay owned.
81
+ */
82
+ export declare const DEFAULT_MANAGED_OVERRIDES: string[];
71
83
  export declare function manifestPath(projectRoot: string): string;
72
84
  export declare function filesLockPath(projectRoot: string): string;
73
85
  export declare function readManifest(projectRoot: string): PodokitManifest | null;
@@ -93,6 +105,18 @@ export declare function isLocalEnvFile(relPath: string): boolean;
93
105
  export declare function walkFiles(projectRoot: string, prefix?: string): string[];
94
106
  /** Recompute the files.lock by hashing and classifying every project file. */
95
107
  export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[], managedOverrides?: string[]): FilesLock;
108
+ /**
109
+ * The template content an earlier lock knew for a path: the recorded template
110
+ * hash of an owned file, or the generated baseline of a managed one.
111
+ */
112
+ export declare function recordedTemplateHash(entry: FileEntry | undefined): string | undefined;
113
+ /**
114
+ * Carry the template hashes of an earlier lock into a recomputed one for files
115
+ * that are owned now. Paths in `changed` were just rewritten from a different
116
+ * template, such as the files a module added to or removed from, so their old
117
+ * hash would be wrong and is dropped.
118
+ */
119
+ export declare function keepTemplateHashes(next: FilesLock, previous: FilesLock | null, changed?: Iterable<string>): FilesLock;
96
120
  export declare function readFilesLock(projectRoot: string): FilesLock | null;
97
121
  export interface Drift {
98
122
  /** Managed/assembled files whose content differs from what PodoKit wrote. */
package/dist/lockfile.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_OWNED_GLOBS = exports.LOCK_SCHEMA_VERSION = void 0;
3
+ exports.DEFAULT_MANAGED_OVERRIDES = exports.DEFAULT_OWNED_GLOBS = exports.LOCK_SCHEMA_VERSION = void 0;
4
4
  exports.manifestPath = manifestPath;
5
5
  exports.filesLockPath = filesLockPath;
6
6
  exports.readManifest = readManifest;
@@ -14,6 +14,8 @@ exports.classifyTier = classifyTier;
14
14
  exports.isLocalEnvFile = isLocalEnvFile;
15
15
  exports.walkFiles = walkFiles;
16
16
  exports.computeFilesLock = computeFilesLock;
17
+ exports.recordedTemplateHash = recordedTemplateHash;
18
+ exports.keepTemplateHashes = keepTemplateHashes;
17
19
  exports.readFilesLock = readFilesLock;
18
20
  exports.computeDrift = computeDrift;
19
21
  exports.initLockfile = initLockfile;
@@ -55,11 +57,23 @@ exports.DEFAULT_OWNED_GLOBS = [
55
57
  "apps/web/src/lib/i18n/catalogs/app/**",
56
58
  // Owned Elysia extension slot — register application services and plugins here.
57
59
  "apps/api/src/app.extensions.ts",
60
+ // Owned application environment variables, validated with PodoKit's.
61
+ "apps/api/src/config/env.extensions.ts",
62
+ // Owned TypeORM entities the migration data source loads.
63
+ "apps/api/src/database/entities.ts",
64
+ // Owned application operations that `bun run contract` requires.
65
+ "apps/api/src/core/api-contract.extensions.ts",
66
+ // Owned entry points the API build bundles next to PodoKit's.
67
+ "apps/api/scripts/build.extensions.mjs",
58
68
  // Owned additions to the managed /api proxy's header lists, body limits, and
59
69
  // response deadlines.
60
70
  "apps/web/src/lib/server/proxy-headers.ts",
61
71
  "apps/web/src/lib/server/body-limits.ts",
62
72
  "apps/web/src/lib/server/proxy-timeouts.ts",
73
+ // Owned additions to the managed Vite and SvelteKit configuration.
74
+ "apps/web/vite.extensions.ts",
75
+ // Owned styles the managed app.css imports last.
76
+ "apps/web/src/app.extensions.css",
63
77
  // Owned fallback page SvelteKit serves when the root layout or a hook fails.
64
78
  "apps/web/src/error.html",
65
79
  // AI agent guidance — yours to customize; update never touches it.
@@ -81,6 +95,27 @@ exports.DEFAULT_OWNED_GLOBS = [
81
95
  ".podokit/dev.json",
82
96
  // Optional application-owned Playwright device/browser projects.
83
97
  "tests/playwright.projects.cjs",
98
+ // Application screens the managed e2e specs visit.
99
+ "tests/helpers/app-paths.ts",
100
+ // Project documents, ignore rules, environment examples, and example
101
+ // manifests every application rewrites. PodoKit seeds them once, `podo add`
102
+ // still appends module environment lines and wiring, and `podo update`
103
+ // reports their template changes for review instead of merging them.
104
+ "README.md",
105
+ "tests/README.md",
106
+ ".gitignore",
107
+ ".env.example",
108
+ ".env.docker.example",
109
+ "infra/k3s/**",
110
+ ];
111
+ /**
112
+ * PodoKit's own agent skills stay managed inside the owned `.claude/**` and
113
+ * `.agents/**` trees, so updates keep their commands and APIs current. Skills an
114
+ * application adds under other names stay owned.
115
+ */
116
+ exports.DEFAULT_MANAGED_OVERRIDES = [
117
+ ".claude/skills/podokit-*/**",
118
+ ".agents/skills/podokit-*/**",
84
119
  ];
85
120
  const TEXT_EXT = /\.(json|md|ts|tsx|js|mjs|cjs|css|html|yml|yaml|txt|env|svelte|gitignore|example)$/i;
86
121
  function podokitDir(projectRoot) {
@@ -209,6 +244,34 @@ function computeFilesLock(projectRoot, ownedGlobs, managedOverrides = []) {
209
244
  }
210
245
  return { schemaVersion: exports.LOCK_SCHEMA_VERSION, files };
211
246
  }
247
+ /**
248
+ * The template content an earlier lock knew for a path: the recorded template
249
+ * hash of an owned file, or the generated baseline of a managed one.
250
+ */
251
+ function recordedTemplateHash(entry) {
252
+ if (!entry)
253
+ return undefined;
254
+ return entry.tier === "owned" ? entry.templateHash : entry.outHash;
255
+ }
256
+ /**
257
+ * Carry the template hashes of an earlier lock into a recomputed one for files
258
+ * that are owned now. Paths in `changed` were just rewritten from a different
259
+ * template, such as the files a module added to or removed from, so their old
260
+ * hash would be wrong and is dropped.
261
+ */
262
+ function keepTemplateHashes(next, previous, changed = []) {
263
+ if (!previous)
264
+ return next;
265
+ const skip = new Set(changed);
266
+ for (const [path, entry] of Object.entries(next.files)) {
267
+ if (entry.tier !== "owned" || entry.templateHash || skip.has(path))
268
+ continue;
269
+ const templateHash = recordedTemplateHash(previous.files[path]);
270
+ if (templateHash)
271
+ entry.templateHash = templateHash;
272
+ }
273
+ return next;
274
+ }
212
275
  function readFilesLock(projectRoot) {
213
276
  const file = filesLockPath(projectRoot);
214
277
  if (!(0, node_fs_1.existsSync)(file))
@@ -246,6 +309,7 @@ function computeDrift(projectRoot) {
246
309
  /** Write the initial `.podokit/` lockfile for a freshly created project. */
247
310
  function initLockfile(projectRoot, options) {
248
311
  const ownedGlobs = options.ownedGlobs ?? exports.DEFAULT_OWNED_GLOBS;
312
+ const managedOverrides = [...exports.DEFAULT_MANAGED_OVERRIDES];
249
313
  const providers = (0, podokit_runtime_1.resolveProviderSelections)(options.providers ?? podokit_runtime_1.SERVER_PROVIDER_SELECTIONS);
250
314
  const manifest = {
251
315
  schemaVersion: exports.LOCK_SCHEMA_VERSION,
@@ -256,10 +320,16 @@ function initLockfile(projectRoot, options) {
256
320
  providers,
257
321
  modules: [],
258
322
  ownedGlobs,
259
- managedOverrides: [],
323
+ managedOverrides,
260
324
  };
261
325
  writeManifest(projectRoot, manifest);
262
- writeFilesLock(projectRoot, computeFilesLock(projectRoot, ownedGlobs));
326
+ const lock = computeFilesLock(projectRoot, ownedGlobs, managedOverrides);
327
+ // Create wrote every file straight from the template.
328
+ for (const entry of Object.values(lock.files)) {
329
+ if (entry.tier === "owned")
330
+ entry.templateHash = entry.outHash;
331
+ }
332
+ writeFilesLock(projectRoot, lock);
263
333
  }
264
334
  /** Union two glob lists, preserving order and dropping duplicates. Used to fold
265
335
  * module-declared and ejected owned paths into the project's `ownedGlobs`, which
@@ -359,5 +429,5 @@ insertBefore) {
359
429
  continue;
360
430
  files[path] = current;
361
431
  }
362
- writeFilesLock(projectRoot, { schemaVersion: exports.LOCK_SCHEMA_VERSION, files });
432
+ writeFilesLock(projectRoot, keepTemplateHashes({ schemaVersion: exports.LOCK_SCHEMA_VERSION, files }, lockSnapshot.previous, modulePaths));
363
433
  }
package/dist/provider.js CHANGED
@@ -29,16 +29,20 @@ function assertProvider(capability, provider) {
29
29
  }
30
30
  /** The managed provider source; it must render byte for byte like the template's copy. */
31
31
  function providerSource(providers) {
32
- return `import type { ProviderSelections } from "@podosoft/podokit-runtime";\n\n` +
33
- `// Typed with every provider name, not the selected ones: the module of a\n` +
32
+ return `import type { ProviderSelections } from "@podosoft/podokit-runtime";\n` +
33
+ `import { providersFromEnvironment } from "./provider-selection";\n\n` +
34
+ `// The providers selected when the project was generated or by \`podo provider\n` +
35
+ `// set\`. Typed with every provider name, not the selected ones: the module of a\n` +
34
36
  `// provider that stays installed after a switch compares against its own name.\n` +
35
- `export const PROVIDERS: Readonly<ProviderSelections> = {\n` +
37
+ `// A <CAPABILITY>_PROVIDER variable replaces one at startup; see\n` +
38
+ `// provider-selection.ts.\n` +
39
+ `export const PROVIDERS: Readonly<ProviderSelections> = providersFromEnvironment({\n` +
36
40
  ` database: "${providers.database}",\n` +
37
41
  ` cache: "${providers.cache}",\n` +
38
42
  ` "object-storage": "${providers["object-storage"]}",\n` +
39
43
  ` events: "${providers.events}",\n` +
40
44
  ` jobs: "${providers.jobs}",\n` +
41
- `};\n`;
45
+ `});\n`;
42
46
  }
43
47
  function listProviders(projectRoot) {
44
48
  const manifest = requireManifest(projectRoot);
@@ -122,6 +126,7 @@ function applyProviderChange(projectRoot, capabilityInput, provider, options = {
122
126
  [manifestFile, (0, node_fs_1.readFileSync)(manifestFile, "utf8")],
123
127
  [lockFile, (0, node_fs_1.existsSync)(lockFile) ? (0, node_fs_1.readFileSync)(lockFile, "utf8") : null],
124
128
  ]);
129
+ const previousLock = (0, lockfile_1.readFilesLock)(projectRoot);
125
130
  const temporary = `${sourcePath}.podokit-provider-${process.pid}`;
126
131
  try {
127
132
  manifest.providers[plan.capability] = plan.to;
@@ -129,7 +134,7 @@ function applyProviderChange(projectRoot, capabilityInput, provider, options = {
129
134
  (0, node_fs_1.writeFileSync)(temporary, providerSource(manifest.providers), { mode: 0o600 });
130
135
  (0, node_fs_1.renameSync)(temporary, sourcePath);
131
136
  (0, lockfile_1.writeManifest)(projectRoot, manifest);
132
- (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides ?? []));
137
+ (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.keepTemplateHashes)((0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides ?? []), previousLock));
133
138
  }
134
139
  catch (error) {
135
140
  (0, node_fs_1.rmSync)(temporary, { force: true });
package/dist/remove.js CHANGED
@@ -135,7 +135,7 @@ function removeModule(options) {
135
135
  sharedOwnedGlobs.has(glob) ||
136
136
  keptEdited.some((path) => (0, lockfile_1.matchGlob)(path, glob)));
137
137
  (0, lockfile_1.writeManifest)(projectRoot, manifest);
138
- (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides));
138
+ (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.keepTemplateHashes)((0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides), lock, [...unwired, ".env.example"]));
139
139
  return { module, removed, keptEdited, keptShared, unwired, apiPaths };
140
140
  }
141
141
  /** Remove now-empty directories left behind by a deleted file, up to the root. */
@@ -0,0 +1,42 @@
1
+ import { type Tier } from "./lockfile";
2
+ /**
3
+ * `podo diff <path>` and `podo restore <path>`: compare a working file with
4
+ * what the installed CLI's templates generate for it, and put a managed or
5
+ * ejected file back to that content.
6
+ */
7
+ export type TemplateFileStatus = "same" | "differs" | "missing" | "not-generated";
8
+ export interface TemplateFileDiff {
9
+ path: string;
10
+ /** Ownership tier the lock records, or null for a file the lock does not track. */
11
+ tier: Tier | null;
12
+ status: TemplateFileStatus;
13
+ /** Unified diff from the template to the working file; empty unless it differs. */
14
+ diff: string;
15
+ /** Whether either side is binary, so no line diff is shown. */
16
+ binary: boolean;
17
+ }
18
+ export interface TemplateDiffResult {
19
+ /** PodoKit version the project was generated or last updated with. */
20
+ projectVersion: string;
21
+ /** Version of the CLI whose templates were rendered. */
22
+ templateVersion: string;
23
+ files: TemplateFileDiff[];
24
+ }
25
+ /** Normalize a user-supplied path to the lock's POSIX project-relative form. */
26
+ export declare function projectPath(path: string): string;
27
+ export declare function templateDiff(projectRoot: string, templatesDir: string, paths: string[]): TemplateDiffResult;
28
+ export interface RestoreResult {
29
+ /** Files rewritten from the template. */
30
+ restored: string[];
31
+ /** Files that were ejected and are managed again. */
32
+ remanaged: string[];
33
+ }
34
+ /**
35
+ * Rewrite managed or assembled files with the template's content and record
36
+ * that content as the lock baseline. A file that `podo eject` made owned is
37
+ * handed back to PodoKit: its exact path leaves the manifest's owned paths, so
38
+ * future updates manage it again. Files owned because a template or module
39
+ * declares them owned, such as routes, are refused. Nothing is written unless
40
+ * every path can be restored.
41
+ */
42
+ export declare function restore(projectRoot: string, templatesDir: string, paths: string[]): RestoreResult;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectPath = projectPath;
4
+ exports.templateDiff = templateDiff;
5
+ exports.restore = restore;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
9
+ const lockfile_1 = require("./lockfile");
10
+ const update_1 = require("./update");
11
+ const text_diff_1 = require("./text-diff");
12
+ /** Normalize a user-supplied path to the lock's POSIX project-relative form. */
13
+ function projectPath(path) {
14
+ const normalized = (0, node_path_1.normalize)(path).split(node_path_1.sep).join("/").replace(/^\.\//, "");
15
+ if (normalized.startsWith("../") || normalized === ".." || normalized.startsWith("/")) {
16
+ throw new Error(`${path} is outside the project.`);
17
+ }
18
+ return normalized;
19
+ }
20
+ function isUtf8Text(content) {
21
+ return !content.includes(0) && Buffer.from(content.toString("utf8"), "utf8").equals(content);
22
+ }
23
+ function fileBuffer(file) {
24
+ return typeof file.content === "string" ? Buffer.from(file.content, "utf8") : file.content;
25
+ }
26
+ function templateDiff(projectRoot, templatesDir, paths) {
27
+ const target = (0, update_1.targetProject)(projectRoot, templatesDir);
28
+ const templateVersion = (0, lockfile_1.podokitVersion)();
29
+ const files = paths.map((input) => {
30
+ const path = projectPath(input);
31
+ const tier = target.lock.files[path]?.tier ?? null;
32
+ const generated = target.tree.get(path);
33
+ const abs = (0, node_path_1.join)(projectRoot, path);
34
+ if (!generated) {
35
+ return { path, tier, status: "not-generated", diff: "", binary: false };
36
+ }
37
+ if (!(0, node_fs_1.existsSync)(abs))
38
+ return { path, tier, status: "missing", diff: "", binary: false };
39
+ const templateBytes = fileBuffer(generated);
40
+ const diskBytes = (0, node_fs_1.readFileSync)(abs);
41
+ if (templateBytes.equals(diskBytes)) {
42
+ return { path, tier, status: "same", diff: "", binary: false };
43
+ }
44
+ const binary = !isUtf8Text(templateBytes) || !isUtf8Text(diskBytes);
45
+ return {
46
+ path,
47
+ tier,
48
+ status: "differs",
49
+ binary,
50
+ diff: binary
51
+ ? ""
52
+ : (0, text_diff_1.unifiedDiff)(templateBytes.toString("utf8"), diskBytes.toString("utf8"), {
53
+ fromLabel: `podokit-${templateVersion}/${path}`,
54
+ toLabel: path,
55
+ }),
56
+ };
57
+ });
58
+ return { projectVersion: target.manifest.podokitVersion, templateVersion, files };
59
+ }
60
+ /**
61
+ * Rewrite managed or assembled files with the template's content and record
62
+ * that content as the lock baseline. A file that `podo eject` made owned is
63
+ * handed back to PodoKit: its exact path leaves the manifest's owned paths, so
64
+ * future updates manage it again. Files owned because a template or module
65
+ * declares them owned, such as routes, are refused. Nothing is written unless
66
+ * every path can be restored.
67
+ */
68
+ function restore(projectRoot, templatesDir, paths) {
69
+ const target = (0, update_1.targetProject)(projectRoot, templatesDir);
70
+ const cliVersion = (0, lockfile_1.podokitVersion)();
71
+ if (target.manifest.podokitVersion !== cliVersion) {
72
+ throw new Error(`This project is on PodoKit ${target.manifest.podokitVersion} and this CLI renders ${cliVersion}. ` +
73
+ "Run podo update --apply first, or restore with the CLI version the project is on.");
74
+ }
75
+ const declared = new Set((0, update_1.declaredOwnedGlobs)(projectRoot, templatesDir, target.modules));
76
+ const requested = [...new Set(paths.map(projectPath))];
77
+ const ejected = requested.filter((path) => target.manifest.ownedGlobs.includes(path) && !declared.has(path));
78
+ const ownedGlobs = target.manifest.ownedGlobs.filter((glob) => !ejected.includes(glob));
79
+ const classifyGlobs = [...new Set([...declared, ...ownedGlobs])];
80
+ const writes = [];
81
+ for (const path of requested) {
82
+ const file = target.tree.get(path);
83
+ if (!file)
84
+ throw new Error(`PodoKit does not generate ${path}, so there is nothing to restore.`);
85
+ const tier = (0, lockfile_1.classifyTier)(path, fileBuffer(file), classifyGlobs, target.managedOverrides);
86
+ if (tier === "owned") {
87
+ throw new Error(`${path} is owned by the application through the template or a module, so PodoKit never manages it. ` +
88
+ `Run podo diff ${path} to see the template's version.`);
89
+ }
90
+ writes.push({ path, file, tier });
91
+ }
92
+ for (const { path, file, tier } of writes) {
93
+ const abs = (0, node_path_1.join)(projectRoot, path);
94
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(abs), { recursive: true });
95
+ (0, node_fs_1.writeFileSync)(abs, file.content);
96
+ target.lock.files[path] = { tier, outHash: (0, podokit_template_engine_1.hashContent)(fileBuffer(file)) };
97
+ }
98
+ (0, lockfile_1.writeFilesLock)(projectRoot, target.lock);
99
+ if (ejected.length)
100
+ (0, lockfile_1.writeManifest)(projectRoot, { ...target.manifest, ownedGlobs });
101
+ return { restored: writes.map((write) => write.path), remanaged: ejected };
102
+ }