@podosoft/podokit 0.4.0 → 0.5.1

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 (141) hide show
  1. package/README.md +33 -2
  2. package/dist/add.d.ts +2 -0
  3. package/dist/add.js +5 -1
  4. package/dist/assemble.d.ts +14 -0
  5. package/dist/assemble.js +60 -0
  6. package/dist/create.d.ts +2 -0
  7. package/dist/create.js +7 -0
  8. package/dist/eject.d.ts +11 -0
  9. package/dist/eject.js +27 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +122 -1
  12. package/dist/inspect.d.ts +43 -0
  13. package/dist/inspect.js +82 -0
  14. package/dist/lockfile.d.ts +80 -0
  15. package/dist/lockfile.js +193 -0
  16. package/dist/templates/base/dot-env.example +11 -6
  17. package/dist/templates/fullstack-nest-svelte/README.md +2 -2
  18. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
  20. package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
  21. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
  22. package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
  23. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -2
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +1 -1
  29. package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
  30. package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
  31. package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
  32. package/dist/templates/fullstack-nest-svelte/tests/README.md +1 -1
  33. package/dist/templates/fullstack-nest-svelte/tests/package.json +2 -1
  34. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +1 -1
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +7 -1
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +21 -22
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +224 -8
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +19 -3
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +21 -1
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +360 -49
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +2 -4
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +82 -37
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +3 -4
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +79 -45
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +3 -5
  55. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +254 -108
  56. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +8 -1
  57. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +127 -5
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +18 -2
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
  61. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +51 -2
  63. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
  64. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +264 -1
  65. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
  66. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +10 -10
  67. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +2 -2
  68. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
  69. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
  70. package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
  71. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +1 -1
  72. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +12 -1
  73. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +77 -6
  74. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +1 -1
  75. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
  76. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
  77. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
  78. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +1 -0
  79. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
  80. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +55 -0
  81. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
  82. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +9 -2
  83. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
  84. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
  85. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +28 -12
  86. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +35 -3
  87. package/dist/templates/modules/admin-dashboard/module.manifest.json +13 -7
  88. package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
  89. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
  90. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +4 -0
  91. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +2 -0
  92. package/dist/templates/modules/audit-log/module.manifest.json +15 -9
  93. package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +78 -13
  94. package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
  95. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
  96. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +197 -52
  97. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
  98. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
  99. package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
  100. package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
  101. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
  102. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
  103. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
  104. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
  105. package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
  106. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
  107. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
  108. package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
  109. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
  110. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
  111. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
  112. package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
  113. package/dist/templates/modules/auth/module.manifest.json +54 -20
  114. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
  115. package/dist/templates/modules/bullmq/module.manifest.json +4 -4
  116. package/dist/templates/modules/file-upload/module.manifest.json +3 -3
  117. package/dist/templates/modules/job-progress/module.manifest.json +9 -9
  118. package/dist/templates/modules/logging/module.manifest.json +3 -3
  119. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
  120. package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
  121. package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
  122. package/dist/templates/modules/redis/module.manifest.json +4 -4
  123. package/dist/templates/modules/sse/module.manifest.json +4 -4
  124. package/dist/templates/todo/README.md +2 -2
  125. package/dist/templates/todo/apps/api/package.json +1 -0
  126. package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
  127. package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
  128. package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
  129. package/dist/templates/todo/apps/api/src/main.ts +1 -1
  130. package/dist/templates/todo/apps/web/package.json +2 -2
  131. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  132. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  133. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +1 -1
  134. package/dist/templates/todo/dot-env.example +10 -5
  135. package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
  136. package/dist/templates/todo/tests/README.md +1 -1
  137. package/dist/templates/todo/tests/package.json +2 -1
  138. package/dist/templates/todo/tests/playwright.config.ts +1 -1
  139. package/dist/update.d.ts +49 -0
  140. package/dist/update.js +158 -0
  141. package/package.json +2 -2
package/README.md CHANGED
@@ -14,8 +14,8 @@ cp .env.example .env
14
14
  npm run dev
15
15
  ```
16
16
 
17
- - API: http://localhost:3000 (health at `/health`)
18
- - Web: http://localhost:5173
17
+ - API: http://localhost:5002 (health at `/health`)
18
+ - Web: http://localhost:5001
19
19
 
20
20
  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
21
 
@@ -23,6 +23,18 @@ The `todo` template (`--template todo`) generates a working todo app (SvelteKit
23
23
 
24
24
  ![Generated todo app](https://raw.githubusercontent.com/podosoft-dev/podokit/main/docs/images/todo-app.png)
25
25
 
26
+ ## Commands
27
+
28
+ | Command | What it does |
29
+ |---|---|
30
+ | `podo create <name>` | Scaffold a new project from a template |
31
+ | `podo add <module>` | Add a feature module (auth, admin-dashboard, redis, …) to a project |
32
+ | `podo status` | Show the PodoKit version, modules, file tiers, and local edits |
33
+ | `podo diff` | List the PodoKit-managed files you've edited since generation |
34
+ | `podo doctor` | Check framework versions against the supported ranges |
35
+ | `podo update [--apply]` | Preview (or apply) what a version update would change |
36
+ | `podo eject <path…>` | Take ownership of a managed file so updates skip it |
37
+
26
38
  ## Usage
27
39
 
28
40
  ```
@@ -61,6 +73,25 @@ npx @podosoft/podokit add auth # full auth (better-auth): email/password, s
61
73
 
62
74
  `podo add <module>` overlays files, merges dependencies, appends env vars, and wires the module into the NestJS app. Run `podo add` with no argument to list available modules.
63
75
 
76
+ The **`admin-dashboard`** module adds a ready-made admin console on top of `auth` — login/signup pages, user & session management, an audit log, and a Settings page where OAuth providers, SMTP, and server toggles are configured at runtime (stored encrypted in the DB, applied without a restart):
77
+
78
+ ![PodoKit admin dashboard — users](https://raw.githubusercontent.com/podosoft-dev/podokit/main/docs/images/admin-users.png)
79
+
80
+ ## Keep your project up to date
81
+
82
+ Every generated project records how it was assembled in a committed `.podokit/`
83
+ directory, so it can receive template and module improvements later without
84
+ losing your work:
85
+
86
+ ```bash
87
+ podo status # version, modules, and which managed files you've edited
88
+ podo update # preview what a newer PodoKit version would change
89
+ podo update --apply # apply it: clean updates are written, your edits are 3-way merged
90
+ ```
91
+
92
+ Files you own (routes, your components, shadcn UI) are never touched. See the
93
+ [updating guide](https://github.com/podosoft-dev/podokit/blob/main/docs/updating.md).
94
+
64
95
  ## What you get (`fullstack-nest-svelte`)
65
96
 
66
97
  ```
package/dist/add.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface AddOptions {
19
19
  projectRoot: string;
20
20
  module: string;
21
21
  modulesDir: string;
22
+ /** PodoKit version stamped into the lockfile. Defaults to the CLI version. */
23
+ podokitVersion?: string;
22
24
  }
23
25
  export interface AddResult {
24
26
  module: string;
package/dist/add.js CHANGED
@@ -5,6 +5,7 @@ exports.addModule = addModule;
5
5
  const node_fs_1 = require("node:fs");
6
6
  const node_path_1 = require("node:path");
7
7
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
8
+ const lockfile_1 = require("./lockfile");
8
9
  /** List modules available under `modulesDir` (each has a module.manifest.json). */
9
10
  function listModules(modulesDir) {
10
11
  if (!(0, node_fs_1.existsSync)(modulesDir))
@@ -56,7 +57,10 @@ function isApplied(projectRoot, modulesDir, module) {
56
57
  * and inject wiring at markers. Missing required modules are added first.
57
58
  */
58
59
  function addModule(options) {
59
- return applyModule(options.projectRoot, options.module, options.modulesDir, new Set());
60
+ const result = applyModule(options.projectRoot, options.module, options.modulesDir, new Set());
61
+ // Record the module (and any auto-added requirements) and refresh the lock.
62
+ (0, lockfile_1.recordModules)(options.projectRoot, [...result.added, result.module], options.podokitVersion);
63
+ return result;
60
64
  }
61
65
  function applyModule(projectRoot, module, modulesDir, applied) {
62
66
  const moduleDir = (0, node_path_1.join)(modulesDir, module);
@@ -0,0 +1,14 @@
1
+ import { type TemplateVars, type VfsTree } from "@podosoft/podokit-template-engine";
2
+ /**
3
+ * Reconstruct a generated project in memory (no disk writes) from a template
4
+ * plus an ordered module list, mirroring `create` + `add`. `podo update` uses
5
+ * this to build the new-version tree and diff it against the working copy.
6
+ */
7
+ export interface AssembleOptions {
8
+ templatesDir: string;
9
+ template: string;
10
+ answers: TemplateVars;
11
+ modules: string[];
12
+ }
13
+ /** Assemble the project tree for a template + ordered modules, in memory. */
14
+ export declare function assembleProject(options: AssembleOptions): VfsTree;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assembleProject = assembleProject;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
7
+ function readManifest(moduleDir) {
8
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8"));
9
+ }
10
+ function textOf(tree, path) {
11
+ return String(tree.get(path)?.content ?? "");
12
+ }
13
+ function setText(tree, path, content) {
14
+ tree.set(path, { content, text: true });
15
+ }
16
+ function applyModuleToTree(tree, moduleDir, vars) {
17
+ const manifest = readManifest(moduleDir);
18
+ // 1) overlay files (module files override the base)
19
+ const filesDir = (0, node_path_1.join)(moduleDir, "files");
20
+ if ((0, node_fs_1.existsSync)(filesDir)) {
21
+ for (const [path, file] of (0, podokit_template_engine_1.renderTemplate)(filesDir, vars))
22
+ tree.set(path, file);
23
+ }
24
+ // 2) merge dependencies/scripts into the target app's package.json
25
+ if (manifest.dependencies || manifest.devDependencies || manifest.scripts) {
26
+ const pkgPath = `apps/${manifest.targetApp}/package.json`;
27
+ const base = JSON.parse(textOf(tree, pkgPath) || "{}");
28
+ const overlay = {};
29
+ if (manifest.dependencies)
30
+ overlay.dependencies = manifest.dependencies;
31
+ if (manifest.devDependencies)
32
+ overlay.devDependencies = manifest.devDependencies;
33
+ if (manifest.scripts)
34
+ overlay.scripts = manifest.scripts;
35
+ setText(tree, pkgPath, `${JSON.stringify((0, podokit_template_engine_1.mergePackageJson)(base, overlay), null, 2)}\n`);
36
+ }
37
+ // 3) append env example lines (deduped)
38
+ if (manifest.env?.length) {
39
+ const current = textOf(tree, ".env.example");
40
+ const existing = current.split("\n");
41
+ const missing = manifest.env.filter((line) => !existing.includes(line));
42
+ if (missing.length) {
43
+ const sep = current.endsWith("\n") ? "" : "\n";
44
+ setText(tree, ".env.example", `${current}${sep}\n${missing.join("\n")}\n`);
45
+ }
46
+ }
47
+ // 4) inject wiring at markers
48
+ for (const injection of manifest.inject ?? []) {
49
+ const target = injection.file;
50
+ setText(tree, target, (0, podokit_template_engine_1.insertAtMarker)(textOf(tree, target), injection.marker, injection.text));
51
+ }
52
+ }
53
+ /** Assemble the project tree for a template + ordered modules, in memory. */
54
+ function assembleProject(options) {
55
+ const tree = (0, podokit_template_engine_1.renderTemplate)((0, node_path_1.join)(options.templatesDir, options.template), options.answers);
56
+ for (const mod of options.modules) {
57
+ applyModuleToTree(tree, (0, node_path_1.join)(options.templatesDir, "modules", mod), options.answers);
58
+ }
59
+ return tree;
60
+ }
package/dist/create.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface CreateOptions {
12
12
  targetDir?: string;
13
13
  /** Package manager recorded in the generated project. Defaults to `npm`. */
14
14
  packageManager?: PackageManager;
15
+ /** PodoKit version stamped into the lockfile. Defaults to the CLI version. */
16
+ podokitVersion?: string;
15
17
  }
16
18
  export interface CreateResult {
17
19
  projectDir: string;
package/dist/create.js CHANGED
@@ -8,6 +8,7 @@ const node_path_1 = require("node:path");
8
8
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
9
9
  const templates_1 = require("./templates");
10
10
  Object.defineProperty(exports, "DEFAULT_TEMPLATE", { enumerable: true, get: function () { return templates_1.DEFAULT_TEMPLATE; } });
11
+ const lockfile_1 = require("./lockfile");
11
12
  const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?$/i;
12
13
  /** Validate a project name: no path separators, npm-friendly characters. */
13
14
  function assertValidName(name) {
@@ -41,5 +42,11 @@ function create(options) {
41
42
  }
42
43
  const vars = { projectName: name, packageManager };
43
44
  (0, podokit_template_engine_1.copyTemplate)(templateDir, projectDir, vars);
45
+ (0, lockfile_1.initLockfile)(projectDir, {
46
+ template,
47
+ packageManager,
48
+ answers: { projectName: name, packageManager },
49
+ version: options.podokitVersion,
50
+ });
44
51
  return { projectDir, packageManager, template };
45
52
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Take ownership of PodoKit-managed files: flip their lock tier to `owned` so
3
+ * `podo update` skips them from then on (they show up only in `podo diff`).
4
+ * File-level and reversible by re-recording, unlike CRA's one-way eject.
5
+ */
6
+ export interface EjectResult {
7
+ ejected: string[];
8
+ /** Paths not tracked in the lock (nothing to eject). */
9
+ unknown: string[];
10
+ }
11
+ export declare function eject(projectRoot: string, paths: string[]): EjectResult;
package/dist/eject.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.eject = eject;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const lockfile_1 = require("./lockfile");
7
+ const inspect_1 = require("./inspect");
8
+ function eject(projectRoot, paths) {
9
+ const lock = (0, lockfile_1.readFilesLock)(projectRoot);
10
+ if (!lock)
11
+ throw new inspect_1.NotAProjectError();
12
+ const result = { ejected: [], unknown: [] };
13
+ for (const path of paths) {
14
+ const entry = lock.files[path];
15
+ if (!entry || !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, path))) {
16
+ result.unknown.push(path);
17
+ continue;
18
+ }
19
+ if (entry.tier !== "owned") {
20
+ entry.tier = "owned";
21
+ result.ejected.push(path);
22
+ }
23
+ }
24
+ if (result.ejected.length)
25
+ (0, lockfile_1.writeFilesLock)(projectRoot, lock);
26
+ return result;
27
+ }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ interface ParsedArgs {
6
6
  template?: string;
7
7
  dir?: string;
8
8
  pm?: PackageManager;
9
+ from?: string;
10
+ apply: boolean;
9
11
  yes: boolean;
10
12
  help: boolean;
11
13
  }
package/dist/index.js CHANGED
@@ -8,11 +8,19 @@ const create_1 = require("./create");
8
8
  const prompt_1 = require("./prompt");
9
9
  const templates_1 = require("./templates");
10
10
  const add_1 = require("./add");
11
+ const inspect_1 = require("./inspect");
12
+ const update_1 = require("./update");
13
+ const eject_1 = require("./eject");
11
14
  const HELP = `podo — PodoKit project generator
12
15
 
13
16
  Usage:
14
17
  podo create <name> [options]
15
18
  podo add <module>
19
+ podo status Show version, modules, file tiers, and local edits
20
+ podo diff List PodoKit-managed files you have edited
21
+ podo doctor Check framework versions against supported ranges
22
+ podo update [--apply] Preview (or apply) what a version update would change
23
+ podo eject <path...> Take ownership of managed files (update skips them)
16
24
 
17
25
  Options:
18
26
  --template <t> Template to scaffold (see below)
@@ -30,7 +38,7 @@ Example:
30
38
  cd my-app && npx @podosoft/podokit add auth
31
39
  `;
32
40
  function parseArgs(argv) {
33
- const parsed = { help: false, yes: false };
41
+ const parsed = { help: false, yes: false, apply: false };
34
42
  const positionals = [];
35
43
  for (let i = 0; i < argv.length; i += 1) {
36
44
  const arg = argv[i];
@@ -40,12 +48,18 @@ function parseArgs(argv) {
40
48
  else if (arg === "-y" || arg === "--yes") {
41
49
  parsed.yes = true;
42
50
  }
51
+ else if (arg === "--apply") {
52
+ parsed.apply = true;
53
+ }
43
54
  else if (arg === "--template") {
44
55
  parsed.template = argv[++i];
45
56
  }
46
57
  else if (arg === "--dir") {
47
58
  parsed.dir = argv[++i];
48
59
  }
60
+ else if (arg === "--from") {
61
+ parsed.from = argv[++i];
62
+ }
49
63
  else if (arg === "--pm") {
50
64
  parsed.pm = argv[++i];
51
65
  }
@@ -93,6 +107,113 @@ async function main(argv) {
93
107
  }
94
108
  return;
95
109
  }
110
+ if (args.command === "status") {
111
+ try {
112
+ const s = (0, inspect_1.status)(process.cwd());
113
+ const tiers = `managed ${s.tiers.managed}, assembled ${s.tiers.assembled}, owned ${s.tiers.owned}`;
114
+ process.stdout.write(`PodoKit ${s.podokitVersion} (template: ${s.template}, ${s.packageManager})\n` +
115
+ `Modules: ${s.modules.length ? s.modules.join(", ") : "(none)"}\n` +
116
+ `Files: ${tiers}\n` +
117
+ `Edited: ${s.drifted.length} managed file(s)${s.missing.length ? `, ${s.missing.length} missing` : ""}\n` +
118
+ (s.drifted.length ? s.drifted.map((f) => ` ~ ${f}`).join("\n") + "\n" : ""));
119
+ }
120
+ catch (err) {
121
+ fail(err.message);
122
+ }
123
+ return;
124
+ }
125
+ if (args.command === "diff") {
126
+ try {
127
+ const { drifted, missing } = (0, inspect_1.diff)(process.cwd());
128
+ if (!drifted.length && !missing.length) {
129
+ process.stdout.write("No local edits to PodoKit-managed files.\n");
130
+ }
131
+ else {
132
+ process.stdout.write([...drifted.map((f) => `edited ${f}`), ...missing.map((f) => `missing ${f}`)].join("\n") + "\n");
133
+ }
134
+ }
135
+ catch (err) {
136
+ fail(err.message);
137
+ }
138
+ return;
139
+ }
140
+ if (args.command === "doctor") {
141
+ try {
142
+ const findings = (0, inspect_1.doctor)(process.cwd());
143
+ if (!findings.length) {
144
+ process.stdout.write("No known frameworks found to check.\n");
145
+ }
146
+ else {
147
+ for (const f of findings) {
148
+ const mark = f.ok ? "ok " : "WARN";
149
+ process.stdout.write(`${mark} ${f.package} ${f.installed} (supported: ${f.supported})\n`);
150
+ }
151
+ }
152
+ if (findings.some((f) => !f.ok)) {
153
+ process.stdout.write("\nSome frameworks are outside the supported range; @podosoft/* extensions may not match.\n");
154
+ }
155
+ }
156
+ catch (err) {
157
+ fail(err.message);
158
+ }
159
+ return;
160
+ }
161
+ if (args.command === "update") {
162
+ const templatesDir = (0, node_path_1.join)(__dirname, "templates");
163
+ try {
164
+ if (args.apply) {
165
+ const result = (0, update_1.applyUpdate)(process.cwd(), templatesDir, { oldTemplatesDir: args.from });
166
+ process.stdout.write(`Applied: ${result.written.length} written, ${result.removed.length} removed, ` +
167
+ `${result.merged.length} merged, ${result.conflicts.length} conflict.\n`);
168
+ if (result.conflicts.length) {
169
+ process.stdout.write("\nResolve the following, then commit:\n" +
170
+ result.conflicts.map((f) => ` ${f}`).join("\n") +
171
+ "\n");
172
+ }
173
+ return;
174
+ }
175
+ const plan = (0, update_1.planUpdate)(process.cwd(), templatesDir);
176
+ const counts = (0, update_1.summarize)(plan);
177
+ const shown = plan.changes.filter((c) => c.action !== "up-to-date" && c.action !== "skip");
178
+ process.stdout.write(`podo update ${plan.fromVersion} -> ${plan.toVersion} (template: ${plan.template}; modules: ${plan.modules.join(", ") || "none"})\n\n`);
179
+ if (!shown.length) {
180
+ process.stdout.write("Everything is up to date.\n");
181
+ }
182
+ else {
183
+ for (const c of shown) {
184
+ process.stdout.write(` ${c.action.padEnd(9)} ${c.path} (${c.note})\n`);
185
+ }
186
+ process.stdout.write(`\n${counts.update} update, ${counts.add} add, ${counts.remove} remove, ${counts.conflict} conflict. ` +
187
+ `Dry-run — nothing was written. Re-run with --apply to write (use --from <dir> for a 3-way merge).\n`);
188
+ }
189
+ }
190
+ catch (err) {
191
+ fail(err.message);
192
+ }
193
+ return;
194
+ }
195
+ if (args.command === "eject") {
196
+ const targets = argv.filter((a) => !a.startsWith("-")).slice(1);
197
+ if (!targets.length) {
198
+ fail("Usage: podo eject <path...>");
199
+ }
200
+ try {
201
+ const result = (0, eject_1.eject)(process.cwd(), targets);
202
+ if (result.ejected.length) {
203
+ process.stdout.write(`Ejected (now owned): ${result.ejected.join(", ")}\n`);
204
+ }
205
+ if (result.unknown.length) {
206
+ process.stdout.write(`Not tracked, skipped: ${result.unknown.join(", ")}\n`);
207
+ }
208
+ if (!result.ejected.length && !result.unknown.length) {
209
+ process.stdout.write("Nothing to eject (already owned).\n");
210
+ }
211
+ }
212
+ catch (err) {
213
+ fail(err.message);
214
+ }
215
+ return;
216
+ }
96
217
  if (args.command !== "create") {
97
218
  fail(`Unknown command "${args.command}". Run "podo --help".`);
98
219
  }
@@ -0,0 +1,43 @@
1
+ import { type Tier } from "./lockfile";
2
+ /**
3
+ * Read-only project inspection for `podo status`, `podo diff`, and
4
+ * `podo doctor`. None of these write to disk.
5
+ */
6
+ export interface StatusReport {
7
+ podokitVersion: string;
8
+ template: string;
9
+ packageManager: string;
10
+ modules: string[];
11
+ tiers: Record<Tier, number>;
12
+ drifted: string[];
13
+ missing: string[];
14
+ }
15
+ export declare class NotAProjectError extends Error {
16
+ constructor();
17
+ }
18
+ export declare function status(projectRoot: string): StatusReport;
19
+ /** Managed/assembled files the user has edited or deleted since generation. */
20
+ export declare function diff(projectRoot: string): {
21
+ drifted: string[];
22
+ missing: string[];
23
+ };
24
+ /** Framework version-compatibility check. See ADR-0008 (4-layer defense). */
25
+ export interface DoctorFinding {
26
+ package: string;
27
+ installed: string;
28
+ supported: string;
29
+ ok: boolean;
30
+ }
31
+ /**
32
+ * Known-good framework ranges for the current PodoKit line. A generated app may
33
+ * bump these itself; doctor warns when it moves outside the supported range so
34
+ * `@podosoft/*` extensions still match.
35
+ */
36
+ export declare const SUPPORTED_FRAMEWORKS: Record<string, {
37
+ app: "api" | "web";
38
+ range: string;
39
+ majors: number[];
40
+ }>;
41
+ /** Parse the leading major from a semver range like `^11.1.0` or `>=1.6.23 <1.7`. */
42
+ export declare function leadingMajor(range: string): number | null;
43
+ export declare function doctor(projectRoot: string): DoctorFinding[];
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUPPORTED_FRAMEWORKS = exports.NotAProjectError = void 0;
4
+ exports.status = status;
5
+ exports.diff = diff;
6
+ exports.leadingMajor = leadingMajor;
7
+ exports.doctor = doctor;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const lockfile_1 = require("./lockfile");
11
+ class NotAProjectError extends Error {
12
+ constructor() {
13
+ super("No .podokit/manifest.json found. Run inside a project created by `podo create`.");
14
+ this.name = "NotAProjectError";
15
+ }
16
+ }
17
+ exports.NotAProjectError = NotAProjectError;
18
+ function status(projectRoot) {
19
+ const manifest = (0, lockfile_1.readManifest)(projectRoot);
20
+ const lock = (0, lockfile_1.readFilesLock)(projectRoot);
21
+ if (!manifest || !lock)
22
+ throw new NotAProjectError();
23
+ const tiers = { managed: 0, assembled: 0, owned: 0 };
24
+ for (const entry of Object.values(lock.files))
25
+ tiers[entry.tier] += 1;
26
+ const { drifted, missing } = (0, lockfile_1.computeDrift)(projectRoot);
27
+ return {
28
+ podokitVersion: manifest.podokitVersion,
29
+ template: manifest.template,
30
+ packageManager: manifest.packageManager,
31
+ modules: manifest.modules.map((m) => m.name),
32
+ tiers,
33
+ drifted,
34
+ missing,
35
+ };
36
+ }
37
+ /** Managed/assembled files the user has edited or deleted since generation. */
38
+ function diff(projectRoot) {
39
+ if (!(0, lockfile_1.readManifest)(projectRoot))
40
+ throw new NotAProjectError();
41
+ return (0, lockfile_1.computeDrift)(projectRoot);
42
+ }
43
+ /**
44
+ * Known-good framework ranges for the current PodoKit line. A generated app may
45
+ * bump these itself; doctor warns when it moves outside the supported range so
46
+ * `@podosoft/*` extensions still match.
47
+ */
48
+ exports.SUPPORTED_FRAMEWORKS = {
49
+ "@nestjs/core": { app: "api", range: "^11", majors: [11] },
50
+ "better-auth": { app: "api", range: ">=1.6.23 <1.7", majors: [1] },
51
+ svelte: { app: "web", range: "^5", majors: [5] },
52
+ };
53
+ function declaredVersion(projectRoot, app, pkg) {
54
+ const file = (0, node_path_1.join)(projectRoot, "apps", app, "package.json");
55
+ if (!(0, node_fs_1.existsSync)(file))
56
+ return null;
57
+ const json = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
58
+ return json.dependencies?.[pkg] ?? json.devDependencies?.[pkg] ?? null;
59
+ }
60
+ /** Parse the leading major from a semver range like `^11.1.0` or `>=1.6.23 <1.7`. */
61
+ function leadingMajor(range) {
62
+ const match = range.match(/(\d+)\./);
63
+ return match ? Number(match[1]) : null;
64
+ }
65
+ function doctor(projectRoot) {
66
+ if (!(0, lockfile_1.readManifest)(projectRoot))
67
+ throw new NotAProjectError();
68
+ const findings = [];
69
+ for (const [pkg, spec] of Object.entries(exports.SUPPORTED_FRAMEWORKS)) {
70
+ const installed = declaredVersion(projectRoot, spec.app, pkg);
71
+ if (installed === null)
72
+ continue; // package not in this app
73
+ const major = leadingMajor(installed);
74
+ findings.push({
75
+ package: pkg,
76
+ installed,
77
+ supported: spec.range,
78
+ ok: major !== null && spec.majors.includes(major),
79
+ });
80
+ }
81
+ return findings;
82
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The generation lockfile (`.podokit/`) records how a project was assembled so
3
+ * `podo update` can rebuild the same inputs and detect user edits. See
4
+ * ADR-0009 (update mechanism) and ADR-0010 (ownership model).
5
+ */
6
+ export declare const LOCK_SCHEMA_VERSION = 1;
7
+ /** File ownership tier. See ADR-0010. */
8
+ export type Tier = "managed" | "assembled" | "owned";
9
+ export interface ManifestModule {
10
+ name: string;
11
+ /** Application order; preserves layered injection determinism. */
12
+ order: number;
13
+ /** PodoKit version that applied the module. */
14
+ addedWith: string;
15
+ }
16
+ export interface PodokitManifest {
17
+ schemaVersion: number;
18
+ podokitVersion: string;
19
+ template: string;
20
+ packageManager: string;
21
+ /** Render vars / prompt answers, replayed on update (copier-style). */
22
+ answers: Record<string, string>;
23
+ modules: ManifestModule[];
24
+ /** Globs whose files are user-owned and never touched by update. */
25
+ ownedGlobs: string[];
26
+ }
27
+ export interface FileEntry {
28
+ tier: Tier;
29
+ /** Hash of the exact bytes PodoKit last wrote; user-edit detector. */
30
+ outHash: string;
31
+ }
32
+ export interface FilesLock {
33
+ schemaVersion: number;
34
+ files: Record<string, FileEntry>;
35
+ }
36
+ /** Default user-owned globs seeded into a new project. See ADR-0010. */
37
+ export declare const DEFAULT_OWNED_GLOBS: string[];
38
+ export declare function manifestPath(projectRoot: string): string;
39
+ export declare function filesLockPath(projectRoot: string): string;
40
+ export declare function readManifest(projectRoot: string): PodokitManifest | null;
41
+ export declare function writeManifest(projectRoot: string, manifest: PodokitManifest): void;
42
+ export declare function writeFilesLock(projectRoot: string, lock: FilesLock): void;
43
+ /** Read the PodoKit CLI's own version to stamp into the manifest. */
44
+ export declare function podokitVersion(): string;
45
+ /** Match a POSIX relative path against a simple glob supporting `*` and `**`. */
46
+ export declare function matchGlob(path: string, glob: string): boolean;
47
+ /** Classify a file's ownership tier. See ADR-0010 priority order. */
48
+ export declare function classifyTier(relPath: string, content: string | Buffer, ownedGlobs: string[]): Tier;
49
+ /** List project files as POSIX-relative paths, skipping generated/vendored dirs. */
50
+ export declare function walkFiles(projectRoot: string, prefix?: string): string[];
51
+ /** Recompute the files.lock by hashing and classifying every project file. */
52
+ export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[]): FilesLock;
53
+ export declare function readFilesLock(projectRoot: string): FilesLock | null;
54
+ export interface Drift {
55
+ /** Managed/assembled files whose content differs from what PodoKit wrote. */
56
+ drifted: string[];
57
+ /** Locked files that no longer exist on disk. */
58
+ missing: string[];
59
+ }
60
+ /**
61
+ * Compare the current disk against the lock: report managed/assembled files the
62
+ * user has edited (hash mismatch) or deleted. Owned files are never reported —
63
+ * they belong to the user. Returns empty drift when there is no lock.
64
+ */
65
+ export declare function computeDrift(projectRoot: string): Drift;
66
+ export interface InitLockOptions {
67
+ template: string;
68
+ packageManager: string;
69
+ answers: Record<string, string>;
70
+ version?: string;
71
+ ownedGlobs?: string[];
72
+ }
73
+ /** Write the initial `.podokit/` lockfile for a freshly created project. */
74
+ export declare function initLockfile(projectRoot: string, options: InitLockOptions): void;
75
+ /**
76
+ * Record applied modules in the manifest and recompute the files.lock. Modules
77
+ * already present are left in place (idempotent); the lock always reflects disk.
78
+ * No-op on projects without a manifest (pre-lockfile projects; see `podo adopt`).
79
+ */
80
+ export declare function recordModules(projectRoot: string, moduleNames: string[], version?: string): void;