@ankhorage/infra 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,8 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
1
6
  import { describe, expect, test } from 'bun:test';
2
7
 
3
8
  import type { InfraManifestInput } from '../../../types';
@@ -43,6 +48,159 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
43
48
  );
44
49
  });
45
50
 
51
+ test('uses app project identity for Supabase ports independently from namespace', () => {
52
+ const scannerEnv = getGeneratedFileContent(
53
+ 'local-example-test',
54
+ 'infra/minikube/.env.example',
55
+ {},
56
+ 'scanner',
57
+ );
58
+ const scannerNamespace = getGeneratedFileContent(
59
+ 'local-example-test',
60
+ 'infra/minikube/k8s/namespace.yaml',
61
+ {},
62
+ 'scanner',
63
+ );
64
+ const chessEnv = getGeneratedFileContent(
65
+ 'local-example-test',
66
+ 'infra/minikube/.env.example',
67
+ {},
68
+ 'chess',
69
+ );
70
+
71
+ expect(scannerNamespace).toContain('name: local-example-test');
72
+ expect(scannerEnv).toContain('SUPABASE_LOCAL_PORT_BASE=64020');
73
+ expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
74
+ expect(scannerEnv).not.toContain('SUPABASE_LOCAL_PORT_BASE=62200');
75
+ });
76
+
77
+ test('generates canonical Supabase project identity checks', () => {
78
+ const script = getGeneratedFileContent(
79
+ 'scanner',
80
+ 'infra/minikube/scripts/supabase-local-env.sh',
81
+ );
82
+ const status = getGeneratedFileContent('scanner', 'infra/minikube/scripts/status.sh');
83
+
84
+ expect(script).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
85
+ expect(script).toContain('write_supabase_project_identity_for_new_config');
86
+ expect(script).toContain('validate_supabase_project_identity');
87
+ expect(script).toContain('project_id = "{expected}"');
88
+ expect(script).toContain(
89
+ 'The existing local Supabase project belongs to a different identity.',
90
+ );
91
+ expect(script).toContain(
92
+ 'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
93
+ );
94
+ expect(script).toContain(
95
+ 'supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes >/dev/null\n write_supabase_project_identity_for_new_config',
96
+ );
97
+ expect(script).toContain('validate_supabase_project_identity\nconfigure_supabase_local_ports');
98
+ expect(script).toContain(
99
+ 'validate_supabase_project_identity\n assert_supabase_local_ports_available',
100
+ );
101
+ expect(script).toContain('validate_supabase_project_identity\nrun_checked_command');
102
+ expect(script).not.toContain('basename "${APP_SOURCE_DIR}"');
103
+ expect(script).not.toContain('_minikube');
104
+
105
+ expect(status).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
106
+ expect(status).toContain('reject_supabase_project_id_override');
107
+ expect(status).toContain('validate_supabase_project_identity');
108
+ expect(status).toContain('Supabase project identity mismatch.');
109
+ expect(status).toContain('Destroy the invalid local stack and run Infra Up again.');
110
+ expect(status).not.toContain('write_supabase_project_identity_for_new_config');
111
+ expect(status).not.toContain('configure_supabase_local_ports');
112
+ expect(status).not.toContain('_minikube');
113
+ });
114
+
115
+ test('patches only a newly initialized Supabase config to the canonical project identity', () => {
116
+ const run = runGeneratedSupabaseScript({
117
+ projectId: 'scanner',
118
+ existingConfig: null,
119
+ });
120
+
121
+ expect(run.status).toBe(0);
122
+ expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "scanner"');
123
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).toContain(
124
+ '--workdir ' + run.root + ' init --yes',
125
+ );
126
+ });
127
+
128
+ test('rejects an existing Supabase config with a different project identity before lifecycle work', () => {
129
+ const run = runGeneratedSupabaseScript({
130
+ projectId: 'scanner',
131
+ existingConfig: 'project_id = "minikube"\n[api]\nport = 54321\n',
132
+ });
133
+
134
+ expect(run.status).not.toBe(0);
135
+ expect(run.combinedOutput).toContain('Supabase project identity mismatch.');
136
+ expect(run.combinedOutput).toContain('Expected "scanner", found "minikube".');
137
+ expect(run.combinedOutput).toContain(
138
+ 'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
139
+ );
140
+ expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "minikube"');
141
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
142
+ });
143
+
144
+ test('rejects an existing Supabase config with missing top-level project identity', () => {
145
+ const run = runGeneratedSupabaseScript({
146
+ projectId: 'scanner',
147
+ existingConfig: '[api]\nport = 54321\n',
148
+ });
149
+
150
+ expect(run.status).not.toBe(0);
151
+ expect(run.combinedOutput).toContain('Expected "scanner", found missing.');
152
+ expect(readFileSync(run.configPath, 'utf-8')).not.toContain('project_id = "scanner"');
153
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
154
+ });
155
+
156
+ test('rejects ambient SUPABASE_PROJECT_ID before Supabase CLI operations', () => {
157
+ const run = runGeneratedSupabaseScript({
158
+ projectId: 'scanner',
159
+ existingConfig: 'project_id = "scanner"\n[api]\nport = 54321\n',
160
+ env: {
161
+ SUPABASE_PROJECT_ID: 'minikube',
162
+ },
163
+ });
164
+
165
+ expect(run.status).not.toBe(0);
166
+ expect(run.combinedOutput).toContain(
167
+ 'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
168
+ );
169
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
170
+ });
171
+
172
+ test('rejects empty Supabase project identity before Supabase CLI operations', () => {
173
+ const run = runGeneratedSupabaseScript({
174
+ projectId: 'plain',
175
+ supabaseProjectId: null,
176
+ existingConfig: null,
177
+ });
178
+
179
+ expect(run.status).not.toBe(0);
180
+ expect(run.combinedOutput).toContain(
181
+ 'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
182
+ );
183
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
184
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
185
+ expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
186
+ });
187
+
188
+ test('requires python before Supabase init mutates the workdir', () => {
189
+ const run = runGeneratedSupabaseScript({
190
+ projectId: 'scanner',
191
+ existingConfig: null,
192
+ withoutPython: true,
193
+ });
194
+
195
+ expect(run.status).not.toBe(0);
196
+ expect(run.combinedOutput).toContain(
197
+ 'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
198
+ );
199
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
200
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
201
+ expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
202
+ });
203
+
46
204
  test('generates bootstrap logic that retries unhealthy local Supabase startup once', () => {
47
205
  const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
48
206
 
@@ -67,9 +225,16 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
67
225
  const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
68
226
 
69
227
  expect(script).toContain('SUPABASE_PROJECT_DIR="${SUPABASE_PROJECT_DIR:-${ROOT_DIR}}"');
228
+ expect(script).toContain('require_expected_supabase_project_identity');
229
+ expect(script).toContain(
230
+ 'require_expected_supabase_project_identity\nreject_supabase_project_id_override\nrequire_supabase_cli_capabilities',
231
+ );
70
232
  expect(script).toContain('supabase CLI >= 2.106.0 is required');
71
233
  expect(script).toContain('supabase CLI does not support required global --workdir flag');
72
234
  expect(script).toContain('supabase CLI does not support required migration up --local command');
235
+ expect(script).toContain(
236
+ 'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
237
+ );
73
238
  expect(script).toContain('psql is required but not installed');
74
239
  expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes');
75
240
  expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" migration up --local');
@@ -136,16 +301,113 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
136
301
  );
137
302
  expect(status).toContain('generated trigger function execute privilege must be revoked');
138
303
  });
304
+
305
+ test('generates relation-safe disabled profile verification for local env and status', () => {
306
+ const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
307
+ const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh');
308
+ const expectedDynamicLookup = `execute
309
+ 'select exists (
310
+ select 1
311
+ from ankhorage_internal.generated_schema_state
312
+ where artifact_key = $1
313
+ )'
314
+ into has_profile_state
315
+ using 'auth.profile';`;
316
+
317
+ for (const generated of [script, status]) {
318
+ expect(generated).toContain('has_profile_state boolean := false;');
319
+ expect(generated).toContain(expectedDynamicLookup);
320
+ expect(generated).toContain(
321
+ 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration',
322
+ );
323
+ expect(generated).not.toContain(
324
+ `if to_regclass('ankhorage_internal.generated_schema_state') is not null
325
+ and exists (`,
326
+ );
327
+ expect(generated.replace(expectedDynamicLookup, '')).not.toContain(
328
+ `from ankhorage_internal.generated_schema_state
329
+ where artifact_key = 'auth.profile'`,
330
+ );
331
+ }
332
+
333
+ expect(script).toContain('Generated profile disabled-state verification');
334
+ expect(status).toContain('- profile reconciliation: skipped (no profile table configured)');
335
+ expect(status).toContain('- profile schema: skipped (no profile table configured)');
336
+ });
337
+
338
+ test('status succeeds for a clean disabled profile local Supabase stack', () => {
339
+ const run = runGeneratedStatusScript({
340
+ manifest: createInfraManifest(),
341
+ supabaseProjectId: 'plain',
342
+ env: {
343
+ AUTH_RUNTIME_MODE: 'local',
344
+ },
345
+ });
346
+
347
+ expect(run.status).toBe(0);
348
+ expect(run.combinedOutput).toContain('- immutable migrations: applied');
349
+ expect(run.combinedOutput).toContain(
350
+ '- profile reconciliation: skipped (no profile table configured)',
351
+ );
352
+ expect(run.combinedOutput).toContain('- profile schema: skipped (no profile table configured)');
353
+ expect(run.supabaseLog).toContain('--workdir');
354
+ });
355
+
356
+ test('does not reject SUPABASE_PROJECT_ID for non-Supabase status checks', () => {
357
+ const run = runGeneratedStatusScript({
358
+ manifest: {
359
+ deployment: {
360
+ target: 'minikube',
361
+ monitoring: false,
362
+ },
363
+ plugins: [],
364
+ },
365
+ supabaseProjectId: null,
366
+ env: {
367
+ SUPABASE_PROJECT_ID: 'ambient-project',
368
+ },
369
+ });
370
+
371
+ expect(run.status).toBe(0);
372
+ expect(run.combinedOutput).not.toContain(
373
+ 'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
374
+ );
375
+ expect(run.kubectlLog).toContain('get all -n plain');
376
+ });
377
+
378
+ test('rejects empty Supabase project identity before local Supabase status checks', () => {
379
+ const run = runGeneratedStatusScript({
380
+ manifest: {
381
+ deployment: {
382
+ target: 'minikube',
383
+ monitoring: false,
384
+ },
385
+ plugins: [],
386
+ },
387
+ supabaseProjectId: null,
388
+ env: {
389
+ SUPABASE_LOCAL_ENABLED: 'true',
390
+ },
391
+ });
392
+
393
+ expect(run.status).not.toBe(0);
394
+ expect(run.combinedOutput).toContain(
395
+ 'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
396
+ );
397
+ expect(run.supabaseLog).toBe('');
398
+ });
139
399
  });
140
400
 
141
401
  function getGeneratedFileContent(
142
402
  namespace: string,
143
403
  filePath: string,
144
404
  overrides: Partial<InfraManifestInput> = {},
405
+ supabaseProjectId = namespace,
145
406
  ): string {
146
407
  const artifact = generateMinikubeBaseArtifacts({
147
408
  manifest: createInfraManifest(overrides),
148
409
  namespace,
410
+ supabaseProjectId,
149
411
  extraResources: [],
150
412
  extraEnvEntries: [],
151
413
  }).find((file) => file.path === filePath);
@@ -195,3 +457,293 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
195
457
  },
196
458
  };
197
459
  }
460
+
461
+ function runGeneratedSupabaseScript(args: {
462
+ projectId: string;
463
+ supabaseProjectId?: string | null;
464
+ existingConfig: string | null;
465
+ env?: Record<string, string>;
466
+ withoutPython?: boolean;
467
+ }): {
468
+ status: number | null;
469
+ combinedOutput: string;
470
+ root: string;
471
+ configPath: string;
472
+ supabaseLogPath: string;
473
+ } {
474
+ const tempRoot = mkdtempSync(join(tmpdir(), 'infra-supabase-identity-'));
475
+ const root = join(tempRoot, 'infra', 'minikube');
476
+ const scriptsDir = join(root, 'scripts');
477
+ const supabaseDir = join(root, 'supabase');
478
+ const binDir = join(tempRoot, 'bin');
479
+ const appSourceDir = join(tempRoot, 'app');
480
+ const scriptPath = join(scriptsDir, 'supabase-local-env.sh');
481
+ const configPath = join(supabaseDir, 'config.toml');
482
+ const supabaseLogPath = join(tempRoot, 'supabase.log');
483
+
484
+ mkdirSync(scriptsDir, { recursive: true });
485
+ mkdirSync(binDir, { recursive: true });
486
+ mkdirSync(appSourceDir, { recursive: true });
487
+ writeFileSync(join(root, '.env.example'), '');
488
+ writeFileSync(supabaseLogPath, '');
489
+
490
+ if (args.existingConfig !== null) {
491
+ mkdirSync(supabaseDir, { recursive: true });
492
+ writeFileSync(configPath, args.existingConfig);
493
+ }
494
+
495
+ const script = getGeneratedFileContent(
496
+ args.projectId,
497
+ 'infra/minikube/scripts/supabase-local-env.sh',
498
+ {},
499
+ args.supabaseProjectId === undefined ? args.projectId : args.supabaseProjectId,
500
+ );
501
+ writeFileSync(scriptPath, script);
502
+ chmodSync(scriptPath, 0o755);
503
+
504
+ const supabaseStubPath = join(binDir, 'supabase');
505
+ writeFileSync(
506
+ supabaseStubPath,
507
+ `#!/bin/bash
508
+ set -euo pipefail
509
+ printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
510
+
511
+ if [[ "\${1:-}" == "--version" ]]; then
512
+ echo "supabase 2.106.0"
513
+ exit 0
514
+ fi
515
+
516
+ if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
517
+ echo "--workdir"
518
+ exit 0
519
+ fi
520
+
521
+ if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
522
+ echo "--local"
523
+ exit 0
524
+ fi
525
+
526
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "init" && "\${4:-}" == "--yes" ]]; then
527
+ mkdir -p "\${2}/supabase"
528
+ printf '%s\\n' 'project_id = "minikube"' '[api]' 'port = 54321' > "\${2}/supabase/config.toml"
529
+ exit 0
530
+ fi
531
+
532
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
533
+ cat <<'ENV'
534
+ DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
535
+ API_URL=http://127.0.0.1:54321
536
+ ANON_KEY=anon
537
+ SERVICE_ROLE_KEY=service
538
+ JWT_SECRET=secret
539
+ ENV
540
+ exit 0
541
+ fi
542
+
543
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
544
+ exit 0
545
+ fi
546
+
547
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "migration" && "\${4:-}" == "up" && "\${5:-}" == "--local" ]]; then
548
+ exit 0
549
+ fi
550
+
551
+ echo "unexpected supabase call: $*" >&2
552
+ exit 1
553
+ `,
554
+ );
555
+ chmodSync(supabaseStubPath, 0o755);
556
+
557
+ const psqlStubPath = join(binDir, 'psql');
558
+ writeFileSync(
559
+ psqlStubPath,
560
+ `#!/bin/bash
561
+ exit 0
562
+ `,
563
+ );
564
+ chmodSync(psqlStubPath, 0o755);
565
+
566
+ const result = spawnSync('/bin/bash', [scriptPath], {
567
+ cwd: tempRoot,
568
+ env: {
569
+ ...(args.withoutPython ? {} : process.env),
570
+ PATH: args.withoutPython ? binDir : `${binDir}:${process.env.PATH ?? ''}`,
571
+ APP_SOURCE_DIR: appSourceDir,
572
+ SUPABASE_STUB_LOG: supabaseLogPath,
573
+ ...args.env,
574
+ },
575
+ encoding: 'utf-8',
576
+ });
577
+
578
+ return {
579
+ status: result.status,
580
+ combinedOutput: `${result.stdout}${result.stderr}`,
581
+ root,
582
+ configPath,
583
+ supabaseLogPath,
584
+ };
585
+ }
586
+
587
+ function runGeneratedStatusScript(args: {
588
+ manifest: InfraManifestInput;
589
+ supabaseProjectId: string | null;
590
+ env?: Record<string, string>;
591
+ }): {
592
+ status: number | null;
593
+ combinedOutput: string;
594
+ kubectlLog: string;
595
+ supabaseLog: string;
596
+ } {
597
+ const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
598
+ const root = join(tempRoot, 'infra', 'minikube');
599
+ const scriptsDir = join(root, 'scripts');
600
+ const supabaseDir = join(root, 'supabase');
601
+ const binDir = join(tempRoot, 'bin');
602
+ const scriptPath = join(scriptsDir, 'status.sh');
603
+ const kubectlLogPath = join(tempRoot, 'kubectl.log');
604
+ const supabaseLogPath = join(tempRoot, 'supabase.log');
605
+ const psqlLogPath = join(tempRoot, 'psql.log');
606
+
607
+ mkdirSync(scriptsDir, { recursive: true });
608
+ mkdirSync(binDir, { recursive: true });
609
+ writeFileSync(join(root, '.env.example'), '');
610
+ writeFileSync(kubectlLogPath, '');
611
+ writeFileSync(supabaseLogPath, '');
612
+ writeFileSync(psqlLogPath, '');
613
+
614
+ if (args.supabaseProjectId !== null) {
615
+ mkdirSync(supabaseDir, { recursive: true });
616
+ writeFileSync(join(supabaseDir, 'config.toml'), `project_id = "${args.supabaseProjectId}"\n`);
617
+ }
618
+
619
+ const artifact = generateMinikubeBaseArtifacts({
620
+ manifest: args.manifest,
621
+ namespace: 'plain',
622
+ supabaseProjectId: args.supabaseProjectId,
623
+ extraResources: [],
624
+ extraEnvEntries: [],
625
+ }).find((file) => file.path === 'infra/minikube/scripts/status.sh');
626
+
627
+ if (!artifact) {
628
+ throw new Error('Expected generated status.sh artifact');
629
+ }
630
+
631
+ writeFileSync(scriptPath, artifact.content);
632
+ chmodSync(scriptPath, 0o755);
633
+
634
+ const kubectlStubPath = join(binDir, 'kubectl');
635
+ writeFileSync(
636
+ kubectlStubPath,
637
+ `#!/bin/bash
638
+ set -euo pipefail
639
+ printf '%s\\n' "$*" >> "\${KUBECTL_STUB_LOG}"
640
+
641
+ if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
642
+ echo "minikube"
643
+ exit 0
644
+ fi
645
+
646
+ if [[ "\${1:-}" == "cluster-info" ]]; then
647
+ echo "Kubernetes control plane is running"
648
+ exit 0
649
+ fi
650
+
651
+ if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
652
+ exit 0
653
+ fi
654
+
655
+ if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
656
+ echo "No resources found"
657
+ exit 0
658
+ fi
659
+
660
+ echo "unexpected kubectl call: $*" >&2
661
+ exit 1
662
+ `,
663
+ );
664
+ chmodSync(kubectlStubPath, 0o755);
665
+
666
+ const supabaseStubPath = join(binDir, 'supabase');
667
+ writeFileSync(
668
+ supabaseStubPath,
669
+ `#!/bin/bash
670
+ set -euo pipefail
671
+ printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
672
+
673
+ if [[ "\${1:-}" == "--version" ]]; then
674
+ echo "supabase 2.106.0"
675
+ exit 0
676
+ fi
677
+
678
+ if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
679
+ echo "--workdir"
680
+ exit 0
681
+ fi
682
+
683
+ if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
684
+ echo "--local"
685
+ exit 0
686
+ fi
687
+
688
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
689
+ cat <<'ENV'
690
+ DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
691
+ API_URL=http://127.0.0.1:54321
692
+ ANON_KEY=anon
693
+ SERVICE_ROLE_KEY=service
694
+ JWT_SECRET=secret
695
+ ENV
696
+ exit 0
697
+ fi
698
+
699
+ if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
700
+ exit 0
701
+ fi
702
+
703
+ echo "unexpected supabase call: $*" >&2
704
+ exit 1
705
+ `,
706
+ );
707
+ chmodSync(supabaseStubPath, 0o755);
708
+
709
+ const psqlStubPath = join(binDir, 'psql');
710
+ writeFileSync(
711
+ psqlStubPath,
712
+ `#!/bin/bash
713
+ set -euo pipefail
714
+ printf '%s\\n' "$*" >> "\${PSQL_STUB_LOG}"
715
+
716
+ if [[ "$*" == *"select version from supabase_migrations.schema_migrations"* ]]; then
717
+ exit 0
718
+ fi
719
+
720
+ if [[ "$*" == *"-f"* ]]; then
721
+ exit 0
722
+ fi
723
+
724
+ echo "unexpected psql call: $*" >&2
725
+ exit 1
726
+ `,
727
+ );
728
+ chmodSync(psqlStubPath, 0o755);
729
+
730
+ const result = spawnSync('/bin/bash', [scriptPath], {
731
+ cwd: tempRoot,
732
+ env: {
733
+ ...process.env,
734
+ PATH: `${binDir}:${process.env.PATH ?? ''}`,
735
+ KUBECTL_STUB_LOG: kubectlLogPath,
736
+ SUPABASE_STUB_LOG: supabaseLogPath,
737
+ PSQL_STUB_LOG: psqlLogPath,
738
+ ...args.env,
739
+ },
740
+ encoding: 'utf-8',
741
+ });
742
+
743
+ return {
744
+ status: result.status,
745
+ combinedOutput: `${result.stdout}${result.stderr}`,
746
+ kubectlLog: readFileSync(kubectlLogPath, 'utf-8'),
747
+ supabaseLog: readFileSync(supabaseLogPath, 'utf-8'),
748
+ };
749
+ }