@podosoft/podokit 0.12.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/add.d.ts +27 -1
  2. package/dist/add.js +3 -1
  3. package/dist/index.js +6 -3
  4. package/dist/lib.d.ts +1 -1
  5. package/dist/lockfile.d.ts +4 -1
  6. package/dist/lockfile.js +4 -3
  7. package/dist/module-migrations.d.ts +25 -0
  8. package/dist/module-migrations.js +233 -0
  9. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +3 -1
  10. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
  11. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +3 -0
  12. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
  13. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +3 -1
  14. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +5 -0
  15. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +6 -0
  16. package/dist/templates/fullstack-nest-svelte/dot-env.example +2 -0
  17. package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +1 -0
  18. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image-upload.interceptor.ts +39 -0
  19. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.controller.ts +73 -0
  20. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.module.ts +9 -0
  21. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +43 -0
  22. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +110 -0
  23. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.spec.ts +79 -0
  24. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.ts +74 -0
  25. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +1 -1
  26. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +155 -2
  27. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +109 -0
  28. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
  29. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +5 -0
  30. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +21 -1
  31. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +21 -1
  32. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +78 -0
  33. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts +8 -0
  34. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/+layout.svelte +2 -2
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.server.ts +8 -1
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.svelte +73 -2
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +2 -47
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +3 -2
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +5 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +15 -1
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +4 -0
  42. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +44 -4
  43. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +10 -10
  44. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +3 -3
  45. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +96 -0
  46. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -2
  47. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +3 -3
  48. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +70 -7
  49. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +1 -1
  50. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +18 -2
  51. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +1 -1
  52. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
  53. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +3 -3
  54. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -9
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +1 -1
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -2
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +1 -1
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +2 -2
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
  60. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +2 -2
  61. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +36 -3
  62. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +77 -7
  63. package/dist/templates/modules/admin-dashboard/files/tests/ui/sidebar.ui.spec.ts +1 -1
  64. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +1 -1
  65. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
  66. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
  67. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +4 -4
  68. package/dist/templates/modules/admin-dashboard/files/tests/ui/verify-email.ui.spec.ts +1 -1
  69. package/dist/templates/modules/admin-dashboard/module.manifest.json +74 -11
  70. package/dist/templates/modules/audit-log/module.manifest.json +1 -1
  71. package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +3 -6
  72. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +6 -0
  73. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +12 -5
  74. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +9 -2
  75. package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.spec.ts +42 -0
  76. package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.ts +27 -0
  77. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +55 -3
  78. package/dist/templates/modules/auth/module.manifest.json +5 -4
  79. package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
  80. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  81. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
  82. package/dist/templates/todo/Dockerfile.dev +3 -1
  83. package/dist/templates/todo/apps/api/package.json +1 -1
  84. package/dist/templates/todo/apps/web/package.json +1 -1
  85. package/dist/templates/todo/compose.dev.yaml +6 -0
  86. package/dist/update.d.ts +8 -1
  87. package/dist/update.js +68 -12
  88. package/package.json +1 -1
  89. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/{app-sidebar.svelte → admin-sidebar.svelte} +0 -0
  90. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/+page.svelte +0 -0
  91. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.server.ts +0 -0
  92. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.svelte +0 -0
  93. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.server.ts +0 -0
  94. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.svelte +0 -0
  95. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.server.ts +0 -0
  96. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.svelte +0 -0
  97. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.server.ts +0 -0
  98. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.svelte +0 -0
  99. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/general-settings.svelte +0 -0
  100. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.server.ts +0 -0
  101. /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.svelte +0 -0
@@ -2,7 +2,17 @@ import { BadRequestException, Injectable } from "@nestjs/common";
2
2
  import { InjectRepository } from "@nestjs/typeorm";
3
3
  import { Repository } from "typeorm";
4
4
  import { AuthConfigRow } from "./auth-config.entity";
5
- import { encryptSecret, envAuthConfig, socialKey, SUPPORTED_PROVIDER_IDS, SUPPORTED_SOCIAL_PROVIDERS } from "@podosoft/podokit-auth";
5
+ import {
6
+ DEFAULT_SESSION_LIFETIME_SECONDS,
7
+ encryptSecret,
8
+ envAuthConfig,
9
+ isSessionIdleTimeoutMinutes,
10
+ resolveSessionIdleTimeoutMinutes,
11
+ socialKey,
12
+ SUPPORTED_PROVIDER_IDS,
13
+ SUPPORTED_SOCIAL_PROVIDERS,
14
+ } from "@podosoft/podokit-auth";
15
+ import { refreshAuthNow } from "../auth/auth-provider";
6
16
 
7
17
  /** One configured social provider as shown to the admin (no secret — just a
8
18
  * `hasSecret` flag). */
@@ -16,7 +26,14 @@ export type AuthConfigView = {
16
26
  /** Catalog of addable providers (id + display label) for the "add provider" picker. */
17
27
  catalog: ReadonlyArray<{ id: string; label: string }>;
18
28
  smtp: { enabled: boolean; host: string; port: number; secure: boolean; user: string; from: string; hasSecret: boolean };
19
- server: { requireEmailVerification: boolean; requireSignupApproval: boolean; allowDelete: boolean; hibp: boolean; auditLog: boolean };
29
+ server: {
30
+ requireEmailVerification: boolean;
31
+ requireSignupApproval: boolean;
32
+ allowDelete: boolean;
33
+ hibp: boolean;
34
+ auditLog: boolean;
35
+ sessionIdleTimeoutMinutes: number | null;
36
+ };
20
37
  };
21
38
 
22
39
  type ProviderUpdate = { enabled?: boolean; clientId?: string; clientSecret?: string; redirectURI?: string; delete?: boolean };
@@ -24,7 +41,14 @@ export type AuthConfigUpdate = {
24
41
  /** Per-provider upsert (keyed by provider id); `{ delete: true }` removes it. */
25
42
  social?: Record<string, ProviderUpdate>;
26
43
  smtp?: { enabled?: boolean; host?: string; port?: number; secure?: boolean; user?: string; pass?: string; from?: string };
27
- server?: { requireEmailVerification?: boolean; requireSignupApproval?: boolean; allowDelete?: boolean; hibp?: boolean; auditLog?: boolean };
44
+ server?: {
45
+ requireEmailVerification?: boolean;
46
+ requireSignupApproval?: boolean;
47
+ allowDelete?: boolean;
48
+ hibp?: boolean;
49
+ auditLog?: boolean;
50
+ sessionIdleTimeoutMinutes?: number | null;
51
+ };
28
52
  };
29
53
 
30
54
  @Injectable()
@@ -59,6 +83,7 @@ export class AuthConfigService {
59
83
  allowDelete?: boolean;
60
84
  hibp?: boolean;
61
85
  auditLog?: boolean;
86
+ sessionIdleTimeoutMinutes?: number | null;
62
87
  };
63
88
  return {
64
89
  social,
@@ -78,6 +103,10 @@ export class AuthConfigService {
78
103
  allowDelete: serverC.allowDelete ?? env.allowDelete,
79
104
  hibp: serverC.hibp ?? env.hibp,
80
105
  auditLog: serverC.auditLog ?? env.auditLog,
106
+ sessionIdleTimeoutMinutes: resolveSessionIdleTimeoutMinutes(
107
+ serverC.sessionIdleTimeoutMinutes,
108
+ env.sessionIdleTimeoutMinutes,
109
+ ),
81
110
  },
82
111
  };
83
112
  }
@@ -90,6 +119,10 @@ export class AuthConfigService {
90
119
  }
91
120
  if (dto.smtp) await this.upsertSmtp(dto.smtp);
92
121
  if (dto.server) await this.upsertServer(dto.server);
122
+ await refreshAuthNow();
123
+ if (dto.server?.sessionIdleTimeoutMinutes !== undefined) {
124
+ await this.resetSessionExpirations(dto.server.sessionIdleTimeoutMinutes);
125
+ }
93
126
  return this.describe();
94
127
  }
95
128
 
@@ -116,12 +149,31 @@ export class AuthConfigService {
116
149
  }
117
150
 
118
151
  private async upsertServer(u: NonNullable<AuthConfigUpdate["server"]>): Promise<void> {
152
+ if (u.sessionIdleTimeoutMinutes !== undefined && !isSessionIdleTimeoutMinutes(u.sessionIdleTimeoutMinutes)) {
153
+ throw new BadRequestException("Session idle timeout must be null or an integer from 5 to 10080 minutes");
154
+ }
119
155
  const row = (await this.repo.findOneBy({ key: "server" })) ?? this.repo.create({ key: "server", enabled: true, config: {}, secret: null });
120
156
  const config = { ...(row.config as Record<string, unknown>) };
121
157
  for (const f of ["requireEmailVerification", "requireSignupApproval", "allowDelete", "hibp", "auditLog"] as const) {
122
158
  if (u[f] !== undefined) config[f] = u[f];
123
159
  }
160
+ if (u.sessionIdleTimeoutMinutes !== undefined) {
161
+ config.sessionIdleTimeoutMinutes = u.sessionIdleTimeoutMinutes;
162
+ }
124
163
  row.config = config;
125
164
  await this.repo.save(row);
126
165
  }
166
+
167
+ private async resetSessionExpirations(minutes: number | null): Promise<void> {
168
+ const lifetimeSeconds = minutes === null
169
+ ? DEFAULT_SESSION_LIFETIME_SECONDS
170
+ : minutes * 60;
171
+ await this.repo.manager.query(
172
+ `UPDATE "session"
173
+ SET "expiresAt" = CURRENT_TIMESTAMP + ($1 * INTERVAL '1 second'),
174
+ "updatedAt" = CURRENT_TIMESTAMP
175
+ WHERE "expiresAt" > CURRENT_TIMESTAMP`,
176
+ [lifetimeSeconds],
177
+ );
178
+ }
127
179
  }
@@ -7,8 +7,8 @@
7
7
  ".claude/skills/podokit-configure-auth/**"
8
8
  ],
9
9
  "dependencies": {
10
- "@podosoft/podokit-auth": "^0.2.0",
11
- "@podosoft/podokit-contracts": "^0.2.0",
10
+ "@podosoft/podokit-auth": "^0.3.0",
11
+ "@podosoft/podokit-contracts": "^0.4.0",
12
12
  "better-auth": "^1.6.23",
13
13
  "@thallesp/nestjs-better-auth": "^2.7.0",
14
14
  "pg": "^8.12.0",
@@ -44,6 +44,7 @@
44
44
  "# AUTH_EMAIL_VERIFICATION=false",
45
45
  "# AUTH_REQUIRE_SIGNUP_APPROVAL=false",
46
46
  "# AUTH_ALLOW_DELETE=false",
47
+ "# AUTH_SESSION_IDLE_TIMEOUT_MINUTES=30",
47
48
  "# GOOGLE_CLIENT_ID=",
48
49
  "# GOOGLE_CLIENT_SECRET=",
49
50
  "# GITHUB_CLIENT_ID=",
@@ -109,7 +110,7 @@
109
110
  "file": "AGENTS.md",
110
111
  "marker": "<!-- podokit:end:agents-modules -->",
111
112
  "optional": true,
112
- "text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, sign-up approval, and the other server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Use `npm run auth:configure -w <app>-api -- ...` and `.claude/skills/podokit-configure-auth` to apply OAuth/SMTP credentials without committing secrets. The sign-up approval gate is provider-independent; do not add provider-specific approval checks.\n- Development setup: create auth tables with `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`, then run TypeORM migrations. Production images can run both from compiled output with `npm run migrate:all`.\n"
113
+ "text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, sign-up approval, automatic logout, and the other server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Use `npm run auth:configure -w <app>-api -- ...` and `.claude/skills/podokit-configure-auth` to apply OAuth/SMTP credentials without committing secrets. The sign-up approval gate is provider-independent; do not add provider-specific approval checks.\n- Development setup: create auth tables with `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`, then run TypeORM migrations. Production images can run both from compiled output with `npm run migrate:all`.\n"
113
114
  }
114
115
  ],
115
116
  "instructions": [
@@ -120,6 +121,6 @@
120
121
  "Sign up: curl -c cookies.txt -XPOST localhost:5002/api/auth/sign-up/email -H 'content-type: application/json' -d '{\"email\":\"a@example.com\",\"password\":\"password123\",\"name\":\"A\"}'",
121
122
  "Call a protected route with the session: curl -b cookies.txt localhost:5002/account/me",
122
123
  "Use npm run auth:configure -w <app>-api -- --help for redacted OAuth, SMTP, and sign-up approval automation.",
123
- "Everything is protected by default; mark public routes with @Public(). Configure OAuth providers, SMTP, and server-enforced toggles on the admin Settings page \u2014 stored encrypted in the DB and applied live (no restart). Environment variables (GOOGLE_CLIENT_*, SMTP_*, AUTH_*) still work as an optional fallback/override."
124
+ "Everything is protected by default; mark public routes with @Public(). Configure OAuth providers, SMTP, automatic logout, and server-enforced toggles on the admin Settings page \u2014 stored in the DB and applied live (no restart). Environment variables (GOOGLE_CLIENT_*, SMTP_*, AUTH_*) still work as an optional fallback/override."
124
125
  ]
125
126
  }
@@ -23,7 +23,7 @@ test("file upload: multipart upload returns a key and url @smoke", async ({ play
23
23
  await ctx.dispose();
24
24
  });
25
25
 
26
- test("file upload: a request with no file is a 400 @smoke", async ({ playwright }) => {
26
+ test("file upload: a request with no file is a 400", async ({ playwright }) => {
27
27
  const ctx = await session(playwright);
28
28
  const res = await ctx.post("/api/files", { multipart: { notfile: "x" } });
29
29
  expect(res.status()).toBe(400);
@@ -5,7 +5,7 @@
5
5
  "targetApp": "api",
6
6
  "dependencies": {
7
7
  "nodemailer": "^7.0.9",
8
- "@podosoft/podokit-auth": "^0.2.0",
8
+ "@podosoft/podokit-auth": "^0.3.0",
9
9
  "pg": "^8.12.0"
10
10
  },
11
11
  "devDependencies": {
@@ -10,7 +10,7 @@ async function session(playwright: import("@playwright/test").PlaywrightWorkerAr
10
10
  return ctx;
11
11
  }
12
12
 
13
- test("sse: publish accepts a message @smoke", async ({ playwright }) => {
13
+ test("sse: publish accepts a message", async ({ playwright }) => {
14
14
  const ctx = await session(playwright);
15
15
  const pub = await ctx.post("/api/events", { data: { message: `hi-${Date.now()}` } });
16
16
  expect(pub.ok()).toBeTruthy();
@@ -17,7 +17,9 @@ WORKDIR /app
17
17
  COPY package.json ./
18
18
  COPY apps/api/package.json ./apps/api/package.json
19
19
  COPY apps/web/package.json ./apps/web/package.json
20
- RUN npm install
20
+ ARG PODOKIT_NPM_REGISTRY=""
21
+ RUN if [ -n "$PODOKIT_NPM_REGISTRY" ]; then npm config set registry "$PODOKIT_NPM_REGISTRY"; fi \
22
+ && npm install
21
23
 
22
24
  # Baked source (kept in sync during development by `docker compose watch`).
23
25
  COPY . .
@@ -14,7 +14,7 @@
14
14
  "migration:generate": "npm run typeorm -- migration:generate"
15
15
  },
16
16
  "dependencies": {
17
- "@podosoft/podokit-contracts": "^0.2.0",
17
+ "@podosoft/podokit-contracts": "^0.4.0",
18
18
  "@nestjs/common": "^11.1.27",
19
19
  "@nestjs/config": "^4.0.4",
20
20
  "@nestjs/core": "^11.1.27",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@internationalized/date": "^3.12.2",
15
15
  "@lucide/svelte": "^1.23.0",
16
- "@podosoft/podokit-api-client": "^0.6.0",
16
+ "@podosoft/podokit-api-client": "^0.8.0",
17
17
  "bits-ui": "^2.18.1",
18
18
  "clsx": "^2.1.1",
19
19
  "mode-watcher": "^1.1.0",
@@ -41,6 +41,8 @@ services:
41
41
  build:
42
42
  context: .
43
43
  dockerfile: Dockerfile.dev
44
+ args:
45
+ PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
44
46
  command: npm run dev -w {{projectName}}-web -- --host 0.0.0.0
45
47
  working_dir: /app
46
48
  env_file: .env.docker
@@ -64,6 +66,8 @@ services:
64
66
  build:
65
67
  context: .
66
68
  dockerfile: Dockerfile.dev
69
+ args:
70
+ PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
67
71
  init: true
68
72
  command: npm run dev -w {{projectName}}-api
69
73
  working_dir: /app
@@ -87,6 +91,8 @@ services:
87
91
  build:
88
92
  context: .
89
93
  dockerfile: Dockerfile.dev
94
+ args:
95
+ PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
90
96
  init: true
91
97
  command: npm run dev:worker -w {{projectName}}-api
92
98
  working_dir: /app
package/dist/update.d.ts CHANGED
@@ -5,9 +5,11 @@ import { type Tier } from "./lockfile";
5
5
  * working copy, classifying each file. Applying the plan (writing files, 3-way
6
6
  * merging) is a later step (see ADR-0009).
7
7
  */
8
- export type Action = "update" | "merge" | "conflict" | "add" | "remove" | "skip" | "up-to-date";
8
+ export type Action = "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
9
9
  export interface FileChange {
10
10
  path: string;
11
+ /** Previous path when this change is an update migration move. */
12
+ fromPath?: string;
11
13
  tier: Tier;
12
14
  action: Action;
13
15
  /** Why the action was chosen (for the report). */
@@ -33,6 +35,11 @@ export interface ApplyOptions {
33
35
  }
34
36
  export interface ApplyResult {
35
37
  written: string[];
38
+ /** Files relocated by module update migrations. */
39
+ moved: {
40
+ from: string;
41
+ to: string;
42
+ }[];
36
43
  removed: string[];
37
44
  /** Files 3-way merged cleanly. */
38
45
  merged: string[];
package/dist/update.js CHANGED
@@ -12,6 +12,7 @@ const assemble_1 = require("./assemble");
12
12
  const lockfile_1 = require("./lockfile");
13
13
  const inspect_1 = require("./inspect");
14
14
  const add_1 = require("./add");
15
+ const module_migrations_1 = require("./module-migrations");
15
16
  function treeText(tree, path) {
16
17
  const file = tree.get(path);
17
18
  if (!file)
@@ -64,9 +65,29 @@ function planUpdate(projectRoot, templatesDir) {
64
65
  });
65
66
  const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, manifest.modules, manifest.managedOverrides);
66
67
  const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, manifest.modules, manifest.ownedGlobs);
67
- const changes = [];
68
+ const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, manifest.modules);
69
+ const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, lock, pendingMigrations);
70
+ const changes = migrationPlan.moves.map((move) => ({
71
+ path: move.to,
72
+ fromPath: move.from,
73
+ tier: move.tier,
74
+ action: "move",
75
+ note: "module path migration",
76
+ }));
77
+ for (const conflict of migrationPlan.conflicts) {
78
+ changes.push({
79
+ path: "module migration",
80
+ tier: "managed",
81
+ action: "conflict",
82
+ note: conflict,
83
+ });
84
+ }
85
+ const movedSources = new Set(migrationPlan.moves.map((move) => move.from));
86
+ const movedTargets = new Set(migrationPlan.moves.map((move) => move.to));
68
87
  const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
69
88
  for (const path of [...paths].sort()) {
89
+ if (movedSources.has(path) || movedTargets.has(path))
90
+ continue;
70
91
  const locked = lock.files[path];
71
92
  const newText = treeText(newTree, path);
72
93
  const disk = diskContent(projectRoot, path);
@@ -232,6 +253,19 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
232
253
  });
233
254
  const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides);
234
255
  const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs);
256
+ const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, modules);
257
+ const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, previousLock, pendingMigrations);
258
+ if (migrationPlan.conflicts.length) {
259
+ throw new Error(`Cannot apply module path migration:\n${migrationPlan.conflicts
260
+ .map((conflict) => ` - ${conflict}`)
261
+ .join("\n")}`);
262
+ }
263
+ const migratedOwnedGlobs = [
264
+ ...new Set(ownedGlobs.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
265
+ ];
266
+ const migratedManagedOverrides = [
267
+ ...new Set(managedOverrides.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
268
+ ];
235
269
  const needsMergeBase = plan.changes.some((change) => change.action === "conflict");
236
270
  let oldTree = null;
237
271
  if (options.oldTemplatesDir && needsMergeBase) {
@@ -250,7 +284,13 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
250
284
  (0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
251
285
  }
252
286
  }
253
- const result = { written: [], removed: [], merged: [], conflicts: [] };
287
+ const result = {
288
+ written: [],
289
+ moved: (0, module_migrations_1.applyModuleMigrations)(projectRoot, migrationPlan),
290
+ removed: [],
291
+ merged: [],
292
+ conflicts: [],
293
+ };
254
294
  for (const change of plan.changes) {
255
295
  const newText = treeText(newTree, change.path);
256
296
  if (change.action === "update" || change.action === "add") {
@@ -280,22 +320,37 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
280
320
  // Keep the assembled target as the managed baseline. A merged working file
281
321
  // intentionally remains drifted so a future update performs another 3-way
282
322
  // merge instead of treating the user's lines as clean generated output.
283
- (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, ownedGlobs, managedOverrides));
323
+ (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, migratedOwnedGlobs, migratedManagedOverrides));
284
324
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
325
+ const appliedMigrations = new Map();
326
+ for (const pending of pendingMigrations) {
327
+ const ids = appliedMigrations.get(pending.module) ?? [];
328
+ ids.push(pending.migration.id);
329
+ appliedMigrations.set(pending.module, ids);
330
+ }
285
331
  const refreshedModules = manifest.modules.map((module) => {
286
332
  const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
287
- return resolved?.packageName
288
- ? {
289
- ...module,
290
- packageName: resolved.packageName,
291
- moduleVersion: resolved.moduleVersion,
292
- }
293
- : module;
333
+ const applied = [
334
+ ...new Set([
335
+ ...(module.appliedMigrations ?? []),
336
+ ...(appliedMigrations.get(module.name) ?? []),
337
+ ]),
338
+ ];
339
+ return {
340
+ ...module,
341
+ ...(resolved?.packageName
342
+ ? {
343
+ packageName: resolved.packageName,
344
+ moduleVersion: resolved.moduleVersion,
345
+ }
346
+ : {}),
347
+ ...(applied.length ? { appliedMigrations: applied } : {}),
348
+ };
294
349
  });
295
350
  (0, lockfile_1.writeManifest)(projectRoot, {
296
351
  ...manifest,
297
- ownedGlobs,
298
- managedOverrides,
352
+ ownedGlobs: migratedOwnedGlobs,
353
+ managedOverrides: migratedManagedOverrides,
299
354
  modules: refreshedModules,
300
355
  podokitVersion: (0, lockfile_1.podokitVersion)(),
301
356
  });
@@ -308,6 +363,7 @@ function summarize(plan) {
308
363
  merge: 0,
309
364
  conflict: 0,
310
365
  add: 0,
366
+ move: 0,
311
367
  remove: 0,
312
368
  skip: 0,
313
369
  "up-to-date": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "0.12.4",
3
+ "version": "0.14.0",
4
4
  "description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {