@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
@@ -1,14 +1,9 @@
1
1
  import { expect, test, type APIRequestContext, type PlaywrightWorkerArgs } from "@playwright/test";
2
+ import { seededAdmin } from "../helpers/seeded-admin";
2
3
 
3
4
  const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
4
5
  const secondaryApi = process.env.E2E_SECONDARY_API_URL;
5
6
  const origin = { origin: base };
6
- // The administrator seeded by the admin-dashboard Playwright setup. Inlined so this
7
- // spec also compiles in apps that do not install that module.
8
- const admin = {
9
- email: "admin@example.com",
10
- password: process.env.E2E_ADMIN_PASSWORD || "Podokit3e-Str0ng!pw",
11
- };
12
7
 
13
8
  type Playwright = PlaywrightWorkerArgs["playwright"];
14
9
 
@@ -39,6 +34,8 @@ async function signUp(ctx: APIRequestContext): Promise<SessionUser | undefined>
39
34
 
40
35
  /** Resolves false when no administrator session is available. */
41
36
  async function signInAdmin(ctx: APIRequestContext): Promise<boolean> {
37
+ const admin = await seededAdmin();
38
+ if (!admin) return false;
42
39
  const response = await ctx.post("/api/auth/sign-in/email", { data: admin });
43
40
  return response.ok() && ((await sessionUser(ctx))?.roles ?? []).includes("admin");
44
41
  }
@@ -87,7 +87,11 @@ Merge added module settings from `.env.example` into an existing `.env`.
87
87
  Better Auth schema before the application migrations. `{{apiRun}} migration:run` runs
88
88
  only the application migrations and never creates Better Auth tables;
89
89
  `{{apiRun}} migration:revert` reverts the latest application migration.
90
- - e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`.
90
+ - e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`. Run the whole suite on more
91
+ workers with `E2E_WORKERS=2`, never `--workers`. Tag a test `{ tag: "@exclusive" }` when
92
+ it changes state other tests rely on (a server-wide setting, a seeded account's profile);
93
+ tagged tests run alone after the rest. Give each test its own mail and SMS recipients and
94
+ never empty the shared Mailpit inbox or SMS sink. See `tests/README.md`.
91
95
  - Playwright officially runs on Node, so `bunx playwright` respects its Node
92
96
  shebang. Node LTS is a test-tool dependency only; API, web, worker, migrations,
93
97
  builds, and unit tests run on Bun.
@@ -55,8 +55,9 @@ cp .env.example .env
55
55
  ```
56
56
 
57
57
  Open the URL printed by the command; a new project defaults to
58
- **http://{{projectName}}.localhost**. The committed `.podokit/dev.json` is the
59
- source of truth if you change the hostname. The first running PodoKit project starts
58
+ **http://{{devHostname}}**. The committed `.podokit/dev.json` is the
59
+ source of truth if you change the hostname: `podo dev` sets `CORS_ORIGIN` and
60
+ `BETTER_AUTH_URL` for the web and API containers from it. The first running PodoKit project starts
60
61
  one user-level Traefik gateway on `127.0.0.1:80`; additional projects reuse it and
61
62
  route by that hostname. Even a single app uses
62
63
  the same topology, with one route and no project-specific host port.
@@ -164,5 +165,6 @@ PodoKit [deployment guide](https://github.com/podosoft-dev/podokit/blob/main/doc
164
165
  For a Docker Compose deployment with API WebSockets, list their exact paths under
165
166
  `exposure.webSocketPaths`. The driver keeps one published port, routes those paths
166
167
  to API, and sends every other request to web. Large-upload routes listed under
167
- `exposure.apiPaths` reach the API the same way, with `/api` removed. If an external reverse proxy supplies
168
- forwarding headers, list only its direct CIDR blocks in `exposure.trustedProxyCidrs`.
168
+ `exposure.apiPaths` reach the API the same way, with `/api` removed. Set `exposure.edgeProxies` to the
169
+ number of reverse proxies in front of the deployment, and with the gateway list only their direct CIDR
170
+ blocks in `exposure.trustedProxyCidrs`.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Line-based unified diff for `podo diff <path>`. It is dependency-free and
3
+ * meant for source files of a few thousand lines; above `MAX_CELLS` it reports
4
+ * the whole file as replaced instead of building the LCS table.
5
+ */
6
+ export interface UnifiedDiffOptions {
7
+ fromLabel: string;
8
+ toLabel: string;
9
+ context?: number;
10
+ }
11
+ /** Unified diff from `from` to `to`, or an empty string when they are equal. */
12
+ export declare function unifiedDiff(from: string, to: string, options: UnifiedDiffOptions): string;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Line-based unified diff for `podo diff <path>`. It is dependency-free and
4
+ * meant for source files of a few thousand lines; above `MAX_CELLS` it reports
5
+ * the whole file as replaced instead of building the LCS table.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.unifiedDiff = unifiedDiff;
9
+ const MAX_CELLS = 25_000_000;
10
+ function splitLines(text) {
11
+ if (text === "")
12
+ return [];
13
+ const lines = text.split("\n");
14
+ if (lines[lines.length - 1] === "")
15
+ lines.pop();
16
+ return lines;
17
+ }
18
+ function operations(a, b) {
19
+ let start = 0;
20
+ while (start < a.length && start < b.length && a[start] === b[start])
21
+ start += 1;
22
+ let endA = a.length;
23
+ let endB = b.length;
24
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
25
+ endA -= 1;
26
+ endB -= 1;
27
+ }
28
+ const head = a.slice(0, start).map((line) => ({ kind: " ", line }));
29
+ const tail = a.slice(endA).map((line) => ({ kind: " ", line }));
30
+ const midA = a.slice(start, endA);
31
+ const midB = b.slice(start, endB);
32
+ const m = midA.length;
33
+ const n = midB.length;
34
+ const middle = [];
35
+ if ((m + 1) * (n + 1) > MAX_CELLS) {
36
+ middle.push(...midA.map((line) => ({ kind: "-", line })));
37
+ middle.push(...midB.map((line) => ({ kind: "+", line })));
38
+ return [...head, ...middle, ...tail];
39
+ }
40
+ const width = n + 1;
41
+ const table = new Uint32Array((m + 1) * width);
42
+ for (let i = m - 1; i >= 0; i -= 1) {
43
+ for (let j = n - 1; j >= 0; j -= 1) {
44
+ table[i * width + j] = midA[i] === midB[j]
45
+ ? table[(i + 1) * width + j + 1] + 1
46
+ : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
47
+ }
48
+ }
49
+ let i = 0;
50
+ let j = 0;
51
+ while (i < m || j < n) {
52
+ if (i < m && j < n && midA[i] === midB[j]) {
53
+ middle.push({ kind: " ", line: midA[i] });
54
+ i += 1;
55
+ j += 1;
56
+ }
57
+ else if (i < m && (j === n || table[(i + 1) * width + j] >= table[i * width + j + 1])) {
58
+ middle.push({ kind: "-", line: midA[i] });
59
+ i += 1;
60
+ }
61
+ else {
62
+ middle.push({ kind: "+", line: midB[j] });
63
+ j += 1;
64
+ }
65
+ }
66
+ return [...head, ...middle, ...tail];
67
+ }
68
+ function range(start, count) {
69
+ if (count === 0)
70
+ return `${start},0`;
71
+ return count === 1 ? `${start + 1}` : `${start + 1},${count}`;
72
+ }
73
+ /** Unified diff from `from` to `to`, or an empty string when they are equal. */
74
+ function unifiedDiff(from, to, options) {
75
+ if (from === to)
76
+ return "";
77
+ const context = options.context ?? 3;
78
+ const ops = operations(splitLines(from), splitLines(to));
79
+ const changed = ops.flatMap((op, index) => (op.kind === " " ? [] : [index]));
80
+ const out = [`--- ${options.fromLabel}`, `+++ ${options.toLabel}`];
81
+ if (!changed.length) {
82
+ // Only the trailing newline differs.
83
+ out.push(from.endsWith("\n") ? "\\ the template ends with a newline" : "\\ the template has no newline at end of file", to.endsWith("\n") ? "\\ the file ends with a newline" : "\\ the file has no newline at end of file");
84
+ return `${out.join("\n")}\n`;
85
+ }
86
+ let hunkStart = 0;
87
+ while (hunkStart < changed.length) {
88
+ let hunkEnd = hunkStart;
89
+ while (hunkEnd + 1 < changed.length &&
90
+ changed[hunkEnd + 1] - changed[hunkEnd] <= context * 2 + 1) {
91
+ hunkEnd += 1;
92
+ }
93
+ const first = Math.max(0, changed[hunkStart] - context);
94
+ const last = Math.min(ops.length - 1, changed[hunkEnd] + context);
95
+ let fromLine = 0;
96
+ let toLine = 0;
97
+ for (let index = 0; index < first; index += 1) {
98
+ if (ops[index].kind !== "+")
99
+ fromLine += 1;
100
+ if (ops[index].kind !== "-")
101
+ toLine += 1;
102
+ }
103
+ const body = ops.slice(first, last + 1);
104
+ const fromCount = body.filter((op) => op.kind !== "+").length;
105
+ const toCount = body.filter((op) => op.kind !== "-").length;
106
+ out.push(`@@ -${range(fromLine, fromCount)} +${range(toLine, toCount)} @@`);
107
+ out.push(...body.map((op) => `${op.kind}${op.line}`));
108
+ hunkStart = hunkEnd + 1;
109
+ }
110
+ return `${out.join("\n")}\n`;
111
+ }
package/dist/update.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type TemplateVars } from "@podosoft/podokit-template-engine";
2
- import { type Tier } from "./lockfile";
1
+ import { type TemplateVars, type VfsTree } from "@podosoft/podokit-template-engine";
2
+ import { type Tier, type FilesLock, type ManifestModule, type PodokitManifest } from "./lockfile";
3
3
  import type { ProviderCapability } from "@podosoft/podokit-runtime";
4
4
  import type { Toolchain } from "./toolchain";
5
5
  import { type ApiPathResult } from "./module-api-paths";
@@ -9,7 +9,7 @@ import { type ApiPathResult } from "./module-api-paths";
9
9
  * working copy, classifying each file. Applying the plan (writing files, 3-way
10
10
  * merging) is a later step (see ADR-0009).
11
11
  */
12
- export type Action = "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
12
+ export type Action = "review" | "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
13
13
  export interface FileChange {
14
14
  path: string;
15
15
  /** Previous path when this change is an update migration move. */
@@ -36,10 +36,38 @@ export interface UpdatePlan {
36
36
  modules: string[];
37
37
  /** Modules the update adds, ordered ahead of the modules that need them. */
38
38
  addedModules: AddedModule[];
39
+ /**
40
+ * External modules whose installed package is older than the version the
41
+ * project last applied. Applying would write the older module's files, so
42
+ * `applyUpdate` refuses while this is not empty.
43
+ */
44
+ moduleDowngrades: ModuleDowngrade[];
39
45
  changes: FileChange[];
40
46
  /** Direct API paths that modules newly declare, per configuration file. */
41
47
  apiPaths: ApiPathResult;
48
+ /**
49
+ * Owned files that differ from this version's template while the lock has no
50
+ * record of the template they came from, so PodoKit cannot tell whether the
51
+ * template changed. Applying the update records it for the next one.
52
+ */
53
+ ownedWithoutTemplate: string[];
42
54
  }
55
+ /** An external module installed at an older version than the project last applied. */
56
+ export interface ModuleDowngrade {
57
+ name: string;
58
+ packageName: string;
59
+ /** Version recorded in the manifest. */
60
+ recorded: string;
61
+ /** Version installed in node_modules. */
62
+ installed: string;
63
+ }
64
+ /** Semantic version order of two exact versions: negative when `a` is older. */
65
+ export declare function compareVersions(a: string, b: string): number;
66
+ /** Owned paths the template and the installed modules declare, without the app's own. */
67
+ export declare function declaredOwnedGlobs(projectRoot: string, templatesDir: string, modules: {
68
+ name: string;
69
+ packageName?: string;
70
+ }[]): string[];
43
71
  /**
44
72
  * Build the update plan. `templatesDir` is the installed CLI's template set (the
45
73
  * new version); the lock records what PodoKit last wrote (to detect user edits).
@@ -52,6 +80,20 @@ export interface PlanOptions {
52
80
  /** Restrict planning to selected paths, used by focused conversions. */
53
81
  onlyPaths?: string[];
54
82
  }
83
+ /** The project as the installed CLI's templates would generate it. */
84
+ export interface TargetProject {
85
+ manifest: PodokitManifest;
86
+ lock: FilesLock;
87
+ /** Recorded modules plus any the target version requires, in configure order. */
88
+ modules: ManifestModule[];
89
+ addedModules: AddedModule[];
90
+ /** External modules installed at an older version than the project applied. */
91
+ moduleDowngrades: ModuleDowngrade[];
92
+ tree: VfsTree;
93
+ ownedGlobs: string[];
94
+ managedOverrides: string[];
95
+ }
96
+ export declare function targetProject(projectRoot: string, templatesDir: string, targetAnswers?: TemplateVars): TargetProject;
55
97
  export declare function planUpdate(projectRoot: string, templatesDir: string, options?: PlanOptions): UpdatePlan;
56
98
  export interface ApplyOptions {
57
99
  /** Templates for the version the project is currently on, for 3-way merges. */
@@ -80,9 +122,23 @@ export interface ApplyResult {
80
122
  merged: string[];
81
123
  /** Files written with conflict markers for manual resolution. */
82
124
  conflicts: string[];
125
+ /** Owned files whose template changed; they are left as they are. */
126
+ review: string[];
83
127
  /** Direct API paths added to `.podokit/dev.json` and deployment profiles. */
84
128
  apiPaths: ApiPathResult;
85
129
  }
130
+ export interface UpstreamChange {
131
+ path: string;
132
+ /** Unified diff from the previous template to the new one; empty when the
133
+ * previous templates did not generate the file. */
134
+ diff: string;
135
+ }
136
+ /**
137
+ * What the template changed between the project's version and this one for
138
+ * each owned file the plan marks for review, given the previous version's
139
+ * templates.
140
+ */
141
+ export declare function upstreamChanges(projectRoot: string, templatesDir: string, plan: UpdatePlan, oldTemplatesDir: string, oldExternalModulesRoot?: string): UpstreamChange[];
86
142
  /**
87
143
  * Apply an update to the working copy. Clean updates and additions are written,
88
144
  * upstream removals deleted, and user-edited files 3-way merged against the old
@@ -91,6 +147,8 @@ export interface ApplyResult {
91
147
  * stamps the new version.
92
148
  */
93
149
  export declare function applyUpdate(projectRoot: string, templatesDir: string, options?: ApplyOptions): ApplyResult;
150
+ /** One line per external module installed at an older version than recorded. */
151
+ export declare function describeDowngrades(downgrades: ModuleDowngrade[]): string;
94
152
  /** Why an update adds a module, for the plan report. */
95
153
  export declare function addedModuleReason(module: AddedModule): string;
96
154
  /** Count actions for a one-line summary. */
package/dist/update.js CHANGED
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareVersions = compareVersions;
4
+ exports.declaredOwnedGlobs = declaredOwnedGlobs;
5
+ exports.targetProject = targetProject;
3
6
  exports.planUpdate = planUpdate;
7
+ exports.upstreamChanges = upstreamChanges;
4
8
  exports.applyUpdate = applyUpdate;
9
+ exports.describeDowngrades = describeDowngrades;
5
10
  exports.addedModuleReason = addedModuleReason;
6
11
  exports.summarize = summarize;
7
12
  const node_child_process_1 = require("node:child_process");
@@ -10,6 +15,7 @@ const node_os_1 = require("node:os");
10
15
  const node_path_1 = require("node:path");
11
16
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
12
17
  const assemble_1 = require("./assemble");
18
+ const text_diff_1 = require("./text-diff");
13
19
  const lockfile_1 = require("./lockfile");
14
20
  const inspect_1 = require("./inspect");
15
21
  const add_1 = require("./add");
@@ -25,6 +31,31 @@ function diskContent(projectRoot, path) {
25
31
  const abs = (0, node_path_1.join)(projectRoot, path);
26
32
  return (0, node_fs_1.existsSync)(abs) ? (0, node_fs_1.readFileSync)(abs) : null;
27
33
  }
34
+ function versionParts(version) {
35
+ const [withoutBuild = ""] = version.split("+");
36
+ const dash = withoutBuild.indexOf("-");
37
+ const core = (dash === -1 ? withoutBuild : withoutBuild.slice(0, dash))
38
+ .split(".")
39
+ .map((part) => Number.parseInt(part, 10) || 0);
40
+ return { core, prerelease: dash === -1 ? null : withoutBuild.slice(dash + 1) };
41
+ }
42
+ /** Semantic version order of two exact versions: negative when `a` is older. */
43
+ function compareVersions(a, b) {
44
+ const left = versionParts(a);
45
+ const right = versionParts(b);
46
+ for (let index = 0; index < Math.max(left.core.length, right.core.length); index += 1) {
47
+ const difference = (left.core[index] ?? 0) - (right.core[index] ?? 0);
48
+ if (difference !== 0)
49
+ return difference;
50
+ }
51
+ if (left.prerelease === right.prerelease)
52
+ return 0;
53
+ if (left.prerelease === null)
54
+ return 1;
55
+ if (right.prerelease === null)
56
+ return -1;
57
+ return left.prerelease.localeCompare(right.prerelease, "en", { numeric: true });
58
+ }
28
59
  /**
29
60
  * Expand the recorded module list against the target manifests. A module may
30
61
  * gain a new requirement after it was first added to a project, either another
@@ -39,6 +70,7 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
39
70
  const recorded = new Map(modules.map((module) => [module.name, module]));
40
71
  const ordered = [];
41
72
  const added = [];
73
+ const downgrades = [];
42
74
  const visiting = new Set();
43
75
  const visited = new Set();
44
76
  const visit = (name, requiredBy, capability) => {
@@ -54,6 +86,17 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
54
86
  ? `Module "${requiredBy}" requires unknown module "${name}".`
55
87
  : `Cannot resolve module "${name}" while updating the project.`);
56
88
  }
89
+ if (existing?.moduleVersion &&
90
+ resolved.packageName &&
91
+ resolved.moduleVersion &&
92
+ compareVersions(resolved.moduleVersion, existing.moduleVersion) < 0) {
93
+ downgrades.push({
94
+ name: resolved.name,
95
+ packageName: resolved.packageName,
96
+ recorded: existing.moduleVersion,
97
+ installed: resolved.moduleVersion,
98
+ });
99
+ }
57
100
  visiting.add(name);
58
101
  const moduleManifest = (0, add_1.readModuleManifest)(resolved.dir);
59
102
  for (const required of moduleManifest.requires ?? [])
@@ -89,7 +132,7 @@ function targetModules(projectRoot, templatesDir, modules, providers) {
89
132
  for (const module of [...modules].sort((a, b) => a.order - b.order)) {
90
133
  visit(module.name);
91
134
  }
92
- return { modules: ordered, added };
135
+ return { modules: ordered, added, downgrades };
93
136
  }
94
137
  function targetApiPaths(projectRoot, templatesDir, modules) {
95
138
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
@@ -104,7 +147,7 @@ function targetApiPaths(projectRoot, templatesDir, modules) {
104
147
  return { declared, pending };
105
148
  }
106
149
  function targetManagedOverrides(projectRoot, templatesDir, modules, current = []) {
107
- const overrides = new Set(current);
150
+ const overrides = new Set([...lockfile_1.DEFAULT_MANAGED_OVERRIDES, ...current]);
108
151
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
109
152
  for (const module of modules) {
110
153
  const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
@@ -127,21 +170,36 @@ function targetOwnedGlobs(projectRoot, templatesDir, modules, current) {
127
170
  }
128
171
  return [...owned];
129
172
  }
130
- function planUpdate(projectRoot, templatesDir, options = {}) {
173
+ /** Owned paths the template and the installed modules declare, without the app's own. */
174
+ function declaredOwnedGlobs(projectRoot, templatesDir, modules) {
175
+ return targetOwnedGlobs(projectRoot, templatesDir, modules, []);
176
+ }
177
+ function targetProject(projectRoot, templatesDir, targetAnswers) {
131
178
  const manifest = (0, lockfile_1.readManifest)(projectRoot);
132
179
  const lock = (0, lockfile_1.readFilesLock)(projectRoot);
133
180
  if (!manifest || !lock)
134
181
  throw new inspect_1.NotAProjectError();
135
- const { modules, added } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
136
- const newTree = (0, assemble_1.assembleProject)({
182
+ const { modules, added, downgrades } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
183
+ const tree = (0, assemble_1.assembleProject)({
137
184
  templatesDir,
138
185
  template: manifest.template,
139
- answers: options.targetAnswers ?? (0, lockfile_1.manifestTemplateVars)(manifest),
186
+ answers: targetAnswers ?? (0, lockfile_1.manifestTemplateVars)(manifest),
140
187
  modules,
141
188
  projectRoot,
142
189
  });
143
- const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides);
144
- const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs);
190
+ return {
191
+ manifest,
192
+ lock,
193
+ modules,
194
+ addedModules: added,
195
+ moduleDowngrades: downgrades,
196
+ tree,
197
+ ownedGlobs: targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs),
198
+ managedOverrides: targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides),
199
+ };
200
+ }
201
+ function planUpdate(projectRoot, templatesDir, options = {}) {
202
+ const { manifest, lock, modules, addedModules: added, moduleDowngrades, tree: newTree, ownedGlobs, managedOverrides, } = targetProject(projectRoot, templatesDir, options.targetAnswers);
145
203
  const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, modules);
146
204
  const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, lock, pendingMigrations);
147
205
  const changes = migrationPlan.moves.map((move) => ({
@@ -159,6 +217,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
159
217
  note: conflict,
160
218
  });
161
219
  }
220
+ const ownedWithoutTemplate = [];
162
221
  const movedSources = new Set(migrationPlan.moves.map((move) => move.from));
163
222
  const movedTargets = new Set(migrationPlan.moves.map((move) => move.to));
164
223
  const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
@@ -192,6 +251,24 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
192
251
  changes.push({ path, tier, action: "add", note: "new owned seed in this version" });
193
252
  continue;
194
253
  }
254
+ if (newText !== null && disk !== null) {
255
+ const newHash = (0, podokit_template_engine_1.hashContent)(newText);
256
+ const recorded = (0, lockfile_1.recordedTemplateHash)(locked);
257
+ if ((0, podokit_template_engine_1.hashContent)(disk) !== newHash) {
258
+ if (recorded && recorded !== newHash) {
259
+ changes.push({
260
+ path,
261
+ tier,
262
+ action: "review",
263
+ note: `owned; the template changed, see podo diff ${path}`,
264
+ });
265
+ continue;
266
+ }
267
+ // The lock never records files under .podokit/, so only judge tracked files.
268
+ if (!recorded && locked)
269
+ ownedWithoutTemplate.push(path);
270
+ }
271
+ }
195
272
  const explicitlyOwned = ownedGlobs.some((glob) => !glob.includes("*") && (0, lockfile_1.matchGlob)(path, glob));
196
273
  changes.push({
197
274
  path,
@@ -251,8 +328,10 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
251
328
  template: manifest.template,
252
329
  modules: modules.map((module) => module.name),
253
330
  addedModules: added,
331
+ moduleDowngrades,
254
332
  changes,
255
333
  apiPaths,
334
+ ownedWithoutTemplate,
256
335
  };
257
336
  }
258
337
  function assertRecordedExternalModules(moduleRoot, templatesDir, modules) {
@@ -297,6 +376,48 @@ function previousExternalModules(projectRoot, templatesDir, modules, providedRoo
297
376
  }
298
377
  return { root, cleanup: true };
299
378
  }
379
+ /** The project as the templates of the version it is on generated it. */
380
+ function previousTree(projectRoot, manifest, oldTemplatesDir, oldExternalModulesRoot) {
381
+ const previousModules = previousExternalModules(projectRoot, oldTemplatesDir, manifest.modules, oldExternalModulesRoot);
382
+ try {
383
+ return (0, assemble_1.assembleProject)({
384
+ templatesDir: oldTemplatesDir,
385
+ template: manifest.template,
386
+ answers: (0, lockfile_1.manifestTemplateVars)(manifest),
387
+ modules: manifest.modules,
388
+ projectRoot: previousModules.root,
389
+ });
390
+ }
391
+ finally {
392
+ if (previousModules.cleanup)
393
+ (0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
394
+ }
395
+ }
396
+ /**
397
+ * What the template changed between the project's version and this one for
398
+ * each owned file the plan marks for review, given the previous version's
399
+ * templates.
400
+ */
401
+ function upstreamChanges(projectRoot, templatesDir, plan, oldTemplatesDir, oldExternalModulesRoot) {
402
+ const review = plan.changes.filter((change) => change.action === "review");
403
+ if (!review.length)
404
+ return [];
405
+ const target = targetProject(projectRoot, templatesDir);
406
+ const oldTree = previousTree(projectRoot, target.manifest, oldTemplatesDir, oldExternalModulesRoot);
407
+ return review.map((change) => {
408
+ const before = treeText(oldTree, change.path);
409
+ const after = treeText(target.tree, change.path) ?? "";
410
+ return {
411
+ path: change.path,
412
+ diff: before === null
413
+ ? ""
414
+ : (0, text_diff_1.unifiedDiff)(before, after, {
415
+ fromLabel: `podokit-${plan.fromVersion}/${change.path}`,
416
+ toLabel: `podokit-${plan.toVersion}/${change.path}`,
417
+ }),
418
+ };
419
+ });
420
+ }
300
421
  function writeFile(projectRoot, path, content) {
301
422
  const abs = (0, node_path_1.join)(projectRoot, path);
302
423
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(abs), { recursive: true });
@@ -328,6 +449,8 @@ function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs, managedOve
328
449
  }
329
450
  entry.outHash = (0, podokit_template_engine_1.hashContent)(newText);
330
451
  }
452
+ if (entry.tier === "owned")
453
+ entry.templateHash = (0, podokit_template_engine_1.hashContent)(newText);
331
454
  continue;
332
455
  }
333
456
  if (entry.tier === "owned")
@@ -344,16 +467,19 @@ function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs, managedOve
344
467
  delete next.files[path];
345
468
  }
346
469
  }
347
- // Keep a tombstone for an owned seed that the application deleted or moved.
348
- // Without the previous entry, the next update would mistake the missing path
349
- // for a newly introduced seed and recreate it.
470
+ // Keep a tombstone for an owned seed that the application deleted or moved,
471
+ // including a file this update turns from managed into owned. Without the
472
+ // entry, the next update would mistake the missing path for a newly
473
+ // introduced seed and recreate it.
350
474
  for (const [path, entry] of Object.entries(previous.files)) {
351
- if (entry.tier !== "owned" || next.files[path])
475
+ if (next.files[path])
352
476
  continue;
353
477
  const newText = treeText(newTree, path);
354
478
  if (newText !== null &&
355
479
  (0, lockfile_1.classifyTier)(path, newText, ownedGlobs, managedOverrides) === "owned") {
356
- next.files[path] = entry;
480
+ next.files[path] = entry.tier === "owned"
481
+ ? entry
482
+ : { tier: "owned", outHash: entry.outHash, templateHash: (0, podokit_template_engine_1.hashContent)(newText) };
357
483
  }
358
484
  }
359
485
  return next;
@@ -375,6 +501,10 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
375
501
  forceManagedPaths: options.forceManagedPaths,
376
502
  onlyPaths: options.onlyPaths,
377
503
  });
504
+ if (plan.moduleDowngrades.length) {
505
+ throw new Error(`Refusing to downgrade external modules:\n${describeDowngrades(plan.moduleDowngrades)}\n` +
506
+ "Install at least the recorded version (for example bun install), then run podo update again.");
507
+ }
378
508
  const { modules } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
379
509
  const newTree = (0, assemble_1.assembleProject)({
380
510
  templatesDir,
@@ -401,20 +531,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
401
531
  const needsMergeBase = plan.changes.some((change) => change.action === "conflict");
402
532
  let oldTree = null;
403
533
  if (options.oldTemplatesDir && needsMergeBase) {
404
- const previousModules = previousExternalModules(projectRoot, options.oldTemplatesDir, manifest.modules, options.oldExternalModulesRoot);
405
- try {
406
- oldTree = (0, assemble_1.assembleProject)({
407
- templatesDir: options.oldTemplatesDir,
408
- template: manifest.template,
409
- answers: (0, lockfile_1.manifestTemplateVars)(manifest),
410
- modules: manifest.modules,
411
- projectRoot: previousModules.root,
412
- });
413
- }
414
- finally {
415
- if (previousModules.cleanup)
416
- (0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
417
- }
534
+ oldTree = previousTree(projectRoot, manifest, options.oldTemplatesDir, options.oldExternalModulesRoot);
418
535
  }
419
536
  if (options.abortOnConflict) {
420
537
  const unresolved = [];
@@ -444,6 +561,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
444
561
  removed: [],
445
562
  merged: [],
446
563
  conflicts: [],
564
+ review: plan.changes.filter((change) => change.action === "review").map((change) => change.path),
447
565
  apiPaths: { changed: [], skipped: [] },
448
566
  };
449
567
  for (const change of plan.changes) {
@@ -523,6 +641,12 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
523
641
  });
524
642
  return result;
525
643
  }
644
+ /** One line per external module installed at an older version than recorded. */
645
+ function describeDowngrades(downgrades) {
646
+ return downgrades
647
+ .map((entry) => ` ${entry.packageName} ${entry.installed} is installed; the project applied ${entry.recorded}`)
648
+ .join("\n");
649
+ }
526
650
  /** Why an update adds a module, for the plan report. */
527
651
  function addedModuleReason(module) {
528
652
  return module.capability
@@ -532,6 +656,7 @@ function addedModuleReason(module) {
532
656
  /** Count actions for a one-line summary. */
533
657
  function summarize(plan) {
534
658
  const counts = {
659
+ review: 0,
535
660
  update: 0,
536
661
  merge: 0,
537
662
  conflict: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "An opinionated Bun and Elysia full-stack TypeScript starter toolkit and CLI with SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,7 +45,7 @@
45
45
  "test": "vitest run"
46
46
  },
47
47
  "dependencies": {
48
- "@podosoft/podokit-runtime": "^0.4.1",
48
+ "@podosoft/podokit-runtime": "^0.5.1",
49
49
  "@podosoft/podokit-template-engine": "^0.4.2"
50
50
  }
51
51
  }