@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 +16 -0
- package/dist/add.js +63 -20
- package/dist/assemble.js +9 -9
- package/dist/lockfile.d.ts +5 -3
- package/dist/lockfile.js +17 -5
- package/dist/remove.js +26 -23
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/scripts/.keep +1 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +187 -0
- package/dist/templates/modules/admin-dashboard/module.manifest.json +10 -3
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/SKILL.md +15 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/agents/openai.yaml +2 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +51 -0
- package/dist/templates/modules/auth/module.manifest.json +3 -0
- package/dist/templates/todo/apps/api/Dockerfile +1 -0
- package/dist/templates/todo/apps/api/scripts/.keep +1 -0
- package/dist/templates/todo/compose.dev.yaml +2 -0
- package/dist/update.js +30 -7
- package/package.json +1 -1
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 =
|
|
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,
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
196
|
+
const manifest = readModuleManifest(moduleDir);
|
|
167
197
|
assertManifestVersion(module, manifest);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
206
|
-
|
|
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 (
|
|
209
|
-
overlay.dependencies =
|
|
210
|
-
if (
|
|
211
|
-
overlay.devDependencies =
|
|
212
|
-
if (
|
|
213
|
-
overlay.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/${
|
|
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 {
|
|
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
|
|
26
|
-
|
|
27
|
-
const pkgPath = `apps/${
|
|
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 (
|
|
31
|
-
overlay.dependencies =
|
|
32
|
-
if (
|
|
33
|
-
overlay.devDependencies =
|
|
34
|
-
if (
|
|
35
|
-
overlay.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)
|
package/dist/lockfile.d.ts
CHANGED
|
@@ -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] = {
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 (
|
|
152
|
-
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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).
|
|
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).
|
|
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
|
-
"
|
|
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:
|
|
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.
|
|
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: "
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|