@podosoft/podokit 0.16.4 → 0.17.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 (84) hide show
  1. package/dist/deploy-compose-exec.d.ts +93 -0
  2. package/dist/deploy-compose-exec.js +198 -0
  3. package/dist/deploy-compose-profile.d.ts +107 -0
  4. package/dist/deploy-compose-profile.js +361 -0
  5. package/dist/deploy-compose-render.d.ts +48 -0
  6. package/dist/deploy-compose-render.js +333 -0
  7. package/dist/deploy-compose-sync.d.ts +144 -0
  8. package/dist/deploy-compose-sync.js +412 -0
  9. package/dist/deploy-compose.d.ts +98 -0
  10. package/dist/deploy-compose.js +667 -0
  11. package/dist/deploy-dispatch.d.ts +33 -0
  12. package/dist/deploy-dispatch.js +58 -0
  13. package/dist/deploy-driver.d.ts +25 -0
  14. package/dist/deploy-driver.js +78 -0
  15. package/dist/deploy-profile.d.ts +3 -10
  16. package/dist/deploy-profile.js +82 -291
  17. package/dist/deploy-schema.d.ts +56 -0
  18. package/dist/deploy-schema.js +261 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +178 -6
  21. package/dist/lib.d.ts +9 -0
  22. package/dist/lib.js +30 -1
  23. package/dist/templates/fullstack-nest-svelte/AGENTS.md +2 -1
  24. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +34 -3
  25. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +68 -7
  26. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +60 -8
  27. package/dist/templates/fullstack-nest-svelte/apps/web/server.js +31 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +15 -1
  30. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.js +267 -0
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  32. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +17 -7
  33. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
  34. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +50 -1
  35. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/release.md +64 -0
  36. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
  37. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
  38. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
  39. package/dist/templates/fullstack-nest-svelte/dot-dockerignore +7 -0
  40. package/dist/templates/fullstack-nest-svelte/dot-env.example +7 -0
  41. package/dist/templates/fullstack-nest-svelte/dot-github/workflows/release.yml +142 -0
  42. package/dist/templates/fullstack-nest-svelte/infra/k3s/README.md +18 -0
  43. package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +6 -1
  44. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +8 -0
  45. package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +11 -1
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +15 -2
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.test.ts +62 -0
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.ts +50 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +5 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +5 -0
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +3 -2
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +3 -2
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/reset-password/+page.svelte +12 -1
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +13 -1
  55. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +27 -2
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +20 -1
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +26 -1
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +10 -1
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
  60. package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +2 -1
  61. package/dist/templates/todo/AGENTS.md +2 -1
  62. package/dist/templates/todo/Dockerfile.dev +34 -3
  63. package/dist/templates/todo/apps/api/Dockerfile +68 -7
  64. package/dist/templates/todo/apps/web/Dockerfile +60 -8
  65. package/dist/templates/todo/apps/web/server.js +31 -0
  66. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
  67. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +15 -1
  68. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.js +267 -0
  69. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  70. package/dist/templates/todo/apps/web/vite.config.ts +17 -7
  71. package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
  72. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +47 -0
  73. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/release.md +64 -0
  74. package/dist/templates/todo/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
  75. package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
  76. package/dist/templates/todo/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
  77. package/dist/templates/todo/dot-dockerignore +7 -0
  78. package/dist/templates/todo/dot-env.example +7 -0
  79. package/dist/templates/todo/dot-github/workflows/release.yml +142 -0
  80. package/dist/templates/todo/infra/k3s/README.md +18 -0
  81. package/dist/templates/todo/infra/k3s/api-deployment.yaml +6 -1
  82. package/dist/templates/todo/infra/k3s/ingress.yaml +8 -0
  83. package/dist/templates/todo/infra/k3s/web-deployment.yaml +11 -1
  84. package/package.json +1 -1
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MANAGED_RUNTIME_CONFIG_KEYS = exports.STABLE_SEMVER_TAG_PATTERN = exports.SENSITIVE_KEY = exports.DNS_LABEL = exports.PROFILE_NAME = void 0;
4
+ exports.containsUrlCredentials = containsUrlCredentials;
5
+ exports.isRecord = isRecord;
6
+ exports.assertOnlyKeys = assertOnlyKeys;
7
+ exports.requiredRecord = requiredRecord;
8
+ exports.requiredString = requiredString;
9
+ exports.optionalString = optionalString;
10
+ exports.requiredNumber = requiredNumber;
11
+ exports.parseRequiredKeys = parseRequiredKeys;
12
+ exports.parseCommand = parseCommand;
13
+ exports.validateDnsLabel = validateDnsLabel;
14
+ exports.validateDnsSubdomain = validateDnsSubdomain;
15
+ exports.parseImageReference = parseImageReference;
16
+ exports.validateImage = validateImage;
17
+ exports.validateImageRepository = validateImageRepository;
18
+ exports.parseReleaseProfile = parseReleaseProfile;
19
+ exports.parseRuntimeConfig = parseRuntimeConfig;
20
+ exports.validateHttpsOrigin = validateHttpsOrigin;
21
+ exports.parseVerification = parseVerification;
22
+ exports.profileDirectory = profileDirectory;
23
+ exports.profilePath = profilePath;
24
+ const node_path_1 = require("node:path");
25
+ exports.PROFILE_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
26
+ exports.DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
27
+ const IMAGE_NAME = /^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[1-9][0-9]{0,4})?\/)?(?:[a-z0-9]+(?:[._-][a-z0-9]+)*\/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
28
+ const IMAGE_TAG = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
29
+ exports.SENSITIVE_KEY = /(password|passphrase|secret|token|private.?key|credential|access.?key|api.?keys?|signing.?key|encryption.?key)/i;
30
+ exports.STABLE_SEMVER_TAG_PATTERN = "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$";
31
+ /** Keys PodoKit derives itself, so a profile may not set them. */
32
+ exports.MANAGED_RUNTIME_CONFIG_KEYS = ["PORT", "BACKEND_INTERNAL_URL"];
33
+ function containsUrlCredentials(value) {
34
+ if (!value.includes("://"))
35
+ return false;
36
+ try {
37
+ const parsed = new URL(value);
38
+ return parsed.username !== "" || parsed.password !== "";
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ function isRecord(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+ function assertOnlyKeys(value, allowed, field) {
48
+ const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
49
+ if (unexpected.length) {
50
+ throw new Error(`Deployment profile field "${field}" contains unknown key(s): ${unexpected.join(", ")}.`);
51
+ }
52
+ }
53
+ function requiredRecord(parent, key) {
54
+ const value = parent[key];
55
+ if (!isRecord(value))
56
+ throw new Error(`Deployment profile field "${key}" must be an object.`);
57
+ return value;
58
+ }
59
+ function requiredString(parent, key) {
60
+ const value = parent[key];
61
+ if (typeof value !== "string" || value.trim() === "") {
62
+ throw new Error(`Deployment profile field "${key}" must be a non-empty string.`);
63
+ }
64
+ return value;
65
+ }
66
+ function optionalString(parent, key) {
67
+ const value = parent[key];
68
+ if (value === null || value === undefined)
69
+ return null;
70
+ if (typeof value !== "string" || value.trim() === "") {
71
+ throw new Error(`Deployment profile field "${key}" must be a string or null.`);
72
+ }
73
+ return value;
74
+ }
75
+ function requiredNumber(parent, key) {
76
+ const value = parent[key];
77
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
78
+ throw new Error(`Deployment profile field "${key}" must be a positive integer.`);
79
+ }
80
+ return value;
81
+ }
82
+ function parseRequiredKeys(value, field) {
83
+ if (!Array.isArray(value)) {
84
+ throw new Error(`Deployment profile field "${field}" must be a string array.`);
85
+ }
86
+ const keys = value.map((entry) => {
87
+ if (typeof entry !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(entry)) {
88
+ throw new Error(`Deployment profile field "${field}" contains an invalid environment key.`);
89
+ }
90
+ return entry;
91
+ });
92
+ if (new Set(keys).size !== keys.length) {
93
+ throw new Error(`Deployment profile field "${field}" contains duplicate keys.`);
94
+ }
95
+ return keys.sort();
96
+ }
97
+ function parseCommand(value, field) {
98
+ if (!Array.isArray(value) || value.length === 0) {
99
+ throw new Error(`Deployment profile field "${field}" must be a non-empty string array.`);
100
+ }
101
+ return value.map((entry, index) => {
102
+ if (typeof entry !== "string" || entry.trim() === "") {
103
+ throw new Error(`Deployment profile field "${field}[${index}]" must be a non-empty string.`);
104
+ }
105
+ if (/[\u0000-\u001f\u007f]/.test(entry)) {
106
+ throw new Error(`Deployment profile field "${field}[${index}]" must not contain control characters.`);
107
+ }
108
+ return entry;
109
+ });
110
+ }
111
+ function validateDnsLabel(value, field) {
112
+ if (!exports.DNS_LABEL.test(value)) {
113
+ throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS label.`);
114
+ }
115
+ }
116
+ function validateDnsSubdomain(value, field) {
117
+ if (value.length > 253 || !value.split(".").every((label) => exports.DNS_LABEL.test(label))) {
118
+ throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS subdomain.`);
119
+ }
120
+ }
121
+ function parseImageReference(value) {
122
+ if (value.length > 512 || value.includes("://") || /[\s\u0000-\u001f\u007f]/.test(value)) {
123
+ return null;
124
+ }
125
+ const at = value.indexOf("@");
126
+ if (at !== -1 && at !== value.lastIndexOf("@"))
127
+ return null;
128
+ const digest = at === -1 ? null : value.slice(at + 1);
129
+ const tagged = at === -1 ? value : value.slice(0, at);
130
+ const lastSlash = tagged.lastIndexOf("/");
131
+ const lastColon = tagged.lastIndexOf(":");
132
+ const hasTag = lastColon > lastSlash;
133
+ const name = hasTag ? tagged.slice(0, lastColon) : tagged;
134
+ const tag = hasTag ? tagged.slice(lastColon + 1) : null;
135
+ if (!IMAGE_NAME.test(name) ||
136
+ (tag !== null && !IMAGE_TAG.test(tag)) ||
137
+ (digest !== null && !/^sha256:[a-f0-9]{64}$/.test(digest))) {
138
+ return null;
139
+ }
140
+ return { name, tag, digest };
141
+ }
142
+ function validateImage(value, field) {
143
+ const parsed = parseImageReference(value);
144
+ if (!parsed || parsed.tag === "latest" || (!parsed.tag && !parsed.digest)) {
145
+ throw new Error(`Deployment profile field "${field}" must use an explicit non-latest image tag or digest.`);
146
+ }
147
+ }
148
+ function validateImageRepository(value, field) {
149
+ const parsed = parseImageReference(value);
150
+ if (!parsed || parsed.tag || parsed.digest) {
151
+ throw new Error(`Deployment profile field "${field}" must be an image repository without a tag or digest.`);
152
+ }
153
+ }
154
+ function parseReleaseProfile(release) {
155
+ assertOnlyKeys(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
156
+ const tagPattern = requiredString(release, "tagPattern");
157
+ if (tagPattern !== exports.STABLE_SEMVER_TAG_PATTERN) {
158
+ throw new Error("Deployment release.tagPattern must require stable vMAJOR.MINOR.PATCH tags.");
159
+ }
160
+ if (release.strategy !== "shared-tag") {
161
+ throw new Error("Deployment release.strategy must be shared-tag.");
162
+ }
163
+ const apiRepository = requiredString(release, "apiRepository");
164
+ const webRepository = requiredString(release, "webRepository");
165
+ validateImageRepository(apiRepository, "release.apiRepository");
166
+ validateImageRepository(webRepository, "release.webRepository");
167
+ return { strategy: "shared-tag", tagPattern, apiRepository, webRepository };
168
+ }
169
+ function parseRuntimeConfig(runtimeConfig) {
170
+ const parsed = {};
171
+ for (const [key, entry] of Object.entries(runtimeConfig)) {
172
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
173
+ throw new Error(`Deployment runtimeConfig key "${key}" must be an uppercase environment name.`);
174
+ }
175
+ if (exports.SENSITIVE_KEY.test(key)) {
176
+ throw new Error(`Deployment runtimeConfig key "${key}" may contain a secret; use a secret store.`);
177
+ }
178
+ if (exports.MANAGED_RUNTIME_CONFIG_KEYS.includes(key)) {
179
+ throw new Error(`Deployment runtimeConfig key "${key}" is managed by PodoKit.`);
180
+ }
181
+ if (typeof entry !== "string") {
182
+ throw new Error(`Deployment runtimeConfig value "${key}" must be a string.`);
183
+ }
184
+ if (containsUrlCredentials(entry)) {
185
+ throw new Error(`Deployment runtimeConfig value "${key}" contains URL credentials; use a secret store.`);
186
+ }
187
+ parsed[key] = entry;
188
+ }
189
+ return parsed;
190
+ }
191
+ function validateHttpsOrigin(value) {
192
+ let parsed;
193
+ try {
194
+ parsed = new URL(value);
195
+ }
196
+ catch {
197
+ throw new Error(`Deployment verification baseUrl "${value}" is invalid.`);
198
+ }
199
+ if (parsed.username || parsed.password) {
200
+ throw new Error("Deployment verification baseUrl must not contain URL credentials; use a credential-free HTTPS origin.");
201
+ }
202
+ if (parsed.protocol !== "https:" || parsed.pathname !== "/" || parsed.search || parsed.hash) {
203
+ throw new Error("Deployment verification baseUrl must be an HTTPS origin without a path.");
204
+ }
205
+ }
206
+ function parseVerification(verification) {
207
+ assertOnlyKeys(verification, ["baseUrl", "checks"], "verification");
208
+ const baseUrl = requiredString(verification, "baseUrl");
209
+ validateHttpsOrigin(baseUrl);
210
+ const baseOrigin = new URL(baseUrl).origin;
211
+ const checksValue = verification.checks;
212
+ if (!Array.isArray(checksValue) || checksValue.length === 0) {
213
+ throw new Error("Deployment verification.checks must be a non-empty array.");
214
+ }
215
+ const checks = checksValue.map((entry, index) => {
216
+ if (!isRecord(entry)) {
217
+ throw new Error(`Deployment verification.checks[${index}] must be an object.`);
218
+ }
219
+ assertOnlyKeys(entry, ["path", "expectedStatus", "expectedJson"], `verification.checks[${index}]`);
220
+ const path = requiredString(entry, "path");
221
+ if (!path.startsWith("/") ||
222
+ path.startsWith("//") ||
223
+ path.includes("\\") ||
224
+ /[\u0000-\u001f\u007f]/.test(path) ||
225
+ new URL(path, baseUrl).origin !== baseOrigin) {
226
+ throw new Error("Every deployment verification path must be an absolute URL path.");
227
+ }
228
+ const expectedStatus = requiredNumber(entry, "expectedStatus");
229
+ if (expectedStatus < 100 || expectedStatus > 599) {
230
+ throw new Error("Deployment verification expectedStatus must be an HTTP status.");
231
+ }
232
+ const expectedJsonValue = entry.expectedJson;
233
+ let expectedJson;
234
+ if (expectedJsonValue !== undefined) {
235
+ if (!isRecord(expectedJsonValue)) {
236
+ throw new Error("Deployment verification expectedJson must be an object.");
237
+ }
238
+ expectedJson = {};
239
+ for (const [key, value] of Object.entries(expectedJsonValue)) {
240
+ if (value !== null &&
241
+ typeof value !== "string" &&
242
+ typeof value !== "number" &&
243
+ typeof value !== "boolean") {
244
+ throw new Error("Deployment verification expectedJson values must be scalar.");
245
+ }
246
+ expectedJson[key] = value;
247
+ }
248
+ }
249
+ return { path, expectedStatus, ...(expectedJson ? { expectedJson } : {}) };
250
+ });
251
+ return { baseUrl, checks };
252
+ }
253
+ function profileDirectory(projectRoot) {
254
+ return (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "deploy");
255
+ }
256
+ function profilePath(projectRoot, name) {
257
+ if (!exports.PROFILE_NAME.test(name)) {
258
+ throw new Error(`Invalid deployment profile name "${name}".`);
259
+ }
260
+ return (0, node_path_1.join)(profileDirectory(projectRoot), `${name}.json`);
261
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { type PackageManager } from "./create";
3
3
  import { type LocaleDirection } from "./locale";
4
+ import { type DeployDriver } from "./deploy-driver";
4
5
  interface ParsedArgs {
5
6
  command?: string;
6
7
  name?: string;
@@ -22,8 +23,14 @@ interface ParsedArgs {
22
23
  revision?: number;
23
24
  context?: string;
24
25
  clusterFingerprint?: string;
26
+ endpointFingerprint?: string;
27
+ driver?: DeployDriver;
28
+ secretsDir?: string;
25
29
  host?: string;
26
30
  json: boolean;
31
+ build: boolean;
32
+ clean: boolean;
33
+ revert: boolean;
27
34
  }
28
35
  export declare function parseArgs(argv: string[]): ParsedArgs;
29
36
  export {};
package/dist/index.js CHANGED
@@ -17,6 +17,11 @@ const locale_1 = require("./locale");
17
17
  const deploy_1 = require("./deploy");
18
18
  const deploy_profile_1 = require("./deploy-profile");
19
19
  const deploy_render_1 = require("./deploy-render");
20
+ const deploy_driver_1 = require("./deploy-driver");
21
+ const deploy_compose_profile_1 = require("./deploy-compose-profile");
22
+ const deploy_compose_render_1 = require("./deploy-compose-render");
23
+ const deploy_compose_1 = require("./deploy-compose");
24
+ const deploy_compose_sync_1 = require("./deploy-compose-sync");
20
25
  const HELP = `podo — PodoKit project generator
21
26
 
22
27
  Usage:
@@ -27,7 +32,8 @@ Usage:
27
32
  podo diff List PodoKit-managed files you have edited
28
33
  podo doctor Check framework versions against supported ranges
29
34
  podo dev <action> [...] Run the shared, portless container development gateway
30
- podo deploy <action> Plan, apply, verify, or roll back a Kubernetes deployment
35
+ podo deploy <action> Plan, apply, verify, or roll back a deployment
36
+ (Kubernetes/Helm or Docker Compose)
31
37
  podo locale <command> Add, validate, activate, or list JSON locales
32
38
  podo update [--apply] Preview (or apply) what a version update would change
33
39
  podo eject <path...> Take ownership of managed files (update skips them)
@@ -59,10 +65,11 @@ Example:
59
65
  npx @podosoft/podokit create my-app --template todo
60
66
  cd my-app && npx @podosoft/podokit add auth
61
67
  `;
62
- const DEPLOY_HELP = `podo deploy — Kubernetes and Helm deployment workflow
68
+ const DEPLOY_HELP = `podo deploy — release an application to Kubernetes or Docker Compose
63
69
 
64
70
  Usage:
65
- podo deploy init --profile <name> --context <context> [--host <hostname>]
71
+ podo deploy init --profile <name> [--driver <driver>] --context <context>
72
+ [--host <hostname>] [--secrets-dir <path>]
66
73
  podo deploy doctor --profile <name> [--json]
67
74
  podo deploy render --profile <name> --release <tag> [--json]
68
75
  podo deploy plan --profile <name> --release <tag> [--json]
@@ -70,10 +77,24 @@ Usage:
70
77
  podo deploy status --profile <name> [--json]
71
78
  podo deploy verify --profile <name> [--json]
72
79
  podo deploy rollback --profile <name> --revision <number> [--confirm <plan-hash>]
80
+ podo deploy sync --profile <name> [--build] [--clean] [--revert] [--json]
73
81
 
74
- The namespace, referenced Secrets, ingress controller, and storage classes must
75
- already exist. Plan and rollback require access to the profile's exact cluster.
76
- Apply and rollback require confirmation of the exact plan hash.
82
+ Drivers:
83
+ kubernetes-helm (default) Helm releases on an existing cluster. --context is a
84
+ kubeconfig context; the namespace, Secrets, IngressClass, and
85
+ storage classes must already exist.
86
+ docker-compose A Compose project on one Docker host. --context is a Docker
87
+ context, local or ssh://; the env files named by the profile
88
+ must already exist on that host.
89
+
90
+ A profile records which driver it uses, so every action after init reads it from
91
+ the profile. Both drivers pin the target by fingerprint, resolve image tags to
92
+ digests, and require apply and rollback to confirm the exact plan hash.
93
+
94
+ sync is a development shortcut for the docker-compose driver only. It copies local
95
+ build output into the running containers and restarts them, so the deployment runs
96
+ code its image tag does not describe until the next apply recreates the containers.
97
+ It never runs migrations and refuses when runtime dependencies have changed.
77
98
  `;
78
99
  function parseArgs(argv) {
79
100
  const parsed = {
@@ -83,6 +104,9 @@ function parseArgs(argv) {
83
104
  adopt: false,
84
105
  ai: true,
85
106
  json: false,
107
+ build: false,
108
+ clean: false,
109
+ revert: false,
86
110
  positionals: [],
87
111
  };
88
112
  const positionals = [];
@@ -147,12 +171,34 @@ function parseArgs(argv) {
147
171
  else if (arg === "--cluster-fingerprint") {
148
172
  parsed.clusterFingerprint = argv[++i];
149
173
  }
174
+ else if (arg === "--endpoint-fingerprint") {
175
+ parsed.endpointFingerprint = argv[++i];
176
+ }
177
+ else if (arg === "--driver") {
178
+ const driver = argv[++i];
179
+ if (driver !== "kubernetes-helm" && driver !== "docker-compose") {
180
+ throw new Error(`--driver must be one of: ${deploy_driver_1.DEPLOY_DRIVERS.join(", ")}.`);
181
+ }
182
+ parsed.driver = driver;
183
+ }
184
+ else if (arg === "--secrets-dir") {
185
+ parsed.secretsDir = argv[++i];
186
+ }
150
187
  else if (arg === "--host") {
151
188
  parsed.host = argv[++i];
152
189
  }
153
190
  else if (arg === "--json") {
154
191
  parsed.json = true;
155
192
  }
193
+ else if (arg === "--build") {
194
+ parsed.build = true;
195
+ }
196
+ else if (arg === "--clean") {
197
+ parsed.clean = true;
198
+ }
199
+ else if (arg === "--revert") {
200
+ parsed.revert = true;
201
+ }
156
202
  else if (arg !== undefined && !arg.startsWith("-")) {
157
203
  positionals.push(arg);
158
204
  }
@@ -166,6 +212,118 @@ function fail(message) {
166
212
  process.stderr.write(`error: ${message}\n`);
167
213
  process.exit(1);
168
214
  }
215
+ /**
216
+ * The docker-compose driver's half of `podo deploy`. It answers the same actions as
217
+ * the Kubernetes half and enforces the same rule: every mutation echoes back the
218
+ * exact hash of a plan that was printed first.
219
+ */
220
+ async function runComposeDeploy(action, args, profileName) {
221
+ const cwd = process.cwd();
222
+ if (action === "init") {
223
+ if (!args.context) {
224
+ fail("Usage: podo deploy init --profile <name> --driver docker-compose --context <docker-context> [--host <hostname>] [--secrets-dir <path>].");
225
+ }
226
+ const endpointFingerprint = args.endpointFingerprint ?? (0, deploy_compose_1.inspectComposeEndpointFingerprint)(args.context);
227
+ const initialized = (0, deploy_compose_profile_1.initializeComposeProfile)(cwd, profileName, {
228
+ context: args.context,
229
+ endpointFingerprint,
230
+ host: args.host,
231
+ secretsDirectory: args.secretsDir,
232
+ });
233
+ process.stdout.write(args.json
234
+ ? `${JSON.stringify(initialized, null, 2)}\n`
235
+ : `Created deployment profile ${initialized.name} at ${initialized.path}.\n`);
236
+ return;
237
+ }
238
+ if (action === "doctor") {
239
+ // A release makes the doctor probe with the image about to run, and lets it
240
+ // check registry access at all.
241
+ const findings = (0, deploy_compose_1.doctorComposeDeployment)(cwd, profileName, undefined, args.release);
242
+ process.stdout.write(args.json
243
+ ? `${JSON.stringify(findings, null, 2)}\n`
244
+ : `${findings.map((f) => `${f.ok ? "ok " : "FAIL"} ${f.message}`).join("\n")}\n`);
245
+ if (findings.some((finding) => !finding.ok))
246
+ process.exitCode = 1;
247
+ return;
248
+ }
249
+ if (action === "render") {
250
+ if (!args.release)
251
+ fail("podo deploy render requires --release <tag>.");
252
+ const profile = (0, deploy_driver_1.loadComposeProfile)(cwd, profileName);
253
+ const runtime = (0, deploy_compose_render_1.renderComposeDeployment)(cwd, profileName, profile, args.release);
254
+ process.stdout.write(args.json
255
+ ? `${JSON.stringify(runtime, null, 2)}\n`
256
+ : `Rendered Compose project into ${runtime.root}.\n`);
257
+ return;
258
+ }
259
+ if (action === "plan") {
260
+ if (!args.release)
261
+ fail("podo deploy plan requires --release <tag>.");
262
+ const plan = (0, deploy_compose_1.planComposeDeployment)(cwd, profileName, args.release);
263
+ process.stdout.write(args.json
264
+ ? `${JSON.stringify(plan, null, 2)}\n`
265
+ : `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\n` +
266
+ `${plan.warnings.map((warning) => `warning: ${warning}`).join("\n")}\n` +
267
+ `Plan hash: ${plan.planHash}\n`);
268
+ return;
269
+ }
270
+ if (action === "apply") {
271
+ if (!args.release || !args.confirm) {
272
+ fail("podo deploy apply requires --release <tag> --confirm <plan-hash>.");
273
+ }
274
+ const result = await (0, deploy_compose_1.applyComposeDeployment)(cwd, profileName, args.release, args.confirm);
275
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
276
+ if (!result.verification.ok)
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+ if (action === "status") {
281
+ process.stdout.write(`${JSON.stringify((0, deploy_compose_1.getComposeStatus)(cwd, profileName), null, 2)}\n`);
282
+ return;
283
+ }
284
+ if (action === "verify") {
285
+ const result = await (0, deploy_compose_1.verifyComposeDeployment)(cwd, profileName);
286
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
287
+ if (!result.ok)
288
+ process.exitCode = 1;
289
+ return;
290
+ }
291
+ if (action === "rollback") {
292
+ if (!args.revision)
293
+ fail("podo deploy rollback requires --revision <number>.");
294
+ if (!args.confirm) {
295
+ const plan = (0, deploy_compose_1.planComposeRollback)(cwd, profileName, args.revision);
296
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\nRe-run with --confirm ${plan.planHash}.\n`);
297
+ return;
298
+ }
299
+ const result = await (0, deploy_compose_1.rollbackComposeDeployment)(cwd, profileName, args.revision, args.confirm);
300
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
301
+ if (!result.verification.ok)
302
+ process.exitCode = 1;
303
+ return;
304
+ }
305
+ if (action === "sync") {
306
+ if (args.revert) {
307
+ const reverted = await (0, deploy_compose_sync_1.revertComposeSync)(cwd, profileName);
308
+ process.stdout.write(args.json
309
+ ? `${JSON.stringify(reverted, null, 2)}\n`
310
+ : `Recreated ${reverted.services.join(", ")} from the applied Compose project.\n`);
311
+ return;
312
+ }
313
+ const result = await (0, deploy_compose_sync_1.syncComposeDeployment)(cwd, profileName, {
314
+ build: args.build,
315
+ clean: args.clean,
316
+ });
317
+ process.stdout.write(args.json
318
+ ? `${JSON.stringify(result, null, 2)}\n`
319
+ : `${result.plan.warnings.map((warning) => `warning: ${warning}`).join("\n")}\n` +
320
+ `Copied ${result.plan.artifacts.length} artifact(s) into ${result.restarted.join(", ")}.\n` +
321
+ "This deployment now runs code its image tag does not describe; " +
322
+ "podo deploy sync --revert restores the image.\n");
323
+ return;
324
+ }
325
+ fail(`Unknown deploy command "${action}". Use init, doctor, render, plan, apply, status, verify, rollback, or sync.`);
326
+ }
169
327
  async function main(argv) {
170
328
  const args = parseArgs(argv);
171
329
  if (args.help || !args.command) {
@@ -300,6 +458,14 @@ async function main(argv) {
300
458
  fail("Deployment commands require --profile <name>.");
301
459
  }
302
460
  try {
461
+ // A profile names its own driver, so everything but `init` reads it from disk.
462
+ const driver = action === "init"
463
+ ? (args.driver ?? "kubernetes-helm")
464
+ : (0, deploy_driver_1.readDeploymentDriver)(process.cwd(), profileName);
465
+ if (driver === "docker-compose") {
466
+ await runComposeDeploy(action, args, profileName);
467
+ return;
468
+ }
303
469
  if (action === "init") {
304
470
  if (!args.context) {
305
471
  fail("Usage: podo deploy init --profile <name> --context <context> [--host <hostname>].");
@@ -376,6 +542,12 @@ async function main(argv) {
376
542
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
377
543
  return;
378
544
  }
545
+ if (action === "sync") {
546
+ // Not a gap to fill later. There is no image to swap artifacts into: a
547
+ // cluster schedules pods across nodes from a registry, so the equivalent
548
+ // shortcut would have to reach every node and would stop being a shortcut.
549
+ fail("podo deploy sync is only available for the docker-compose driver. On a cluster, build and roll out a release.");
550
+ }
379
551
  fail(`Unknown deploy command "${action}". Use init, doctor, render, plan, apply, status, verify, or rollback.`);
380
552
  }
381
553
  catch (err) {
package/dist/lib.d.ts CHANGED
@@ -16,6 +16,15 @@ export { assertReleaseTag, deploymentProfileDigest, initializeDeploymentProfile,
16
16
  export type { DependencyMode, DeployProfileV1, DeploymentProfileSummary, InitializeDeploymentProfileOptions, ObjectStorageDependencyProfile, RedisDependencyProfile, ResourceProfile, StatefulDependencyProfile, WorkloadProfile, } from "./deploy-profile";
17
17
  export { renderDeployment } from "./deploy-render";
18
18
  export type { DeploymentRuntime } from "./deploy-render";
19
+ export { DEPLOY_DRIVERS, assertAnyReleaseTag, listAnyDeploymentProfiles, loadAnyDeploymentProfile, loadComposeProfile, loadKubernetesProfile, readDeploymentDriver, } from "./deploy-driver";
20
+ export type { AnyDeployProfile, DeployDriver } from "./deploy-driver";
21
+ export { initializeComposeProfile, parseComposeProfile } from "./deploy-compose-profile";
22
+ export type { DockerComposeProfileV1 } from "./deploy-compose-profile";
23
+ export { renderComposeDeployment } from "./deploy-compose-render";
24
+ export type { ComposeRuntime } from "./deploy-compose-render";
25
+ export { applyComposeDeployment, doctorComposeDeployment, getComposeStatus, inspectComposeEndpointFingerprint, planComposeDeployment, planComposeRollback, rollbackComposeDeployment, verifyComposeDeployment, } from "./deploy-compose";
26
+ export type { ComposePlan, ComposeStatus } from "./deploy-compose";
27
+ export { describeDeploymentTarget, doctorAnyDeployment, getAnyDeploymentStatus, planAnyDeployment, verifyAnyDeployment, } from "./deploy-dispatch";
19
28
  /** Absolute path to the templates bundled in this installed package. */
20
29
  export declare function builtinTemplatesDir(): string;
21
30
  /** Absolute path to the modules bundled in this installed package. */
package/dist/lib.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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;
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;
4
5
  exports.builtinTemplatesDir = builtinTemplatesDir;
5
6
  exports.builtinModulesDir = builtinModulesDir;
6
7
  // Library entry point: the reusable functions behind the `podo` CLI, for other
@@ -47,6 +48,34 @@ Object.defineProperty(exports, "listDeploymentProfiles", { enumerable: true, get
47
48
  Object.defineProperty(exports, "loadDeploymentProfile", { enumerable: true, get: function () { return deploy_profile_1.loadDeploymentProfile; } });
48
49
  var deploy_render_1 = require("./deploy-render");
49
50
  Object.defineProperty(exports, "renderDeployment", { enumerable: true, get: function () { return deploy_render_1.renderDeployment; } });
51
+ var deploy_driver_1 = require("./deploy-driver");
52
+ Object.defineProperty(exports, "DEPLOY_DRIVERS", { enumerable: true, get: function () { return deploy_driver_1.DEPLOY_DRIVERS; } });
53
+ Object.defineProperty(exports, "assertAnyReleaseTag", { enumerable: true, get: function () { return deploy_driver_1.assertAnyReleaseTag; } });
54
+ Object.defineProperty(exports, "listAnyDeploymentProfiles", { enumerable: true, get: function () { return deploy_driver_1.listAnyDeploymentProfiles; } });
55
+ Object.defineProperty(exports, "loadAnyDeploymentProfile", { enumerable: true, get: function () { return deploy_driver_1.loadAnyDeploymentProfile; } });
56
+ Object.defineProperty(exports, "loadComposeProfile", { enumerable: true, get: function () { return deploy_driver_1.loadComposeProfile; } });
57
+ Object.defineProperty(exports, "loadKubernetesProfile", { enumerable: true, get: function () { return deploy_driver_1.loadKubernetesProfile; } });
58
+ Object.defineProperty(exports, "readDeploymentDriver", { enumerable: true, get: function () { return deploy_driver_1.readDeploymentDriver; } });
59
+ var deploy_compose_profile_1 = require("./deploy-compose-profile");
60
+ Object.defineProperty(exports, "initializeComposeProfile", { enumerable: true, get: function () { return deploy_compose_profile_1.initializeComposeProfile; } });
61
+ Object.defineProperty(exports, "parseComposeProfile", { enumerable: true, get: function () { return deploy_compose_profile_1.parseComposeProfile; } });
62
+ var deploy_compose_render_1 = require("./deploy-compose-render");
63
+ Object.defineProperty(exports, "renderComposeDeployment", { enumerable: true, get: function () { return deploy_compose_render_1.renderComposeDeployment; } });
64
+ var deploy_compose_1 = require("./deploy-compose");
65
+ Object.defineProperty(exports, "applyComposeDeployment", { enumerable: true, get: function () { return deploy_compose_1.applyComposeDeployment; } });
66
+ Object.defineProperty(exports, "doctorComposeDeployment", { enumerable: true, get: function () { return deploy_compose_1.doctorComposeDeployment; } });
67
+ Object.defineProperty(exports, "getComposeStatus", { enumerable: true, get: function () { return deploy_compose_1.getComposeStatus; } });
68
+ Object.defineProperty(exports, "inspectComposeEndpointFingerprint", { enumerable: true, get: function () { return deploy_compose_1.inspectComposeEndpointFingerprint; } });
69
+ Object.defineProperty(exports, "planComposeDeployment", { enumerable: true, get: function () { return deploy_compose_1.planComposeDeployment; } });
70
+ Object.defineProperty(exports, "planComposeRollback", { enumerable: true, get: function () { return deploy_compose_1.planComposeRollback; } });
71
+ Object.defineProperty(exports, "rollbackComposeDeployment", { enumerable: true, get: function () { return deploy_compose_1.rollbackComposeDeployment; } });
72
+ Object.defineProperty(exports, "verifyComposeDeployment", { enumerable: true, get: function () { return deploy_compose_1.verifyComposeDeployment; } });
73
+ var deploy_dispatch_1 = require("./deploy-dispatch");
74
+ Object.defineProperty(exports, "describeDeploymentTarget", { enumerable: true, get: function () { return deploy_dispatch_1.describeDeploymentTarget; } });
75
+ Object.defineProperty(exports, "doctorAnyDeployment", { enumerable: true, get: function () { return deploy_dispatch_1.doctorAnyDeployment; } });
76
+ Object.defineProperty(exports, "getAnyDeploymentStatus", { enumerable: true, get: function () { return deploy_dispatch_1.getAnyDeploymentStatus; } });
77
+ Object.defineProperty(exports, "planAnyDeployment", { enumerable: true, get: function () { return deploy_dispatch_1.planAnyDeployment; } });
78
+ Object.defineProperty(exports, "verifyAnyDeployment", { enumerable: true, get: function () { return deploy_dispatch_1.verifyAnyDeployment; } });
50
79
  /** Absolute path to the templates bundled in this installed package. */
51
80
  function builtinTemplatesDir() {
52
81
  return (0, node_path_1.join)(__dirname, "templates");
@@ -69,7 +69,8 @@ Useful commands:
69
69
  - `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
70
70
  - `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
71
71
  - `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
72
- - `podo deploy` — plan, apply, verify, or roll back an immutable Kubernetes release. Use `.agents/skills/podokit-deploy`.
72
+ - `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`.
73
+ - `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`.
73
74
 
74
75
  ## Commit conventions
75
76