@podosoft/podokit 0.11.1 → 0.11.2

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.
package/dist/add.d.ts CHANGED
@@ -18,6 +18,9 @@ export interface ModuleManifest {
18
18
  dependencies?: Record<string, string>;
19
19
  devDependencies?: Record<string, string>;
20
20
  scripts?: Record<string, string>;
21
+ /** Extra package.json overlays keyed by app name. Use when a module's files
22
+ * span more than its primary targetApp. */
23
+ packageOverlays?: Record<string, PackageOverlay>;
21
24
  env?: string[];
22
25
  inject?: Injection[];
23
26
  instructions?: string[];
@@ -29,7 +32,17 @@ export interface ModuleManifest {
29
32
  /** Paths that an existing app may explicitly hand back to this module with
30
33
  * `podo add --adopt`. Broad app-owned route globs are never removed. */
31
34
  managedGlobs?: string[];
35
+ /** Module files that remain update-managed even when a broad default glob
36
+ * (for example `.claude/**`) would otherwise classify them as owned. */
37
+ managedOverrides?: string[];
32
38
  }
39
+ export interface PackageOverlay {
40
+ dependencies?: Record<string, string>;
41
+ devDependencies?: Record<string, string>;
42
+ scripts?: Record<string, string>;
43
+ }
44
+ /** Return the legacy targetApp package fields plus any additional app overlays. */
45
+ export declare function modulePackageOverlays(manifest: ModuleManifest): Map<string, PackageOverlay>;
33
46
  export interface AddOptions {
34
47
  projectRoot: string;
35
48
  module: string;
@@ -46,6 +59,8 @@ export interface AddResult {
46
59
  added: string[];
47
60
  /** ownedGlobs declared by this module and every module it pulled in. */
48
61
  ownedGlobs: string[];
62
+ /** Managed exceptions declared by this module and its requirements. */
63
+ managedOverrides: string[];
49
64
  /** Existing owned presentation files intentionally left untouched. */
50
65
  preserved: string[];
51
66
  /** Paths explicitly adopted as module-managed files. */
@@ -75,6 +90,7 @@ export declare function listModules(modulesDir: string, projectRoot?: string): {
75
90
  name: string;
76
91
  description: string;
77
92
  }[];
93
+ export declare function readModuleManifest(moduleDir: string): ModuleManifest;
78
94
  /**
79
95
  * Apply a module to an existing generated project: overlay files, merge the
80
96
  * target app's package.json dependencies and scripts, append env example lines,
package/dist/add.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SUPPORTED_MANIFEST_VERSION = void 0;
4
+ exports.modulePackageOverlays = modulePackageOverlays;
4
5
  exports.resolveModuleDir = resolveModuleDir;
5
6
  exports.resolveModule = resolveModule;
6
7
  exports.listModules = listModules;
8
+ exports.readModuleManifest = readModuleManifest;
7
9
  exports.addModule = addModule;
8
10
  const node_fs_1 = require("node:fs");
9
11
  const node_path_1 = require("node:path");
@@ -13,6 +15,34 @@ const lockfile_1 = require("./lockfile");
13
15
  /** Highest `module.manifest.json` schema version this CLI understands. A package
14
16
  * module declaring a higher version is rejected rather than mis-applied. */
15
17
  exports.SUPPORTED_MANIFEST_VERSION = 1;
18
+ /** Return the legacy targetApp package fields plus any additional app overlays. */
19
+ function modulePackageOverlays(manifest) {
20
+ const overlays = new Map();
21
+ const merge = (app, overlay) => {
22
+ const current = overlays.get(app) ?? {};
23
+ overlays.set(app, {
24
+ ...(current.dependencies || overlay.dependencies
25
+ ? { dependencies: { ...current.dependencies, ...overlay.dependencies } }
26
+ : {}),
27
+ ...(current.devDependencies || overlay.devDependencies
28
+ ? { devDependencies: { ...current.devDependencies, ...overlay.devDependencies } }
29
+ : {}),
30
+ ...(current.scripts || overlay.scripts
31
+ ? { scripts: { ...current.scripts, ...overlay.scripts } }
32
+ : {}),
33
+ });
34
+ };
35
+ if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
36
+ merge(manifest.targetApp, {
37
+ dependencies: manifest.dependencies,
38
+ devDependencies: manifest.devDependencies,
39
+ scripts: manifest.scripts,
40
+ });
41
+ }
42
+ for (const [app, overlay] of Object.entries(manifest.packageOverlays ?? {}))
43
+ merge(app, overlay);
44
+ return overlays;
45
+ }
16
46
  const PACKAGE_PREFIX = "@podosoft/podokit-module-";
17
47
  /**
18
48
  * Resolve a module's directory by name: first the bundled `modulesDir`, then a
@@ -38,7 +68,7 @@ function resolveModule(name, modulesDir, projectRoot) {
38
68
  const dir = resolveModuleDir(name, modulesDir, projectRoot);
39
69
  if (!dir)
40
70
  return null;
41
- const manifest = readManifest(dir);
71
+ const manifest = readModuleManifest(dir);
42
72
  const bundled = (0, node_path_1.join)(modulesDir, manifest.name);
43
73
  if (dir === bundled)
44
74
  return { dir, name: manifest.name };
@@ -66,7 +96,7 @@ function listModules(modulesDir, projectRoot) {
66
96
  if ((0, node_fs_1.existsSync)(modulesDir)) {
67
97
  for (const name of (0, node_fs_1.readdirSync)(modulesDir)) {
68
98
  if ((0, node_fs_1.existsSync)((0, node_path_1.join)(modulesDir, name, "module.manifest.json"))) {
69
- out.set(name, readManifest((0, node_path_1.join)(modulesDir, name)).description);
99
+ out.set(name, readModuleManifest((0, node_path_1.join)(modulesDir, name)).description);
70
100
  }
71
101
  }
72
102
  }
@@ -79,7 +109,7 @@ function listModules(modulesDir, projectRoot) {
79
109
  const manifestPath = (0, node_path_1.join)(scope, dir, "module.manifest.json");
80
110
  if (!(0, node_fs_1.existsSync)(manifestPath))
81
111
  continue;
82
- const manifest = readManifest((0, node_path_1.join)(scope, dir));
112
+ const manifest = readModuleManifest((0, node_path_1.join)(scope, dir));
83
113
  if (!out.has(manifest.name))
84
114
  out.set(manifest.name, manifest.description);
85
115
  }
@@ -87,7 +117,7 @@ function listModules(modulesDir, projectRoot) {
87
117
  }
88
118
  return [...out].map(([name, description]) => ({ name, description }));
89
119
  }
90
- function readManifest(moduleDir) {
120
+ function readModuleManifest(moduleDir) {
91
121
  return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8"));
92
122
  }
93
123
  function readJson(path) {
@@ -113,7 +143,7 @@ function isApplied(projectRoot, modulesDir, module) {
113
143
  const moduleDir = resolveModuleDir(module, modulesDir, projectRoot);
114
144
  if (!moduleDir)
115
145
  return false;
116
- const manifest = readManifest(moduleDir);
146
+ const manifest = readModuleManifest(moduleDir);
117
147
  const firstInject = manifest.inject?.[0];
118
148
  if (firstInject) {
119
149
  const target = (0, node_path_1.join)(projectRoot, firstInject.file);
@@ -153,7 +183,7 @@ function addModule(options) {
153
183
  modulePaths: [...new Set(result.touched)],
154
184
  adoptedPaths: result.adopted,
155
185
  }
156
- : undefined);
186
+ : undefined, result.managedOverrides);
157
187
  return result;
158
188
  }
159
189
  function applyModule(projectRoot, module, modulesDir, applied, adopt) {
@@ -163,16 +193,18 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
163
193
  throw new Error(`Unknown module "${module}".${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
164
194
  }
165
195
  const moduleDir = resolved.dir;
166
- const manifest = readManifest(moduleDir);
196
+ const manifest = readModuleManifest(moduleDir);
167
197
  assertManifestVersion(module, manifest);
168
- const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", manifest.targetApp, "package.json");
169
- if (!(0, node_fs_1.existsSync)(appPkgPath)) {
170
- throw new Error(`This does not look like a PodoKit project: ${(0, node_path_1.join)("apps", manifest.targetApp, "package.json")} not found. Run inside a generated project.`);
198
+ const requiredApps = new Set([manifest.targetApp, ...Object.keys(manifest.packageOverlays ?? {})]);
199
+ const missingApp = [...requiredApps].find((app) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "apps", app, "package.json")));
200
+ if (missingApp) {
201
+ throw new Error(`This does not look like a PodoKit project: ${(0, node_path_1.join)("apps", missingApp, "package.json")} not found. Run inside a generated project.`);
171
202
  }
172
203
  applied.add(module);
173
204
  // 0) apply required modules first (auto-add if missing)
174
205
  const added = [];
175
206
  const ownedGlobs = [...(manifest.ownedGlobs ?? [])];
207
+ const managedOverrides = [...(manifest.managedOverrides ?? [])];
176
208
  const preserved = [];
177
209
  const adopted = [];
178
210
  const touched = [];
@@ -182,6 +214,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
182
214
  const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
183
215
  added.push(required, ...result.added);
184
216
  ownedGlobs.push(...result.ownedGlobs);
217
+ managedOverrides.push(...result.managedOverrides);
185
218
  preserved.push(...result.preserved);
186
219
  adopted.push(...result.adopted);
187
220
  touched.push(...result.touched);
@@ -202,18 +235,19 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
202
235
  adopted.push(...copied.adopted);
203
236
  touched.push(...copied.touched);
204
237
  }
205
- // 2) merge dependencies and scripts into the target app
206
- if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
238
+ // 2) merge dependencies and scripts into every declared app package
239
+ for (const [app, declaration] of modulePackageOverlays(manifest)) {
240
+ const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", app, "package.json");
207
241
  const overlay = {};
208
- if (manifest.dependencies)
209
- overlay.dependencies = manifest.dependencies;
210
- if (manifest.devDependencies)
211
- overlay.devDependencies = manifest.devDependencies;
212
- if (manifest.scripts)
213
- overlay.scripts = manifest.scripts;
242
+ if (declaration.dependencies)
243
+ overlay.dependencies = declaration.dependencies;
244
+ if (declaration.devDependencies)
245
+ overlay.devDependencies = declaration.devDependencies;
246
+ if (declaration.scripts)
247
+ overlay.scripts = declaration.scripts;
214
248
  const merged = (0, podokit_template_engine_1.mergePackageJson)(readJson(appPkgPath), overlay);
215
249
  (0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(merged, null, 2)}\n`);
216
- touched.push(`apps/${manifest.targetApp}/package.json`);
250
+ touched.push(`apps/${app}/package.json`);
217
251
  }
218
252
  // 3) append env example lines
219
253
  if (manifest.env?.length) {
@@ -235,7 +269,16 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
235
269
  touched.push(injection.file);
236
270
  }
237
271
  const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
238
- return { module: manifest.name, instructions, added, ownedGlobs, preserved, adopted, touched };
272
+ return {
273
+ module: manifest.name,
274
+ instructions,
275
+ added,
276
+ ownedGlobs,
277
+ managedOverrides,
278
+ preserved,
279
+ adopted,
280
+ touched,
281
+ };
239
282
  }
240
283
  function moduleRecord(module) {
241
284
  return {
package/dist/assemble.js CHANGED
@@ -22,17 +22,17 @@ function applyModuleToTree(tree, moduleDir, vars) {
22
22
  for (const [path, file] of (0, podokit_template_engine_1.renderTemplate)(filesDir, vars))
23
23
  tree.set(path, file);
24
24
  }
25
- // 2) merge dependencies/scripts into the target app's package.json
26
- if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
27
- const pkgPath = `apps/${manifest.targetApp}/package.json`;
25
+ // 2) merge dependencies/scripts into every declared app package.json
26
+ for (const [app, declaration] of (0, add_1.modulePackageOverlays)(manifest)) {
27
+ const pkgPath = `apps/${app}/package.json`;
28
28
  const base = JSON.parse(textOf(tree, pkgPath) || "{}");
29
29
  const overlay = {};
30
- if (manifest.dependencies)
31
- overlay.dependencies = manifest.dependencies;
32
- if (manifest.devDependencies)
33
- overlay.devDependencies = manifest.devDependencies;
34
- if (manifest.scripts)
35
- overlay.scripts = manifest.scripts;
30
+ if (declaration.dependencies)
31
+ overlay.dependencies = declaration.dependencies;
32
+ if (declaration.devDependencies)
33
+ overlay.devDependencies = declaration.devDependencies;
34
+ if (declaration.scripts)
35
+ overlay.scripts = declaration.scripts;
36
36
  setText(tree, pkgPath, `${JSON.stringify((0, podokit_template_engine_1.mergePackageJson)(base, overlay), null, 2)}\n`);
37
37
  }
38
38
  // 3) append env example lines (deduped)
@@ -32,6 +32,8 @@ export interface PodokitManifest {
32
32
  modules: ManifestModule[];
33
33
  /** Globs whose files are user-owned and never touched by update. */
34
34
  ownedGlobs: string[];
35
+ /** Module-supplied globs that stay managed even inside a broad owned glob. */
36
+ managedOverrides?: string[];
35
37
  }
36
38
  export interface FileEntry {
37
39
  tier: Tier;
@@ -64,11 +66,11 @@ export declare function podokitVersion(): string;
64
66
  /** Match a POSIX relative path against a simple glob supporting `*` and `**`. */
65
67
  export declare function matchGlob(path: string, glob: string): boolean;
66
68
  /** Classify a file's ownership tier. See ADR-0010 priority order. */
67
- export declare function classifyTier(relPath: string, content: string | Buffer, ownedGlobs: string[]): Tier;
69
+ export declare function classifyTier(relPath: string, content: string | Buffer, ownedGlobs: string[], managedOverrides?: string[]): Tier;
68
70
  /** List project files as POSIX-relative paths, skipping generated/vendored dirs. */
69
71
  export declare function walkFiles(projectRoot: string, prefix?: string): string[];
70
72
  /** Recompute the files.lock by hashing and classifying every project file. */
71
- export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[]): FilesLock;
73
+ export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[], managedOverrides?: string[]): FilesLock;
72
74
  export declare function readFilesLock(projectRoot: string): FilesLock | null;
73
75
  export interface Drift {
74
76
  /** Managed/assembled files whose content differs from what PodoKit wrote. */
@@ -101,4 +103,4 @@ export declare function mergeOwnedGlobs(existing: string[], add: string[]): stri
101
103
  * `ownedGlobs` a module declares are merged into the manifest so its owned paths
102
104
  * survive every future recompute. No-op on projects without a manifest.
103
105
  */
104
- export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot): void;
106
+ export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[]): void;
package/dist/lockfile.js CHANGED
@@ -114,7 +114,12 @@ function isTextPath(relPath) {
114
114
  return relPath.startsWith("dot-") || TEXT_EXT.test(relPath);
115
115
  }
116
116
  /** Classify a file's ownership tier. See ADR-0010 priority order. */
117
- function classifyTier(relPath, content, ownedGlobs) {
117
+ function classifyTier(relPath, content, ownedGlobs, managedOverrides = []) {
118
+ const explicitlyOwned = ownedGlobs.some((glob) => !glob.includes("*") && matchGlob(relPath, glob));
119
+ if (explicitlyOwned)
120
+ return "owned";
121
+ if (managedOverrides.some((glob) => matchGlob(relPath, glob)))
122
+ return "managed";
118
123
  if (ownedGlobs.some((glob) => matchGlob(relPath, glob)))
119
124
  return "owned";
120
125
  if (isTextPath(relPath) && content.toString("utf8").includes(INJECTION_MARKER))
@@ -138,11 +143,14 @@ function walkFiles(projectRoot, prefix = "") {
138
143
  return out;
139
144
  }
140
145
  /** Recompute the files.lock by hashing and classifying every project file. */
141
- function computeFilesLock(projectRoot, ownedGlobs) {
146
+ function computeFilesLock(projectRoot, ownedGlobs, managedOverrides = []) {
142
147
  const files = {};
143
148
  for (const rel of walkFiles(projectRoot).sort()) {
144
149
  const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, rel));
145
- files[rel] = { tier: classifyTier(rel, content, ownedGlobs), outHash: (0, podokit_template_engine_1.hashContent)(content) };
150
+ files[rel] = {
151
+ tier: classifyTier(rel, content, ownedGlobs, managedOverrides),
152
+ outHash: (0, podokit_template_engine_1.hashContent)(content),
153
+ };
146
154
  }
147
155
  return { schemaVersion: exports.LOCK_SCHEMA_VERSION, files };
148
156
  }
@@ -185,6 +193,7 @@ function initLockfile(projectRoot, options) {
185
193
  answers: options.answers,
186
194
  modules: [],
187
195
  ownedGlobs,
196
+ managedOverrides: [],
188
197
  };
189
198
  writeManifest(projectRoot, manifest);
190
199
  writeFilesLock(projectRoot, computeFilesLock(projectRoot, ownedGlobs));
@@ -209,7 +218,7 @@ function mergeOwnedGlobs(existing, add) {
209
218
  * `ownedGlobs` a module declares are merged into the manifest so its owned paths
210
219
  * survive every future recompute. No-op on projects without a manifest.
211
220
  */
212
- function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot) {
221
+ function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot, managedOverrides) {
213
222
  const manifest = readManifest(projectRoot);
214
223
  if (!manifest)
215
224
  return;
@@ -234,8 +243,11 @@ function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnaps
234
243
  }
235
244
  if (ownedGlobs?.length)
236
245
  manifest.ownedGlobs = mergeOwnedGlobs(manifest.ownedGlobs, ownedGlobs);
246
+ if (managedOverrides?.length) {
247
+ manifest.managedOverrides = mergeOwnedGlobs(manifest.managedOverrides ?? [], managedOverrides);
248
+ }
237
249
  writeManifest(projectRoot, manifest);
238
- const computed = computeFilesLock(projectRoot, manifest.ownedGlobs);
250
+ const computed = computeFilesLock(projectRoot, manifest.ownedGlobs, manifest.managedOverrides ?? []);
239
251
  if (!lockSnapshot) {
240
252
  writeFilesLock(projectRoot, computed);
241
253
  return;
package/dist/remove.js CHANGED
@@ -107,8 +107,11 @@ function removeModule(options) {
107
107
  manifest.modules = manifest.modules
108
108
  .filter((m) => m.name !== module)
109
109
  .map((m, i) => ({ ...m, order: i }));
110
+ const sharedManagedOverrides = new Set(others.flatMap((other) => other.manifest.managedOverrides ?? []));
111
+ const targetManagedOverrides = new Set(targetManifest.managedOverrides ?? []);
112
+ manifest.managedOverrides = (manifest.managedOverrides ?? []).filter((glob) => !targetManagedOverrides.has(glob) || sharedManagedOverrides.has(glob));
110
113
  (0, lockfile_1.writeManifest)(projectRoot, manifest);
111
- (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs));
114
+ (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides));
112
115
  return { module, removed, keptEdited, keptShared, unwired };
113
116
  }
114
117
  /** Remove now-empty directories left behind by a deleted file, up to the root. */
@@ -126,33 +129,33 @@ function pruneEmptyDirs(projectRoot, relDir) {
126
129
  }
127
130
  }
128
131
  function pruneAppPackage(projectRoot, target, others) {
129
- if (!(target.dependencies || target.devDependencies || target.scripts))
130
- return;
131
- const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", target.targetApp, "package.json");
132
- if (!(0, node_fs_1.existsSync)(appPkgPath))
133
- return;
134
- const keep = (section) => new Set(others.flatMap((o) => Object.keys(o.manifest[section] ?? {})));
135
- const pkg = readJson(appPkgPath);
136
- let changed = false;
137
- for (const section of ["dependencies", "devDependencies", "scripts"]) {
138
- const declared = target[section];
139
- const current = pkg[section];
140
- if (!declared || !current)
132
+ for (const [app, declaration] of (0, add_1.modulePackageOverlays)(target)) {
133
+ const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", app, "package.json");
134
+ if (!(0, node_fs_1.existsSync)(appPkgPath))
141
135
  continue;
142
- const shared = keep(section);
143
- for (const name of Object.keys(declared)) {
144
- if (shared.has(name))
136
+ const keep = (section) => new Set(others.flatMap((other) => Object.keys((0, add_1.modulePackageOverlays)(other.manifest).get(app)?.[section] ?? {})));
137
+ const pkg = readJson(appPkgPath);
138
+ let changed = false;
139
+ for (const section of ["dependencies", "devDependencies", "scripts"]) {
140
+ const declared = declaration[section];
141
+ const current = pkg[section];
142
+ if (!declared || !current)
145
143
  continue;
146
- if (name in current) {
147
- delete current[name];
148
- changed = true;
144
+ const shared = keep(section);
145
+ for (const name of Object.keys(declared)) {
146
+ if (shared.has(name))
147
+ continue;
148
+ if (name in current) {
149
+ delete current[name];
150
+ changed = true;
151
+ }
149
152
  }
153
+ if (current && Object.keys(current).length === 0)
154
+ delete pkg[section];
150
155
  }
151
- if (current && Object.keys(current).length === 0)
152
- delete pkg[section];
156
+ if (changed)
157
+ (0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
153
158
  }
154
- if (changed)
155
- (0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
156
159
  }
157
160
  function pruneEnvExample(projectRoot, target, others) {
158
161
  if (!target.env?.length)
@@ -20,6 +20,7 @@ RUN addgroup -S app && adduser -S app -G app
20
20
  COPY --from=build /app/node_modules ./node_modules
21
21
  COPY --from=deps /app/apps/api ./apps/api
22
22
  COPY --from=build /app/apps/api/dist ./apps/api/dist
23
+ COPY --from=build /app/apps/api/scripts ./apps/api/scripts
23
24
  WORKDIR /app/apps/api
24
25
  USER app
25
26
  EXPOSE 3000
@@ -83,6 +83,7 @@ services:
83
83
  # sync+restart is the stable, recommended approach; the web keeps instant
84
84
  # HMR via plain sync.
85
85
  - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
86
+ - { action: sync, path: ./apps/api/scripts, target: /app/apps/api/scripts }
86
87
  - { action: rebuild, path: ./apps/api/package.json }
87
88
 
88
89
  worker:
@@ -105,6 +106,7 @@ services:
105
106
  # sync+restart is the stable, recommended approach; the web keeps instant
106
107
  # HMR via plain sync.
107
108
  - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
109
+ - { action: sync, path: ./apps/api/scripts, target: /app/apps/api/scripts }
108
110
  - { action: rebuild, path: ./apps/api/package.json }
109
111
 
110
112
  postgres:
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ function has(flag) {
8
+ return args.includes(flag);
9
+ }
10
+
11
+ function required(name, value) {
12
+ if (!value?.trim()) throw new Error(`${name} is required.`);
13
+ return value.trim();
14
+ }
15
+
16
+ function usage() {
17
+ console.log(`Create or verify the initial PodoKit administrator.
18
+
19
+ Usage:
20
+ npm run admin:bootstrap -w <app>-api
21
+ npm run admin:bootstrap -w <app>-api -- --dry-run
22
+ npm run admin:bootstrap -w <app>-api -- --check-only
23
+
24
+ Required environment:
25
+ ADMIN_EMAILS Allowed administrator emails (comma-separated)
26
+ ADMIN_BOOTSTRAP_EMAIL Initial administrator email
27
+ ADMIN_BOOTSTRAP_PASSWORD Initial administrator password
28
+
29
+ Optional environment:
30
+ ADMIN_BOOTSTRAP_NAME Display name (default: Administrator)
31
+
32
+ AUTH_SETUP_ADMIN_EMAIL and AUTH_SETUP_ADMIN_PASSWORD work as fallbacks so the
33
+ same ephemeral credentials can be reused by auth:configure.
34
+
35
+ Options:
36
+ --dry-run Validate and print a redacted plan without writing
37
+ --check-only Verify an existing administrator without creating
38
+ `);
39
+ }
40
+
41
+ export function resolveBootstrapInput(env = process.env) {
42
+ const email = required(
43
+ "ADMIN_BOOTSTRAP_EMAIL",
44
+ env.ADMIN_BOOTSTRAP_EMAIL ?? env.AUTH_SETUP_ADMIN_EMAIL,
45
+ ).toLowerCase();
46
+ const password = required(
47
+ "ADMIN_BOOTSTRAP_PASSWORD",
48
+ env.ADMIN_BOOTSTRAP_PASSWORD ?? env.AUTH_SETUP_ADMIN_PASSWORD,
49
+ );
50
+ const name = required("ADMIN_BOOTSTRAP_NAME", env.ADMIN_BOOTSTRAP_NAME ?? "Administrator");
51
+ const adminEmails = new Set(
52
+ required("ADMIN_EMAILS", env.ADMIN_EMAILS)
53
+ .split(",")
54
+ .map((value) => value.trim().toLowerCase())
55
+ .filter(Boolean),
56
+ );
57
+
58
+ if (!adminEmails.has(email)) {
59
+ throw new Error("ADMIN_BOOTSTRAP_EMAIL must be listed in ADMIN_EMAILS.");
60
+ }
61
+ if (password.length < 8 || password.length > 128) {
62
+ throw new Error("ADMIN_BOOTSTRAP_PASSWORD must be between 8 and 128 characters.");
63
+ }
64
+
65
+ return { email, password, name };
66
+ }
67
+
68
+ function roles(value) {
69
+ return (value ?? "")
70
+ .split(",")
71
+ .map((role) => role.trim())
72
+ .filter(Boolean);
73
+ }
74
+
75
+ async function verifyExisting(dependencies, input, user) {
76
+ if (!roles(user.role).includes("admin")) {
77
+ throw new Error("The administrator email already exists without the admin role; no changes were made.");
78
+ }
79
+ if (user.signupApproved !== true) {
80
+ throw new Error("The administrator account is not approved; no changes were made.");
81
+ }
82
+ if (user.emailVerified !== true) {
83
+ throw new Error("The administrator email is not verified; no changes were made.");
84
+ }
85
+ if (!user.passwordHash || !(await dependencies.verifyPassword(user.passwordHash, input.password))) {
86
+ throw new Error("The administrator account already exists but the supplied password does not match; no changes were made.");
87
+ }
88
+ }
89
+
90
+ export async function bootstrapAdmin(dependencies, input, options = {}) {
91
+ const existing = await dependencies.findUser(input.email);
92
+
93
+ if (options.dryRun) {
94
+ return {
95
+ status: "planned",
96
+ action: existing ? "verify" : "create",
97
+ email: input.email,
98
+ name: input.name,
99
+ password: "<redacted>",
100
+ };
101
+ }
102
+
103
+ if (existing) {
104
+ await verifyExisting(dependencies, input, existing);
105
+ return { status: "existing", email: input.email };
106
+ }
107
+
108
+ if (options.checkOnly) {
109
+ throw new Error("The administrator account does not exist; no changes were made.");
110
+ }
111
+
112
+ await dependencies.createUser({
113
+ email: input.email,
114
+ password: input.password,
115
+ name: input.name,
116
+ role: "admin",
117
+ data: { emailVerified: true, signupApproved: true },
118
+ });
119
+
120
+ const created = await dependencies.findUser(input.email);
121
+ if (!created) throw new Error("The administrator account was not found after creation.");
122
+ await verifyExisting(dependencies, input, created);
123
+ return { status: "created", email: input.email };
124
+ }
125
+
126
+ async function main() {
127
+ if (has("--help") || has("-h")) {
128
+ usage();
129
+ return;
130
+ }
131
+
132
+ const input = resolveBootstrapInput();
133
+ // Load the auth graph before importing one of its dependencies directly.
134
+ // Concurrent ESM imports can ask Node to link the same Better Auth submodule
135
+ // through two graph roots and fail before the command reaches the database.
136
+ const { auth } = await import("../dist/auth/auth.js");
137
+ const { pool } = await import("../dist/auth/db.js");
138
+ const { verifyPassword } = await import("better-auth/crypto");
139
+
140
+ try {
141
+ const result = await bootstrapAdmin(
142
+ {
143
+ findUser: async (email) => {
144
+ const query = await pool.query(
145
+ `SELECT u.id,
146
+ u.email,
147
+ u.role,
148
+ u."emailVerified" AS "emailVerified",
149
+ u."signupApproved" AS "signupApproved",
150
+ a.password AS "passwordHash"
151
+ FROM "user" AS u
152
+ LEFT JOIN "account" AS a
153
+ ON a."userId" = u.id
154
+ AND a."providerId" = 'credential'
155
+ WHERE lower(u.email) = lower($1)
156
+ LIMIT 1`,
157
+ [email],
158
+ );
159
+ return query.rows[0] ?? null;
160
+ },
161
+ createUser: async (user) => auth.api.createUser({ body: user }),
162
+ verifyPassword: async (hash, password) => verifyPassword({ hash, password }),
163
+ },
164
+ input,
165
+ { dryRun: has("--dry-run"), checkOnly: has("--check-only") },
166
+ );
167
+
168
+ if (result.status === "planned") {
169
+ console.log("Validated administrator bootstrap plan.");
170
+ console.log(JSON.stringify(result, null, 2));
171
+ } else if (result.status === "created") {
172
+ console.log(`Created and verified administrator account ${result.email}.`);
173
+ } else {
174
+ console.log(`Verified existing administrator account ${result.email}.`);
175
+ }
176
+ } finally {
177
+ await pool.end();
178
+ }
179
+ }
180
+
181
+ const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
182
+ if (invokedPath === import.meta.url) {
183
+ main().catch((error) => {
184
+ console.error(error instanceof Error ? error.message : String(error));
185
+ process.exitCode = 1;
186
+ });
187
+ }
@@ -13,6 +13,13 @@
13
13
  "@types/qrcode": "^1.5.5",
14
14
  "@types/multer": "^1.4.12"
15
15
  },
16
+ "packageOverlays": {
17
+ "api": {
18
+ "scripts": {
19
+ "admin:bootstrap": "node scripts/bootstrap-admin.mjs"
20
+ }
21
+ }
22
+ },
16
23
  "env": [
17
24
  "# admin-dashboard: emails auto-promoted to the admin role on sign-up (comma-separated)",
18
25
  "ADMIN_EMAILS=admin@example.com"
@@ -42,7 +49,7 @@
42
49
  "file": "AGENTS.md",
43
50
  "marker": "<!-- podokit:end:agents-modules -->",
44
51
  "optional": true,
45
- "text": "### admin-dashboard\n- Admin console lives at `/admin/*` (users, sessions, organizations, audit, settings, account). Emails in `ADMIN_EMAILS` become admins on sign-up. When sign-up approval is enabled, self-registered users stay pending until an administrator approves them on `/admin/users`; administrator-created users bypass the queue.\n- **Every list/table MUST use the shared `$lib/components/data-table.svelte` (`DataTable`)** \u2014 it gives sortable headers + a footer pagination. Add search/filters with `$lib/components/table-toolbar.svelte`. Never assemble `Table.Root`/`Table.Header`/`Table.Body` by hand.\n"
52
+ "text": "### admin-dashboard\n- Admin console lives at `/admin/*` (users, sessions, organizations, audit, settings, account). Set `ADMIN_EMAILS`, then create or verify the initial account with `npm run admin:bootstrap -w <app>-api`; never commit or persist `ADMIN_BOOTSTRAP_PASSWORD`. When sign-up approval is enabled, self-registered users stay pending until an administrator approves them on `/admin/users`; administrator-created users bypass the queue.\n- **Every list/table MUST use the shared `$lib/components/data-table.svelte` (`DataTable`)** \u2014 it gives sortable headers + a footer pagination. Add search/filters with `$lib/components/table-toolbar.svelte`. Never assemble `Table.Root`/`Table.Header`/`Table.Body` by hand.\n"
46
53
  },
47
54
  {
48
55
  "file": "apps/api/src/app.module.ts",
@@ -56,9 +63,9 @@
56
63
  }
57
64
  ],
58
65
  "instructions": [
59
- "Set ADMIN_EMAILS in .env (comma-separated). Those emails become admins on sign-up.",
66
+ "Set ADMIN_EMAILS in .env (comma-separated). After building and migrating, inject ADMIN_BOOTSTRAP_EMAIL and ADMIN_BOOTSTRAP_PASSWORD for one command only, then run npm run admin:bootstrap -w <app>-api. The command creates a verified, approved admin or safely verifies an existing one.",
60
67
  "Re-run the auth migration to add the admin and sign-up approval columns: npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts",
61
- "Start the app (npm run dev), open /signup, register an ADMIN_EMAILS address, then sign in.",
68
+ "Remove ADMIN_BOOTSTRAP_PASSWORD from the environment after bootstrap. Keep ADMIN_EMAILS so matching future sign-ups are promoted consistently.",
62
69
  "Admin pages: /admin/users (list, approve pending registrations, ban, set role, revoke sessions). Signed-in users manage their profile and security at /account; /admin/account remains available in the admin shell. Admin, account, authentication, maintenance, and API responses include X-Robots-Tag: noindex, nofollow by default.",
63
70
  "Password reset emails go through the mailer module (installed automatically with auth); in dev without SMTP they are logged to the API console. Configure SMTP in admin settings or via SMTP_* env for production."
64
71
  ]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: podokit-configure-auth
3
- description: Configure and verify PodoKit social OAuth providers, SMTP delivery, and the administrator sign-up approval policy. Use when enabling Google, Apple, or another Better Auth provider; calculating callback URLs; applying credentials through Admin Settings; configuring outbound mail; or diagnosing why a configured sign-in method is unavailable.
3
+ description: Bootstrap and verify the initial PodoKit administrator, then configure social OAuth providers, SMTP delivery, and sign-up approval. Use when setting the first admin email/password; enabling Google, Apple, or another Better Auth provider; calculating callback URLs; applying credentials through Admin Settings; configuring outbound mail; or diagnosing unavailable sign-in methods.
4
4
  ---
5
5
 
6
6
  # Configure PodoKit authentication
@@ -12,6 +12,7 @@ Use the generated `auth:configure` command so credentials enter the encrypted `a
12
12
  - Read [references/google.md](references/google.md) for Google sign-in, including External audience requirements.
13
13
  - Read [references/apple.md](references/apple.md) for Apple sign-in and client-secret rotation.
14
14
  - Read [references/smtp.md](references/smtp.md) for Google Workspace SMTP relay and DNS authentication.
15
+ - Read [references/bootstrap-admin.md](references/bootstrap-admin.md) before creating or repairing access to the first administrator.
15
16
  - Run the same generated command for other provider IDs. Confirm the provider appears in Admin Settings before applying it.
16
17
 
17
18
  ## Prepare the app
@@ -24,7 +25,8 @@ Use the generated `auth:configure` command so credentials enter the encrypted `a
24
25
  npm run migrate:all -w <app>-api
25
26
  ```
26
27
 
27
- 4. Derive the default callback as `<BETTER_AUTH_URL>/api/auth/callback/<provider>`. Only set `OAUTH_REDIRECT_URI` when the provider requires an explicit override.
28
+ 4. Create or verify the initial administrator with `admin:bootstrap` before enabling mandatory email verification or other runtime authentication settings.
29
+ 5. Derive the default callback as `<BETTER_AUTH_URL>/api/auth/callback/<provider>`. Only set `OAUTH_REDIRECT_URI` when the provider requires an explicit override.
28
30
 
29
31
  ## Handle secrets safely
30
32
 
@@ -44,6 +46,17 @@ export OAUTH_CLIENT_SECRET
44
46
  export OAUTH_CLIENT_ID="provider-client-id"
45
47
  ```
46
48
 
49
+ The bootstrap command accepts the same `AUTH_SETUP_ADMIN_EMAIL` and
50
+ `AUTH_SETUP_ADMIN_PASSWORD` variables as fallbacks. Keep `ADMIN_EMAILS` in the
51
+ deployment environment, run the bootstrap once, and reuse the ephemeral
52
+ credentials for `auth:configure`:
53
+
54
+ ```bash
55
+ export ADMIN_EMAILS="admin@example.com"
56
+ npm run admin:bootstrap -w <app>-api -- --dry-run
57
+ npm run admin:bootstrap -w <app>-api
58
+ ```
59
+
47
60
  Unset sensitive variables when finished:
48
61
 
49
62
  ```bash
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "PodoKit Auth Setup"
3
- short_description: "Configure social login, SMTP, and signup approval"
4
- default_prompt: "Use $podokit-configure-auth to configure Google login, SMTP, and sign-up approval safely."
3
+ short_description: "Bootstrap admin and configure authentication safely"
4
+ default_prompt: "Use $podokit-configure-auth to bootstrap the first administrator, then configure Google login, SMTP, and sign-up approval safely."
@@ -0,0 +1,51 @@
1
+ # Bootstrap the initial administrator
2
+
3
+ Use the generated database-backed command after the API build and migrations.
4
+ Do not create the first administrator through the public sign-up page.
5
+
6
+ ## Set the persistent allowlist
7
+
8
+ Set `ADMIN_EMAILS` in the same deployment environment used by the API. Include
9
+ the exact lowercase-normalized address selected for the initial administrator.
10
+ Keep this value after bootstrap so future matching sign-ups retain the same role
11
+ policy.
12
+
13
+ ## Inject credentials for one command
14
+
15
+ Prefer the deployment secret manager or a hidden shell prompt. Do not put the
16
+ password in a command argument, repository file, image layer, CI log, or
17
+ long-lived Kubernetes Secret.
18
+
19
+ ```bash
20
+ export ADMIN_BOOTSTRAP_EMAIL="admin@example.com"
21
+ printf "Initial admin password: " >&2
22
+ IFS= read -r -s ADMIN_BOOTSTRAP_PASSWORD
23
+ printf "\n" >&2
24
+ export ADMIN_BOOTSTRAP_PASSWORD
25
+ ```
26
+
27
+ `AUTH_SETUP_ADMIN_EMAIL` and `AUTH_SETUP_ADMIN_PASSWORD` are accepted as
28
+ fallbacks so the same ephemeral values can immediately configure OAuth and SMTP.
29
+
30
+ ## Preview, create, and verify
31
+
32
+ ```bash
33
+ npm run admin:bootstrap -w <app>-api -- --dry-run
34
+ npm run admin:bootstrap -w <app>-api
35
+ npm run admin:bootstrap -w <app>-api -- --check-only
36
+ ```
37
+
38
+ The create path writes `role=admin`, `emailVerified=true`, and
39
+ `signupApproved=true`. A rerun never updates an existing row or resets a
40
+ password. It succeeds only when the account still has the admin role, verified
41
+ and approved flags, and the supplied password matches.
42
+
43
+ Unset the password immediately after `auth:configure` or the verification step:
44
+
45
+ ```bash
46
+ unset ADMIN_BOOTSTRAP_PASSWORD AUTH_SETUP_ADMIN_PASSWORD
47
+ ```
48
+
49
+ If the command reports an existing mismatched account, stop. Recover it through
50
+ an authenticated administrator or an audited operator procedure; do not delete,
51
+ promote, verify, or reset the row automatically.
@@ -3,6 +3,9 @@
3
3
  "description": "Full authentication (better-auth): email/password, sessions, and \u2014 via config \u2014 OAuth and 2FA. Installs a global guard so the API is secure by default; health and /api/auth stay public.",
4
4
  "requires": ["mailer"],
5
5
  "targetApp": "api",
6
+ "managedOverrides": [
7
+ ".claude/skills/podokit-configure-auth/**"
8
+ ],
6
9
  "dependencies": {
7
10
  "@podosoft/podokit-auth": "^0.2.0",
8
11
  "@podosoft/podokit-contracts": "^0.2.0",
@@ -20,6 +20,7 @@ RUN addgroup -S app && adduser -S app -G app
20
20
  COPY --from=build /app/node_modules ./node_modules
21
21
  COPY --from=deps /app/apps/api ./apps/api
22
22
  COPY --from=build /app/apps/api/dist ./apps/api/dist
23
+ COPY --from=build /app/apps/api/scripts ./apps/api/scripts
23
24
  WORKDIR /app/apps/api
24
25
  USER app
25
26
  EXPOSE 3000
@@ -83,6 +83,7 @@ services:
83
83
  # sync+restart is the stable, recommended approach; the web keeps instant
84
84
  # HMR via plain sync.
85
85
  - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
86
+ - { action: sync, path: ./apps/api/scripts, target: /app/apps/api/scripts }
86
87
  - { action: rebuild, path: ./apps/api/package.json }
87
88
 
88
89
  worker:
@@ -105,6 +106,7 @@ services:
105
106
  # sync+restart is the stable, recommended approach; the web keeps instant
106
107
  # HMR via plain sync.
107
108
  - { action: sync+restart, path: ./apps/api/src, target: /app/apps/api/src }
109
+ - { action: sync, path: ./apps/api/scripts, target: /app/apps/api/scripts }
108
110
  - { action: rebuild, path: ./apps/api/package.json }
109
111
 
110
112
  postgres:
package/dist/update.js CHANGED
@@ -20,6 +20,18 @@ function diskContent(projectRoot, path) {
20
20
  const abs = (0, node_path_1.join)(projectRoot, path);
21
21
  return (0, node_fs_1.existsSync)(abs) ? (0, node_fs_1.readFileSync)(abs) : null;
22
22
  }
23
+ function targetManagedOverrides(projectRoot, templatesDir, modules, current = []) {
24
+ const overrides = new Set(current);
25
+ const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
26
+ for (const module of modules) {
27
+ const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
28
+ if (!resolved)
29
+ continue;
30
+ for (const glob of (0, add_1.readModuleManifest)(resolved.dir).managedOverrides ?? [])
31
+ overrides.add(glob);
32
+ }
33
+ return [...overrides];
34
+ }
23
35
  /**
24
36
  * Build the update plan. `templatesDir` is the installed CLI's template set (the
25
37
  * new version); the lock records what PodoKit last wrote (to detect user edits).
@@ -36,15 +48,22 @@ function planUpdate(projectRoot, templatesDir) {
36
48
  modules: manifest.modules,
37
49
  projectRoot,
38
50
  });
51
+ const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, manifest.modules, manifest.managedOverrides);
39
52
  const changes = [];
40
53
  const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
41
54
  for (const path of [...paths].sort()) {
42
55
  const locked = lock.files[path];
43
56
  const newText = treeText(newTree, path);
44
57
  const disk = diskContent(projectRoot, path);
45
- // tier: prefer the lock; else classify the new file.
46
- const tier = locked?.tier ??
47
- (newText !== null ? (0, lockfile_1.classifyTier)(path, newText, manifest.ownedGlobs) : "managed");
58
+ // A newer module can explicitly take responsibility for a path that older
59
+ // projects classified under a broad owned glob (notably generated skills).
60
+ const managedByTarget = managedOverrides.some((glob) => (0, lockfile_1.matchGlob)(path, glob));
61
+ const tier = managedByTarget
62
+ ? "managed"
63
+ : locked?.tier ??
64
+ (newText !== null
65
+ ? (0, lockfile_1.classifyTier)(path, newText, manifest.ownedGlobs, managedOverrides)
66
+ : "managed");
48
67
  if (tier === "owned") {
49
68
  changes.push({ path, tier, action: "skip", note: "owned — never modified" });
50
69
  continue;
@@ -101,13 +120,15 @@ function writeFile(projectRoot, path, content) {
101
120
  * Files that are neither in the assembled tree nor explicitly owned stay out of
102
121
  * the lock instead of being accidentally adopted as managed files.
103
122
  */
104
- function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs) {
105
- const next = (0, lockfile_1.computeFilesLock)(projectRoot, ownedGlobs);
123
+ function updatedFilesLock(projectRoot, newTree, previous, ownedGlobs, managedOverrides) {
124
+ const next = (0, lockfile_1.computeFilesLock)(projectRoot, ownedGlobs, managedOverrides);
106
125
  for (const [path, entry] of Object.entries(next.files)) {
107
126
  const newText = treeText(newTree, path);
108
127
  if (newText !== null) {
109
128
  if (entry.tier !== "owned") {
110
- entry.tier = previous.files[path]?.tier ?? entry.tier;
129
+ if (!managedOverrides.some((glob) => (0, lockfile_1.matchGlob)(path, glob))) {
130
+ entry.tier = previous.files[path]?.tier ?? entry.tier;
131
+ }
111
132
  entry.outHash = (0, podokit_template_engine_1.hashContent)(newText);
112
133
  }
113
134
  continue;
@@ -149,6 +170,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
149
170
  modules,
150
171
  projectRoot,
151
172
  });
173
+ const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides);
152
174
  const oldTree = options.oldTemplatesDir
153
175
  ? (0, assemble_1.assembleProject)({
154
176
  templatesDir: options.oldTemplatesDir,
@@ -188,7 +210,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
188
210
  // Keep the assembled target as the managed baseline. A merged working file
189
211
  // intentionally remains drifted so a future update performs another 3-way
190
212
  // merge instead of treating the user's lines as clean generated output.
191
- (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, manifest.ownedGlobs));
213
+ (0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, manifest.ownedGlobs, managedOverrides));
192
214
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
193
215
  const refreshedModules = manifest.modules.map((module) => {
194
216
  const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
@@ -202,6 +224,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
202
224
  });
203
225
  (0, lockfile_1.writeManifest)(projectRoot, {
204
226
  ...manifest,
227
+ managedOverrides,
205
228
  modules: refreshedModules,
206
229
  podokitVersion: (0, lockfile_1.podokitVersion)(),
207
230
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
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": {