@ankhorage/infra 4.0.0 → 4.1.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.
@@ -0,0 +1,624 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { promises as fs } from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { generateInfrastructure } from '../../../index.js';
7
+ import { ensureProjectInfrastructureRuntime } from '../../../project/index.js';
8
+ import { createAppManifest } from '../../../testSupport.js';
9
+ import type { InfraManifestInput } from '../../../types.js';
10
+
11
+ const temporaryPaths = new Set<string>();
12
+
13
+ afterEach(async () => {
14
+ for (const rootPath of temporaryPaths) {
15
+ await stopFixtureProcesses(rootPath);
16
+ await fs.rm(rootPath, { force: true, recursive: true });
17
+ }
18
+ temporaryPaths.clear();
19
+ });
20
+
21
+ describe('generated Minikube runtime port-forward lifecycle', () => {
22
+ test(
23
+ 'keeps healthy app forwarding, repairs a stopped Supabase gateway, and isolates operational forwards',
24
+ async () => {
25
+ const fixture = await createPortForwardFixture(createSupabaseManifest(), 'runtime-recovery');
26
+
27
+ const firstStart = await runPortForward(fixture, 'start', 'runtime');
28
+ expect(firstStart.stdout).toContain('app: started');
29
+ expect(firstStart.stdout).toContain('supabase-gateway: started');
30
+
31
+ const appPid = await readForwardPid(fixture, 'app');
32
+ const firstGatewayPid = await readForwardPid(fixture, 'supabase-gateway');
33
+ await expectForwardPid(fixture, 'studio', false);
34
+ await expectForwardPid(fixture, 'db-migration', false);
35
+
36
+ const repeatedStart = await runPortForward(fixture, 'start', 'runtime');
37
+ expect(repeatedStart.stdout).toContain(`app: running (pid ${appPid})`);
38
+ expect(repeatedStart.stdout).toContain(`supabase-gateway: running (pid ${firstGatewayPid})`);
39
+ expect(await readForwardPid(fixture, 'app')).toBe(appPid);
40
+ expect(await readForwardPid(fixture, 'supabase-gateway')).toBe(firstGatewayPid);
41
+
42
+ await runPortForward(fixture, 'stop', 'supabase-gateway');
43
+ expect(await readForwardPid(fixture, 'app')).toBe(appPid);
44
+ await expectForwardPid(fixture, 'supabase-gateway', false);
45
+
46
+ const recovery = await ensureProjectInfrastructureRuntime({
47
+ projectId: fixture.profile,
48
+ projectPath: fixture.rootPath,
49
+ target: 'minikube',
50
+ });
51
+ expect(recovery.stdout).toContain(`app: running (pid ${appPid})`);
52
+ expect(recovery.stdout).toContain('supabase-gateway: started');
53
+ expect(await readForwardPid(fixture, 'app')).toBe(appPid);
54
+ expect(await readForwardPid(fixture, 'supabase-gateway')).not.toBe(firstGatewayPid);
55
+ await expectForwardPid(fixture, 'studio', false);
56
+ await expectForwardPid(fixture, 'db-migration', false);
57
+
58
+ await runPortForward(fixture, 'stop', 'runtime');
59
+ },
60
+ { timeout: 30_000 },
61
+ );
62
+
63
+ test(
64
+ 'starts only the app forward when no provider owns another runtime endpoint',
65
+ async () => {
66
+ const fixture = await createPortForwardFixture(
67
+ { deployment: { target: 'minikube', monitoring: false }, modules: [] },
68
+ 'runtime-app-only',
69
+ );
70
+
71
+ const result = await runPortForward(fixture, 'start', 'runtime');
72
+
73
+ expect(result.stdout).toContain('app: started');
74
+ expect(result.stdout).not.toContain('supabase-gateway');
75
+ await expectForwardPid(fixture, 'app', true);
76
+ await expectForwardPid(fixture, 'supabase-gateway', false);
77
+ await expectForwardPid(fixture, 'studio', false);
78
+ await expectForwardPid(fixture, 'db-migration', false);
79
+
80
+ await runPortForward(fixture, 'stop', 'runtime');
81
+ },
82
+ { timeout: 15_000 },
83
+ );
84
+
85
+ test(
86
+ 'restores only the Supabase gateway for a native-only app without an app service',
87
+ async () => {
88
+ const appManifest = {
89
+ ...createAppManifest('native-runtime'),
90
+ deploy: {
91
+ targets: {
92
+ android: { enabled: true, package: 'com.ankh.nativeruntime' },
93
+ },
94
+ },
95
+ };
96
+ const fixture = await createPortForwardFixture(
97
+ createSupabaseManifest(),
98
+ 'native-runtime',
99
+ appManifest,
100
+ 'service/app-runtime',
101
+ );
102
+
103
+ const firstEnsure = await ensureProjectInfrastructureRuntime({
104
+ projectId: fixture.profile,
105
+ projectPath: fixture.rootPath,
106
+ target: 'minikube',
107
+ });
108
+ expect(firstEnsure.stdout).toContain('supabase-gateway: started');
109
+ expect(firstEnsure.stdout).not.toContain('app:');
110
+ const gatewayPid = await readForwardPid(fixture, 'supabase-gateway');
111
+
112
+ const repeatedEnsure = await ensureProjectInfrastructureRuntime({
113
+ projectId: fixture.profile,
114
+ projectPath: fixture.rootPath,
115
+ target: 'minikube',
116
+ });
117
+ expect(repeatedEnsure.stdout).toContain(`supabase-gateway: running (pid ${gatewayPid})`);
118
+ expect(await readForwardPid(fixture, 'supabase-gateway')).toBe(gatewayPid);
119
+ await expectForwardPid(fixture, 'app', false);
120
+ },
121
+ { timeout: 15_000 },
122
+ );
123
+
124
+ test(
125
+ 'retries a transient forward that becomes reachable and then loses its stale pod',
126
+ async () => {
127
+ const appManifest = {
128
+ ...createAppManifest('native-runtime-retry'),
129
+ deploy: {
130
+ targets: {
131
+ android: { enabled: true, package: 'com.ankh.nativeruntimeretry' },
132
+ },
133
+ },
134
+ };
135
+ const fixture = await createPortForwardFixture(
136
+ createSupabaseManifest(),
137
+ 'native-runtime-retry',
138
+ appManifest,
139
+ undefined,
140
+ 1,
141
+ 750,
142
+ );
143
+
144
+ const recovery = await ensureProjectInfrastructureRuntime({
145
+ projectId: fixture.profile,
146
+ projectPath: fixture.rootPath,
147
+ target: 'minikube',
148
+ });
149
+
150
+ expect(recovery.stdout).toContain(
151
+ 'supabase-gateway: port-forward attempt 1 exited before readiness; retrying',
152
+ );
153
+ expect(recovery.stdout).toContain('supabase-gateway: started');
154
+ expect(Number.parseInt(await fs.readFile(fixture.forwardAttemptPath, 'utf8'), 10)).toBe(2);
155
+ await expectForwardPid(fixture, 'supabase-gateway', true);
156
+ await expectForwardPid(fixture, 'app', false);
157
+ },
158
+ { timeout: 15_000 },
159
+ );
160
+
161
+ test(
162
+ 'cleans an owned app forward removed from the native-only active topology',
163
+ async () => {
164
+ const fixture = await createPortForwardFixture(
165
+ createSupabaseManifest(),
166
+ 'native-runtime-upgrade',
167
+ createNativeAppManifest('native-runtime-upgrade'),
168
+ );
169
+ const appPort = Number.parseInt(fixture.env.APP_PORT_FORWARD_LOCAL_PORT ?? '', 10);
170
+ const oldAppForward = Bun.spawn(
171
+ [
172
+ 'kubectl',
173
+ '--context',
174
+ fixture.profile,
175
+ '-n',
176
+ 'app',
177
+ 'port-forward',
178
+ 'service/app-runtime',
179
+ `${appPort}:80`,
180
+ ],
181
+ { env: fixture.env, stderr: 'ignore', stdout: 'ignore' },
182
+ );
183
+ await waitForHttpPort(appPort);
184
+ await writeForwardPid(fixture, 'app', oldAppForward.pid);
185
+
186
+ const cleanup = await runPortForward(fixture, 'stop', 'all');
187
+
188
+ expect(cleanup.stdout).toContain('app: stopped');
189
+ await oldAppForward.exited;
190
+ expect(isProcessRunning(oldAppForward.pid)).toBe(false);
191
+ await expectForwardPid(fixture, 'app', false);
192
+
193
+ const recovery = await runPortForward(fixture, 'start', 'runtime');
194
+ expect(recovery.stdout).toContain('supabase-gateway: started');
195
+ expect(recovery.stdout).not.toContain('app: started');
196
+ await expectForwardPid(fixture, 'app', false);
197
+ await expectForwardPid(fixture, 'supabase-gateway', true);
198
+ },
199
+ { timeout: 15_000 },
200
+ );
201
+
202
+ test(
203
+ 'refuses to terminate a non-owned PID recorded as an obsolete app forward',
204
+ async () => {
205
+ const fixture = await createPortForwardFixture(
206
+ createSupabaseManifest(),
207
+ 'native-runtime-non-owned',
208
+ createNativeAppManifest('native-runtime-non-owned'),
209
+ );
210
+ const appPort = Number.parseInt(fixture.env.APP_PORT_FORWARD_LOCAL_PORT ?? '', 10);
211
+ const nonOwnedProcess = Bun.spawn(['bun', fixture.serverScript, String(appPort)], {
212
+ env: fixture.env,
213
+ stderr: 'ignore',
214
+ stdout: 'ignore',
215
+ });
216
+
217
+ try {
218
+ await waitForHttpPort(appPort);
219
+ await writeForwardPid(fixture, 'app', nonOwnedProcess.pid);
220
+
221
+ const cleanup = await executePortForward(fixture, 'stop', 'all');
222
+
223
+ expect(cleanup.exitCode).not.toBe(0);
224
+ expect(cleanup.stdout).toContain(
225
+ `app: stale pid file pointed at non-owned live pid ${nonOwnedProcess.pid}`,
226
+ );
227
+ expect(cleanup.stdout).toContain(
228
+ `app: port occupied by non-owned process on local port ${appPort}`,
229
+ );
230
+ expect(isProcessRunning(nonOwnedProcess.pid)).toBe(true);
231
+ await expectForwardPid(fixture, 'app', false);
232
+ } finally {
233
+ nonOwnedProcess.kill();
234
+ await nonOwnedProcess.exited;
235
+ }
236
+ },
237
+ { timeout: 15_000 },
238
+ );
239
+
240
+ test('fails actionably when a generated Web app target is genuinely missing', async () => {
241
+ const fixture = await createPortForwardFixture(
242
+ createSupabaseManifest(),
243
+ 'missing-web-runtime',
244
+ createAppManifest('missing-web-runtime'),
245
+ 'service/app-runtime',
246
+ );
247
+
248
+ await expectFailureMessage(
249
+ ensureProjectInfrastructureRuntime({
250
+ projectId: fixture.profile,
251
+ projectPath: fixture.rootPath,
252
+ target: 'minikube',
253
+ }),
254
+ 'app: target app/service/app-runtime not found',
255
+ );
256
+ await expectForwardPid(fixture, 'supabase-gateway', false);
257
+ });
258
+
259
+ test('fails actionably when a generated provider target is genuinely missing', async () => {
260
+ const appManifest = {
261
+ ...createAppManifest('missing-provider-runtime'),
262
+ deploy: {
263
+ targets: {
264
+ android: { enabled: true, package: 'com.ankh.missingprovider' },
265
+ },
266
+ },
267
+ };
268
+ const fixture = await createPortForwardFixture(
269
+ createSupabaseManifest(),
270
+ 'missing-provider-runtime',
271
+ appManifest,
272
+ 'service/gateway',
273
+ );
274
+
275
+ await expectFailureMessage(
276
+ ensureProjectInfrastructureRuntime({
277
+ projectId: fixture.profile,
278
+ projectPath: fixture.rootPath,
279
+ target: 'minikube',
280
+ }),
281
+ 'supabase-gateway: target supabase/service/gateway not found',
282
+ );
283
+ });
284
+ });
285
+
286
+ interface PortForwardFixture {
287
+ readonly env: Record<string, string>;
288
+ readonly forwardAttemptPath: string;
289
+ readonly processDirectory: string;
290
+ readonly profile: string;
291
+ readonly rootPath: string;
292
+ readonly serverScript: string;
293
+ readonly scriptPath: string;
294
+ }
295
+
296
+ async function createPortForwardFixture(
297
+ manifest: InfraManifestInput,
298
+ profile: string,
299
+ appManifest = createAppManifest(profile),
300
+ missingResource?: string,
301
+ transientPortForwardFailures = 0,
302
+ transientPortForwardLifetimeMs = 0,
303
+ ): Promise<PortForwardFixture> {
304
+ const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'infra-port-forward-runtime-'));
305
+ temporaryPaths.add(rootPath);
306
+ const generated = generateInfrastructure(manifest, {
307
+ appManifest,
308
+ });
309
+ const generatedScript = generated.files.find(
310
+ (file) => file.path === 'infra/minikube/scripts/port-forward.sh',
311
+ );
312
+ if (generatedScript === undefined) {
313
+ throw new Error('Generated port-forward script was missing.');
314
+ }
315
+
316
+ const scriptPath = path.join(rootPath, generatedScript.path);
317
+ const fakeBin = path.join(rootPath, 'fake-bin');
318
+ const forwardAttemptPath = path.join(rootPath, 'forward-attempts');
319
+ const processDirectory = path.join(rootPath, 'fake-processes');
320
+ const serverScript = path.join(rootPath, 'forward-server.ts');
321
+ await fs.mkdir(path.dirname(scriptPath), { recursive: true });
322
+ await fs.mkdir(fakeBin, { recursive: true });
323
+ await fs.mkdir(processDirectory, { recursive: true });
324
+ await fs.writeFile(
325
+ serverScript,
326
+ [
327
+ "const port = Number.parseInt(Bun.argv[2] ?? '', 10);",
328
+ "const lifetimeMs = Number.parseInt(Bun.argv[3] ?? '0', 10);",
329
+ "const server = Bun.serve({ hostname: '127.0.0.1', port, fetch: () => new Response('ok') });",
330
+ 'if (lifetimeMs > 0) {',
331
+ ' await Bun.sleep(lifetimeMs);',
332
+ ' server.stop(true);',
333
+ ' process.exit(1);',
334
+ '}',
335
+ 'await new Promise(() => undefined);',
336
+ '',
337
+ ].join('\n'),
338
+ 'utf8',
339
+ );
340
+ await writeExecutable(
341
+ path.join(fakeBin, 'kubectl'),
342
+ `#!/usr/bin/env bash
343
+ set -euo pipefail
344
+ if [[ " $* " == *" get "* ]]; then
345
+ if [[ -n "\${FAKE_MISSING_RESOURCE:-}" && " $* " == *" \${FAKE_MISSING_RESOURCE} "* ]]; then
346
+ exit 1
347
+ fi
348
+ exit 0
349
+ fi
350
+ if [[ " $* " != *" port-forward "* ]]; then
351
+ exit 1
352
+ fi
353
+ endpoint="\${!#}"
354
+ local_port="\${endpoint%%:*}"
355
+ forward_attempt=0
356
+ if [[ -f "\${FAKE_FORWARD_ATTEMPT_FILE}" ]]; then
357
+ forward_attempt="$(cat "\${FAKE_FORWARD_ATTEMPT_FILE}")"
358
+ fi
359
+ forward_attempt="$((forward_attempt + 1))"
360
+ printf '%s\n' "\${forward_attempt}" > "\${FAKE_FORWARD_ATTEMPT_FILE}"
361
+ if [[ "\${forward_attempt}" -le "\${FAKE_TRANSIENT_FORWARD_FAILURES}" ]]; then
362
+ if [[ "\${FAKE_TRANSIENT_FORWARD_LIFETIME_MS}" -gt 0 ]]; then
363
+ bun "\${FAKE_FORWARD_SERVER_SCRIPT}" "\${local_port}" "\${FAKE_TRANSIENT_FORWARD_LIFETIME_MS}" || true
364
+ fi
365
+ echo 'error: error upgrading connection: unable to upgrade connection: pod does not exist' >&2
366
+ exit 1
367
+ fi
368
+ printf 'kubectl %s\n' "$*" > "\${FAKE_PROCESS_DIRECTORY}/$$.command"
369
+ exec bun "\${FAKE_FORWARD_SERVER_SCRIPT}" "\${local_port}"
370
+ `,
371
+ );
372
+ await writeExecutable(
373
+ path.join(fakeBin, 'ps'),
374
+ `#!/usr/bin/env bash
375
+ set -euo pipefail
376
+ pid=""
377
+ format=""
378
+ while [[ $# -gt 0 ]]; do
379
+ case "$1" in
380
+ -p) pid="$2"; shift 2 ;;
381
+ -o) format="$2"; shift 2 ;;
382
+ *) shift ;;
383
+ esac
384
+ done
385
+ command_file="\${FAKE_PROCESS_DIRECTORY}/\${pid}.command"
386
+ if [[ -f "\${command_file}" && "\${format}" == "comm=" ]]; then
387
+ echo kubectl
388
+ elif [[ -f "\${command_file}" && "\${format}" == "command=" ]]; then
389
+ cat "\${command_file}"
390
+ else
391
+ /bin/ps -p "\${pid}" -o "\${format}"
392
+ fi
393
+ `,
394
+ );
395
+
396
+ const ports = reserveAvailablePorts(4);
397
+ const env = {
398
+ ...process.env,
399
+ ANKH_APP_SLUG: profile,
400
+ APP_PORT_FORWARD_LOCAL_PORT: String(ports[0]),
401
+ SUPABASE_GATEWAY_FORWARD_LOCAL_PORT: String(ports[1]),
402
+ SUPABASE_STUDIO_FORWARD_LOCAL_PORT: String(ports[2]),
403
+ SUPABASE_DB_FORWARD_LOCAL_PORT: String(ports[3]),
404
+ FAKE_FORWARD_ATTEMPT_FILE: forwardAttemptPath,
405
+ FAKE_FORWARD_SERVER_SCRIPT: serverScript,
406
+ FAKE_PROCESS_DIRECTORY: processDirectory,
407
+ FAKE_TRANSIENT_FORWARD_FAILURES: String(transientPortForwardFailures),
408
+ FAKE_TRANSIENT_FORWARD_LIFETIME_MS: String(transientPortForwardLifetimeMs),
409
+ ...(missingResource ? { FAKE_MISSING_RESOURCE: missingResource } : {}),
410
+ PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
411
+ };
412
+ const injectedEnvironment = Object.entries(env)
413
+ .filter(([key]) =>
414
+ [
415
+ 'ANKH_APP_SLUG',
416
+ 'APP_PORT_FORWARD_LOCAL_PORT',
417
+ 'SUPABASE_GATEWAY_FORWARD_LOCAL_PORT',
418
+ 'SUPABASE_STUDIO_FORWARD_LOCAL_PORT',
419
+ 'SUPABASE_DB_FORWARD_LOCAL_PORT',
420
+ 'FAKE_FORWARD_ATTEMPT_FILE',
421
+ 'FAKE_FORWARD_SERVER_SCRIPT',
422
+ 'FAKE_PROCESS_DIRECTORY',
423
+ 'FAKE_TRANSIENT_FORWARD_FAILURES',
424
+ 'FAKE_TRANSIENT_FORWARD_LIFETIME_MS',
425
+ 'FAKE_MISSING_RESOURCE',
426
+ 'PATH',
427
+ ].includes(key),
428
+ )
429
+ .map(([key, value]) => `export ${key}=${shellQuote(value)}`)
430
+ .join('\n');
431
+ await fs.writeFile(
432
+ scriptPath,
433
+ generatedScript.content.replace(
434
+ '#!/usr/bin/env bash\n',
435
+ `#!/usr/bin/env bash\n${injectedEnvironment}\n`,
436
+ ),
437
+ 'utf8',
438
+ );
439
+ await fs.chmod(scriptPath, 0o755);
440
+ return {
441
+ env,
442
+ forwardAttemptPath,
443
+ processDirectory,
444
+ profile,
445
+ rootPath,
446
+ serverScript,
447
+ scriptPath,
448
+ };
449
+ }
450
+
451
+ function shellQuote(value: string): string {
452
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
453
+ }
454
+
455
+ async function expectFailureMessage(promise: Promise<unknown>, expected: string): Promise<void> {
456
+ try {
457
+ await promise;
458
+ throw new Error('Expected runtime ensure to fail.');
459
+ } catch (error) {
460
+ expect(error instanceof Error ? error.message : String(error)).toContain(expected);
461
+ }
462
+ }
463
+
464
+ async function runPortForward(
465
+ fixture: PortForwardFixture,
466
+ action: string,
467
+ name: string,
468
+ ): Promise<{ readonly exitCode: number; readonly stderr: string; readonly stdout: string }> {
469
+ const result = await executePortForward(fixture, action, name);
470
+ if (result.exitCode !== 0) {
471
+ throw new Error(
472
+ `port-forward ${action} ${name} failed (${result.exitCode}): ${result.stdout}${result.stderr}`,
473
+ );
474
+ }
475
+ return result;
476
+ }
477
+
478
+ async function executePortForward(
479
+ fixture: PortForwardFixture,
480
+ action: string,
481
+ name: string,
482
+ ): Promise<{ readonly exitCode: number; readonly stderr: string; readonly stdout: string }> {
483
+ const process = Bun.spawn(['bash', fixture.scriptPath, action, name], {
484
+ env: fixture.env,
485
+ stderr: 'pipe',
486
+ stdout: 'pipe',
487
+ });
488
+ const [exitCode, stdout, stderr] = await Promise.all([
489
+ process.exited,
490
+ new Response(process.stdout).text(),
491
+ new Response(process.stderr).text(),
492
+ ]);
493
+ return { exitCode, stderr, stdout };
494
+ }
495
+
496
+ async function writeForwardPid(
497
+ fixture: PortForwardFixture,
498
+ name: string,
499
+ pid: number,
500
+ ): Promise<void> {
501
+ const pidPath = forwardPidPath(fixture, name);
502
+ await fs.mkdir(path.dirname(pidPath), { recursive: true });
503
+ await fs.writeFile(pidPath, `${pid}\n`, 'utf8');
504
+ }
505
+
506
+ async function waitForHttpPort(port: number): Promise<void> {
507
+ for (let attempt = 0; attempt < 40; attempt += 1) {
508
+ try {
509
+ const response = await fetch(`http://127.0.0.1:${port}`, {
510
+ signal: AbortSignal.timeout(250),
511
+ });
512
+ if (response.ok) return;
513
+ } catch {
514
+ // The fixture process may still be binding its port.
515
+ }
516
+ await Bun.sleep(50);
517
+ }
518
+ throw new Error(`Timed out waiting for fixture port ${port}.`);
519
+ }
520
+
521
+ function isProcessRunning(pid: number): boolean {
522
+ try {
523
+ process.kill(pid, 0);
524
+ return true;
525
+ } catch {
526
+ return false;
527
+ }
528
+ }
529
+
530
+ async function readForwardPid(fixture: PortForwardFixture, name: string): Promise<number> {
531
+ const value = await fs.readFile(forwardPidPath(fixture, name), 'utf8');
532
+ return Number.parseInt(value.trim(), 10);
533
+ }
534
+
535
+ async function expectForwardPid(
536
+ fixture: PortForwardFixture,
537
+ name: string,
538
+ exists: boolean,
539
+ ): Promise<void> {
540
+ expect(await pathExists(forwardPidPath(fixture, name))).toBe(exists);
541
+ }
542
+
543
+ function forwardPidPath(fixture: PortForwardFixture, name: string): string {
544
+ return path.join(
545
+ fixture.rootPath,
546
+ 'infra/minikube/.state/forwards',
547
+ `${fixture.profile}-${name}.pid`,
548
+ );
549
+ }
550
+
551
+ function reserveAvailablePorts(count: number): number[] {
552
+ const listeners = Array.from({ length: count }, () =>
553
+ Bun.listen({
554
+ hostname: '127.0.0.1',
555
+ port: 0,
556
+ socket: { data: () => undefined },
557
+ }),
558
+ );
559
+ const ports = listeners.map((listener) => listener.port);
560
+ listeners.forEach((listener) => listener.stop(true));
561
+ return ports;
562
+ }
563
+
564
+ async function writeExecutable(filePath: string, content: string): Promise<void> {
565
+ await fs.writeFile(filePath, content, 'utf8');
566
+ await fs.chmod(filePath, 0o755);
567
+ }
568
+
569
+ async function pathExists(filePath: string): Promise<boolean> {
570
+ try {
571
+ await fs.access(filePath);
572
+ return true;
573
+ } catch {
574
+ return false;
575
+ }
576
+ }
577
+
578
+ async function stopFixtureProcesses(rootPath: string): Promise<void> {
579
+ const stateDirectory = path.join(rootPath, 'infra/minikube/.state/forwards');
580
+ let entries: string[];
581
+ try {
582
+ entries = await fs.readdir(stateDirectory);
583
+ } catch {
584
+ return;
585
+ }
586
+
587
+ await Promise.all(
588
+ entries
589
+ .filter((entry) => entry.endsWith('.pid'))
590
+ .map(async (entry) => {
591
+ const pid = Number.parseInt(
592
+ await fs.readFile(path.join(stateDirectory, entry), 'utf8'),
593
+ 10,
594
+ );
595
+ if (Number.isInteger(pid)) {
596
+ try {
597
+ process.kill(pid, 'SIGKILL');
598
+ } catch {
599
+ // The generated lifecycle may already have stopped the process.
600
+ }
601
+ }
602
+ }),
603
+ );
604
+ }
605
+
606
+ function createSupabaseManifest(): InfraManifestInput {
607
+ return {
608
+ deployment: { target: 'minikube', monitoring: false },
609
+ auth: { scope: 'global', provider: 'supabase' },
610
+ database: { provider: 'supabase', tier: 'dev' },
611
+ modules: [],
612
+ };
613
+ }
614
+
615
+ function createNativeAppManifest(profile: string) {
616
+ return {
617
+ ...createAppManifest(profile),
618
+ deploy: {
619
+ targets: {
620
+ android: { enabled: true, package: `com.ankh.${profile.replaceAll('-', '')}` },
621
+ },
622
+ },
623
+ };
624
+ }