@geekmidas/cli 10.0.0-alpha.29 → 10.0.0-alpha.30

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 (55) hide show
  1. package/dist/{HostingerProvider-Btxpto71.mjs → HostingerProvider-GF6NAZAK.mjs} +2 -2
  2. package/dist/{HostingerProvider-Btxpto71.mjs.map → HostingerProvider-GF6NAZAK.mjs.map} +1 -1
  3. package/dist/{openapi-K4H5tHzA.cjs → OpenApiTsGenerator-D1nhN98m.cjs} +2 -224
  4. package/dist/OpenApiTsGenerator-D1nhN98m.cjs.map +1 -0
  5. package/dist/{openapi-D-Lk5Hr5.mjs → OpenApiTsGenerator-hDINScnz.mjs} +4 -190
  6. package/dist/OpenApiTsGenerator-hDINScnz.mjs.map +1 -0
  7. package/dist/{bundler-7iaO93G8.mjs → bundler-D9IPNvDt.mjs} +2 -2
  8. package/dist/{bundler-7iaO93G8.mjs.map → bundler-D9IPNvDt.mjs.map} +1 -1
  9. package/dist/{config-Dqxa-_Ge.mjs → config-BH3Ki69F.mjs} +2 -2
  10. package/dist/{config-Dqxa-_Ge.mjs.map → config-BH3Ki69F.mjs.map} +1 -1
  11. package/dist/{config-BTFLHfRb.cjs → config-URVD4bTj.cjs} +2 -2
  12. package/dist/{config-BTFLHfRb.cjs.map → config-URVD4bTj.cjs.map} +1 -1
  13. package/dist/config.cjs +1 -1
  14. package/dist/config.mjs +1 -1
  15. package/dist/{discover-CXGlQHm1.mjs → discover-CMxesskG.mjs} +8 -2
  16. package/dist/{discover-CXGlQHm1.mjs.map → discover-CMxesskG.mjs.map} +1 -1
  17. package/dist/{discover-BTh87DOO.cjs → discover-CroTIm0K.cjs} +12 -1
  18. package/dist/{discover-BTh87DOO.cjs.map → discover-CroTIm0K.cjs.map} +1 -1
  19. package/dist/{github-ZL3RcUVD.cjs → github-B2fis34P.cjs} +2 -2
  20. package/dist/{github-ZL3RcUVD.cjs.map → github-B2fis34P.cjs.map} +1 -1
  21. package/dist/{github-DPpNfzu4.mjs → github-BrDmlm1x.mjs} +2 -2
  22. package/dist/{github-DPpNfzu4.mjs.map → github-BrDmlm1x.mjs.map} +1 -1
  23. package/dist/index.cjs +187 -455
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.mjs +181 -451
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/migrate-BuCx7tIv.cjs +498 -0
  28. package/dist/migrate-BuCx7tIv.cjs.map +1 -0
  29. package/dist/migrate-CBICVMAi.mjs +493 -0
  30. package/dist/migrate-CBICVMAi.mjs.map +1 -0
  31. package/dist/openapi.cjs +197 -7
  32. package/dist/openapi.cjs.map +1 -0
  33. package/dist/openapi.mjs +191 -1
  34. package/dist/openapi.mjs.map +1 -0
  35. package/dist/ready-CvP3JVEw.mjs +355 -0
  36. package/dist/ready-CvP3JVEw.mjs.map +1 -0
  37. package/dist/ready-DXtNfk7v.cjs +390 -0
  38. package/dist/ready-DXtNfk7v.cjs.map +1 -0
  39. package/dist/reconcile/public.cjs +1 -1
  40. package/dist/reconcile/public.mjs +1 -1
  41. package/dist/vitest.cjs +61 -0
  42. package/dist/vitest.cjs.map +1 -0
  43. package/dist/vitest.d.cts +28 -0
  44. package/dist/vitest.d.cts.map +1 -0
  45. package/dist/vitest.d.mts +28 -0
  46. package/dist/vitest.d.mts.map +1 -0
  47. package/dist/vitest.mjs +61 -0
  48. package/dist/vitest.mjs.map +1 -0
  49. package/dist/{workspace-B3ApLW6p.cjs → workspace-BhiMkRuA.cjs} +3 -3
  50. package/dist/{workspace-B3ApLW6p.cjs.map → workspace-BhiMkRuA.cjs.map} +1 -1
  51. package/dist/{workspace-DnHW8ooI.mjs → workspace-C97QUCIf.mjs} +3 -3
  52. package/dist/{workspace-DnHW8ooI.mjs.map → workspace-C97QUCIf.mjs.map} +1 -1
  53. package/package.json +24 -14
  54. package/dist/openapi-D-Lk5Hr5.mjs.map +0 -1
  55. package/dist/openapi-K4H5tHzA.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
4
- const require_discover = require("./discover-BTh87DOO.cjs");
5
- const require_config = require("./config-BTFLHfRb.cjs");
4
+ const require_discover = require("./discover-CroTIm0K.cjs");
5
+ const require_config = require("./config-URVD4bTj.cjs");
6
6
  const require_workspace = require("./workspace-Bav3TVrP.cjs");
7
7
  const require_storage = require("./storage-C2JC31V9.cjs");
8
- const require_workspace$1 = require("./workspace-B3ApLW6p.cjs");
9
- const require_openapi = require("./openapi-K4H5tHzA.cjs");
8
+ const require_workspace$1 = require("./workspace-BhiMkRuA.cjs");
9
+ const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-D1nhN98m.cjs");
10
+ const require_openapi = require("./openapi.cjs");
11
+ const require_ready = require("./ready-DXtNfk7v.cjs");
10
12
  const require_emit = require("./emit-BdoPFjZf.cjs");
11
13
  const require_reconcile = require("./reconcile-BxZTV2vl.cjs");
12
14
  let commander = require("commander");
@@ -39,10 +41,8 @@ let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
39
41
  let _geekmidas_constructs_subscribers = require("@geekmidas/constructs/subscribers");
40
42
  let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
41
43
  let node_dns_promises = require("node:dns/promises");
42
- let _geekmidas_envkit = require("@geekmidas/envkit");
43
- let _geekmidas_services = require("@geekmidas/services");
44
44
  //#region package.json
45
- var version = "10.0.0-alpha.29";
45
+ var version = "10.0.0-alpha.30";
46
46
  //#endregion
47
47
  //#region src/auth/credentials.ts
48
48
  /**
@@ -1091,6 +1091,9 @@ async function workspaceDevCommand(workspace, options) {
1091
1091
  ...rawSecrets,
1092
1092
  ...reconciled.env
1093
1093
  };
1094
+ await Promise.resolve().then(() => require("./migrate-BuCx7tIv.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1095
+ logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1096
+ });
1094
1097
  const appUrls = {};
1095
1098
  for (const resource of reconciled.plan.resources) {
1096
1099
  if (resource.kind !== "rest-api" && resource.kind !== "site") continue;
@@ -1784,7 +1787,7 @@ export async function stopCrons(): Promise<void> {
1784
1787
  const handlerFileName = `${exportName}.ts`;
1785
1788
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
1786
1789
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1787
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1790
+ const runtime = require_OpenApiTsGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1788
1791
  const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
1789
1792
  import { ${exportName} } from '${importPath}';
1790
1793
  ${runtime.imports}
@@ -1799,111 +1802,6 @@ export const handler = adapter.handler;
1799
1802
  }
1800
1803
  };
1801
1804
  //#endregion
1802
- //#region src/generators/drivers.ts
1803
- /**
1804
- * Driver registration for generated entry points.
1805
- *
1806
- * A construct declares that it needs object storage and is handed one URL; the
1807
- * scheme in that URL picks the driver that builds the client. Which drivers
1808
- * exist is therefore the *entry point's* decision, not the construct's and not
1809
- * the application's — an app that never talks to S3 should never resolve the AWS
1810
- * SDK, and application code that registers a driver has named a provider in the
1811
- * one layer this design keeps provider-free.
1812
- *
1813
- * So the generated entries register: `gkm dev`'s server for local development,
1814
- * and each Lambda handler for its own target. Registration is an explicit call
1815
- * rather than an import side effect, because a side-effecting module is exactly
1816
- * what a bundler is entitled to drop.
1817
- */
1818
- /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
1819
- const S3 = {
1820
- imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
1821
- setup: "registerStorageDriver(s3Driver);"
1822
- };
1823
- const NONE = {
1824
- imports: "",
1825
- setup: ""
1826
- };
1827
- /**
1828
- * The cache driver for a backend — exactly one, never all three.
1829
- *
1830
- * Keyed off the *backend* rather than off a dependency, because unlike storage
1831
- * the backend already says which driver is right: a project caching in Postgres
1832
- * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
1833
- * neither. That is the whole reason the drivers live behind separate subpaths.
1834
- *
1835
- * Both HTTP schemes are registered for Upstash, because the local proxy speaks
1836
- * `http://` and Upstash speaks `https://`, and an entry that registered one
1837
- * would work in exactly one of the two places.
1838
- */
1839
- const CACHE_DRIVERS = {
1840
- upstash: {
1841
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
1842
- setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
1843
- },
1844
- elasticache: {
1845
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
1846
- setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
1847
- },
1848
- db: {
1849
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
1850
- setup: "registerCacheDriver(postgresCacheDriver);"
1851
- }
1852
- };
1853
- /**
1854
- * Everything a generated entry should register: object storage, and the cache.
1855
- *
1856
- * Merged here rather than threaded separately, because a generated file has one
1857
- * import block and one setup block whatever fills them.
1858
- */
1859
- function driversFor(options) {
1860
- const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
1861
- const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[require_discover.DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
1862
- if (parts.length === 0) return NONE;
1863
- return {
1864
- imports: parts.map((part) => part.imports).join("\n"),
1865
- setup: parts.map((part) => part.setup).join("\n")
1866
- };
1867
- }
1868
- /**
1869
- * Which cache backends an app's entry actually has to speak.
1870
- *
1871
- * The config answers this for a cache that named nowhere, and the *declaration*
1872
- * answers it for one that named a database — `orders.cache('Sessions')` is a
1873
- * statement about the application, so a deployment cannot move that cache and
1874
- * the entry cannot register a driver for somewhere else.
1875
- *
1876
- * Reading only the config is how an entry ends up registering the Upstash
1877
- * driver for a URL the target composed as `postgres://`, which fails at the
1878
- * first request with `UnregisteredCacheScheme` and a stack that points at the
1879
- * cache rather than at the config that disagreed.
1880
- */
1881
- function cacheBackendsIn(manifest, configured) {
1882
- const caches = Object.values(manifest).filter((d) => d.kind === "cache");
1883
- return [...new Set(caches.map((c) => c.of ? "db" : configured))];
1884
- }
1885
- /**
1886
- * The drivers an app's entry points should register.
1887
- *
1888
- * Keyed off the dependency rather than off a declared bucket, because the entry
1889
- * is generated before discovery has run on the watcher's rebuild path — and an
1890
- * app that installed `@geekmidas/storage` has already paid for the SDK it would
1891
- * otherwise resolve lazily.
1892
- */
1893
- function storageDriversFor(appRoot) {
1894
- return dependsOnStorage(appRoot) ? S3 : NONE;
1895
- }
1896
- function dependsOnStorage(appRoot) {
1897
- const manifest = (0, node_path.join)(appRoot, "package.json");
1898
- if (!(0, node_fs.existsSync)(manifest)) return false;
1899
- try {
1900
- const pkg = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
1901
- return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
1902
- } catch {
1903
- return false;
1904
- }
1905
- }
1906
- //#endregion
1907
1805
  //#region src/generators/FunctionGenerator.ts
1908
1806
  var FunctionGenerator = class extends require_discover.ConstructGenerator {
1909
1807
  isConstruct(value) {
@@ -1934,7 +1832,7 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
1934
1832
  const handlerFileName = `${exportName}.ts`;
1935
1833
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
1936
1834
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1937
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1835
+ const runtime = require_OpenApiTsGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1938
1836
  const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
1939
1837
  import { ${exportName} } from '${importPath}';
1940
1838
  ${runtime.imports}
@@ -1993,7 +1891,7 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
1993
1891
  async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
1994
1892
  const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
1995
1893
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
1996
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1894
+ const runtime = require_OpenApiTsGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
1997
1895
  const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
1998
1896
  import { ${exportName} } from '${importPath}';
1999
1897
  ${runtime.imports}
@@ -2173,7 +2071,7 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
2173
2071
  const handlerFileName = `${exportName}.ts`;
2174
2072
  const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
2175
2073
  const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
2176
- const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
2074
+ const runtime = require_OpenApiTsGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
2177
2075
  const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
2178
2076
  import { ${exportName} } from '${importPath}';
2179
2077
  ${runtime.imports}
@@ -2827,13 +2725,13 @@ async function buildApp(input) {
2827
2725
  constructGlobs,
2828
2726
  cacheBackend,
2829
2727
  emailBackend: "ses",
2830
- storageDrivers: driversFor({
2728
+ storageDrivers: require_ready.driversFor({
2831
2729
  appRoot,
2832
- cache: cacheBackendsIn(declared, cacheBackend)
2730
+ cache: require_ready.cacheBackendsIn(declared, cacheBackend)
2833
2731
  }),
2834
2732
  markOptional: input.markOptional ?? false
2835
2733
  };
2836
- const endpointGenerator = new require_openapi.EndpointGenerator();
2734
+ const endpointGenerator = new require_OpenApiTsGenerator.EndpointGenerator();
2837
2735
  const functionGenerator = new FunctionGenerator();
2838
2736
  const cronGenerator = new CronGenerator();
2839
2737
  const subscriberGenerator = new SubscriberGenerator();
@@ -8060,6 +7958,22 @@ function printStateDetails(state) {
8060
7958
  }
8061
7959
  }
8062
7960
  //#endregion
7961
+ //#region src/migrate/names.ts
7962
+ /**
7963
+ * What a migration file is called: a UTC timestamp, then a name.
7964
+ *
7965
+ * Kept apart from the commands so the scaffold can name the migration it
7966
+ * writes the same way `gkm migration` does, without loading a migrator.
7967
+ */
7968
+ /** UTC `YYYYMMDDHHmmss` — sorts as text, and reads as a date. */
7969
+ function stamp(now = /* @__PURE__ */ new Date()) {
7970
+ return now.toISOString().replace(/\.\d+Z$/, "").replace(/\D/g, "");
7971
+ }
7972
+ /** `Add workouts` → `add_workouts`: the part of a file name after its stamp. */
7973
+ function slug(name) {
7974
+ return name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
7975
+ }
7976
+ //#endregion
8063
7977
  //#region src/init/constructs.ts
8064
7978
  /**
8065
7979
  * What a scaffolded project declares, and the keys that derive from it.
@@ -8176,7 +8090,7 @@ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
8176
8090
  */
8177
8091
  function usersMigration() {
8178
8092
  return {
8179
- path: "src/db/migrations/001_create_users.ts",
8093
+ path: `${(0, _geekmidas_manifest.migrationFolder)(databaseFor().id)}/${stamp()}_create_users.ts`,
8180
8094
  content: `import type { Kysely } from 'kysely';
8181
8095
 
8182
8096
  export async function up(db: Kysely<unknown>): Promise<void> {
@@ -8339,7 +8253,7 @@ environment and fail in a way that looks like a config bug. Go through
8339
8253
  \`gkm exec --\`, which is what injects them.`);
8340
8254
  if (!isWorker) sections.push(endpointSection(options));
8341
8255
  if (isWorker || isServerless) sections.push(backgroundSection(template));
8342
- if (database) sections.push(databaseSection(), queriesSection());
8256
+ if (database) sections.push(databaseSection(), migrationsSection(), queriesSection());
8343
8257
  sections.push(`## Environment
8344
8258
 
8345
8259
  Environment is parsed once, through \`envkit\`, and never read from
@@ -8390,7 +8304,7 @@ function layoutBlock(options, template) {
8390
8304
  const { database } = options.constructs;
8391
8305
  const isWorker = template.name === "worker";
8392
8306
  if (monorepo) return `constructs/ # every construct: the surface, the database, topics
8393
- ${apiPath}/ # the API app
8307
+ ${database ? "db/<construct>/ # migrations, one folder per database construct\n" : ""}${apiPath}/ # the API app
8394
8308
  gkm.config.ts # name, constructs glob, secrets`;
8395
8309
  const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
8396
8310
  if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
@@ -8398,7 +8312,7 @@ gkm.config.ts # name, constructs glob, secrets`;
8398
8312
  lines.push("src/crons/ # scheduled work");
8399
8313
  lines.push("src/subscribers/ # topic consumers");
8400
8314
  }
8401
- if (database) lines.push("src/db/ # migrations, schema");
8315
+ if (database) lines.push("db/<construct>/ # migrations, one folder per database construct");
8402
8316
  lines.push("gkm.config.ts # name, constructs glob, secrets");
8403
8317
  return lines.join("\n");
8404
8318
  }
@@ -8526,6 +8440,38 @@ export const monthly = router
8526
8440
  Queries go through Kysely. Never open your own connection pool — the one the
8527
8441
  construct provides is the one that gets credentials, pooling and shutdown.`;
8528
8442
  }
8443
+ function migrationsSection() {
8444
+ return `## Migrations
8445
+
8446
+ Each database construct has its own folder, named after the construct:
8447
+ \`Database\` is \`db/database/\`, a tenant \`AuthDatabase\` is
8448
+ \`db/auth-database/\`. Each keeps its own history, in its own schema, and is
8449
+ migrated as its own **owner** role — the role a handler runs as cannot create a
8450
+ table.
8451
+
8452
+ \`\`\`bash
8453
+ gkm migration database add_workouts # db/database/20260930143012_add_workouts.ts
8454
+ gkm migration auth # Better Auth's schema change, as SQL
8455
+ gkm migrate # apply every folder, parents first
8456
+ gkm migrate database # apply one construct's
8457
+ \`\`\`
8458
+
8459
+ - Files are \`<UTC timestamp>_<name>.ts\` exporting \`up\` (and \`down\`), or
8460
+ \`.sql\`, which runs whole. Never rename or edit one that has been applied —
8461
+ it is recorded by name. Change the schema with a new migration.
8462
+ - Write migrations with \`Kysely<unknown>\` and snake_case names: they are the
8463
+ SQL, and the camelCase plugin is the app's, not theirs.
8464
+ - \`gkm migration auth\` compares Better Auth's schema to its tenant and writes
8465
+ only the difference. Run it after adding a Better Auth plugin, and commit the
8466
+ file.
8467
+ - Tests migrate themselves: the root vitest config's
8468
+ \`globalSetup: ['@geekmidas/cli/vitest']\` applies every folder to the test
8469
+ stage before any test runs. \`gkm dev\` only reports what is pending.
8470
+ - A deploy migrates before the new version goes live, so the old version runs
8471
+ against the new schema until it is replaced. Keep each migration compatible
8472
+ with the code before it: add a column in one release, stop using the old one,
8473
+ drop it in a later release.`;
8474
+ }
8529
8475
  function queriesSection() {
8530
8476
  return `## Writing queries and relations
8531
8477
 
@@ -8675,7 +8621,6 @@ const DEPENDENCY_VERSIONS = {
8675
8621
  hono: "~4.13.9",
8676
8622
  "@hono/node-server": "~1.19.17",
8677
8623
  kysely: "~0.29.6",
8678
- "kysely-ctl": "~0.21.0",
8679
8624
  pg: "~8.23.0",
8680
8625
  pino: "~10.3.1",
8681
8626
  zod: "~4.6.5",
@@ -8782,26 +8727,26 @@ function loadPackageJson() {
8782
8727
  * Run: pnpm --filter @geekmidas/cli sync-versions
8783
8728
  */
8784
8729
  const GEEKMIDAS_VERSIONS = {
8785
- "@geekmidas/audit": "~10.0.0-alpha.29",
8786
- "@geekmidas/auth": "~10.0.0-alpha.29",
8787
- "@geekmidas/cache": "~10.0.0-alpha.29",
8788
- "@geekmidas/client": "~10.0.0-alpha.29",
8789
- "@geekmidas/cloud": "~10.0.0-alpha.29",
8790
- "@geekmidas/constructs": "~10.0.0-alpha.29",
8791
- "@geekmidas/db": "~10.0.0-alpha.29",
8792
- "@geekmidas/emailkit": "~10.0.0-alpha.29",
8793
- "@geekmidas/envkit": "~10.0.0-alpha.29",
8794
- "@geekmidas/errors": "~10.0.0-alpha.29",
8795
- "@geekmidas/events": "~10.0.0-alpha.29",
8796
- "@geekmidas/logger": "~10.0.0-alpha.29",
8797
- "@geekmidas/manifest": "~10.0.0-alpha.29",
8798
- "@geekmidas/rate-limit": "~10.0.0-alpha.29",
8799
- "@geekmidas/schema": "~10.0.0-alpha.29",
8800
- "@geekmidas/services": "~10.0.0-alpha.29",
8801
- "@geekmidas/storage": "~10.0.0-alpha.29",
8802
- "@geekmidas/studio": "~10.0.0-alpha.29",
8803
- "@geekmidas/telescope": "~10.0.0-alpha.29",
8804
- "@geekmidas/testkit": "~10.0.0-alpha.29",
8730
+ "@geekmidas/audit": "~10.0.0-alpha.30",
8731
+ "@geekmidas/auth": "~10.0.0-alpha.30",
8732
+ "@geekmidas/cache": "~10.0.0-alpha.30",
8733
+ "@geekmidas/client": "~10.0.0-alpha.30",
8734
+ "@geekmidas/cloud": "~10.0.0-alpha.30",
8735
+ "@geekmidas/constructs": "~10.0.0-alpha.30",
8736
+ "@geekmidas/db": "~10.0.0-alpha.30",
8737
+ "@geekmidas/emailkit": "~10.0.0-alpha.30",
8738
+ "@geekmidas/envkit": "~10.0.0-alpha.30",
8739
+ "@geekmidas/errors": "~10.0.0-alpha.30",
8740
+ "@geekmidas/events": "~10.0.0-alpha.30",
8741
+ "@geekmidas/logger": "~10.0.0-alpha.30",
8742
+ "@geekmidas/manifest": "~10.0.0-alpha.30",
8743
+ "@geekmidas/rate-limit": "~10.0.0-alpha.30",
8744
+ "@geekmidas/schema": "~10.0.0-alpha.30",
8745
+ "@geekmidas/services": "~10.0.0-alpha.30",
8746
+ "@geekmidas/storage": "~10.0.0-alpha.30",
8747
+ "@geekmidas/studio": "~10.0.0-alpha.30",
8748
+ "@geekmidas/telescope": "~10.0.0-alpha.30",
8749
+ "@geekmidas/testkit": "~10.0.0-alpha.30",
8805
8750
  "@geekmidas/cli": `~${loadPackageJson().version}`
8806
8751
  };
8807
8752
  //#endregion
@@ -8819,9 +8764,7 @@ function generateAuthAppFiles(options) {
8819
8764
  type: "module",
8820
8765
  scripts: {
8821
8766
  dev: "gkm dev",
8822
- build: "gkm build",
8823
- "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
8824
- "db:generate": "gkm exec -- npx @better-auth/cli generate"
8767
+ build: "gkm build"
8825
8768
  },
8826
8769
  dependencies: {
8827
8770
  [`@${options.name}/models`]: "workspace:*",
@@ -8901,16 +8844,24 @@ function stageEnv(stages) {
8901
8844
  //#endregion
8902
8845
  //#region src/init/generators/config.ts
8903
8846
  /**
8904
- * Vitest config content with globalSetup for database-enabled apps
8847
+ * Vitest config for a database-enabled app.
8848
+ *
8849
+ * On its own, it is the root config and carries gkm's setup: the test stage
8850
+ * reconciled and every construct migrated, however the suite starts. In a
8851
+ * workspace the root config carries it instead — once, so no project filter
8852
+ * can skip it and nothing runs it twice.
8905
8853
  */
8906
- const vitestConfigContent = `import { defineConfig } from 'vitest/config';
8854
+ const vitestConfigFor = (monorepo) => `import { defineConfig } from 'vitest/config';
8907
8855
 
8908
8856
  export default defineConfig({
8909
8857
  // Vite resolves tsconfig \`paths\` itself; no plugin needed.
8910
8858
  resolve: { tsconfigPaths: true },
8911
8859
  test: {
8912
- environment: 'node',
8913
- globalSetup: './test/globalSetup.ts',
8860
+ environment: 'node',${monorepo ? "" : `
8861
+ // The test stage, reconciled and every database construct's migrations
8862
+ // applied, before any test runs — whether \`gkm test\` or plain
8863
+ // \`vitest\` started it.
8864
+ globalSetup: ['@geekmidas/cli/vitest'],`}
8914
8865
  },
8915
8866
  });
8916
8867
  `;
@@ -8988,7 +8939,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8988
8939
  }];
8989
8940
  if (options.constructs.database) files.push({
8990
8941
  path: "vitest.config.ts",
8991
- content: vitestConfigContent
8942
+ content: vitestConfigFor(Boolean(options.monorepo))
8992
8943
  });
8993
8944
  return files;
8994
8945
  }
@@ -9078,7 +9029,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
9078
9029
  ];
9079
9030
  if (options.constructs.database) files.push({
9080
9031
  path: "vitest.config.ts",
9081
- content: vitestConfigContent
9032
+ content: vitestConfigFor(Boolean(options.monorepo))
9082
9033
  });
9083
9034
  return files;
9084
9035
  }
@@ -9114,7 +9065,7 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
9114
9065
  }];
9115
9066
  if (options.constructs.database) files.push({
9116
9067
  path: "vitest.config.ts",
9117
- content: vitestConfigContent
9068
+ content: vitestConfigFor(Boolean(options.monorepo))
9118
9069
  });
9119
9070
  return files;
9120
9071
  }
@@ -10561,8 +10512,12 @@ ${isSst ? "\n# SST\n.sst/\n" : ""}`;
10561
10512
 
10562
10513
  export default defineConfig({
10563
10514
  test: {
10564
- // Each app and package is its own project, so its own config — an app's
10565
- // \`globalSetup\`, its path aliases — applies to its own tests.
10515
+ // The test stage, reconciled and every database construct's migrations
10516
+ // applied, before any project's tests start — declared here, once, so no
10517
+ // project filter can skip it and nothing runs it twice.
10518
+ globalSetup: ['@geekmidas/cli/vitest'],
10519
+ // Each app and package is its own project, so its own config — its path
10520
+ // aliases — applies to its own tests.
10566
10521
  projects: ['apps/*', 'packages/*'],
10567
10522
  // The shared packages ship without tests of their own.
10568
10523
  passWithNoTests: true,
@@ -10780,6 +10735,7 @@ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
10780
10735
  export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb');
10781
10736
  `
10782
10737
  });
10738
+ files.push(usersMigration());
10783
10739
  files.push({
10784
10740
  path: "constructs/auth.ts",
10785
10741
  content: `import { BetterAuth } from '@geekmidas/constructs/auth';
@@ -11168,7 +11124,6 @@ export const router = api${options.constructs.database ? ".database(database)" :
11168
11124
  `
11169
11125
  });
11170
11126
  if (options.constructs.database && declares) files.push(...databaseFiles());
11171
- else if (options.constructs.database) files.push(usersMigration());
11172
11127
  if (uploads && declares) files.push({
11173
11128
  path: "src/constructs/storage.ts",
11174
11129
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -11936,7 +11891,6 @@ function generatePackageJson(options, template) {
11936
11891
  devDependencies["@types/pg"] = DEPENDENCY_VERSIONS["@types/pg"];
11937
11892
  devDependencies["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
11938
11893
  devDependencies["@faker-js/faker"] = DEPENDENCY_VERSIONS["@faker-js/faker"];
11939
- devDependencies["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
11940
11894
  }
11941
11895
  if (monorepo) {
11942
11896
  delete devDependencies["@biomejs/biome"];
@@ -11987,38 +11941,16 @@ function generateSourceFiles(options, template) {
11987
11941
  //#region src/init/generators/test.ts
11988
11942
  /**
11989
11943
  * Generate test infrastructure files when database is enabled.
11990
- * Includes transaction-isolated test config, global setup with migrations,
11944
+ * Includes transaction-isolated test config,
11991
11945
  * factory system with builders/seeds, and an example spec.
11992
11946
  */
11993
11947
  function generateTestFiles(options, _template) {
11994
11948
  if (!options.constructs.database) return [];
11995
11949
  const db = databaseFor();
11996
11950
  const schema = !options.monorepo ? "../src/constructs/database.ts" : `@${options.name}/constructs/database.ts`;
11997
- const runtimeUrl = db.urlKey;
11998
- const ownerUrl = db.ownerUrlKey;
11951
+ db.urlKey;
11952
+ db.ownerUrlKey;
11999
11953
  return [
12000
- {
12001
- path: "kysely.config.ts",
12002
- content: `import { Credentials } from '@geekmidas/envkit/credentials';
12003
- import { PostgresDialect } from 'kysely';
12004
- import { defineConfig } from 'kysely-ctl';
12005
- import pg from 'pg';
12006
-
12007
- // The owner role's URL — the one that may create, alter, and drop. Both keys
12008
- // are published by the declared database construct; run this under
12009
- // \`gkm exec -- pnpm kysely migrate:latest\` so they are injected.
12010
- const url = Credentials.${ownerUrl} ?? Credentials.${runtimeUrl};
12011
-
12012
- export default defineConfig({
12013
- dialect: new PostgresDialect({
12014
- pool: new pg.Pool({ connectionString: url }),
12015
- }),
12016
- migrations: {
12017
- migrationFolder: './src/db/migrations',
12018
- },
12019
- });
12020
- `
12021
- },
12022
11954
  {
12023
11955
  path: "test/config.ts",
12024
11956
  content: `import { it as itVitest } from 'vitest';
@@ -12030,44 +11962,6 @@ export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
12030
11962
  // the plugins it was given, \`CamelCasePlugin\` included.
12031
11963
  connection: database,
12032
11964
  });
12033
- `
12034
- },
12035
- {
12036
- path: "test/globalSetup.ts",
12037
- content: `import fs from 'node:fs/promises';
12038
- import path from 'node:path';
12039
- import { Credentials } from '@geekmidas/envkit/credentials';
12040
- import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
12041
- import { Kysely, PostgresDialect } from 'kysely';
12042
- import { FileMigrationProvider } from 'kysely/migration';
12043
- import pg from 'pg';
12044
-
12045
- export default async function globalSetup() {
12046
- // \`gkm test\` reconciles the test stage before this runs: the container is
12047
- // up, the database exists, and its roles are created. What is left is the
12048
- // schema, which is what migrations are for.
12049
- const databaseUrl = Credentials.${ownerUrl} ?? Credentials.${runtimeUrl}!;
12050
- const migrationFolder = path.resolve(import.meta.dirname, '../src/db/migrations');
12051
-
12052
- const db = new Kysely({
12053
- dialect: new PostgresDialect({
12054
- pool: new pg.Pool({ connectionString: databaseUrl }),
12055
- }),
12056
- });
12057
-
12058
- const migrator = new PostgresKyselyMigrator({
12059
- uri: databaseUrl,
12060
- db,
12061
- provider: new FileMigrationProvider({
12062
- fs,
12063
- path,
12064
- migrationFolder,
12065
- }),
12066
- });
12067
-
12068
- const teardown = await migrator.start();
12069
- return teardown;
12070
- }
12071
11965
  `
12072
11966
  },
12073
11967
  {
@@ -14695,10 +14589,14 @@ function printNextSteps(projectName, options, pkgManager) {
14695
14589
  console.log("Next steps:\n");
14696
14590
  console.log(` ${cdCommand}`);
14697
14591
  console.log(` ${devCommand}`);
14698
- if (!options.monorepo && options.constructs.database) {
14592
+ if (options.constructs.database) {
14699
14593
  console.log("");
14700
- console.log(" # Then, once the container is up:");
14701
- console.log(` gkm exec -- pnpm kysely migrate:latest`);
14594
+ console.log(" # Apply each database construct's db/<construct>/ migrations:");
14595
+ console.log(" gkm migrate");
14596
+ if (options.monorepo) {
14597
+ console.log(" # Better Auth's tables, as its tenant's first migration:");
14598
+ console.log(" gkm migration auth && gkm migrate");
14599
+ }
14702
14600
  }
14703
14601
  console.log("");
14704
14602
  if (options.monorepo) {
@@ -15050,7 +14948,7 @@ var NoStoredSecrets = class extends Error {
15050
14948
  }
15051
14949
  };
15052
14950
  async function reconcile(secrets, workspace, stage) {
15053
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-B3ApLW6p.cjs")).then((n) => n.workspace_exports);
14951
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-BhiMkRuA.cjs")).then((n) => n.workspace_exports);
15054
14952
  const result = require_reconcile.reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15055
14953
  return result ? {
15056
14954
  secrets: result.secrets,
@@ -15349,231 +15247,6 @@ function printSummary(workspace, stage) {
15349
15247
  logger$1.log("");
15350
15248
  }
15351
15249
  //#endregion
15352
- //#region src/test/harness.ts
15353
- /**
15354
- * What `gkm test` writes for a feature test to be built from.
15355
- *
15356
- * `gkm test` already discovers an app's constructs and resolves the test
15357
- * stage's environment; this keeps both, under `.gkm/test/`:
15358
- *
15359
- * - `manifest.json` — which construct and endpoint is exported where, and the
15360
- * environment the test stage resolved, keyed as the constructs derive keys.
15361
- * - `clients/<surface>.ts` — each surface's typed client, from the same
15362
- * generator `gkm build` uses for the app's own.
15363
- * - `index.ts` — the configured `it`, and a `Browser` holding one client per
15364
- * surface and one per auth server, each on the browser's `fetch`.
15365
- *
15366
- * A test imports `it` from there and declares nothing — not a construct, not an
15367
- * environment key, not a client.
15368
- */
15369
- /**
15370
- * The variable the manifest's path travels in — what
15371
- * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
15372
- * that entry carries the whole harness (MSW, Hono, Vitest), and importing it
15373
- * here put all of it on the start-up path of every `gkm` command.
15374
- */
15375
- const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
15376
- /** Where the harness lives, relative to the workspace root. */
15377
- const TEST_HARNESS_DIR = ".gkm/test";
15378
- /**
15379
- * Better-auth's client plugin for each server plugin id.
15380
- *
15381
- * Written down rather than derived: the ids and the client names do not follow
15382
- * one rule (`admin` pairs with `adminClient`, `email-otp` with
15383
- * `emailOTPClient`), and a guess that missed would hand a test a client
15384
- * silently lacking every method of that plugin. A server plugin not listed has
15385
- * no client counterpart — `bearer`, `open-api` — or needs one from another
15386
- * package, and is left out.
15387
- */
15388
- const CLIENT_PLUGINS = {
15389
- admin: "adminClient",
15390
- anonymous: "anonymousClient",
15391
- "device-authorization": "deviceAuthorizationClient",
15392
- "email-otp": "emailOTPClient",
15393
- jwt: "jwtClient",
15394
- "last-login-method": "lastLoginMethodClient",
15395
- "magic-link": "magicLinkClient",
15396
- "multi-session": "multiSessionClient",
15397
- "oauth-popup": "oauthPopupClient",
15398
- "one-tap": "oneTapClient",
15399
- "one-time-token": "oneTimeTokenClient",
15400
- organization: "organizationClient",
15401
- "phone-number": "phoneNumberClient",
15402
- siwe: "siweClient",
15403
- "two-factor": "twoFactorClient",
15404
- username: "usernameClient"
15405
- };
15406
- /** Write the manifest, the clients and the harness; return the manifest path. */
15407
- async function writeTestHarness(options) {
15408
- const sources = {};
15409
- const declared = await require_discover.discover({
15410
- patterns: options.patterns,
15411
- cwd: options.root,
15412
- sources
15413
- });
15414
- const endpoints = (await new require_openapi.EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
15415
- surface: construct.owner ?? construct.surface?.id,
15416
- construct,
15417
- source: {
15418
- file: path.absolute,
15419
- export: key
15420
- }
15421
- })).filter((endpoint) => endpoint.surface !== void 0);
15422
- const manifest = {
15423
- stage: options.stage,
15424
- constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
15425
- const source = sources[id];
15426
- return source ? [[id, {
15427
- kind: declaration.kind,
15428
- source: {
15429
- file: source.file,
15430
- export: source.exportName
15431
- }
15432
- }]] : [];
15433
- })),
15434
- endpoints: endpoints.map(({ surface, source }) => ({
15435
- surface,
15436
- source
15437
- })),
15438
- env: options.env
15439
- };
15440
- const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
15441
- const generator = new require_openapi.OpenApiTsGenerator();
15442
- const clients = await Promise.all(surfaces.map(async (surface) => ({
15443
- file: `${(0, _geekmidas_manifest.kebabCase)(surface)}.ts`,
15444
- content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
15445
- title: surface,
15446
- version: "1.0.0"
15447
- })
15448
- })));
15449
- const auths = await authClients(declared, sources, options.env);
15450
- const drivers = driversFor({
15451
- appRoot: options.root,
15452
- cache: cacheBackendsIn(declared, options.cacheBackend)
15453
- });
15454
- const database = await databaseSource(declared, sources, endpoints.map(({ construct }) => construct));
15455
- const files = [.../* @__PURE__ */ new Set([...Object.values(manifest.constructs).map(({ source }) => source.file), ...manifest.endpoints.map(({ source }) => source.file)])];
15456
- const harnessFor = (dir) => harnessModule({
15457
- dir,
15458
- files,
15459
- database,
15460
- surfaces: surfaces.map((id, index) => ({
15461
- id,
15462
- secured: clients[index].content.includes("authStrategies:")
15463
- })),
15464
- auths,
15465
- drivers
15466
- });
15467
- const json = `${JSON.stringify(manifest, null, 2)}\n`;
15468
- return (await Promise.all([...new Set(options.targets)].map(async (target) => {
15469
- const dir = (0, node_path.join)(target, TEST_HARNESS_DIR);
15470
- await (0, node_fs_promises.rm)(dir, {
15471
- recursive: true,
15472
- force: true
15473
- });
15474
- await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "clients"), { recursive: true });
15475
- await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "manifest.json"), json);
15476
- await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "index.ts"), harnessFor(dir));
15477
- for (const client of clients) await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "clients", client.file), client.content);
15478
- return (0, node_path.join)(dir, "manifest.json");
15479
- })))[0];
15480
- }
15481
- /** Each auth server, with the client plugins its server plugins pair with. */
15482
- async function authClients(declared, sources, env) {
15483
- const clients = [];
15484
- const envParser = new _geekmidas_envkit.EnvironmentParser({ ...env });
15485
- for (const [id, declaration] of Object.entries(declared)) {
15486
- const source = sources[id];
15487
- if (declaration.kind !== "rest-api" || !source) continue;
15488
- const exported = (await import(source.file))[source.exportName];
15489
- if (!isBetterAuth(exported) || exported.id !== id) continue;
15490
- const ids = await exported.pluginIds({
15491
- envParser,
15492
- context: _geekmidas_services.serviceContext
15493
- });
15494
- clients.push({
15495
- id,
15496
- basePath: exported.basePath,
15497
- plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? [])
15498
- });
15499
- }
15500
- return clients;
15501
- }
15502
- function isBetterAuth(value) {
15503
- return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
15504
- }
15505
- /** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
15506
- function propertyOf(id) {
15507
- return id.charAt(0).toLowerCase() + id.slice(1);
15508
- }
15509
- /**
15510
- * The database a test's `db` is — the one the endpoints were given with
15511
- * `.database()`, found by the service they hold, which is the construct's own.
15512
- * The same rule `featureTest` applies at runtime, applied here so `db` can be
15513
- * typed by the construct's schema.
15514
- */
15515
- async function databaseSource(declared, sources, endpoints) {
15516
- const services = new Set(endpoints.map((endpoint) => endpoint.databaseService).filter(Boolean));
15517
- if (services.size === 0) return void 0;
15518
- for (const [id, declaration] of Object.entries(declared)) {
15519
- const source = sources[id];
15520
- if (!source || !declaration.kind.startsWith("database")) continue;
15521
- const exported = (await import(source.file))[source.exportName];
15522
- if (services.has(exported?.service)) return source;
15523
- }
15524
- }
15525
- /** An import specifier for `file`, from the harness written into `dir`. */
15526
- function specifierFrom(dir, file) {
15527
- const path = (0, node_path.relative)(dir, file).replace(/\.tsx?$/, ".js");
15528
- return path.startsWith(".") ? path : `./${path}`;
15529
- }
15530
- function harnessModule(options) {
15531
- const { surfaces, auths, drivers, database, dir, files } = options;
15532
- const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
15533
- const imports = [
15534
- `import {${database ? " type DatabaseOf," : ""} featureTest, loadTestManifest } from '@geekmidas/constructs/testing';`,
15535
- ...database ? [`import type { ${database.exportName} as __database } from '${specifierFrom(dir, database.file)}';`] : [],
15536
- `import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
15537
- ...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${(0, _geekmidas_manifest.kebabCase)(id)}.js';`),
15538
- ...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
15539
- ...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
15540
- ...drivers.imports ? [drivers.imports] : [],
15541
- ...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
15542
- ];
15543
- const url = (id) => `manifest.env[${JSON.stringify((0, _geekmidas_manifest.provideKey)(id, "url"))}]!`;
15544
- const members = [...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${(0, _geekmidas_manifest.provideKey)(id, "url")}. */
15545
- readonly ${propertyOf(id)} = create${id}({
15546
- baseURL: ${url(id)},
15547
- fetch: this.fetch,${secured ? `
15548
- // Served in-process: no authorizer runs, so nothing is signed. A scheme
15549
- // with no strategy sends its request without credentials.
15550
- authStrategies: {} as never,` : ""}
15551
- });`), ...auths.map((auth) => ` /** \`${auth.id}\`, at ${(0, _geekmidas_manifest.provideKey)(auth.id, "url")}. */
15552
- readonly ${propertyOf(auth.id)} = createAuthClient({
15553
- baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
15554
- plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
15555
- fetchOptions: { customFetchImpl: this.fetch },
15556
- });`)];
15557
- return `// Generated by \`gkm test\` from the app's constructs — do not edit.
15558
- ${imports.join("\n")}
15559
-
15560
- ${drivers.setup ? `// The drivers the app's server entry registers, registered the same way.\n${drivers.setup}\n\n` : ""}const manifest = loadTestManifest(new URL('./manifest.json', import.meta.url));
15561
-
15562
- /** A browser with a client for every surface the app declares. */
15563
- export class Browser extends TestBrowser {
15564
- ${members.join("\n\n")}
15565
- }
15566
-
15567
- /** The app's modules, by the path the manifest records for each. */
15568
- const modules = {
15569
- ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
15570
- };
15571
-
15572
- /** \`it\`, for a test that drives the app the way it runs deployed. */
15573
- export const it = featureTest${database ? "<Browser, DatabaseOf<typeof __database>>" : ""}({ manifest, modules, browser: Browser });
15574
- `;
15575
- }
15576
- //#endregion
15577
15250
  //#region src/test/index.ts
15578
15251
  /**
15579
15252
  * Run tests with secrets, dependency URLs, and .env files loaded.
@@ -15613,7 +15286,7 @@ async function testCommand(options = {}) {
15613
15286
  const preloadPath = (0, node_path.join)(gkmDir, "test-credentials-preload.ts");
15614
15287
  await require_workspace$1.createCredentialsPreload(preloadPath, result.secretsJsonPath);
15615
15288
  const workspace = await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace).catch(() => void 0);
15616
- const manifestPath = workspace ? await writeTestHarness({
15289
+ const manifestPath = workspace ? await require_ready.writeTestHarness({
15617
15290
  root: workspace.root,
15618
15291
  targets: [workspace.root, ...Object.values(workspace.apps).map((app) => (0, node_path.isAbsolute)(app.path) ? app.path : (0, node_path.join)(workspace.root, app.path))],
15619
15292
  patterns: require_workspace$1.constructGlobs(workspace),
@@ -15625,6 +15298,16 @@ async function testCommand(options = {}) {
15625
15298
  console.log(manifestPath ? ` 🧪 Test harness written beside ${manifestPath}` : " 🧪 Nothing to prepare: this project declares no constructs");
15626
15299
  return;
15627
15300
  }
15301
+ if (workspace) await migrateTestStage(workspace, result.credentials);
15302
+ const readyPath = (0, node_path.join)(cwd, require_ready.TEST_READY_FILE);
15303
+ await (0, node_fs_promises.writeFile)(readyPath, JSON.stringify({
15304
+ env: result.secretsJsonPath,
15305
+ ...manifestPath ? { manifest: manifestPath } : {}
15306
+ }, null, 2));
15307
+ if (options.setup) {
15308
+ console.log(` 🧪 Test stage ready: ${require_ready.TEST_READY_FILE}`);
15309
+ return;
15310
+ }
15628
15311
  const nodeOptions = [
15629
15312
  process.env.NODE_OPTIONS ?? "",
15630
15313
  "--import=tsx",
@@ -15642,7 +15325,8 @@ async function testCommand(options = {}) {
15642
15325
  env: {
15643
15326
  ...process.env,
15644
15327
  ...finalCredentials,
15645
- ...manifestPath ? { [TEST_MANIFEST_ENV]: manifestPath } : {},
15328
+ ...manifestPath ? { [require_ready.TEST_MANIFEST_ENV]: manifestPath } : {},
15329
+ [require_ready.TEST_READY_ENV]: readyPath,
15646
15330
  NODE_ENV: "test",
15647
15331
  NODE_OPTIONS: nodeOptions
15648
15332
  }
@@ -15657,6 +15341,24 @@ async function testCommand(options = {}) {
15657
15341
  });
15658
15342
  });
15659
15343
  }
15344
+ /** Apply every construct's migrations to the test stage just reconciled. */
15345
+ async function migrateTestStage(workspace, env) {
15346
+ const { migrateDatabases } = await Promise.resolve().then(() => require("./migrate-BuCx7tIv.cjs"));
15347
+ const { discover } = await Promise.resolve().then(() => require("./discover-CroTIm0K.cjs")).then((n) => n.discover_exports);
15348
+ const sources = {};
15349
+ const manifest = await discover({
15350
+ patterns: require_workspace$1.constructGlobs(workspace),
15351
+ cwd: workspace.root,
15352
+ sources
15353
+ });
15354
+ const runs = await migrateDatabases({
15355
+ root: workspace.root,
15356
+ manifest,
15357
+ sources,
15358
+ env
15359
+ });
15360
+ for (const { target, applied } of runs) if (applied.length > 0) console.log(` 🗄️ ${target.folder}: applied ${applied.join(", ")}`);
15361
+ }
15660
15362
  /** `gkm test` outside a project that declares stages, with none named. */
15661
15363
  var NoStageToTest = class extends Error {
15662
15364
  constructor() {
@@ -16065,7 +15767,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
16065
15767
  process.exit(1);
16066
15768
  }
16067
15769
  });
16068
- program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from (default: stages.local)").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").option("--prepare", "Write the test manifest and #test harness, then stop — for a typecheck that runs before the suite").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
15770
+ program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from (default: stages.local)").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").option("--prepare", "Write the test manifest and #test harness, then stop — for a typecheck that runs before the suite").option("--setup", "Reconcile and migrate the test stage and write the harness, then stop — what @geekmidas/cli/vitest runs").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
16069
15771
  try {
16070
15772
  const globalOptions = program.opts();
16071
15773
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16078,6 +15780,34 @@ program.command("test").description("Run tests with secrets loaded from environm
16078
15780
  process.exit(1);
16079
15781
  }
16080
15782
  });
15783
+ program.command("migrate").description("Apply each database construct's migrations from db/<construct>/, as its owner").argument("[construct]", "Only this construct, by name").option("--stage <stage>", "The local stage (default) or test — a deploy migrates its own stage").action(async (construct, options) => {
15784
+ try {
15785
+ const globalOptions = program.opts();
15786
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15787
+ const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-BuCx7tIv.cjs"));
15788
+ await migrateCommand({
15789
+ ...options,
15790
+ ...construct ? { construct } : {}
15791
+ });
15792
+ } catch (error) {
15793
+ console.error(formatError(error));
15794
+ process.exit(1);
15795
+ }
15796
+ });
15797
+ program.command("migration").description("Write a construct's next migration: an empty one for a database, the schema change for an auth server").argument("<construct>", "The construct, by name").argument("[name]", "What the migration is called").action(async (construct, name) => {
15798
+ try {
15799
+ const globalOptions = program.opts();
15800
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15801
+ const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-BuCx7tIv.cjs"));
15802
+ await migrationCommand({
15803
+ construct,
15804
+ ...name ? { name } : {}
15805
+ });
15806
+ } catch (error) {
15807
+ console.error(formatError(error));
15808
+ process.exit(1);
15809
+ }
15810
+ });
16081
15811
  program.command("cron").description("Manage cron jobs").action(() => {
16082
15812
  const globalOptions = program.opts();
16083
15813
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16236,7 +15966,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16236
15966
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16237
15967
  process.exit(1);
16238
15968
  }
16239
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-B3ApLW6p.cjs")).then((n) => n.workspace_exports);
15969
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-BhiMkRuA.cjs")).then((n) => n.workspace_exports);
16240
15970
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16241
15971
  if (!result) {
16242
15972
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16295,7 +16025,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16295
16025
  try {
16296
16026
  const globalOptions = program.opts();
16297
16027
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16298
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-ZL3RcUVD.cjs"));
16028
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-B2fis34P.cjs"));
16299
16029
  await deployGithubCommand(options);
16300
16030
  } catch (error) {
16301
16031
  console.error(formatError(error));
@@ -16429,5 +16159,7 @@ Object.defineProperty(exports, "encryption_exports", {
16429
16159
  exports.getHostingerToken = getHostingerToken;
16430
16160
  exports.isRemoteStore = isRemoteStore;
16431
16161
  exports.pushStageSecrets = pushStageSecrets;
16162
+ exports.slug = slug;
16163
+ exports.stamp = stamp;
16432
16164
 
16433
16165
  //# sourceMappingURL=index.cjs.map