@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.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
3
- import { i as DEFAULT_CACHE, r as ConstructGenerator, s as isPartitionedRoutes, t as discover } from "./discover-CXGlQHm1.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-Dqxa-_Ge.mjs";
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-BH3Ki69F.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-T4IVGSoB.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-DnHW8ooI.mjs";
8
- import { a as openapiCommand, c as EndpointGenerator, l as runtimeFor, n as generateOpenApi, o as resolveOpenApiConfig, s as OpenApiTsGenerator } from "./openapi-D-Lk5Hr5.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-C97QUCIf.mjs";
8
+ import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-hDINScnz.mjs";
9
+ import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
10
+ import { a as cacheBackendsIn, i as writeTestHarness, n as TEST_READY_FILE, o as driversFor, r as TEST_MANIFEST_ENV, t as TEST_READY_ENV } from "./ready-CvP3JVEw.mjs";
9
11
  import { i as withRoutes, r as withCompute } from "./emit-Bzq7JsMP.mjs";
10
12
  import { a as createStageSecrets, c as generateServiceCredentials, i as writeDockerEnvFromSecrets, l as rotateServicePassword, o as generateConnectionUrls, r as generateFullstackCustomSecrets, s as generateSecurePassword, t as reconcileMissingSecrets } from "./reconcile-yNq6Shau.mjs";
11
13
  import { createRequire } from "node:module";
@@ -13,11 +15,11 @@ import { Command } from "commander";
13
15
  import { stdin, stdout } from "node:process";
14
16
  import * as readline from "node:readline/promises";
15
17
  import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
16
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
18
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
17
19
  import { homedir } from "node:os";
18
20
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
19
21
  import { execFile, execSync, spawn } from "node:child_process";
20
- import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, kebabCase, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
22
+ import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, migrationFolder, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
21
23
  import fg from "fast-glob";
22
24
  import chokidar from "chokidar";
23
25
  import { createCipheriv, createHash, randomBytes } from "node:crypto";
@@ -34,10 +36,8 @@ import { Queue } from "@geekmidas/constructs/queue";
34
36
  import { Subscriber } from "@geekmidas/constructs/subscribers";
35
37
  import { Topic } from "@geekmidas/constructs/topic";
36
38
  import { lookup } from "node:dns/promises";
37
- import { EnvironmentParser } from "@geekmidas/envkit";
38
- import { serviceContext } from "@geekmidas/services";
39
39
  //#region package.json
40
- var version = "10.0.0-alpha.29";
40
+ var version = "10.0.0-alpha.30";
41
41
  //#endregion
42
42
  //#region src/auth/credentials.ts
43
43
  /**
@@ -1086,6 +1086,9 @@ async function workspaceDevCommand(workspace, options) {
1086
1086
  ...rawSecrets,
1087
1087
  ...reconciled.env
1088
1088
  };
1089
+ await import("./migrate-CBICVMAi.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1090
+ logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1091
+ });
1089
1092
  const appUrls = {};
1090
1093
  for (const resource of reconciled.plan.resources) {
1091
1094
  if (resource.kind !== "rest-api" && resource.kind !== "site") continue;
@@ -1794,111 +1797,6 @@ export const handler = adapter.handler;
1794
1797
  }
1795
1798
  };
1796
1799
  //#endregion
1797
- //#region src/generators/drivers.ts
1798
- /**
1799
- * Driver registration for generated entry points.
1800
- *
1801
- * A construct declares that it needs object storage and is handed one URL; the
1802
- * scheme in that URL picks the driver that builds the client. Which drivers
1803
- * exist is therefore the *entry point's* decision, not the construct's and not
1804
- * the application's — an app that never talks to S3 should never resolve the AWS
1805
- * SDK, and application code that registers a driver has named a provider in the
1806
- * one layer this design keeps provider-free.
1807
- *
1808
- * So the generated entries register: `gkm dev`'s server for local development,
1809
- * and each Lambda handler for its own target. Registration is an explicit call
1810
- * rather than an import side effect, because a side-effecting module is exactly
1811
- * what a bundler is entitled to drop.
1812
- */
1813
- /** The `s3://` driver, which serves MinIO locally and S3 deployed. */
1814
- const S3 = {
1815
- imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
1816
- setup: "registerStorageDriver(s3Driver);"
1817
- };
1818
- const NONE = {
1819
- imports: "",
1820
- setup: ""
1821
- };
1822
- /**
1823
- * The cache driver for a backend — exactly one, never all three.
1824
- *
1825
- * Keyed off the *backend* rather than off a dependency, because unlike storage
1826
- * the backend already says which driver is right: a project caching in Postgres
1827
- * resolves `pg` and never `ioredis`, and one caching in Upstash resolves
1828
- * neither. That is the whole reason the drivers live behind separate subpaths.
1829
- *
1830
- * Both HTTP schemes are registered for Upstash, because the local proxy speaks
1831
- * `http://` and Upstash speaks `https://`, and an entry that registered one
1832
- * would work in exactly one of the two places.
1833
- */
1834
- const CACHE_DRIVERS = {
1835
- upstash: {
1836
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
1837
- setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
1838
- },
1839
- elasticache: {
1840
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
1841
- setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
1842
- },
1843
- db: {
1844
- imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
1845
- setup: "registerCacheDriver(postgresCacheDriver);"
1846
- }
1847
- };
1848
- /**
1849
- * Everything a generated entry should register: object storage, and the cache.
1850
- *
1851
- * Merged here rather than threaded separately, because a generated file has one
1852
- * import block and one setup block whatever fills them.
1853
- */
1854
- function driversFor(options) {
1855
- const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
1856
- const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
1857
- if (parts.length === 0) return NONE;
1858
- return {
1859
- imports: parts.map((part) => part.imports).join("\n"),
1860
- setup: parts.map((part) => part.setup).join("\n")
1861
- };
1862
- }
1863
- /**
1864
- * Which cache backends an app's entry actually has to speak.
1865
- *
1866
- * The config answers this for a cache that named nowhere, and the *declaration*
1867
- * answers it for one that named a database — `orders.cache('Sessions')` is a
1868
- * statement about the application, so a deployment cannot move that cache and
1869
- * the entry cannot register a driver for somewhere else.
1870
- *
1871
- * Reading only the config is how an entry ends up registering the Upstash
1872
- * driver for a URL the target composed as `postgres://`, which fails at the
1873
- * first request with `UnregisteredCacheScheme` and a stack that points at the
1874
- * cache rather than at the config that disagreed.
1875
- */
1876
- function cacheBackendsIn(manifest, configured) {
1877
- const caches = Object.values(manifest).filter((d) => d.kind === "cache");
1878
- return [...new Set(caches.map((c) => c.of ? "db" : configured))];
1879
- }
1880
- /**
1881
- * The drivers an app's entry points should register.
1882
- *
1883
- * Keyed off the dependency rather than off a declared bucket, because the entry
1884
- * is generated before discovery has run on the watcher's rebuild path — and an
1885
- * app that installed `@geekmidas/storage` has already paid for the SDK it would
1886
- * otherwise resolve lazily.
1887
- */
1888
- function storageDriversFor(appRoot) {
1889
- return dependsOnStorage(appRoot) ? S3 : NONE;
1890
- }
1891
- function dependsOnStorage(appRoot) {
1892
- const manifest = join(appRoot, "package.json");
1893
- if (!existsSync(manifest)) return false;
1894
- try {
1895
- const pkg = JSON.parse(readFileSync(manifest, "utf8"));
1896
- return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
1897
- } catch {
1898
- return false;
1899
- }
1900
- }
1901
- //#endregion
1902
1800
  //#region src/generators/FunctionGenerator.ts
1903
1801
  var FunctionGenerator = class extends ConstructGenerator {
1904
1802
  isConstruct(value) {
@@ -2901,7 +2799,7 @@ async function buildForProvider(provider, context, appRoot, discovered, endpoint
2901
2799
  let masterKey;
2902
2800
  if (context.production?.bundle && !skipBundle) {
2903
2801
  logger$8.log(`\n📦 Bundling production server...`);
2904
- const { bundleServer } = await import("./bundler-7iaO93G8.mjs");
2802
+ const { bundleServer } = await import("./bundler-D9IPNvDt.mjs");
2905
2803
  const allConstructs = [
2906
2804
  ...endpoints.map((e) => e.construct),
2907
2805
  ...functions.map((f) => f.construct),
@@ -3912,7 +3810,7 @@ async function createDnsProvider(options) {
3912
3810
  if (isDnsProvider(config.provider)) return config.provider;
3913
3811
  const provider = config.provider;
3914
3812
  if (provider === "hostinger") {
3915
- const { HostingerProvider } = await import("./HostingerProvider-Btxpto71.mjs");
3813
+ const { HostingerProvider } = await import("./HostingerProvider-GF6NAZAK.mjs");
3916
3814
  return new HostingerProvider();
3917
3815
  }
3918
3816
  if (provider === "route53") {
@@ -8049,6 +7947,22 @@ function printStateDetails(state) {
8049
7947
  }
8050
7948
  }
8051
7949
  //#endregion
7950
+ //#region src/migrate/names.ts
7951
+ /**
7952
+ * What a migration file is called: a UTC timestamp, then a name.
7953
+ *
7954
+ * Kept apart from the commands so the scaffold can name the migration it
7955
+ * writes the same way `gkm migration` does, without loading a migrator.
7956
+ */
7957
+ /** UTC `YYYYMMDDHHmmss` — sorts as text, and reads as a date. */
7958
+ function stamp(now = /* @__PURE__ */ new Date()) {
7959
+ return now.toISOString().replace(/\.\d+Z$/, "").replace(/\D/g, "");
7960
+ }
7961
+ /** `Add workouts` → `add_workouts`: the part of a file name after its stamp. */
7962
+ function slug(name) {
7963
+ return name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
7964
+ }
7965
+ //#endregion
8052
7966
  //#region src/init/constructs.ts
8053
7967
  /**
8054
7968
  * What a scaffolded project declares, and the keys that derive from it.
@@ -8165,7 +8079,7 @@ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
8165
8079
  */
8166
8080
  function usersMigration() {
8167
8081
  return {
8168
- path: "src/db/migrations/001_create_users.ts",
8082
+ path: `${migrationFolder(databaseFor().id)}/${stamp()}_create_users.ts`,
8169
8083
  content: `import type { Kysely } from 'kysely';
8170
8084
 
8171
8085
  export async function up(db: Kysely<unknown>): Promise<void> {
@@ -8328,7 +8242,7 @@ environment and fail in a way that looks like a config bug. Go through
8328
8242
  \`gkm exec --\`, which is what injects them.`);
8329
8243
  if (!isWorker) sections.push(endpointSection(options));
8330
8244
  if (isWorker || isServerless) sections.push(backgroundSection(template));
8331
- if (database) sections.push(databaseSection(), queriesSection());
8245
+ if (database) sections.push(databaseSection(), migrationsSection(), queriesSection());
8332
8246
  sections.push(`## Environment
8333
8247
 
8334
8248
  Environment is parsed once, through \`envkit\`, and never read from
@@ -8379,7 +8293,7 @@ function layoutBlock(options, template) {
8379
8293
  const { database } = options.constructs;
8380
8294
  const isWorker = template.name === "worker";
8381
8295
  if (monorepo) return `constructs/ # every construct: the surface, the database, topics
8382
- ${apiPath}/ # the API app
8296
+ ${database ? "db/<construct>/ # migrations, one folder per database construct\n" : ""}${apiPath}/ # the API app
8383
8297
  gkm.config.ts # name, constructs glob, secrets`;
8384
8298
  const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
8385
8299
  if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
@@ -8387,7 +8301,7 @@ gkm.config.ts # name, constructs glob, secrets`;
8387
8301
  lines.push("src/crons/ # scheduled work");
8388
8302
  lines.push("src/subscribers/ # topic consumers");
8389
8303
  }
8390
- if (database) lines.push("src/db/ # migrations, schema");
8304
+ if (database) lines.push("db/<construct>/ # migrations, one folder per database construct");
8391
8305
  lines.push("gkm.config.ts # name, constructs glob, secrets");
8392
8306
  return lines.join("\n");
8393
8307
  }
@@ -8515,6 +8429,38 @@ export const monthly = router
8515
8429
  Queries go through Kysely. Never open your own connection pool — the one the
8516
8430
  construct provides is the one that gets credentials, pooling and shutdown.`;
8517
8431
  }
8432
+ function migrationsSection() {
8433
+ return `## Migrations
8434
+
8435
+ Each database construct has its own folder, named after the construct:
8436
+ \`Database\` is \`db/database/\`, a tenant \`AuthDatabase\` is
8437
+ \`db/auth-database/\`. Each keeps its own history, in its own schema, and is
8438
+ migrated as its own **owner** role — the role a handler runs as cannot create a
8439
+ table.
8440
+
8441
+ \`\`\`bash
8442
+ gkm migration database add_workouts # db/database/20260930143012_add_workouts.ts
8443
+ gkm migration auth # Better Auth's schema change, as SQL
8444
+ gkm migrate # apply every folder, parents first
8445
+ gkm migrate database # apply one construct's
8446
+ \`\`\`
8447
+
8448
+ - Files are \`<UTC timestamp>_<name>.ts\` exporting \`up\` (and \`down\`), or
8449
+ \`.sql\`, which runs whole. Never rename or edit one that has been applied —
8450
+ it is recorded by name. Change the schema with a new migration.
8451
+ - Write migrations with \`Kysely<unknown>\` and snake_case names: they are the
8452
+ SQL, and the camelCase plugin is the app's, not theirs.
8453
+ - \`gkm migration auth\` compares Better Auth's schema to its tenant and writes
8454
+ only the difference. Run it after adding a Better Auth plugin, and commit the
8455
+ file.
8456
+ - Tests migrate themselves: the root vitest config's
8457
+ \`globalSetup: ['@geekmidas/cli/vitest']\` applies every folder to the test
8458
+ stage before any test runs. \`gkm dev\` only reports what is pending.
8459
+ - A deploy migrates before the new version goes live, so the old version runs
8460
+ against the new schema until it is replaced. Keep each migration compatible
8461
+ with the code before it: add a column in one release, stop using the old one,
8462
+ drop it in a later release.`;
8463
+ }
8518
8464
  function queriesSection() {
8519
8465
  return `## Writing queries and relations
8520
8466
 
@@ -8664,7 +8610,6 @@ const DEPENDENCY_VERSIONS = {
8664
8610
  hono: "~4.13.9",
8665
8611
  "@hono/node-server": "~1.19.17",
8666
8612
  kysely: "~0.29.6",
8667
- "kysely-ctl": "~0.21.0",
8668
8613
  pg: "~8.23.0",
8669
8614
  pino: "~10.3.1",
8670
8615
  zod: "~4.6.5",
@@ -8771,26 +8716,26 @@ function loadPackageJson() {
8771
8716
  * Run: pnpm --filter @geekmidas/cli sync-versions
8772
8717
  */
8773
8718
  const GEEKMIDAS_VERSIONS = {
8774
- "@geekmidas/audit": "~10.0.0-alpha.29",
8775
- "@geekmidas/auth": "~10.0.0-alpha.29",
8776
- "@geekmidas/cache": "~10.0.0-alpha.29",
8777
- "@geekmidas/client": "~10.0.0-alpha.29",
8778
- "@geekmidas/cloud": "~10.0.0-alpha.29",
8779
- "@geekmidas/constructs": "~10.0.0-alpha.29",
8780
- "@geekmidas/db": "~10.0.0-alpha.29",
8781
- "@geekmidas/emailkit": "~10.0.0-alpha.29",
8782
- "@geekmidas/envkit": "~10.0.0-alpha.29",
8783
- "@geekmidas/errors": "~10.0.0-alpha.29",
8784
- "@geekmidas/events": "~10.0.0-alpha.29",
8785
- "@geekmidas/logger": "~10.0.0-alpha.29",
8786
- "@geekmidas/manifest": "~10.0.0-alpha.29",
8787
- "@geekmidas/rate-limit": "~10.0.0-alpha.29",
8788
- "@geekmidas/schema": "~10.0.0-alpha.29",
8789
- "@geekmidas/services": "~10.0.0-alpha.29",
8790
- "@geekmidas/storage": "~10.0.0-alpha.29",
8791
- "@geekmidas/studio": "~10.0.0-alpha.29",
8792
- "@geekmidas/telescope": "~10.0.0-alpha.29",
8793
- "@geekmidas/testkit": "~10.0.0-alpha.29",
8719
+ "@geekmidas/audit": "~10.0.0-alpha.30",
8720
+ "@geekmidas/auth": "~10.0.0-alpha.30",
8721
+ "@geekmidas/cache": "~10.0.0-alpha.30",
8722
+ "@geekmidas/client": "~10.0.0-alpha.30",
8723
+ "@geekmidas/cloud": "~10.0.0-alpha.30",
8724
+ "@geekmidas/constructs": "~10.0.0-alpha.30",
8725
+ "@geekmidas/db": "~10.0.0-alpha.30",
8726
+ "@geekmidas/emailkit": "~10.0.0-alpha.30",
8727
+ "@geekmidas/envkit": "~10.0.0-alpha.30",
8728
+ "@geekmidas/errors": "~10.0.0-alpha.30",
8729
+ "@geekmidas/events": "~10.0.0-alpha.30",
8730
+ "@geekmidas/logger": "~10.0.0-alpha.30",
8731
+ "@geekmidas/manifest": "~10.0.0-alpha.30",
8732
+ "@geekmidas/rate-limit": "~10.0.0-alpha.30",
8733
+ "@geekmidas/schema": "~10.0.0-alpha.30",
8734
+ "@geekmidas/services": "~10.0.0-alpha.30",
8735
+ "@geekmidas/storage": "~10.0.0-alpha.30",
8736
+ "@geekmidas/studio": "~10.0.0-alpha.30",
8737
+ "@geekmidas/telescope": "~10.0.0-alpha.30",
8738
+ "@geekmidas/testkit": "~10.0.0-alpha.30",
8794
8739
  "@geekmidas/cli": `~${loadPackageJson().version}`
8795
8740
  };
8796
8741
  //#endregion
@@ -8808,9 +8753,7 @@ function generateAuthAppFiles(options) {
8808
8753
  type: "module",
8809
8754
  scripts: {
8810
8755
  dev: "gkm dev",
8811
- build: "gkm build",
8812
- "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
8813
- "db:generate": "gkm exec -- npx @better-auth/cli generate"
8756
+ build: "gkm build"
8814
8757
  },
8815
8758
  dependencies: {
8816
8759
  [`@${options.name}/models`]: "workspace:*",
@@ -8890,16 +8833,24 @@ function stageEnv(stages) {
8890
8833
  //#endregion
8891
8834
  //#region src/init/generators/config.ts
8892
8835
  /**
8893
- * Vitest config content with globalSetup for database-enabled apps
8836
+ * Vitest config for a database-enabled app.
8837
+ *
8838
+ * On its own, it is the root config and carries gkm's setup: the test stage
8839
+ * reconciled and every construct migrated, however the suite starts. In a
8840
+ * workspace the root config carries it instead — once, so no project filter
8841
+ * can skip it and nothing runs it twice.
8894
8842
  */
8895
- const vitestConfigContent = `import { defineConfig } from 'vitest/config';
8843
+ const vitestConfigFor = (monorepo) => `import { defineConfig } from 'vitest/config';
8896
8844
 
8897
8845
  export default defineConfig({
8898
8846
  // Vite resolves tsconfig \`paths\` itself; no plugin needed.
8899
8847
  resolve: { tsconfigPaths: true },
8900
8848
  test: {
8901
- environment: 'node',
8902
- globalSetup: './test/globalSetup.ts',
8849
+ environment: 'node',${monorepo ? "" : `
8850
+ // The test stage, reconciled and every database construct's migrations
8851
+ // applied, before any test runs — whether \`gkm test\` or plain
8852
+ // \`vitest\` started it.
8853
+ globalSetup: ['@geekmidas/cli/vitest'],`}
8903
8854
  },
8904
8855
  });
8905
8856
  `;
@@ -8977,7 +8928,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
8977
8928
  }];
8978
8929
  if (options.constructs.database) files.push({
8979
8930
  path: "vitest.config.ts",
8980
- content: vitestConfigContent
8931
+ content: vitestConfigFor(Boolean(options.monorepo))
8981
8932
  });
8982
8933
  return files;
8983
8934
  }
@@ -9067,7 +9018,7 @@ export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ?
9067
9018
  ];
9068
9019
  if (options.constructs.database) files.push({
9069
9020
  path: "vitest.config.ts",
9070
- content: vitestConfigContent
9021
+ content: vitestConfigFor(Boolean(options.monorepo))
9071
9022
  });
9072
9023
  return files;
9073
9024
  }
@@ -9103,7 +9054,7 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
9103
9054
  }];
9104
9055
  if (options.constructs.database) files.push({
9105
9056
  path: "vitest.config.ts",
9106
- content: vitestConfigContent
9057
+ content: vitestConfigFor(Boolean(options.monorepo))
9107
9058
  });
9108
9059
  return files;
9109
9060
  }
@@ -10550,8 +10501,12 @@ ${isSst ? "\n# SST\n.sst/\n" : ""}`;
10550
10501
 
10551
10502
  export default defineConfig({
10552
10503
  test: {
10553
- // Each app and package is its own project, so its own config — an app's
10554
- // \`globalSetup\`, its path aliases — applies to its own tests.
10504
+ // The test stage, reconciled and every database construct's migrations
10505
+ // applied, before any project's tests start — declared here, once, so no
10506
+ // project filter can skip it and nothing runs it twice.
10507
+ globalSetup: ['@geekmidas/cli/vitest'],
10508
+ // Each app and package is its own project, so its own config — its path
10509
+ // aliases — applies to its own tests.
10555
10510
  projects: ['apps/*', 'packages/*'],
10556
10511
  // The shared packages ship without tests of their own.
10557
10512
  passWithNoTests: true,
@@ -10769,6 +10724,7 @@ export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}', {
10769
10724
  export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb');
10770
10725
  `
10771
10726
  });
10727
+ files.push(usersMigration());
10772
10728
  files.push({
10773
10729
  path: "constructs/auth.ts",
10774
10730
  content: `import { BetterAuth } from '@geekmidas/constructs/auth';
@@ -11157,7 +11113,6 @@ export const router = api${options.constructs.database ? ".database(database)" :
11157
11113
  `
11158
11114
  });
11159
11115
  if (options.constructs.database && declares) files.push(...databaseFiles());
11160
- else if (options.constructs.database) files.push(usersMigration());
11161
11116
  if (uploads && declares) files.push({
11162
11117
  path: "src/constructs/storage.ts",
11163
11118
  content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
@@ -11925,7 +11880,6 @@ function generatePackageJson(options, template) {
11925
11880
  devDependencies["@types/pg"] = DEPENDENCY_VERSIONS["@types/pg"];
11926
11881
  devDependencies["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
11927
11882
  devDependencies["@faker-js/faker"] = DEPENDENCY_VERSIONS["@faker-js/faker"];
11928
- devDependencies["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
11929
11883
  }
11930
11884
  if (monorepo) {
11931
11885
  delete devDependencies["@biomejs/biome"];
@@ -11976,38 +11930,16 @@ function generateSourceFiles(options, template) {
11976
11930
  //#region src/init/generators/test.ts
11977
11931
  /**
11978
11932
  * Generate test infrastructure files when database is enabled.
11979
- * Includes transaction-isolated test config, global setup with migrations,
11933
+ * Includes transaction-isolated test config,
11980
11934
  * factory system with builders/seeds, and an example spec.
11981
11935
  */
11982
11936
  function generateTestFiles(options, _template) {
11983
11937
  if (!options.constructs.database) return [];
11984
11938
  const db = databaseFor();
11985
11939
  const schema = !options.monorepo ? "../src/constructs/database.ts" : `@${options.name}/constructs/database.ts`;
11986
- const runtimeUrl = db.urlKey;
11987
- const ownerUrl = db.ownerUrlKey;
11940
+ db.urlKey;
11941
+ db.ownerUrlKey;
11988
11942
  return [
11989
- {
11990
- path: "kysely.config.ts",
11991
- content: `import { Credentials } from '@geekmidas/envkit/credentials';
11992
- import { PostgresDialect } from 'kysely';
11993
- import { defineConfig } from 'kysely-ctl';
11994
- import pg from 'pg';
11995
-
11996
- // The owner role's URL — the one that may create, alter, and drop. Both keys
11997
- // are published by the declared database construct; run this under
11998
- // \`gkm exec -- pnpm kysely migrate:latest\` so they are injected.
11999
- const url = Credentials.${ownerUrl} ?? Credentials.${runtimeUrl};
12000
-
12001
- export default defineConfig({
12002
- dialect: new PostgresDialect({
12003
- pool: new pg.Pool({ connectionString: url }),
12004
- }),
12005
- migrations: {
12006
- migrationFolder: './src/db/migrations',
12007
- },
12008
- });
12009
- `
12010
- },
12011
11943
  {
12012
11944
  path: "test/config.ts",
12013
11945
  content: `import { it as itVitest } from 'vitest';
@@ -12019,44 +11951,6 @@ export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
12019
11951
  // the plugins it was given, \`CamelCasePlugin\` included.
12020
11952
  connection: database,
12021
11953
  });
12022
- `
12023
- },
12024
- {
12025
- path: "test/globalSetup.ts",
12026
- content: `import fs from 'node:fs/promises';
12027
- import path from 'node:path';
12028
- import { Credentials } from '@geekmidas/envkit/credentials';
12029
- import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
12030
- import { Kysely, PostgresDialect } from 'kysely';
12031
- import { FileMigrationProvider } from 'kysely/migration';
12032
- import pg from 'pg';
12033
-
12034
- export default async function globalSetup() {
12035
- // \`gkm test\` reconciles the test stage before this runs: the container is
12036
- // up, the database exists, and its roles are created. What is left is the
12037
- // schema, which is what migrations are for.
12038
- const databaseUrl = Credentials.${ownerUrl} ?? Credentials.${runtimeUrl}!;
12039
- const migrationFolder = path.resolve(import.meta.dirname, '../src/db/migrations');
12040
-
12041
- const db = new Kysely({
12042
- dialect: new PostgresDialect({
12043
- pool: new pg.Pool({ connectionString: databaseUrl }),
12044
- }),
12045
- });
12046
-
12047
- const migrator = new PostgresKyselyMigrator({
12048
- uri: databaseUrl,
12049
- db,
12050
- provider: new FileMigrationProvider({
12051
- fs,
12052
- path,
12053
- migrationFolder,
12054
- }),
12055
- });
12056
-
12057
- const teardown = await migrator.start();
12058
- return teardown;
12059
- }
12060
11954
  `
12061
11955
  },
12062
11956
  {
@@ -14684,10 +14578,14 @@ function printNextSteps(projectName, options, pkgManager) {
14684
14578
  console.log("Next steps:\n");
14685
14579
  console.log(` ${cdCommand}`);
14686
14580
  console.log(` ${devCommand}`);
14687
- if (!options.monorepo && options.constructs.database) {
14581
+ if (options.constructs.database) {
14688
14582
  console.log("");
14689
- console.log(" # Then, once the container is up:");
14690
- console.log(` gkm exec -- pnpm kysely migrate:latest`);
14583
+ console.log(" # Apply each database construct's db/<construct>/ migrations:");
14584
+ console.log(" gkm migrate");
14585
+ if (options.monorepo) {
14586
+ console.log(" # Better Auth's tables, as its tenant's first migration:");
14587
+ console.log(" gkm migration auth && gkm migrate");
14588
+ }
14691
14589
  }
14692
14590
  console.log("");
14693
14591
  if (options.monorepo) {
@@ -15039,7 +14937,7 @@ var NoStoredSecrets = class extends Error {
15039
14937
  }
15040
14938
  };
15041
14939
  async function reconcile(secrets, workspace, stage) {
15042
- const { derivedContainers } = await import("./workspace-DnHW8ooI.mjs").then((n) => n.a);
14940
+ const { derivedContainers } = await import("./workspace-C97QUCIf.mjs").then((n) => n.a);
15043
14941
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
15044
14942
  return result ? {
15045
14943
  secrets: result.secrets,
@@ -15338,231 +15236,6 @@ function printSummary(workspace, stage) {
15338
15236
  logger$1.log("");
15339
15237
  }
15340
15238
  //#endregion
15341
- //#region src/test/harness.ts
15342
- /**
15343
- * What `gkm test` writes for a feature test to be built from.
15344
- *
15345
- * `gkm test` already discovers an app's constructs and resolves the test
15346
- * stage's environment; this keeps both, under `.gkm/test/`:
15347
- *
15348
- * - `manifest.json` — which construct and endpoint is exported where, and the
15349
- * environment the test stage resolved, keyed as the constructs derive keys.
15350
- * - `clients/<surface>.ts` — each surface's typed client, from the same
15351
- * generator `gkm build` uses for the app's own.
15352
- * - `index.ts` — the configured `it`, and a `Browser` holding one client per
15353
- * surface and one per auth server, each on the browser's `fetch`.
15354
- *
15355
- * A test imports `it` from there and declares nothing — not a construct, not an
15356
- * environment key, not a client.
15357
- */
15358
- /**
15359
- * The variable the manifest's path travels in — what
15360
- * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
15361
- * that entry carries the whole harness (MSW, Hono, Vitest), and importing it
15362
- * here put all of it on the start-up path of every `gkm` command.
15363
- */
15364
- const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
15365
- /** Where the harness lives, relative to the workspace root. */
15366
- const TEST_HARNESS_DIR = ".gkm/test";
15367
- /**
15368
- * Better-auth's client plugin for each server plugin id.
15369
- *
15370
- * Written down rather than derived: the ids and the client names do not follow
15371
- * one rule (`admin` pairs with `adminClient`, `email-otp` with
15372
- * `emailOTPClient`), and a guess that missed would hand a test a client
15373
- * silently lacking every method of that plugin. A server plugin not listed has
15374
- * no client counterpart — `bearer`, `open-api` — or needs one from another
15375
- * package, and is left out.
15376
- */
15377
- const CLIENT_PLUGINS = {
15378
- admin: "adminClient",
15379
- anonymous: "anonymousClient",
15380
- "device-authorization": "deviceAuthorizationClient",
15381
- "email-otp": "emailOTPClient",
15382
- jwt: "jwtClient",
15383
- "last-login-method": "lastLoginMethodClient",
15384
- "magic-link": "magicLinkClient",
15385
- "multi-session": "multiSessionClient",
15386
- "oauth-popup": "oauthPopupClient",
15387
- "one-tap": "oneTapClient",
15388
- "one-time-token": "oneTimeTokenClient",
15389
- organization: "organizationClient",
15390
- "phone-number": "phoneNumberClient",
15391
- siwe: "siweClient",
15392
- "two-factor": "twoFactorClient",
15393
- username: "usernameClient"
15394
- };
15395
- /** Write the manifest, the clients and the harness; return the manifest path. */
15396
- async function writeTestHarness(options) {
15397
- const sources = {};
15398
- const declared = await discover({
15399
- patterns: options.patterns,
15400
- cwd: options.root,
15401
- sources
15402
- });
15403
- const endpoints = (await new EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
15404
- surface: construct.owner ?? construct.surface?.id,
15405
- construct,
15406
- source: {
15407
- file: path.absolute,
15408
- export: key
15409
- }
15410
- })).filter((endpoint) => endpoint.surface !== void 0);
15411
- const manifest = {
15412
- stage: options.stage,
15413
- constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
15414
- const source = sources[id];
15415
- return source ? [[id, {
15416
- kind: declaration.kind,
15417
- source: {
15418
- file: source.file,
15419
- export: source.exportName
15420
- }
15421
- }]] : [];
15422
- })),
15423
- endpoints: endpoints.map(({ surface, source }) => ({
15424
- surface,
15425
- source
15426
- })),
15427
- env: options.env
15428
- };
15429
- const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
15430
- const generator = new OpenApiTsGenerator();
15431
- const clients = await Promise.all(surfaces.map(async (surface) => ({
15432
- file: `${kebabCase(surface)}.ts`,
15433
- content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
15434
- title: surface,
15435
- version: "1.0.0"
15436
- })
15437
- })));
15438
- const auths = await authClients(declared, sources, options.env);
15439
- const drivers = driversFor({
15440
- appRoot: options.root,
15441
- cache: cacheBackendsIn(declared, options.cacheBackend)
15442
- });
15443
- const database = await databaseSource(declared, sources, endpoints.map(({ construct }) => construct));
15444
- const files = [.../* @__PURE__ */ new Set([...Object.values(manifest.constructs).map(({ source }) => source.file), ...manifest.endpoints.map(({ source }) => source.file)])];
15445
- const harnessFor = (dir) => harnessModule({
15446
- dir,
15447
- files,
15448
- database,
15449
- surfaces: surfaces.map((id, index) => ({
15450
- id,
15451
- secured: clients[index].content.includes("authStrategies:")
15452
- })),
15453
- auths,
15454
- drivers
15455
- });
15456
- const json = `${JSON.stringify(manifest, null, 2)}\n`;
15457
- return (await Promise.all([...new Set(options.targets)].map(async (target) => {
15458
- const dir = join(target, TEST_HARNESS_DIR);
15459
- await rm(dir, {
15460
- recursive: true,
15461
- force: true
15462
- });
15463
- await mkdir(join(dir, "clients"), { recursive: true });
15464
- await writeFile(join(dir, "manifest.json"), json);
15465
- await writeFile(join(dir, "index.ts"), harnessFor(dir));
15466
- for (const client of clients) await writeFile(join(dir, "clients", client.file), client.content);
15467
- return join(dir, "manifest.json");
15468
- })))[0];
15469
- }
15470
- /** Each auth server, with the client plugins its server plugins pair with. */
15471
- async function authClients(declared, sources, env) {
15472
- const clients = [];
15473
- const envParser = new EnvironmentParser({ ...env });
15474
- for (const [id, declaration] of Object.entries(declared)) {
15475
- const source = sources[id];
15476
- if (declaration.kind !== "rest-api" || !source) continue;
15477
- const exported = (await import(source.file))[source.exportName];
15478
- if (!isBetterAuth(exported) || exported.id !== id) continue;
15479
- const ids = await exported.pluginIds({
15480
- envParser,
15481
- context: serviceContext
15482
- });
15483
- clients.push({
15484
- id,
15485
- basePath: exported.basePath,
15486
- plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? [])
15487
- });
15488
- }
15489
- return clients;
15490
- }
15491
- function isBetterAuth(value) {
15492
- return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
15493
- }
15494
- /** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
15495
- function propertyOf(id) {
15496
- return id.charAt(0).toLowerCase() + id.slice(1);
15497
- }
15498
- /**
15499
- * The database a test's `db` is — the one the endpoints were given with
15500
- * `.database()`, found by the service they hold, which is the construct's own.
15501
- * The same rule `featureTest` applies at runtime, applied here so `db` can be
15502
- * typed by the construct's schema.
15503
- */
15504
- async function databaseSource(declared, sources, endpoints) {
15505
- const services = new Set(endpoints.map((endpoint) => endpoint.databaseService).filter(Boolean));
15506
- if (services.size === 0) return void 0;
15507
- for (const [id, declaration] of Object.entries(declared)) {
15508
- const source = sources[id];
15509
- if (!source || !declaration.kind.startsWith("database")) continue;
15510
- const exported = (await import(source.file))[source.exportName];
15511
- if (services.has(exported?.service)) return source;
15512
- }
15513
- }
15514
- /** An import specifier for `file`, from the harness written into `dir`. */
15515
- function specifierFrom(dir, file) {
15516
- const path = relative(dir, file).replace(/\.tsx?$/, ".js");
15517
- return path.startsWith(".") ? path : `./${path}`;
15518
- }
15519
- function harnessModule(options) {
15520
- const { surfaces, auths, drivers, database, dir, files } = options;
15521
- const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
15522
- const imports = [
15523
- `import {${database ? " type DatabaseOf," : ""} featureTest, loadTestManifest } from '@geekmidas/constructs/testing';`,
15524
- ...database ? [`import type { ${database.exportName} as __database } from '${specifierFrom(dir, database.file)}';`] : [],
15525
- `import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
15526
- ...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`),
15527
- ...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
15528
- ...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
15529
- ...drivers.imports ? [drivers.imports] : [],
15530
- ...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
15531
- ];
15532
- const url = (id) => `manifest.env[${JSON.stringify(provideKey(id, "url"))}]!`;
15533
- const members = [...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${provideKey(id, "url")}. */
15534
- readonly ${propertyOf(id)} = create${id}({
15535
- baseURL: ${url(id)},
15536
- fetch: this.fetch,${secured ? `
15537
- // Served in-process: no authorizer runs, so nothing is signed. A scheme
15538
- // with no strategy sends its request without credentials.
15539
- authStrategies: {} as never,` : ""}
15540
- });`), ...auths.map((auth) => ` /** \`${auth.id}\`, at ${provideKey(auth.id, "url")}. */
15541
- readonly ${propertyOf(auth.id)} = createAuthClient({
15542
- baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
15543
- plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
15544
- fetchOptions: { customFetchImpl: this.fetch },
15545
- });`)];
15546
- return `// Generated by \`gkm test\` from the app's constructs — do not edit.
15547
- ${imports.join("\n")}
15548
-
15549
- ${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));
15550
-
15551
- /** A browser with a client for every surface the app declares. */
15552
- export class Browser extends TestBrowser {
15553
- ${members.join("\n\n")}
15554
- }
15555
-
15556
- /** The app's modules, by the path the manifest records for each. */
15557
- const modules = {
15558
- ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
15559
- };
15560
-
15561
- /** \`it\`, for a test that drives the app the way it runs deployed. */
15562
- export const it = featureTest${database ? "<Browser, DatabaseOf<typeof __database>>" : ""}({ manifest, modules, browser: Browser });
15563
- `;
15564
- }
15565
- //#endregion
15566
15239
  //#region src/test/index.ts
15567
15240
  /**
15568
15241
  * Run tests with secrets, dependency URLs, and .env files loaded.
@@ -15614,6 +15287,16 @@ async function testCommand(options = {}) {
15614
15287
  console.log(manifestPath ? ` 🧪 Test harness written beside ${manifestPath}` : " 🧪 Nothing to prepare: this project declares no constructs");
15615
15288
  return;
15616
15289
  }
15290
+ if (workspace) await migrateTestStage(workspace, result.credentials);
15291
+ const readyPath = join(cwd, TEST_READY_FILE);
15292
+ await writeFile(readyPath, JSON.stringify({
15293
+ env: result.secretsJsonPath,
15294
+ ...manifestPath ? { manifest: manifestPath } : {}
15295
+ }, null, 2));
15296
+ if (options.setup) {
15297
+ console.log(` 🧪 Test stage ready: ${TEST_READY_FILE}`);
15298
+ return;
15299
+ }
15617
15300
  const nodeOptions = [
15618
15301
  process.env.NODE_OPTIONS ?? "",
15619
15302
  "--import=tsx",
@@ -15632,6 +15315,7 @@ async function testCommand(options = {}) {
15632
15315
  ...process.env,
15633
15316
  ...finalCredentials,
15634
15317
  ...manifestPath ? { [TEST_MANIFEST_ENV]: manifestPath } : {},
15318
+ [TEST_READY_ENV]: readyPath,
15635
15319
  NODE_ENV: "test",
15636
15320
  NODE_OPTIONS: nodeOptions
15637
15321
  }
@@ -15646,6 +15330,24 @@ async function testCommand(options = {}) {
15646
15330
  });
15647
15331
  });
15648
15332
  }
15333
+ /** Apply every construct's migrations to the test stage just reconciled. */
15334
+ async function migrateTestStage(workspace, env) {
15335
+ const { migrateDatabases } = await import("./migrate-CBICVMAi.mjs");
15336
+ const { discover } = await import("./discover-CMxesskG.mjs").then((n) => n.n);
15337
+ const sources = {};
15338
+ const manifest = await discover({
15339
+ patterns: constructGlobs(workspace),
15340
+ cwd: workspace.root,
15341
+ sources
15342
+ });
15343
+ const runs = await migrateDatabases({
15344
+ root: workspace.root,
15345
+ manifest,
15346
+ sources,
15347
+ env
15348
+ });
15349
+ for (const { target, applied } of runs) if (applied.length > 0) console.log(` 🗄️ ${target.folder}: applied ${applied.join(", ")}`);
15350
+ }
15649
15351
  /** `gkm test` outside a project that declares stages, with none named. */
15650
15352
  var NoStageToTest = class extends Error {
15651
15353
  constructor() {
@@ -16054,7 +15756,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
16054
15756
  process.exit(1);
16055
15757
  }
16056
15758
  });
16057
- 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) => {
15759
+ 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) => {
16058
15760
  try {
16059
15761
  const globalOptions = program.opts();
16060
15762
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16067,6 +15769,34 @@ program.command("test").description("Run tests with secrets loaded from environm
16067
15769
  process.exit(1);
16068
15770
  }
16069
15771
  });
15772
+ 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) => {
15773
+ try {
15774
+ const globalOptions = program.opts();
15775
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15776
+ const { migrateCommand } = await import("./migrate-CBICVMAi.mjs");
15777
+ await migrateCommand({
15778
+ ...options,
15779
+ ...construct ? { construct } : {}
15780
+ });
15781
+ } catch (error) {
15782
+ console.error(formatError(error));
15783
+ process.exit(1);
15784
+ }
15785
+ });
15786
+ 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) => {
15787
+ try {
15788
+ const globalOptions = program.opts();
15789
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
15790
+ const { migrationCommand } = await import("./migrate-CBICVMAi.mjs");
15791
+ await migrationCommand({
15792
+ construct,
15793
+ ...name ? { name } : {}
15794
+ });
15795
+ } catch (error) {
15796
+ console.error(formatError(error));
15797
+ process.exit(1);
15798
+ }
15799
+ });
16070
15800
  program.command("cron").description("Manage cron jobs").action(() => {
16071
15801
  const globalOptions = program.opts();
16072
15802
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16225,7 +15955,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16225
15955
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16226
15956
  process.exit(1);
16227
15957
  }
16228
- const { derivedContainers } = await import("./workspace-DnHW8ooI.mjs").then((n) => n.a);
15958
+ const { derivedContainers } = await import("./workspace-C97QUCIf.mjs").then((n) => n.a);
16229
15959
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16230
15960
  if (!result) {
16231
15961
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -16284,7 +16014,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
16284
16014
  try {
16285
16015
  const globalOptions = program.opts();
16286
16016
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16287
- const { deployGithubCommand } = await import("./github-DPpNfzu4.mjs");
16017
+ const { deployGithubCommand } = await import("./github-BrDmlm1x.mjs");
16288
16018
  await deployGithubCommand(options);
16289
16019
  } catch (error) {
16290
16020
  console.error(formatError(error));
@@ -16409,6 +16139,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
16409
16139
  });
16410
16140
  program.parse();
16411
16141
  //#endregion
16412
- export { getHostingerToken as i, isRemoteStore as n, encryption_exports as r, pushStageSecrets as t };
16142
+ export { encryption_exports as a, stamp as i, isRemoteStore as n, getHostingerToken as o, slug as r, pushStageSecrets as t };
16413
16143
 
16414
16144
  //# sourceMappingURL=index.mjs.map