@ankhorage/infra 0.4.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ankhorage/infra
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f48b53f: Preserve app-specific Supabase local project identity while keeping the canonical Minikube workdir.
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # INFRA
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.3.7](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.4.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Executable infra provider and standalone CLI for Ankhorage project workflows.
9
9
 
@@ -3,6 +3,7 @@ import type { InfraManifestInput } from '../../../types';
3
3
  export declare function generateMinikubeBaseArtifacts(args: {
4
4
  manifest: InfraManifestInput;
5
5
  namespace: string;
6
+ supabaseProjectId: string | null;
6
7
  extraResources: string[];
7
8
  extraEnvEntries: string[];
8
9
  }): GeneratedInfrastructureFile[];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA4BzD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,2BAA2B,EAAE,CA8HhC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA4BzD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,2BAA2B,EAAE,CAiIhC"}
@@ -4,13 +4,13 @@ const SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;
4
4
  const SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;
5
5
  const SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';
6
6
  export function generateMinikubeBaseArtifacts(args) {
7
- const { manifest, namespace, extraResources, extraEnvEntries } = args;
7
+ const { manifest, namespace, supabaseProjectId, extraResources, extraEnvEntries } = args;
8
8
  const root = 'infra/minikube';
9
9
  const k8sRoot = `${root}/k8s`;
10
10
  const scriptsRoot = `${root}/scripts`;
11
11
  const appImageRoot = `${root}/app-image`;
12
12
  const defaultAppImage = getDefaultAppImage(namespace);
13
- const supabaseLocalPorts = resolveSupabaseLocalPorts(namespace);
13
+ const supabaseLocalPorts = resolveSupabaseLocalPorts(supabaseProjectId ?? namespace);
14
14
  const authScope = manifest.auth?.scope ?? 'none';
15
15
  const authProvider = manifest.auth?.provider ?? 'none';
16
16
  const authzEngine = manifest.auth?.authorization?.engine ?? 'none';
@@ -35,6 +35,7 @@ export function generateMinikubeBaseArtifacts(args) {
35
35
  authzEngine,
36
36
  extraResources,
37
37
  supabaseLocalPorts,
38
+ supabaseProjectId,
38
39
  supabaseLocalEnabled,
39
40
  profileModel,
40
41
  }),
@@ -107,6 +108,7 @@ export function generateMinikubeBaseArtifacts(args) {
107
108
  path: `${scriptsRoot}/status.sh`,
108
109
  content: getStatusScript({
109
110
  defaultNamespace: namespace,
111
+ supabaseProjectId,
110
112
  supabaseLocalEnabled,
111
113
  profileModel,
112
114
  }),
@@ -115,6 +117,7 @@ export function generateMinikubeBaseArtifacts(args) {
115
117
  {
116
118
  path: `${scriptsRoot}/supabase-local-env.sh`,
117
119
  content: getSupabaseLocalEnvScript({
120
+ supabaseProjectId,
118
121
  supabaseLocalPorts,
119
122
  profileModel,
120
123
  }),
@@ -127,7 +130,7 @@ export function generateMinikubeBaseArtifacts(args) {
127
130
  ];
128
131
  }
129
132
  function getReadmeMarkdown(args) {
130
- const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseLocalEnabled, profileModel, } = args;
133
+ const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseProjectId, supabaseLocalEnabled, profileModel, } = args;
131
134
  const resourceLines = [
132
135
  'namespace.yaml',
133
136
  'app.configmap.yaml',
@@ -190,6 +193,7 @@ ${resourceLines}
190
193
  ## Runtime Conventions
191
194
 
192
195
  - Namespace: \`${namespace}\`
196
+ - Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`
193
197
  - Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
194
198
  - Auth provider: \`${authProvider}\`
195
199
  - Authorization engine: \`${authzEngine}\`
@@ -277,6 +281,7 @@ Build flow:
277
281
 
278
282
  - \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
279
283
  - Supabase local project root: \`infra/minikube\`${supabaseLocalEnabled ? '' : ' (unused unless Supabase-backed auth/database/storage is enabled)'}.
284
+ - Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`; existing local configs with another \`project_id\` must be destroyed and recreated manually.
280
285
  - Immutable migrations live in \`supabase/migrations/\` and are applied with \`supabase migration up --local\`.
281
286
  ${profileModel.enabled ? '- Generated profile desired-state reconciliation lives in `supabase/generated/auth_profiles.sql` and records checksum state in `ankhorage_internal.generated_schema_state`.\n' : ''}- \`./scripts/supabase-local-env.sh\` can populate Supabase keys in \`.env\` from local \`supabase status -o env\`.
282
287
  - \`./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.
@@ -1293,7 +1298,7 @@ end;
1293
1298
  $$;`;
1294
1299
  }
1295
1300
  function getStatusScript(args) {
1296
- const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
1301
+ const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
1297
1302
  const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
1298
1303
  const profileVerificationSql = profileModel.enabled
1299
1304
  ? getSupabaseProfileVerificationSql(profileModel)
@@ -1316,8 +1321,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
1316
1321
  SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
1317
1322
  SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
1318
1323
  SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
1324
+ EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
1319
1325
  SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
1320
1326
  SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
1327
+ export EXPECTED_SUPABASE_PROJECT_ID
1328
+
1329
+ reject_supabase_project_id_override() {
1330
+ if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
1331
+ echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
1332
+ echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
1333
+ return 1
1334
+ fi
1335
+
1336
+ unset SUPABASE_PROJECT_ID
1337
+ }
1338
+
1339
+ require_expected_supabase_project_identity() {
1340
+ if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
1341
+ echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
1342
+ echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
1343
+ return 1
1344
+ fi
1345
+ }
1321
1346
 
1322
1347
  if ! command -v kubectl >/dev/null 2>&1; then
1323
1348
  echo "kubectl is required but not installed."
@@ -1481,6 +1506,61 @@ run_supabase_sql() {
1481
1506
  return 1
1482
1507
  }
1483
1508
 
1509
+ validate_supabase_project_identity() {
1510
+ local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
1511
+
1512
+ if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
1513
+ return 0
1514
+ fi
1515
+
1516
+ if [[ ! -f "\${config_file}" ]]; then
1517
+ echo "- local Supabase project identity: config missing at \${config_file}"
1518
+ return 1
1519
+ fi
1520
+
1521
+ if ! command -v python3 >/dev/null 2>&1; then
1522
+ echo "- local Supabase project identity: python3 is required to read \${config_file}"
1523
+ return 1
1524
+ fi
1525
+
1526
+ python3 - "\${config_file}" <<'PY'
1527
+ import os
1528
+ import re
1529
+ import sys
1530
+
1531
+
1532
+ def read_top_level_project_id(config_path):
1533
+ key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
1534
+ section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
1535
+
1536
+ with open(config_path, 'r', encoding='utf-8') as f:
1537
+ for line in f:
1538
+ if section_re.match(line):
1539
+ return None
1540
+ match = key_re.match(line)
1541
+ if not match:
1542
+ continue
1543
+ raw = match.group(1).strip()
1544
+ quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
1545
+ return quoted.group(1) if quoted else raw
1546
+
1547
+ return None
1548
+
1549
+
1550
+ expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
1551
+ actual = read_top_level_project_id(sys.argv[1])
1552
+
1553
+ if actual == expected:
1554
+ raise SystemExit(0)
1555
+
1556
+ found = 'missing' if actual is None else f'"{actual}"'
1557
+ print('Supabase project identity mismatch.', file=sys.stderr)
1558
+ print(f'Expected "{expected}", found {found}.', file=sys.stderr)
1559
+ print('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)
1560
+ raise SystemExit(1)
1561
+ PY
1562
+ }
1563
+
1484
1564
  check_immutable_migrations_applied() {
1485
1565
  local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
1486
1566
  local applied_versions
@@ -1515,8 +1595,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
1515
1595
  echo "Local Supabase database checks:"
1516
1596
  status_failed=0
1517
1597
 
1518
- if require_supabase_cli_capabilities; then
1519
- if supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
1598
+ if ! require_expected_supabase_project_identity; then
1599
+ status_failed=1
1600
+ elif ! reject_supabase_project_id_override; then
1601
+ status_failed=1
1602
+ elif require_supabase_cli_capabilities; then
1603
+ if ! validate_supabase_project_identity; then
1604
+ status_failed=1
1605
+ elif supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
1520
1606
  STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
1521
1607
  SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
1522
1608
  if [[ -z "\${SUPABASE_DB_URL}" ]]; then
@@ -1587,7 +1673,7 @@ fi
1587
1673
  `;
1588
1674
  }
1589
1675
  function getSupabaseLocalEnvScript(args) {
1590
- const { supabaseLocalPorts, profileModel } = args;
1676
+ const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
1591
1677
  const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
1592
1678
  const profileVerificationSql = profileModel.enabled
1593
1679
  ? getSupabaseProfileVerificationSql(profileModel)
@@ -1609,9 +1695,29 @@ fi
1609
1695
 
1610
1696
  APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
1611
1697
  SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
1698
+ EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
1612
1699
  APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
1613
1700
  SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
1614
1701
  SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
1702
+ export EXPECTED_SUPABASE_PROJECT_ID
1703
+
1704
+ reject_supabase_project_id_override() {
1705
+ if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
1706
+ echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
1707
+ echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
1708
+ exit 1
1709
+ fi
1710
+
1711
+ unset SUPABASE_PROJECT_ID
1712
+ }
1713
+
1714
+ require_expected_supabase_project_identity() {
1715
+ if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
1716
+ echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
1717
+ echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
1718
+ exit 1
1719
+ fi
1720
+ }
1615
1721
 
1616
1722
  SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
1617
1723
  SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
@@ -1657,6 +1763,11 @@ require_supabase_cli_capabilities() {
1657
1763
  exit 1
1658
1764
  fi
1659
1765
 
1766
+ if ! command -v python3 >/dev/null 2>&1; then
1767
+ echo "python3 is required to reconcile supabase/config.toml before local Supabase startup."
1768
+ exit 1
1769
+ fi
1770
+
1660
1771
  local version
1661
1772
  version="$(supabase --version | awk '{print $NF}')"
1662
1773
  if ! version_ge "\${version}" "2.106.0"; then
@@ -1718,6 +1829,8 @@ run_checked_sql_file() {
1718
1829
  psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
1719
1830
  }
1720
1831
 
1832
+ require_expected_supabase_project_identity
1833
+ reject_supabase_project_id_override
1721
1834
  require_supabase_cli_capabilities
1722
1835
 
1723
1836
  if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
@@ -1728,18 +1841,6 @@ fi
1728
1841
  configure_supabase_local_ports() {
1729
1842
  local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
1730
1843
 
1731
- if ! command -v python3 >/dev/null 2>&1; then
1732
- echo "python3 is required to patch supabase/config.toml with generated local port defaults."
1733
- echo "Install python3 (recommended) or manually edit \${config_file} ports:"
1734
- echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
1735
- echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
1736
- echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
1737
- echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
1738
- echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
1739
- echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
1740
- exit 1
1741
- fi
1742
-
1743
1844
  python3 - "\${config_file}" <<'PY'
1744
1845
  import os
1745
1846
  import re
@@ -1843,8 +1944,114 @@ if __name__ == "__main__":
1843
1944
  PY
1844
1945
  }
1845
1946
 
1947
+ write_supabase_project_identity_for_new_config() {
1948
+ local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
1949
+
1950
+ require_expected_supabase_project_identity
1951
+
1952
+ if ! command -v python3 >/dev/null 2>&1; then
1953
+ echo "python3 is required to write Supabase project_id in \${config_file}."
1954
+ exit 1
1955
+ fi
1956
+
1957
+ python3 - "\${config_file}" <<'PY'
1958
+ import os
1959
+ import re
1960
+ import sys
1961
+
1962
+
1963
+ def write_top_level_project_id(config_path, expected):
1964
+ key_re = re.compile(r'^(\\s*)project_id\\s*=\\s*(.*?)(\\s*(#.*)?)?$')
1965
+ section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
1966
+
1967
+ with open(config_path, 'r', encoding='utf-8') as f:
1968
+ lines = f.readlines()
1969
+
1970
+ section_start = len(lines)
1971
+ for i, line in enumerate(lines):
1972
+ if section_re.match(line):
1973
+ section_start = i
1974
+ break
1975
+
1976
+ for i in range(section_start):
1977
+ match = key_re.match(lines[i])
1978
+ if not match:
1979
+ continue
1980
+ indent = match.group(1) or ''
1981
+ trailing = (match.group(3) or '').rstrip('\\n')
1982
+ lines[i] = f'{indent}project_id = "{expected}"{trailing}\\n'
1983
+ break
1984
+ else:
1985
+ insert_at = section_start
1986
+ lines.insert(insert_at, f'project_id = "{expected}"\\n')
1987
+
1988
+ with open(config_path, 'w', encoding='utf-8') as f:
1989
+ f.writelines(lines)
1990
+
1991
+
1992
+ write_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])
1993
+ PY
1994
+ }
1995
+
1996
+ validate_supabase_project_identity() {
1997
+ local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
1998
+
1999
+ require_expected_supabase_project_identity
2000
+
2001
+ if [[ ! -f "\${config_file}" ]]; then
2002
+ echo "Supabase project identity mismatch."
2003
+ printf 'Expected "%s", found missing.\n' "\${EXPECTED_SUPABASE_PROJECT_ID}"
2004
+ echo "The existing local Supabase project belongs to a different identity."
2005
+ echo "Destroy the invalid local stack and its project-owned resources, then run Infra Up again."
2006
+ exit 1
2007
+ fi
2008
+
2009
+ if ! command -v python3 >/dev/null 2>&1; then
2010
+ echo "python3 is required to read Supabase project_id in \${config_file}."
2011
+ exit 1
2012
+ fi
2013
+
2014
+ python3 - "\${config_file}" <<'PY'
2015
+ import os
2016
+ import re
2017
+ import sys
2018
+
2019
+
2020
+ def read_top_level_project_id(config_path):
2021
+ key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
2022
+ section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
2023
+
2024
+ with open(config_path, 'r', encoding='utf-8') as f:
2025
+ for line in f:
2026
+ if section_re.match(line):
2027
+ return None
2028
+ match = key_re.match(line)
2029
+ if not match:
2030
+ continue
2031
+ raw = match.group(1).strip()
2032
+ quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
2033
+ return quoted.group(1) if quoted else raw
2034
+
2035
+ return None
2036
+
2037
+
2038
+ expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
2039
+ actual = read_top_level_project_id(sys.argv[1])
2040
+
2041
+ if actual == expected:
2042
+ raise SystemExit(0)
2043
+
2044
+ found = 'missing' if actual is None else f'"{actual}"'
2045
+ print('Supabase project identity mismatch.', file=sys.stderr)
2046
+ print(f'Expected "{expected}", found {found}.', file=sys.stderr)
2047
+ print('The existing local Supabase project belongs to a different identity.', file=sys.stderr)
2048
+ print('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)
2049
+ raise SystemExit(1)
2050
+ PY
2051
+ }
2052
+
1846
2053
  assert_supabase_local_ports_available() {
1847
- local project_label="\${ANKH_NAMESPACE:-$(basename "\${APP_SOURCE_DIR}")}"
2054
+ local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
1848
2055
 
1849
2056
  ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
1850
2057
  import os
@@ -1922,8 +2129,10 @@ PY
1922
2129
  if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
1923
2130
  echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
1924
2131
  supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
2132
+ write_supabase_project_identity_for_new_config
1925
2133
  fi
1926
2134
 
2135
+ validate_supabase_project_identity
1927
2136
  configure_supabase_local_ports
1928
2137
 
1929
2138
  STATUS_ENV="$(
@@ -1995,6 +2204,7 @@ start_supabase_local_stack() {
1995
2204
  }
1996
2205
 
1997
2206
  if [[ -z "\${STATUS_ENV}" ]]; then
2207
+ validate_supabase_project_identity
1998
2208
  assert_supabase_local_ports_available
1999
2209
  echo "Supabase local stack not detected. Starting with 'supabase start'..."
2000
2210
  start_supabase_local_stack
@@ -2008,6 +2218,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
2008
2218
  fi
2009
2219
 
2010
2220
  echo "Applying pending immutable Supabase migrations..."
2221
+ validate_supabase_project_identity
2011
2222
  run_checked_command \
2012
2223
  "Immutable Supabase migration application" \
2013
2224
  "" \