@podosoft/podokit 1.0.2 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +19 -5
  2. package/dist/add.d.ts +14 -1
  3. package/dist/add.js +42 -5
  4. package/dist/create.d.ts +4 -0
  5. package/dist/create.js +9 -0
  6. package/dist/deploy-compose-profile.d.ts +3 -0
  7. package/dist/deploy-compose-profile.js +74 -8
  8. package/dist/deploy-compose-render.d.ts +1 -0
  9. package/dist/deploy-compose-render.js +46 -1
  10. package/dist/deploy-compose.js +2 -1
  11. package/dist/deploy-profile.js +18 -8
  12. package/dist/deploy.js +157 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +41 -0
  15. package/dist/lib.d.ts +2 -0
  16. package/dist/lib.js +8 -2
  17. package/dist/lockfile.d.ts +6 -1
  18. package/dist/lockfile.js +22 -4
  19. package/dist/provider.d.ts +24 -0
  20. package/dist/provider.js +142 -0
  21. package/dist/remove.js +6 -3
  22. package/dist/templates/fullstack/AGENTS.md +5 -4
  23. package/dist/templates/fullstack/README.md +10 -3
  24. package/dist/templates/fullstack/apps/api/package.json +3 -2
  25. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +1 -0
  26. package/dist/templates/fullstack/apps/api/src/config/providers.ts +9 -0
  27. package/dist/templates/fullstack/apps/api/src/core/services.ts +7 -2
  28. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +30 -8
  29. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +58 -0
  30. package/dist/templates/fullstack/apps/api/src/database/database.ts +58 -2
  31. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +24 -2
  32. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.spec.ts +50 -0
  33. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +134 -0
  34. package/dist/templates/fullstack/compose.dev.yaml +2 -2
  35. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
  36. package/dist/templates/fullstack/dot-env.example +2 -0
  37. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +2 -2
  38. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +10 -4
  39. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +10 -4
  40. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +10 -4
  41. package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -5
  42. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +2 -5
  43. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +33 -0
  44. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +13 -0
  45. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +17 -8
  46. package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +5 -4
  47. package/dist/templates/modules/audit-log/module.manifest.json +1 -1
  48. package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.spec.ts +170 -0
  49. package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.ts +260 -0
  50. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +2 -2
  51. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +4 -0
  52. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +2 -2
  53. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +4 -4
  54. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +55 -10
  55. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +10 -14
  56. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +22 -6
  57. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +2 -1
  58. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +4 -3
  59. package/dist/templates/modules/auth/files/apps/api/src/settings/app-settings.migration.spec.ts +15 -4
  60. package/dist/templates/modules/auth/files/apps/api/src/settings/auth-config.migration.spec.ts +30 -0
  61. package/dist/templates/modules/auth/module.manifest.json +1 -1
  62. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +10 -0
  63. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +107 -0
  64. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +12 -11
  65. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +5 -0
  66. package/dist/templates/modules/bullmq/module.manifest.json +7 -0
  67. package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.spec.ts +16 -0
  68. package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.ts +12 -0
  69. package/dist/templates/modules/cache-memory/module.manifest.json +30 -0
  70. package/dist/templates/modules/events-memory/files/apps/api/src/events-provider-memory/events-memory.module.ts +14 -0
  71. package/dist/templates/modules/events-memory/module.manifest.json +30 -0
  72. package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +16 -0
  73. package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +75 -0
  74. package/dist/templates/modules/events-redis/module.manifest.json +32 -0
  75. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -5
  76. package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
  77. package/dist/templates/modules/file-upload/module.manifest.json +4 -3
  78. package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +36 -14
  79. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +9 -32
  80. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +16 -11
  81. package/dist/templates/modules/job-progress/module.manifest.json +7 -8
  82. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +42 -0
  83. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +45 -0
  84. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +212 -0
  85. package/dist/templates/modules/jobs-local/module.manifest.json +34 -0
  86. package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +7 -3
  87. package/dist/templates/modules/mailer/module.manifest.json +2 -4
  88. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +25 -0
  89. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +60 -0
  90. package/dist/templates/modules/object-storage-local/module.manifest.json +34 -0
  91. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +50 -0
  92. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +8 -0
  93. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +4 -4
  94. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +20 -12
  95. package/dist/templates/modules/object-storage-s3/module.manifest.json +4 -2
  96. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +14 -1
  97. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +18 -37
  98. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +12 -0
  99. package/dist/templates/modules/rate-limit/module.manifest.json +8 -6
  100. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +82 -0
  101. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +6 -0
  102. package/dist/templates/modules/redis/module.manifest.json +2 -0
  103. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +5 -6
  104. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +4 -4
  105. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +6 -5
  106. package/dist/templates/modules/sse/module.manifest.json +4 -6
  107. package/dist/templates/todo/AGENTS.md +1 -1
  108. package/dist/templates/todo/README.md +8 -3
  109. package/package.json +2 -1
  110. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +0 -32
  111. package/dist/templates/modules/mailer/files/apps/api/src/mail/db.ts +0 -13
  112. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +0 -111
package/dist/deploy.js CHANGED
@@ -491,6 +491,154 @@ function helmFailureFlag(runner) {
491
491
  const version = checked(runner, "helm", ["version", "--short"]).trim();
492
492
  return version.startsWith("v4.") ? "--rollback-on-failure" : "--atomic";
493
493
  }
494
+ function isRecord(value) {
495
+ return typeof value === "object" && value !== null && !Array.isArray(value);
496
+ }
497
+ function managedStorageImage(resource, profile) {
498
+ const name = `${profile.target.release}-minio`;
499
+ if (!isRecord(resource) ||
500
+ resource.apiVersion !== "apps/v1" ||
501
+ resource.kind !== "StatefulSet" ||
502
+ !isRecord(resource.metadata) ||
503
+ resource.metadata.name !== name ||
504
+ resource.metadata.namespace !== profile.target.namespace ||
505
+ !isRecord(resource.spec) ||
506
+ !isRecord(resource.spec.template) ||
507
+ !isRecord(resource.spec.template.spec) ||
508
+ !Array.isArray(resource.spec.template.spec.containers)) {
509
+ return null;
510
+ }
511
+ const containers = resource.spec.template.spec.containers;
512
+ const container = containers[0];
513
+ if (containers.length !== 1 ||
514
+ !isRecord(container) ||
515
+ container.name !== "minio" ||
516
+ typeof container.image !== "string" ||
517
+ !container.image.trim()) {
518
+ return null;
519
+ }
520
+ return container.image;
521
+ }
522
+ function jsonObjectStream(output) {
523
+ const documents = [];
524
+ let start = -1;
525
+ let depth = 0;
526
+ let quoted = false;
527
+ let escaped = false;
528
+ for (let index = 0; index < output.length; index += 1) {
529
+ const character = output[index];
530
+ if (start < 0) {
531
+ if (/\s/.test(character ?? ""))
532
+ continue;
533
+ if (character !== "{")
534
+ return null;
535
+ start = index;
536
+ }
537
+ if (quoted) {
538
+ if (escaped)
539
+ escaped = false;
540
+ else if (character === "\\")
541
+ escaped = true;
542
+ else if (character === '"')
543
+ quoted = false;
544
+ continue;
545
+ }
546
+ if (character === '"')
547
+ quoted = true;
548
+ else if (character === "{")
549
+ depth += 1;
550
+ else if (character === "}")
551
+ depth -= 1;
552
+ if (depth === 0) {
553
+ try {
554
+ documents.push(JSON.parse(output.slice(start, index + 1)));
555
+ }
556
+ catch {
557
+ return null;
558
+ }
559
+ start = -1;
560
+ }
561
+ }
562
+ return start < 0 && documents.length > 0 ? documents : null;
563
+ }
564
+ function recordedStorageImage(profile, runner) {
565
+ const manifest = runner("helm", helmArgs(profile, ["get", "manifest", `${profile.target.release}-dependencies`]), { capture: true });
566
+ if (manifest.status !== 0 || !manifest.stdout.trim())
567
+ return null;
568
+ const temporaryRoot = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "podokit-storage-manifest-"));
569
+ try {
570
+ const path = (0, node_path_1.join)(temporaryRoot, "dependencies.yaml");
571
+ (0, node_fs_1.writeFileSync)(path, manifest.stdout, { mode: 0o600 });
572
+ // Client dry-run parses Helm's YAML without submitting a create request to Kubernetes.
573
+ const parsed = runner("kubectl", kubectlArgs(profile, [
574
+ "create", "--dry-run=client", "--validate=false", "-f", path, "-o", "json",
575
+ ]), { capture: true });
576
+ if (parsed.status !== 0)
577
+ return null;
578
+ // kubectl prints consecutive JSON objects for a manifest containing multiple resources.
579
+ const documents = jsonObjectStream(parsed.stdout);
580
+ if (!documents)
581
+ return null;
582
+ const resources = [];
583
+ for (const document of documents) {
584
+ if (!isRecord(document))
585
+ return null;
586
+ if (document.kind !== "List")
587
+ resources.push(document);
588
+ else if (document.apiVersion === "v1" && Array.isArray(document.items)) {
589
+ resources.push(...document.items);
590
+ }
591
+ else
592
+ return null;
593
+ }
594
+ const name = `${profile.target.release}-minio`;
595
+ const matches = resources.filter((item) => isRecord(item) && item.kind === "StatefulSet" &&
596
+ isRecord(item.metadata) && item.metadata.name === name);
597
+ return matches.length === 1 ? managedStorageImage(matches[0], profile) : null;
598
+ }
599
+ finally {
600
+ (0, node_fs_1.rmSync)(temporaryRoot, { recursive: true, force: true });
601
+ }
602
+ }
603
+ function dependencyRollbackWarning(profile, plan, runner) {
604
+ if (profile.dependencies.objectStorage.mode !== "inCluster")
605
+ return null;
606
+ const name = `${profile.target.release}-minio`;
607
+ const current = runner("kubectl", kubectlArgs(profile, ["get", "statefulset", name, "--ignore-not-found=true", "-o", "json"]), { capture: true });
608
+ if (current.status !== 0)
609
+ return "the current managed storage image could not be inspected";
610
+ if (!current.stdout.trim()) {
611
+ return plan.dependencyRevision === null
612
+ ? null
613
+ : "the existing managed storage StatefulSet is missing";
614
+ }
615
+ let resource;
616
+ try {
617
+ resource = JSON.parse(current.stdout);
618
+ }
619
+ catch {
620
+ return "the managed storage StatefulSet has an invalid schema";
621
+ }
622
+ const currentImage = managedStorageImage(resource, profile);
623
+ if (!currentImage || typeof plan.images.objectStorage !== "string") {
624
+ return "the current managed storage image could not be confirmed";
625
+ }
626
+ if (currentImage !== plan.images.objectStorage) {
627
+ return "the managed storage server image is changing";
628
+ }
629
+ // A failed revision can already contain the new image while Helm's rollback target uses the old one.
630
+ if (plan.dependencyRevision !== null && plan.dependencyStatus !== "deployed") {
631
+ return "the previous dependency release is not deployed";
632
+ }
633
+ if (plan.dependencyRevision !== null) {
634
+ const recordedImage = recordedStorageImage(profile, runner);
635
+ if (!recordedImage)
636
+ return "the storage image recorded by Helm could not be confirmed";
637
+ if (recordedImage !== currentImage)
638
+ return "the storage image recorded by Helm differs from the current image";
639
+ }
640
+ return null;
641
+ }
494
642
  function waitForDependencies(profile, runner) {
495
643
  const release = profile.target.release;
496
644
  const resources = [];
@@ -603,7 +751,13 @@ async function applyDeployment(projectRoot, profileName, release, confirmation,
603
751
  }
604
752
  const runtime = (0, deploy_render_1.renderDeployment)(projectRoot, profileName, profile, release, plan.images, plan.rolloutStateDigest);
605
753
  const failureFlag = helmFailureFlag(runner);
606
- const common = ["--wait", "--timeout", "5m", failureFlag];
754
+ const common = ["--wait", "--timeout", "5m"];
755
+ const rollbackWarning = dependencyRollbackWarning(profile, plan, runner);
756
+ if (rollbackWarning) {
757
+ console.warn(`Disable automatic dependency rollback because ${rollbackWarning}. ` +
758
+ "If the upgrade fails, stop storage and restore the complete data and IAM backup before starting the previous image. " +
759
+ "Application upgrades retain automatic rollback.");
760
+ }
607
761
  if (profile.dependencies.objectStorage.mode === "inCluster") {
608
762
  checked(runner, "kubectl", kubectlArgs(profile, [
609
763
  "delete",
@@ -620,6 +774,7 @@ async function applyDeployment(projectRoot, profileName, release, confirmation,
620
774
  `${profile.target.release}-dependencies`,
621
775
  runtime.dependencyChart,
622
776
  ...common,
777
+ ...(rollbackWarning ? [] : [failureFlag]),
623
778
  ]), false);
624
779
  waitForDependencies(profile, runner);
625
780
  const job = migrationJobName(profile, release);
@@ -632,6 +787,7 @@ async function applyDeployment(projectRoot, profileName, release, confirmation,
632
787
  profile.target.release,
633
788
  runtime.applicationChart,
634
789
  ...common,
790
+ failureFlag,
635
791
  ]), false);
636
792
  for (const kind of [
637
793
  "api",
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ interface ParsedArgs {
8
8
  dir?: string;
9
9
  pm?: string;
10
10
  runtime?: string;
11
+ database?: string;
11
12
  from?: string;
12
13
  apply: boolean;
13
14
  adopt: boolean;
package/dist/index.js CHANGED
@@ -22,11 +22,14 @@ const deploy_compose_profile_1 = require("./deploy-compose-profile");
22
22
  const deploy_compose_render_1 = require("./deploy-compose-render");
23
23
  const deploy_compose_1 = require("./deploy-compose");
24
24
  const deploy_compose_sync_1 = require("./deploy-compose-sync");
25
+ const provider_1 = require("./provider");
25
26
  const HELP = `podo — PodoKit project generator
26
27
 
27
28
  Usage:
28
29
  podo create <name> [options]
29
30
  podo add <module> [--adopt]
31
+ podo provider list Show active runtime providers
32
+ podo provider set <capability> <provider> [--apply]
30
33
  podo remove <module> Un-apply a module (inverse of add)
31
34
  podo status Show version, modules, file tiers, and local edits
32
35
  podo diff List PodoKit-managed files you have edited
@@ -42,6 +45,7 @@ Options:
42
45
  --template <t> Template to scaffold (see below)
43
46
  --dir <path> Target directory (default: ./<name>)
44
47
  --runtime bun Optional explicit Bun runtime selection
48
+ --database <provider> Database provider: postgres | sqlite
45
49
  --name <label> Display name for a locale
46
50
  --direction <direction> Text direction: ltr | rtl (default: ltr)
47
51
  --profile <name> Deployment profile name
@@ -143,6 +147,9 @@ function parseArgs(argv) {
143
147
  else if (arg === "--runtime") {
144
148
  parsed.runtime = argv[++i];
145
149
  }
150
+ else if (arg === "--database") {
151
+ parsed.database = argv[++i];
152
+ }
146
153
  else if (arg === "--name") {
147
154
  parsed.localeName = argv[++i];
148
155
  }
@@ -335,6 +342,39 @@ async function main(argv) {
335
342
  return;
336
343
  }
337
344
  const modulesDir = (0, node_path_1.join)(__dirname, "templates", "modules");
345
+ if (args.command === "provider") {
346
+ const action = args.positionals[1] ?? "list";
347
+ try {
348
+ if (action === "list") {
349
+ const providers = (0, provider_1.listProviders)(process.cwd());
350
+ process.stdout.write(providers.map((item) => `${item.capability.padEnd(14)} ${item.selected.padEnd(10)} (${item.available.join(", ")})`).join("\n") + "\n");
351
+ return;
352
+ }
353
+ if (action === "set") {
354
+ const capability = args.positionals[2];
355
+ const provider = args.positionals[3];
356
+ if (!capability || !provider) {
357
+ fail("Usage: podo provider set <capability> <provider> [--apply]");
358
+ }
359
+ const plan = args.apply
360
+ ? (0, provider_1.applyProviderChange)(process.cwd(), capability, provider, { modulesDir })
361
+ : (0, provider_1.planProviderChange)(process.cwd(), capability, provider, modulesDir);
362
+ process.stdout.write(`${plan.capability}: ${plan.from} -> ${plan.to}${plan.changed ? "" : " (unchanged)"}\n` +
363
+ plan.modulesToAdd.map((module) => ` ${args.apply ? "add" : "would add"} module ${module}`).join("\n") +
364
+ (plan.modulesToAdd.length ? "\n" : "") +
365
+ plan.files.map((file) => ` ${args.apply ? "write" : "would write"} ${file}`).join("\n") +
366
+ (plan.files.length ? "\n" : "") +
367
+ plan.warnings.map((warning) => `warning: ${warning}`).join("\n") +
368
+ (plan.warnings.length ? "\n" : "") +
369
+ (args.apply ? "Applied provider configuration.\n" : "Dry-run — nothing was written. Re-run with --apply to write.\n"));
370
+ return;
371
+ }
372
+ fail(`Unknown provider command "${action}". Use list or set.`);
373
+ }
374
+ catch (err) {
375
+ fail(err.message);
376
+ }
377
+ }
338
378
  if (args.command === "add") {
339
379
  const moduleName = args.name;
340
380
  if (!moduleName) {
@@ -695,6 +735,7 @@ async function main(argv) {
695
735
  targetDir: args.dir,
696
736
  runtime: resolved.toolchain.runtime,
697
737
  ai: args.ai,
738
+ database: args.database === undefined ? undefined : args.database,
698
739
  });
699
740
  const relPath = (0, node_path_1.relative)(process.cwd(), result.projectDir) || ".";
700
741
  const rel = relPath.startsWith("..") ? result.projectDir : relPath;
package/dist/lib.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { create } from "./create";
2
2
  export type { CreateOptions, CreateResult } from "./create";
3
+ export { applyProviderChange, listProviders, planProviderChange, providerSource, PROVIDERS_SOURCE, } from "./provider";
4
+ export type { ProviderChangePlan, ProviderListItem } from "./provider";
3
5
  export { TEMPLATES, DEFAULT_TEMPLATE, TEMPLATE_NAMES, isKnownTemplate } from "./templates";
4
6
  export type { TemplateInfo } from "./templates";
5
7
  export { listModules, addModule } from "./add";
package/dist/lib.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = exports.getComposeStatus = exports.doctorComposeDeployment = exports.applyComposeDeployment = exports.renderComposeDeployment = exports.parseComposeProfile = exports.initializeComposeProfile = exports.readDeploymentDriver = exports.loadKubernetesProfile = exports.loadComposeProfile = exports.loadAnyDeploymentProfile = exports.listAnyDeploymentProfiles = exports.assertAnyReleaseTag = exports.DEPLOY_DRIVERS = exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.create = void 0;
4
- exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = void 0;
3
+ exports.getComposeStatus = exports.doctorComposeDeployment = exports.applyComposeDeployment = exports.renderComposeDeployment = exports.parseComposeProfile = exports.initializeComposeProfile = exports.readDeploymentDriver = exports.loadKubernetesProfile = exports.loadComposeProfile = exports.loadAnyDeploymentProfile = exports.listAnyDeploymentProfiles = exports.assertAnyReleaseTag = exports.DEPLOY_DRIVERS = exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.PROVIDERS_SOURCE = exports.providerSource = exports.planProviderChange = exports.listProviders = exports.applyProviderChange = exports.create = void 0;
4
+ exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = void 0;
5
5
  exports.builtinTemplatesDir = builtinTemplatesDir;
6
6
  exports.builtinModulesDir = builtinModulesDir;
7
7
  // Library entry point: the reusable functions behind the `podo` CLI, for other
@@ -9,6 +9,12 @@ exports.builtinModulesDir = builtinModulesDir;
9
9
  const node_path_1 = require("node:path");
10
10
  var create_1 = require("./create");
11
11
  Object.defineProperty(exports, "create", { enumerable: true, get: function () { return create_1.create; } });
12
+ var provider_1 = require("./provider");
13
+ Object.defineProperty(exports, "applyProviderChange", { enumerable: true, get: function () { return provider_1.applyProviderChange; } });
14
+ Object.defineProperty(exports, "listProviders", { enumerable: true, get: function () { return provider_1.listProviders; } });
15
+ Object.defineProperty(exports, "planProviderChange", { enumerable: true, get: function () { return provider_1.planProviderChange; } });
16
+ Object.defineProperty(exports, "providerSource", { enumerable: true, get: function () { return provider_1.providerSource; } });
17
+ Object.defineProperty(exports, "PROVIDERS_SOURCE", { enumerable: true, get: function () { return provider_1.PROVIDERS_SOURCE; } });
12
18
  var templates_1 = require("./templates");
13
19
  Object.defineProperty(exports, "TEMPLATES", { enumerable: true, get: function () { return templates_1.TEMPLATES; } });
14
20
  Object.defineProperty(exports, "DEFAULT_TEMPLATE", { enumerable: true, get: function () { return templates_1.DEFAULT_TEMPLATE; } });
@@ -1,11 +1,12 @@
1
1
  import type { TemplateVars } from "@podosoft/podokit-template-engine";
2
+ import { type ProviderSelections } from "@podosoft/podokit-runtime";
2
3
  import { type Toolchain } from "./toolchain";
3
4
  /**
4
5
  * The generation lockfile (`.podokit/`) records how a project was assembled so
5
6
  * `podo update` can rebuild the same inputs and detect user edits. See
6
7
  * ADR-0009 (update mechanism) and ADR-0010 (ownership model).
7
8
  */
8
- export declare const LOCK_SCHEMA_VERSION = 3;
9
+ export declare const LOCK_SCHEMA_VERSION = 4;
9
10
  /** File ownership tier. See ADR-0010. */
10
11
  export type Tier = "managed" | "assembled" | "owned";
11
12
  export interface ManifestModule {
@@ -35,6 +36,8 @@ export interface PodokitManifest {
35
36
  toolchain: Toolchain;
36
37
  /** Render vars / prompt answers, replayed on update (copier-style). */
37
38
  answers: Record<string, string>;
39
+ /** Active implementation for each provider-neutral runtime capability. */
40
+ providers: ProviderSelections;
38
41
  modules: ManifestModule[];
39
42
  /** Globs whose files are user-owned and never touched by update. */
40
43
  ownedGlobs: string[];
@@ -68,6 +71,7 @@ export declare function readManifest(projectRoot: string): PodokitManifest | nul
68
71
  export declare function writeManifest(projectRoot: string, manifest: PodokitManifest): void;
69
72
  /** Complete replay variables for a Bun-only v1 project. */
70
73
  export declare function manifestTemplateVars(manifest: PodokitManifest): TemplateVars;
74
+ export declare function providerTemplateVars(providers: ProviderSelections): TemplateVars;
71
75
  export declare function writeFilesLock(projectRoot: string, lock: FilesLock): void;
72
76
  /** Read the PodoKit CLI's own version to stamp into the manifest. */
73
77
  export declare function podokitVersion(): string;
@@ -98,6 +102,7 @@ export interface InitLockOptions {
98
102
  answers: Record<string, string>;
99
103
  version?: string;
100
104
  ownedGlobs?: string[];
105
+ providers?: Partial<Record<keyof ProviderSelections, string>>;
101
106
  }
102
107
  /** Write the initial `.podokit/` lockfile for a freshly created project. */
103
108
  export declare function initLockfile(projectRoot: string, options: InitLockOptions): void;
package/dist/lockfile.js CHANGED
@@ -6,6 +6,7 @@ exports.filesLockPath = filesLockPath;
6
6
  exports.readManifest = readManifest;
7
7
  exports.writeManifest = writeManifest;
8
8
  exports.manifestTemplateVars = manifestTemplateVars;
9
+ exports.providerTemplateVars = providerTemplateVars;
9
10
  exports.writeFilesLock = writeFilesLock;
10
11
  exports.podokitVersion = podokitVersion;
11
12
  exports.matchGlob = matchGlob;
@@ -20,13 +21,14 @@ exports.recordModules = recordModules;
20
21
  const node_fs_1 = require("node:fs");
21
22
  const node_path_1 = require("node:path");
22
23
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
24
+ const podokit_runtime_1 = require("@podosoft/podokit-runtime");
23
25
  const toolchain_1 = require("./toolchain");
24
26
  /**
25
27
  * The generation lockfile (`.podokit/`) records how a project was assembled so
26
28
  * `podo update` can rebuild the same inputs and detect user edits. See
27
29
  * ADR-0009 (update mechanism) and ADR-0010 (ownership model).
28
30
  */
29
- exports.LOCK_SCHEMA_VERSION = 3;
31
+ exports.LOCK_SCHEMA_VERSION = 4;
30
32
  const PODOKIT_DIR = ".podokit";
31
33
  const MANIFEST_FILE = "manifest.json";
32
34
  const FILES_LOCK_FILE = "files.lock";
@@ -87,12 +89,13 @@ function readManifest(projectRoot) {
87
89
  if (!(0, node_fs_1.existsSync)(file))
88
90
  return null;
89
91
  const raw = JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
90
- if (raw.schemaVersion !== exports.LOCK_SCHEMA_VERSION
92
+ if ((raw.schemaVersion !== 3 && raw.schemaVersion !== exports.LOCK_SCHEMA_VERSION)
91
93
  || raw.toolchain?.runtime !== "bun"
92
94
  || raw.toolchain.packageManager !== "bun") {
93
95
  throw new Error("This project was generated by PodoKit v0 and is not compatible with PodoKit v1 updates. Use @podosoft/podokit@0.17.4 for this project; create a new v1 project for Bun and Elysia.");
94
96
  }
95
- return raw;
97
+ const providers = (0, podokit_runtime_1.resolveProviderSelections)(raw.providers);
98
+ return { ...raw, schemaVersion: exports.LOCK_SCHEMA_VERSION, providers };
96
99
  }
97
100
  function writeManifest(projectRoot, manifest) {
98
101
  (0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
@@ -100,7 +103,20 @@ function writeManifest(projectRoot, manifest) {
100
103
  }
101
104
  /** Complete replay variables for a Bun-only v1 project. */
102
105
  function manifestTemplateVars(manifest) {
103
- return { ...manifest.answers, ...(0, toolchain_1.toolchainTemplateVars)(manifest.toolchain) };
106
+ return {
107
+ ...manifest.answers,
108
+ ...providerTemplateVars(manifest.providers),
109
+ ...(0, toolchain_1.toolchainTemplateVars)(manifest.toolchain),
110
+ };
111
+ }
112
+ function providerTemplateVars(providers) {
113
+ return {
114
+ databaseProvider: providers.database,
115
+ cacheProvider: providers.cache,
116
+ objectStorageProvider: providers["object-storage"],
117
+ eventsProvider: providers.events,
118
+ jobsProvider: providers.jobs,
119
+ };
104
120
  }
105
121
  function writeFilesLock(projectRoot, lock) {
106
122
  (0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
@@ -206,12 +222,14 @@ function computeDrift(projectRoot) {
206
222
  /** Write the initial `.podokit/` lockfile for a freshly created project. */
207
223
  function initLockfile(projectRoot, options) {
208
224
  const ownedGlobs = options.ownedGlobs ?? exports.DEFAULT_OWNED_GLOBS;
225
+ const providers = (0, podokit_runtime_1.resolveProviderSelections)(options.providers ?? podokit_runtime_1.SERVER_PROVIDER_SELECTIONS);
209
226
  const manifest = {
210
227
  schemaVersion: exports.LOCK_SCHEMA_VERSION,
211
228
  podokitVersion: options.version ?? podokitVersion(),
212
229
  template: options.template,
213
230
  toolchain: options.toolchain ?? (0, toolchain_1.resolveToolchain)(),
214
231
  answers: options.answers,
232
+ providers,
215
233
  modules: [],
216
234
  ownedGlobs,
217
235
  managedOverrides: [],
@@ -0,0 +1,24 @@
1
+ import { type ProviderCapability, type ProviderSelections } from "@podosoft/podokit-runtime";
2
+ export declare const PROVIDERS_SOURCE = "apps/api/src/config/providers.ts";
3
+ export interface ProviderListItem {
4
+ capability: ProviderCapability;
5
+ selected: string;
6
+ available: readonly string[];
7
+ }
8
+ export interface ProviderChangePlan {
9
+ capability: ProviderCapability;
10
+ from: string;
11
+ to: string;
12
+ changed: boolean;
13
+ files: string[];
14
+ modulesToAdd: string[];
15
+ warnings: string[];
16
+ }
17
+ export interface ApplyProviderChangeOptions {
18
+ modulesDir?: string;
19
+ podokitVersion?: string;
20
+ }
21
+ export declare function providerSource(providers: ProviderSelections): string;
22
+ export declare function listProviders(projectRoot: string): ProviderListItem[];
23
+ export declare function planProviderChange(projectRoot: string, capabilityInput: string, provider: string, modulesDir?: string): ProviderChangePlan;
24
+ export declare function applyProviderChange(projectRoot: string, capabilityInput: string, provider: string, options?: ApplyProviderChangeOptions): ProviderChangePlan;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROVIDERS_SOURCE = void 0;
4
+ exports.providerSource = providerSource;
5
+ exports.listProviders = listProviders;
6
+ exports.planProviderChange = planProviderChange;
7
+ exports.applyProviderChange = applyProviderChange;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const podokit_runtime_1 = require("@podosoft/podokit-runtime");
11
+ const add_1 = require("./add");
12
+ const lockfile_1 = require("./lockfile");
13
+ exports.PROVIDERS_SOURCE = "apps/api/src/config/providers.ts";
14
+ function requireManifest(projectRoot) {
15
+ const manifest = (0, lockfile_1.readManifest)(projectRoot);
16
+ if (!manifest) {
17
+ throw new Error("This does not look like a PodoKit project: .podokit/manifest.json not found.");
18
+ }
19
+ return manifest;
20
+ }
21
+ function assertProvider(capability, provider) {
22
+ if (!(0, podokit_runtime_1.isProviderCapability)(capability)) {
23
+ throw new Error(`Unknown provider capability "${capability}". Use one of: ${podokit_runtime_1.PROVIDER_CAPABILITIES.join(", ")}.`);
24
+ }
25
+ if (!(0, podokit_runtime_1.isProviderName)(capability, provider)) {
26
+ throw new Error(`Unknown ${capability} provider "${provider}". Use one of: ${podokit_runtime_1.PROVIDER_NAMES[capability].join(", ")}.`);
27
+ }
28
+ return capability;
29
+ }
30
+ function providerSource(providers) {
31
+ return `import type { ProviderSelections } from "@podosoft/podokit-runtime";\n\n` +
32
+ `export const PROVIDERS = {\n` +
33
+ ` database: "${providers.database}",\n` +
34
+ ` cache: "${providers.cache}",\n` +
35
+ ` "object-storage": "${providers["object-storage"]}",\n` +
36
+ ` events: "${providers.events}",\n` +
37
+ ` jobs: "${providers.jobs}",\n` +
38
+ `} as const satisfies ProviderSelections;\n`;
39
+ }
40
+ function listProviders(projectRoot) {
41
+ const manifest = requireManifest(projectRoot);
42
+ return podokit_runtime_1.PROVIDER_CAPABILITIES.map((capability) => ({
43
+ capability,
44
+ selected: manifest.providers[capability],
45
+ available: podokit_runtime_1.PROVIDER_NAMES[capability],
46
+ }));
47
+ }
48
+ function planProviderChange(projectRoot, capabilityInput, provider, modulesDir) {
49
+ const capability = assertProvider(capabilityInput, provider);
50
+ const manifest = requireManifest(projectRoot);
51
+ const from = manifest.providers[capability];
52
+ const changed = from !== provider;
53
+ const sourcePath = (0, node_path_1.join)(projectRoot, exports.PROVIDERS_SOURCE);
54
+ const files = changed || !(0, node_fs_1.existsSync)(sourcePath)
55
+ ? [exports.PROVIDERS_SOURCE, ".podokit/manifest.json", ".podokit/files.lock"]
56
+ : [];
57
+ const implementation = modulesDir
58
+ ? providerImplementationModule(projectRoot, modulesDir, capability, provider)
59
+ : undefined;
60
+ const modulesToAdd = implementation
61
+ && !manifest.modules.some((module) => module.name === implementation)
62
+ ? [implementation]
63
+ : [];
64
+ return {
65
+ capability,
66
+ from,
67
+ to: provider,
68
+ changed,
69
+ files,
70
+ modulesToAdd,
71
+ warnings: [
72
+ ...(changed ? [
73
+ "Provider switching changes configuration and code only.",
74
+ "Existing data and objects are not migrated or deleted.",
75
+ ] : []),
76
+ ...(modulesToAdd.length
77
+ ? [`Provider implementation module ${modulesToAdd[0]} will be added.`]
78
+ : []),
79
+ ],
80
+ };
81
+ }
82
+ function providerImplementationModule(projectRoot, modulesDir, capability, provider) {
83
+ for (const available of (0, add_1.listModules)(modulesDir, projectRoot)) {
84
+ const resolved = (0, add_1.resolveModule)(available.name, modulesDir, projectRoot);
85
+ if (!resolved)
86
+ continue;
87
+ if ((0, add_1.readModuleManifest)(resolved.dir).provides?.[capability] === provider) {
88
+ return available.name;
89
+ }
90
+ }
91
+ if (capability === "database")
92
+ return undefined;
93
+ throw new Error(`No module provides the ${capability} provider "${provider}".`);
94
+ }
95
+ function applyProviderChange(projectRoot, capabilityInput, provider, options = {}) {
96
+ const plan = planProviderChange(projectRoot, capabilityInput, provider, options.modulesDir);
97
+ if (!plan.changed && plan.files.length === 0 && plan.modulesToAdd.length === 0)
98
+ return plan;
99
+ const drift = (0, lockfile_1.computeDrift)(projectRoot);
100
+ if (drift.drifted.includes(exports.PROVIDERS_SOURCE) || drift.missing.includes(exports.PROVIDERS_SOURCE)) {
101
+ throw new Error(`${exports.PROVIDERS_SOURCE} has local edits. Resolve them or run podo eject before changing providers.`);
102
+ }
103
+ for (const module of plan.modulesToAdd) {
104
+ if (!options.modulesDir)
105
+ throw new Error("A modules directory is required to add provider implementations.");
106
+ (0, add_1.addModule)({
107
+ projectRoot,
108
+ module,
109
+ modulesDir: options.modulesDir,
110
+ podokitVersion: options.podokitVersion,
111
+ });
112
+ }
113
+ const manifest = requireManifest(projectRoot);
114
+ const sourcePath = (0, node_path_1.join)(projectRoot, exports.PROVIDERS_SOURCE);
115
+ const manifestFile = (0, lockfile_1.manifestPath)(projectRoot);
116
+ const lockFile = (0, lockfile_1.filesLockPath)(projectRoot);
117
+ const snapshots = new Map([
118
+ [sourcePath, (0, node_fs_1.existsSync)(sourcePath) ? (0, node_fs_1.readFileSync)(sourcePath, "utf8") : null],
119
+ [manifestFile, (0, node_fs_1.readFileSync)(manifestFile, "utf8")],
120
+ [lockFile, (0, node_fs_1.existsSync)(lockFile) ? (0, node_fs_1.readFileSync)(lockFile, "utf8") : null],
121
+ ]);
122
+ const temporary = `${sourcePath}.podokit-provider-${process.pid}`;
123
+ try {
124
+ manifest.providers[plan.capability] = plan.to;
125
+ manifest.answers = { ...manifest.answers, ...(0, lockfile_1.providerTemplateVars)(manifest.providers) };
126
+ (0, node_fs_1.writeFileSync)(temporary, providerSource(manifest.providers), { mode: 0o600 });
127
+ (0, node_fs_1.renameSync)(temporary, sourcePath);
128
+ (0, lockfile_1.writeManifest)(projectRoot, manifest);
129
+ (0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides ?? []));
130
+ }
131
+ catch (error) {
132
+ (0, node_fs_1.rmSync)(temporary, { force: true });
133
+ for (const [path, content] of snapshots) {
134
+ if (content === null)
135
+ (0, node_fs_1.rmSync)(path, { force: true });
136
+ else
137
+ (0, node_fs_1.writeFileSync)(path, content);
138
+ }
139
+ throw error;
140
+ }
141
+ return plan;
142
+ }
package/dist/remove.js CHANGED
@@ -6,7 +6,6 @@ const node_path_1 = require("node:path");
6
6
  const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
7
7
  const add_1 = require("./add");
8
8
  const lockfile_1 = require("./lockfile");
9
- const toolchain_1 = require("./toolchain");
10
9
  function readModuleManifest(moduleDir, vars) {
11
10
  const source = (0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8");
12
11
  return JSON.parse(vars ? (0, podokit_template_engine_1.renderTokens)(source, vars) : source);
@@ -51,7 +50,7 @@ function removeModule(options) {
51
50
  }
52
51
  const vars = {
53
52
  projectName: projectName(projectRoot),
54
- ...(0, toolchain_1.toolchainTemplateVars)(manifest.toolchain),
53
+ ...(0, lockfile_1.manifestTemplateVars)(manifest),
55
54
  };
56
55
  const targetManifest = readModuleManifest(moduleDir, vars);
57
56
  // Resolve the manifests of the OTHER installed modules once — used for the
@@ -64,7 +63,11 @@ function removeModule(options) {
64
63
  })
65
64
  .filter((m) => m !== null);
66
65
  // Guard: refuse if another installed module requires this one.
67
- const dependents = others.filter((o) => (o.manifest.requires ?? []).includes(module)).map((o) => o.name);
66
+ const selectedCapabilities = Object.entries(targetManifest.provides ?? {})
67
+ .filter(([capability, provider]) => manifest.providers[capability] === provider)
68
+ .map(([capability]) => capability);
69
+ const dependents = others.filter((other) => (other.manifest.requires ?? []).includes(module)
70
+ || (other.manifest.requiresCapabilities ?? []).some((capability) => selectedCapabilities.includes(capability))).map((other) => other.name);
68
71
  if (dependents.length) {
69
72
  throw new Error(`Cannot remove "${module}": required by ${dependents.join(", ")}. Remove ${dependents.length > 1 ? "those" : "that"} first.`);
70
73
  }
@@ -8,9 +8,9 @@ Follow it so your changes match the project's conventions.
8
8
 
9
9
  A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
10
10
 
11
- - `apps/api` — **Elysia** backend (`{{projectName}}-api`), Bun.SQL + PostgreSQL, merged OpenAPI at `/api-docs`.
11
+ - `apps/api` — **Elysia** backend (`{{projectName}}-api`), Bun.SQL + `{{databaseProvider}}`, merged OpenAPI at `/api-docs`.
12
12
  - `apps/web` — **SvelteKit** frontend (`{{projectName}}-web`), Tailwind v4 + shadcn-svelte + typesafe-i18n.
13
- - `infra/` — Docker Compose (PostgreSQL/Redis) and example k3s manifests.
13
+ - `infra/` — provider-aware Docker Compose and example k3s manifests.
14
14
  - `.podokit/` — PodoKit generation metadata plus application-owned development and
15
15
  deployment profiles (see "PodoKit tooling" below).
16
16
 
@@ -19,7 +19,7 @@ A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
19
19
  ```bash
20
20
  {{packageManager}} install
21
21
  cp .env.example .env
22
- docker compose -f infra/docker/docker-compose.yml up -d # PostgreSQL (+ Redis)
22
+ docker compose -f infra/docker/docker-compose.yml up -d # selected external providers only
23
23
  {{rootRun}} dev # api on http://localhost:5002, web on http://localhost:5001
24
24
  ```
25
25
 
@@ -74,9 +74,10 @@ Useful commands:
74
74
  exact API WebSocket allowlist; never use a prefix or wildcard for that allowlist.
75
75
  - `podo add <module>` — add a feature (auth, admin-dashboard, redis, bullmq, …). It wires itself into the app; run `podo add` with no argument to list modules.
76
76
  - `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
77
+ - `podo provider list` / `podo provider set <capability> <provider> [--apply]` — inspect or switch database, cache, object storage, events, and jobs. Set is dry-run by default and never migrates or deletes provider data. Local providers require one API process.
77
78
  - `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
78
79
  - `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
79
- - `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
80
+ - `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Production API WebSocket paths belong in the deployment profile's exact `exposure.webSocketPaths` allowlist; Compose trusts forwarding headers only from `exposure.trustedProxyCidrs`. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
80
81
  - `podo deploy sync` — copy local build output into a running Compose deployment and restart it, for iterating without a release. Not a release: no image is published and the next `apply` discards it. Use `.agents/skills/podokit-deploy-fast`.
81
82
 
82
83
  ## Commit conventions