@geekmidas/cli 10.0.0-alpha.33 → 10.0.0-alpha.35

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 (29) hide show
  1. package/dist/{config-DMpzmsQL.cjs → config-DgAHy8nY.cjs} +9 -5
  2. package/dist/config-DgAHy8nY.cjs.map +1 -0
  3. package/dist/{config-Du8AQJRK.mjs → config-Dss1WwtH.mjs} +9 -5
  4. package/dist/config-Dss1WwtH.mjs.map +1 -0
  5. package/dist/config.cjs +1 -1
  6. package/dist/config.mjs +1 -1
  7. package/dist/{github-DN-92LWP.mjs → github-C99BMxf0.mjs} +2 -2
  8. package/dist/{github-DN-92LWP.mjs.map → github-C99BMxf0.mjs.map} +1 -1
  9. package/dist/{github-JhvrmjQp.cjs → github-tZmMZ7td.cjs} +2 -2
  10. package/dist/{github-JhvrmjQp.cjs.map → github-tZmMZ7td.cjs.map} +1 -1
  11. package/dist/index.cjs +221 -86
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.mjs +222 -87
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{migrate-D2rtGKYi.mjs → migrate-C-D9xIbb.mjs} +3 -3
  16. package/dist/{migrate-D2rtGKYi.mjs.map → migrate-C-D9xIbb.mjs.map} +1 -1
  17. package/dist/{migrate-BkytlJlU.cjs → migrate-nlvT-GnY.cjs} +3 -3
  18. package/dist/{migrate-BkytlJlU.cjs.map → migrate-nlvT-GnY.cjs.map} +1 -1
  19. package/dist/openapi.cjs +1 -1
  20. package/dist/openapi.mjs +1 -1
  21. package/dist/{workspace-BNurJoFx.mjs → workspace-CIiGijNi.mjs} +91 -16
  22. package/dist/workspace-CIiGijNi.mjs.map +1 -0
  23. package/dist/{workspace-C8TbIWxO.cjs → workspace-CRjZkCTW.cjs} +90 -15
  24. package/dist/workspace-CRjZkCTW.cjs.map +1 -0
  25. package/package.json +14 -14
  26. package/dist/config-DMpzmsQL.cjs.map +0 -1
  27. package/dist/config-Du8AQJRK.mjs.map +0 -1
  28. package/dist/workspace-BNurJoFx.mjs.map +0 -1
  29. package/dist/workspace-C8TbIWxO.cjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
3
3
  import { c as isPartitionedRoutes, i as ConstructGenerator, t as discover } from "./discover-CMxesskG.mjs";
4
- import { a as loadConfig, c as parseModuleConfig, i as loadAppConfig, l as appKey, r as getAppNameFromCwd, s as loadWorkspaceConfig, u as derivedApps } from "./config-Du8AQJRK.mjs";
4
+ import { a as loadConfig, c as parseModuleConfig, i as loadAppConfig, l as appKey, r as getAppNameFromCwd, s as loadWorkspaceConfig, u as derivedApps } from "./config-Dss1WwtH.mjs";
5
5
  import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-CsL76tpc.mjs";
6
6
  import { c as getKeyPath, i as setCustomSecret, n as readStageSecrets, o as toEmbeddableSecrets, r as secretsExist, s as writeStageSecrets, t as maskPassword } from "./storage-BjTLjDDp.mjs";
7
- import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-BNurJoFx.mjs";
7
+ import { _ as findAvailablePort, b as loadSecretsForApp, c as applyPostgres, d as cacheBackendFor, f as eventsBackendFor, g as createEntryWrapper, h as createCredentialsPreload, i as reconcileWorkspace, l as resourceName, m as storageBackendFor, n as constructGlobs, o as COMPOSE_PATH, p as providerOf, r as derivedContainers, s as LOCAL_CA_PATH, t as backendsOf, u as rootSite, v as isPortAvailable, x as prepareEntryCredentials, y as loadEnvFiles } from "./workspace-CIiGijNi.mjs";
8
8
  import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-hDINScnz.mjs";
9
9
  import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
10
10
  import { a as writeTestHarness, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as cacheBackendsIn, s as driversFor, t as TEST_READY_ENV } from "./ready-D1BCiaYU.mjs";
@@ -19,7 +19,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
19
19
  import { homedir } from "node:os";
20
20
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
21
21
  import { execFile, execSync, spawn } from "node:child_process";
22
- import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, kebabCase, migrationFolder, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
22
+ import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, kebabCase, migrationFolder, mobileOrigins, provideKey, provisionOrder, schemeBase, scopedName, serviceKey } from "@geekmidas/manifest";
23
23
  import fg from "fast-glob";
24
24
  import chokidar from "chokidar";
25
25
  import { createCipheriv, createHash, randomBytes } from "node:crypto";
@@ -37,7 +37,7 @@ import { Subscriber } from "@geekmidas/constructs/subscribers";
37
37
  import { Topic } from "@geekmidas/constructs/topic";
38
38
  import { lookup } from "node:dns/promises";
39
39
  //#region package.json
40
- var version = "10.0.0-alpha.33";
40
+ var version = "10.0.0-alpha.35";
41
41
  //#endregion
42
42
  //#region src/auth/credentials.ts
43
43
  /**
@@ -1087,9 +1087,9 @@ async function workspaceDevCommand(workspace, options) {
1087
1087
  ...reconciled.env
1088
1088
  };
1089
1089
  if (options.migrate || options.seed) {
1090
- const { prepareDevDatabases } = await import("./migrate-D2rtGKYi.mjs");
1090
+ const { prepareDevDatabases } = await import("./migrate-C-D9xIbb.mjs");
1091
1091
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1092
- } else await import("./migrate-D2rtGKYi.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1092
+ } else await import("./migrate-C-D9xIbb.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1093
1093
  logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1094
1094
  });
1095
1095
  const appUrls = {};
@@ -1119,6 +1119,13 @@ async function workspaceDevCommand(workspace, options) {
1119
1119
  for (const appName of buildOrder) {
1120
1120
  const app = workspace.apps[appName];
1121
1121
  if (!app) continue;
1122
+ if (app.type === "mobile") {
1123
+ const own = reconciled.plan.resources.find((r) => r.kind === "mobile-app" && appKey(r.id) === appName);
1124
+ const scheme = own ? reconciled.env[own.envKey] : void 0;
1125
+ const device = Object.entries(reconciled.env).find(([key]) => key.endsWith("_DEVICE_URL"))?.[1];
1126
+ logger$10.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
1127
+ continue;
1128
+ }
1122
1129
  const local = `http://localhost:${app.port}`;
1123
1130
  const url = Object.entries(appUrls).find(([id]) => appKey(id) === appName)?.[1];
1124
1131
  logger$10.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
@@ -3102,6 +3109,7 @@ const PROVISIONERS = {
3102
3109
  context.deferred.push(...statementsFor({
3103
3110
  id: declaration.id,
3104
3111
  database: postgres.databaseName,
3112
+ ownsDatabase: true,
3105
3113
  runtime,
3106
3114
  owner,
3107
3115
  ...reads ? { reader: readerRole(runtime) } : {},
@@ -3312,9 +3320,13 @@ const PROVISIONERS = {
3312
3320
  if (!url) throw new SurfaceHasNoAddress(declaration.id);
3313
3321
  const origins = [...new Set(dependentsOf(context.manifest, declaration.id).map((caller) => context.addresses?.[caller]).filter((address) => Boolean(address)))].sort();
3314
3322
  const domain = cookieDomain([url, ...origins]);
3323
+ const schemes = dependentsOf(context.manifest, declaration.id).flatMap((caller) => {
3324
+ const app = context.manifest[caller];
3325
+ return app?.kind === "mobile-app" ? mobileOrigins(schemeBase(context.project, app.scheme)) : [];
3326
+ });
3315
3327
  return { provides: {
3316
3328
  [provideKey(declaration.id, "url")]: url,
3317
- [provideKey(declaration.id, "trustedOrigins")]: origins.join(","),
3329
+ [provideKey(declaration.id, "trustedOrigins")]: [...origins, ...schemes].join(","),
3318
3330
  ...domain ? { [provideKey(declaration.id, "cookieDomain")]: domain } : {}
3319
3331
  } };
3320
3332
  }
@@ -3337,6 +3349,7 @@ function statementsFor(options) {
3337
3349
  owner,
3338
3350
  ...reader ? { reader } : {},
3339
3351
  schema,
3352
+ ...options.ownsDatabase ? { database } : {},
3340
3353
  passwords: {
3341
3354
  runtime: derivedPassword(context, runtime),
3342
3355
  owner: derivedPassword(context, owner),
@@ -7998,11 +8011,11 @@ function routesGlob(structure) {
7998
8011
  }
7999
8012
  }
8000
8013
  /**
8001
- * Plain ids — `Database`, not `Beetlefit`.
8014
+ * Plain ids — `Database`, not `Shop`.
8002
8015
  *
8003
8016
  * The workspace `name` already scopes every physical name
8004
- * (`production-beetlefit-database`), so an id that carried the project too
8005
- * would say it twice: `production-beetlefit-beetlefit-cache`. Within a
8017
+ * (`production-shop-database`), so an id that carried the project too
8018
+ * would say it twice: `production-shop-shop-cache`. Within a
8006
8019
  * workspace the constructs are declared once, at its root, so there is no
8007
8020
  * second app's `Database` to collide with.
8008
8021
  */
@@ -8728,30 +8741,32 @@ const TOOLCHAIN_VERSIONS = {
8728
8741
  "@storybook/react-vite": "^10.6.0"
8729
8742
  };
8730
8743
  /**
8731
- * The Expo app's packages, which track an Expo SDK release (55) rather than
8732
- * their own latest: SDK 55 is tested against exactly these, React Native and
8744
+ * The Expo app's packages, which track an Expo SDK release (57) rather than
8745
+ * their own latest: SDK 57 is tested against exactly these, React Native and
8733
8746
  * React included, so they move when the SDK does.
8734
8747
  */
8735
8748
  const EXPO_VERSIONS = {
8736
- expo: "~55.0.0",
8737
- "expo-constants": "~55.0.0",
8738
- "expo-dev-client": "~55.0.0",
8739
- "expo-linking": "~55.0.0",
8740
- "expo-router": "~55.0.0",
8741
- "expo-splash-screen": "~55.0.0",
8742
- "expo-status-bar": "~55.0.0",
8743
- "expo-secure-store": "~14.2.0",
8744
- react: "19.2.0",
8745
- "react-dom": "19.2.0",
8746
- "react-native": "0.83.6",
8747
- "react-native-gesture-handler": "~2.30.0",
8748
- "react-native-reanimated": "~4.2.0",
8749
- "react-native-safe-area-context": "~5.6.0",
8750
- "react-native-screens": "~4.23.0",
8749
+ expo: "~57.0.26",
8750
+ "expo-constants": "~57.0.20",
8751
+ "expo-dev-client": "~57.0.19",
8752
+ "expo-device": "~57.0.2",
8753
+ "expo-linking": "~57.0.11",
8754
+ "expo-router": "~57.0.24",
8755
+ "expo-splash-screen": "~57.0.9",
8756
+ "expo-status-bar": "~57.0.1",
8757
+ "expo-secure-store": "~57.0.4",
8758
+ react: "19.2.3",
8759
+ "react-dom": "19.2.3",
8760
+ "react-native": "0.86.3",
8761
+ "react-native-gesture-handler": "~2.32.0",
8762
+ "react-native-reanimated": "4.5.1",
8763
+ "react-native-worklets": "0.10.1",
8764
+ "react-native-safe-area-context": "~5.7.0",
8765
+ "react-native-screens": "~4.26.0",
8751
8766
  "react-native-web": "~0.21.0",
8752
8767
  "@react-navigation/native": "^7.1.0",
8753
8768
  "@babel/core": "^7.25.0",
8754
- "@types/react": "~19.0.0",
8769
+ "@types/react": "~19.2.2",
8755
8770
  nativewind: "~4.2.0",
8756
8771
  tailwindcss: "~3.4.0"
8757
8772
  };
@@ -8783,26 +8798,26 @@ function loadPackageJson() {
8783
8798
  * Run: pnpm --filter @geekmidas/cli sync-versions
8784
8799
  */
8785
8800
  const GEEKMIDAS_VERSIONS = {
8786
- "@geekmidas/audit": "~10.0.0-alpha.33",
8787
- "@geekmidas/auth": "~10.0.0-alpha.33",
8788
- "@geekmidas/cache": "~10.0.0-alpha.33",
8789
- "@geekmidas/client": "~10.0.0-alpha.33",
8790
- "@geekmidas/cloud": "~10.0.0-alpha.33",
8791
- "@geekmidas/constructs": "~10.0.0-alpha.33",
8792
- "@geekmidas/db": "~10.0.0-alpha.33",
8793
- "@geekmidas/emailkit": "~10.0.0-alpha.33",
8794
- "@geekmidas/envkit": "~10.0.0-alpha.33",
8795
- "@geekmidas/errors": "~10.0.0-alpha.33",
8796
- "@geekmidas/events": "~10.0.0-alpha.33",
8797
- "@geekmidas/logger": "~10.0.0-alpha.33",
8798
- "@geekmidas/manifest": "~10.0.0-alpha.33",
8799
- "@geekmidas/rate-limit": "~10.0.0-alpha.33",
8800
- "@geekmidas/schema": "~10.0.0-alpha.33",
8801
- "@geekmidas/services": "~10.0.0-alpha.33",
8802
- "@geekmidas/storage": "~10.0.0-alpha.33",
8803
- "@geekmidas/studio": "~10.0.0-alpha.33",
8804
- "@geekmidas/telescope": "~10.0.0-alpha.33",
8805
- "@geekmidas/testkit": "~10.0.0-alpha.33",
8801
+ "@geekmidas/audit": "~10.0.0-alpha.35",
8802
+ "@geekmidas/auth": "~10.0.0-alpha.35",
8803
+ "@geekmidas/cache": "~10.0.0-alpha.35",
8804
+ "@geekmidas/client": "~10.0.0-alpha.35",
8805
+ "@geekmidas/cloud": "~10.0.0-alpha.35",
8806
+ "@geekmidas/constructs": "~10.0.0-alpha.35",
8807
+ "@geekmidas/db": "~10.0.0-alpha.35",
8808
+ "@geekmidas/emailkit": "~10.0.0-alpha.35",
8809
+ "@geekmidas/envkit": "~10.0.0-alpha.35",
8810
+ "@geekmidas/errors": "~10.0.0-alpha.35",
8811
+ "@geekmidas/events": "~10.0.0-alpha.35",
8812
+ "@geekmidas/logger": "~10.0.0-alpha.35",
8813
+ "@geekmidas/manifest": "~10.0.0-alpha.35",
8814
+ "@geekmidas/rate-limit": "~10.0.0-alpha.35",
8815
+ "@geekmidas/schema": "~10.0.0-alpha.35",
8816
+ "@geekmidas/services": "~10.0.0-alpha.35",
8817
+ "@geekmidas/storage": "~10.0.0-alpha.35",
8818
+ "@geekmidas/studio": "~10.0.0-alpha.35",
8819
+ "@geekmidas/telescope": "~10.0.0-alpha.35",
8820
+ "@geekmidas/testkit": "~10.0.0-alpha.35",
8806
8821
  "@geekmidas/cli": `~${loadPackageJson().version}`
8807
8822
  };
8808
8823
  //#endregion
@@ -9524,8 +9539,7 @@ function generateExpoAppFiles(options) {
9524
9539
  const apiPackage = `@${options.name}/api`;
9525
9540
  const modelsPackage = `@${options.name}/models`;
9526
9541
  const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
9527
- const scheme = slug.replace(/-/g, "");
9528
- const bundleId = `com.${scheme}.app`;
9542
+ const scheme = schemeBase(options.name);
9529
9543
  const packageJson = {
9530
9544
  name: packageName,
9531
9545
  version: "0.0.1",
@@ -9550,6 +9564,7 @@ function generateExpoAppFiles(options) {
9550
9564
  expo: EXPO_VERSIONS["expo"],
9551
9565
  "expo-constants": EXPO_VERSIONS["expo-constants"],
9552
9566
  "expo-dev-client": EXPO_VERSIONS["expo-dev-client"],
9567
+ "expo-device": EXPO_VERSIONS["expo-device"],
9553
9568
  "expo-linking": EXPO_VERSIONS["expo-linking"],
9554
9569
  "expo-router": EXPO_VERSIONS["expo-router"],
9555
9570
  "expo-secure-store": EXPO_VERSIONS["expo-secure-store"],
@@ -9561,6 +9576,7 @@ function generateExpoAppFiles(options) {
9561
9576
  "react-native": EXPO_VERSIONS["react-native"],
9562
9577
  "react-native-gesture-handler": EXPO_VERSIONS["react-native-gesture-handler"],
9563
9578
  "react-native-reanimated": EXPO_VERSIONS["react-native-reanimated"],
9579
+ "react-native-worklets": EXPO_VERSIONS["react-native-worklets"],
9564
9580
  "react-native-safe-area-context": EXPO_VERSIONS["react-native-safe-area-context"],
9565
9581
  "react-native-screens": EXPO_VERSIONS["react-native-screens"],
9566
9582
  "react-native-web": EXPO_VERSIONS["react-native-web"],
@@ -9573,27 +9589,53 @@ function generateExpoAppFiles(options) {
9573
9589
  }
9574
9590
  };
9575
9591
  const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
9592
+ import { EnvironmentParser } from '@geekmidas/envkit';
9576
9593
 
9577
9594
  /**
9578
- * Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
9579
- * at build time; everything else is read at runtime from \`extra\`.
9595
+ * What the app is built with, from the environment \`gkm\` injects — the
9596
+ * \`MobileApp\` construct's \`.dependsOn([api, auth])\` is where every value
9597
+ * here comes from, so none of it is written down.
9598
+ *
9599
+ * - \`APP_SCHEME\`: the URL scheme for this stage — \`${scheme}\` deployed,
9600
+ * \`${scheme}-dev\` locally, so a development build and the store build on
9601
+ * one phone never answer each other's links.
9602
+ * - \`EXPO_PUBLIC_API_URL\` / \`EXPO_PUBLIC_AUTH_URL\`: where the servers
9603
+ * answer. Locally that is their own port on \`localhost\`, which
9604
+ * \`config.ts\` points at the machine Metro was served from.
9605
+ *
9606
+ * Read at runtime from \`Constants.expoConfig.extra.config\` — see \`config.ts\`.
9580
9607
  */
9581
- export default function config(_context: ConfigContext): ExpoConfig {
9608
+ const config = new EnvironmentParser({ ...process.env })
9609
+ .create((get) => ({
9610
+ scheme: get('APP_SCHEME').string(),
9611
+ apiUrl: get('EXPO_PUBLIC_API_URL').string(),
9612
+ authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
9613
+ }))
9614
+ .parse();
9615
+
9616
+ export type Config = typeof config;
9617
+
9618
+ // A bundle id and an Android package allow no hyphens; one per scheme, so
9619
+ // each stage installs beside the others.
9620
+ const bundleId = \`com.\${config.scheme.replace(/[^a-z0-9]/g, '')}.app\`;
9621
+
9622
+ export default function expoConfig({ config: base }: ConfigContext): ExpoConfig {
9582
9623
  return {
9624
+ ...base,
9583
9625
  name: '${options.name}',
9584
9626
  slug: '${slug}',
9585
9627
  version: '0.0.1',
9586
9628
  orientation: 'portrait',
9587
9629
  icon: './assets/icon.png',
9588
- scheme: '${scheme}',
9630
+ scheme: config.scheme,
9589
9631
  userInterfaceStyle: 'automatic',
9590
9632
  newArchEnabled: true,
9591
9633
  ios: {
9592
9634
  supportsTablet: true,
9593
- bundleIdentifier: '${bundleId}',
9635
+ bundleIdentifier: bundleId,
9594
9636
  },
9595
9637
  android: {
9596
- package: '${bundleId}',
9638
+ package: bundleId,
9597
9639
  adaptiveIcon: {
9598
9640
  foregroundImage: './assets/adaptive-icon.png',
9599
9641
  backgroundColor: '#ffffff',
@@ -9619,6 +9661,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9619
9661
  experiments: {
9620
9662
  typedRoutes: true,
9621
9663
  },
9664
+ extra: { ...base.extra, config },
9622
9665
  };
9623
9666
  }
9624
9667
  `;
@@ -9631,16 +9674,13 @@ export default function config(_context: ConfigContext): ExpoConfig {
9631
9674
  dev: {
9632
9675
  developmentClient: true,
9633
9676
  distribution: "internal",
9634
- environment: "development",
9635
- env: {
9636
- EXPO_PUBLIC_API_URL: "http://localhost:3000",
9637
- EXPO_PUBLIC_AUTH_URL: "http://localhost:3002"
9638
- }
9677
+ environment: "development"
9639
9678
  },
9640
9679
  preview: {
9641
9680
  distribution: "internal",
9642
9681
  environment: "preview",
9643
9682
  env: {
9683
+ APP_SCHEME: scheme,
9644
9684
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9645
9685
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9646
9686
  }
@@ -9649,6 +9689,7 @@ export default function config(_context: ConfigContext): ExpoConfig {
9649
9689
  autoIncrement: true,
9650
9690
  environment: "production",
9651
9691
  env: {
9692
+ APP_SCHEME: scheme,
9652
9693
  EXPO_PUBLIC_API_URL: "https://api.example.com",
9653
9694
  EXPO_PUBLIC_AUTH_URL: "https://auth.example.com"
9654
9695
  }
@@ -9710,23 +9751,49 @@ export default {
9710
9751
  `;
9711
9752
  const expoEnvDts = `/// <reference types="expo/types" />\n`;
9712
9753
  const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
9713
- const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
9754
+ const configTs = `import Constants from 'expo-constants';
9755
+ import * as Device from 'expo-device';
9756
+ import { Platform } from 'react-native';
9757
+
9758
+ import type { Config } from './app.config.ts';
9759
+
9760
+ const built = Constants.expoConfig?.extra?.config as Config;
9761
+
9762
+ /** The host this device loaded the bundle from: Metro's, as the device sees it. */
9763
+ const metro = (Constants.expoConfig?.hostUri ?? '').split(':')[0];
9714
9764
 
9715
9765
  /**
9716
- * Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
9717
- * build time, so they're safe to read in client code.
9766
+ * A private address on the local network — what a phone on the same Wi-Fi
9767
+ * reaches this machine on. A tunnel's \`*.exp.direct\` host is not one: a
9768
+ * tunnel forwards only Metro, so the servers are not behind it.
9718
9769
  */
9719
- const envParser = new EnvironmentParser({
9720
- EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
9721
- EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
9722
- });
9770
+ const isLan = (host: string) =>
9771
+ /^10\\./.test(host) ||
9772
+ /^192\\.168\\./.test(host) ||
9773
+ /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host);
9723
9774
 
9724
- export const config = envParser
9725
- .create((get) => ({
9726
- apiUrl: get('EXPO_PUBLIC_API_URL').string(),
9727
- authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
9728
- }))
9729
- .parse();
9775
+ /** \`localhost\` from inside the Android emulator is the emulator itself. */
9776
+ const isAndroidEmulator = Platform.OS === 'android' && !Device.isDevice;
9777
+
9778
+ /**
9779
+ * A server's URL as this device reaches it.
9780
+ *
9781
+ * Locally the servers are injected as \`http://localhost:<port>\`. That is this
9782
+ * device only on the iOS simulator. The Android emulator reaches this machine
9783
+ * at \`10.0.2.2\`, and a phone at the LAN address Metro was served from.
9784
+ * Deployed URLs have no \`localhost\` in them and pass through untouched.
9785
+ */
9786
+ export function reachable(url: string): string {
9787
+ const host = isLan(metro) ? metro : isAndroidEmulator ? '10.0.2.2' : undefined;
9788
+ if (!host) return url;
9789
+ return url.replace(/\\/\\/(localhost|127\\.0\\.0\\.1)(?=[:/]|$)/, \`//\${host}\`);
9790
+ }
9791
+
9792
+ export const config = {
9793
+ scheme: built.scheme,
9794
+ apiUrl: reachable(built.apiUrl),
9795
+ authUrl: reachable(built.authUrl),
9796
+ };
9730
9797
  `;
9731
9798
  const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
9732
9799
 
@@ -9738,21 +9805,23 @@ export const queryClient = new QueryClient({
9738
9805
  },
9739
9806
  });
9740
9807
  `;
9741
- const authClientTs = `import { expoClient } from '@better-auth/expo/client';
9808
+ const authClientTs = `import { expoClient, getSetCookie } from '@better-auth/expo/client';
9742
9809
  import { magicLinkClient } from 'better-auth/client/plugins';
9743
9810
  import { createAuthClient } from 'better-auth/react';
9811
+ import * as Linking from 'expo-linking';
9744
9812
  import * as SecureStore from 'expo-secure-store';
9813
+ import { useEffect } from 'react';
9745
9814
 
9746
9815
  import { config } from '@/config.ts';
9747
9816
 
9748
- export const STORAGE_PREFIX = '${scheme}';
9817
+ export const STORAGE_PREFIX = config.scheme;
9749
9818
  export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
9750
9819
 
9751
9820
  export const authClient = createAuthClient({
9752
9821
  baseURL: config.authUrl,
9753
9822
  plugins: [
9754
9823
  expoClient({
9755
- scheme: '${scheme}',
9824
+ scheme: config.scheme,
9756
9825
  storagePrefix: STORAGE_PREFIX,
9757
9826
  storage: SecureStore,
9758
9827
  }),
@@ -9762,6 +9831,36 @@ export const authClient = createAuthClient({
9762
9831
 
9763
9832
  export const { signIn, useSession } = authClient;
9764
9833
 
9834
+ /**
9835
+ * Sign in from the emailed link.
9836
+ *
9837
+ * The link opens in the phone's browser, which is what receives the session
9838
+ * cookie — not the app. The auth server's Expo plugin carries the cookie on the
9839
+ * redirect back into the app (\`?cookie=\`), and this stores it where the
9840
+ * Expo client keeps its own, merged with what is already there, then tells the
9841
+ * client the session changed. The Expo client does this itself only for a
9842
+ * sign-in it opened (social); a link opened from mail arrives as a deep link.
9843
+ *
9844
+ * Called once, from the root layout.
9845
+ */
9846
+ export function useSessionFromLink() {
9847
+ const url = Linking.useLinkingURL();
9848
+
9849
+ useEffect(() => {
9850
+ const cookie = url ? Linking.parse(url).queryParams?.cookie : undefined;
9851
+ if (typeof cookie !== 'string') return;
9852
+
9853
+ void (async () => {
9854
+ const previous = await SecureStore.getItemAsync(COOKIE_STORE_KEY);
9855
+ await SecureStore.setItemAsync(
9856
+ COOKIE_STORE_KEY,
9857
+ getSetCookie(cookie, previous ?? undefined),
9858
+ );
9859
+ authClient.$store.notify('$sessionSignal');
9860
+ })();
9861
+ }, [url]);
9862
+ }
9863
+
9765
9864
  export async function signOut() {
9766
9865
  try {
9767
9866
  await authClient.signOut();
@@ -9828,7 +9927,7 @@ import { StatusBar } from 'expo-status-bar';
9828
9927
  import { useEffect, useMemo } from 'react';
9829
9928
  import 'react-native-reanimated';
9830
9929
 
9831
- import { authClient } from '@/lib/auth-client.ts';
9930
+ import { authClient, useSessionFromLink } from '@/lib/auth-client.ts';
9832
9931
  import { ApiProvider } from '@/lib/api-context.tsx';
9833
9932
  import { createAppApi } from '@/lib/api.ts';
9834
9933
  import { queryClient } from '@/lib/query-client.ts';
@@ -9853,6 +9952,8 @@ function AuthGate({ children }: { children: React.ReactNode }) {
9853
9952
 
9854
9953
  export default function RootLayout() {
9855
9954
  const api = useMemo(() => createAppApi(), []);
9955
+ // The emailed sign-in link comes back as a deep link carrying the session.
9956
+ useSessionFromLink();
9856
9957
 
9857
9958
  return (
9858
9959
  <QueryClientProvider client={queryClient}>
@@ -10374,7 +10475,20 @@ function rootConstructDependencies(options) {
10374
10475
  pg: DEPENDENCY_VERSIONS.pg,
10375
10476
  ...options.constructs.uploads ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
10376
10477
  ...options.constructs.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
10377
- ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
10478
+ ...options.constructs.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {},
10479
+ ...options.frontendFramework === "expo" ? { "@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"] } : {}
10480
+ };
10481
+ }
10482
+ /**
10483
+ * What `test/factories/` imports at the root: the factory and faker, and
10484
+ * Kysely's types where the root does not already install it for the
10485
+ * constructs.
10486
+ */
10487
+ function rootFactoryDependencies(options) {
10488
+ return {
10489
+ "@geekmidas/testkit": GEEKMIDAS_VERSIONS["@geekmidas/testkit"],
10490
+ "@faker-js/faker": DEPENDENCY_VERSIONS["@faker-js/faker"],
10491
+ ...options.template === "fullstack" ? {} : { kysely: DEPENDENCY_VERSIONS.kysely }
10378
10492
  };
10379
10493
  }
10380
10494
  /**
@@ -10416,6 +10530,7 @@ function generateMonorepoFiles(options, _template) {
10416
10530
  turbo: DEPENDENCY_VERSIONS["turbo"],
10417
10531
  typescript: TOOLCHAIN_VERSIONS["typescript"],
10418
10532
  vitest: TOOLCHAIN_VERSIONS["vitest"],
10533
+ ...options.constructs.database ? rootFactoryDependencies(options) : {},
10419
10534
  ...deploy.devDependencies
10420
10535
  }
10421
10536
  };
@@ -10838,6 +10953,26 @@ export const api = new RestApi('Api', {
10838
10953
  `
10839
10954
  });
10840
10955
  const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
10956
+ if (frontendFramework === "expo") files.push({
10957
+ path: "constructs/app.ts",
10958
+ content: `import { MobileApp } from '@geekmidas/constructs/mobile-app';
10959
+ import { api } from './api.ts';
10960
+ import { auth } from './auth.ts';
10961
+
10962
+ /**
10963
+ * The mobile app — a construct like a site, which is what makes it an app.
10964
+ *
10965
+ * \`.dependsOn()\` is the single fact behind what a mobile app otherwise has
10966
+ * written down by hand: its API and auth URLs, its URL scheme per stage, the
10967
+ * scheme in the auth server's trusted origins, Better Auth's Expo plugin, and
10968
+ * sign-in links a phone can open.
10969
+ */
10970
+ export const app = new MobileApp('App', { path: 'apps/app' }).dependsOn([
10971
+ api,
10972
+ auth,
10973
+ ]);
10974
+ `
10975
+ });
10841
10976
  if (variant !== void 0) files.push({
10842
10977
  path: "constructs/site.ts",
10843
10978
  content: `import { StaticSite } from '@geekmidas/constructs/site';
@@ -12044,7 +12179,7 @@ describe('example', () => {
12044
12179
  function generateTestFactoryFiles(options) {
12045
12180
  if (!options.constructs.database) return [];
12046
12181
  const db = databaseFor();
12047
- const schema = options.monorepo ? `@${options.name}/constructs/database.ts` : "../../src/constructs/database.ts";
12182
+ const schema = options.monorepo && options.template === "fullstack" ? `@${options.name}/constructs/database.ts` : options.monorepo ? `../../${options.apiPath}/src/constructs/database.ts` : "../../src/constructs/database.ts";
12048
12183
  return [{
12049
12184
  path: `test/factories/${kebabCase(db.id)}.ts`,
12050
12185
  content: `import type { Kysely } from 'kysely';
@@ -15012,7 +15147,7 @@ var NoStoredSecrets = class extends Error {
15012
15147
  }
15013
15148
  };
15014
15149
  async function reconcile(secrets, workspace, stage) {
15015
- const { derivedContainers } = await import("./workspace-BNurJoFx.mjs").then((n) => n.a);
15150
+ const { derivedContainers } = await import("./workspace-CIiGijNi.mjs").then((n) => n.a);
15016
15151
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15017
15152
  return result ? {
15018
15153
  secrets: result.secrets,
@@ -15411,7 +15546,7 @@ async function testCommand(options = {}) {
15411
15546
  * starts without the reference data the app needs.
15412
15547
  */
15413
15548
  async function migrateTestStage(workspace, env) {
15414
- const { migrateAndSeed } = await import("./migrate-D2rtGKYi.mjs");
15549
+ const { migrateAndSeed } = await import("./migrate-C-D9xIbb.mjs");
15415
15550
  const { discover } = await import("./discover-CMxesskG.mjs").then((n) => n.n);
15416
15551
  const sources = {};
15417
15552
  const manifest = await discover({
@@ -15856,7 +15991,7 @@ program.command("migrate").description("Apply each database construct's migratio
15856
15991
  try {
15857
15992
  const globalOptions = program.opts();
15858
15993
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15859
- const { migrateCommand } = await import("./migrate-D2rtGKYi.mjs");
15994
+ const { migrateCommand } = await import("./migrate-C-D9xIbb.mjs");
15860
15995
  await migrateCommand({
15861
15996
  ...options,
15862
15997
  ...construct ? { construct } : {}
@@ -15870,7 +16005,7 @@ program.command("seed").description("Migrate, then run each database construct's
15870
16005
  try {
15871
16006
  const globalOptions = program.opts();
15872
16007
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15873
- const { seedCommand } = await import("./migrate-D2rtGKYi.mjs");
16008
+ const { seedCommand } = await import("./migrate-C-D9xIbb.mjs");
15874
16009
  await seedCommand({
15875
16010
  ...options,
15876
16011
  ...construct ? { construct } : {}
@@ -15884,7 +16019,7 @@ program.command("migration").description("Write a construct's next migration: an
15884
16019
  try {
15885
16020
  const globalOptions = program.opts();
15886
16021
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15887
- const { migrationCommand } = await import("./migrate-D2rtGKYi.mjs");
16022
+ const { migrationCommand } = await import("./migrate-C-D9xIbb.mjs");
15888
16023
  await migrationCommand({
15889
16024
  construct,
15890
16025
  ...name ? { name } : {}
@@ -16052,7 +16187,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16052
16187
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16053
16188
  process.exit(1);
16054
16189
  }
16055
- const { derivedContainers } = await import("./workspace-BNurJoFx.mjs").then((n) => n.a);
16190
+ const { derivedContainers } = await import("./workspace-CIiGijNi.mjs").then((n) => n.a);
16056
16191
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16057
16192
  if (!result) {
16058
16193
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16111,7 +16246,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16111
16246
  try {
16112
16247
  const globalOptions = program.opts();
16113
16248
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16114
- const { deployGithubCommand } = await import("./github-DN-92LWP.mjs");
16249
+ const { deployGithubCommand } = await import("./github-C99BMxf0.mjs");
16115
16250
  await deployGithubCommand(options);
16116
16251
  } catch (error) {
16117
16252
  console.error(formatError(error));