@vibesdotdev/infra-doks 0.0.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 (113) hide show
  1. package/README.md +107 -0
  2. package/SPEC.md +285 -0
  3. package/dist/client/digitalocean-app-deploy.client.d.ts +46 -0
  4. package/dist/client/digitalocean-app-deploy.client.d.ts.map +1 -0
  5. package/dist/client/digitalocean-app-deploy.client.js +135 -0
  6. package/dist/client/digitalocean-app-deploy.client.js.map +1 -0
  7. package/dist/client/index.d.ts +15 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +18 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/cloud/base.d.ts +33 -0
  12. package/dist/cloud/base.d.ts.map +1 -0
  13. package/dist/cloud/base.js +86 -0
  14. package/dist/cloud/base.js.map +1 -0
  15. package/dist/cloud/digitalocean.d.ts +33 -0
  16. package/dist/cloud/digitalocean.d.ts.map +1 -0
  17. package/dist/cloud/digitalocean.js +258 -0
  18. package/dist/cloud/digitalocean.js.map +1 -0
  19. package/dist/cloud/factory.d.ts +28 -0
  20. package/dist/cloud/factory.d.ts.map +1 -0
  21. package/dist/cloud/factory.js +151 -0
  22. package/dist/cloud/factory.js.map +1 -0
  23. package/dist/cloud/index.d.ts +12 -0
  24. package/dist/cloud/index.d.ts.map +1 -0
  25. package/dist/cloud/index.js +11 -0
  26. package/dist/cloud/index.js.map +1 -0
  27. package/dist/doks.plugin.d.ts +41 -0
  28. package/dist/doks.plugin.d.ts.map +1 -0
  29. package/dist/doks.plugin.js +287 -0
  30. package/dist/doks.plugin.js.map +1 -0
  31. package/dist/implementations/deployment.impl.d.ts +34 -0
  32. package/dist/implementations/deployment.impl.d.ts.map +1 -0
  33. package/dist/implementations/deployment.impl.js +86 -0
  34. package/dist/implementations/deployment.impl.js.map +1 -0
  35. package/dist/implementations/droplet.impl.d.ts +85 -0
  36. package/dist/implementations/droplet.impl.d.ts.map +1 -0
  37. package/dist/implementations/droplet.impl.js +113 -0
  38. package/dist/implementations/droplet.impl.js.map +1 -0
  39. package/dist/implementations/gitea.impl.d.ts +68 -0
  40. package/dist/implementations/gitea.impl.d.ts.map +1 -0
  41. package/dist/implementations/gitea.impl.js +295 -0
  42. package/dist/implementations/gitea.impl.js.map +1 -0
  43. package/dist/implementations/managed-db.impl.d.ts +25 -0
  44. package/dist/implementations/managed-db.impl.d.ts.map +1 -0
  45. package/dist/implementations/managed-db.impl.js +31 -0
  46. package/dist/implementations/managed-db.impl.js.map +1 -0
  47. package/dist/implementations/managed-redis.impl.d.ts +37 -0
  48. package/dist/implementations/managed-redis.impl.d.ts.map +1 -0
  49. package/dist/implementations/managed-redis.impl.js +40 -0
  50. package/dist/implementations/managed-redis.impl.js.map +1 -0
  51. package/dist/implementations/spaces.impl.d.ts +36 -0
  52. package/dist/implementations/spaces.impl.d.ts.map +1 -0
  53. package/dist/implementations/spaces.impl.js +40 -0
  54. package/dist/implementations/spaces.impl.js.map +1 -0
  55. package/dist/implementations/statefulset.impl.d.ts +65 -0
  56. package/dist/implementations/statefulset.impl.d.ts.map +1 -0
  57. package/dist/implementations/statefulset.impl.js +165 -0
  58. package/dist/implementations/statefulset.impl.js.map +1 -0
  59. package/dist/implementations/verdaccio.impl.d.ts +65 -0
  60. package/dist/implementations/verdaccio.impl.d.ts.map +1 -0
  61. package/dist/implementations/verdaccio.impl.js +259 -0
  62. package/dist/implementations/verdaccio.impl.js.map +1 -0
  63. package/dist/index.d.ts +15 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +19 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/kubernetes/index.d.ts +95 -0
  68. package/dist/kubernetes/index.d.ts.map +1 -0
  69. package/dist/kubernetes/index.js +625 -0
  70. package/dist/kubernetes/index.js.map +1 -0
  71. package/dist/secrets/index.d.ts +4 -0
  72. package/dist/secrets/index.d.ts.map +1 -0
  73. package/dist/secrets/index.js +4 -0
  74. package/dist/secrets/index.js.map +1 -0
  75. package/dist/secrets/vault.descriptor.d.ts +10 -0
  76. package/dist/secrets/vault.descriptor.d.ts.map +1 -0
  77. package/dist/secrets/vault.descriptor.js +25 -0
  78. package/dist/secrets/vault.descriptor.js.map +1 -0
  79. package/dist/secrets/vault.impl.cloud.d.ts +40 -0
  80. package/dist/secrets/vault.impl.cloud.d.ts.map +1 -0
  81. package/dist/secrets/vault.impl.cloud.js +178 -0
  82. package/dist/secrets/vault.impl.cloud.js.map +1 -0
  83. package/dist/secrets/vault.impl.d.ts +29 -0
  84. package/dist/secrets/vault.impl.d.ts.map +1 -0
  85. package/dist/secrets/vault.impl.js +137 -0
  86. package/dist/secrets/vault.impl.js.map +1 -0
  87. package/dist/types.d.ts +509 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +47 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +145 -0
  92. package/src/client/digitalocean-app-deploy.client.ts +226 -0
  93. package/src/client/index.ts +24 -0
  94. package/src/cloud/base.ts +149 -0
  95. package/src/cloud/digitalocean.ts +363 -0
  96. package/src/cloud/factory.ts +190 -0
  97. package/src/cloud/index.ts +81 -0
  98. package/src/doks.plugin.ts +401 -0
  99. package/src/implementations/deployment.impl.ts +93 -0
  100. package/src/implementations/droplet.impl.ts +157 -0
  101. package/src/implementations/gitea.impl.ts +319 -0
  102. package/src/implementations/managed-db.impl.ts +37 -0
  103. package/src/implementations/managed-redis.impl.ts +49 -0
  104. package/src/implementations/spaces.impl.ts +52 -0
  105. package/src/implementations/statefulset.impl.ts +186 -0
  106. package/src/implementations/verdaccio.impl.ts +300 -0
  107. package/src/index.ts +136 -0
  108. package/src/kubernetes/index.ts +754 -0
  109. package/src/secrets/index.ts +9 -0
  110. package/src/secrets/vault.descriptor.ts +28 -0
  111. package/src/secrets/vault.impl.cloud.ts +278 -0
  112. package/src/secrets/vault.impl.ts +149 -0
  113. package/src/types.ts +563 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * DOKS Runtime Plugin
3
+ *
4
+ * Registers DigitalOcean Kubernetes infrastructure implementations with the runtime.
5
+ */
6
+
7
+ import { createRuntimePlugin, createRuntimeImplementation, createRuntimeAsset } from '@vibesdotdev/runtime';
8
+ import {
9
+ generateK8sDeployment,
10
+ DoksDeploymentDescriptorSchema,
11
+ type DoksDeploymentDescriptorInput
12
+ } from './implementations/deployment.impl';
13
+ import type { K8sDeployment } from './types.ts';
14
+ import { stringify } from 'yaml';
15
+ import { resolve } from 'node:path';
16
+ import {
17
+ generateK8sStatefulSet,
18
+ DoksQueueDescriptorSchema,
19
+ DoksDatabaseDescriptorSchema,
20
+ type DoksQueueDescriptorInput,
21
+ type DoksDatabaseDescriptorInput
22
+ } from './implementations/statefulset.impl';
23
+ import {
24
+ generateDOManagedDB,
25
+ DOManagedPostgresDescriptorSchema,
26
+ type DOManagedPostgresDescriptorInput
27
+ } from './implementations/managed-db.impl';
28
+ import {
29
+ generateDOManagedRedis,
30
+ DOManagedRedisDescriptorSchema,
31
+ type DOManagedRedisDescriptorInput
32
+ } from './implementations/managed-redis.impl';
33
+ import {
34
+ generateDOSpaces,
35
+ DOSpacesDescriptorSchema,
36
+ type DOSpacesDescriptorInput
37
+ } from './implementations/spaces.impl';
38
+ import {
39
+ generateGiteaManifest,
40
+ GiteaDoksDescriptorSchema,
41
+ type GiteaDoksDescriptorInput
42
+ } from './implementations/gitea.impl';
43
+ import {
44
+ generateVerdaccioManifest,
45
+ VerdaccioDoksDescriptorSchema,
46
+ type VerdaccioDoksDescriptorInput
47
+ } from './implementations/verdaccio.impl';
48
+ import { createDoDropletImpl } from './implementations/droplet.impl';
49
+
50
+ // Factory functions that wrap implementation generators in RuntimeImplementation
51
+
52
+ function createDeploymentImpl(input: DoksDeploymentDescriptorInput) {
53
+ const descriptor = DoksDeploymentDescriptorSchema.parse(input);
54
+ return createRuntimeImplementation({
55
+ id: 'doks-deployment',
56
+ kind: 'infra/worker',
57
+ priority: 20,
58
+ implementation: {
59
+ generateConfig: () => generateK8sDeployment(descriptor)
60
+ }
61
+ });
62
+ }
63
+
64
+ function createQueueStatefulSetImpl(input: DoksQueueDescriptorInput) {
65
+ const descriptor = DoksQueueDescriptorSchema.parse(input);
66
+ return createRuntimeImplementation({
67
+ id: 'doks-statefulset',
68
+ kind: 'infra/queue',
69
+ priority: 20,
70
+ implementation: {
71
+ generateConfig: () => generateK8sStatefulSet(descriptor)
72
+ }
73
+ });
74
+ }
75
+
76
+ function createDatabaseStatefulSetImpl(input: DoksDatabaseDescriptorInput) {
77
+ const descriptor = DoksDatabaseDescriptorSchema.parse(input);
78
+ return createRuntimeImplementation({
79
+ id: 'doks-statefulset',
80
+ kind: 'infra/database',
81
+ priority: 20,
82
+ implementation: {
83
+ generateConfig: () => generateK8sStatefulSet(descriptor)
84
+ }
85
+ });
86
+ }
87
+
88
+ function createManagedPostgresImpl(input: DOManagedPostgresDescriptorInput) {
89
+ const descriptor = DOManagedPostgresDescriptorSchema.parse(input);
90
+ return createRuntimeImplementation({
91
+ id: 'do-managed-postgres',
92
+ kind: 'infra/database',
93
+ priority: 30,
94
+ implementation: {
95
+ generateConfig: () => generateDOManagedDB(descriptor)
96
+ }
97
+ });
98
+ }
99
+
100
+ function createManagedRedisImpl(input: DOManagedRedisDescriptorInput) {
101
+ const descriptor = DOManagedRedisDescriptorSchema.parse(input);
102
+ return createRuntimeImplementation({
103
+ id: 'do-managed-redis',
104
+ kind: 'infra/cache',
105
+ priority: 20,
106
+ implementation: {
107
+ generateConfig: () => generateDOManagedRedis(descriptor)
108
+ }
109
+ });
110
+ }
111
+
112
+ function createSpacesImpl(input: DOSpacesDescriptorInput) {
113
+ const descriptor = DOSpacesDescriptorSchema.parse(input);
114
+ return createRuntimeImplementation({
115
+ id: 'do-spaces',
116
+ kind: 'infra/object-storage',
117
+ priority: 20,
118
+ implementation: {
119
+ generateConfig: () => generateDOSpaces(descriptor)
120
+ }
121
+ });
122
+ }
123
+
124
+ function createGiteaImpl(input: GiteaDoksDescriptorInput) {
125
+ const descriptor = GiteaDoksDescriptorSchema.parse(input);
126
+ return createRuntimeImplementation({
127
+ id: 'gitea-doks',
128
+ kind: 'infra/git-hosting',
129
+ priority: 20,
130
+ implementation: {
131
+ generateConfig: () => generateGiteaManifest(descriptor)
132
+ }
133
+ });
134
+ }
135
+
136
+ function createVerdaccioImpl(input: VerdaccioDoksDescriptorInput) {
137
+ const descriptor = VerdaccioDoksDescriptorSchema.parse(input);
138
+ return createRuntimeImplementation({
139
+ id: 'verdaccio-doks',
140
+ kind: 'infra/package-registry',
141
+ priority: 20,
142
+ implementation: {
143
+ generateConfig: () => generateVerdaccioManifest(descriptor)
144
+ }
145
+ });
146
+ }
147
+
148
+ export {
149
+ createDeploymentImpl,
150
+ createQueueStatefulSetImpl,
151
+ createDatabaseStatefulSetImpl,
152
+ createManagedPostgresImpl,
153
+ createManagedRedisImpl,
154
+ createSpacesImpl,
155
+ createGiteaImpl,
156
+ createVerdaccioImpl
157
+ };
158
+
159
+ export default createRuntimePlugin({
160
+ id: 'infra-doks',
161
+ name: 'DigitalOcean Kubernetes Infrastructure',
162
+ dependencies: ['infra'],
163
+
164
+ onActivate: async (runtime) => {
165
+ // infra/worker: DOKS Deployment
166
+ runtime.registerLoader(
167
+ 'infra/worker',
168
+ 'doks-deployment',
169
+ async () => {
170
+ return createDeploymentImpl({
171
+ kind: 'infra/worker',
172
+ id: 'default-worker',
173
+ entrypoint: 'src/worker.ts',
174
+ queues: [],
175
+ concurrency: 1,
176
+ config: {
177
+ adapter: 'doks-deployment'
178
+ }
179
+ });
180
+ },
181
+ { priority: 20, origin: 'infra-doks' }
182
+ );
183
+
184
+ // infra/queue: DOKS StatefulSet for Kafka/Redpanda
185
+ runtime.registerLoader(
186
+ 'infra/queue',
187
+ 'doks-statefulset',
188
+ async () => {
189
+ return createQueueStatefulSetImpl({
190
+ kind: 'infra/queue',
191
+ id: 'default-queue',
192
+ engine: 'kafka',
193
+ config: {
194
+ adapter: 'doks-statefulset'
195
+ }
196
+ });
197
+ },
198
+ { priority: 20, origin: 'infra-doks' }
199
+ );
200
+
201
+ // infra/database: DOKS StatefulSet for self-hosted Postgres
202
+ runtime.registerLoader(
203
+ 'infra/database',
204
+ 'doks-statefulset',
205
+ async () => {
206
+ return createDatabaseStatefulSetImpl({
207
+ kind: 'infra/database',
208
+ id: 'default-db',
209
+ engine: 'postgres',
210
+ replicas: 0,
211
+ config: {
212
+ adapter: 'doks-statefulset'
213
+ }
214
+ });
215
+ },
216
+ { priority: 20, origin: 'infra-doks' }
217
+ );
218
+
219
+ // infra/database: DO Managed Postgres (higher priority than StatefulSet)
220
+ runtime.registerLoader(
221
+ 'infra/database',
222
+ 'do-managed-postgres',
223
+ async () => {
224
+ return createManagedPostgresImpl({
225
+ kind: 'infra/database',
226
+ id: 'default-managed-db',
227
+ engine: 'postgres',
228
+ replicas: 0,
229
+ config: {
230
+ adapter: 'do-managed-postgres'
231
+ }
232
+ });
233
+ },
234
+ { priority: 30, origin: 'infra-doks' }
235
+ );
236
+
237
+ // infra/cache: DO Managed Redis
238
+ runtime.registerLoader(
239
+ 'infra/cache',
240
+ 'do-managed-redis',
241
+ async () => {
242
+ return createManagedRedisImpl({
243
+ kind: 'infra/cache',
244
+ id: 'default-redis',
245
+ engine: 'redis',
246
+ config: {
247
+ adapter: 'do-managed-redis'
248
+ }
249
+ });
250
+ },
251
+ { priority: 20, origin: 'infra-doks' }
252
+ );
253
+
254
+ // infra/object-storage: DO Spaces
255
+ runtime.registerLoader(
256
+ 'infra/object-storage',
257
+ 'do-spaces',
258
+ async () => {
259
+ return createSpacesImpl({
260
+ kind: 'infra/object-storage',
261
+ id: 'default-spaces',
262
+ publicAccess: false,
263
+ config: {
264
+ adapter: 'do-spaces'
265
+ }
266
+ });
267
+ },
268
+ { priority: 20, origin: 'infra-doks' }
269
+ );
270
+
271
+ // infra/git-hosting: Gitea on DOKS
272
+ runtime.registerLoader(
273
+ 'infra/git-hosting',
274
+ 'gitea-doks',
275
+ async () => {
276
+ return createGiteaImpl({
277
+ kind: 'infra/git-hosting',
278
+ id: 'gitea',
279
+ url: 'https://git.vibes.dev',
280
+ hostname: 'git.vibes.dev',
281
+ type: 'gitea',
282
+ storagePath: '/data',
283
+ authMethod: 'token',
284
+ ssl: true,
285
+ config: {
286
+ adapter: 'gitea-doks',
287
+ dropletSize: 's-2vcpu-2gb',
288
+ dockerImage: 'gitea/gitea:1.21',
289
+ volumeSizeGb: 50,
290
+ domain: 'git.vibes.dev',
291
+ sshPort: 22,
292
+ httpPort: 3000
293
+ }
294
+ });
295
+ },
296
+ { priority: 20, origin: 'infra-doks' }
297
+ );
298
+
299
+ // infra/sandbox: DigitalOcean droplet provisioner
300
+ runtime.registerLoader(
301
+ 'infra/sandbox',
302
+ 'digitalocean-droplet',
303
+ async () =>
304
+ createRuntimeImplementation({
305
+ id: 'digitalocean-droplet',
306
+ kind: 'infra/sandbox',
307
+ priority: 20,
308
+ implementation: createDoDropletImpl()
309
+ }),
310
+ { priority: 20, origin: 'infra-doks' }
311
+ );
312
+
313
+ // infra/package-registry: Verdaccio on DOKS
314
+ runtime.registerLoader(
315
+ 'infra/package-registry',
316
+ 'verdaccio-doks',
317
+ async () => {
318
+ return createVerdaccioImpl({
319
+ kind: 'infra/package-registry',
320
+ id: 'verdaccio',
321
+ url: 'https://packages.vibes.dev',
322
+ registryType: 'npm',
323
+ scope: '@vibesdotdev',
324
+ upstreamUrl: 'https://registry.npmjs.org',
325
+ authRequired: true,
326
+ ssl: true,
327
+ config: {
328
+ adapter: 'verdaccio-doks',
329
+ dockerImage: 'verdaccio/verdaccio:5',
330
+ storageSize: '10Gi',
331
+ domain: 'packages.vibes.dev',
332
+ upstreamNpmUrl: 'https://registry.npmjs.org',
333
+ authEnabled: true
334
+ }
335
+ });
336
+ },
337
+ { priority: 20, origin: 'infra-doks' }
338
+ );
339
+
340
+ // Canonical DigitalOcean App Platform Deploy client (`runtime/client` kind).
341
+ // Consumers resolve via `getDigitalOceanAppDeployClient()` or
342
+ // `getVibesClient<DigitalOceanAppDeployClient>('digitalocean-app-deploy')`.
343
+ runtime.registerDescriptor('runtime/client', {
344
+ id: 'digitalocean-app-deploy',
345
+ kind: 'runtime/client',
346
+ description:
347
+ 'DigitalOcean App Platform deploy client — creates/updates DO Apps from AppDeployment manifests.'
348
+ });
349
+ runtime.registerLoader('runtime/client', 'digitalocean-app-deploy', async () => {
350
+ const { DigitalOceanAppDeployClient } = await import(
351
+ './client/digitalocean-app-deploy.client'
352
+ );
353
+ return { impl: DigitalOceanAppDeployClient };
354
+ });
355
+
356
+ // infra/artifact: K8s Deployment generator
357
+ runtime.registerDescriptor('infra/artifact', {
358
+ id: 'k8s-deployment',
359
+ kind: 'infra/artifact',
360
+ description: 'Generates Kubernetes Deployment YAML from infra/worker descriptors'
361
+ });
362
+ runtime.registerLoader('infra/artifact', 'k8s-deployment', async () => {
363
+ return createRuntimeImplementation({
364
+ id: 'k8s-deployment',
365
+ kind: 'infra/artifact',
366
+ priority: 20,
367
+ implementation: {
368
+ async generate(context?: any) {
369
+ const resources = context?.resources || [];
370
+ const outputDir = context?.outputDir || './infra/generated';
371
+ const workerDescriptors = resources.filter((r: any) => r.kind === 'infra/worker');
372
+
373
+ if (workerDescriptors.length === 0) {
374
+ return {
375
+ content: '# No infra/worker descriptors found\n',
376
+ filename: 'k8s-deployment.yaml',
377
+ path: resolve(outputDir, 'kubernetes', 'k8s-deployment.yaml'),
378
+ metadata: { sourceKind: 'infra/worker', format: 'yaml', generator: 'k8s-deployment' }
379
+ };
380
+ }
381
+
382
+ const artifacts: string[] = [];
383
+ for (const descriptor of workerDescriptors) {
384
+ const parsed = DoksDeploymentDescriptorSchema.parse(descriptor);
385
+ const deployment: K8sDeployment = generateK8sDeployment(parsed);
386
+ const yamlContent = stringify(deployment);
387
+ artifacts.push(`---\n# Resource: ${descriptor.id}\n${yamlContent}`);
388
+ }
389
+
390
+ return {
391
+ content: artifacts.join('\n'),
392
+ filename: 'k8s-deployment.yaml',
393
+ path: resolve(outputDir, 'kubernetes', 'k8s-deployment.yaml'),
394
+ metadata: { sourceKind: 'infra/worker', format: 'yaml', generator: 'k8s-deployment' }
395
+ };
396
+ }
397
+ }
398
+ });
399
+ });
400
+ }
401
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Implementation: infra/worker -> kubernetes (doks-deployment)
3
+ *
4
+ * Generates a K8s Deployment manifest for worker descriptors.
5
+ */
6
+
7
+ import * as z from 'zod/v4';
8
+ import { DoksDeploymentWorkerConfigSchema } from '@vibesdotdev/infra-core/kinds';
9
+ import type { K8sDeployment } from '../types.ts';
10
+
11
+ export const DoksDeploymentDescriptorSchema = z.object({
12
+ kind: z.literal('infra/worker'),
13
+ id: z.string().min(1),
14
+ name: z.string().optional(),
15
+ description: z.string().optional(),
16
+ /** Path to worker entrypoint */
17
+ entrypoint: z.string().min(1),
18
+ /** Queue names to consume from */
19
+ queues: z.array(z.string().min(1)).default([]),
20
+ /** Worker concurrency */
21
+ concurrency: z.number().int().positive().default(1),
22
+ /** Scaling configuration */
23
+ scaling: z
24
+ .object({
25
+ min: z.number().int().nonnegative().default(1),
26
+ max: z.number().int().positive().default(1),
27
+ cpu: z.string().optional(),
28
+ memory: z.string().optional()
29
+ })
30
+ .optional(),
31
+ config: DoksDeploymentWorkerConfigSchema.extend({
32
+ adapter: z.literal('doks-deployment')
33
+ })
34
+ });
35
+
36
+ export type DoksDeploymentDescriptorInput = z.input<typeof DoksDeploymentDescriptorSchema>;
37
+ export type DoksDeploymentDescriptor = z.infer<typeof DoksDeploymentDescriptorSchema>;
38
+
39
+ export function generateK8sDeployment(descriptor: DoksDeploymentDescriptor): K8sDeployment {
40
+ const parsed = DoksDeploymentDescriptorSchema.parse(descriptor);
41
+ const ns = parsed.config?.namespace ?? 'vibes';
42
+ const image = parsed.config?.image ?? `vibes/${parsed.id}:latest`;
43
+ const healthPort = parsed.config?.healthPort ?? 8080;
44
+ const healthPath = parsed.config?.healthPath ?? '/healthz';
45
+ const readyPath = parsed.config?.readyPath ?? '/readyz';
46
+
47
+ return {
48
+ apiVersion: 'apps/v1',
49
+ kind: 'Deployment',
50
+ metadata: {
51
+ name: `vibes-${parsed.id}`,
52
+ namespace: ns,
53
+ labels: { app: parsed.id, 'app.kubernetes.io/part-of': 'vibes' }
54
+ },
55
+ spec: {
56
+ replicas: parsed.scaling?.min ?? 1,
57
+ selector: { matchLabels: { app: parsed.id } },
58
+ template: {
59
+ metadata: {
60
+ labels: { app: parsed.id }
61
+ },
62
+ spec: {
63
+ containers: [
64
+ {
65
+ name: parsed.id,
66
+ image,
67
+ command: ['bun', 'run', parsed.entrypoint],
68
+ resources: {
69
+ requests: {
70
+ cpu: parsed.scaling?.cpu ?? '250m',
71
+ memory: parsed.scaling?.memory ?? '256Mi'
72
+ },
73
+ limits: {
74
+ cpu: parsed.scaling?.cpu ?? '1',
75
+ memory: parsed.scaling?.memory ?? '512Mi'
76
+ }
77
+ },
78
+ envFrom: [{ configMapRef: { name: `vibes-${parsed.id}-config` } }],
79
+ livenessProbe: {
80
+ httpGet: { path: healthPath, port: healthPort },
81
+ periodSeconds: 30
82
+ },
83
+ readinessProbe: {
84
+ httpGet: { path: readyPath, port: healthPort },
85
+ periodSeconds: 10
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ }
91
+ }
92
+ };
93
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Implementation: infra/sandbox → digitalocean-droplet
3
+ *
4
+ * Wires the existing `DigitalOceanProvider` (./cloud/digitalocean.ts) into
5
+ * the runtime kind resolution path. The resolved impl exposes a
6
+ * `provision(descriptor)` method that creates a DigitalOcean droplet via
7
+ * the v2 API, waits for `status: 'active'`, and returns the platform-side
8
+ * `SandboxProvisionResult` shape that
9
+ * `apps/platform-web/src/lib/hosts/server/queries/host-provision.ts`
10
+ * consumes.
11
+ *
12
+ * The DO API token resolves in this order:
13
+ * 1. `descriptor.config.apiToken` (explicit override; tests use this).
14
+ * 2. `process.env.DIGITALOCEAN_API_TOKEN` (the existing dev fallback,
15
+ * matching the `DigitalOceanAppDeployClient` in `./client/`).
16
+ * 3. `packages/secrets` `cloud-credentials.digitalocean.token` —
17
+ * brokerage path, currently DEFERRED to the migration debt entry
18
+ * in `infra-deploy/SPEC.md`. When that lands, this impl resolves
19
+ * the same secret per actor scope.
20
+ */
21
+
22
+ import * as z from 'zod/v4';
23
+ import { DigitalOceanDropletSandboxConfigSchema } from '@vibesdotdev/infra-core/kinds';
24
+ import { DigitalOceanProvider } from '../cloud/digitalocean.ts';
25
+ import type { CloudInstance } from '../types.ts';
26
+
27
+ export const DoDropletConfigSchema = DigitalOceanDropletSandboxConfigSchema.extend({
28
+ adapter: z.literal('digitalocean-droplet'),
29
+ region: z.string().default('nyc3'),
30
+ image: z.string().default('ubuntu-24-04-x64'),
31
+ tags: z.array(z.string()).default([]),
32
+ sshKeyIds: z.array(z.string()).default([]),
33
+ apiToken: z.string().optional()
34
+ });
35
+
36
+ export const DoDropletDescriptorSchema = z.object({
37
+ kind: z.literal('infra/sandbox'),
38
+ id: z.string().min(1),
39
+ name: z.string().optional(),
40
+ description: z.string().optional(),
41
+ config: DoDropletConfigSchema,
42
+ cloudInitScript: z.string().optional()
43
+ });
44
+
45
+ export type DoDropletDescriptorInput = z.input<typeof DoDropletDescriptorSchema>;
46
+ export type DoDropletDescriptor = z.infer<typeof DoDropletDescriptorSchema>;
47
+
48
+ export interface SandboxProvisionResult {
49
+ instanceId: string;
50
+ publicIp?: string;
51
+ privateIp?: string;
52
+ region: string;
53
+ createdAt: Date;
54
+ }
55
+
56
+ import type { InstanceFilters } from '../types.ts';
57
+
58
+ export type ProviderFactory = (
59
+ apiToken: string,
60
+ defaultRegion: string
61
+ ) => {
62
+ createInstance(config: {
63
+ name: string;
64
+ type: string;
65
+ region: string;
66
+ image: string;
67
+ sshKeys?: string[];
68
+ userData?: string;
69
+ tags: Record<string, string>;
70
+ }): Promise<CloudInstance>;
71
+ listInstances(filters?: InstanceFilters): Promise<CloudInstance[]>;
72
+ };
73
+
74
+ function resolveApiToken(config: z.infer<typeof DoDropletConfigSchema>): string {
75
+ if (config.apiToken) return config.apiToken;
76
+ const envToken =
77
+ (typeof process !== 'undefined' && process.env?.DIGITALOCEAN_API_TOKEN) || '';
78
+ if (envToken) return envToken;
79
+ throw new Error(
80
+ 'digitalocean-droplet provision failed: no API token. Pass config.apiToken or set DIGITALOCEAN_API_TOKEN.'
81
+ );
82
+ }
83
+
84
+ function tagsArrayToRecord(tags: string[]): Record<string, string> {
85
+ const record: Record<string, string> = {};
86
+ for (const tag of tags) record[tag] = '';
87
+ return record;
88
+ }
89
+
90
+ /**
91
+ * `DoDropletSandbox` is a singleton-friendly impl: a single instance is
92
+ * registered per worker and serves every `provision(descriptor)` call.
93
+ * Per-call descriptors carry their own API token + region, so multi-tenant
94
+ * use is safe — the impl holds no per-tenant state.
95
+ *
96
+ * For unit tests, pass `providerFactory` to swap in a mock
97
+ * `DigitalOceanProvider`.
98
+ */
99
+ export class DoDropletSandbox {
100
+ private readonly providerFactory: ProviderFactory;
101
+
102
+ constructor(
103
+ providerFactory: ProviderFactory = (token, region) => new DigitalOceanProvider(token, region)
104
+ ) {
105
+ this.providerFactory = providerFactory;
106
+ }
107
+
108
+ async provision(descriptorInput: DoDropletDescriptorInput): Promise<SandboxProvisionResult> {
109
+ const descriptor = DoDropletDescriptorSchema.parse(descriptorInput);
110
+ const cfg = descriptor.config;
111
+ const provider = this.providerFactory(resolveApiToken(cfg), cfg.region);
112
+ const resourceTag = `vibes:resourceId=${descriptor.id}`;
113
+
114
+ // Idempotency: list existing droplets carrying our resource tag.
115
+ // Returning early prevents double-create on retry / restart while
116
+ // the upstream DB row still tracks the provisioning intent (per
117
+ // SPEC §Hard rule "Droplet creation is idempotent against the
118
+ // descriptor id").
119
+ const existing = await provider.listInstances({ tags: { vibes: resourceTag.replace('vibes:', '') } });
120
+ if (existing.length > 0) {
121
+ const instance = existing[0];
122
+ return {
123
+ instanceId: instance.id,
124
+ publicIp: instance.publicIp,
125
+ privateIp: instance.privateIp,
126
+ region: instance.region,
127
+ createdAt: instance.createdAt
128
+ };
129
+ }
130
+
131
+ // Inject the resource tag onto user-supplied tags. Duplicates are
132
+ // harmless — Record dedupes by key.
133
+ const tags = tagsArrayToRecord([...cfg.tags, resourceTag]);
134
+ const instance = await provider.createInstance({
135
+ name: descriptor.name ?? `vibes-${descriptor.id}`,
136
+ type: cfg.dropletSize,
137
+ region: cfg.region,
138
+ image: cfg.image,
139
+ sshKeys: cfg.sshKeyIds,
140
+ userData: descriptor.cloudInitScript,
141
+ tags
142
+ });
143
+ return {
144
+ instanceId: instance.id,
145
+ publicIp: instance.publicIp,
146
+ privateIp: instance.privateIp,
147
+ region: instance.region,
148
+ createdAt: instance.createdAt
149
+ };
150
+ }
151
+ }
152
+
153
+ export function createDoDropletImpl(
154
+ providerFactory?: ProviderFactory
155
+ ): DoDropletSandbox {
156
+ return new DoDropletSandbox(providerFactory);
157
+ }