@podosoft/podokit 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add.d.ts +20 -0
- package/dist/add.js +121 -8
- package/dist/assemble.d.ts +4 -1
- package/dist/assemble.js +35 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -4
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.js +5 -0
- package/dist/lockfile.d.ts +20 -1
- package/dist/lockfile.js +48 -6
- package/dist/remove.js +3 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +42 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +6 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +4 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +60 -1
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +730 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/appearance-card.svelte +371 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +12 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/apply-theme.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/themes.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +8 -717
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +17 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +0 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +4 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +56 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +9 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +13 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +21 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +14 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +12 -4
- package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
- package/dist/templates/modules/api-key-auth/files/tests/api/machine.api.spec.ts +25 -0
- package/dist/templates/modules/bullmq/files/tests/api/jobs.api.spec.ts +38 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +31 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +22 -0
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +28 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +35 -0
- package/dist/templates/modules/redis/files/tests/api/redis.api.spec.ts +30 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/todo/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +3 -1
- package/dist/templates/todo/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/todo/dot-gitignore +6 -0
- package/dist/templates/todo/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/todo/infra/traefik/dynamic.yml +4 -0
- package/dist/update.js +74 -8
- package/package.json +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +0 -61
package/dist/add.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface ModuleManifest {
|
|
|
26
26
|
* they survive lock recompute. Use for public presentation pages a consumer
|
|
27
27
|
* restyles, while keeping the module's `$lib` logic managed. */
|
|
28
28
|
ownedGlobs?: string[];
|
|
29
|
+
/** Paths that an existing app may explicitly hand back to this module with
|
|
30
|
+
* `podo add --adopt`. Broad app-owned route globs are never removed. */
|
|
31
|
+
managedGlobs?: string[];
|
|
29
32
|
}
|
|
30
33
|
export interface AddOptions {
|
|
31
34
|
projectRoot: string;
|
|
@@ -33,6 +36,8 @@ export interface AddOptions {
|
|
|
33
36
|
modulesDir: string;
|
|
34
37
|
/** PodoKit version stamped into the lockfile. Defaults to the CLI version. */
|
|
35
38
|
podokitVersion?: string;
|
|
39
|
+
/** Adopt colliding files covered by the module's managedGlobs. */
|
|
40
|
+
adopt?: boolean;
|
|
36
41
|
}
|
|
37
42
|
export interface AddResult {
|
|
38
43
|
module: string;
|
|
@@ -41,6 +46,18 @@ export interface AddResult {
|
|
|
41
46
|
added: string[];
|
|
42
47
|
/** ownedGlobs declared by this module and every module it pulled in. */
|
|
43
48
|
ownedGlobs: string[];
|
|
49
|
+
/** Existing owned presentation files intentionally left untouched. */
|
|
50
|
+
preserved: string[];
|
|
51
|
+
/** Paths explicitly adopted as module-managed files. */
|
|
52
|
+
adopted: string[];
|
|
53
|
+
/** Files shipped, merged, or injected while applying the module. */
|
|
54
|
+
touched: string[];
|
|
55
|
+
}
|
|
56
|
+
export interface ResolvedModule {
|
|
57
|
+
dir: string;
|
|
58
|
+
name: string;
|
|
59
|
+
packageName?: string;
|
|
60
|
+
moduleVersion?: string;
|
|
44
61
|
}
|
|
45
62
|
/**
|
|
46
63
|
* Resolve a module's directory by name: first the bundled `modulesDir`, then a
|
|
@@ -48,6 +65,9 @@ export interface AddResult {
|
|
|
48
65
|
* project (or a fully-qualified `@scope/pkg` name). Returns null if not found.
|
|
49
66
|
*/
|
|
50
67
|
export declare function resolveModuleDir(name: string, modulesDir: string, projectRoot: string): string | null;
|
|
68
|
+
/** Resolve a module and capture the package identity needed to replay external
|
|
69
|
+
* modules during `podo update`. */
|
|
70
|
+
export declare function resolveModule(name: string, modulesDir: string, projectRoot: string): ResolvedModule | null;
|
|
51
71
|
/** List modules available to a project: the bundled ones under `modulesDir` plus,
|
|
52
72
|
* when `projectRoot` is given, any `@podosoft/podokit-module-*` packages installed
|
|
53
73
|
* in it. Bundled names take precedence on a clash. */
|
package/dist/add.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SUPPORTED_MANIFEST_VERSION = void 0;
|
|
4
4
|
exports.resolveModuleDir = resolveModuleDir;
|
|
5
|
+
exports.resolveModule = resolveModule;
|
|
5
6
|
exports.listModules = listModules;
|
|
6
7
|
exports.addModule = addModule;
|
|
7
8
|
const node_fs_1 = require("node:fs");
|
|
@@ -31,6 +32,27 @@ function resolveModuleDir(name, modulesDir, projectRoot) {
|
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
/** Resolve a module and capture the package identity needed to replay external
|
|
36
|
+
* modules during `podo update`. */
|
|
37
|
+
function resolveModule(name, modulesDir, projectRoot) {
|
|
38
|
+
const dir = resolveModuleDir(name, modulesDir, projectRoot);
|
|
39
|
+
if (!dir)
|
|
40
|
+
return null;
|
|
41
|
+
const manifest = readManifest(dir);
|
|
42
|
+
const bundled = (0, node_path_1.join)(modulesDir, manifest.name);
|
|
43
|
+
if (dir === bundled)
|
|
44
|
+
return { dir, name: manifest.name };
|
|
45
|
+
const packageFile = (0, node_path_1.join)(dir, "package.json");
|
|
46
|
+
if (!(0, node_fs_1.existsSync)(packageFile))
|
|
47
|
+
return { dir, name: manifest.name };
|
|
48
|
+
const pkg = readJson(packageFile);
|
|
49
|
+
return {
|
|
50
|
+
dir,
|
|
51
|
+
name: manifest.name,
|
|
52
|
+
packageName: typeof pkg.name === "string" ? pkg.name : undefined,
|
|
53
|
+
moduleVersion: typeof pkg.version === "string" ? pkg.version : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
34
56
|
function assertManifestVersion(name, manifest) {
|
|
35
57
|
if ((manifest.manifestVersion ?? exports.SUPPORTED_MANIFEST_VERSION) > exports.SUPPORTED_MANIFEST_VERSION) {
|
|
36
58
|
throw new Error(`Module "${name}" needs a newer PodoKit (manifest v${manifest.manifestVersion}, this CLI supports v${exports.SUPPORTED_MANIFEST_VERSION}). Update @podosoft/podokit.`);
|
|
@@ -105,18 +127,42 @@ function isApplied(projectRoot, modulesDir, module) {
|
|
|
105
127
|
* and inject wiring at markers. Missing required modules are added first.
|
|
106
128
|
*/
|
|
107
129
|
function addModule(options) {
|
|
108
|
-
const
|
|
130
|
+
const previousLock = (0, lockfile_1.readFilesLock)(options.projectRoot);
|
|
131
|
+
const cleanPaths = previousLock
|
|
132
|
+
? Object.entries(previousLock.files)
|
|
133
|
+
.filter(([path, entry]) => {
|
|
134
|
+
const target = (0, node_path_1.join)(options.projectRoot, path);
|
|
135
|
+
return (0, node_fs_1.existsSync)(target) && (0, podokit_template_engine_1.hashContent)((0, node_fs_1.readFileSync)(target)) === entry.outHash;
|
|
136
|
+
})
|
|
137
|
+
.map(([path]) => path)
|
|
138
|
+
: [];
|
|
139
|
+
const result = applyModule(options.projectRoot, options.module, options.modulesDir, new Set(), options.adopt ?? false);
|
|
140
|
+
if (options.adopt && result.adopted.length) {
|
|
141
|
+
releaseOwnedPaths(options.projectRoot, result.adopted);
|
|
142
|
+
}
|
|
109
143
|
// Record the module (and any auto-added requirements), fold in the modules'
|
|
110
144
|
// declared ownedGlobs, and refresh the lock.
|
|
111
|
-
|
|
145
|
+
const records = [...result.added, result.module]
|
|
146
|
+
.map((name) => resolveModule(name, options.modulesDir, options.projectRoot))
|
|
147
|
+
.filter((module) => module !== null)
|
|
148
|
+
.map(moduleRecord);
|
|
149
|
+
(0, lockfile_1.recordModules)(options.projectRoot, records, options.podokitVersion, result.ownedGlobs, previousLock
|
|
150
|
+
? {
|
|
151
|
+
previous: previousLock,
|
|
152
|
+
cleanPaths,
|
|
153
|
+
modulePaths: [...new Set(result.touched)],
|
|
154
|
+
adoptedPaths: result.adopted,
|
|
155
|
+
}
|
|
156
|
+
: undefined);
|
|
112
157
|
return result;
|
|
113
158
|
}
|
|
114
|
-
function applyModule(projectRoot, module, modulesDir, applied) {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
159
|
+
function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
160
|
+
const resolved = resolveModule(module, modulesDir, projectRoot);
|
|
161
|
+
if (!resolved) {
|
|
117
162
|
const available = listModules(modulesDir, projectRoot).map((m) => m.name);
|
|
118
163
|
throw new Error(`Unknown module "${module}".${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
|
|
119
164
|
}
|
|
165
|
+
const moduleDir = resolved.dir;
|
|
120
166
|
const manifest = readManifest(moduleDir);
|
|
121
167
|
assertManifestVersion(module, manifest);
|
|
122
168
|
const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", manifest.targetApp, "package.json");
|
|
@@ -127,19 +173,34 @@ function applyModule(projectRoot, module, modulesDir, applied) {
|
|
|
127
173
|
// 0) apply required modules first (auto-add if missing)
|
|
128
174
|
const added = [];
|
|
129
175
|
const ownedGlobs = [...(manifest.ownedGlobs ?? [])];
|
|
176
|
+
const preserved = [];
|
|
177
|
+
const adopted = [];
|
|
178
|
+
const touched = [];
|
|
130
179
|
for (const required of manifest.requires ?? []) {
|
|
131
180
|
if (applied.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
132
181
|
continue;
|
|
133
|
-
const result = applyModule(projectRoot, required, modulesDir, applied);
|
|
182
|
+
const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
|
|
134
183
|
added.push(required, ...result.added);
|
|
135
184
|
ownedGlobs.push(...result.ownedGlobs);
|
|
185
|
+
preserved.push(...result.preserved);
|
|
186
|
+
adopted.push(...result.adopted);
|
|
187
|
+
touched.push(...result.touched);
|
|
136
188
|
}
|
|
137
189
|
const appName = projectName(projectRoot);
|
|
138
190
|
const vars = { projectName: appName };
|
|
139
191
|
// 1) overlay files
|
|
140
192
|
const filesDir = (0, node_path_1.join)(moduleDir, "files");
|
|
141
193
|
if ((0, node_fs_1.existsSync)(filesDir)) {
|
|
142
|
-
|
|
194
|
+
const copied = copyModuleFiles({
|
|
195
|
+
filesDir,
|
|
196
|
+
projectRoot,
|
|
197
|
+
vars,
|
|
198
|
+
managedGlobs: manifest.managedGlobs ?? [],
|
|
199
|
+
adopt,
|
|
200
|
+
});
|
|
201
|
+
preserved.push(...copied.preserved);
|
|
202
|
+
adopted.push(...copied.adopted);
|
|
203
|
+
touched.push(...copied.touched);
|
|
143
204
|
}
|
|
144
205
|
// 2) merge dependencies and scripts into the target app
|
|
145
206
|
if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
|
|
@@ -152,10 +213,12 @@ function applyModule(projectRoot, module, modulesDir, applied) {
|
|
|
152
213
|
overlay.scripts = manifest.scripts;
|
|
153
214
|
const merged = (0, podokit_template_engine_1.mergePackageJson)(readJson(appPkgPath), overlay);
|
|
154
215
|
(0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
216
|
+
touched.push(`apps/${manifest.targetApp}/package.json`);
|
|
155
217
|
}
|
|
156
218
|
// 3) append env example lines
|
|
157
219
|
if (manifest.env?.length) {
|
|
158
220
|
appendEnv(projectRoot, manifest.env);
|
|
221
|
+
touched.push(".env.example");
|
|
159
222
|
}
|
|
160
223
|
// 4) inject wiring at markers
|
|
161
224
|
for (const injection of manifest.inject ?? []) {
|
|
@@ -169,7 +232,57 @@ function applyModule(projectRoot, module, modulesDir, applied) {
|
|
|
169
232
|
if (injection.optional && !content.includes(injection.marker))
|
|
170
233
|
continue;
|
|
171
234
|
(0, node_fs_1.writeFileSync)(target, (0, podokit_template_engine_1.insertAtMarker)(content, injection.marker, injection.text));
|
|
235
|
+
touched.push(injection.file);
|
|
172
236
|
}
|
|
173
237
|
const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
|
|
174
|
-
return { module, instructions, added, ownedGlobs };
|
|
238
|
+
return { module: manifest.name, instructions, added, ownedGlobs, preserved, adopted, touched };
|
|
239
|
+
}
|
|
240
|
+
function moduleRecord(module) {
|
|
241
|
+
return {
|
|
242
|
+
name: module.name,
|
|
243
|
+
packageName: module.packageName,
|
|
244
|
+
moduleVersion: module.moduleVersion,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Copy a module overlay without clobbering app-owned presentation files. */
|
|
248
|
+
function copyModuleFiles(options) {
|
|
249
|
+
const tree = (0, podokit_template_engine_1.renderTemplate)(options.filesDir, options.vars);
|
|
250
|
+
const lock = (0, lockfile_1.readFilesLock)(options.projectRoot);
|
|
251
|
+
const preserved = [];
|
|
252
|
+
const adopted = [];
|
|
253
|
+
const touched = [];
|
|
254
|
+
for (const [rel, file] of tree) {
|
|
255
|
+
touched.push(rel);
|
|
256
|
+
const target = (0, node_path_1.join)(options.projectRoot, rel);
|
|
257
|
+
const existed = (0, node_fs_1.existsSync)(target);
|
|
258
|
+
const entry = lock?.files[rel];
|
|
259
|
+
const canAdopt = options.managedGlobs.some((glob) => (0, lockfile_1.matchGlob)(rel, glob));
|
|
260
|
+
if (existed && entry?.tier === "owned" && !(options.adopt && canAdopt)) {
|
|
261
|
+
preserved.push(rel);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (existed && !entry && !(options.adopt && canAdopt)) {
|
|
265
|
+
const current = (0, node_fs_1.readFileSync)(target);
|
|
266
|
+
const next = Buffer.isBuffer(file.content) ? file.content : Buffer.from(file.content);
|
|
267
|
+
if (!current.equals(next)) {
|
|
268
|
+
throw new Error(`Cannot add module: ${rel} already exists outside PodoKit ownership. ` +
|
|
269
|
+
"Move it, or re-run with --adopt when the module declares the path managed.");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
273
|
+
(0, node_fs_1.writeFileSync)(target, file.content);
|
|
274
|
+
if (existed && options.adopt && canAdopt)
|
|
275
|
+
adopted.push(rel);
|
|
276
|
+
}
|
|
277
|
+
return { preserved, adopted, touched };
|
|
278
|
+
}
|
|
279
|
+
/** Remove only exact owned paths that an explicit adoption handed back. Broad
|
|
280
|
+
* ownership rules such as `apps/web/src/routes/**` remain intact. */
|
|
281
|
+
function releaseOwnedPaths(projectRoot, adopted) {
|
|
282
|
+
const manifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
283
|
+
if (!manifest)
|
|
284
|
+
return;
|
|
285
|
+
const adoptedSet = new Set(adopted);
|
|
286
|
+
manifest.ownedGlobs = manifest.ownedGlobs.filter((glob) => glob.includes("*") || !adoptedSet.has(glob));
|
|
287
|
+
(0, lockfile_1.writeManifest)(projectRoot, manifest);
|
|
175
288
|
}
|
package/dist/assemble.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TemplateVars, type VfsTree } from "@podosoft/podokit-template-engine";
|
|
2
|
+
import type { ManifestModule } from "./lockfile";
|
|
2
3
|
/**
|
|
3
4
|
* Reconstruct a generated project in memory (no disk writes) from a template
|
|
4
5
|
* plus an ordered module list, mirroring `create` + `add`. `podo update` uses
|
|
@@ -8,7 +9,9 @@ export interface AssembleOptions {
|
|
|
8
9
|
templatesDir: string;
|
|
9
10
|
template: string;
|
|
10
11
|
answers: TemplateVars;
|
|
11
|
-
modules: string[];
|
|
12
|
+
modules: (string | ManifestModule)[];
|
|
13
|
+
/** Generated project whose node_modules may contain external modules. */
|
|
14
|
+
projectRoot?: string;
|
|
12
15
|
}
|
|
13
16
|
/** Assemble the project tree for a template + ordered modules, in memory. */
|
|
14
17
|
export declare function assembleProject(options: AssembleOptions): VfsTree;
|
package/dist/assemble.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.assembleProject = assembleProject;
|
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
7
|
+
const add_1 = require("./add");
|
|
7
8
|
function readManifest(moduleDir) {
|
|
8
9
|
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8"));
|
|
9
10
|
}
|
|
@@ -53,11 +54,44 @@ function applyModuleToTree(tree, moduleDir, vars) {
|
|
|
53
54
|
setText(tree, target, (0, podokit_template_engine_1.insertAtMarker)(content, injection.marker, injection.text));
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
function preserveExternalPackageDependency(tree, projectRoot, packageName) {
|
|
58
|
+
const diskPackagePath = (0, node_path_1.join)(projectRoot, "package.json");
|
|
59
|
+
if (!(0, node_fs_1.existsSync)(diskPackagePath))
|
|
60
|
+
return;
|
|
61
|
+
const diskPackage = JSON.parse((0, node_fs_1.readFileSync)(diskPackagePath, "utf8"));
|
|
62
|
+
const rootPackage = JSON.parse(textOf(tree, "package.json") || "{}");
|
|
63
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
64
|
+
const diskSection = diskPackage[section];
|
|
65
|
+
if (!diskSection || typeof diskSection !== "object" || Array.isArray(diskSection))
|
|
66
|
+
continue;
|
|
67
|
+
const version = diskSection[packageName];
|
|
68
|
+
if (typeof version !== "string")
|
|
69
|
+
continue;
|
|
70
|
+
const current = rootPackage[section];
|
|
71
|
+
const dependencies = current && typeof current === "object" && !Array.isArray(current)
|
|
72
|
+
? { ...current }
|
|
73
|
+
: {};
|
|
74
|
+
dependencies[packageName] = version;
|
|
75
|
+
rootPackage[section] = dependencies;
|
|
76
|
+
setText(tree, "package.json", `${JSON.stringify(rootPackage, null, 2)}\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
56
80
|
/** Assemble the project tree for a template + ordered modules, in memory. */
|
|
57
81
|
function assembleProject(options) {
|
|
58
82
|
const tree = (0, podokit_template_engine_1.renderTemplate)((0, node_path_1.join)(options.templatesDir, options.template), options.answers);
|
|
59
83
|
for (const mod of options.modules) {
|
|
60
|
-
|
|
84
|
+
const name = typeof mod === "string" ? mod : (mod.packageName ?? mod.name);
|
|
85
|
+
const resolved = (0, add_1.resolveModule)(name, (0, node_path_1.join)(options.templatesDir, "modules"), options.projectRoot ?? options.templatesDir);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
const label = typeof mod === "string" ? mod : mod.name;
|
|
88
|
+
throw new Error(`Cannot resolve module "${label}" while assembling the project. ` +
|
|
89
|
+
"Install its package before running podo update.");
|
|
90
|
+
}
|
|
91
|
+
applyModuleToTree(tree, resolved.dir, options.answers);
|
|
92
|
+
if (typeof mod !== "string" && mod.packageName && options.projectRoot) {
|
|
93
|
+
preserveExternalPackageDependency(tree, options.projectRoot, mod.packageName);
|
|
94
|
+
}
|
|
61
95
|
}
|
|
62
96
|
return tree;
|
|
63
97
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const HELP = `podo — PodoKit project generator
|
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
18
|
podo create <name> [options]
|
|
19
|
-
podo add <module>
|
|
19
|
+
podo add <module> [--adopt]
|
|
20
20
|
podo remove <module> Un-apply a module (inverse of add)
|
|
21
21
|
podo status Show version, modules, file tiers, and local edits
|
|
22
22
|
podo diff List PodoKit-managed files you have edited
|
|
@@ -28,6 +28,7 @@ Options:
|
|
|
28
28
|
--template <t> Template to scaffold (see below)
|
|
29
29
|
--dir <path> Target directory (default: ./<name>)
|
|
30
30
|
--pm <name> Package manager: npm | pnpm | yarn (default: npm)
|
|
31
|
+
--adopt Adopt colliding paths explicitly declared managed by a module
|
|
31
32
|
--no-ai Skip AI agent guidance (AGENTS.md, CLAUDE.md, editor rules)
|
|
32
33
|
-y, --yes Skip prompts and accept defaults
|
|
33
34
|
-h, --help Show this help
|
|
@@ -41,7 +42,7 @@ Example:
|
|
|
41
42
|
cd my-app && npx @podosoft/podokit add auth
|
|
42
43
|
`;
|
|
43
44
|
function parseArgs(argv) {
|
|
44
|
-
const parsed = { help: false, yes: false, apply: false, ai: true };
|
|
45
|
+
const parsed = { help: false, yes: false, apply: false, adopt: false, ai: true };
|
|
45
46
|
const positionals = [];
|
|
46
47
|
for (let i = 0; i < argv.length; i += 1) {
|
|
47
48
|
const arg = argv[i];
|
|
@@ -57,6 +58,9 @@ function parseArgs(argv) {
|
|
|
57
58
|
else if (arg === "--apply") {
|
|
58
59
|
parsed.apply = true;
|
|
59
60
|
}
|
|
61
|
+
else if (arg === "--adopt") {
|
|
62
|
+
parsed.adopt = true;
|
|
63
|
+
}
|
|
60
64
|
else if (arg === "--template") {
|
|
61
65
|
parsed.template = argv[++i];
|
|
62
66
|
}
|
|
@@ -99,11 +103,22 @@ async function main(argv) {
|
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
try {
|
|
102
|
-
const result = (0, add_1.addModule)({
|
|
106
|
+
const result = (0, add_1.addModule)({
|
|
107
|
+
projectRoot: process.cwd(),
|
|
108
|
+
module: moduleName,
|
|
109
|
+
modulesDir,
|
|
110
|
+
adopt: args.adopt,
|
|
111
|
+
});
|
|
103
112
|
if (result.added.length) {
|
|
104
113
|
process.stdout.write(`\nAlso added required module(s): ${result.added.join(", ")}\n`);
|
|
105
114
|
}
|
|
106
115
|
process.stdout.write(`\nAdded ${result.module}.\n`);
|
|
116
|
+
if (result.preserved.length) {
|
|
117
|
+
process.stdout.write(`\nPreserved app-owned presentation file(s):\n${result.preserved.map((file) => ` ${file}`).join("\n")}\n`);
|
|
118
|
+
}
|
|
119
|
+
if (result.adopted.length) {
|
|
120
|
+
process.stdout.write(`\nAdopted as module-managed file(s):\n${result.adopted.map((file) => ` ${file}`).join("\n")}\n`);
|
|
121
|
+
}
|
|
107
122
|
if (result.instructions.length) {
|
|
108
123
|
process.stdout.write(`\nNext steps:\n${result.instructions.map((i) => ` ${i}`).join("\n")}\n`);
|
|
109
124
|
}
|
|
@@ -140,8 +155,9 @@ async function main(argv) {
|
|
|
140
155
|
try {
|
|
141
156
|
const s = (0, inspect_1.status)(process.cwd());
|
|
142
157
|
const tiers = `managed ${s.tiers.managed}, assembled ${s.tiers.assembled}, owned ${s.tiers.owned}`;
|
|
158
|
+
const modules = s.moduleDetails.map((module) => module.version ? `${module.name}@${module.version}` : module.name);
|
|
143
159
|
process.stdout.write(`PodoKit ${s.podokitVersion} (template: ${s.template}, ${s.packageManager})\n` +
|
|
144
|
-
`Modules: ${
|
|
160
|
+
`Modules: ${modules.length ? modules.join(", ") : "(none)"}\n` +
|
|
145
161
|
`Files: ${tiers}\n` +
|
|
146
162
|
`Edited: ${s.drifted.length} managed file(s)${s.missing.length ? `, ${s.missing.length} missing` : ""}\n` +
|
|
147
163
|
(s.drifted.length ? s.drifted.map((f) => ` ~ ${f}`).join("\n") + "\n" : ""));
|
package/dist/inspect.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface StatusReport {
|
|
|
8
8
|
template: string;
|
|
9
9
|
packageManager: string;
|
|
10
10
|
modules: string[];
|
|
11
|
+
moduleDetails: {
|
|
12
|
+
name: string;
|
|
13
|
+
packageName?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
}[];
|
|
11
16
|
tiers: Record<Tier, number>;
|
|
12
17
|
drifted: string[];
|
|
13
18
|
missing: string[];
|
package/dist/inspect.js
CHANGED
|
@@ -29,6 +29,11 @@ function status(projectRoot) {
|
|
|
29
29
|
template: manifest.template,
|
|
30
30
|
packageManager: manifest.packageManager,
|
|
31
31
|
modules: manifest.modules.map((m) => m.name),
|
|
32
|
+
moduleDetails: manifest.modules.map((m) => ({
|
|
33
|
+
name: m.name,
|
|
34
|
+
packageName: m.packageName,
|
|
35
|
+
version: m.moduleVersion,
|
|
36
|
+
})),
|
|
32
37
|
tiers,
|
|
33
38
|
drifted,
|
|
34
39
|
missing,
|
package/dist/lockfile.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export interface ManifestModule {
|
|
|
12
12
|
order: number;
|
|
13
13
|
/** PodoKit version that applied the module. */
|
|
14
14
|
addedWith: string;
|
|
15
|
+
/** npm package backing an external module. Absent for bundled modules. */
|
|
16
|
+
packageName?: string;
|
|
17
|
+
/** Installed external-module version last applied to this project. */
|
|
18
|
+
moduleVersion?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ManifestModuleInput {
|
|
21
|
+
name: string;
|
|
22
|
+
packageName?: string;
|
|
23
|
+
moduleVersion?: string;
|
|
15
24
|
}
|
|
16
25
|
export interface PodokitManifest {
|
|
17
26
|
schemaVersion: number;
|
|
@@ -33,6 +42,16 @@ export interface FilesLock {
|
|
|
33
42
|
schemaVersion: number;
|
|
34
43
|
files: Record<string, FileEntry>;
|
|
35
44
|
}
|
|
45
|
+
export interface RecordModulesLockSnapshot {
|
|
46
|
+
/** Lock state captured before module files are written. */
|
|
47
|
+
previous: FilesLock;
|
|
48
|
+
/** Previously locked paths whose working bytes still matched their baseline. */
|
|
49
|
+
cleanPaths: string[];
|
|
50
|
+
/** Files shipped, merged, or injected by the module application. */
|
|
51
|
+
modulePaths: string[];
|
|
52
|
+
/** Owned paths explicitly handed back with `podo add --adopt`. */
|
|
53
|
+
adoptedPaths: string[];
|
|
54
|
+
}
|
|
36
55
|
/** Default user-owned globs seeded into a new project. See ADR-0010. */
|
|
37
56
|
export declare const DEFAULT_OWNED_GLOBS: string[];
|
|
38
57
|
export declare function manifestPath(projectRoot: string): string;
|
|
@@ -82,4 +101,4 @@ export declare function mergeOwnedGlobs(existing: string[], add: string[]): stri
|
|
|
82
101
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
83
102
|
* survive every future recompute. No-op on projects without a manifest.
|
|
84
103
|
*/
|
|
85
|
-
export declare function recordModules(projectRoot: string,
|
|
104
|
+
export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot): void;
|
package/dist/lockfile.js
CHANGED
|
@@ -209,20 +209,62 @@ function mergeOwnedGlobs(existing, add) {
|
|
|
209
209
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
210
210
|
* survive every future recompute. No-op on projects without a manifest.
|
|
211
211
|
*/
|
|
212
|
-
function recordModules(projectRoot,
|
|
212
|
+
function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot) {
|
|
213
213
|
const manifest = readManifest(projectRoot);
|
|
214
214
|
if (!manifest)
|
|
215
215
|
return;
|
|
216
216
|
const stampedWith = version ?? podokitVersion();
|
|
217
217
|
const known = new Set(manifest.modules.map((m) => m.name));
|
|
218
|
-
for (const
|
|
219
|
-
|
|
218
|
+
for (const input of moduleInputs) {
|
|
219
|
+
const module = typeof input === "string" ? { name: input } : input;
|
|
220
|
+
const existing = manifest.modules.find((entry) => entry.name === module.name);
|
|
221
|
+
if (existing) {
|
|
222
|
+
existing.packageName = module.packageName ?? existing.packageName;
|
|
223
|
+
existing.moduleVersion = module.moduleVersion ?? existing.moduleVersion;
|
|
220
224
|
continue;
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
}
|
|
226
|
+
manifest.modules.push({
|
|
227
|
+
name: module.name,
|
|
228
|
+
order: manifest.modules.length,
|
|
229
|
+
addedWith: stampedWith,
|
|
230
|
+
packageName: module.packageName,
|
|
231
|
+
moduleVersion: module.moduleVersion,
|
|
232
|
+
});
|
|
233
|
+
known.add(module.name);
|
|
223
234
|
}
|
|
224
235
|
if (ownedGlobs?.length)
|
|
225
236
|
manifest.ownedGlobs = mergeOwnedGlobs(manifest.ownedGlobs, ownedGlobs);
|
|
226
237
|
writeManifest(projectRoot, manifest);
|
|
227
|
-
|
|
238
|
+
const computed = computeFilesLock(projectRoot, manifest.ownedGlobs);
|
|
239
|
+
if (!lockSnapshot) {
|
|
240
|
+
writeFilesLock(projectRoot, computed);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const clean = new Set(lockSnapshot.cleanPaths);
|
|
244
|
+
const modulePaths = new Set(lockSnapshot.modulePaths);
|
|
245
|
+
const adopted = new Set(lockSnapshot.adoptedPaths);
|
|
246
|
+
const files = {};
|
|
247
|
+
for (const [path, previous] of Object.entries(lockSnapshot.previous.files)) {
|
|
248
|
+
const current = computed.files[path];
|
|
249
|
+
if (!current) {
|
|
250
|
+
// Preserve missing-file drift that existed before the add operation.
|
|
251
|
+
files[path] = previous;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (current.tier === "owned" || adopted.has(path) || clean.has(path)) {
|
|
255
|
+
files[path] = current;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Adding a module must not teach the lock that pre-existing local edits
|
|
259
|
+
// are generated output. Keep the old baseline so update stays safe.
|
|
260
|
+
files[path] = previous;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const path of modulePaths) {
|
|
264
|
+
const current = computed.files[path];
|
|
265
|
+
if (!current || files[path])
|
|
266
|
+
continue;
|
|
267
|
+
files[path] = current;
|
|
268
|
+
}
|
|
269
|
+
writeFilesLock(projectRoot, { schemaVersion: exports.LOCK_SCHEMA_VERSION, files });
|
|
228
270
|
}
|
package/dist/remove.js
CHANGED
|
@@ -42,7 +42,8 @@ function removeModule(options) {
|
|
|
42
42
|
const installed = manifest.modules.map((m) => m.name).join(", ") || "(none)";
|
|
43
43
|
throw new Error(`Module "${module}" is not installed. Installed: ${installed}.`);
|
|
44
44
|
}
|
|
45
|
-
const
|
|
45
|
+
const installedModule = manifest.modules.find((entry) => entry.name === module);
|
|
46
|
+
const moduleDir = (0, add_1.resolveModuleDir)(installedModule?.packageName ?? module, modulesDir, projectRoot);
|
|
46
47
|
if (!moduleDir) {
|
|
47
48
|
throw new Error(`Cannot resolve module "${module}" to un-apply it. Is it still installed/bundled?`);
|
|
48
49
|
}
|
|
@@ -52,7 +53,7 @@ function removeModule(options) {
|
|
|
52
53
|
const others = manifest.modules
|
|
53
54
|
.filter((m) => m.name !== module)
|
|
54
55
|
.map((m) => {
|
|
55
|
-
const dir = (0, add_1.resolveModuleDir)(m.name, modulesDir, projectRoot);
|
|
56
|
+
const dir = (0, add_1.resolveModuleDir)(m.packageName ?? m.name, modulesDir, projectRoot);
|
|
56
57
|
return dir ? { name: m.name, dir, manifest: readModuleManifest(dir) } : null;
|
|
57
58
|
})
|
|
58
59
|
.filter((m) => m !== null);
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
--border: oklch(0.92 0.004 286.32);
|
|
24
24
|
--input: oklch(0.92 0.004 286.32);
|
|
25
25
|
--ring: oklch(0.705 0.015 286.067);
|
|
26
|
+
--sidebar: oklch(0.985 0 0);
|
|
27
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
28
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
29
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
30
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
31
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
33
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
.dark {
|
|
@@ -44,6 +52,14 @@
|
|
|
44
52
|
--border: oklch(1 0 0 / 10%);
|
|
45
53
|
--input: oklch(1 0 0 / 15%);
|
|
46
54
|
--ring: oklch(0.552 0.016 285.938);
|
|
55
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
56
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
57
|
+
--sidebar-primary: oklch(0.92 0.004 286.32);
|
|
58
|
+
--sidebar-primary-foreground: oklch(0.21 0.006 285.885);
|
|
59
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
60
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
61
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
62
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
@theme inline {
|
|
@@ -69,6 +85,14 @@
|
|
|
69
85
|
--color-border: var(--border);
|
|
70
86
|
--color-input: var(--input);
|
|
71
87
|
--color-ring: var(--ring);
|
|
88
|
+
--color-sidebar: var(--sidebar);
|
|
89
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
90
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
91
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
92
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
93
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
94
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
95
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
@layer base {
|
|
@@ -79,3 +103,21 @@
|
|
|
79
103
|
@apply bg-background text-foreground;
|
|
80
104
|
}
|
|
81
105
|
}
|
|
106
|
+
|
|
107
|
+
/* shadcn-svelte sidebar active-state correction.
|
|
108
|
+
* Upstream (shadcn-svelte registry, `nova` style) styles the active item with
|
|
109
|
+
* the `data-active:` Tailwind variant, which matches attribute PRESENCE
|
|
110
|
+
* (`[data-active]`). The menu buttons always render `data-active="true|false"`,
|
|
111
|
+
* so the active look (accent background, accent text, medium weight) leaks onto
|
|
112
|
+
* EVERY item — nearly invisible in the default palette but glaring once a theme
|
|
113
|
+
* raises the contrast. We keep `ui/sidebar/*` as a pristine mirror of upstream
|
|
114
|
+
* (so `shadcn-svelte` updates apply cleanly) and correct the behaviour here:
|
|
115
|
+
* neutralize the resting state of non-active items, leaving hover/press feedback
|
|
116
|
+
* (their `:hover`/`:active` rules) untouched. Drop this once shadcn-svelte ships
|
|
117
|
+
* value-matching `data-[active=true]:` (canonical shadcn/ui already does). */
|
|
118
|
+
[data-sidebar="menu-button"][data-active="false"]:not(:hover):not(:active),
|
|
119
|
+
[data-sidebar="menu-sub-button"][data-active="false"]:not(:hover):not(:active) {
|
|
120
|
+
background-color: transparent;
|
|
121
|
+
color: var(--sidebar-foreground);
|
|
122
|
+
font-weight: 400;
|
|
123
|
+
}
|
|
@@ -37,7 +37,9 @@ export async function proxyRequest(
|
|
|
37
37
|
const upstream = await fetch(targetUrl, {
|
|
38
38
|
method: request.method,
|
|
39
39
|
headers,
|
|
40
|
-
|
|
40
|
+
// Preserve multipart uploads and other binary bodies byte-for-byte. Reading
|
|
41
|
+
// them as text corrupts bytes that are not valid UTF-8 before forwarding.
|
|
42
|
+
body: hasBody ? await request.arrayBuffer() : undefined,
|
|
41
43
|
redirect: "manual",
|
|
42
44
|
});
|
|
43
45
|
|