@podosoft/podokit 0.11.1 → 0.12.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/README.md +7 -3
- package/dist/add.d.ts +16 -0
- package/dist/add.js +63 -20
- package/dist/assemble.js +9 -9
- package/dist/dev.d.ts +26 -0
- package/dist/dev.js +297 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +82 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +7 -1
- package/dist/locale.d.ts +22 -0
- package/dist/locale.js +184 -0
- package/dist/lockfile.d.ts +5 -3
- package/dist/lockfile.js +21 -5
- package/dist/remove.js +26 -23
- package/dist/templates/fullstack-nest-svelte/README.md +42 -0
- 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/apps/web/src/lib/i18n/README.md +7 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/en.json +6 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/ko.json +6 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +3 -9
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +8 -11
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.docker +2 -2
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +3 -0
- package/dist/templates/fullstack-nest-svelte/dot-podokit/dev.json +4 -0
- package/dist/templates/fullstack-nest-svelte/tests/README.md +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-errors.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-redirect.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +8 -3
- 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/i18n/catalogs/admin-dashboard/en.json +564 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +564 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +13 -12
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +90 -722
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +8 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +15 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +32 -35
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +10 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +11 -7
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +78 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +28 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +59 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +1 -1
- 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 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/module.manifest.json +14 -3
- package/dist/templates/modules/auth/files/apps/api/scripts/configure-auth.mjs +22 -11
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +6 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +50 -25
- 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/files/dot-claude/skills/podokit-configure-auth/references/google.md +44 -0
- package/dist/templates/modules/auth/module.manifest.json +3 -0
- package/dist/templates/todo/README.md +42 -0
- package/dist/templates/todo/apps/api/Dockerfile +1 -0
- package/dist/templates/todo/apps/api/scripts/.keep +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -5
- package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/locales/en.json +6 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/locales/ko.json +6 -0
- package/dist/templates/todo/apps/web/vite.config.ts +3 -9
- package/dist/templates/todo/compose.dev.yaml +8 -11
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
- package/dist/templates/todo/dot-env.docker +2 -2
- package/dist/templates/todo/dot-gitignore +3 -0
- package/dist/templates/todo/dot-podokit/dev.json +4 -0
- package/dist/templates/todo/tests/README.md +10 -4
- package/dist/update.js +48 -7
- package/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/en.ts +0 -10
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +0 -11
- package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +0 -10
- package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +0 -11
package/README.md
CHANGED
|
@@ -11,11 +11,13 @@ npx @podosoft/podokit create my-app
|
|
|
11
11
|
cd my-app
|
|
12
12
|
npm install
|
|
13
13
|
cp .env.example .env
|
|
14
|
-
|
|
14
|
+
npx @podosoft/podokit dev watch
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
Open **http://my-app.localhost**. The first project starts one user-level
|
|
18
|
+
Traefik gateway on loopback port 80; later projects reuse it and route by
|
|
19
|
+
hostname. Stop this project with `npx @podosoft/podokit dev down`. The traditional
|
|
20
|
+
`npm run dev` loop remains available on web port 5001 and API port 5002.
|
|
19
21
|
|
|
20
22
|
When run in a terminal, `podo create` lists the templates with descriptions and asks which one (and which package manager) to use. Pass flags (or `--yes`) to skip the prompts.
|
|
21
23
|
|
|
@@ -32,8 +34,10 @@ The `todo` template (`--template todo`) generates a working todo app (SvelteKit
|
|
|
32
34
|
| `podo status` | Show the PodoKit version, modules, file tiers, and local edits |
|
|
33
35
|
| `podo diff` | List the PodoKit-managed files you've edited since generation |
|
|
34
36
|
| `podo doctor` | Check framework versions against the supported ranges |
|
|
37
|
+
| `podo locale <command>` | Add, validate, activate, deactivate, or list JSON locales |
|
|
35
38
|
| `podo update [--apply]` | Preview (or apply) what a version update would change |
|
|
36
39
|
| `podo eject <path…>` | Take ownership of a managed file so updates skip it |
|
|
40
|
+
| `podo dev <action>` | Watch, inspect, execute in, or stop a container stack behind the shared `*.localhost` gateway |
|
|
37
41
|
|
|
38
42
|
## Usage
|
|
39
43
|
|
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/dev.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface DevConfig {
|
|
2
|
+
schemaVersion: 1;
|
|
3
|
+
hostname: string;
|
|
4
|
+
publicUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DevRuntime {
|
|
7
|
+
alias: string;
|
|
8
|
+
config: DevConfig;
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
routeId: string;
|
|
11
|
+
runtimeComposePath: string;
|
|
12
|
+
}
|
|
13
|
+
interface CommandResult {
|
|
14
|
+
status: number | null;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
}
|
|
18
|
+
export type CommandRunner = (command: string, args: string[], options: {
|
|
19
|
+
capture: boolean;
|
|
20
|
+
}) => CommandResult;
|
|
21
|
+
export declare function readDevConfig(projectRoot: string): DevConfig;
|
|
22
|
+
export declare function resolveDevRuntime(projectRoot: string): DevRuntime;
|
|
23
|
+
export declare function renderRuntimeCompose(runtime: DevRuntime): string;
|
|
24
|
+
export declare function renderRoute(runtime: DevRuntime): string;
|
|
25
|
+
export declare function runDevCommand(projectRoot: string, action: string | undefined, args: string[], runner?: CommandRunner): void;
|
|
26
|
+
export {};
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readDevConfig = readDevConfig;
|
|
4
|
+
exports.resolveDevRuntime = resolveDevRuntime;
|
|
5
|
+
exports.renderRuntimeCompose = renderRuntimeCompose;
|
|
6
|
+
exports.renderRoute = renderRoute;
|
|
7
|
+
exports.runDevCommand = runDevCommand;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_os_1 = require("node:os");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const lockfile_1 = require("./lockfile");
|
|
14
|
+
const GATEWAY_CONTAINER = "podokit-dev-gateway";
|
|
15
|
+
const GATEWAY_IMAGE = "traefik:v3.5";
|
|
16
|
+
const GATEWAY_LABEL = "io.podosoft.podokit.dev-gateway";
|
|
17
|
+
const GATEWAY_NETWORK = "podokit-dev-gateway";
|
|
18
|
+
const RUNTIME_COMPOSE = ".podokit/runtime/dev.compose.yaml";
|
|
19
|
+
const DEV_CONFIG = ".podokit/dev.json";
|
|
20
|
+
function defaultRunner(command, args, options) {
|
|
21
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: options.capture ? "pipe" : "inherit",
|
|
24
|
+
});
|
|
25
|
+
if (result.error)
|
|
26
|
+
throw result.error;
|
|
27
|
+
return {
|
|
28
|
+
status: result.status,
|
|
29
|
+
stdout: options.capture ? (result.stdout ?? "") : "",
|
|
30
|
+
stderr: options.capture ? (result.stderr ?? "") : "",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function parseJson(path) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`Cannot read ${path}: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function validateHostname(hostname) {
|
|
42
|
+
if (hostname.length > 253 ||
|
|
43
|
+
!hostname.endsWith(".localhost") ||
|
|
44
|
+
!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.localhost$/.test(hostname)) {
|
|
45
|
+
throw new Error(`Invalid development hostname "${hostname}". Use a lowercase *.localhost hostname without a scheme or port.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function validatePublicUrl(publicUrl) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(publicUrl);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`Invalid publicUrl "${publicUrl}". Use an absolute HTTPS URL.`);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.protocol !== "https:" || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
57
|
+
throw new Error(`Invalid publicUrl "${publicUrl}". Use an HTTPS origin without a path.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readDevConfig(projectRoot) {
|
|
61
|
+
const path = (0, node_path_1.join)(projectRoot, DEV_CONFIG);
|
|
62
|
+
const manifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
63
|
+
if (!manifest)
|
|
64
|
+
throw new Error("Not a PodoKit project: .podokit/manifest.json is missing.");
|
|
65
|
+
const fallbackName = manifest.answers.projectName;
|
|
66
|
+
if (!fallbackName)
|
|
67
|
+
throw new Error("PodoKit manifest is missing answers.projectName.");
|
|
68
|
+
const config = (0, node_fs_1.existsSync)(path)
|
|
69
|
+
? parseJson(path)
|
|
70
|
+
: { schemaVersion: 1, hostname: `${fallbackName}.localhost` };
|
|
71
|
+
if (config.schemaVersion !== 1) {
|
|
72
|
+
throw new Error(`Unsupported ${DEV_CONFIG} schemaVersion: ${String(config.schemaVersion)}`);
|
|
73
|
+
}
|
|
74
|
+
validateHostname(config.hostname);
|
|
75
|
+
if (config.publicUrl)
|
|
76
|
+
validatePublicUrl(config.publicUrl);
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
function stableId(projectRoot) {
|
|
80
|
+
return (0, node_crypto_1.createHash)("sha256").update(projectRoot).digest("hex").slice(0, 10);
|
|
81
|
+
}
|
|
82
|
+
function slug(value) {
|
|
83
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
84
|
+
return normalized.slice(0, 36) || "app";
|
|
85
|
+
}
|
|
86
|
+
function resolveDevRuntime(projectRoot) {
|
|
87
|
+
const root = (0, node_path_1.resolve)(projectRoot);
|
|
88
|
+
const config = readDevConfig(root);
|
|
89
|
+
const id = stableId(root);
|
|
90
|
+
const projectSlug = slug(config.hostname.replace(/\.localhost$/, ""));
|
|
91
|
+
return {
|
|
92
|
+
alias: `podokit-${projectSlug}-${id}`,
|
|
93
|
+
config,
|
|
94
|
+
projectRoot: root,
|
|
95
|
+
routeId: `route-${projectSlug}-${id}`,
|
|
96
|
+
runtimeComposePath: (0, node_path_1.join)(root, RUNTIME_COMPOSE),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function renderRuntimeCompose(runtime) {
|
|
100
|
+
return `# Generated by podo dev. Do not edit.\nservices:\n traefik:\n profiles: [podokit-legacy-proxy]\n web:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}]\nnetworks:\n ${GATEWAY_NETWORK}:\n external: true\n name: ${GATEWAY_NETWORK}\n`;
|
|
101
|
+
}
|
|
102
|
+
function renderRoute(runtime) {
|
|
103
|
+
const { alias, config, routeId } = runtime;
|
|
104
|
+
return `http:\n routers:\n ${routeId}:\n entryPoints: [web]\n rule: Host(\`${config.hostname}\`)\n service: ${routeId}\n middlewares: [${routeId}-compression]\n services:\n ${routeId}:\n loadBalancer:\n servers:\n - url: http://${alias}:5001\n middlewares:\n ${routeId}-compression:\n compress: {}\n`;
|
|
105
|
+
}
|
|
106
|
+
function devHome() {
|
|
107
|
+
return process.env.PODOKIT_DEV_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".podokit", "dev-gateway");
|
|
108
|
+
}
|
|
109
|
+
function registryDirectory() {
|
|
110
|
+
return (0, node_path_1.join)(devHome(), "projects");
|
|
111
|
+
}
|
|
112
|
+
function routesDirectory() {
|
|
113
|
+
return (0, node_path_1.join)(devHome(), "routes");
|
|
114
|
+
}
|
|
115
|
+
function registryPath(runtime) {
|
|
116
|
+
return (0, node_path_1.join)(registryDirectory(), `${runtime.routeId}.json`);
|
|
117
|
+
}
|
|
118
|
+
function routePath(runtime) {
|
|
119
|
+
return (0, node_path_1.join)(routesDirectory(), `${runtime.routeId}.yml`);
|
|
120
|
+
}
|
|
121
|
+
function activeEntries() {
|
|
122
|
+
const dir = registryDirectory();
|
|
123
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
124
|
+
return [];
|
|
125
|
+
const entries = [];
|
|
126
|
+
for (const name of (0, node_fs_1.readdirSync)(dir).filter((entry) => entry.endsWith(".json"))) {
|
|
127
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
128
|
+
const entry = parseJson(path);
|
|
129
|
+
if (!(0, node_fs_1.existsSync)(entry.projectRoot)) {
|
|
130
|
+
(0, node_fs_1.rmSync)(path, { force: true });
|
|
131
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(routesDirectory(), `${entry.routeId}.yml`), { force: true });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
entries.push(entry);
|
|
135
|
+
}
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
function writeRuntimeFiles(runtime) {
|
|
139
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(runtime.projectRoot, ".podokit", "runtime"), { recursive: true });
|
|
140
|
+
(0, node_fs_1.writeFileSync)(runtime.runtimeComposePath, renderRuntimeCompose(runtime));
|
|
141
|
+
}
|
|
142
|
+
function registerRoute(runtime) {
|
|
143
|
+
const collision = activeEntries().find((entry) => entry.hostname === runtime.config.hostname && entry.projectRoot !== runtime.projectRoot);
|
|
144
|
+
if (collision) {
|
|
145
|
+
throw new Error(`Development hostname ${runtime.config.hostname} is already registered by ${collision.projectRoot}. Change .podokit/dev.json or run podo dev down there.`);
|
|
146
|
+
}
|
|
147
|
+
(0, node_fs_1.mkdirSync)(registryDirectory(), { recursive: true });
|
|
148
|
+
(0, node_fs_1.mkdirSync)(routesDirectory(), { recursive: true });
|
|
149
|
+
(0, node_fs_1.writeFileSync)(routePath(runtime), renderRoute(runtime));
|
|
150
|
+
const entry = {
|
|
151
|
+
hostname: runtime.config.hostname,
|
|
152
|
+
projectRoot: runtime.projectRoot,
|
|
153
|
+
routeId: runtime.routeId,
|
|
154
|
+
};
|
|
155
|
+
(0, node_fs_1.writeFileSync)(registryPath(runtime), `${JSON.stringify(entry, null, 2)}\n`);
|
|
156
|
+
}
|
|
157
|
+
function unregisterRoute(runtime) {
|
|
158
|
+
(0, node_fs_1.rmSync)(registryPath(runtime), { force: true });
|
|
159
|
+
(0, node_fs_1.rmSync)(routePath(runtime), { force: true });
|
|
160
|
+
}
|
|
161
|
+
function commandSucceeded(result) {
|
|
162
|
+
return result.status === 0;
|
|
163
|
+
}
|
|
164
|
+
function ensureDocker(runner) {
|
|
165
|
+
const result = runner("docker", ["info", "--format", "{{.ServerVersion}}"], { capture: true });
|
|
166
|
+
if (!commandSucceeded(result)) {
|
|
167
|
+
throw new Error(`Docker is not available. ${result.stderr.trim()}`.trim());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function ensureNetwork(runner) {
|
|
171
|
+
const inspect = runner("docker", ["network", "inspect", GATEWAY_NETWORK], { capture: true });
|
|
172
|
+
if (commandSucceeded(inspect))
|
|
173
|
+
return;
|
|
174
|
+
const create = runner("docker", ["network", "create", GATEWAY_NETWORK], { capture: true });
|
|
175
|
+
if (!commandSucceeded(create)) {
|
|
176
|
+
throw new Error(`Cannot create Docker network ${GATEWAY_NETWORK}: ${create.stderr.trim()}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function ensureGateway(runner) {
|
|
180
|
+
const inspect = runner("docker", ["inspect", "--format", `{{ index .Config.Labels \"${GATEWAY_LABEL}\" }} {{.State.Running}}`, GATEWAY_CONTAINER], { capture: true });
|
|
181
|
+
if (commandSucceeded(inspect)) {
|
|
182
|
+
const [label, running] = inspect.stdout.trim().split(/\s+/);
|
|
183
|
+
if (label !== "1") {
|
|
184
|
+
throw new Error(`Container ${GATEWAY_CONTAINER} exists but is not managed by PodoKit.`);
|
|
185
|
+
}
|
|
186
|
+
if (running !== "true") {
|
|
187
|
+
const start = runner("docker", ["start", GATEWAY_CONTAINER], { capture: true });
|
|
188
|
+
if (!commandSucceeded(start)) {
|
|
189
|
+
throw new Error(`Cannot start ${GATEWAY_CONTAINER}: ${start.stderr.trim()}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
(0, node_fs_1.mkdirSync)(routesDirectory(), { recursive: true });
|
|
195
|
+
const run = runner("docker", [
|
|
196
|
+
"run",
|
|
197
|
+
"--detach",
|
|
198
|
+
"--name",
|
|
199
|
+
GATEWAY_CONTAINER,
|
|
200
|
+
"--label",
|
|
201
|
+
`${GATEWAY_LABEL}=1`,
|
|
202
|
+
"--restart",
|
|
203
|
+
"unless-stopped",
|
|
204
|
+
"--network",
|
|
205
|
+
GATEWAY_NETWORK,
|
|
206
|
+
"--publish",
|
|
207
|
+
"127.0.0.1:80:80",
|
|
208
|
+
"--volume",
|
|
209
|
+
`${routesDirectory()}:/etc/traefik/dynamic:ro`,
|
|
210
|
+
GATEWAY_IMAGE,
|
|
211
|
+
"--providers.file.directory=/etc/traefik/dynamic",
|
|
212
|
+
"--providers.file.watch=true",
|
|
213
|
+
"--entrypoints.web.address=:80",
|
|
214
|
+
"--api.dashboard=false",
|
|
215
|
+
], { capture: true });
|
|
216
|
+
if (!commandSucceeded(run)) {
|
|
217
|
+
throw new Error(`Cannot start the shared PodoKit gateway on 127.0.0.1:80. Stop the current port owner, then retry. ${run.stderr.trim()}`.trim());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function composeArgs(runtime, args) {
|
|
221
|
+
return ["compose", "-f", "compose.dev.yaml", "-f", runtime.runtimeComposePath, ...args];
|
|
222
|
+
}
|
|
223
|
+
function runCompose(runtime, args, runner) {
|
|
224
|
+
const result = runner("docker", composeArgs(runtime, args), { capture: false });
|
|
225
|
+
if (!commandSucceeded(result)) {
|
|
226
|
+
throw new Error(`docker compose ${args.join(" ")} exited with status ${String(result.status)}.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function composeCommand(action, args) {
|
|
230
|
+
const global = [];
|
|
231
|
+
const command = [];
|
|
232
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
233
|
+
const value = args[index];
|
|
234
|
+
if (value === undefined)
|
|
235
|
+
continue;
|
|
236
|
+
if (value === "--profile") {
|
|
237
|
+
const profile = args[index + 1];
|
|
238
|
+
if (!profile)
|
|
239
|
+
throw new Error("--profile requires a value.");
|
|
240
|
+
global.push(value, profile);
|
|
241
|
+
index += 1;
|
|
242
|
+
}
|
|
243
|
+
else if (value.startsWith("--profile=")) {
|
|
244
|
+
global.push(value);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
command.push(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const activeProfiles = action === "down" ? ["--profile", "*"] : [];
|
|
251
|
+
return [...activeProfiles, ...global, action, ...command];
|
|
252
|
+
}
|
|
253
|
+
function stopGatewayWhenUnused(runner) {
|
|
254
|
+
if (activeEntries().length > 0)
|
|
255
|
+
return;
|
|
256
|
+
runner("docker", ["rm", "--force", GATEWAY_CONTAINER], { capture: true });
|
|
257
|
+
runner("docker", ["network", "rm", GATEWAY_NETWORK], { capture: true });
|
|
258
|
+
}
|
|
259
|
+
function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
|
|
260
|
+
const runtime = resolveDevRuntime(projectRoot);
|
|
261
|
+
writeRuntimeFiles(runtime);
|
|
262
|
+
if (action === "url") {
|
|
263
|
+
process.stdout.write(`http://${runtime.config.hostname}\n`);
|
|
264
|
+
if (runtime.config.publicUrl)
|
|
265
|
+
process.stdout.write(`${runtime.config.publicUrl}\n`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
ensureDocker(runner);
|
|
269
|
+
if (action === "watch") {
|
|
270
|
+
ensureNetwork(runner);
|
|
271
|
+
registerRoute(runtime);
|
|
272
|
+
try {
|
|
273
|
+
ensureGateway(runner);
|
|
274
|
+
process.stdout.write(`Development URL: http://${runtime.config.hostname}\n`);
|
|
275
|
+
if (runtime.config.publicUrl)
|
|
276
|
+
process.stdout.write(`Public OAuth URL: ${runtime.config.publicUrl}\n`);
|
|
277
|
+
runCompose(runtime, composeCommand("watch", args), runner);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
unregisterRoute(runtime);
|
|
281
|
+
stopGatewayWhenUnused(runner);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (action === "down") {
|
|
287
|
+
runCompose(runtime, composeCommand("down", args), runner);
|
|
288
|
+
unregisterRoute(runtime);
|
|
289
|
+
stopGatewayWhenUnused(runner);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (action === "exec" || action === "logs" || action === "ps") {
|
|
293
|
+
runCompose(runtime, composeCommand(action, args), runner);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
throw new Error("Usage: podo dev <watch|exec|logs|ps|down|url> [docker compose options]");
|
|
297
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type PackageManager } from "./create";
|
|
3
|
+
import { type LocaleDirection } from "./locale";
|
|
3
4
|
interface ParsedArgs {
|
|
4
5
|
command?: string;
|
|
5
6
|
name?: string;
|
|
@@ -12,6 +13,9 @@ interface ParsedArgs {
|
|
|
12
13
|
yes: boolean;
|
|
13
14
|
help: boolean;
|
|
14
15
|
ai: boolean;
|
|
16
|
+
positionals: string[];
|
|
17
|
+
localeName?: string;
|
|
18
|
+
localeDirection?: LocaleDirection;
|
|
15
19
|
}
|
|
16
20
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
17
21
|
export {};
|