@ankhorage/infra 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/adapters/minikube/auth/index.d.ts +7 -0
  4. package/dist/adapters/minikube/auth/index.d.ts.map +1 -0
  5. package/dist/adapters/minikube/auth/index.js +14 -0
  6. package/dist/adapters/minikube/auth/index.js.map +1 -0
  7. package/dist/adapters/minikube/auth/supabase/index.d.ts +7 -0
  8. package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -0
  9. package/dist/adapters/minikube/auth/supabase/index.js +213 -0
  10. package/dist/adapters/minikube/auth/supabase/index.js.map +1 -0
  11. package/dist/adapters/minikube/authz/cerbos/index.d.ts +9 -0
  12. package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -0
  13. package/dist/adapters/minikube/authz/cerbos/index.js +360 -0
  14. package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -0
  15. package/dist/adapters/minikube/authz/index.d.ts +9 -0
  16. package/dist/adapters/minikube/authz/index.d.ts.map +1 -0
  17. package/dist/adapters/minikube/authz/index.js +23 -0
  18. package/dist/adapters/minikube/authz/index.js.map +1 -0
  19. package/dist/adapters/minikube/base/index.d.ts +9 -0
  20. package/dist/adapters/minikube/base/index.d.ts.map +1 -0
  21. package/dist/adapters/minikube/base/index.js +1371 -0
  22. package/dist/adapters/minikube/base/index.js.map +1 -0
  23. package/dist/adapters/minikube/contracts.d.ts +9 -0
  24. package/dist/adapters/minikube/contracts.d.ts.map +1 -0
  25. package/dist/adapters/minikube/contracts.js +9 -0
  26. package/dist/adapters/minikube/contracts.js.map +1 -0
  27. package/dist/adapters/minikube/index.d.ts +3 -0
  28. package/dist/adapters/minikube/index.d.ts.map +1 -0
  29. package/dist/adapters/minikube/index.js +114 -0
  30. package/dist/adapters/minikube/index.js.map +1 -0
  31. package/dist/adapters/minikube/storage/index.d.ts +7 -0
  32. package/dist/adapters/minikube/storage/index.d.ts.map +1 -0
  33. package/dist/adapters/minikube/storage/index.js +77 -0
  34. package/dist/adapters/minikube/storage/index.js.map +1 -0
  35. package/dist/adapters/minikube/storage/supabase/index.d.ts +7 -0
  36. package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -0
  37. package/dist/adapters/minikube/storage/supabase/index.js +81 -0
  38. package/dist/adapters/minikube/storage/supabase/index.js.map +1 -0
  39. package/dist/datasets.d.ts +11 -0
  40. package/dist/datasets.d.ts.map +1 -0
  41. package/dist/datasets.js +397 -0
  42. package/dist/datasets.js.map +1 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +15 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/types.d.ts +70 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +86 -0
  52. package/src/adapters/minikube/README.md +73 -0
  53. package/src/adapters/minikube/auth/index.ts +23 -0
  54. package/src/adapters/minikube/auth/supabase/index.ts +269 -0
  55. package/src/adapters/minikube/authz/cerbos/index.ts +458 -0
  56. package/src/adapters/minikube/authz/index.ts +32 -0
  57. package/src/adapters/minikube/base/index.test.ts +103 -0
  58. package/src/adapters/minikube/base/index.ts +1478 -0
  59. package/src/adapters/minikube/contracts.ts +17 -0
  60. package/src/adapters/minikube/index.ts +149 -0
  61. package/src/adapters/minikube/storage/index.ts +91 -0
  62. package/src/adapters/minikube/storage/supabase/index.ts +112 -0
  63. package/src/datasets.test.ts +178 -0
  64. package/src/datasets.ts +562 -0
  65. package/src/index.test.ts +679 -0
  66. package/src/index.ts +43 -0
  67. package/src/minikubeImageLifecycle.test.ts +34 -0
  68. package/src/types.ts +82 -0
@@ -0,0 +1,1371 @@
1
+ const SUPABASE_LOCAL_PORT_BASE = 55320;
2
+ const SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;
3
+ const SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;
4
+ const SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';
5
+ export function generateMinikubeBaseArtifacts(args) {
6
+ const { manifest, namespace, extraResources, extraEnvEntries } = args;
7
+ const root = 'infra/minikube';
8
+ const k8sRoot = `${root}/k8s`;
9
+ const scriptsRoot = `${root}/scripts`;
10
+ const appImageRoot = `${root}/app-image`;
11
+ const defaultAppImage = getDefaultAppImage(namespace);
12
+ const supabaseLocalPorts = resolveSupabaseLocalPorts(namespace);
13
+ const authScope = manifest.auth?.scope ?? 'none';
14
+ const authProvider = manifest.auth?.provider ?? 'none';
15
+ const authzEngine = manifest.auth?.authorization.engine ?? 'none';
16
+ const databaseProvider = manifest.database?.provider ?? 'none';
17
+ const storageMetadata = resolveAppInfraStorageMetadata(manifest);
18
+ const monitoringEnabled = manifest.deployment?.monitoring === true;
19
+ const domain = manifest.networking?.domain ?? '';
20
+ return [
21
+ {
22
+ path: `${root}/README.md`,
23
+ content: getReadmeMarkdown({
24
+ namespace,
25
+ defaultAppImage,
26
+ monitoringEnabled,
27
+ authProvider,
28
+ authzEngine,
29
+ extraResources,
30
+ supabaseLocalPorts,
31
+ }),
32
+ },
33
+ {
34
+ path: `${root}/.env.example`,
35
+ content: getEnvExample({
36
+ namespace,
37
+ domain,
38
+ extraEnvEntries,
39
+ defaultAppImage,
40
+ supabaseLocalPorts,
41
+ }),
42
+ },
43
+ {
44
+ path: `${k8sRoot}/namespace.yaml`,
45
+ content: getNamespaceManifest(namespace),
46
+ },
47
+ {
48
+ path: `${k8sRoot}/app.configmap.yaml`,
49
+ content: getAppConfigMap({
50
+ namespace,
51
+ authScope,
52
+ authProvider,
53
+ databaseProvider,
54
+ storageMetadata,
55
+ monitoringEnabled,
56
+ domain,
57
+ }),
58
+ },
59
+ {
60
+ path: `${k8sRoot}/app/deployment.yaml`,
61
+ content: getAppDeploymentManifest({ namespace, defaultAppImage }),
62
+ },
63
+ {
64
+ path: `${k8sRoot}/app/service.yaml`,
65
+ content: getAppServiceManifest(namespace),
66
+ },
67
+ {
68
+ path: `${k8sRoot}/kustomization.yaml`,
69
+ content: getKustomizationManifest(extraResources),
70
+ },
71
+ {
72
+ path: `${scriptsRoot}/up.sh`,
73
+ content: getUpScript({ defaultNamespace: namespace, defaultAppImage }),
74
+ executable: true,
75
+ },
76
+ {
77
+ path: `${scriptsRoot}/build-app-image.sh`,
78
+ content: getBuildAppImageScript({ defaultNamespace: namespace, defaultAppImage }),
79
+ executable: true,
80
+ },
81
+ {
82
+ path: `${scriptsRoot}/port-forward.sh`,
83
+ content: getPortForwardScript(namespace),
84
+ executable: true,
85
+ },
86
+ {
87
+ path: `${scriptsRoot}/down.sh`,
88
+ content: getDownScript(),
89
+ executable: true,
90
+ },
91
+ {
92
+ path: `${scriptsRoot}/status.sh`,
93
+ content: getStatusScript(namespace),
94
+ executable: true,
95
+ },
96
+ {
97
+ path: `${scriptsRoot}/supabase-local-env.sh`,
98
+ content: getSupabaseLocalEnvScript(supabaseLocalPorts),
99
+ executable: true,
100
+ },
101
+ {
102
+ path: `${appImageRoot}/Dockerfile`,
103
+ content: getAppImageDockerfile(),
104
+ },
105
+ ];
106
+ }
107
+ function getReadmeMarkdown(args) {
108
+ const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, } = args;
109
+ const resourceLines = [
110
+ 'namespace.yaml',
111
+ 'app.configmap.yaml',
112
+ 'app/deployment.yaml',
113
+ 'app/service.yaml',
114
+ ...extraResources,
115
+ ]
116
+ .map((r) => `- \`k8s/${r}\``)
117
+ .join('\n');
118
+ return `# Minikube Infra
119
+
120
+ This directory is generated from \`ankh.config.json\` (infra manifest).
121
+
122
+ ## Requirements
123
+
124
+ - minikube
125
+ - kubectl
126
+ - docker (when using the default driver)
127
+ - bun (for Expo web export in app image build flow)
128
+ - python3 (for patching generated local Supabase port defaults and checking port availability)
129
+
130
+ ## Quick Start
131
+
132
+ 1. Copy env template:
133
+ \`\`\`bash
134
+ cp .env.example .env
135
+ \`\`\`
136
+ 2. Optional: import local Supabase credentials from \`supabase status\` output:
137
+ \`\`\`bash
138
+ ./scripts/supabase-local-env.sh
139
+ \`\`\`
140
+ 3. Apply baseline infra:
141
+ \`\`\`bash
142
+ ./scripts/up.sh
143
+ \`\`\`
144
+ 4. Start app port-forward:
145
+ \`\`\`bash
146
+ ./scripts/port-forward.sh
147
+ \`\`\`
148
+ 5. Check status:
149
+ \`\`\`bash
150
+ ./scripts/status.sh
151
+ \`\`\`
152
+ 6. Tear down:
153
+ \`\`\`bash
154
+ ./scripts/down.sh
155
+ \`\`\`
156
+
157
+ ## Generated Resources
158
+
159
+ ${resourceLines}
160
+
161
+ ## Generated Helpers
162
+
163
+ - \`scripts/build-app-image.sh\`
164
+ - \`scripts/port-forward.sh\`
165
+ - \`scripts/supabase-local-env.sh\`
166
+ - \`app-image/Dockerfile\`
167
+
168
+ ## Runtime Conventions
169
+
170
+ - Namespace: \`${namespace}\`
171
+ - Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
172
+ - Auth provider: \`${authProvider}\`
173
+ - Authorization engine: \`${authzEngine}\`
174
+ - Namespace source: \`infra.networking.domain\` first, otherwise project slug hint from CLI.
175
+
176
+ ## App Runtime
177
+
178
+ Default app runtime manifests are generated:
179
+
180
+ - \`k8s/app/deployment.yaml\`
181
+ - \`k8s/app/service.yaml\`
182
+
183
+ Deployment env wiring includes:
184
+
185
+ - \`ConfigMap/app-infra-config\`
186
+ - \`ConfigMap/app-runtime-auth-env\` (optional)
187
+ - \`ConfigMap/app-runtime-storage-env\` (optional)
188
+ - \`Secret/supabase-auth-secrets\` (optional)
189
+
190
+ If \`auth.provider\` is \`supabase\`, use:
191
+
192
+ - \`k8s/auth/supabase/supabase-auth.secret.yaml\`
193
+ - \`k8s/auth/supabase/app-runtime-auth.env.configmap.yaml\`
194
+ - \`auth/supabase-runtime-wiring.md\`
195
+
196
+ The app deployment should import runtime config sources in this order:
197
+
198
+ \`\`\`yaml
199
+ envFrom:
200
+ - configMapRef:
201
+ name: app-runtime-auth-env
202
+ - configMapRef:
203
+ name: app-runtime-storage-env
204
+ - secretRef:
205
+ name: supabase-auth-secrets
206
+ \`\`\`
207
+
208
+ Default runtime knobs in \`.env\`:
209
+
210
+ - \`APP_BUILD_ENABLED\`
211
+ - \`APP_SOURCE_DIR\`
212
+ - \`APP_WEB_EXPORT_DIR\`
213
+ - \`APP_IMAGE\`
214
+ - \`APP_IMAGE_SYNC_STRATEGY\`
215
+ - \`APP_IMAGE_CLEANUP_ON_DOWN\`
216
+ - \`APP_IMAGE_CLEANUP_MINIKUBE\`
217
+ - \`APP_IMAGE_CLEANUP_DOCKER\`
218
+ - \`AUTH_RUNTIME_MODE\`
219
+ - \`APP_PORT_FORWARD_LOCAL_PORT\`
220
+ - \`APP_PORT_FORWARD_REMOTE_PORT\`
221
+ - \`APP_REPLICAS\`
222
+ - \`APP_FORCE_ROLLOUT_RESTART\`
223
+ - \`SUPABASE_PROJECT_DIR\` (optional supabase CLI project path)
224
+ - \`SUPABASE_LOCAL_PORT_BASE\` (generated default \`${supabaseLocalPorts.base}\`)
225
+ - \`SUPABASE_LOCAL_API_PORT\` (generated default \`${supabaseLocalPorts.api}\`)
226
+ - \`SUPABASE_LOCAL_DB_PORT\` (generated default \`${supabaseLocalPorts.db}\`)
227
+ - \`SUPABASE_LOCAL_STUDIO_PORT\` (generated default \`${supabaseLocalPorts.studio}\`)
228
+ - \`SUPABASE_LOCAL_INBUCKET_PORT\` (generated default \`${supabaseLocalPorts.inbucket}\`)
229
+ - \`SUPABASE_LOCAL_ANALYTICS_PORT\` (generated default \`${supabaseLocalPorts.analytics}\`)
230
+ - \`SUPABASE_LOCAL_SHADOW_PORT\` (generated default \`${supabaseLocalPorts.shadow}\`)
231
+ - \`APP_IMAGE_PULL_SECRET_NAME\` + credentials (optional, private registry)
232
+ - \`SUPABASE_SECRET_SYNC_ENABLED\`
233
+
234
+ Generated app Supabase local defaults:
235
+
236
+ - Port base: \`${supabaseLocalPorts.base}\`
237
+ - API: \`http://127.0.0.1:${supabaseLocalPorts.api}\`
238
+ - DB: \`127.0.0.1:${supabaseLocalPorts.db}\`
239
+ - Studio: \`http://127.0.0.1:${supabaseLocalPorts.studio}\`
240
+ - Inbucket: \`http://127.0.0.1:${supabaseLocalPorts.inbucket}\`
241
+ - Analytics: \`http://127.0.0.1:${supabaseLocalPorts.analytics}\`
242
+ - Shadow DB: \`127.0.0.1:${supabaseLocalPorts.shadow}\`
243
+
244
+ Override with:
245
+
246
+ - \`SUPABASE_LOCAL_PORT_BASE\`
247
+ - \`SUPABASE_LOCAL_API_PORT\`
248
+ - \`SUPABASE_LOCAL_DB_PORT\`
249
+ - \`SUPABASE_LOCAL_STUDIO_PORT\`
250
+ - \`SUPABASE_LOCAL_INBUCKET_PORT\`
251
+ - \`SUPABASE_LOCAL_ANALYTICS_PORT\`
252
+ - \`SUPABASE_LOCAL_SHADOW_PORT\`
253
+
254
+ Build flow:
255
+
256
+ - \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
257
+ - \`./scripts/supabase-local-env.sh\` can populate Supabase keys in \`.env\` from local \`supabase status -o env\`.
258
+ - \`./scripts/supabase-local-env.sh\` writes \`EXPO_PUBLIC_SUPABASE_URL\` + \`EXPO_PUBLIC_SUPABASE_ANON_KEY\` into \`$APP_SOURCE_DIR/.env.local\` (e.g. \`apps/card/.env.local\`) for local app runs.
259
+ - With \`AUTH_RUNTIME_MODE=local\` (default), \`up.sh\` auto-runs \`supabase-local-env.sh\` when Supabase keys are missing.
260
+ - \`supabase-local-env.sh\` checks configured host ports before starting a missing local Supabase stack.
261
+ - \`build-app-image.sh\` runs \`bunx expo export --platform web\` from app source.
262
+ - \`app-image/Dockerfile\` builds an nginx image from exported web artifacts.
263
+ - \`build-app-image.sh\` labels the generated Docker image with Ankhorage metadata for diagnostics.
264
+ - \`up.sh\` syncs runtime image by strategy:
265
+ - \`APP_IMAGE_SYNC_STRATEGY=docker-load\` (default): load prebuilt local Docker image into Minikube.
266
+ - \`APP_IMAGE_SYNC_STRATEGY=minikube-build\`: build directly into Minikube image store.
267
+ - \`APP_IMAGE_SYNC_STRATEGY=none\`: skip local image sync.
268
+ - \`up.sh\` applies image/replica changes and (by default) restarts \`deployment/app-runtime\`.
269
+ - If Supabase keys are provided, \`up.sh\` syncs \`Secret/supabase-auth-secrets\` from env.
270
+
271
+ Default generated app image tag: \`${defaultAppImage}\`.
272
+
273
+ ## Notes
274
+
275
+ - Auth/database/authorization resources are generated in adapter modules.
276
+ - Cerbos route/screen policies derive from app navigator + authFlow intent when available.
277
+ - Re-generate infra by saving the manifest or calling the bridge infra endpoint.
278
+ `;
279
+ }
280
+ function getEnvExample(args) {
281
+ const { namespace, domain, extraEnvEntries, defaultAppImage, supabaseLocalPorts } = args;
282
+ const baseEntries = [
283
+ '# Minikube runtime configuration',
284
+ `ANKH_NAMESPACE=${namespace}`,
285
+ 'MINIKUBE_PROFILE=minikube',
286
+ 'MINIKUBE_DRIVER=docker',
287
+ 'APP_BUILD_ENABLED=true',
288
+ 'APP_SOURCE_DIR=',
289
+ 'APP_WEB_EXPORT_DIR=.ankh/web-export',
290
+ 'SUPABASE_PROJECT_DIR=',
291
+ `SUPABASE_LOCAL_PORT_BASE=${supabaseLocalPorts.base}`,
292
+ `SUPABASE_LOCAL_SHADOW_PORT=${supabaseLocalPorts.shadow}`,
293
+ `SUPABASE_LOCAL_API_PORT=${supabaseLocalPorts.api}`,
294
+ `SUPABASE_LOCAL_DB_PORT=${supabaseLocalPorts.db}`,
295
+ `SUPABASE_LOCAL_STUDIO_PORT=${supabaseLocalPorts.studio}`,
296
+ `SUPABASE_LOCAL_INBUCKET_PORT=${supabaseLocalPorts.inbucket}`,
297
+ `SUPABASE_LOCAL_ANALYTICS_PORT=${supabaseLocalPorts.analytics}`,
298
+ `APP_IMAGE=${defaultAppImage}`,
299
+ 'APP_IMAGE_SYNC_STRATEGY=docker-load',
300
+ 'APP_IMAGE_CLEANUP_ON_DOWN=true',
301
+ 'APP_IMAGE_CLEANUP_MINIKUBE=true',
302
+ 'APP_IMAGE_CLEANUP_DOCKER=true',
303
+ 'AUTH_RUNTIME_MODE=local',
304
+ 'APP_PORT_FORWARD_LOCAL_PORT=18080',
305
+ 'APP_PORT_FORWARD_REMOTE_PORT=80',
306
+ 'APP_REPLICAS=1',
307
+ 'APP_FORCE_ROLLOUT_RESTART=true',
308
+ '',
309
+ '# Optional private registry auth for APP_IMAGE',
310
+ 'APP_IMAGE_PULL_SECRET_NAME=',
311
+ 'APP_IMAGE_PULL_SECRET_SERVER=ghcr.io',
312
+ 'APP_IMAGE_PULL_SECRET_USERNAME=',
313
+ 'APP_IMAGE_PULL_SECRET_PASSWORD=',
314
+ 'APP_IMAGE_PULL_SECRET_EMAIL=',
315
+ '',
316
+ '# Optional app networking domain',
317
+ `APP_DOMAIN=${domain}`,
318
+ '',
319
+ '# Adapter-provided runtime keys',
320
+ ];
321
+ const merged = [...baseEntries, ...extraEnvEntries];
322
+ const unique = [];
323
+ for (const entry of merged) {
324
+ if (!unique.includes(entry))
325
+ unique.push(entry);
326
+ }
327
+ return `${unique.join('\n')}\n`;
328
+ }
329
+ function getNamespaceManifest(namespace) {
330
+ return `apiVersion: v1
331
+ kind: Namespace
332
+ metadata:
333
+ name: ${namespace}
334
+ labels:
335
+ app.kubernetes.io/part-of: ankhorage
336
+ `;
337
+ }
338
+ function getAppConfigMap(args) {
339
+ const { namespace, authScope, authProvider, databaseProvider, storageMetadata, monitoringEnabled, domain, } = args;
340
+ const storageLines = storageMetadata
341
+ ? ` STORAGE_PROVIDER: "${storageMetadata.provider}"
342
+ STORAGE_BUCKETS: "${escapeYamlDoubleQuoted(storageMetadata.bucketsCsv)}"
343
+ STORAGE_DEFAULT_BUCKET: "${escapeYamlDoubleQuoted(storageMetadata.defaultBucket)}"
344
+ `
345
+ : '';
346
+ return `apiVersion: v1
347
+ kind: ConfigMap
348
+ metadata:
349
+ name: app-infra-config
350
+ namespace: ${namespace}
351
+ data:
352
+ DEPLOYMENT_TARGET: "minikube"
353
+ MONITORING_ENABLED: "${monitoringEnabled ? 'true' : 'false'}"
354
+ AUTH_SCOPE: "${authScope}"
355
+ AUTH_PROVIDER: "${authProvider}"
356
+ DATABASE_PROVIDER: "${databaseProvider}"
357
+ ${storageLines} NETWORK_DOMAIN: "${domain}"
358
+ `;
359
+ }
360
+ function resolveAppInfraStorageMetadata(manifest) {
361
+ const spec = manifest.storage;
362
+ if (!spec)
363
+ return null;
364
+ const buckets = normalizeBuckets(spec.buckets);
365
+ if (buckets.length === 0)
366
+ return null;
367
+ const [defaultBucket] = buckets;
368
+ if (!defaultBucket)
369
+ return null;
370
+ if (spec.provider === 'supabase') {
371
+ return {
372
+ provider: 'supabase',
373
+ bucketsCsv: buckets.join(','),
374
+ defaultBucket,
375
+ };
376
+ }
377
+ if (spec.provider === 'auto') {
378
+ const shouldResolveToSupabase = manifest.database?.provider === 'supabase' || manifest.auth?.provider === 'supabase';
379
+ if (!shouldResolveToSupabase)
380
+ return null;
381
+ return {
382
+ provider: 'supabase',
383
+ bucketsCsv: buckets.join(','),
384
+ defaultBucket,
385
+ };
386
+ }
387
+ return null;
388
+ }
389
+ function normalizeBuckets(rawBuckets) {
390
+ const normalized = [];
391
+ const seen = new Set();
392
+ for (const raw of rawBuckets) {
393
+ const bucket = raw.trim();
394
+ if (!bucket)
395
+ continue;
396
+ if (seen.has(bucket))
397
+ continue;
398
+ seen.add(bucket);
399
+ normalized.push(bucket);
400
+ }
401
+ return normalized;
402
+ }
403
+ function escapeYamlDoubleQuoted(value) {
404
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
405
+ }
406
+ function getKustomizationManifest(extraResources) {
407
+ const resources = [
408
+ 'namespace.yaml',
409
+ 'app.configmap.yaml',
410
+ 'app/deployment.yaml',
411
+ 'app/service.yaml',
412
+ ...extraResources,
413
+ ];
414
+ const resourceLines = resources.map((r) => ` - ${r}`).join('\n');
415
+ return `apiVersion: kustomize.config.k8s.io/v1beta1
416
+ kind: Kustomization
417
+ resources:
418
+ ${resourceLines}
419
+ `;
420
+ }
421
+ function getAppDeploymentManifest(args) {
422
+ const { namespace, defaultAppImage } = args;
423
+ return `apiVersion: apps/v1
424
+ kind: Deployment
425
+ metadata:
426
+ name: app-runtime
427
+ namespace: ${namespace}
428
+ labels:
429
+ app.kubernetes.io/name: app-runtime
430
+ spec:
431
+ replicas: 1
432
+ selector:
433
+ matchLabels:
434
+ app.kubernetes.io/name: app-runtime
435
+ template:
436
+ metadata:
437
+ labels:
438
+ app.kubernetes.io/name: app-runtime
439
+ spec:
440
+ containers:
441
+ - name: app
442
+ image: ${defaultAppImage}
443
+ imagePullPolicy: IfNotPresent
444
+ ports:
445
+ - containerPort: 8080
446
+ name: http
447
+ envFrom:
448
+ - configMapRef:
449
+ name: app-infra-config
450
+ - configMapRef:
451
+ name: app-runtime-auth-env
452
+ optional: true
453
+ - configMapRef:
454
+ name: app-runtime-storage-env
455
+ optional: true
456
+ - secretRef:
457
+ name: supabase-auth-secrets
458
+ optional: true
459
+ readinessProbe:
460
+ httpGet:
461
+ path: /
462
+ port: http
463
+ initialDelaySeconds: 5
464
+ periodSeconds: 10
465
+ livenessProbe:
466
+ httpGet:
467
+ path: /
468
+ port: http
469
+ initialDelaySeconds: 15
470
+ periodSeconds: 20
471
+ `;
472
+ }
473
+ function getAppServiceManifest(namespace) {
474
+ return `apiVersion: v1
475
+ kind: Service
476
+ metadata:
477
+ name: app-runtime
478
+ namespace: ${namespace}
479
+ labels:
480
+ app.kubernetes.io/name: app-runtime
481
+ spec:
482
+ type: ClusterIP
483
+ selector:
484
+ app.kubernetes.io/name: app-runtime
485
+ ports:
486
+ - name: http
487
+ port: 80
488
+ targetPort: http
489
+ `;
490
+ }
491
+ function getUpScript(args) {
492
+ const { defaultNamespace, defaultAppImage } = args;
493
+ return `#!/usr/bin/env bash
494
+ set -euo pipefail
495
+
496
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
497
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
498
+ K8S_DIR="\${ROOT_DIR}/k8s"
499
+ BUILD_SCRIPT="\${SCRIPT_DIR}/build-app-image.sh"
500
+
501
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
502
+ set -a
503
+ # shellcheck disable=SC1090
504
+ source "\${ROOT_DIR}/.env"
505
+ set +a
506
+ fi
507
+
508
+ PROFILE="\${MINIKUBE_PROFILE:-minikube}"
509
+ DRIVER="\${MINIKUBE_DRIVER:-docker}"
510
+ NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
511
+ APP_BUILD_ENABLED="\${APP_BUILD_ENABLED:-true}"
512
+ APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
513
+ APP_WEB_EXPORT_DIR="\${APP_WEB_EXPORT_DIR:-.ankh/web-export}"
514
+ APP_IMAGE="\${APP_IMAGE:-${defaultAppImage}}"
515
+ APP_IMAGE_SYNC_STRATEGY="\${APP_IMAGE_SYNC_STRATEGY:-docker-load}"
516
+ APP_REPLICAS="\${APP_REPLICAS:-1}"
517
+ APP_FORCE_ROLLOUT_RESTART="\${APP_FORCE_ROLLOUT_RESTART:-true}"
518
+ APP_IMAGE_PULL_SECRET_NAME="\${APP_IMAGE_PULL_SECRET_NAME:-}"
519
+ APP_IMAGE_PULL_SECRET_SERVER="\${APP_IMAGE_PULL_SECRET_SERVER:-ghcr.io}"
520
+ APP_IMAGE_PULL_SECRET_USERNAME="\${APP_IMAGE_PULL_SECRET_USERNAME:-}"
521
+ APP_IMAGE_PULL_SECRET_PASSWORD="\${APP_IMAGE_PULL_SECRET_PASSWORD:-}"
522
+ APP_IMAGE_PULL_SECRET_EMAIL="\${APP_IMAGE_PULL_SECRET_EMAIL:-}"
523
+ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
524
+ SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
525
+ SUPABASE_LOCAL_ENV_SCRIPT="\${SCRIPT_DIR}/supabase-local-env.sh"
526
+ SUPABASE_URL="\${SUPABASE_URL:-}"
527
+ SUPABASE_ANON_KEY="\${SUPABASE_ANON_KEY:-}"
528
+ SUPABASE_SERVICE_ROLE_KEY="\${SUPABASE_SERVICE_ROLE_KEY:-}"
529
+ SUPABASE_JWT_SECRET="\${SUPABASE_JWT_SECRET:-}"
530
+ SUPABASE_PUBLIC_URL="\${EXPO_PUBLIC_SUPABASE_URL:-\${SUPABASE_URL}}"
531
+ SUPABASE_PUBLIC_ANON_KEY="\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\${SUPABASE_ANON_KEY}}"
532
+
533
+ refresh_supabase_env() {
534
+ SUPABASE_URL="\${SUPABASE_URL:-}"
535
+ SUPABASE_ANON_KEY="\${SUPABASE_ANON_KEY:-}"
536
+ SUPABASE_SERVICE_ROLE_KEY="\${SUPABASE_SERVICE_ROLE_KEY:-}"
537
+ SUPABASE_JWT_SECRET="\${SUPABASE_JWT_SECRET:-}"
538
+ SUPABASE_PUBLIC_URL="\${EXPO_PUBLIC_SUPABASE_URL:-\${SUPABASE_URL}}"
539
+ SUPABASE_PUBLIC_ANON_KEY="\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\${SUPABASE_ANON_KEY}}"
540
+ }
541
+
542
+ has_required_supabase_env() {
543
+ [[ -n "\${SUPABASE_URL}" && -n "\${SUPABASE_ANON_KEY}" && -n "\${SUPABASE_SERVICE_ROLE_KEY}" && -n "\${SUPABASE_JWT_SECRET}" ]]
544
+ }
545
+
546
+ refresh_supabase_env
547
+
548
+ if [[ "\${AUTH_RUNTIME_MODE}" != "local" && "\${AUTH_RUNTIME_MODE}" != "remote" ]]; then
549
+ echo "Unsupported AUTH_RUNTIME_MODE=\${AUTH_RUNTIME_MODE}. Use local or remote."
550
+ exit 1
551
+ fi
552
+
553
+ if ! command -v minikube >/dev/null 2>&1; then
554
+ echo "minikube is required but not installed."
555
+ exit 1
556
+ fi
557
+
558
+ if ! command -v kubectl >/dev/null 2>&1; then
559
+ echo "kubectl is required but not installed."
560
+ exit 1
561
+ fi
562
+
563
+ HOST_STATUS="$(minikube -p "\${PROFILE}" status --format='{{.Host}}' 2>/dev/null || true)"
564
+ if [[ "\${HOST_STATUS}" != "Running" ]]; then
565
+ minikube start -p "\${PROFILE}" --driver="\${DRIVER}"
566
+ fi
567
+
568
+ if ! has_required_supabase_env && [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
569
+ if [[ ! -x "\${SUPABASE_LOCAL_ENV_SCRIPT}" ]]; then
570
+ echo "Supabase local bootstrap skipped before app build: missing executable \${SUPABASE_LOCAL_ENV_SCRIPT}."
571
+ else
572
+ echo "Supabase keys missing before app build; running \${SUPABASE_LOCAL_ENV_SCRIPT}."
573
+ if ! "\${SUPABASE_LOCAL_ENV_SCRIPT}"; then
574
+ echo "Supabase local bootstrap failed before app build; continuing."
575
+ fi
576
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
577
+ set -a
578
+ # shellcheck disable=SC1090
579
+ source "\${ROOT_DIR}/.env"
580
+ set +a
581
+ fi
582
+ refresh_supabase_env
583
+ fi
584
+ fi
585
+
586
+ if [[ "\${APP_BUILD_ENABLED}" == "true" ]]; then
587
+ if [[ ! -x "\${BUILD_SCRIPT}" ]]; then
588
+ echo "Missing build helper script: \${BUILD_SCRIPT}"
589
+ exit 1
590
+ fi
591
+
592
+ "\${BUILD_SCRIPT}"
593
+ fi
594
+
595
+ if [[ "\${APP_WEB_EXPORT_DIR}" = /* ]]; then
596
+ EXPORT_DIR="\${APP_WEB_EXPORT_DIR}"
597
+ else
598
+ EXPORT_DIR="\${APP_SOURCE_DIR}/\${APP_WEB_EXPORT_DIR}"
599
+ fi
600
+
601
+ docker_load_image_to_minikube() {
602
+ if command -v docker >/dev/null 2>&1 && docker image inspect "\${APP_IMAGE}" >/dev/null 2>&1; then
603
+ minikube -p "\${PROFILE}" image load --daemon=true --overwrite=true "\${APP_IMAGE}" >/dev/null
604
+ return 0
605
+ fi
606
+
607
+ return 1
608
+ }
609
+
610
+ case "\${APP_IMAGE_SYNC_STRATEGY}" in
611
+ minikube-build)
612
+ if [[ ! -f "\${EXPORT_DIR}/index.html" ]]; then
613
+ echo "Expected web export output not found at \${EXPORT_DIR}/index.html"
614
+ echo "Run build-app-image.sh first or switch APP_IMAGE_SYNC_STRATEGY=docker-load."
615
+ exit 1
616
+ fi
617
+
618
+ if minikube -p "\${PROFILE}" image build -t "\${APP_IMAGE}" -f "\${ROOT_DIR}/app-image/Dockerfile" "\${EXPORT_DIR}" >/dev/null; then
619
+ if ! minikube -p "\${PROFILE}" image ls 2>/dev/null | grep -Fq "\${APP_IMAGE}"; then
620
+ echo "minikube image build did not register \${APP_IMAGE}; falling back to docker-load."
621
+ if ! docker_load_image_to_minikube; then
622
+ echo "Fallback docker-load failed: local Docker image \${APP_IMAGE} not found."
623
+ exit 1
624
+ fi
625
+ fi
626
+ else
627
+ echo "minikube image build failed for \${APP_IMAGE}; falling back to docker-load."
628
+ if ! docker_load_image_to_minikube; then
629
+ echo "Fallback docker-load failed: local Docker image \${APP_IMAGE} not found."
630
+ exit 1
631
+ fi
632
+ fi
633
+ ;;
634
+ docker-load)
635
+ if ! docker_load_image_to_minikube; then
636
+ echo "Skipping docker-load image sync; local Docker image \${APP_IMAGE} not found."
637
+ fi
638
+ ;;
639
+ none)
640
+ echo "Skipping local image sync (APP_IMAGE_SYNC_STRATEGY=none)."
641
+ ;;
642
+ *)
643
+ echo "Unsupported APP_IMAGE_SYNC_STRATEGY=\${APP_IMAGE_SYNC_STRATEGY}. Use minikube-build, docker-load, or none."
644
+ exit 1
645
+ ;;
646
+ esac
647
+
648
+ kubectl apply -k "\${K8S_DIR}"
649
+
650
+ if [[ "\${SUPABASE_SECRET_SYNC_ENABLED}" == "true" ]]; then
651
+ if ! has_required_supabase_env && [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
652
+ if [[ ! -x "\${SUPABASE_LOCAL_ENV_SCRIPT}" ]]; then
653
+ echo "Supabase local bootstrap skipped: missing executable \${SUPABASE_LOCAL_ENV_SCRIPT}."
654
+ else
655
+ echo "Supabase keys missing; running \${SUPABASE_LOCAL_ENV_SCRIPT}."
656
+ if ! "\${SUPABASE_LOCAL_ENV_SCRIPT}"; then
657
+ echo "Supabase local bootstrap failed; continuing without local credential import."
658
+ fi
659
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
660
+ set -a
661
+ # shellcheck disable=SC1090
662
+ source "\${ROOT_DIR}/.env"
663
+ set +a
664
+ fi
665
+ refresh_supabase_env
666
+ fi
667
+ fi
668
+
669
+ if ! has_required_supabase_env; then
670
+ if [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
671
+ echo "Supabase secret sync skipped: local credentials still missing after bootstrap."
672
+ echo "Run ./scripts/supabase-local-env.sh and retry."
673
+ else
674
+ echo "Supabase secret sync skipped: set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_JWT_SECRET in .env."
675
+ fi
676
+ else
677
+ TMP_SUPABASE_ENV_FILE="$(mktemp)"
678
+ trap 'rm -f "\${TMP_SUPABASE_ENV_FILE}"' EXIT
679
+
680
+ cat > "\${TMP_SUPABASE_ENV_FILE}" <<EOF
681
+ SUPABASE_URL=\${SUPABASE_URL}
682
+ SUPABASE_ANON_KEY=\${SUPABASE_ANON_KEY}
683
+ SUPABASE_SERVICE_ROLE_KEY=\${SUPABASE_SERVICE_ROLE_KEY}
684
+ SUPABASE_JWT_SECRET=\${SUPABASE_JWT_SECRET}
685
+ EXPO_PUBLIC_SUPABASE_URL=\${SUPABASE_PUBLIC_URL}
686
+ EXPO_PUBLIC_SUPABASE_ANON_KEY=\${SUPABASE_PUBLIC_ANON_KEY}
687
+ EOF
688
+
689
+ kubectl -n "\${NAMESPACE}" create secret generic supabase-auth-secrets --from-env-file="\${TMP_SUPABASE_ENV_FILE}" --dry-run=client -o yaml | kubectl -n "\${NAMESPACE}" apply -f -
690
+
691
+ rm -f "\${TMP_SUPABASE_ENV_FILE}"
692
+ trap - EXIT
693
+ echo "Synchronized supabase-auth-secrets from environment values."
694
+ fi
695
+ fi
696
+
697
+ if [[ -n "\${APP_IMAGE_PULL_SECRET_NAME}" ]]; then
698
+ if [[ -z "\${APP_IMAGE_PULL_SECRET_USERNAME}" || -z "\${APP_IMAGE_PULL_SECRET_PASSWORD}" ]]; then
699
+ echo "APP_IMAGE_PULL_SECRET_NAME is set, but username/password are missing."
700
+ exit 1
701
+ fi
702
+
703
+ SECRET_EMAIL_ARG=()
704
+ if [[ -n "\${APP_IMAGE_PULL_SECRET_EMAIL}" ]]; then
705
+ SECRET_EMAIL_ARG=(--docker-email="\${APP_IMAGE_PULL_SECRET_EMAIL}")
706
+ fi
707
+
708
+ kubectl -n "\${NAMESPACE}" create secret docker-registry "\${APP_IMAGE_PULL_SECRET_NAME}" --docker-server="\${APP_IMAGE_PULL_SECRET_SERVER}" --docker-username="\${APP_IMAGE_PULL_SECRET_USERNAME}" --docker-password="\${APP_IMAGE_PULL_SECRET_PASSWORD}" "\${SECRET_EMAIL_ARG[@]}" --dry-run=client -o yaml | kubectl -n "\${NAMESPACE}" apply -f -
709
+
710
+ PULL_SECRET_PATCH="$(cat <<EOF
711
+ {"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"\${APP_IMAGE_PULL_SECRET_NAME}"}]}}}}
712
+ EOF
713
+ )"
714
+ kubectl -n "\${NAMESPACE}" patch deployment app-runtime --type=merge --patch "\${PULL_SECRET_PATCH}" >/dev/null
715
+ else
716
+ kubectl -n "\${NAMESPACE}" patch deployment app-runtime --type=json --patch='[{"op":"remove","path":"/spec/template/spec/imagePullSecrets"}]' >/dev/null 2>&1 || true
717
+ fi
718
+
719
+ kubectl -n "\${NAMESPACE}" set image deployment/app-runtime app="\${APP_IMAGE}" >/dev/null
720
+ kubectl -n "\${NAMESPACE}" scale deployment/app-runtime --replicas="\${APP_REPLICAS}" >/dev/null
721
+
722
+ if [[ "\${APP_FORCE_ROLLOUT_RESTART}" == "true" ]]; then
723
+ kubectl -n "\${NAMESPACE}" rollout restart deployment/app-runtime >/dev/null
724
+ fi
725
+
726
+ kubectl -n "\${NAMESPACE}" rollout status deployment/app-runtime --timeout=180s >/dev/null
727
+ echo "Minikube infrastructure applied."
728
+ `;
729
+ }
730
+ function getBuildAppImageScript(args) {
731
+ const { defaultNamespace, defaultAppImage } = args;
732
+ return `#!/usr/bin/env bash
733
+ set -euo pipefail
734
+
735
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
736
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
737
+ DOCKERFILE_PATH="\${ROOT_DIR}/app-image/Dockerfile"
738
+
739
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
740
+ set -a
741
+ # shellcheck disable=SC1090
742
+ source "\${ROOT_DIR}/.env"
743
+ set +a
744
+ fi
745
+
746
+ NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
747
+ APP_BUILD_ENABLED="\${APP_BUILD_ENABLED:-true}"
748
+ APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
749
+ APP_WEB_EXPORT_DIR="\${APP_WEB_EXPORT_DIR:-.ankh/web-export}"
750
+ APP_IMAGE="\${APP_IMAGE:-${defaultAppImage}}"
751
+
752
+ if [[ "\${APP_BUILD_ENABLED}" != "true" ]]; then
753
+ echo "Skipping app image build (APP_BUILD_ENABLED=\${APP_BUILD_ENABLED})."
754
+ exit 0
755
+ fi
756
+
757
+ if ! command -v docker >/dev/null 2>&1; then
758
+ echo "docker is required to build app image."
759
+ exit 1
760
+ fi
761
+
762
+ if ! command -v bunx >/dev/null 2>&1; then
763
+ echo "bunx is required to export the app for container image build."
764
+ exit 1
765
+ fi
766
+
767
+ if [[ ! -f "\${APP_SOURCE_DIR}/package.json" ]]; then
768
+ echo "APP_SOURCE_DIR does not look like an app root: \${APP_SOURCE_DIR}"
769
+ exit 1
770
+ fi
771
+
772
+ if [[ ! -f "\${DOCKERFILE_PATH}" ]]; then
773
+ echo "Dockerfile for app image build is missing: \${DOCKERFILE_PATH}"
774
+ exit 1
775
+ fi
776
+
777
+ (
778
+ cd "\${APP_SOURCE_DIR}"
779
+ bunx expo export --platform web --output-dir "\${APP_WEB_EXPORT_DIR}"
780
+ )
781
+
782
+ if [[ "\${APP_WEB_EXPORT_DIR}" = /* ]]; then
783
+ EXPORT_DIR="\${APP_WEB_EXPORT_DIR}"
784
+ else
785
+ EXPORT_DIR="\${APP_SOURCE_DIR}/\${APP_WEB_EXPORT_DIR}"
786
+ fi
787
+
788
+ if [[ ! -f "\${EXPORT_DIR}/index.html" ]]; then
789
+ echo "Expected web export output not found at \${EXPORT_DIR}/index.html"
790
+ exit 1
791
+ fi
792
+
793
+ docker build -t "\${APP_IMAGE}" \
794
+ --label "ankhorage.kind=generated-app" \
795
+ --label "ankhorage.namespace=\${NAMESPACE}" \
796
+ --label "ankhorage.image=\${APP_IMAGE}" \
797
+ -f "\${DOCKERFILE_PATH}" \
798
+ "\${EXPORT_DIR}"
799
+ echo "Built app image: \${APP_IMAGE} (namespace: \${NAMESPACE})"
800
+ `;
801
+ }
802
+ function getAppImageDockerfile() {
803
+ return `FROM nginx:1.27-alpine
804
+
805
+ RUN cat <<'EOF' > /etc/nginx/conf.d/default.conf
806
+ server {
807
+ listen 8080;
808
+ server_name _;
809
+ root /usr/share/nginx/html;
810
+ index index.html;
811
+
812
+ location / {
813
+ try_files $uri $uri/ /index.html;
814
+ }
815
+ }
816
+ EOF
817
+
818
+ COPY . /usr/share/nginx/html
819
+
820
+ EXPOSE 8080
821
+
822
+ CMD ["nginx", "-g", "daemon off;"]
823
+ `;
824
+ }
825
+ function getPortForwardScript(defaultNamespace) {
826
+ return `#!/usr/bin/env bash
827
+ set -euo pipefail
828
+
829
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
830
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
831
+
832
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
833
+ set -a
834
+ # shellcheck disable=SC1090
835
+ source "\${ROOT_DIR}/.env"
836
+ set +a
837
+ fi
838
+
839
+ NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
840
+ LOCAL_PORT="\${APP_PORT_FORWARD_LOCAL_PORT:-18080}"
841
+ REMOTE_PORT="\${APP_PORT_FORWARD_REMOTE_PORT:-80}"
842
+
843
+ if ! command -v kubectl >/dev/null 2>&1; then
844
+ echo "kubectl is required but not installed."
845
+ exit 1
846
+ fi
847
+
848
+ if ! kubectl get namespace "\${NAMESPACE}" >/dev/null 2>&1; then
849
+ echo "Namespace '\${NAMESPACE}' was not found."
850
+ echo "Run ./scripts/up.sh to create infrastructure first."
851
+ exit 1
852
+ fi
853
+
854
+ if ! kubectl -n "\${NAMESPACE}" get service app-runtime >/dev/null 2>&1; then
855
+ echo "Service 'app-runtime' was not found in namespace '\${NAMESPACE}'."
856
+ echo "Run ./scripts/up.sh to apply runtime resources."
857
+ exit 1
858
+ fi
859
+
860
+ echo "Forwarding app-runtime from namespace '\${NAMESPACE}' to http://127.0.0.1:\${LOCAL_PORT}"
861
+ echo "Press Ctrl+C to stop."
862
+ kubectl -n "\${NAMESPACE}" port-forward service/app-runtime "\${LOCAL_PORT}:\${REMOTE_PORT}"
863
+ `;
864
+ }
865
+ function getDownScript() {
866
+ return `#!/usr/bin/env bash
867
+ set -euo pipefail
868
+
869
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
870
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
871
+ K8S_DIR="\${ROOT_DIR}/k8s"
872
+
873
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
874
+ set -a
875
+ # shellcheck disable=SC1090
876
+ source "\${ROOT_DIR}/.env"
877
+ set +a
878
+ fi
879
+
880
+ kubectl delete -k "\${K8S_DIR}" --ignore-not-found
881
+ echo "Minikube infrastructure removed."
882
+ `;
883
+ }
884
+ function getStatusScript(defaultNamespace) {
885
+ return `#!/usr/bin/env bash
886
+ set -euo pipefail
887
+
888
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
889
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
890
+
891
+ if [[ -f "\${ROOT_DIR}/.env" ]]; then
892
+ set -a
893
+ # shellcheck disable=SC1090
894
+ source "\${ROOT_DIR}/.env"
895
+ set +a
896
+ fi
897
+
898
+ NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
899
+ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
900
+ SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
901
+
902
+ if ! command -v kubectl >/dev/null 2>&1; then
903
+ echo "kubectl is required but not installed."
904
+ exit 1
905
+ fi
906
+
907
+ if ! kubectl config current-context >/dev/null 2>&1; then
908
+ echo "No active kubectl context is configured."
909
+ echo "Run ./scripts/up.sh to start Minikube and apply manifests."
910
+ exit 1
911
+ fi
912
+
913
+ if ! kubectl cluster-info >/dev/null 2>&1; then
914
+ CONTEXT="$(kubectl config current-context 2>/dev/null || echo "unknown")"
915
+ echo "Cannot reach Kubernetes API server for context '\${CONTEXT}'."
916
+ echo "Run ./scripts/up.sh or verify your kubeconfig context."
917
+ exit 1
918
+ fi
919
+
920
+ if ! kubectl get namespace "\${NAMESPACE}" >/dev/null 2>&1; then
921
+ echo "Namespace '\${NAMESPACE}' was not found in the current cluster."
922
+ echo "Run ./scripts/up.sh to create it and apply resources."
923
+ exit 1
924
+ fi
925
+
926
+ kubectl get all -n "\${NAMESPACE}"
927
+
928
+ echo
929
+ echo "Runtime auth checks:"
930
+ echo "- AUTH_RUNTIME_MODE=\${AUTH_RUNTIME_MODE}"
931
+ echo "- SUPABASE_SECRET_SYNC_ENABLED=\${SUPABASE_SECRET_SYNC_ENABLED}"
932
+
933
+ if kubectl -n "\${NAMESPACE}" get configmap app-runtime-auth-env >/dev/null 2>&1; then
934
+ echo "- configmap/app-runtime-auth-env: present"
935
+ else
936
+ echo "- configmap/app-runtime-auth-env: missing"
937
+ fi
938
+
939
+ if [[ "\${SUPABASE_SECRET_SYNC_ENABLED}" == "true" ]]; then
940
+ if kubectl -n "\${NAMESPACE}" get secret supabase-auth-secrets >/dev/null 2>&1; then
941
+ echo "- secret/supabase-auth-secrets: present"
942
+
943
+ has_secret_key() {
944
+ local key="$1"
945
+ local value
946
+ value="$(kubectl -n "\${NAMESPACE}" get secret supabase-auth-secrets -o "jsonpath={.data.\${key}}" 2>/dev/null || true)"
947
+ [[ -n "\${value}" ]]
948
+ }
949
+
950
+ required_secret_keys=(
951
+ SUPABASE_URL
952
+ SUPABASE_ANON_KEY
953
+ SUPABASE_SERVICE_ROLE_KEY
954
+ SUPABASE_JWT_SECRET
955
+ EXPO_PUBLIC_SUPABASE_URL
956
+ EXPO_PUBLIC_SUPABASE_ANON_KEY
957
+ )
958
+
959
+ for key in "\${required_secret_keys[@]}"; do
960
+ if has_secret_key "\${key}"; then
961
+ echo " - \${key}: present"
962
+ else
963
+ echo " - \${key}: missing"
964
+ fi
965
+ done
966
+ else
967
+ echo "- secret/supabase-auth-secrets: missing"
968
+ fi
969
+ else
970
+ echo "- secret/supabase-auth-secrets: skipped (SUPABASE_SECRET_SYNC_ENABLED=false)"
971
+ fi
972
+ `;
973
+ }
974
+ function getSupabaseLocalEnvScript(supabaseLocalPorts) {
975
+ return `#!/usr/bin/env bash
976
+ set -euo pipefail
977
+
978
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
979
+ ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
980
+ ENV_FILE="\${ROOT_DIR}/.env"
981
+ ENV_EXAMPLE_FILE="\${ROOT_DIR}/.env.example"
982
+
983
+ if [[ -f "\${ENV_FILE}" ]]; then
984
+ set -a
985
+ # shellcheck disable=SC1090
986
+ source "\${ENV_FILE}"
987
+ set +a
988
+ fi
989
+
990
+ APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
991
+ SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${APP_SOURCE_DIR}}"
992
+ APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
993
+
994
+ SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
995
+ SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
996
+ SUPABASE_LOCAL_API_PORT="\${SUPABASE_LOCAL_API_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 1))}"
997
+ SUPABASE_LOCAL_DB_PORT="\${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}"
998
+ SUPABASE_LOCAL_STUDIO_PORT="\${SUPABASE_LOCAL_STUDIO_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 3))}"
999
+ SUPABASE_LOCAL_INBUCKET_PORT="\${SUPABASE_LOCAL_INBUCKET_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 4))}"
1000
+ SUPABASE_LOCAL_ANALYTICS_PORT="\${SUPABASE_LOCAL_ANALYTICS_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 5))}"
1001
+ export SUPABASE_LOCAL_PORT_BASE
1002
+ export SUPABASE_LOCAL_SHADOW_PORT
1003
+ export SUPABASE_LOCAL_API_PORT
1004
+ export SUPABASE_LOCAL_DB_PORT
1005
+ export SUPABASE_LOCAL_STUDIO_PORT
1006
+ export SUPABASE_LOCAL_INBUCKET_PORT
1007
+ export SUPABASE_LOCAL_ANALYTICS_PORT
1008
+ if ! command -v supabase >/dev/null 2>&1; then
1009
+ echo "supabase CLI is required but not installed."
1010
+ echo "Install: https://supabase.com/docs/guides/local-development/cli/getting-started"
1011
+ exit 1
1012
+ fi
1013
+
1014
+ configure_supabase_local_ports() {
1015
+ local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
1016
+
1017
+ if ! command -v python3 >/dev/null 2>&1; then
1018
+ echo "python3 is required to patch supabase/config.toml with generated local port defaults."
1019
+ echo "Install python3 (recommended) or manually edit \${config_file} ports:"
1020
+ echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
1021
+ echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
1022
+ echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
1023
+ echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
1024
+ echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
1025
+ echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
1026
+ exit 1
1027
+ fi
1028
+
1029
+ python3 - "\${config_file}" <<'PY'
1030
+ import os
1031
+ import re
1032
+ import sys
1033
+ from dataclasses import dataclass
1034
+
1035
+
1036
+ @dataclass(frozen=True)
1037
+ class Patch:
1038
+ section: str
1039
+ key: str
1040
+ value: str
1041
+
1042
+
1043
+ def _env_required(name):
1044
+ value = os.environ.get(name)
1045
+ if value is None or value == "":
1046
+ raise RuntimeError(f"Missing required env var: {name}")
1047
+ return value
1048
+
1049
+
1050
+ def _is_section_header(line):
1051
+ return re.match(r"^\\s*\\[[^\\]]+\\]\\s*$", line) is not None
1052
+
1053
+
1054
+ def _section_name(line):
1055
+ match = re.match(r"^\\s*\\[([^\\]]+)\\]\\s*$", line)
1056
+ return match.group(1) if match else None
1057
+
1058
+
1059
+ def _patch_section(lines, section, key, value):
1060
+ current_section = None
1061
+ section_start = None
1062
+
1063
+ for i, line in enumerate(lines):
1064
+ if _is_section_header(line):
1065
+ current_section = _section_name(line)
1066
+ if current_section == section and section_start is None:
1067
+ section_start = i
1068
+
1069
+ if section_start is None:
1070
+ if lines and not lines[-1].endswith("\\n"):
1071
+ lines[-1] = lines[-1] + "\\n"
1072
+ if lines and lines[-1].strip() != "":
1073
+ lines.append("\\n")
1074
+ lines.append(f"[{section}]\\n")
1075
+ lines.append(f"{key} = {value}\\n")
1076
+ return lines
1077
+
1078
+ section_end = len(lines)
1079
+ for j in range(section_start + 1, len(lines)):
1080
+ if _is_section_header(lines[j]):
1081
+ section_end = j
1082
+ break
1083
+
1084
+ key_re = re.compile(rf"^(\\s*){re.escape(key)}\\s*=\\s*(.*?)(\\s*(#.*)?)?$")
1085
+ for k in range(section_start + 1, section_end):
1086
+ match = key_re.match(lines[k])
1087
+ if match:
1088
+ indent = match.group(1) or ""
1089
+ trailing = match.group(3) or ""
1090
+ suffix = trailing.rstrip("\\n")
1091
+ lines[k] = f"{indent}{key} = {value}{suffix}\\n"
1092
+ return lines
1093
+
1094
+ insert_at = section_end
1095
+ while insert_at > section_start + 1 and lines[insert_at - 1].strip() == "":
1096
+ insert_at -= 1
1097
+ lines.insert(insert_at, f"{key} = {value}\\n")
1098
+ return lines
1099
+
1100
+
1101
+ def main():
1102
+ if len(sys.argv) != 2:
1103
+ raise RuntimeError("Usage: patch_config.py <supabase/config.toml>")
1104
+
1105
+ config_path = sys.argv[1]
1106
+ with open(config_path, "r", encoding="utf-8") as f:
1107
+ lines = f.readlines()
1108
+
1109
+ patches = [
1110
+ Patch("api", "port", _env_required("SUPABASE_LOCAL_API_PORT")),
1111
+ Patch("db", "port", _env_required("SUPABASE_LOCAL_DB_PORT")),
1112
+ Patch("db", "shadow_port", _env_required("SUPABASE_LOCAL_SHADOW_PORT")),
1113
+ Patch("studio", "port", _env_required("SUPABASE_LOCAL_STUDIO_PORT")),
1114
+ Patch("inbucket", "port", _env_required("SUPABASE_LOCAL_INBUCKET_PORT")),
1115
+ Patch("analytics", "port", _env_required("SUPABASE_LOCAL_ANALYTICS_PORT")),
1116
+ ]
1117
+
1118
+ for p in patches:
1119
+ lines = _patch_section(lines, p.section, p.key, p.value)
1120
+
1121
+ with open(config_path, "w", encoding="utf-8") as f:
1122
+ f.writelines(lines)
1123
+
1124
+ return 0
1125
+
1126
+
1127
+ if __name__ == "__main__":
1128
+ raise SystemExit(main())
1129
+ PY
1130
+ }
1131
+
1132
+ assert_supabase_local_ports_available() {
1133
+ local project_label="\${ANKH_NAMESPACE:-$(basename "\${APP_SOURCE_DIR}")}"
1134
+
1135
+ ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
1136
+ import os
1137
+ import socket
1138
+ import sys
1139
+
1140
+
1141
+ PORT_KEYS = [
1142
+ "SUPABASE_LOCAL_SHADOW_PORT",
1143
+ "SUPABASE_LOCAL_API_PORT",
1144
+ "SUPABASE_LOCAL_DB_PORT",
1145
+ "SUPABASE_LOCAL_STUDIO_PORT",
1146
+ "SUPABASE_LOCAL_INBUCKET_PORT",
1147
+ "SUPABASE_LOCAL_ANALYTICS_PORT",
1148
+ ]
1149
+
1150
+
1151
+ def read_port(key):
1152
+ value = os.environ.get(key, "")
1153
+ try:
1154
+ port = int(value)
1155
+ except ValueError:
1156
+ raise RuntimeError(f"{key} must be an integer, got {value!r}") from None
1157
+
1158
+ if port < 1 or port > 65535:
1159
+ raise RuntimeError(f"{key} must be between 1 and 65535, got {port}")
1160
+
1161
+ return port
1162
+
1163
+
1164
+ def can_bind(port):
1165
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
1166
+ try:
1167
+ sock.bind(("0.0.0.0", port))
1168
+ except OSError:
1169
+ return False
1170
+ return True
1171
+
1172
+
1173
+ def main():
1174
+ project_label = os.environ.get("ANKH_PROJECT_LABEL", "unknown")
1175
+ ports = [(key, read_port(key)) for key in PORT_KEYS]
1176
+
1177
+ seen = {}
1178
+ duplicates = []
1179
+ for key, port in ports:
1180
+ existing = seen.get(port)
1181
+ if existing is not None:
1182
+ duplicates.append((key, existing, port))
1183
+ else:
1184
+ seen[port] = key
1185
+
1186
+ blocked = [(key, port) for key, port in ports if not can_bind(port)]
1187
+
1188
+ if not duplicates and not blocked:
1189
+ return 0
1190
+
1191
+ print(f"Supabase local port preflight failed for project '{project_label}'.", file=sys.stderr)
1192
+
1193
+ for key, existing, port in duplicates:
1194
+ print(f"- {key} duplicates {existing} on port {port}.", file=sys.stderr)
1195
+
1196
+ for key, port in blocked:
1197
+ print(f"- {key}={port} is already in use on this host.", file=sys.stderr)
1198
+
1199
+ print("Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values in infra/minikube/.env and retry.", file=sys.stderr)
1200
+ return 1
1201
+
1202
+
1203
+ if __name__ == "__main__":
1204
+ raise SystemExit(main())
1205
+ PY
1206
+ }
1207
+
1208
+ if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
1209
+ echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
1210
+ (
1211
+ cd "\${SUPABASE_PROJECT_DIR}"
1212
+ supabase init --yes >/dev/null
1213
+ )
1214
+ fi
1215
+
1216
+ configure_supabase_local_ports
1217
+
1218
+ STATUS_ENV="$(
1219
+ cd "\${SUPABASE_PROJECT_DIR}"
1220
+ supabase status -o env 2>/dev/null || true
1221
+ )"
1222
+
1223
+ start_supabase_local_stack() {
1224
+ if supabase start >/dev/null; then
1225
+ return 0
1226
+ fi
1227
+
1228
+ echo "Supabase local start failed. Stopping stale local stack and retrying once..."
1229
+ supabase stop --no-backup >/dev/null 2>&1 || true
1230
+
1231
+ if supabase start >/dev/null; then
1232
+ return 0
1233
+ fi
1234
+
1235
+ echo "Supabase local start failed after retry."
1236
+ return 1
1237
+ }
1238
+
1239
+ if [[ -z "\${STATUS_ENV}" ]]; then
1240
+ assert_supabase_local_ports_available
1241
+ echo "Supabase local stack not detected. Starting with 'supabase start'..."
1242
+ (
1243
+ cd "\${SUPABASE_PROJECT_DIR}"
1244
+ start_supabase_local_stack
1245
+ )
1246
+
1247
+ STATUS_ENV="$(
1248
+ cd "\${SUPABASE_PROJECT_DIR}"
1249
+ supabase status -o env
1250
+ )"
1251
+ fi
1252
+
1253
+ read_env_value() {
1254
+ local key="$1"
1255
+ local line
1256
+ line="$(printf '%s\\n' "\${STATUS_ENV}" | awk -v key="\${key}" '
1257
+ $0 ~ "^(export[[:space:]]+)?" key "=" {print}
1258
+ ' | tail -n1)"
1259
+
1260
+ if [[ -z "\${line}" ]]; then
1261
+ echo ""
1262
+ return
1263
+ fi
1264
+
1265
+ line="\${line#export }"
1266
+ line="\${line#\${key}=}"
1267
+ line="\${line%\\"}"
1268
+ line="\${line#\\"}"
1269
+ printf '%s' "\${line}"
1270
+ }
1271
+
1272
+ API_URL="$(read_env_value API_URL)"
1273
+ ANON_KEY="$(read_env_value ANON_KEY)"
1274
+ SERVICE_ROLE_KEY="$(read_env_value SERVICE_ROLE_KEY)"
1275
+ JWT_SECRET="$(read_env_value JWT_SECRET)"
1276
+
1277
+ if [[ -z "\${API_URL}" || -z "\${ANON_KEY}" || -z "\${SERVICE_ROLE_KEY}" || -z "\${JWT_SECRET}" ]]; then
1278
+ echo "Unable to read required Supabase values from 'supabase status -o env'."
1279
+ echo "Expected API_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET."
1280
+ exit 1
1281
+ fi
1282
+
1283
+ upsert_env() {
1284
+ local file="$1"
1285
+ local key="$2"
1286
+ local value="$3"
1287
+ local tmp_file
1288
+ tmp_file="$(mktemp)"
1289
+
1290
+ awk -v key="\${key}" -v value="\${value}" '
1291
+ BEGIN { replaced = 0 }
1292
+ index($0, key "=") == 1 {
1293
+ print key "=" value
1294
+ replaced = 1
1295
+ next
1296
+ }
1297
+ { print }
1298
+ END {
1299
+ if (replaced == 0) {
1300
+ print key "=" value
1301
+ }
1302
+ }
1303
+ ' "\${file}" > "\${tmp_file}"
1304
+
1305
+ mv "\${tmp_file}" "\${file}"
1306
+ }
1307
+
1308
+ if [[ ! -f "\${ENV_FILE}" ]]; then
1309
+ cp "\${ENV_EXAMPLE_FILE}" "\${ENV_FILE}"
1310
+ fi
1311
+
1312
+ if [[ ! -f "\${APP_SUPABASE_ENV_FILE}" ]]; then
1313
+ touch "\${APP_SUPABASE_ENV_FILE}"
1314
+ fi
1315
+
1316
+ upsert_env "\${ENV_FILE}" "SUPABASE_SECRET_SYNC_ENABLED" "true"
1317
+ upsert_env "\${ENV_FILE}" "SUPABASE_URL" "\${API_URL}"
1318
+ upsert_env "\${ENV_FILE}" "SUPABASE_ANON_KEY" "\${ANON_KEY}"
1319
+ upsert_env "\${ENV_FILE}" "SUPABASE_SERVICE_ROLE_KEY" "\${SERVICE_ROLE_KEY}"
1320
+ upsert_env "\${ENV_FILE}" "SUPABASE_JWT_SECRET" "\${JWT_SECRET}"
1321
+ upsert_env "\${ENV_FILE}" "EXPO_PUBLIC_SUPABASE_URL" "\${API_URL}"
1322
+ upsert_env "\${ENV_FILE}" "EXPO_PUBLIC_SUPABASE_ANON_KEY" "\${ANON_KEY}"
1323
+ upsert_env "\${ENV_FILE}" "SUPABASE_PROJECT_DIR" "\${SUPABASE_PROJECT_DIR}"
1324
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_PORT_BASE" "\${SUPABASE_LOCAL_PORT_BASE}"
1325
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_SHADOW_PORT" "\${SUPABASE_LOCAL_SHADOW_PORT}"
1326
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_API_PORT" "\${SUPABASE_LOCAL_API_PORT}"
1327
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_DB_PORT" "\${SUPABASE_LOCAL_DB_PORT}"
1328
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_STUDIO_PORT" "\${SUPABASE_LOCAL_STUDIO_PORT}"
1329
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_INBUCKET_PORT" "\${SUPABASE_LOCAL_INBUCKET_PORT}"
1330
+ upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_ANALYTICS_PORT" "\${SUPABASE_LOCAL_ANALYTICS_PORT}"
1331
+
1332
+ upsert_env "\${APP_SUPABASE_ENV_FILE}" "EXPO_PUBLIC_SUPABASE_URL" "\${API_URL}"
1333
+ upsert_env "\${APP_SUPABASE_ENV_FILE}" "EXPO_PUBLIC_SUPABASE_ANON_KEY" "\${ANON_KEY}"
1334
+ upsert_env "\${APP_SUPABASE_ENV_FILE}" "SUPABASE_URL" "\${API_URL}"
1335
+ upsert_env "\${APP_SUPABASE_ENV_FILE}" "SUPABASE_ANON_KEY" "\${ANON_KEY}"
1336
+
1337
+ echo "Updated \${ENV_FILE} with local Supabase credentials from \${SUPABASE_PROJECT_DIR}."
1338
+ echo "Updated \${APP_SUPABASE_ENV_FILE} with Expo Supabase credentials for local app runs."
1339
+ echo "If Expo is already running, restart it to pick up updated EXPO_PUBLIC_* variables."
1340
+ echo "Next: run ./scripts/up.sh to sync Kubernetes secrets and apply infrastructure."
1341
+ `;
1342
+ }
1343
+ function getDefaultAppImage(namespace) {
1344
+ return `ankh/${namespace}:dev`;
1345
+ }
1346
+ function resolveSupabaseLocalPorts(namespace) {
1347
+ const base = SUPABASE_LOCAL_PORT_BASE +
1348
+ resolveSupabaseLocalPortBucket(namespace) * SUPABASE_LOCAL_PORT_BUCKET_SIZE;
1349
+ return {
1350
+ base,
1351
+ shadow: base,
1352
+ api: base + 1,
1353
+ db: base + 2,
1354
+ studio: base + 3,
1355
+ inbucket: base + 4,
1356
+ analytics: base + 5,
1357
+ };
1358
+ }
1359
+ function resolveSupabaseLocalPortBucket(namespace) {
1360
+ const rawBucket = hashProjectId(namespace) - hashProjectId(SUPABASE_LOCAL_PORT_REFERENCE_PROJECT);
1361
+ return (rawBucket + SUPABASE_LOCAL_PORT_BUCKET_COUNT) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;
1362
+ }
1363
+ function hashProjectId(value) {
1364
+ const source = value.trim().length > 0 ? value.trim() : 'app';
1365
+ let hash = 0;
1366
+ for (const char of source) {
1367
+ hash = (hash * 31 + char.charCodeAt(0)) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;
1368
+ }
1369
+ return hash;
1370
+ }
1371
+ //# sourceMappingURL=index.js.map