@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.
Files changed (94) hide show
  1. package/README.md +7 -3
  2. package/dist/add.d.ts +16 -0
  3. package/dist/add.js +63 -20
  4. package/dist/assemble.js +9 -9
  5. package/dist/dev.d.ts +26 -0
  6. package/dist/dev.js +297 -0
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.js +82 -2
  9. package/dist/lib.d.ts +2 -0
  10. package/dist/lib.js +7 -1
  11. package/dist/locale.d.ts +22 -0
  12. package/dist/locale.js +184 -0
  13. package/dist/lockfile.d.ts +5 -3
  14. package/dist/lockfile.js +21 -5
  15. package/dist/remove.js +26 -23
  16. package/dist/templates/fullstack-nest-svelte/README.md +42 -0
  17. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +1 -0
  18. package/dist/templates/fullstack-nest-svelte/apps/api/scripts/.keep +1 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/README.md +7 -5
  20. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
  21. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
  22. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/en.json +6 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/ko.json +6 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +3 -9
  25. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +8 -11
  26. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
  27. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
  28. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
  29. package/dist/templates/fullstack-nest-svelte/dot-env.docker +2 -2
  30. package/dist/templates/fullstack-nest-svelte/dot-gitignore +3 -0
  31. package/dist/templates/fullstack-nest-svelte/dot-podokit/dev.json +4 -0
  32. package/dist/templates/fullstack-nest-svelte/tests/README.md +10 -4
  33. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +187 -0
  34. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +9 -0
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +5 -1
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-errors.ts +3 -0
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-redirect.ts +27 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +8 -3
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +564 -0
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +564 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +13 -12
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +90 -722
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +8 -1
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +4 -4
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +2 -2
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +4 -4
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +8 -2
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +15 -4
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +4 -4
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +32 -35
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +10 -3
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +11 -7
  54. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +78 -0
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +28 -1
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +15 -0
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +59 -6
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +1 -1
  60. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
  61. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +2 -2
  63. package/dist/templates/modules/admin-dashboard/module.manifest.json +14 -3
  64. package/dist/templates/modules/auth/files/apps/api/scripts/configure-auth.mjs +22 -11
  65. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +6 -2
  66. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +50 -25
  67. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/SKILL.md +15 -2
  68. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/agents/openai.yaml +2 -2
  69. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +51 -0
  70. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/google.md +44 -0
  71. package/dist/templates/modules/auth/module.manifest.json +3 -0
  72. package/dist/templates/todo/README.md +42 -0
  73. package/dist/templates/todo/apps/api/Dockerfile +1 -0
  74. package/dist/templates/todo/apps/api/scripts/.keep +1 -0
  75. package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -5
  76. package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
  77. package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
  78. package/dist/templates/todo/apps/web/src/lib/i18n/locales/en.json +6 -0
  79. package/dist/templates/todo/apps/web/src/lib/i18n/locales/ko.json +6 -0
  80. package/dist/templates/todo/apps/web/vite.config.ts +3 -9
  81. package/dist/templates/todo/compose.dev.yaml +8 -11
  82. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
  83. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
  84. package/dist/templates/todo/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
  85. package/dist/templates/todo/dot-env.docker +2 -2
  86. package/dist/templates/todo/dot-gitignore +3 -0
  87. package/dist/templates/todo/dot-podokit/dev.json +4 -0
  88. package/dist/templates/todo/tests/README.md +10 -4
  89. package/dist/update.js +48 -7
  90. package/package.json +1 -1
  91. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/en.ts +0 -10
  92. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +0 -11
  93. package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +0 -10
  94. 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
- npm run dev
14
+ npx @podosoft/podokit dev watch
15
15
  ```
16
16
 
17
- - API: http://localhost:5002 (health at `/health`)
18
- - Web: http://localhost:5001
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 = readManifest(dir);
71
+ const manifest = readModuleManifest(dir);
42
72
  const bundled = (0, node_path_1.join)(modulesDir, manifest.name);
43
73
  if (dir === bundled)
44
74
  return { dir, name: manifest.name };
@@ -66,7 +96,7 @@ function listModules(modulesDir, projectRoot) {
66
96
  if ((0, node_fs_1.existsSync)(modulesDir)) {
67
97
  for (const name of (0, node_fs_1.readdirSync)(modulesDir)) {
68
98
  if ((0, node_fs_1.existsSync)((0, node_path_1.join)(modulesDir, name, "module.manifest.json"))) {
69
- out.set(name, readManifest((0, node_path_1.join)(modulesDir, name)).description);
99
+ out.set(name, readModuleManifest((0, node_path_1.join)(modulesDir, name)).description);
70
100
  }
71
101
  }
72
102
  }
@@ -79,7 +109,7 @@ function listModules(modulesDir, projectRoot) {
79
109
  const manifestPath = (0, node_path_1.join)(scope, dir, "module.manifest.json");
80
110
  if (!(0, node_fs_1.existsSync)(manifestPath))
81
111
  continue;
82
- const manifest = readManifest((0, node_path_1.join)(scope, dir));
112
+ const manifest = readModuleManifest((0, node_path_1.join)(scope, dir));
83
113
  if (!out.has(manifest.name))
84
114
  out.set(manifest.name, manifest.description);
85
115
  }
@@ -87,7 +117,7 @@ function listModules(modulesDir, projectRoot) {
87
117
  }
88
118
  return [...out].map(([name, description]) => ({ name, description }));
89
119
  }
90
- function readManifest(moduleDir) {
120
+ function readModuleManifest(moduleDir) {
91
121
  return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8"));
92
122
  }
93
123
  function readJson(path) {
@@ -113,7 +143,7 @@ function isApplied(projectRoot, modulesDir, module) {
113
143
  const moduleDir = resolveModuleDir(module, modulesDir, projectRoot);
114
144
  if (!moduleDir)
115
145
  return false;
116
- const manifest = readManifest(moduleDir);
146
+ const manifest = readModuleManifest(moduleDir);
117
147
  const firstInject = manifest.inject?.[0];
118
148
  if (firstInject) {
119
149
  const target = (0, node_path_1.join)(projectRoot, firstInject.file);
@@ -153,7 +183,7 @@ function addModule(options) {
153
183
  modulePaths: [...new Set(result.touched)],
154
184
  adoptedPaths: result.adopted,
155
185
  }
156
- : undefined);
186
+ : undefined, result.managedOverrides);
157
187
  return result;
158
188
  }
159
189
  function applyModule(projectRoot, module, modulesDir, applied, adopt) {
@@ -163,16 +193,18 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
163
193
  throw new Error(`Unknown module "${module}".${available.length ? ` Available: ${available.join(", ")}.` : ""}`);
164
194
  }
165
195
  const moduleDir = resolved.dir;
166
- const manifest = readManifest(moduleDir);
196
+ const manifest = readModuleManifest(moduleDir);
167
197
  assertManifestVersion(module, manifest);
168
- const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", manifest.targetApp, "package.json");
169
- if (!(0, node_fs_1.existsSync)(appPkgPath)) {
170
- throw new Error(`This does not look like a PodoKit project: ${(0, node_path_1.join)("apps", manifest.targetApp, "package.json")} not found. Run inside a generated project.`);
198
+ const requiredApps = new Set([manifest.targetApp, ...Object.keys(manifest.packageOverlays ?? {})]);
199
+ const missingApp = [...requiredApps].find((app) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, "apps", app, "package.json")));
200
+ if (missingApp) {
201
+ throw new Error(`This does not look like a PodoKit project: ${(0, node_path_1.join)("apps", missingApp, "package.json")} not found. Run inside a generated project.`);
171
202
  }
172
203
  applied.add(module);
173
204
  // 0) apply required modules first (auto-add if missing)
174
205
  const added = [];
175
206
  const ownedGlobs = [...(manifest.ownedGlobs ?? [])];
207
+ const managedOverrides = [...(manifest.managedOverrides ?? [])];
176
208
  const preserved = [];
177
209
  const adopted = [];
178
210
  const touched = [];
@@ -182,6 +214,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
182
214
  const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
183
215
  added.push(required, ...result.added);
184
216
  ownedGlobs.push(...result.ownedGlobs);
217
+ managedOverrides.push(...result.managedOverrides);
185
218
  preserved.push(...result.preserved);
186
219
  adopted.push(...result.adopted);
187
220
  touched.push(...result.touched);
@@ -202,18 +235,19 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
202
235
  adopted.push(...copied.adopted);
203
236
  touched.push(...copied.touched);
204
237
  }
205
- // 2) merge dependencies and scripts into the target app
206
- if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
238
+ // 2) merge dependencies and scripts into every declared app package
239
+ for (const [app, declaration] of modulePackageOverlays(manifest)) {
240
+ const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", app, "package.json");
207
241
  const overlay = {};
208
- if (manifest.dependencies)
209
- overlay.dependencies = manifest.dependencies;
210
- if (manifest.devDependencies)
211
- overlay.devDependencies = manifest.devDependencies;
212
- if (manifest.scripts)
213
- overlay.scripts = manifest.scripts;
242
+ if (declaration.dependencies)
243
+ overlay.dependencies = declaration.dependencies;
244
+ if (declaration.devDependencies)
245
+ overlay.devDependencies = declaration.devDependencies;
246
+ if (declaration.scripts)
247
+ overlay.scripts = declaration.scripts;
214
248
  const merged = (0, podokit_template_engine_1.mergePackageJson)(readJson(appPkgPath), overlay);
215
249
  (0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(merged, null, 2)}\n`);
216
- touched.push(`apps/${manifest.targetApp}/package.json`);
250
+ touched.push(`apps/${app}/package.json`);
217
251
  }
218
252
  // 3) append env example lines
219
253
  if (manifest.env?.length) {
@@ -235,7 +269,16 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
235
269
  touched.push(injection.file);
236
270
  }
237
271
  const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
238
- return { module: manifest.name, instructions, added, ownedGlobs, preserved, adopted, touched };
272
+ return {
273
+ module: manifest.name,
274
+ instructions,
275
+ added,
276
+ ownedGlobs,
277
+ managedOverrides,
278
+ preserved,
279
+ adopted,
280
+ touched,
281
+ };
239
282
  }
240
283
  function moduleRecord(module) {
241
284
  return {
package/dist/assemble.js CHANGED
@@ -22,17 +22,17 @@ function applyModuleToTree(tree, moduleDir, vars) {
22
22
  for (const [path, file] of (0, podokit_template_engine_1.renderTemplate)(filesDir, vars))
23
23
  tree.set(path, file);
24
24
  }
25
- // 2) merge dependencies/scripts into the target app's package.json
26
- if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
27
- const pkgPath = `apps/${manifest.targetApp}/package.json`;
25
+ // 2) merge dependencies/scripts into every declared app package.json
26
+ for (const [app, declaration] of (0, add_1.modulePackageOverlays)(manifest)) {
27
+ const pkgPath = `apps/${app}/package.json`;
28
28
  const base = JSON.parse(textOf(tree, pkgPath) || "{}");
29
29
  const overlay = {};
30
- if (manifest.dependencies)
31
- overlay.dependencies = manifest.dependencies;
32
- if (manifest.devDependencies)
33
- overlay.devDependencies = manifest.devDependencies;
34
- if (manifest.scripts)
35
- overlay.scripts = manifest.scripts;
30
+ if (declaration.dependencies)
31
+ overlay.dependencies = declaration.dependencies;
32
+ if (declaration.devDependencies)
33
+ overlay.devDependencies = declaration.devDependencies;
34
+ if (declaration.scripts)
35
+ overlay.scripts = declaration.scripts;
36
36
  setText(tree, pkgPath, `${JSON.stringify((0, podokit_template_engine_1.mergePackageJson)(base, overlay), null, 2)}\n`);
37
37
  }
38
38
  // 3) append env example lines (deduped)
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 {};