@geekmidas/cli 10.0.0-alpha.113 → 10.0.0-alpha.115

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 (123) hide show
  1. package/dist/{GoDaddyProvider-DWrDIXzQ.cjs → GoDaddyProvider-BDvIMTO-.cjs} +2 -2
  2. package/dist/{GoDaddyProvider-DWrDIXzQ.cjs.map → GoDaddyProvider-BDvIMTO-.cjs.map} +1 -1
  3. package/dist/{GoDaddyProvider-CPabc3XW.mjs → GoDaddyProvider-eW1mre1g.mjs} +2 -2
  4. package/dist/{GoDaddyProvider-CPabc3XW.mjs.map → GoDaddyProvider-eW1mre1g.mjs.map} +1 -1
  5. package/dist/{LegacyStateStore-BdXoAlYY.mjs → LegacyStateStore-Ba1_x0cr.mjs} +3 -3
  6. package/dist/{LegacyStateStore-BdXoAlYY.mjs.map → LegacyStateStore-Ba1_x0cr.mjs.map} +1 -1
  7. package/dist/{LegacyStateStore-fsyPGBWu.cjs → LegacyStateStore-DFeykTd8.cjs} +3 -3
  8. package/dist/{LegacyStateStore-fsyPGBWu.cjs.map → LegacyStateStore-DFeykTd8.cjs.map} +1 -1
  9. package/dist/{LocalStateStore-BrAX5yy7.mjs → LocalStateStore-C1rUUvBr.mjs} +2 -2
  10. package/dist/{LocalStateStore-BrAX5yy7.mjs.map → LocalStateStore-C1rUUvBr.mjs.map} +1 -1
  11. package/dist/{LocalStateStore-AtviUBky.cjs → LocalStateStore-CV-04jpI.cjs} +2 -2
  12. package/dist/{LocalStateStore-AtviUBky.cjs.map → LocalStateStore-CV-04jpI.cjs.map} +1 -1
  13. package/dist/{S3StateStore-tn5g80Sx.cjs → S3StateStore-CqYi-1sb.cjs} +2 -2
  14. package/dist/{S3StateStore-tn5g80Sx.cjs.map → S3StateStore-CqYi-1sb.cjs.map} +1 -1
  15. package/dist/{S3StateStore-Cx7njkBz.mjs → S3StateStore-Dszt4fN5.mjs} +2 -2
  16. package/dist/{S3StateStore-Cx7njkBz.mjs.map → S3StateStore-Dszt4fN5.mjs.map} +1 -1
  17. package/dist/{SSMStateStore-sqZ3oDmn.mjs → SSMStateStore-BYRUyMkE.mjs} +2 -2
  18. package/dist/{SSMStateStore-sqZ3oDmn.mjs.map → SSMStateStore-BYRUyMkE.mjs.map} +1 -1
  19. package/dist/{SSMStateStore-CAFde-Fq.cjs → SSMStateStore-zpHjDCAW.cjs} +2 -2
  20. package/dist/{SSMStateStore-CAFde-Fq.cjs.map → SSMStateStore-zpHjDCAW.cjs.map} +1 -1
  21. package/dist/{add-Cs8yT5iH.cjs → add-0XI8_HOf.cjs} +3 -3
  22. package/dist/{add-Cs8yT5iH.cjs.map → add-0XI8_HOf.cjs.map} +1 -1
  23. package/dist/{add-DJE7zqJ4.mjs → add-DXdJjp0b.mjs} +3 -3
  24. package/dist/{add-DJE7zqJ4.mjs.map → add-DXdJjp0b.mjs.map} +1 -1
  25. package/dist/{bundler-DrySVbXF.mjs → bundler-B2ysiCTa.mjs} +5 -5
  26. package/dist/{bundler-DrySVbXF.mjs.map → bundler-B2ysiCTa.mjs.map} +1 -1
  27. package/dist/{bundler-NrtItVBp.cjs → bundler-hvCgi0EG.cjs} +5 -5
  28. package/dist/{bundler-NrtItVBp.cjs.map → bundler-hvCgi0EG.cjs.map} +1 -1
  29. package/dist/{cli-CHzpFBh-.cjs → cli-5k6kV_i4.cjs} +2 -2
  30. package/dist/{cli-CHzpFBh-.cjs.map → cli-5k6kV_i4.cjs.map} +1 -1
  31. package/dist/{cli-DDY3jfTt.mjs → cli-H0uB_znq.mjs} +2 -2
  32. package/dist/{cli-DDY3jfTt.mjs.map → cli-H0uB_znq.mjs.map} +1 -1
  33. package/dist/{compose-CDC2bCme.mjs → compose-BEiJWrB7.mjs} +3 -3
  34. package/dist/{compose-CDC2bCme.mjs.map → compose-BEiJWrB7.mjs.map} +1 -1
  35. package/dist/{compose-ByIbIrzs.cjs → compose-CYeMW2_o.cjs} +3 -3
  36. package/dist/{compose-ByIbIrzs.cjs.map → compose-CYeMW2_o.cjs.map} +1 -1
  37. package/dist/deploy/public.cjs +6 -6
  38. package/dist/deploy/public.mjs +6 -6
  39. package/dist/{deploy-DLq9-m6H.mjs → deploy-Cyx5fSb7.mjs} +5 -5
  40. package/dist/{deploy-DLq9-m6H.mjs.map → deploy-Cyx5fSb7.mjs.map} +1 -1
  41. package/dist/{deploy-Dl_NavYE.cjs → deploy-Q5qo1OP-.cjs} +5 -5
  42. package/dist/{deploy-Dl_NavYE.cjs.map → deploy-Q5qo1OP-.cjs.map} +1 -1
  43. package/dist/{dns-pzftumxg.mjs → dns-CsApfzLc.mjs} +2 -2
  44. package/dist/{dns-pzftumxg.mjs.map → dns-CsApfzLc.mjs.map} +1 -1
  45. package/dist/{dns-C2TGXUF4.cjs → dns-v9viladZ.cjs} +2 -2
  46. package/dist/{dns-C2TGXUF4.cjs.map → dns-v9viladZ.cjs.map} +1 -1
  47. package/dist/{dokploy-B7-OwY2s.mjs → dokploy-DfUo_QRw.mjs} +2 -2
  48. package/dist/{dokploy-B7-OwY2s.mjs.map → dokploy-DfUo_QRw.mjs.map} +1 -1
  49. package/dist/{dokploy-OqBh8G4g.cjs → dokploy-Di7rC2z0.cjs} +2 -2
  50. package/dist/{dokploy-OqBh8G4g.cjs.map → dokploy-Di7rC2z0.cjs.map} +1 -1
  51. package/dist/{eas-Cne743nQ.cjs → eas-CIvNT6qa.cjs} +2 -2
  52. package/dist/{eas-Cne743nQ.cjs.map → eas-CIvNT6qa.cjs.map} +1 -1
  53. package/dist/{eas-DEpwISrQ.mjs → eas-CTfRmndW.mjs} +2 -2
  54. package/dist/{eas-DEpwISrQ.mjs.map → eas-CTfRmndW.mjs.map} +1 -1
  55. package/dist/{encryption-DoVnVU82.cjs → encryption--U4NsK7N.cjs} +2 -2
  56. package/dist/{encryption-DoVnVU82.cjs.map → encryption--U4NsK7N.cjs.map} +1 -1
  57. package/dist/{encryption-DJv_jEXi.mjs → encryption-BpTPmd7h.mjs} +2 -2
  58. package/dist/{encryption-DJv_jEXi.mjs.map → encryption-BpTPmd7h.mjs.map} +1 -1
  59. package/dist/{engine-Db71-OZL.mjs → engine-BFmHfek1.mjs} +3 -3
  60. package/dist/{engine-Db71-OZL.mjs.map → engine-BFmHfek1.mjs.map} +1 -1
  61. package/dist/{engine-CJhQb50j.cjs → engine-Ovl1PB6i.cjs} +3 -3
  62. package/dist/{engine-CJhQb50j.cjs.map → engine-Ovl1PB6i.cjs.map} +1 -1
  63. package/dist/{github-C3gblUvr.cjs → github-CTlPQ5Ra.cjs} +4 -4
  64. package/dist/{github-C3gblUvr.cjs.map → github-CTlPQ5Ra.cjs.map} +1 -1
  65. package/dist/{github-BMakI-k3.mjs → github-tWAGuR9i.mjs} +4 -4
  66. package/dist/{github-BMakI-k3.mjs.map → github-tWAGuR9i.mjs.map} +1 -1
  67. package/dist/{googlePlay-B6uiHi65.cjs → googlePlay-BNH729It.cjs} +3 -3
  68. package/dist/{googlePlay-B6uiHi65.cjs.map → googlePlay-BNH729It.cjs.map} +1 -1
  69. package/dist/{googlePlay-BYZ-J5sC.mjs → googlePlay-m1De-qIJ.mjs} +3 -3
  70. package/dist/{googlePlay-BYZ-J5sC.mjs.map → googlePlay-m1De-qIJ.mjs.map} +1 -1
  71. package/dist/index.cjs +104 -68
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.mjs +104 -68
  74. package/dist/index.mjs.map +1 -1
  75. package/dist/{migrate-CQUGbptv.mjs → migrate-4B4RgAjU.mjs} +2 -2
  76. package/dist/{migrate-CQUGbptv.mjs.map → migrate-4B4RgAjU.mjs.map} +1 -1
  77. package/dist/{migrate-bDQzFvBN.cjs → migrate-BYEmpoIg.cjs} +2 -2
  78. package/dist/{migrate-bDQzFvBN.cjs.map → migrate-BYEmpoIg.cjs.map} +1 -1
  79. package/dist/{migrate-GLBZlrlJ.cjs → migrate-DZ2--SaO.cjs} +2 -2
  80. package/dist/{migrate-GLBZlrlJ.cjs.map → migrate-DZ2--SaO.cjs.map} +1 -1
  81. package/dist/{migrate-xHYaeBkd.mjs → migrate-X0Y7fz3N.mjs} +2 -2
  82. package/dist/{migrate-xHYaeBkd.mjs.map → migrate-X0Y7fz3N.mjs.map} +1 -1
  83. package/dist/{publish-SDG1WfYn.cjs → publish-BUvYjiZH.cjs} +2 -2
  84. package/dist/{publish-SDG1WfYn.cjs.map → publish-BUvYjiZH.cjs.map} +1 -1
  85. package/dist/{publish-CSctCNf4.mjs → publish-hhgraEhO.mjs} +2 -2
  86. package/dist/{publish-CSctCNf4.mjs.map → publish-hhgraEhO.mjs.map} +1 -1
  87. package/dist/{resolve-wG49Woex.cjs → resolve-DdFKTx8l.cjs} +3 -3
  88. package/dist/{resolve-wG49Woex.cjs.map → resolve-DdFKTx8l.cjs.map} +1 -1
  89. package/dist/{resolve-CxDqDmCD.mjs → resolve-DotGHvtX.mjs} +3 -3
  90. package/dist/{resolve-CxDqDmCD.mjs.map → resolve-DotGHvtX.mjs.map} +1 -1
  91. package/dist/{rollback-BQgEdr_p.mjs → rollback-9t9G3c_u.mjs} +4 -4
  92. package/dist/{rollback-BQgEdr_p.mjs.map → rollback-9t9G3c_u.mjs.map} +1 -1
  93. package/dist/{rollback-Cw-Z0mWn.cjs → rollback-D4xeUQvG.cjs} +4 -4
  94. package/dist/{rollback-Cw-Z0mWn.cjs.map → rollback-D4xeUQvG.cjs.map} +1 -1
  95. package/dist/{seeds-CIk0l4SC.mjs → seeds-CRkKeyW0.mjs} +19 -6
  96. package/dist/seeds-CRkKeyW0.mjs.map +1 -0
  97. package/dist/{seeds-Dk8YkLkM.cjs → seeds-DbKNcs3o.cjs} +19 -6
  98. package/dist/seeds-DbKNcs3o.cjs.map +1 -0
  99. package/dist/{serverSetup-BCbD8ksJ.cjs → serverSetup-CcWgNtiC.cjs} +93 -21
  100. package/dist/serverSetup-CcWgNtiC.cjs.map +1 -0
  101. package/dist/{serverSetup--O61SRvg.mjs → serverSetup-DlRKRfTi.mjs} +93 -21
  102. package/dist/serverSetup-DlRKRfTi.mjs.map +1 -0
  103. package/dist/{sniffer-2aK7cB2u.cjs → sniffer-B6T6za4n.cjs} +6 -6
  104. package/dist/{sniffer-2aK7cB2u.cjs.map → sniffer-B6T6za4n.cjs.map} +1 -1
  105. package/dist/{sniffer-CABMO-Bh.mjs → sniffer-CakkKKqQ.mjs} +6 -6
  106. package/dist/{sniffer-CABMO-Bh.mjs.map → sniffer-CakkKKqQ.mjs.map} +1 -1
  107. package/dist/{stage-ClKTHQoM.mjs → stage-Mp6qQIoD.mjs} +4 -4
  108. package/dist/{stage-ClKTHQoM.mjs.map → stage-Mp6qQIoD.mjs.map} +1 -1
  109. package/dist/{stage-DrCS0vKD.cjs → stage-W3PUt7ar.cjs} +4 -4
  110. package/dist/{stage-DrCS0vKD.cjs.map → stage-W3PUt7ar.cjs.map} +1 -1
  111. package/dist/{stages-DJrOYLZ-.mjs → stages-Cb9rqRbN.mjs} +16 -2
  112. package/dist/stages-Cb9rqRbN.mjs.map +1 -0
  113. package/dist/{stages-_3RVNG83.cjs → stages-D4SnG5yl.cjs} +16 -2
  114. package/dist/stages-D4SnG5yl.cjs.map +1 -0
  115. package/dist/target/public.cjs +1 -1
  116. package/dist/target/public.mjs +1 -1
  117. package/package.json +15 -15
  118. package/dist/seeds-CIk0l4SC.mjs.map +0 -1
  119. package/dist/seeds-Dk8YkLkM.cjs.map +0 -1
  120. package/dist/serverSetup--O61SRvg.mjs.map +0 -1
  121. package/dist/serverSetup-BCbD8ksJ.cjs.map +0 -1
  122. package/dist/stages-DJrOYLZ-.mjs.map +0 -1
  123. package/dist/stages-_3RVNG83.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
2
2
  import { N as assertDeployedStage, ct as UnknownSecretsStoreProvider, st as SECRETS_STORE_PROVIDERS, ut as s3SecretsLocation } from "./workspace-D1QbNd55.mjs";
3
- import { It as secretsStoreFor, Lt as storeFromConfig } from "./seeds-CIk0l4SC.mjs";
3
+ import { It as secretsStoreFor, Lt as storeFromConfig } from "./seeds-CRkKeyW0.mjs";
4
4
  import { loadWorkspaceSettings } from "./config.mjs";
5
5
  import { n as secretsParameterName } from "./aws-8QQ8n-t6.mjs";
6
6
  import { t as secretsManagerSecretName } from "./secretsManager-CQORubp9.mjs";
@@ -178,4 +178,4 @@ async function secretsMigrateCommand(options) {
178
178
  //#endregion
179
179
  export { secretsMigrateCommand };
180
180
 
181
- //# sourceMappingURL=migrate-CQUGbptv.mjs.map
181
+ //# sourceMappingURL=migrate-4B4RgAjU.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-CQUGbptv.mjs","names":[],"sources":["../src/secrets/migrate.ts"],"sourcesContent":["/**\n * `gkm secrets:migrate` — copy a deployed stage's secrets from the store\n * `secrets.store` names to another one, whole: service passwords, URLs and\n * custom keys alike, so nothing is regenerated and running services keep\n * their credentials.\n *\n * It copies, reads the copy back and compares it, and nothing more. The source\n * is never deleted, and gkm.config.ts is not edited: pointing `secrets.store`\n * at the new store is the switch, and the old copy is deleted by hand — with\n * the command this prints — once deploys read from the new one. Run again, a\n * target already holding the same secrets is verified and left alone.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { loadWorkspaceSettings } from '../config.js';\nimport { GkmError } from '../errors';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { secretsParameterName } from './aws.js';\nimport { secretsManagerSecretName } from './secretsManager.js';\nimport {\n\tSECRETS_STORE_PROVIDERS,\n\ttype SecretsStore,\n\ttype SecretsStoreConfig,\n\ttype SecretsStoreOptions,\n\ttype SecretsStoreProvider,\n\ts3SecretsLocation,\n\tsecretsStoreFor,\n\tstoreFromConfig,\n\tUnknownSecretsStoreProvider,\n} from './store.js';\nimport type { StageSecrets } from './types.js';\n\nconst logger = console;\n\nexport interface SecretsMigrateOptions {\n\tstage: string;\n\t/** The store to copy to: `'file'`, `'s3'`, `'ssm'` or `'secrets-manager'`. */\n\tto: string;\n\t/**\n\t * The target's AWS region; defaults to the configured store's — for `s3`,\n\t * to the S3 state's first.\n\t */\n\tregion?: string;\n\t/** Overwrite a stage the target already holds different secrets for. */\n\tforce?: boolean;\n\t/** The AWS profile for the stage's account, for both stores. */\n\tprofile?: string;\n}\n\n/** An AWS store to copy to, with no region given and none configured. */\nexport class MigrateTargetNeedsRegion extends GkmError {\n\tconstructor(readonly to: string) {\n\t\tsuper(\n\t\t\t`Copying to ${to} needs a region, and secrets.store has none to reuse. ` +\n\t\t\t\t`Pass it: gkm secrets:migrate --to ${to} --region <region> --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'MigrateTargetNeedsRegion';\n\t}\n}\n\n/** The target is the store the stage is already read from. */\nexport class MigrateTargetIsSource extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`Stage \"${stage}\" is already kept in ${to}, so there is nothing to copy. ` +\n\t\t\t\t'Name a different store with --to, or a different region with --region.',\n\t\t);\n\t\tthis.name = 'MigrateTargetIsSource';\n\t}\n}\n\n/** The configured store holds nothing for the stage. */\nexport class NoSecretsToMigrate extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly store: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${store} store holds no secrets for stage \"${stage}\", so there is nothing to copy. ` +\n\t\t\t\t`Check secrets.store in gkm.config.ts still names the store the stage is in, ` +\n\t\t\t\t`or create the stage there with gkm secrets:init --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoSecretsToMigrate';\n\t}\n}\n\n/**\n * The target already holds different secrets for the stage, and `--force` was\n * not given.\n */\nexport class MigrateTargetHoldsStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${to} store already holds different secrets for stage \"${stage}\". ` +\n\t\t\t\t'Pass --force to replace them with the ones being copied.',\n\t\t);\n\t\tthis.name = 'MigrateTargetHoldsStage';\n\t}\n}\n\n/** The copy read back from the target is not what was copied. */\nexport class MigratedSecretsDiffer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t\t/** The keys whose values differ, or that only one side has. */\n\t\treadonly keys: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" read back from ${to} differ from the ones copied` +\n\t\t\t\t(keys.length ? ` (${keys.join(', ')})` : '') +\n\t\t\t\t'. The source is untouched and secrets.store still names it; run the migration again, ' +\n\t\t\t\t'or with --force if something else is writing to the target.',\n\t\t);\n\t\tthis.name = 'MigratedSecretsDiffer';\n\t}\n}\n\n/** The region the configured store is in, where it is an AWS one. */\nfunction configuredRegion(\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): string | undefined {\n\tif (typeof configured !== 'object') return undefined;\n\tif (configured.provider === 's3') {\n\t\treturn s3SecretsLocation(configured, state).region;\n\t}\n\t// A custom store has no region to reuse.\n\treturn 'region' in configured ? configured.region : undefined;\n}\n\n/** The store a `--to` name and region describe. */\nfunction targetConfig(\n\tto: string,\n\tregion: string | undefined,\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): SecretsStoreConfig {\n\tif (!(SECRETS_STORE_PROVIDERS as readonly string[]).includes(to)) {\n\t\tthrow new UnknownSecretsStoreProvider(to);\n\t}\n\tconst provider = to as SecretsStoreProvider;\n\tif (provider === 'file') return 'file';\n\n\tconst fallback = configuredRegion(configured, state);\n\n\tif (provider === 's3') {\n\t\t// The project bucket beside an S3 state is in the state's region.\n\t\tconst inherits =\n\t\t\ttypeof state === 'object' &&\n\t\t\tstate !== null &&\n\t\t\t(state as { provider?: unknown }).provider === 's3';\n\t\tconst resolved = region ?? (inherits ? undefined : fallback);\n\t\tif (!resolved && !inherits) throw new MigrateTargetNeedsRegion(provider);\n\t\treturn { provider, ...(resolved ? { region: resolved } : {}) };\n\t}\n\n\tconst resolved = region ?? fallback;\n\tif (!resolved) throw new MigrateTargetNeedsRegion(provider);\n\treturn { provider, region: resolved };\n}\n\n/** Whether two configs name the same place. */\nfunction samePlace(\n\ta: SecretsStoreConfig,\n\tb: SecretsStoreConfig,\n\tstate: unknown,\n): boolean {\n\tif (typeof a === 'string' || typeof b === 'string') return a === b;\n\tif (a.provider !== b.provider) return false;\n\tif (a.provider === 's3' && b.provider === 's3') {\n\t\treturn isDeepStrictEqual(\n\t\t\ts3SecretsLocation(a, state),\n\t\t\ts3SecretsLocation(b, state),\n\t\t);\n\t}\n\treturn 'region' in a && 'region' in b && a.region === b.region;\n}\n\n/** The `secrets.store` value to write in gkm.config.ts. */\nfunction configLine(target: SecretsStoreConfig): string {\n\tif (typeof target === 'string') return `'${target}'`;\n\tconst region =\n\t\t'region' in target && target.region ? `, region: '${target.region}'` : '';\n\treturn `{ provider: '${String(target.provider)}'${region} }`;\n}\n\n/** The keys whose values differ between two documents, as `section.KEY`. */\nexport function differingKeys(\n\ta: StageSecrets,\n\tb: StageSecrets | null,\n): string[] {\n\tconst keys: string[] = [];\n\tfor (const section of ['custom', 'urls', 'services'] as const) {\n\t\tconst left = a[section] as Record<string, unknown>;\n\t\tconst right = (b?.[section] ?? {}) as Record<string, unknown>;\n\t\tfor (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {\n\t\t\tif (!isDeepStrictEqual(left[key], right[key])) {\n\t\t\t\tkeys.push(`${section}.${key}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn keys.sort();\n}\n\n/**\n * The command that deletes the stage's copy in the source store, for the\n * person to run once deploys read from the target — gkm never runs it.\n */\nexport function deleteSourceCommand(\n\tworkspace: { name: string },\n\tconfigured: SecretsStoreConfig,\n\tsource: SecretsStore,\n\tstage: string,\n\tprofile: string | undefined,\n): string | null {\n\tconst as = profile ? ` --profile ${profile}` : '';\n\tif (configured === 'file') return `rm .gkm/secrets/${stage}.json`;\n\tswitch (configured.provider) {\n\t\tcase 'ssm':\n\t\t\treturn `aws ssm delete-parameter --name ${secretsParameterName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 'secrets-manager':\n\t\t\treturn `aws secretsmanager delete-secret --secret-id ${secretsManagerSecretName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 's3': {\n\t\t\tconst location = (\n\t\t\t\tsource as { location?(stage: string): string }\n\t\t\t).location?.(stage);\n\t\t\tif (!location) return null;\n\t\t\t// Every version, as the bucket keeps them: only a delete marker is added.\n\t\t\treturn `aws s3 rm ${location}${as}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nexport async function secretsMigrateCommand(\n\toptions: SecretsMigrateOptions,\n): Promise<void> {\n\tconst { stage, to } = options;\n\tconst workspace = await loadWorkspaceSettings();\n\tassertDeployedStage(workspace.stages, stage);\n\n\tconst configured = workspace.secrets.store ?? 'file';\n\tconst target = targetConfig(to, options.region, configured, workspace.state);\n\tif (samePlace(configured, target, workspace.state)) {\n\t\tthrow new MigrateTargetIsSource(stage, to);\n\t}\n\n\tconst storeOptions: SecretsStoreOptions = options.profile\n\t\t? { profile: options.profile }\n\t\t: {};\n\tconst source = await secretsStoreFor(workspace, stage, storeOptions);\n\tconst destination = await storeFromConfig(workspace, target, storeOptions);\n\n\tconst secrets = await source.read(stage);\n\tif (!secrets) throw new NoSecretsToMigrate(stage, source.name);\n\n\tconst held = await destination.read(stage);\n\tconst already = held !== null && isDeepStrictEqual(held, secrets);\n\tif (held && !already && !options.force) {\n\t\tthrow new MigrateTargetHoldsStage(stage, destination.name);\n\t}\n\tif (!already) await destination.write(stage, secrets);\n\n\t// Read back through a store of its own, so nothing cached answers.\n\tconst copied = await (\n\t\tawait storeFromConfig(workspace, target, storeOptions)\n\t).read(stage);\n\tif (!isDeepStrictEqual(copied, secrets)) {\n\t\tthrow new MigratedSecretsDiffer(\n\t\t\tstage,\n\t\t\tdestination.name,\n\t\t\tdifferingKeys(secrets, copied),\n\t\t);\n\t}\n\n\tconst count =\n\t\tObject.keys(secrets.custom).length +\n\t\tObject.keys(secrets.urls).length +\n\t\tObject.keys(secrets.services).length;\n\tlogger.log(\n\t\talready\n\t\t\t? `\\n✓ ${destination.name} already holds the same secrets for stage \"${stage}\" as ${source.name}; nothing copied`\n\t\t\t: `\\n✓ Copied the secrets for stage \"${stage}\" from ${source.name} to ${destination.name}`,\n\t);\n\tlogger.log(\n\t\t` Verified: ${count} keys read back from ${destination.name}, equal.`,\n\t);\n\tlogger.log(\n\t\t`\\n Next: set secrets.store to ${configLine(target)} in gkm.config.ts, so commands and deploys read from ${destination.name}.`,\n\t);\n\tconst remove = deleteSourceCommand(\n\t\tworkspace,\n\t\tconfigured,\n\t\tsource,\n\t\tstage,\n\t\toptions.profile,\n\t);\n\tlogger.log(\n\t\t` The copy in ${source.name} is left as it was. Once deploys read from ${destination.name}, delete it` +\n\t\t\t(remove ? ` with:\\n\\n ${remove}\\n` : '.'),\n\t);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-4B4RgAjU.mjs","names":[],"sources":["../src/secrets/migrate.ts"],"sourcesContent":["/**\n * `gkm secrets:migrate` — copy a deployed stage's secrets from the store\n * `secrets.store` names to another one, whole: service passwords, URLs and\n * custom keys alike, so nothing is regenerated and running services keep\n * their credentials.\n *\n * It copies, reads the copy back and compares it, and nothing more. The source\n * is never deleted, and gkm.config.ts is not edited: pointing `secrets.store`\n * at the new store is the switch, and the old copy is deleted by hand — with\n * the command this prints — once deploys read from the new one. Run again, a\n * target already holding the same secrets is verified and left alone.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { loadWorkspaceSettings } from '../config.js';\nimport { GkmError } from '../errors';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { secretsParameterName } from './aws.js';\nimport { secretsManagerSecretName } from './secretsManager.js';\nimport {\n\tSECRETS_STORE_PROVIDERS,\n\ttype SecretsStore,\n\ttype SecretsStoreConfig,\n\ttype SecretsStoreOptions,\n\ttype SecretsStoreProvider,\n\ts3SecretsLocation,\n\tsecretsStoreFor,\n\tstoreFromConfig,\n\tUnknownSecretsStoreProvider,\n} from './store.js';\nimport type { StageSecrets } from './types.js';\n\nconst logger = console;\n\nexport interface SecretsMigrateOptions {\n\tstage: string;\n\t/** The store to copy to: `'file'`, `'s3'`, `'ssm'` or `'secrets-manager'`. */\n\tto: string;\n\t/**\n\t * The target's AWS region; defaults to the configured store's — for `s3`,\n\t * to the S3 state's first.\n\t */\n\tregion?: string;\n\t/** Overwrite a stage the target already holds different secrets for. */\n\tforce?: boolean;\n\t/** The AWS profile for the stage's account, for both stores. */\n\tprofile?: string;\n}\n\n/** An AWS store to copy to, with no region given and none configured. */\nexport class MigrateTargetNeedsRegion extends GkmError {\n\tconstructor(readonly to: string) {\n\t\tsuper(\n\t\t\t`Copying to ${to} needs a region, and secrets.store has none to reuse. ` +\n\t\t\t\t`Pass it: gkm secrets:migrate --to ${to} --region <region> --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'MigrateTargetNeedsRegion';\n\t}\n}\n\n/** The target is the store the stage is already read from. */\nexport class MigrateTargetIsSource extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`Stage \"${stage}\" is already kept in ${to}, so there is nothing to copy. ` +\n\t\t\t\t'Name a different store with --to, or a different region with --region.',\n\t\t);\n\t\tthis.name = 'MigrateTargetIsSource';\n\t}\n}\n\n/** The configured store holds nothing for the stage. */\nexport class NoSecretsToMigrate extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly store: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${store} store holds no secrets for stage \"${stage}\", so there is nothing to copy. ` +\n\t\t\t\t`Check secrets.store in gkm.config.ts still names the store the stage is in, ` +\n\t\t\t\t`or create the stage there with gkm secrets:init --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoSecretsToMigrate';\n\t}\n}\n\n/**\n * The target already holds different secrets for the stage, and `--force` was\n * not given.\n */\nexport class MigrateTargetHoldsStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${to} store already holds different secrets for stage \"${stage}\". ` +\n\t\t\t\t'Pass --force to replace them with the ones being copied.',\n\t\t);\n\t\tthis.name = 'MigrateTargetHoldsStage';\n\t}\n}\n\n/** The copy read back from the target is not what was copied. */\nexport class MigratedSecretsDiffer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t\t/** The keys whose values differ, or that only one side has. */\n\t\treadonly keys: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" read back from ${to} differ from the ones copied` +\n\t\t\t\t(keys.length ? ` (${keys.join(', ')})` : '') +\n\t\t\t\t'. The source is untouched and secrets.store still names it; run the migration again, ' +\n\t\t\t\t'or with --force if something else is writing to the target.',\n\t\t);\n\t\tthis.name = 'MigratedSecretsDiffer';\n\t}\n}\n\n/** The region the configured store is in, where it is an AWS one. */\nfunction configuredRegion(\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): string | undefined {\n\tif (typeof configured !== 'object') return undefined;\n\tif (configured.provider === 's3') {\n\t\treturn s3SecretsLocation(configured, state).region;\n\t}\n\t// A custom store has no region to reuse.\n\treturn 'region' in configured ? configured.region : undefined;\n}\n\n/** The store a `--to` name and region describe. */\nfunction targetConfig(\n\tto: string,\n\tregion: string | undefined,\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): SecretsStoreConfig {\n\tif (!(SECRETS_STORE_PROVIDERS as readonly string[]).includes(to)) {\n\t\tthrow new UnknownSecretsStoreProvider(to);\n\t}\n\tconst provider = to as SecretsStoreProvider;\n\tif (provider === 'file') return 'file';\n\n\tconst fallback = configuredRegion(configured, state);\n\n\tif (provider === 's3') {\n\t\t// The project bucket beside an S3 state is in the state's region.\n\t\tconst inherits =\n\t\t\ttypeof state === 'object' &&\n\t\t\tstate !== null &&\n\t\t\t(state as { provider?: unknown }).provider === 's3';\n\t\tconst resolved = region ?? (inherits ? undefined : fallback);\n\t\tif (!resolved && !inherits) throw new MigrateTargetNeedsRegion(provider);\n\t\treturn { provider, ...(resolved ? { region: resolved } : {}) };\n\t}\n\n\tconst resolved = region ?? fallback;\n\tif (!resolved) throw new MigrateTargetNeedsRegion(provider);\n\treturn { provider, region: resolved };\n}\n\n/** Whether two configs name the same place. */\nfunction samePlace(\n\ta: SecretsStoreConfig,\n\tb: SecretsStoreConfig,\n\tstate: unknown,\n): boolean {\n\tif (typeof a === 'string' || typeof b === 'string') return a === b;\n\tif (a.provider !== b.provider) return false;\n\tif (a.provider === 's3' && b.provider === 's3') {\n\t\treturn isDeepStrictEqual(\n\t\t\ts3SecretsLocation(a, state),\n\t\t\ts3SecretsLocation(b, state),\n\t\t);\n\t}\n\treturn 'region' in a && 'region' in b && a.region === b.region;\n}\n\n/** The `secrets.store` value to write in gkm.config.ts. */\nfunction configLine(target: SecretsStoreConfig): string {\n\tif (typeof target === 'string') return `'${target}'`;\n\tconst region =\n\t\t'region' in target && target.region ? `, region: '${target.region}'` : '';\n\treturn `{ provider: '${String(target.provider)}'${region} }`;\n}\n\n/** The keys whose values differ between two documents, as `section.KEY`. */\nexport function differingKeys(\n\ta: StageSecrets,\n\tb: StageSecrets | null,\n): string[] {\n\tconst keys: string[] = [];\n\tfor (const section of ['custom', 'urls', 'services'] as const) {\n\t\tconst left = a[section] as Record<string, unknown>;\n\t\tconst right = (b?.[section] ?? {}) as Record<string, unknown>;\n\t\tfor (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {\n\t\t\tif (!isDeepStrictEqual(left[key], right[key])) {\n\t\t\t\tkeys.push(`${section}.${key}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn keys.sort();\n}\n\n/**\n * The command that deletes the stage's copy in the source store, for the\n * person to run once deploys read from the target — gkm never runs it.\n */\nexport function deleteSourceCommand(\n\tworkspace: { name: string },\n\tconfigured: SecretsStoreConfig,\n\tsource: SecretsStore,\n\tstage: string,\n\tprofile: string | undefined,\n): string | null {\n\tconst as = profile ? ` --profile ${profile}` : '';\n\tif (configured === 'file') return `rm .gkm/secrets/${stage}.json`;\n\tswitch (configured.provider) {\n\t\tcase 'ssm':\n\t\t\treturn `aws ssm delete-parameter --name ${secretsParameterName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 'secrets-manager':\n\t\t\treturn `aws secretsmanager delete-secret --secret-id ${secretsManagerSecretName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 's3': {\n\t\t\tconst location = (\n\t\t\t\tsource as { location?(stage: string): string }\n\t\t\t).location?.(stage);\n\t\t\tif (!location) return null;\n\t\t\t// Every version, as the bucket keeps them: only a delete marker is added.\n\t\t\treturn `aws s3 rm ${location}${as}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nexport async function secretsMigrateCommand(\n\toptions: SecretsMigrateOptions,\n): Promise<void> {\n\tconst { stage, to } = options;\n\tconst workspace = await loadWorkspaceSettings();\n\tassertDeployedStage(workspace.stages, stage);\n\n\tconst configured = workspace.secrets.store ?? 'file';\n\tconst target = targetConfig(to, options.region, configured, workspace.state);\n\tif (samePlace(configured, target, workspace.state)) {\n\t\tthrow new MigrateTargetIsSource(stage, to);\n\t}\n\n\tconst storeOptions: SecretsStoreOptions = options.profile\n\t\t? { profile: options.profile }\n\t\t: {};\n\tconst source = await secretsStoreFor(workspace, stage, storeOptions);\n\tconst destination = await storeFromConfig(workspace, target, storeOptions);\n\n\tconst secrets = await source.read(stage);\n\tif (!secrets) throw new NoSecretsToMigrate(stage, source.name);\n\n\tconst held = await destination.read(stage);\n\tconst already = held !== null && isDeepStrictEqual(held, secrets);\n\tif (held && !already && !options.force) {\n\t\tthrow new MigrateTargetHoldsStage(stage, destination.name);\n\t}\n\tif (!already) await destination.write(stage, secrets);\n\n\t// Read back through a store of its own, so nothing cached answers.\n\tconst copied = await (\n\t\tawait storeFromConfig(workspace, target, storeOptions)\n\t).read(stage);\n\tif (!isDeepStrictEqual(copied, secrets)) {\n\t\tthrow new MigratedSecretsDiffer(\n\t\t\tstage,\n\t\t\tdestination.name,\n\t\t\tdifferingKeys(secrets, copied),\n\t\t);\n\t}\n\n\tconst count =\n\t\tObject.keys(secrets.custom).length +\n\t\tObject.keys(secrets.urls).length +\n\t\tObject.keys(secrets.services).length;\n\tlogger.log(\n\t\talready\n\t\t\t? `\\n✓ ${destination.name} already holds the same secrets for stage \"${stage}\" as ${source.name}; nothing copied`\n\t\t\t: `\\n✓ Copied the secrets for stage \"${stage}\" from ${source.name} to ${destination.name}`,\n\t);\n\tlogger.log(\n\t\t` Verified: ${count} keys read back from ${destination.name}, equal.`,\n\t);\n\tlogger.log(\n\t\t`\\n Next: set secrets.store to ${configLine(target)} in gkm.config.ts, so commands and deploys read from ${destination.name}.`,\n\t);\n\tconst remove = deleteSourceCommand(\n\t\tworkspace,\n\t\tconfigured,\n\t\tsource,\n\t\tstage,\n\t\toptions.profile,\n\t);\n\tlogger.log(\n\t\t` The copy in ${source.name} is left as it was. Once deploys read from ${destination.name}, delete it` +\n\t\t\t(remove ? ` with:\\n\\n ${remove}\\n` : '.'),\n\t);\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  const require_errors = require("./errors-DMC4Umsb.cjs");
2
2
  const require_workspace = require("./workspace-BwqQnool.cjs");
3
- const require_seeds = require("./seeds-Dk8YkLkM.cjs");
3
+ const require_seeds = require("./seeds-DbKNcs3o.cjs");
4
4
  const require_discover = require("./discover-Bsr7HjqJ.cjs");
5
5
  const require_config = require("./config.cjs");
6
6
  const require_databases = require("./databases-DGALFDG3.cjs");
@@ -268,4 +268,4 @@ exports.prepareDevDatabases = prepareDevDatabases;
268
268
  exports.reportPendingMigrations = reportPendingMigrations;
269
269
  exports.seedCommand = seedCommand;
270
270
 
271
- //# sourceMappingURL=migrate-bDQzFvBN.cjs.map
271
+ //# sourceMappingURL=migrate-BYEmpoIg.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-bDQzFvBN.cjs","names":["GkmError","TEST_STAGE","loadWorkspaceConfig","discover","constructGlobs","reconcileWorkspace","migrateDatabases","migrateAndSeed","pendingMigrations","migrationTargets","canonicalId","NoSuchMigrationTarget","stamp","slug","loadSecretsForApp","secretsStoreFor","EnvironmentParser","serviceContext","join","mkdir","writeFile"],"sources":["../src/migrate/index.ts"],"sourcesContent":["/**\n * `gkm migrate`, `gkm seed` and `gkm migration` — applying a construct's\n * migrations, running its seeds, and writing its next migration.\n *\n * Both work on a stage this machine reconciles: its containers up, its roles\n * provisioned, its owner URLs resolved. A deployed stage is migrated by the\n * deploy that ships it, which is the one thing that can reach its database.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype MigrationTarget,\n\tmigrationTargets,\n} from '@geekmidas/manifest';\nimport {\n\ttype ServiceRegisterOptions,\n\tserviceContext,\n} from '@geekmidas/services';\nimport { loadWorkspaceConfig } from '../config';\nimport { loadSecretsForApp } from '../credentials';\nimport { GkmError } from '../errors';\nimport { type ConstructSource, discover } from '../reconcile/discover';\nimport { constructGlobs, reconcileWorkspace } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { TEST_STAGE } from '../workspace/stages';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport {\n\ttype MigrationRun,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoSuchMigrationTarget,\n\tpendingMigrations,\n\ttype SeedRun,\n} from './databases';\nimport { slug, stamp } from './names';\n\nexport {\n\tKyselyNotFound,\n\tMigrationFailed,\n\ttype MigrationRun,\n\tMigrationsOutsideFolder,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoOwnerCredential,\n\tNoSuchMigrationTarget,\n\ttype PendingMigrations,\n\tpendingMigrations,\n\tSeedFailed,\n\ttype SeedOptions,\n\ttype SeedRun,\n\tseedDatabases,\n\tUnknownDatabaseFolder,\n\tUnknownMigrationFolder,\n} from './databases';\nexport { slug, stamp } from './names';\nexport {\n\tDuplicateMigration,\n\tMigrationHasNoUp,\n\tSeedHasNoSeed,\n} from './provider';\n\nconst logger = console;\n\n/** `gkm migrate --stage` named a stage only a deploy can reach. */\nexport class MigrateDeployedStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly local: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is a deployed stage, and its database is migrated by ` +\n\t\t\t\t`\\`gkm deploy --stage ${stage}\\`, which is what can reach it. ` +\n\t\t\t\t`\\`gkm migrate\\` runs against '${local}' or '${TEST_STAGE}'.`,\n\t\t);\n\t\tthis.name = 'MigrateDeployedStage';\n\t}\n}\n\n/** `gkm migration` for a Kysely database, with no name for the file. */\nexport class MigrationNeedsName extends GkmError {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`Name the migration: \\`gkm migration ${construct} add_workouts\\`. ` +\n\t\t\t\t`The name is what the file — and the history — calls it.`,\n\t\t);\n\t\tthis.name = 'MigrationNeedsName';\n\t}\n}\n\nexport interface MigrateOptions {\n\t/** The stage to migrate: the project's local one, or `test`. */\n\tstage?: string;\n\t/** One construct, by name. All of them when absent. */\n\tconstruct?: string;\n}\n\n/** A workspace's stage, reconciled and ready to migrate. */\nexport interface ReadyStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tsources: Record<string, ConstructSource>;\n\t/** Every URL the stage resolved — the owner URLs among them. */\n\tenv: Record<string, string>;\n\t/** The stage it is: the local one, or `test`. */\n\tstage: string;\n}\n\n/**\n * Reconcile a stage so it can be migrated: containers up, databases, schemas,\n * roles and grants in place, owner URLs resolved.\n *\n * Migrating assumes the roles exist, and this is what makes that true rather\n * than something a developer has to have run first.\n */\nexport async function readyStage(\n\tcwd: string,\n\tstage?: string,\n): Promise<ReadyStage> {\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tconst local = workspace.stages.local;\n\tconst target = stage ?? local;\n\tif (target !== local && target !== TEST_STAGE) {\n\t\tthrow new MigrateDeployedStage(target, local);\n\t}\n\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst reconciled = await reconcileWorkspace(workspace, {\n\t\tstage: target,\n\t\tmanifest,\n\t\tstart: true,\n\t});\n\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tsources,\n\t\tenv: { ...reconciled.env },\n\t\tstage: target,\n\t};\n}\n\n/** `gkm migrate [construct]`. */\nexport async function migrateCommand(\n\toptions: MigrateOptions = {},\n): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst runs = await migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (runs.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to migrate.');\n\t\treturn;\n\t}\n\n\tlogMigrations(runs);\n}\n\n/**\n * `gkm seed [construct]` — migrate, then run the seeds.\n *\n * Always migrates first: a seed is written against the schema its migrations\n * build, and against an older one it fails on a table that is not there yet.\n */\nexport async function seedCommand(options: MigrateOptions = {}): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tstage: ready.stage,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (migrations.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to seed.');\n\t\treturn;\n\t}\n\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\n/** One line per construct: up to date, or what was applied. */\nexport function logMigrations(\n\truns: readonly MigrationRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, applied } of runs) {\n\t\tif (applied.length === 0) {\n\t\t\tlog(`🗄️ ${target.migrations}: up to date`);\n\t\t\tcontinue;\n\t\t}\n\t\tlog(`🗄️ ${target.migrations}: applied ${applied.length}`);\n\t\tfor (const name of applied) log(` ✓ ${name}`);\n\t}\n}\n\n/** One line per construct that has seeds, and each seed it ran. */\nexport function logSeeds(\n\truns: readonly SeedRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, seeded } of runs) {\n\t\tif (seeded.length === 0) continue;\n\t\tlog(`🌱 ${target.seeds}: ran ${seeded.length}`);\n\t\tfor (const name of seeded) log(` ✓ ${name}`);\n\t}\n}\n\n/**\n * What `gkm dev` prints: one line per construct whose folder is ahead of its\n * history, and nothing when every folder is applied.\n *\n * Reported, never applied. A migration being written would otherwise run the\n * moment its file is saved, half-finished, against the database the developer\n * is working in.\n */\nexport async function reportPendingMigrations(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst pending = await pendingMigrations({\n\t\troot: workspace.root,\n\t\tmanifest,\n\t\tsources,\n\t\tenv,\n\t});\n\n\tfor (const { target, pending: names } of pending) {\n\t\tlogger.log(\n\t\t\t`⚠️ ${target.migrations}: ${names.length} pending (${names.join(', ')}) — run gkm migrate`,\n\t\t);\n\t}\n}\n\n/**\n * What `gkm dev --migrate` and `gkm dev --seed` do before the apps start:\n * migrate, and with `seed`, run the seeds after — once, at startup, never on a\n * save, for the reason {@link reportPendingMigrations} gives.\n *\n * A failure here is thrown, unlike the report's: the developer asked for the\n * database to be brought up to date, and apps started against one in an\n * unknown state would fail somewhere less obvious.\n */\nexport async function prepareDevDatabases(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n\t{ seed = false }: { seed?: boolean } = {},\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\tconst options = { root: workspace.root, manifest, sources, env };\n\n\tif (!seed) {\n\t\tlogMigrations(await migrateDatabases(options));\n\t\treturn;\n\t}\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\t...options,\n\t\tstage: workspace.stages.local,\n\t});\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\nexport interface MigrationOptions {\n\t/** The construct the migration is for, by name. */\n\tconstruct: string;\n\t/** What the file is called, after its timestamp. */\n\tname?: string;\n\t/** The file's timestamp; the current time when absent. */\n\tnow?: Date;\n}\n\n/**\n * `gkm migration <construct> [name]` — write the construct's next migration.\n *\n * For a database, an empty `up`/`down` to fill in. For an auth server, what\n * its schema is missing, computed against its tenant once every committed\n * migration has been applied — so the file is only the change, and is\n * reviewed and committed like any other.\n */\nexport async function migrationCommand(\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\tconst ready = await readyStage(process.cwd());\n\tconst targets = migrationTargets(ready.manifest);\n\tconst id = canonicalId(options.construct);\n\n\tconst auth = authConstruct(ready.sources, id);\n\tif (auth) {\n\t\tconst target = targets.find((t) => t.id === auth.databaseId);\n\t\tif (!target) {\n\t\t\tthrow new NoSuchMigrationTarget(auth.databaseId, ids(targets));\n\t\t}\n\t\treturn writeAuthMigration(ready, auth, target, options);\n\t}\n\n\tconst target = targets.find((t) => t.id === id);\n\tif (!target) {\n\t\tthrow new NoSuchMigrationTarget(options.construct, [\n\t\t\t...ids(targets),\n\t\t\t...authIds(ready.sources),\n\t\t]);\n\t}\n\tif (!options.name) throw new MigrationNeedsName(options.construct);\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name)}.ts`,\n\t\tSTUB,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\n/** What an auth construct offers `gkm migration`, duck-typed across copies. */\ninterface AuthConstruct {\n\tid: string;\n\tdatabaseId: string;\n\tpendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined>;\n}\n\nfunction authConstruct(\n\tsources: Readonly<Record<string, ConstructSource>>,\n\tid: string,\n): AuthConstruct | undefined {\n\tconst construct = sources[id]?.construct as\n\t\t| Partial<AuthConstruct>\n\t\t| undefined;\n\treturn construct &&\n\t\ttypeof construct.pendingMigration === 'function' &&\n\t\ttypeof construct.databaseId === 'string'\n\t\t? (construct as AuthConstruct)\n\t\t: undefined;\n}\n\nfunction authIds(sources: Readonly<Record<string, ConstructSource>>): string[] {\n\treturn Object.keys(sources).filter((id) => authConstruct(sources, id));\n}\n\nasync function writeAuthMigration(\n\tready: ReadyStage,\n\tauth: AuthConstruct,\n\ttarget: MigrationTarget,\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\t// Against a tenant already at its committed migrations, so the comparison\n\t// finds only what the files do not yet say.\n\tawait migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tonly: target.id,\n\t});\n\n\t// Its secret and URL come from the stage's secrets; the tenant's owner URL\n\t// from the reconcile above, which wins.\n\tconst stage = ready.workspace.stages.local;\n\tconst secrets = await loadSecretsForApp(\n\t\tawait secretsStoreFor(ready.workspace, stage),\n\t\tstage,\n\t);\n\tconst pending = await auth.pendingMigration({\n\t\tenvParser: new EnvironmentParser({ ...secrets, ...ready.env }),\n\t\tcontext: serviceContext,\n\t});\n\n\tif (!pending) {\n\t\tlogger.log(`✓ ${target.migrations}: ${auth.id} is up to date`);\n\t\treturn undefined;\n\t}\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name ?? 'better_auth')}.sql`,\n\t\t`${pending.trim()}\\n`,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\nconst STUB = `import type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // await db.schema.createTable('…').addColumn(…).execute();\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n // await db.schema.dropTable('…').execute();\n}\n`;\n\nasync function write(\n\troot: string,\n\ttarget: MigrationTarget,\n\tfile: string,\n\tcontent: string,\n): Promise<string> {\n\tconst folder = join(root, target.migrations);\n\tawait mkdir(folder, { recursive: true });\n\tawait writeFile(join(folder, file), content, { flag: 'wx' });\n\treturn `${target.migrations}/${file}`;\n}\n\nfunction ids(targets: readonly MigrationTarget[]): string[] {\n\treturn targets.map((target) => target.id);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-BYEmpoIg.cjs","names":["GkmError","TEST_STAGE","loadWorkspaceConfig","discover","constructGlobs","reconcileWorkspace","migrateDatabases","migrateAndSeed","pendingMigrations","migrationTargets","canonicalId","NoSuchMigrationTarget","stamp","slug","loadSecretsForApp","secretsStoreFor","EnvironmentParser","serviceContext","join","mkdir","writeFile"],"sources":["../src/migrate/index.ts"],"sourcesContent":["/**\n * `gkm migrate`, `gkm seed` and `gkm migration` — applying a construct's\n * migrations, running its seeds, and writing its next migration.\n *\n * Both work on a stage this machine reconciles: its containers up, its roles\n * provisioned, its owner URLs resolved. A deployed stage is migrated by the\n * deploy that ships it, which is the one thing that can reach its database.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype MigrationTarget,\n\tmigrationTargets,\n} from '@geekmidas/manifest';\nimport {\n\ttype ServiceRegisterOptions,\n\tserviceContext,\n} from '@geekmidas/services';\nimport { loadWorkspaceConfig } from '../config';\nimport { loadSecretsForApp } from '../credentials';\nimport { GkmError } from '../errors';\nimport { type ConstructSource, discover } from '../reconcile/discover';\nimport { constructGlobs, reconcileWorkspace } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { TEST_STAGE } from '../workspace/stages';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport {\n\ttype MigrationRun,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoSuchMigrationTarget,\n\tpendingMigrations,\n\ttype SeedRun,\n} from './databases';\nimport { slug, stamp } from './names';\n\nexport {\n\tKyselyNotFound,\n\tMigrationFailed,\n\ttype MigrationRun,\n\tMigrationsOutsideFolder,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoOwnerCredential,\n\tNoSuchMigrationTarget,\n\ttype PendingMigrations,\n\tpendingMigrations,\n\tSeedFailed,\n\ttype SeedOptions,\n\ttype SeedRun,\n\tseedDatabases,\n\tUnknownDatabaseFolder,\n\tUnknownMigrationFolder,\n} from './databases';\nexport { slug, stamp } from './names';\nexport {\n\tDuplicateMigration,\n\tMigrationHasNoUp,\n\tSeedHasNoSeed,\n} from './provider';\n\nconst logger = console;\n\n/** `gkm migrate --stage` named a stage only a deploy can reach. */\nexport class MigrateDeployedStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly local: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is a deployed stage, and its database is migrated by ` +\n\t\t\t\t`\\`gkm deploy --stage ${stage}\\`, which is what can reach it. ` +\n\t\t\t\t`\\`gkm migrate\\` runs against '${local}' or '${TEST_STAGE}'.`,\n\t\t);\n\t\tthis.name = 'MigrateDeployedStage';\n\t}\n}\n\n/** `gkm migration` for a Kysely database, with no name for the file. */\nexport class MigrationNeedsName extends GkmError {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`Name the migration: \\`gkm migration ${construct} add_workouts\\`. ` +\n\t\t\t\t`The name is what the file — and the history — calls it.`,\n\t\t);\n\t\tthis.name = 'MigrationNeedsName';\n\t}\n}\n\nexport interface MigrateOptions {\n\t/** The stage to migrate: the project's local one, or `test`. */\n\tstage?: string;\n\t/** One construct, by name. All of them when absent. */\n\tconstruct?: string;\n}\n\n/** A workspace's stage, reconciled and ready to migrate. */\nexport interface ReadyStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tsources: Record<string, ConstructSource>;\n\t/** Every URL the stage resolved — the owner URLs among them. */\n\tenv: Record<string, string>;\n\t/** The stage it is: the local one, or `test`. */\n\tstage: string;\n}\n\n/**\n * Reconcile a stage so it can be migrated: containers up, databases, schemas,\n * roles and grants in place, owner URLs resolved.\n *\n * Migrating assumes the roles exist, and this is what makes that true rather\n * than something a developer has to have run first.\n */\nexport async function readyStage(\n\tcwd: string,\n\tstage?: string,\n): Promise<ReadyStage> {\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tconst local = workspace.stages.local;\n\tconst target = stage ?? local;\n\tif (target !== local && target !== TEST_STAGE) {\n\t\tthrow new MigrateDeployedStage(target, local);\n\t}\n\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst reconciled = await reconcileWorkspace(workspace, {\n\t\tstage: target,\n\t\tmanifest,\n\t\tstart: true,\n\t});\n\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tsources,\n\t\tenv: { ...reconciled.env },\n\t\tstage: target,\n\t};\n}\n\n/** `gkm migrate [construct]`. */\nexport async function migrateCommand(\n\toptions: MigrateOptions = {},\n): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst runs = await migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (runs.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to migrate.');\n\t\treturn;\n\t}\n\n\tlogMigrations(runs);\n}\n\n/**\n * `gkm seed [construct]` — migrate, then run the seeds.\n *\n * Always migrates first: a seed is written against the schema its migrations\n * build, and against an older one it fails on a table that is not there yet.\n */\nexport async function seedCommand(options: MigrateOptions = {}): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tstage: ready.stage,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (migrations.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to seed.');\n\t\treturn;\n\t}\n\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\n/** One line per construct: up to date, or what was applied. */\nexport function logMigrations(\n\truns: readonly MigrationRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, applied } of runs) {\n\t\tif (applied.length === 0) {\n\t\t\tlog(`🗄️ ${target.migrations}: up to date`);\n\t\t\tcontinue;\n\t\t}\n\t\tlog(`🗄️ ${target.migrations}: applied ${applied.length}`);\n\t\tfor (const name of applied) log(` ✓ ${name}`);\n\t}\n}\n\n/** One line per construct that has seeds, and each seed it ran. */\nexport function logSeeds(\n\truns: readonly SeedRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, seeded } of runs) {\n\t\tif (seeded.length === 0) continue;\n\t\tlog(`🌱 ${target.seeds}: ran ${seeded.length}`);\n\t\tfor (const name of seeded) log(` ✓ ${name}`);\n\t}\n}\n\n/**\n * What `gkm dev` prints: one line per construct whose folder is ahead of its\n * history, and nothing when every folder is applied.\n *\n * Reported, never applied. A migration being written would otherwise run the\n * moment its file is saved, half-finished, against the database the developer\n * is working in.\n */\nexport async function reportPendingMigrations(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst pending = await pendingMigrations({\n\t\troot: workspace.root,\n\t\tmanifest,\n\t\tsources,\n\t\tenv,\n\t});\n\n\tfor (const { target, pending: names } of pending) {\n\t\tlogger.log(\n\t\t\t`⚠️ ${target.migrations}: ${names.length} pending (${names.join(', ')}) — run gkm migrate`,\n\t\t);\n\t}\n}\n\n/**\n * What `gkm dev --migrate` and `gkm dev --seed` do before the apps start:\n * migrate, and with `seed`, run the seeds after — once, at startup, never on a\n * save, for the reason {@link reportPendingMigrations} gives.\n *\n * A failure here is thrown, unlike the report's: the developer asked for the\n * database to be brought up to date, and apps started against one in an\n * unknown state would fail somewhere less obvious.\n */\nexport async function prepareDevDatabases(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n\t{ seed = false }: { seed?: boolean } = {},\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\tconst options = { root: workspace.root, manifest, sources, env };\n\n\tif (!seed) {\n\t\tlogMigrations(await migrateDatabases(options));\n\t\treturn;\n\t}\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\t...options,\n\t\tstage: workspace.stages.local,\n\t});\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\nexport interface MigrationOptions {\n\t/** The construct the migration is for, by name. */\n\tconstruct: string;\n\t/** What the file is called, after its timestamp. */\n\tname?: string;\n\t/** The file's timestamp; the current time when absent. */\n\tnow?: Date;\n}\n\n/**\n * `gkm migration <construct> [name]` — write the construct's next migration.\n *\n * For a database, an empty `up`/`down` to fill in. For an auth server, what\n * its schema is missing, computed against its tenant once every committed\n * migration has been applied — so the file is only the change, and is\n * reviewed and committed like any other.\n */\nexport async function migrationCommand(\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\tconst ready = await readyStage(process.cwd());\n\tconst targets = migrationTargets(ready.manifest);\n\tconst id = canonicalId(options.construct);\n\n\tconst auth = authConstruct(ready.sources, id);\n\tif (auth) {\n\t\tconst target = targets.find((t) => t.id === auth.databaseId);\n\t\tif (!target) {\n\t\t\tthrow new NoSuchMigrationTarget(auth.databaseId, ids(targets));\n\t\t}\n\t\treturn writeAuthMigration(ready, auth, target, options);\n\t}\n\n\tconst target = targets.find((t) => t.id === id);\n\tif (!target) {\n\t\tthrow new NoSuchMigrationTarget(options.construct, [\n\t\t\t...ids(targets),\n\t\t\t...authIds(ready.sources),\n\t\t]);\n\t}\n\tif (!options.name) throw new MigrationNeedsName(options.construct);\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name)}.ts`,\n\t\tSTUB,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\n/** What an auth construct offers `gkm migration`, duck-typed across copies. */\ninterface AuthConstruct {\n\tid: string;\n\tdatabaseId: string;\n\tpendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined>;\n}\n\nfunction authConstruct(\n\tsources: Readonly<Record<string, ConstructSource>>,\n\tid: string,\n): AuthConstruct | undefined {\n\tconst construct = sources[id]?.construct as\n\t\t| Partial<AuthConstruct>\n\t\t| undefined;\n\treturn construct &&\n\t\ttypeof construct.pendingMigration === 'function' &&\n\t\ttypeof construct.databaseId === 'string'\n\t\t? (construct as AuthConstruct)\n\t\t: undefined;\n}\n\nfunction authIds(sources: Readonly<Record<string, ConstructSource>>): string[] {\n\treturn Object.keys(sources).filter((id) => authConstruct(sources, id));\n}\n\nasync function writeAuthMigration(\n\tready: ReadyStage,\n\tauth: AuthConstruct,\n\ttarget: MigrationTarget,\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\t// Against a tenant already at its committed migrations, so the comparison\n\t// finds only what the files do not yet say.\n\tawait migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tonly: target.id,\n\t});\n\n\t// Its secret and URL come from the stage's secrets; the tenant's owner URL\n\t// from the reconcile above, which wins.\n\tconst stage = ready.workspace.stages.local;\n\tconst secrets = await loadSecretsForApp(\n\t\tawait secretsStoreFor(ready.workspace, stage),\n\t\tstage,\n\t);\n\tconst pending = await auth.pendingMigration({\n\t\tenvParser: new EnvironmentParser({ ...secrets, ...ready.env }),\n\t\tcontext: serviceContext,\n\t});\n\n\tif (!pending) {\n\t\tlogger.log(`✓ ${target.migrations}: ${auth.id} is up to date`);\n\t\treturn undefined;\n\t}\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name ?? 'better_auth')}.sql`,\n\t\t`${pending.trim()}\\n`,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\nconst STUB = `import type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // await db.schema.createTable('…').addColumn(…).execute();\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n // await db.schema.dropTable('…').execute();\n}\n`;\n\nasync function write(\n\troot: string,\n\ttarget: MigrationTarget,\n\tfile: string,\n\tcontent: string,\n): Promise<string> {\n\tconst folder = join(root, target.migrations);\n\tawait mkdir(folder, { recursive: true });\n\tawait writeFile(join(folder, file), content, { flag: 'wx' });\n\treturn `${target.migrations}/${file}`;\n}\n\nfunction ids(targets: readonly MigrationTarget[]): string[] {\n\treturn targets.map((target) => target.id);\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  const require_errors = require("./errors-DMC4Umsb.cjs");
2
2
  const require_workspace = require("./workspace-BwqQnool.cjs");
3
- const require_seeds = require("./seeds-Dk8YkLkM.cjs");
3
+ const require_seeds = require("./seeds-DbKNcs3o.cjs");
4
4
  const require_config = require("./config.cjs");
5
5
  const require_aws = require("./aws-DY9NqQIT.cjs");
6
6
  const require_secretsManager = require("./secretsManager-RAGcEDX1.cjs");
@@ -178,4 +178,4 @@ async function secretsMigrateCommand(options) {
178
178
  //#endregion
179
179
  exports.secretsMigrateCommand = secretsMigrateCommand;
180
180
 
181
- //# sourceMappingURL=migrate-GLBZlrlJ.cjs.map
181
+ //# sourceMappingURL=migrate-DZ2--SaO.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-GLBZlrlJ.cjs","names":["GkmError","s3SecretsLocation","SECRETS_STORE_PROVIDERS","UnknownSecretsStoreProvider","isDeepStrictEqual","secretsParameterName","secretsManagerSecretName","loadWorkspaceSettings","secretsStoreFor","storeFromConfig"],"sources":["../src/secrets/migrate.ts"],"sourcesContent":["/**\n * `gkm secrets:migrate` — copy a deployed stage's secrets from the store\n * `secrets.store` names to another one, whole: service passwords, URLs and\n * custom keys alike, so nothing is regenerated and running services keep\n * their credentials.\n *\n * It copies, reads the copy back and compares it, and nothing more. The source\n * is never deleted, and gkm.config.ts is not edited: pointing `secrets.store`\n * at the new store is the switch, and the old copy is deleted by hand — with\n * the command this prints — once deploys read from the new one. Run again, a\n * target already holding the same secrets is verified and left alone.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { loadWorkspaceSettings } from '../config.js';\nimport { GkmError } from '../errors';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { secretsParameterName } from './aws.js';\nimport { secretsManagerSecretName } from './secretsManager.js';\nimport {\n\tSECRETS_STORE_PROVIDERS,\n\ttype SecretsStore,\n\ttype SecretsStoreConfig,\n\ttype SecretsStoreOptions,\n\ttype SecretsStoreProvider,\n\ts3SecretsLocation,\n\tsecretsStoreFor,\n\tstoreFromConfig,\n\tUnknownSecretsStoreProvider,\n} from './store.js';\nimport type { StageSecrets } from './types.js';\n\nconst logger = console;\n\nexport interface SecretsMigrateOptions {\n\tstage: string;\n\t/** The store to copy to: `'file'`, `'s3'`, `'ssm'` or `'secrets-manager'`. */\n\tto: string;\n\t/**\n\t * The target's AWS region; defaults to the configured store's — for `s3`,\n\t * to the S3 state's first.\n\t */\n\tregion?: string;\n\t/** Overwrite a stage the target already holds different secrets for. */\n\tforce?: boolean;\n\t/** The AWS profile for the stage's account, for both stores. */\n\tprofile?: string;\n}\n\n/** An AWS store to copy to, with no region given and none configured. */\nexport class MigrateTargetNeedsRegion extends GkmError {\n\tconstructor(readonly to: string) {\n\t\tsuper(\n\t\t\t`Copying to ${to} needs a region, and secrets.store has none to reuse. ` +\n\t\t\t\t`Pass it: gkm secrets:migrate --to ${to} --region <region> --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'MigrateTargetNeedsRegion';\n\t}\n}\n\n/** The target is the store the stage is already read from. */\nexport class MigrateTargetIsSource extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`Stage \"${stage}\" is already kept in ${to}, so there is nothing to copy. ` +\n\t\t\t\t'Name a different store with --to, or a different region with --region.',\n\t\t);\n\t\tthis.name = 'MigrateTargetIsSource';\n\t}\n}\n\n/** The configured store holds nothing for the stage. */\nexport class NoSecretsToMigrate extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly store: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${store} store holds no secrets for stage \"${stage}\", so there is nothing to copy. ` +\n\t\t\t\t`Check secrets.store in gkm.config.ts still names the store the stage is in, ` +\n\t\t\t\t`or create the stage there with gkm secrets:init --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoSecretsToMigrate';\n\t}\n}\n\n/**\n * The target already holds different secrets for the stage, and `--force` was\n * not given.\n */\nexport class MigrateTargetHoldsStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${to} store already holds different secrets for stage \"${stage}\". ` +\n\t\t\t\t'Pass --force to replace them with the ones being copied.',\n\t\t);\n\t\tthis.name = 'MigrateTargetHoldsStage';\n\t}\n}\n\n/** The copy read back from the target is not what was copied. */\nexport class MigratedSecretsDiffer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t\t/** The keys whose values differ, or that only one side has. */\n\t\treadonly keys: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" read back from ${to} differ from the ones copied` +\n\t\t\t\t(keys.length ? ` (${keys.join(', ')})` : '') +\n\t\t\t\t'. The source is untouched and secrets.store still names it; run the migration again, ' +\n\t\t\t\t'or with --force if something else is writing to the target.',\n\t\t);\n\t\tthis.name = 'MigratedSecretsDiffer';\n\t}\n}\n\n/** The region the configured store is in, where it is an AWS one. */\nfunction configuredRegion(\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): string | undefined {\n\tif (typeof configured !== 'object') return undefined;\n\tif (configured.provider === 's3') {\n\t\treturn s3SecretsLocation(configured, state).region;\n\t}\n\t// A custom store has no region to reuse.\n\treturn 'region' in configured ? configured.region : undefined;\n}\n\n/** The store a `--to` name and region describe. */\nfunction targetConfig(\n\tto: string,\n\tregion: string | undefined,\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): SecretsStoreConfig {\n\tif (!(SECRETS_STORE_PROVIDERS as readonly string[]).includes(to)) {\n\t\tthrow new UnknownSecretsStoreProvider(to);\n\t}\n\tconst provider = to as SecretsStoreProvider;\n\tif (provider === 'file') return 'file';\n\n\tconst fallback = configuredRegion(configured, state);\n\n\tif (provider === 's3') {\n\t\t// The project bucket beside an S3 state is in the state's region.\n\t\tconst inherits =\n\t\t\ttypeof state === 'object' &&\n\t\t\tstate !== null &&\n\t\t\t(state as { provider?: unknown }).provider === 's3';\n\t\tconst resolved = region ?? (inherits ? undefined : fallback);\n\t\tif (!resolved && !inherits) throw new MigrateTargetNeedsRegion(provider);\n\t\treturn { provider, ...(resolved ? { region: resolved } : {}) };\n\t}\n\n\tconst resolved = region ?? fallback;\n\tif (!resolved) throw new MigrateTargetNeedsRegion(provider);\n\treturn { provider, region: resolved };\n}\n\n/** Whether two configs name the same place. */\nfunction samePlace(\n\ta: SecretsStoreConfig,\n\tb: SecretsStoreConfig,\n\tstate: unknown,\n): boolean {\n\tif (typeof a === 'string' || typeof b === 'string') return a === b;\n\tif (a.provider !== b.provider) return false;\n\tif (a.provider === 's3' && b.provider === 's3') {\n\t\treturn isDeepStrictEqual(\n\t\t\ts3SecretsLocation(a, state),\n\t\t\ts3SecretsLocation(b, state),\n\t\t);\n\t}\n\treturn 'region' in a && 'region' in b && a.region === b.region;\n}\n\n/** The `secrets.store` value to write in gkm.config.ts. */\nfunction configLine(target: SecretsStoreConfig): string {\n\tif (typeof target === 'string') return `'${target}'`;\n\tconst region =\n\t\t'region' in target && target.region ? `, region: '${target.region}'` : '';\n\treturn `{ provider: '${String(target.provider)}'${region} }`;\n}\n\n/** The keys whose values differ between two documents, as `section.KEY`. */\nexport function differingKeys(\n\ta: StageSecrets,\n\tb: StageSecrets | null,\n): string[] {\n\tconst keys: string[] = [];\n\tfor (const section of ['custom', 'urls', 'services'] as const) {\n\t\tconst left = a[section] as Record<string, unknown>;\n\t\tconst right = (b?.[section] ?? {}) as Record<string, unknown>;\n\t\tfor (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {\n\t\t\tif (!isDeepStrictEqual(left[key], right[key])) {\n\t\t\t\tkeys.push(`${section}.${key}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn keys.sort();\n}\n\n/**\n * The command that deletes the stage's copy in the source store, for the\n * person to run once deploys read from the target — gkm never runs it.\n */\nexport function deleteSourceCommand(\n\tworkspace: { name: string },\n\tconfigured: SecretsStoreConfig,\n\tsource: SecretsStore,\n\tstage: string,\n\tprofile: string | undefined,\n): string | null {\n\tconst as = profile ? ` --profile ${profile}` : '';\n\tif (configured === 'file') return `rm .gkm/secrets/${stage}.json`;\n\tswitch (configured.provider) {\n\t\tcase 'ssm':\n\t\t\treturn `aws ssm delete-parameter --name ${secretsParameterName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 'secrets-manager':\n\t\t\treturn `aws secretsmanager delete-secret --secret-id ${secretsManagerSecretName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 's3': {\n\t\t\tconst location = (\n\t\t\t\tsource as { location?(stage: string): string }\n\t\t\t).location?.(stage);\n\t\t\tif (!location) return null;\n\t\t\t// Every version, as the bucket keeps them: only a delete marker is added.\n\t\t\treturn `aws s3 rm ${location}${as}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nexport async function secretsMigrateCommand(\n\toptions: SecretsMigrateOptions,\n): Promise<void> {\n\tconst { stage, to } = options;\n\tconst workspace = await loadWorkspaceSettings();\n\tassertDeployedStage(workspace.stages, stage);\n\n\tconst configured = workspace.secrets.store ?? 'file';\n\tconst target = targetConfig(to, options.region, configured, workspace.state);\n\tif (samePlace(configured, target, workspace.state)) {\n\t\tthrow new MigrateTargetIsSource(stage, to);\n\t}\n\n\tconst storeOptions: SecretsStoreOptions = options.profile\n\t\t? { profile: options.profile }\n\t\t: {};\n\tconst source = await secretsStoreFor(workspace, stage, storeOptions);\n\tconst destination = await storeFromConfig(workspace, target, storeOptions);\n\n\tconst secrets = await source.read(stage);\n\tif (!secrets) throw new NoSecretsToMigrate(stage, source.name);\n\n\tconst held = await destination.read(stage);\n\tconst already = held !== null && isDeepStrictEqual(held, secrets);\n\tif (held && !already && !options.force) {\n\t\tthrow new MigrateTargetHoldsStage(stage, destination.name);\n\t}\n\tif (!already) await destination.write(stage, secrets);\n\n\t// Read back through a store of its own, so nothing cached answers.\n\tconst copied = await (\n\t\tawait storeFromConfig(workspace, target, storeOptions)\n\t).read(stage);\n\tif (!isDeepStrictEqual(copied, secrets)) {\n\t\tthrow new MigratedSecretsDiffer(\n\t\t\tstage,\n\t\t\tdestination.name,\n\t\t\tdifferingKeys(secrets, copied),\n\t\t);\n\t}\n\n\tconst count =\n\t\tObject.keys(secrets.custom).length +\n\t\tObject.keys(secrets.urls).length +\n\t\tObject.keys(secrets.services).length;\n\tlogger.log(\n\t\talready\n\t\t\t? `\\n✓ ${destination.name} already holds the same secrets for stage \"${stage}\" as ${source.name}; nothing copied`\n\t\t\t: `\\n✓ Copied the secrets for stage \"${stage}\" from ${source.name} to ${destination.name}`,\n\t);\n\tlogger.log(\n\t\t` Verified: ${count} keys read back from ${destination.name}, equal.`,\n\t);\n\tlogger.log(\n\t\t`\\n Next: set secrets.store to ${configLine(target)} in gkm.config.ts, so commands and deploys read from ${destination.name}.`,\n\t);\n\tconst remove = deleteSourceCommand(\n\t\tworkspace,\n\t\tconfigured,\n\t\tsource,\n\t\tstage,\n\t\toptions.profile,\n\t);\n\tlogger.log(\n\t\t` The copy in ${source.name} is left as it was. Once deploys read from ${destination.name}, delete it` +\n\t\t\t(remove ? ` with:\\n\\n ${remove}\\n` : '.'),\n\t);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-DZ2--SaO.cjs","names":["GkmError","s3SecretsLocation","SECRETS_STORE_PROVIDERS","UnknownSecretsStoreProvider","isDeepStrictEqual","secretsParameterName","secretsManagerSecretName","loadWorkspaceSettings","secretsStoreFor","storeFromConfig"],"sources":["../src/secrets/migrate.ts"],"sourcesContent":["/**\n * `gkm secrets:migrate` — copy a deployed stage's secrets from the store\n * `secrets.store` names to another one, whole: service passwords, URLs and\n * custom keys alike, so nothing is regenerated and running services keep\n * their credentials.\n *\n * It copies, reads the copy back and compares it, and nothing more. The source\n * is never deleted, and gkm.config.ts is not edited: pointing `secrets.store`\n * at the new store is the switch, and the old copy is deleted by hand — with\n * the command this prints — once deploys read from the new one. Run again, a\n * target already holding the same secrets is verified and left alone.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { loadWorkspaceSettings } from '../config.js';\nimport { GkmError } from '../errors';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { secretsParameterName } from './aws.js';\nimport { secretsManagerSecretName } from './secretsManager.js';\nimport {\n\tSECRETS_STORE_PROVIDERS,\n\ttype SecretsStore,\n\ttype SecretsStoreConfig,\n\ttype SecretsStoreOptions,\n\ttype SecretsStoreProvider,\n\ts3SecretsLocation,\n\tsecretsStoreFor,\n\tstoreFromConfig,\n\tUnknownSecretsStoreProvider,\n} from './store.js';\nimport type { StageSecrets } from './types.js';\n\nconst logger = console;\n\nexport interface SecretsMigrateOptions {\n\tstage: string;\n\t/** The store to copy to: `'file'`, `'s3'`, `'ssm'` or `'secrets-manager'`. */\n\tto: string;\n\t/**\n\t * The target's AWS region; defaults to the configured store's — for `s3`,\n\t * to the S3 state's first.\n\t */\n\tregion?: string;\n\t/** Overwrite a stage the target already holds different secrets for. */\n\tforce?: boolean;\n\t/** The AWS profile for the stage's account, for both stores. */\n\tprofile?: string;\n}\n\n/** An AWS store to copy to, with no region given and none configured. */\nexport class MigrateTargetNeedsRegion extends GkmError {\n\tconstructor(readonly to: string) {\n\t\tsuper(\n\t\t\t`Copying to ${to} needs a region, and secrets.store has none to reuse. ` +\n\t\t\t\t`Pass it: gkm secrets:migrate --to ${to} --region <region> --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'MigrateTargetNeedsRegion';\n\t}\n}\n\n/** The target is the store the stage is already read from. */\nexport class MigrateTargetIsSource extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`Stage \"${stage}\" is already kept in ${to}, so there is nothing to copy. ` +\n\t\t\t\t'Name a different store with --to, or a different region with --region.',\n\t\t);\n\t\tthis.name = 'MigrateTargetIsSource';\n\t}\n}\n\n/** The configured store holds nothing for the stage. */\nexport class NoSecretsToMigrate extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly store: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${store} store holds no secrets for stage \"${stage}\", so there is nothing to copy. ` +\n\t\t\t\t`Check secrets.store in gkm.config.ts still names the store the stage is in, ` +\n\t\t\t\t`or create the stage there with gkm secrets:init --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoSecretsToMigrate';\n\t}\n}\n\n/**\n * The target already holds different secrets for the stage, and `--force` was\n * not given.\n */\nexport class MigrateTargetHoldsStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t) {\n\t\tsuper(\n\t\t\t`The ${to} store already holds different secrets for stage \"${stage}\". ` +\n\t\t\t\t'Pass --force to replace them with the ones being copied.',\n\t\t);\n\t\tthis.name = 'MigrateTargetHoldsStage';\n\t}\n}\n\n/** The copy read back from the target is not what was copied. */\nexport class MigratedSecretsDiffer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly to: string,\n\t\t/** The keys whose values differ, or that only one side has. */\n\t\treadonly keys: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" read back from ${to} differ from the ones copied` +\n\t\t\t\t(keys.length ? ` (${keys.join(', ')})` : '') +\n\t\t\t\t'. The source is untouched and secrets.store still names it; run the migration again, ' +\n\t\t\t\t'or with --force if something else is writing to the target.',\n\t\t);\n\t\tthis.name = 'MigratedSecretsDiffer';\n\t}\n}\n\n/** The region the configured store is in, where it is an AWS one. */\nfunction configuredRegion(\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): string | undefined {\n\tif (typeof configured !== 'object') return undefined;\n\tif (configured.provider === 's3') {\n\t\treturn s3SecretsLocation(configured, state).region;\n\t}\n\t// A custom store has no region to reuse.\n\treturn 'region' in configured ? configured.region : undefined;\n}\n\n/** The store a `--to` name and region describe. */\nfunction targetConfig(\n\tto: string,\n\tregion: string | undefined,\n\tconfigured: SecretsStoreConfig,\n\tstate: unknown,\n): SecretsStoreConfig {\n\tif (!(SECRETS_STORE_PROVIDERS as readonly string[]).includes(to)) {\n\t\tthrow new UnknownSecretsStoreProvider(to);\n\t}\n\tconst provider = to as SecretsStoreProvider;\n\tif (provider === 'file') return 'file';\n\n\tconst fallback = configuredRegion(configured, state);\n\n\tif (provider === 's3') {\n\t\t// The project bucket beside an S3 state is in the state's region.\n\t\tconst inherits =\n\t\t\ttypeof state === 'object' &&\n\t\t\tstate !== null &&\n\t\t\t(state as { provider?: unknown }).provider === 's3';\n\t\tconst resolved = region ?? (inherits ? undefined : fallback);\n\t\tif (!resolved && !inherits) throw new MigrateTargetNeedsRegion(provider);\n\t\treturn { provider, ...(resolved ? { region: resolved } : {}) };\n\t}\n\n\tconst resolved = region ?? fallback;\n\tif (!resolved) throw new MigrateTargetNeedsRegion(provider);\n\treturn { provider, region: resolved };\n}\n\n/** Whether two configs name the same place. */\nfunction samePlace(\n\ta: SecretsStoreConfig,\n\tb: SecretsStoreConfig,\n\tstate: unknown,\n): boolean {\n\tif (typeof a === 'string' || typeof b === 'string') return a === b;\n\tif (a.provider !== b.provider) return false;\n\tif (a.provider === 's3' && b.provider === 's3') {\n\t\treturn isDeepStrictEqual(\n\t\t\ts3SecretsLocation(a, state),\n\t\t\ts3SecretsLocation(b, state),\n\t\t);\n\t}\n\treturn 'region' in a && 'region' in b && a.region === b.region;\n}\n\n/** The `secrets.store` value to write in gkm.config.ts. */\nfunction configLine(target: SecretsStoreConfig): string {\n\tif (typeof target === 'string') return `'${target}'`;\n\tconst region =\n\t\t'region' in target && target.region ? `, region: '${target.region}'` : '';\n\treturn `{ provider: '${String(target.provider)}'${region} }`;\n}\n\n/** The keys whose values differ between two documents, as `section.KEY`. */\nexport function differingKeys(\n\ta: StageSecrets,\n\tb: StageSecrets | null,\n): string[] {\n\tconst keys: string[] = [];\n\tfor (const section of ['custom', 'urls', 'services'] as const) {\n\t\tconst left = a[section] as Record<string, unknown>;\n\t\tconst right = (b?.[section] ?? {}) as Record<string, unknown>;\n\t\tfor (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {\n\t\t\tif (!isDeepStrictEqual(left[key], right[key])) {\n\t\t\t\tkeys.push(`${section}.${key}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn keys.sort();\n}\n\n/**\n * The command that deletes the stage's copy in the source store, for the\n * person to run once deploys read from the target — gkm never runs it.\n */\nexport function deleteSourceCommand(\n\tworkspace: { name: string },\n\tconfigured: SecretsStoreConfig,\n\tsource: SecretsStore,\n\tstage: string,\n\tprofile: string | undefined,\n): string | null {\n\tconst as = profile ? ` --profile ${profile}` : '';\n\tif (configured === 'file') return `rm .gkm/secrets/${stage}.json`;\n\tswitch (configured.provider) {\n\t\tcase 'ssm':\n\t\t\treturn `aws ssm delete-parameter --name ${secretsParameterName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 'secrets-manager':\n\t\t\treturn `aws secretsmanager delete-secret --secret-id ${secretsManagerSecretName(workspace.name, stage)} --region ${configured.region}${as}`;\n\t\tcase 's3': {\n\t\t\tconst location = (\n\t\t\t\tsource as { location?(stage: string): string }\n\t\t\t).location?.(stage);\n\t\t\tif (!location) return null;\n\t\t\t// Every version, as the bucket keeps them: only a delete marker is added.\n\t\t\treturn `aws s3 rm ${location}${as}`;\n\t\t}\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nexport async function secretsMigrateCommand(\n\toptions: SecretsMigrateOptions,\n): Promise<void> {\n\tconst { stage, to } = options;\n\tconst workspace = await loadWorkspaceSettings();\n\tassertDeployedStage(workspace.stages, stage);\n\n\tconst configured = workspace.secrets.store ?? 'file';\n\tconst target = targetConfig(to, options.region, configured, workspace.state);\n\tif (samePlace(configured, target, workspace.state)) {\n\t\tthrow new MigrateTargetIsSource(stage, to);\n\t}\n\n\tconst storeOptions: SecretsStoreOptions = options.profile\n\t\t? { profile: options.profile }\n\t\t: {};\n\tconst source = await secretsStoreFor(workspace, stage, storeOptions);\n\tconst destination = await storeFromConfig(workspace, target, storeOptions);\n\n\tconst secrets = await source.read(stage);\n\tif (!secrets) throw new NoSecretsToMigrate(stage, source.name);\n\n\tconst held = await destination.read(stage);\n\tconst already = held !== null && isDeepStrictEqual(held, secrets);\n\tif (held && !already && !options.force) {\n\t\tthrow new MigrateTargetHoldsStage(stage, destination.name);\n\t}\n\tif (!already) await destination.write(stage, secrets);\n\n\t// Read back through a store of its own, so nothing cached answers.\n\tconst copied = await (\n\t\tawait storeFromConfig(workspace, target, storeOptions)\n\t).read(stage);\n\tif (!isDeepStrictEqual(copied, secrets)) {\n\t\tthrow new MigratedSecretsDiffer(\n\t\t\tstage,\n\t\t\tdestination.name,\n\t\t\tdifferingKeys(secrets, copied),\n\t\t);\n\t}\n\n\tconst count =\n\t\tObject.keys(secrets.custom).length +\n\t\tObject.keys(secrets.urls).length +\n\t\tObject.keys(secrets.services).length;\n\tlogger.log(\n\t\talready\n\t\t\t? `\\n✓ ${destination.name} already holds the same secrets for stage \"${stage}\" as ${source.name}; nothing copied`\n\t\t\t: `\\n✓ Copied the secrets for stage \"${stage}\" from ${source.name} to ${destination.name}`,\n\t);\n\tlogger.log(\n\t\t` Verified: ${count} keys read back from ${destination.name}, equal.`,\n\t);\n\tlogger.log(\n\t\t`\\n Next: set secrets.store to ${configLine(target)} in gkm.config.ts, so commands and deploys read from ${destination.name}.`,\n\t);\n\tconst remove = deleteSourceCommand(\n\t\tworkspace,\n\t\tconfigured,\n\t\tsource,\n\t\tstage,\n\t\toptions.profile,\n\t);\n\tlogger.log(\n\t\t` The copy in ${source.name} is left as it was. Once deploys read from ${destination.name}, delete it` +\n\t\t\t(remove ? ` with:\\n\\n ${remove}\\n` : '.'),\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,SAAS;;AAkBf,IAAa,2BAAb,cAA8CA,eAAAA,SAAS;CACjC;CAArB,YAAY,IAAqB;EAChC,MACC,cAAc,GAAG,0FACqB,GAAG,oCAC1C;EAJoB,KAAA,KAAA;EAKpB,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,wBAAb,cAA2CA,eAAAA,SAAS;CAEzC;CACA;CAFV,YACC,OACA,IACC;EACD,MACC,UAAU,MAAM,uBAAuB,GAAG,sGAE3C;EANS,KAAA,QAAA;EACA,KAAA,KAAA;EAMT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,qBAAb,cAAwCA,eAAAA,SAAS;CAEtC;CACA;CAFV,YACC,OACA,OACC;EACD,MACC,OAAO,MAAM,qCAAqC,MAAM,sKAEI,MAAM,EACnE;EAPS,KAAA,QAAA;EACA,KAAA,QAAA;EAOT,KAAK,OAAO;CACb;AACD;;;;;AAMA,IAAa,0BAAb,cAA6CA,eAAAA,SAAS;CAE3C;CACA;CAFV,YACC,OACA,IACC;EACD,MACC,OAAO,GAAG,oDAAoD,MAAM,4DAErE;EANS,KAAA,QAAA;EACA,KAAA,KAAA;EAMT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,wBAAb,cAA2CA,eAAAA,SAAS;CAEzC;CACA;CAEA;CAJV,YACC,OACA,IAEA,MACC;EACD,MACC,0BAA0B,MAAM,mBAAmB,GAAG,iCACpD,KAAK,SAAS,KAAK,KAAK,KAAK,IAAI,EAAE,KAAK,MACzC,kJAEF;EAVS,KAAA,QAAA;EACA,KAAA,KAAA;EAEA,KAAA,OAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,SAAS,iBACR,YACA,OACqB;CACrB,IAAI,OAAO,eAAe,UAAU,OAAO,KAAA;CAC3C,IAAI,WAAW,aAAa,MAC3B,OAAOC,kBAAAA,kBAAkB,YAAY,KAAK,CAAC,CAAC;CAG7C,OAAO,YAAY,aAAa,WAAW,SAAS,KAAA;AACrD;;AAGA,SAAS,aACR,IACA,QACA,YACA,OACqB;CACrB,IAAI,CAAEC,kBAAAA,wBAA8C,SAAS,EAAE,GAC9D,MAAM,IAAIC,kBAAAA,4BAA4B,EAAE;CAEzC,MAAM,WAAW;CACjB,IAAI,aAAa,QAAQ,OAAO;CAEhC,MAAM,WAAW,iBAAiB,YAAY,KAAK;CAEnD,IAAI,aAAa,MAAM;EAEtB,MAAM,WACL,OAAO,UAAU,YACjB,UAAU,QACT,MAAiC,aAAa;EAChD,MAAM,WAAW,WAAW,WAAW,KAAA,IAAY;EACnD,IAAI,CAAC,YAAY,CAAC,UAAU,MAAM,IAAI,yBAAyB,QAAQ;EACvE,OAAO;GAAE;GAAU,GAAI,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;EAAG;CAC9D;CAEA,MAAM,WAAW,UAAU;CAC3B,IAAI,CAAC,UAAU,MAAM,IAAI,yBAAyB,QAAQ;CAC1D,OAAO;EAAE;EAAU,QAAQ;CAAS;AACrC;;AAGA,SAAS,UACR,GACA,GACA,OACU;CACV,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU,OAAO,MAAM;CACjE,IAAI,EAAE,aAAa,EAAE,UAAU,OAAO;CACtC,IAAI,EAAE,aAAa,QAAQ,EAAE,aAAa,MACzC,QAAA,GAAOC,UAAAA,kBAAAA,CACNH,kBAAAA,kBAAkB,GAAG,KAAK,GAC1BA,kBAAAA,kBAAkB,GAAG,KAAK,CAC3B;CAED,OAAO,YAAY,KAAK,YAAY,KAAK,EAAE,WAAW,EAAE;AACzD;;AAGA,SAAS,WAAW,QAAoC;CACvD,IAAI,OAAO,WAAW,UAAU,OAAO,IAAI,OAAO;CAClD,MAAM,SACL,YAAY,UAAU,OAAO,SAAS,cAAc,OAAO,OAAO,KAAK;CACxE,OAAO,gBAAgB,OAAO,OAAO,QAAQ,EAAE,GAAG,OAAO;AAC1D;;AAGA,SAAgB,cACf,GACA,GACW;CACX,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,WAAW;EAAC;EAAU;EAAQ;CAAU,GAAY;EAC9D,MAAM,OAAO,EAAE;EACf,MAAM,QAAS,IAAI,YAAY,CAAC;EAChC,KAAK,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,IAAI,GAAG,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,GACtE,IAAI,EAAA,GAACG,UAAAA,kBAAAA,CAAkB,KAAK,MAAM,MAAM,IAAI,GAC3C,KAAK,KAAK,GAAG,QAAQ,GAAG,KAAK;CAGhC;CACA,OAAO,KAAK,KAAK;AAClB;;;;;AAMA,SAAgB,oBACf,WACA,YACA,QACA,OACA,SACgB;CAChB,MAAM,KAAK,UAAU,cAAc,YAAY;CAC/C,IAAI,eAAe,QAAQ,OAAO,mBAAmB,MAAM;CAC3D,QAAQ,WAAW,UAAnB;EACC,KAAK,OACJ,OAAO,mCAAmCC,YAAAA,qBAAqB,UAAU,MAAM,KAAK,EAAE,YAAY,WAAW,SAAS;EACvH,KAAK,mBACJ,OAAO,gDAAgDC,uBAAAA,yBAAyB,UAAU,MAAM,KAAK,EAAE,YAAY,WAAW,SAAS;EACxI,KAAK,MAAM;GACV,MAAM,WACL,OACC,WAAW,KAAK;GAClB,IAAI,CAAC,UAAU,OAAO;GAEtB,OAAO,aAAa,WAAW;EAChC;EACA,SACC,OAAO;CACT;AACD;AAEA,eAAsB,sBACrB,SACgB;CAChB,MAAM,EAAE,OAAO,OAAO;CACtB,MAAM,YAAY,MAAMC,eAAAA,sBAAsB;CAC9C,kBAAA,oBAAoB,UAAU,QAAQ,KAAK;CAE3C,MAAM,aAAa,UAAU,QAAQ,SAAS;CAC9C,MAAM,SAAS,aAAa,IAAI,QAAQ,QAAQ,YAAY,UAAU,KAAK;CAC3E,IAAI,UAAU,YAAY,QAAQ,UAAU,KAAK,GAChD,MAAM,IAAI,sBAAsB,OAAO,EAAE;CAG1C,MAAM,eAAoC,QAAQ,UAC/C,EAAE,SAAS,QAAQ,QAAQ,IAC3B,CAAC;CACJ,MAAM,SAAS,MAAMC,cAAAA,gBAAgB,WAAW,OAAO,YAAY;CACnE,MAAM,cAAc,MAAMC,cAAAA,gBAAgB,WAAW,QAAQ,YAAY;CAEzE,MAAM,UAAU,MAAM,OAAO,KAAK,KAAK;CACvC,IAAI,CAAC,SAAS,MAAM,IAAI,mBAAmB,OAAO,OAAO,IAAI;CAE7D,MAAM,OAAO,MAAM,YAAY,KAAK,KAAK;CACzC,MAAM,UAAU,SAAS,SAAA,GAAQL,UAAAA,kBAAAA,CAAkB,MAAM,OAAO;CAChE,IAAI,QAAQ,CAAC,WAAW,CAAC,QAAQ,OAChC,MAAM,IAAI,wBAAwB,OAAO,YAAY,IAAI;CAE1D,IAAI,CAAC,SAAS,MAAM,YAAY,MAAM,OAAO,OAAO;CAGpD,MAAM,SAAS,OACd,MAAMK,cAAAA,gBAAgB,WAAW,QAAQ,YAAY,EAAA,CACpD,KAAK,KAAK;CACZ,IAAI,EAAA,GAACL,UAAAA,kBAAAA,CAAkB,QAAQ,OAAO,GACrC,MAAM,IAAI,sBACT,OACA,YAAY,MACZ,cAAc,SAAS,MAAM,CAC9B;CAGD,MAAM,QACL,OAAO,KAAK,QAAQ,MAAM,CAAC,CAAC,SAC5B,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,SAC1B,OAAO,KAAK,QAAQ,QAAQ,CAAC,CAAC;CAC/B,OAAO,IACN,UACG,OAAO,YAAY,KAAK,6CAA6C,MAAM,OAAO,OAAO,KAAK,oBAC9F,qCAAqC,MAAM,SAAS,OAAO,KAAK,MAAM,YAAY,MACtF;CACA,OAAO,IACN,eAAe,MAAM,uBAAuB,YAAY,KAAK,SAC9D;CACA,OAAO,IACN,kCAAkC,WAAW,MAAM,EAAE,uDAAuD,YAAY,KAAK,EAC9H;CACA,MAAM,SAAS,oBACd,WACA,YACA,QACA,OACA,QAAQ,OACT;CACA,OAAO,IACN,iBAAiB,OAAO,KAAK,6CAA6C,YAAY,KAAK,gBACzF,SAAS,iBAAiB,OAAO,MAAM,IAC1C;AACD"}
@@ -1,6 +1,6 @@
1
1
  import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
2
2
  import { j as TEST_STAGE } from "./workspace-D1QbNd55.mjs";
3
- import { It as secretsStoreFor, Nt as loadSecretsForApp, dt as constructGlobs, pt as reconcileWorkspace } from "./seeds-CIk0l4SC.mjs";
3
+ import { It as secretsStoreFor, Nt as loadSecretsForApp, dt as constructGlobs, pt as reconcileWorkspace } from "./seeds-CRkKeyW0.mjs";
4
4
  import { i as discover } from "./discover-Ca8U75dl.mjs";
5
5
  import { loadWorkspaceConfig } from "./config.mjs";
6
6
  import { a as NoSuchMigrationTarget, d as pendingMigrations, l as migrateAndSeed, u as migrateDatabases } from "./databases-BeGP5eDh.mjs";
@@ -263,4 +263,4 @@ function ids(targets) {
263
263
  //#endregion
264
264
  export { migrateAndSeed, migrateCommand, migrationCommand, prepareDevDatabases, reportPendingMigrations, seedCommand };
265
265
 
266
- //# sourceMappingURL=migrate-xHYaeBkd.mjs.map
266
+ //# sourceMappingURL=migrate-X0Y7fz3N.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-xHYaeBkd.mjs","names":[],"sources":["../src/migrate/index.ts"],"sourcesContent":["/**\n * `gkm migrate`, `gkm seed` and `gkm migration` — applying a construct's\n * migrations, running its seeds, and writing its next migration.\n *\n * Both work on a stage this machine reconciles: its containers up, its roles\n * provisioned, its owner URLs resolved. A deployed stage is migrated by the\n * deploy that ships it, which is the one thing that can reach its database.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype MigrationTarget,\n\tmigrationTargets,\n} from '@geekmidas/manifest';\nimport {\n\ttype ServiceRegisterOptions,\n\tserviceContext,\n} from '@geekmidas/services';\nimport { loadWorkspaceConfig } from '../config';\nimport { loadSecretsForApp } from '../credentials';\nimport { GkmError } from '../errors';\nimport { type ConstructSource, discover } from '../reconcile/discover';\nimport { constructGlobs, reconcileWorkspace } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { TEST_STAGE } from '../workspace/stages';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport {\n\ttype MigrationRun,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoSuchMigrationTarget,\n\tpendingMigrations,\n\ttype SeedRun,\n} from './databases';\nimport { slug, stamp } from './names';\n\nexport {\n\tKyselyNotFound,\n\tMigrationFailed,\n\ttype MigrationRun,\n\tMigrationsOutsideFolder,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoOwnerCredential,\n\tNoSuchMigrationTarget,\n\ttype PendingMigrations,\n\tpendingMigrations,\n\tSeedFailed,\n\ttype SeedOptions,\n\ttype SeedRun,\n\tseedDatabases,\n\tUnknownDatabaseFolder,\n\tUnknownMigrationFolder,\n} from './databases';\nexport { slug, stamp } from './names';\nexport {\n\tDuplicateMigration,\n\tMigrationHasNoUp,\n\tSeedHasNoSeed,\n} from './provider';\n\nconst logger = console;\n\n/** `gkm migrate --stage` named a stage only a deploy can reach. */\nexport class MigrateDeployedStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly local: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is a deployed stage, and its database is migrated by ` +\n\t\t\t\t`\\`gkm deploy --stage ${stage}\\`, which is what can reach it. ` +\n\t\t\t\t`\\`gkm migrate\\` runs against '${local}' or '${TEST_STAGE}'.`,\n\t\t);\n\t\tthis.name = 'MigrateDeployedStage';\n\t}\n}\n\n/** `gkm migration` for a Kysely database, with no name for the file. */\nexport class MigrationNeedsName extends GkmError {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`Name the migration: \\`gkm migration ${construct} add_workouts\\`. ` +\n\t\t\t\t`The name is what the file — and the history — calls it.`,\n\t\t);\n\t\tthis.name = 'MigrationNeedsName';\n\t}\n}\n\nexport interface MigrateOptions {\n\t/** The stage to migrate: the project's local one, or `test`. */\n\tstage?: string;\n\t/** One construct, by name. All of them when absent. */\n\tconstruct?: string;\n}\n\n/** A workspace's stage, reconciled and ready to migrate. */\nexport interface ReadyStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tsources: Record<string, ConstructSource>;\n\t/** Every URL the stage resolved — the owner URLs among them. */\n\tenv: Record<string, string>;\n\t/** The stage it is: the local one, or `test`. */\n\tstage: string;\n}\n\n/**\n * Reconcile a stage so it can be migrated: containers up, databases, schemas,\n * roles and grants in place, owner URLs resolved.\n *\n * Migrating assumes the roles exist, and this is what makes that true rather\n * than something a developer has to have run first.\n */\nexport async function readyStage(\n\tcwd: string,\n\tstage?: string,\n): Promise<ReadyStage> {\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tconst local = workspace.stages.local;\n\tconst target = stage ?? local;\n\tif (target !== local && target !== TEST_STAGE) {\n\t\tthrow new MigrateDeployedStage(target, local);\n\t}\n\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst reconciled = await reconcileWorkspace(workspace, {\n\t\tstage: target,\n\t\tmanifest,\n\t\tstart: true,\n\t});\n\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tsources,\n\t\tenv: { ...reconciled.env },\n\t\tstage: target,\n\t};\n}\n\n/** `gkm migrate [construct]`. */\nexport async function migrateCommand(\n\toptions: MigrateOptions = {},\n): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst runs = await migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (runs.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to migrate.');\n\t\treturn;\n\t}\n\n\tlogMigrations(runs);\n}\n\n/**\n * `gkm seed [construct]` — migrate, then run the seeds.\n *\n * Always migrates first: a seed is written against the schema its migrations\n * build, and against an older one it fails on a table that is not there yet.\n */\nexport async function seedCommand(options: MigrateOptions = {}): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tstage: ready.stage,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (migrations.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to seed.');\n\t\treturn;\n\t}\n\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\n/** One line per construct: up to date, or what was applied. */\nexport function logMigrations(\n\truns: readonly MigrationRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, applied } of runs) {\n\t\tif (applied.length === 0) {\n\t\t\tlog(`🗄️ ${target.migrations}: up to date`);\n\t\t\tcontinue;\n\t\t}\n\t\tlog(`🗄️ ${target.migrations}: applied ${applied.length}`);\n\t\tfor (const name of applied) log(` ✓ ${name}`);\n\t}\n}\n\n/** One line per construct that has seeds, and each seed it ran. */\nexport function logSeeds(\n\truns: readonly SeedRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, seeded } of runs) {\n\t\tif (seeded.length === 0) continue;\n\t\tlog(`🌱 ${target.seeds}: ran ${seeded.length}`);\n\t\tfor (const name of seeded) log(` ✓ ${name}`);\n\t}\n}\n\n/**\n * What `gkm dev` prints: one line per construct whose folder is ahead of its\n * history, and nothing when every folder is applied.\n *\n * Reported, never applied. A migration being written would otherwise run the\n * moment its file is saved, half-finished, against the database the developer\n * is working in.\n */\nexport async function reportPendingMigrations(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst pending = await pendingMigrations({\n\t\troot: workspace.root,\n\t\tmanifest,\n\t\tsources,\n\t\tenv,\n\t});\n\n\tfor (const { target, pending: names } of pending) {\n\t\tlogger.log(\n\t\t\t`⚠️ ${target.migrations}: ${names.length} pending (${names.join(', ')}) — run gkm migrate`,\n\t\t);\n\t}\n}\n\n/**\n * What `gkm dev --migrate` and `gkm dev --seed` do before the apps start:\n * migrate, and with `seed`, run the seeds after — once, at startup, never on a\n * save, for the reason {@link reportPendingMigrations} gives.\n *\n * A failure here is thrown, unlike the report's: the developer asked for the\n * database to be brought up to date, and apps started against one in an\n * unknown state would fail somewhere less obvious.\n */\nexport async function prepareDevDatabases(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n\t{ seed = false }: { seed?: boolean } = {},\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\tconst options = { root: workspace.root, manifest, sources, env };\n\n\tif (!seed) {\n\t\tlogMigrations(await migrateDatabases(options));\n\t\treturn;\n\t}\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\t...options,\n\t\tstage: workspace.stages.local,\n\t});\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\nexport interface MigrationOptions {\n\t/** The construct the migration is for, by name. */\n\tconstruct: string;\n\t/** What the file is called, after its timestamp. */\n\tname?: string;\n\t/** The file's timestamp; the current time when absent. */\n\tnow?: Date;\n}\n\n/**\n * `gkm migration <construct> [name]` — write the construct's next migration.\n *\n * For a database, an empty `up`/`down` to fill in. For an auth server, what\n * its schema is missing, computed against its tenant once every committed\n * migration has been applied — so the file is only the change, and is\n * reviewed and committed like any other.\n */\nexport async function migrationCommand(\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\tconst ready = await readyStage(process.cwd());\n\tconst targets = migrationTargets(ready.manifest);\n\tconst id = canonicalId(options.construct);\n\n\tconst auth = authConstruct(ready.sources, id);\n\tif (auth) {\n\t\tconst target = targets.find((t) => t.id === auth.databaseId);\n\t\tif (!target) {\n\t\t\tthrow new NoSuchMigrationTarget(auth.databaseId, ids(targets));\n\t\t}\n\t\treturn writeAuthMigration(ready, auth, target, options);\n\t}\n\n\tconst target = targets.find((t) => t.id === id);\n\tif (!target) {\n\t\tthrow new NoSuchMigrationTarget(options.construct, [\n\t\t\t...ids(targets),\n\t\t\t...authIds(ready.sources),\n\t\t]);\n\t}\n\tif (!options.name) throw new MigrationNeedsName(options.construct);\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name)}.ts`,\n\t\tSTUB,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\n/** What an auth construct offers `gkm migration`, duck-typed across copies. */\ninterface AuthConstruct {\n\tid: string;\n\tdatabaseId: string;\n\tpendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined>;\n}\n\nfunction authConstruct(\n\tsources: Readonly<Record<string, ConstructSource>>,\n\tid: string,\n): AuthConstruct | undefined {\n\tconst construct = sources[id]?.construct as\n\t\t| Partial<AuthConstruct>\n\t\t| undefined;\n\treturn construct &&\n\t\ttypeof construct.pendingMigration === 'function' &&\n\t\ttypeof construct.databaseId === 'string'\n\t\t? (construct as AuthConstruct)\n\t\t: undefined;\n}\n\nfunction authIds(sources: Readonly<Record<string, ConstructSource>>): string[] {\n\treturn Object.keys(sources).filter((id) => authConstruct(sources, id));\n}\n\nasync function writeAuthMigration(\n\tready: ReadyStage,\n\tauth: AuthConstruct,\n\ttarget: MigrationTarget,\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\t// Against a tenant already at its committed migrations, so the comparison\n\t// finds only what the files do not yet say.\n\tawait migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tonly: target.id,\n\t});\n\n\t// Its secret and URL come from the stage's secrets; the tenant's owner URL\n\t// from the reconcile above, which wins.\n\tconst stage = ready.workspace.stages.local;\n\tconst secrets = await loadSecretsForApp(\n\t\tawait secretsStoreFor(ready.workspace, stage),\n\t\tstage,\n\t);\n\tconst pending = await auth.pendingMigration({\n\t\tenvParser: new EnvironmentParser({ ...secrets, ...ready.env }),\n\t\tcontext: serviceContext,\n\t});\n\n\tif (!pending) {\n\t\tlogger.log(`✓ ${target.migrations}: ${auth.id} is up to date`);\n\t\treturn undefined;\n\t}\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name ?? 'better_auth')}.sql`,\n\t\t`${pending.trim()}\\n`,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\nconst STUB = `import type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // await db.schema.createTable('…').addColumn(…).execute();\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n // await db.schema.dropTable('…').execute();\n}\n`;\n\nasync function write(\n\troot: string,\n\ttarget: MigrationTarget,\n\tfile: string,\n\tcontent: string,\n): Promise<string> {\n\tconst folder = join(root, target.migrations);\n\tawait mkdir(folder, { recursive: true });\n\tawait writeFile(join(folder, file), content, { flag: 'wx' });\n\treturn `${target.migrations}/${file}`;\n}\n\nfunction ids(targets: readonly MigrationTarget[]): string[] {\n\treturn targets.map((target) => target.id);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-X0Y7fz3N.mjs","names":[],"sources":["../src/migrate/index.ts"],"sourcesContent":["/**\n * `gkm migrate`, `gkm seed` and `gkm migration` — applying a construct's\n * migrations, running its seeds, and writing its next migration.\n *\n * Both work on a stage this machine reconciles: its containers up, its roles\n * provisioned, its owner URLs resolved. A deployed stage is migrated by the\n * deploy that ships it, which is the one thing that can reach its database.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype MigrationTarget,\n\tmigrationTargets,\n} from '@geekmidas/manifest';\nimport {\n\ttype ServiceRegisterOptions,\n\tserviceContext,\n} from '@geekmidas/services';\nimport { loadWorkspaceConfig } from '../config';\nimport { loadSecretsForApp } from '../credentials';\nimport { GkmError } from '../errors';\nimport { type ConstructSource, discover } from '../reconcile/discover';\nimport { constructGlobs, reconcileWorkspace } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { TEST_STAGE } from '../workspace/stages';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport {\n\ttype MigrationRun,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoSuchMigrationTarget,\n\tpendingMigrations,\n\ttype SeedRun,\n} from './databases';\nimport { slug, stamp } from './names';\n\nexport {\n\tKyselyNotFound,\n\tMigrationFailed,\n\ttype MigrationRun,\n\tMigrationsOutsideFolder,\n\tmigrateAndSeed,\n\tmigrateDatabases,\n\tNoOwnerCredential,\n\tNoSuchMigrationTarget,\n\ttype PendingMigrations,\n\tpendingMigrations,\n\tSeedFailed,\n\ttype SeedOptions,\n\ttype SeedRun,\n\tseedDatabases,\n\tUnknownDatabaseFolder,\n\tUnknownMigrationFolder,\n} from './databases';\nexport { slug, stamp } from './names';\nexport {\n\tDuplicateMigration,\n\tMigrationHasNoUp,\n\tSeedHasNoSeed,\n} from './provider';\n\nconst logger = console;\n\n/** `gkm migrate --stage` named a stage only a deploy can reach. */\nexport class MigrateDeployedStage extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly local: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is a deployed stage, and its database is migrated by ` +\n\t\t\t\t`\\`gkm deploy --stage ${stage}\\`, which is what can reach it. ` +\n\t\t\t\t`\\`gkm migrate\\` runs against '${local}' or '${TEST_STAGE}'.`,\n\t\t);\n\t\tthis.name = 'MigrateDeployedStage';\n\t}\n}\n\n/** `gkm migration` for a Kysely database, with no name for the file. */\nexport class MigrationNeedsName extends GkmError {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`Name the migration: \\`gkm migration ${construct} add_workouts\\`. ` +\n\t\t\t\t`The name is what the file — and the history — calls it.`,\n\t\t);\n\t\tthis.name = 'MigrationNeedsName';\n\t}\n}\n\nexport interface MigrateOptions {\n\t/** The stage to migrate: the project's local one, or `test`. */\n\tstage?: string;\n\t/** One construct, by name. All of them when absent. */\n\tconstruct?: string;\n}\n\n/** A workspace's stage, reconciled and ready to migrate. */\nexport interface ReadyStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tsources: Record<string, ConstructSource>;\n\t/** Every URL the stage resolved — the owner URLs among them. */\n\tenv: Record<string, string>;\n\t/** The stage it is: the local one, or `test`. */\n\tstage: string;\n}\n\n/**\n * Reconcile a stage so it can be migrated: containers up, databases, schemas,\n * roles and grants in place, owner URLs resolved.\n *\n * Migrating assumes the roles exist, and this is what makes that true rather\n * than something a developer has to have run first.\n */\nexport async function readyStage(\n\tcwd: string,\n\tstage?: string,\n): Promise<ReadyStage> {\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tconst local = workspace.stages.local;\n\tconst target = stage ?? local;\n\tif (target !== local && target !== TEST_STAGE) {\n\t\tthrow new MigrateDeployedStage(target, local);\n\t}\n\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst reconciled = await reconcileWorkspace(workspace, {\n\t\tstage: target,\n\t\tmanifest,\n\t\tstart: true,\n\t});\n\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tsources,\n\t\tenv: { ...reconciled.env },\n\t\tstage: target,\n\t};\n}\n\n/** `gkm migrate [construct]`. */\nexport async function migrateCommand(\n\toptions: MigrateOptions = {},\n): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst runs = await migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (runs.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to migrate.');\n\t\treturn;\n\t}\n\n\tlogMigrations(runs);\n}\n\n/**\n * `gkm seed [construct]` — migrate, then run the seeds.\n *\n * Always migrates first: a seed is written against the schema its migrations\n * build, and against an older one it fails on a table that is not there yet.\n */\nexport async function seedCommand(options: MigrateOptions = {}): Promise<void> {\n\tconst ready = await readyStage(process.cwd(), options.stage);\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tstage: ready.stage,\n\t\t...(options.construct ? { only: options.construct } : {}),\n\t});\n\n\tif (migrations.length === 0) {\n\t\tlogger.log('🗄️ No database constructs to seed.');\n\t\treturn;\n\t}\n\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\n/** One line per construct: up to date, or what was applied. */\nexport function logMigrations(\n\truns: readonly MigrationRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, applied } of runs) {\n\t\tif (applied.length === 0) {\n\t\t\tlog(`🗄️ ${target.migrations}: up to date`);\n\t\t\tcontinue;\n\t\t}\n\t\tlog(`🗄️ ${target.migrations}: applied ${applied.length}`);\n\t\tfor (const name of applied) log(` ✓ ${name}`);\n\t}\n}\n\n/** One line per construct that has seeds, and each seed it ran. */\nexport function logSeeds(\n\truns: readonly SeedRun[],\n\tlog: (line: string) => void = logger.log,\n): void {\n\tfor (const { target, seeded } of runs) {\n\t\tif (seeded.length === 0) continue;\n\t\tlog(`🌱 ${target.seeds}: ran ${seeded.length}`);\n\t\tfor (const name of seeded) log(` ✓ ${name}`);\n\t}\n}\n\n/**\n * What `gkm dev` prints: one line per construct whose folder is ahead of its\n * history, and nothing when every folder is applied.\n *\n * Reported, never applied. A migration being written would otherwise run the\n * moment its file is saved, half-finished, against the database the developer\n * is working in.\n */\nexport async function reportPendingMigrations(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\n\tconst pending = await pendingMigrations({\n\t\troot: workspace.root,\n\t\tmanifest,\n\t\tsources,\n\t\tenv,\n\t});\n\n\tfor (const { target, pending: names } of pending) {\n\t\tlogger.log(\n\t\t\t`⚠️ ${target.migrations}: ${names.length} pending (${names.join(', ')}) — run gkm migrate`,\n\t\t);\n\t}\n}\n\n/**\n * What `gkm dev --migrate` and `gkm dev --seed` do before the apps start:\n * migrate, and with `seed`, run the seeds after — once, at startup, never on a\n * save, for the reason {@link reportPendingMigrations} gives.\n *\n * A failure here is thrown, unlike the report's: the developer asked for the\n * database to be brought up to date, and apps started against one in an\n * unknown state would fail somewhere less obvious.\n */\nexport async function prepareDevDatabases(\n\tworkspace: NormalizedWorkspace,\n\tenv: Readonly<Record<string, string>>,\n\t{ seed = false }: { seed?: boolean } = {},\n): Promise<void> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst manifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\tsources,\n\t});\n\tconst options = { root: workspace.root, manifest, sources, env };\n\n\tif (!seed) {\n\t\tlogMigrations(await migrateDatabases(options));\n\t\treturn;\n\t}\n\n\tconst { migrations, seeds } = await migrateAndSeed({\n\t\t...options,\n\t\tstage: workspace.stages.local,\n\t});\n\tlogMigrations(migrations);\n\tlogSeeds(seeds);\n}\n\nexport interface MigrationOptions {\n\t/** The construct the migration is for, by name. */\n\tconstruct: string;\n\t/** What the file is called, after its timestamp. */\n\tname?: string;\n\t/** The file's timestamp; the current time when absent. */\n\tnow?: Date;\n}\n\n/**\n * `gkm migration <construct> [name]` — write the construct's next migration.\n *\n * For a database, an empty `up`/`down` to fill in. For an auth server, what\n * its schema is missing, computed against its tenant once every committed\n * migration has been applied — so the file is only the change, and is\n * reviewed and committed like any other.\n */\nexport async function migrationCommand(\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\tconst ready = await readyStage(process.cwd());\n\tconst targets = migrationTargets(ready.manifest);\n\tconst id = canonicalId(options.construct);\n\n\tconst auth = authConstruct(ready.sources, id);\n\tif (auth) {\n\t\tconst target = targets.find((t) => t.id === auth.databaseId);\n\t\tif (!target) {\n\t\t\tthrow new NoSuchMigrationTarget(auth.databaseId, ids(targets));\n\t\t}\n\t\treturn writeAuthMigration(ready, auth, target, options);\n\t}\n\n\tconst target = targets.find((t) => t.id === id);\n\tif (!target) {\n\t\tthrow new NoSuchMigrationTarget(options.construct, [\n\t\t\t...ids(targets),\n\t\t\t...authIds(ready.sources),\n\t\t]);\n\t}\n\tif (!options.name) throw new MigrationNeedsName(options.construct);\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name)}.ts`,\n\t\tSTUB,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\n/** What an auth construct offers `gkm migration`, duck-typed across copies. */\ninterface AuthConstruct {\n\tid: string;\n\tdatabaseId: string;\n\tpendingMigration(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<string | undefined>;\n}\n\nfunction authConstruct(\n\tsources: Readonly<Record<string, ConstructSource>>,\n\tid: string,\n): AuthConstruct | undefined {\n\tconst construct = sources[id]?.construct as\n\t\t| Partial<AuthConstruct>\n\t\t| undefined;\n\treturn construct &&\n\t\ttypeof construct.pendingMigration === 'function' &&\n\t\ttypeof construct.databaseId === 'string'\n\t\t? (construct as AuthConstruct)\n\t\t: undefined;\n}\n\nfunction authIds(sources: Readonly<Record<string, ConstructSource>>): string[] {\n\treturn Object.keys(sources).filter((id) => authConstruct(sources, id));\n}\n\nasync function writeAuthMigration(\n\tready: ReadyStage,\n\tauth: AuthConstruct,\n\ttarget: MigrationTarget,\n\toptions: MigrationOptions,\n): Promise<string | undefined> {\n\t// Against a tenant already at its committed migrations, so the comparison\n\t// finds only what the files do not yet say.\n\tawait migrateDatabases({\n\t\troot: ready.workspace.root,\n\t\tmanifest: ready.manifest,\n\t\tsources: ready.sources,\n\t\tenv: ready.env,\n\t\tonly: target.id,\n\t});\n\n\t// Its secret and URL come from the stage's secrets; the tenant's owner URL\n\t// from the reconcile above, which wins.\n\tconst stage = ready.workspace.stages.local;\n\tconst secrets = await loadSecretsForApp(\n\t\tawait secretsStoreFor(ready.workspace, stage),\n\t\tstage,\n\t);\n\tconst pending = await auth.pendingMigration({\n\t\tenvParser: new EnvironmentParser({ ...secrets, ...ready.env }),\n\t\tcontext: serviceContext,\n\t});\n\n\tif (!pending) {\n\t\tlogger.log(`✓ ${target.migrations}: ${auth.id} is up to date`);\n\t\treturn undefined;\n\t}\n\n\tconst file = await write(\n\t\tready.workspace.root,\n\t\ttarget,\n\t\t`${stamp(options.now)}_${slug(options.name ?? 'better_auth')}.sql`,\n\t\t`${pending.trim()}\\n`,\n\t);\n\tlogger.log(`📝 ${file}`);\n\treturn file;\n}\n\nconst STUB = `import type { Kysely } from 'kysely';\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n // await db.schema.createTable('…').addColumn(…).execute();\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n // await db.schema.dropTable('…').execute();\n}\n`;\n\nasync function write(\n\troot: string,\n\ttarget: MigrationTarget,\n\tfile: string,\n\tcontent: string,\n): Promise<string> {\n\tconst folder = join(root, target.migrations);\n\tawait mkdir(folder, { recursive: true });\n\tawait writeFile(join(folder, file), content, { flag: 'wx' });\n\treturn `${target.migrations}/${file}`;\n}\n\nfunction ids(targets: readonly MigrationTarget[]): string[] {\n\treturn targets.map((target) => target.id);\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  const require_errors = require("./errors-DMC4Umsb.cjs");
2
- const require_eas = require("./eas-Cne743nQ.cjs");
2
+ const require_eas = require("./eas-CIvNT6qa.cjs");
3
3
  let node_path = require("node:path");
4
4
  let node_fs = require("node:fs");
5
5
  let node_child_process = require("node:child_process");
@@ -121,4 +121,4 @@ function commitSubject(root, stage) {
121
121
  exports.PublishModeConflict = PublishModeConflict;
122
122
  exports.publish = publish;
123
123
 
124
- //# sourceMappingURL=publish-SDG1WfYn.cjs.map
124
+ //# sourceMappingURL=publish-BUvYjiZH.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-SDG1WfYn.cjs","names":["GkmError","easSync","easCli","existsSync","join","readFileSync","spawnSync"],"sources":["../src/mobile/publish.ts"],"sourcesContent":["/**\n * `gkm publish --stage <stage>`: a mobile app shipped for a deployed stage.\n *\n * First the stage's EAS project is synced (`gkm eas:sync`), so what is built\n * reads the stage's values. Then, per platform, the cheapest release that\n * gets the app's current code to the stage's users:\n *\n * - **An update**, when a store build with the same native code is already\n * out: the fingerprint `runtimeVersion: { policy: 'fingerprint' }` gives a\n * build is computed with the stage's profile, and a finished store build\n * of that profile with that fingerprint means only JavaScript changed. It\n * goes out on the stage's channel (`eas update`), which only builds of\n * that runtime receive.\n * - **A store build**, otherwise, submitted to the store as the stage's\n * submit profile says (`eas build --auto-submit`): Android to its Play\n * track; iOS to App Store Connect once `submit.<stage>.ios.ascAppId` is\n * set — without it, iOS is built and not submitted, and gkm says so,\n * rather than leave a prompt nobody in CI can answer.\n *\n * `--build` or `--update` decides instead. An update with no store build to\n * receive it reaches nobody, so `--update` without one is refused.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { GkmError } from '../errors';\nimport {\n\ttype EasCli,\n\ttype EasPlatform,\n\ttype EasSyncOptions,\n\teasCli,\n\teasSync,\n} from './eas.js';\n\nconst logger = console;\n\n/** `--update`, and no store build has this native code. */\nexport class NoStoreBuildForUpdate extends GkmError {\n\tconstructor(\n\t\treadonly platform: EasPlatform,\n\t\treadonly stage: string,\n\t\treadonly fingerprint: string,\n\t) {\n\t\tsuper(\n\t\t\t`No ${platform} store build of '${stage}' has this native code ` +\n\t\t\t\t`(fingerprint ${fingerprint}), so an update would reach nobody: a phone ` +\n\t\t\t\t'only takes updates for the runtime it was built with. Build one: ' +\n\t\t\t\t`gkm publish --stage ${stage} --platform ${platform} --build.`,\n\t\t);\n\t\tthis.name = 'NoStoreBuildForUpdate';\n\t}\n}\n\n/** `--build` and `--update` together. */\nexport class PublishModeConflict extends GkmError {\n\tconstructor() {\n\t\tsuper('Pass --build or --update, not both: one release per platform.');\n\t\tthis.name = 'PublishModeConflict';\n\t}\n}\n\nexport interface PublishOptions extends EasSyncOptions {\n\t/** `ios`, `android`, or both (the default). */\n\tplatform?: EasPlatform | 'all';\n\t/** Build, or update, whatever the fingerprint says. */\n\tmode?: 'build' | 'update';\n\t/** Wait for a build to finish (and submit), rather than leave it on EAS. */\n\twait?: boolean;\n\t/** The update's message: the commit's subject by default. */\n\tmessage?: string;\n}\n\n/** What one platform's release was — or, dry, would be. */\nexport interface PlatformRelease {\n\tplatform: EasPlatform;\n\tfingerprint: string;\n\t/** The store build that already has this native code, if one does. */\n\tstoreBuild?: string;\n\trelease: 'build' | 'update';\n\t/** Whether a build is submitted to its store. */\n\tsubmit: boolean;\n}\n\n/**\n * Ship a mobile app for a deployed stage.\n *\n * @throws {NoStoreBuildForUpdate} — and what `easSync` throws\n */\nexport async function publish(\n\toptions: PublishOptions,\n\teas?: EasCli,\n): Promise<PlatformRelease[]> {\n\tconst synced = await easSync(options, eas);\n\tconst cli = eas ?? easCli(synced.root, synced.values);\n\tconst { dir, stage } = synced;\n\tconst profile = stage.stage;\n\n\tconst easJson = existsSync(join(dir, 'eas.json'))\n\t\t? (JSON.parse(readFileSync(join(dir, 'eas.json'), 'utf8')) as {\n\t\t\t\tsubmit?: Record<string, { ios?: { ascAppId?: string } }>;\n\t\t\t})\n\t\t: {};\n\tconst iosSubmits = Boolean(easJson.submit?.[profile]?.ios?.ascAppId);\n\n\tconst platforms: EasPlatform[] =\n\t\t!options.platform || options.platform === 'all'\n\t\t\t? ['ios', 'android']\n\t\t\t: [options.platform];\n\n\tconst releases: PlatformRelease[] = [];\n\tfor (const platform of platforms) {\n\t\tconst fingerprint = await cli.fingerprint(dir, { platform, profile });\n\t\tconst storeBuild =\n\t\t\toptions.mode === 'build'\n\t\t\t\t? undefined\n\t\t\t\t: await cli.storeBuild(dir, { platform, profile, fingerprint });\n\t\tif (options.mode === 'update' && !storeBuild) {\n\t\t\tthrow new NoStoreBuildForUpdate(platform, profile, fingerprint);\n\t\t}\n\t\tconst release = options.mode ?? (storeBuild ? 'update' : 'build');\n\t\tconst submit = platform === 'android' || iosSubmits;\n\t\treleases.push({\n\t\t\tplatform,\n\t\t\tfingerprint,\n\t\t\t...(storeBuild ? { storeBuild } : {}),\n\t\t\trelease,\n\t\t\tsubmit: release === 'build' && submit,\n\t\t});\n\n\t\tconst why = storeBuild\n\t\t\t? `store build ${storeBuild} has this native code`\n\t\t\t: 'no store build has this native code';\n\t\tif (release === 'update') {\n\t\t\tlogger.log(\n\t\t\t\t`🔁 ${platform}: an update on '${stage.channel}' (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t\t);\n\t\t\tif (options.dryRun) continue;\n\t\t\tawait cli.update(dir, {\n\t\t\t\tplatform,\n\t\t\t\tchannel: stage.channel,\n\t\t\t\tenvironment: stage.environment,\n\t\t\t\tmessage: options.message ?? commitSubject(synced.root, profile),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst where =\n\t\t\tplatform === 'android'\n\t\t\t\t? `submitted to Play's ${stage.android.track} track (${stage.android.releaseStatus})`\n\t\t\t\t: iosSubmits\n\t\t\t\t\t? 'submitted to App Store Connect'\n\t\t\t\t\t: `not submitted: submit.${profile}.ios.ascAppId is not set in eas.json`;\n\t\tlogger.log(\n\t\t\t`📦 ${platform}: a store build of '${profile}', ${where} (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t);\n\t\tif (options.dryRun) continue;\n\t\tawait cli.build(dir, {\n\t\t\tplatform,\n\t\t\tprofile,\n\t\t\tsubmit,\n\t\t\twait: options.wait ?? false,\n\t\t});\n\t}\n\treturn releases;\n}\n\n/** The current commit's subject, for an update's message. */\nfunction commitSubject(root: string, stage: string): string {\n\tconst result = spawnSync('git', ['log', '-1', '--format=%s'], {\n\t\tcwd: root,\n\t\tencoding: 'utf8',\n\t});\n\tconst subject = result.status === 0 ? result.stdout.trim() : '';\n\treturn subject || `gkm publish --stage ${stage}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publish-BUvYjiZH.cjs","names":["GkmError","easSync","easCli","existsSync","join","readFileSync","spawnSync"],"sources":["../src/mobile/publish.ts"],"sourcesContent":["/**\n * `gkm publish --stage <stage>`: a mobile app shipped for a deployed stage.\n *\n * First the stage's EAS project is synced (`gkm eas:sync`), so what is built\n * reads the stage's values. Then, per platform, the cheapest release that\n * gets the app's current code to the stage's users:\n *\n * - **An update**, when a store build with the same native code is already\n * out: the fingerprint `runtimeVersion: { policy: 'fingerprint' }` gives a\n * build is computed with the stage's profile, and a finished store build\n * of that profile with that fingerprint means only JavaScript changed. It\n * goes out on the stage's channel (`eas update`), which only builds of\n * that runtime receive.\n * - **A store build**, otherwise, submitted to the store as the stage's\n * submit profile says (`eas build --auto-submit`): Android to its Play\n * track; iOS to App Store Connect once `submit.<stage>.ios.ascAppId` is\n * set — without it, iOS is built and not submitted, and gkm says so,\n * rather than leave a prompt nobody in CI can answer.\n *\n * `--build` or `--update` decides instead. An update with no store build to\n * receive it reaches nobody, so `--update` without one is refused.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { GkmError } from '../errors';\nimport {\n\ttype EasCli,\n\ttype EasPlatform,\n\ttype EasSyncOptions,\n\teasCli,\n\teasSync,\n} from './eas.js';\n\nconst logger = console;\n\n/** `--update`, and no store build has this native code. */\nexport class NoStoreBuildForUpdate extends GkmError {\n\tconstructor(\n\t\treadonly platform: EasPlatform,\n\t\treadonly stage: string,\n\t\treadonly fingerprint: string,\n\t) {\n\t\tsuper(\n\t\t\t`No ${platform} store build of '${stage}' has this native code ` +\n\t\t\t\t`(fingerprint ${fingerprint}), so an update would reach nobody: a phone ` +\n\t\t\t\t'only takes updates for the runtime it was built with. Build one: ' +\n\t\t\t\t`gkm publish --stage ${stage} --platform ${platform} --build.`,\n\t\t);\n\t\tthis.name = 'NoStoreBuildForUpdate';\n\t}\n}\n\n/** `--build` and `--update` together. */\nexport class PublishModeConflict extends GkmError {\n\tconstructor() {\n\t\tsuper('Pass --build or --update, not both: one release per platform.');\n\t\tthis.name = 'PublishModeConflict';\n\t}\n}\n\nexport interface PublishOptions extends EasSyncOptions {\n\t/** `ios`, `android`, or both (the default). */\n\tplatform?: EasPlatform | 'all';\n\t/** Build, or update, whatever the fingerprint says. */\n\tmode?: 'build' | 'update';\n\t/** Wait for a build to finish (and submit), rather than leave it on EAS. */\n\twait?: boolean;\n\t/** The update's message: the commit's subject by default. */\n\tmessage?: string;\n}\n\n/** What one platform's release was — or, dry, would be. */\nexport interface PlatformRelease {\n\tplatform: EasPlatform;\n\tfingerprint: string;\n\t/** The store build that already has this native code, if one does. */\n\tstoreBuild?: string;\n\trelease: 'build' | 'update';\n\t/** Whether a build is submitted to its store. */\n\tsubmit: boolean;\n}\n\n/**\n * Ship a mobile app for a deployed stage.\n *\n * @throws {NoStoreBuildForUpdate} — and what `easSync` throws\n */\nexport async function publish(\n\toptions: PublishOptions,\n\teas?: EasCli,\n): Promise<PlatformRelease[]> {\n\tconst synced = await easSync(options, eas);\n\tconst cli = eas ?? easCli(synced.root, synced.values);\n\tconst { dir, stage } = synced;\n\tconst profile = stage.stage;\n\n\tconst easJson = existsSync(join(dir, 'eas.json'))\n\t\t? (JSON.parse(readFileSync(join(dir, 'eas.json'), 'utf8')) as {\n\t\t\t\tsubmit?: Record<string, { ios?: { ascAppId?: string } }>;\n\t\t\t})\n\t\t: {};\n\tconst iosSubmits = Boolean(easJson.submit?.[profile]?.ios?.ascAppId);\n\n\tconst platforms: EasPlatform[] =\n\t\t!options.platform || options.platform === 'all'\n\t\t\t? ['ios', 'android']\n\t\t\t: [options.platform];\n\n\tconst releases: PlatformRelease[] = [];\n\tfor (const platform of platforms) {\n\t\tconst fingerprint = await cli.fingerprint(dir, { platform, profile });\n\t\tconst storeBuild =\n\t\t\toptions.mode === 'build'\n\t\t\t\t? undefined\n\t\t\t\t: await cli.storeBuild(dir, { platform, profile, fingerprint });\n\t\tif (options.mode === 'update' && !storeBuild) {\n\t\t\tthrow new NoStoreBuildForUpdate(platform, profile, fingerprint);\n\t\t}\n\t\tconst release = options.mode ?? (storeBuild ? 'update' : 'build');\n\t\tconst submit = platform === 'android' || iosSubmits;\n\t\treleases.push({\n\t\t\tplatform,\n\t\t\tfingerprint,\n\t\t\t...(storeBuild ? { storeBuild } : {}),\n\t\t\trelease,\n\t\t\tsubmit: release === 'build' && submit,\n\t\t});\n\n\t\tconst why = storeBuild\n\t\t\t? `store build ${storeBuild} has this native code`\n\t\t\t: 'no store build has this native code';\n\t\tif (release === 'update') {\n\t\t\tlogger.log(\n\t\t\t\t`🔁 ${platform}: an update on '${stage.channel}' (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t\t);\n\t\t\tif (options.dryRun) continue;\n\t\t\tawait cli.update(dir, {\n\t\t\t\tplatform,\n\t\t\t\tchannel: stage.channel,\n\t\t\t\tenvironment: stage.environment,\n\t\t\t\tmessage: options.message ?? commitSubject(synced.root, profile),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst where =\n\t\t\tplatform === 'android'\n\t\t\t\t? `submitted to Play's ${stage.android.track} track (${stage.android.releaseStatus})`\n\t\t\t\t: iosSubmits\n\t\t\t\t\t? 'submitted to App Store Connect'\n\t\t\t\t\t: `not submitted: submit.${profile}.ios.ascAppId is not set in eas.json`;\n\t\tlogger.log(\n\t\t\t`📦 ${platform}: a store build of '${profile}', ${where} (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t);\n\t\tif (options.dryRun) continue;\n\t\tawait cli.build(dir, {\n\t\t\tplatform,\n\t\t\tprofile,\n\t\t\tsubmit,\n\t\t\twait: options.wait ?? false,\n\t\t});\n\t}\n\treturn releases;\n}\n\n/** The current commit's subject, for an update's message. */\nfunction commitSubject(root: string, stage: string): string {\n\tconst result = spawnSync('git', ['log', '-1', '--format=%s'], {\n\t\tcwd: root,\n\t\tencoding: 'utf8',\n\t});\n\tconst subject = result.status === 0 ? result.stdout.trim() : '';\n\treturn subject || `gkm publish --stage ${stage}`;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
2
- import { easCli, easSync } from "./eas-DEpwISrQ.mjs";
2
+ import { easCli, easSync } from "./eas-CTfRmndW.mjs";
3
3
  import { join } from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { spawnSync } from "node:child_process";
@@ -120,4 +120,4 @@ function commitSubject(root, stage) {
120
120
  //#endregion
121
121
  export { PublishModeConflict, publish };
122
122
 
123
- //# sourceMappingURL=publish-CSctCNf4.mjs.map
123
+ //# sourceMappingURL=publish-hhgraEhO.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-CSctCNf4.mjs","names":[],"sources":["../src/mobile/publish.ts"],"sourcesContent":["/**\n * `gkm publish --stage <stage>`: a mobile app shipped for a deployed stage.\n *\n * First the stage's EAS project is synced (`gkm eas:sync`), so what is built\n * reads the stage's values. Then, per platform, the cheapest release that\n * gets the app's current code to the stage's users:\n *\n * - **An update**, when a store build with the same native code is already\n * out: the fingerprint `runtimeVersion: { policy: 'fingerprint' }` gives a\n * build is computed with the stage's profile, and a finished store build\n * of that profile with that fingerprint means only JavaScript changed. It\n * goes out on the stage's channel (`eas update`), which only builds of\n * that runtime receive.\n * - **A store build**, otherwise, submitted to the store as the stage's\n * submit profile says (`eas build --auto-submit`): Android to its Play\n * track; iOS to App Store Connect once `submit.<stage>.ios.ascAppId` is\n * set — without it, iOS is built and not submitted, and gkm says so,\n * rather than leave a prompt nobody in CI can answer.\n *\n * `--build` or `--update` decides instead. An update with no store build to\n * receive it reaches nobody, so `--update` without one is refused.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { GkmError } from '../errors';\nimport {\n\ttype EasCli,\n\ttype EasPlatform,\n\ttype EasSyncOptions,\n\teasCli,\n\teasSync,\n} from './eas.js';\n\nconst logger = console;\n\n/** `--update`, and no store build has this native code. */\nexport class NoStoreBuildForUpdate extends GkmError {\n\tconstructor(\n\t\treadonly platform: EasPlatform,\n\t\treadonly stage: string,\n\t\treadonly fingerprint: string,\n\t) {\n\t\tsuper(\n\t\t\t`No ${platform} store build of '${stage}' has this native code ` +\n\t\t\t\t`(fingerprint ${fingerprint}), so an update would reach nobody: a phone ` +\n\t\t\t\t'only takes updates for the runtime it was built with. Build one: ' +\n\t\t\t\t`gkm publish --stage ${stage} --platform ${platform} --build.`,\n\t\t);\n\t\tthis.name = 'NoStoreBuildForUpdate';\n\t}\n}\n\n/** `--build` and `--update` together. */\nexport class PublishModeConflict extends GkmError {\n\tconstructor() {\n\t\tsuper('Pass --build or --update, not both: one release per platform.');\n\t\tthis.name = 'PublishModeConflict';\n\t}\n}\n\nexport interface PublishOptions extends EasSyncOptions {\n\t/** `ios`, `android`, or both (the default). */\n\tplatform?: EasPlatform | 'all';\n\t/** Build, or update, whatever the fingerprint says. */\n\tmode?: 'build' | 'update';\n\t/** Wait for a build to finish (and submit), rather than leave it on EAS. */\n\twait?: boolean;\n\t/** The update's message: the commit's subject by default. */\n\tmessage?: string;\n}\n\n/** What one platform's release was — or, dry, would be. */\nexport interface PlatformRelease {\n\tplatform: EasPlatform;\n\tfingerprint: string;\n\t/** The store build that already has this native code, if one does. */\n\tstoreBuild?: string;\n\trelease: 'build' | 'update';\n\t/** Whether a build is submitted to its store. */\n\tsubmit: boolean;\n}\n\n/**\n * Ship a mobile app for a deployed stage.\n *\n * @throws {NoStoreBuildForUpdate} — and what `easSync` throws\n */\nexport async function publish(\n\toptions: PublishOptions,\n\teas?: EasCli,\n): Promise<PlatformRelease[]> {\n\tconst synced = await easSync(options, eas);\n\tconst cli = eas ?? easCli(synced.root, synced.values);\n\tconst { dir, stage } = synced;\n\tconst profile = stage.stage;\n\n\tconst easJson = existsSync(join(dir, 'eas.json'))\n\t\t? (JSON.parse(readFileSync(join(dir, 'eas.json'), 'utf8')) as {\n\t\t\t\tsubmit?: Record<string, { ios?: { ascAppId?: string } }>;\n\t\t\t})\n\t\t: {};\n\tconst iosSubmits = Boolean(easJson.submit?.[profile]?.ios?.ascAppId);\n\n\tconst platforms: EasPlatform[] =\n\t\t!options.platform || options.platform === 'all'\n\t\t\t? ['ios', 'android']\n\t\t\t: [options.platform];\n\n\tconst releases: PlatformRelease[] = [];\n\tfor (const platform of platforms) {\n\t\tconst fingerprint = await cli.fingerprint(dir, { platform, profile });\n\t\tconst storeBuild =\n\t\t\toptions.mode === 'build'\n\t\t\t\t? undefined\n\t\t\t\t: await cli.storeBuild(dir, { platform, profile, fingerprint });\n\t\tif (options.mode === 'update' && !storeBuild) {\n\t\t\tthrow new NoStoreBuildForUpdate(platform, profile, fingerprint);\n\t\t}\n\t\tconst release = options.mode ?? (storeBuild ? 'update' : 'build');\n\t\tconst submit = platform === 'android' || iosSubmits;\n\t\treleases.push({\n\t\t\tplatform,\n\t\t\tfingerprint,\n\t\t\t...(storeBuild ? { storeBuild } : {}),\n\t\t\trelease,\n\t\t\tsubmit: release === 'build' && submit,\n\t\t});\n\n\t\tconst why = storeBuild\n\t\t\t? `store build ${storeBuild} has this native code`\n\t\t\t: 'no store build has this native code';\n\t\tif (release === 'update') {\n\t\t\tlogger.log(\n\t\t\t\t`🔁 ${platform}: an update on '${stage.channel}' (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t\t);\n\t\t\tif (options.dryRun) continue;\n\t\t\tawait cli.update(dir, {\n\t\t\t\tplatform,\n\t\t\t\tchannel: stage.channel,\n\t\t\t\tenvironment: stage.environment,\n\t\t\t\tmessage: options.message ?? commitSubject(synced.root, profile),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst where =\n\t\t\tplatform === 'android'\n\t\t\t\t? `submitted to Play's ${stage.android.track} track (${stage.android.releaseStatus})`\n\t\t\t\t: iosSubmits\n\t\t\t\t\t? 'submitted to App Store Connect'\n\t\t\t\t\t: `not submitted: submit.${profile}.ios.ascAppId is not set in eas.json`;\n\t\tlogger.log(\n\t\t\t`📦 ${platform}: a store build of '${profile}', ${where} (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t);\n\t\tif (options.dryRun) continue;\n\t\tawait cli.build(dir, {\n\t\t\tplatform,\n\t\t\tprofile,\n\t\t\tsubmit,\n\t\t\twait: options.wait ?? false,\n\t\t});\n\t}\n\treturn releases;\n}\n\n/** The current commit's subject, for an update's message. */\nfunction commitSubject(root: string, stage: string): string {\n\tconst result = spawnSync('git', ['log', '-1', '--format=%s'], {\n\t\tcwd: root,\n\t\tencoding: 'utf8',\n\t});\n\tconst subject = result.status === 0 ? result.stdout.trim() : '';\n\treturn subject || `gkm publish --stage ${stage}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,SAAS;;AAGf,IAAa,wBAAb,cAA2C,SAAS;CAEzC;CACA;CACA;CAHV,YACC,UACA,OACA,aACC;EACD,MACC,MAAM,SAAS,mBAAmB,MAAM,sCACvB,YAAY,mIAEL,MAAM,cAAc,SAAS,UACtD;EATS,KAAA,WAAA;EACA,KAAA,QAAA;EACA,KAAA,cAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,sBAAb,cAAyC,SAAS;CACjD,cAAc;EACb,MAAM,+DAA+D;EACrE,KAAK,OAAO;CACb;AACD;;;;;;AA6BA,eAAsB,QACrB,SACA,KAC6B;CAC7B,MAAM,SAAS,MAAM,QAAQ,SAAS,GAAG;CACzC,MAAM,MAAM,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM;CACpD,MAAM,EAAE,KAAK,UAAU;CACvB,MAAM,UAAU,MAAM;CAEtB,MAAM,UAAU,WAAW,KAAK,KAAK,UAAU,CAAC,IAC5C,KAAK,MAAM,aAAa,KAAK,KAAK,UAAU,GAAG,MAAM,CAAC,IAGvD,CAAC;CACJ,MAAM,aAAa,QAAQ,QAAQ,SAAS,QAAQ,EAAE,KAAK,QAAQ;CAEnE,MAAM,YACL,CAAC,QAAQ,YAAY,QAAQ,aAAa,QACvC,CAAC,OAAO,SAAS,IACjB,CAAC,QAAQ,QAAQ;CAErB,MAAM,WAA8B,CAAC;CACrC,KAAK,MAAM,YAAY,WAAW;EACjC,MAAM,cAAc,MAAM,IAAI,YAAY,KAAK;GAAE;GAAU;EAAQ,CAAC;EACpE,MAAM,aACL,QAAQ,SAAS,UACd,KAAA,IACA,MAAM,IAAI,WAAW,KAAK;GAAE;GAAU;GAAS;EAAY,CAAC;EAChE,IAAI,QAAQ,SAAS,YAAY,CAAC,YACjC,MAAM,IAAI,sBAAsB,UAAU,SAAS,WAAW;EAE/D,MAAM,UAAU,QAAQ,SAAS,aAAa,WAAW;EACzD,MAAM,SAAS,aAAa,aAAa;EACzC,SAAS,KAAK;GACb;GACA;GACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC;GACA,QAAQ,YAAY,WAAW;EAChC,CAAC;EAED,MAAM,MAAM,aACT,eAAe,WAAW,yBAC1B;EACH,IAAI,YAAY,UAAU;GACzB,OAAO,IACN,MAAM,SAAS,kBAAkB,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,SAAS,eAAe,IAC5F;GACA,IAAI,QAAQ,QAAQ;GACpB,MAAM,IAAI,OAAO,KAAK;IACrB;IACA,SAAS,MAAM;IACf,aAAa,MAAM;IACnB,SAAS,QAAQ,WAAW,cAAc,OAAO,MAAM,OAAO;GAC/D,CAAC;GACD;EACD;EAEA,MAAM,QACL,aAAa,YACV,uBAAuB,MAAM,QAAQ,MAAM,UAAU,MAAM,QAAQ,cAAc,KACjF,aACC,mCACA,yBAAyB,QAAQ;EACtC,OAAO,IACN,MAAM,SAAS,sBAAsB,QAAQ,KAAK,MAAM,IAAI,IAAI,GAAG,QAAQ,SAAS,eAAe,IACpG;EACA,IAAI,QAAQ,QAAQ;EACpB,MAAM,IAAI,MAAM,KAAK;GACpB;GACA;GACA;GACA,MAAM,QAAQ,QAAQ;EACvB,CAAC;CACF;CACA,OAAO;AACR;;AAGA,SAAS,cAAc,MAAc,OAAuB;CAC3D,MAAM,SAAS,UAAU,OAAO;EAAC;EAAO;EAAM;CAAa,GAAG;EAC7D,KAAK;EACL,UAAU;CACX,CAAC;CAED,QADgB,OAAO,WAAW,IAAI,OAAO,OAAO,KAAK,IAAI,OAC3C,uBAAuB;AAC1C"}
1
+ {"version":3,"file":"publish-hhgraEhO.mjs","names":[],"sources":["../src/mobile/publish.ts"],"sourcesContent":["/**\n * `gkm publish --stage <stage>`: a mobile app shipped for a deployed stage.\n *\n * First the stage's EAS project is synced (`gkm eas:sync`), so what is built\n * reads the stage's values. Then, per platform, the cheapest release that\n * gets the app's current code to the stage's users:\n *\n * - **An update**, when a store build with the same native code is already\n * out: the fingerprint `runtimeVersion: { policy: 'fingerprint' }` gives a\n * build is computed with the stage's profile, and a finished store build\n * of that profile with that fingerprint means only JavaScript changed. It\n * goes out on the stage's channel (`eas update`), which only builds of\n * that runtime receive.\n * - **A store build**, otherwise, submitted to the store as the stage's\n * submit profile says (`eas build --auto-submit`): Android to its Play\n * track; iOS to App Store Connect once `submit.<stage>.ios.ascAppId` is\n * set — without it, iOS is built and not submitted, and gkm says so,\n * rather than leave a prompt nobody in CI can answer.\n *\n * `--build` or `--update` decides instead. An update with no store build to\n * receive it reaches nobody, so `--update` without one is refused.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { GkmError } from '../errors';\nimport {\n\ttype EasCli,\n\ttype EasPlatform,\n\ttype EasSyncOptions,\n\teasCli,\n\teasSync,\n} from './eas.js';\n\nconst logger = console;\n\n/** `--update`, and no store build has this native code. */\nexport class NoStoreBuildForUpdate extends GkmError {\n\tconstructor(\n\t\treadonly platform: EasPlatform,\n\t\treadonly stage: string,\n\t\treadonly fingerprint: string,\n\t) {\n\t\tsuper(\n\t\t\t`No ${platform} store build of '${stage}' has this native code ` +\n\t\t\t\t`(fingerprint ${fingerprint}), so an update would reach nobody: a phone ` +\n\t\t\t\t'only takes updates for the runtime it was built with. Build one: ' +\n\t\t\t\t`gkm publish --stage ${stage} --platform ${platform} --build.`,\n\t\t);\n\t\tthis.name = 'NoStoreBuildForUpdate';\n\t}\n}\n\n/** `--build` and `--update` together. */\nexport class PublishModeConflict extends GkmError {\n\tconstructor() {\n\t\tsuper('Pass --build or --update, not both: one release per platform.');\n\t\tthis.name = 'PublishModeConflict';\n\t}\n}\n\nexport interface PublishOptions extends EasSyncOptions {\n\t/** `ios`, `android`, or both (the default). */\n\tplatform?: EasPlatform | 'all';\n\t/** Build, or update, whatever the fingerprint says. */\n\tmode?: 'build' | 'update';\n\t/** Wait for a build to finish (and submit), rather than leave it on EAS. */\n\twait?: boolean;\n\t/** The update's message: the commit's subject by default. */\n\tmessage?: string;\n}\n\n/** What one platform's release was — or, dry, would be. */\nexport interface PlatformRelease {\n\tplatform: EasPlatform;\n\tfingerprint: string;\n\t/** The store build that already has this native code, if one does. */\n\tstoreBuild?: string;\n\trelease: 'build' | 'update';\n\t/** Whether a build is submitted to its store. */\n\tsubmit: boolean;\n}\n\n/**\n * Ship a mobile app for a deployed stage.\n *\n * @throws {NoStoreBuildForUpdate} — and what `easSync` throws\n */\nexport async function publish(\n\toptions: PublishOptions,\n\teas?: EasCli,\n): Promise<PlatformRelease[]> {\n\tconst synced = await easSync(options, eas);\n\tconst cli = eas ?? easCli(synced.root, synced.values);\n\tconst { dir, stage } = synced;\n\tconst profile = stage.stage;\n\n\tconst easJson = existsSync(join(dir, 'eas.json'))\n\t\t? (JSON.parse(readFileSync(join(dir, 'eas.json'), 'utf8')) as {\n\t\t\t\tsubmit?: Record<string, { ios?: { ascAppId?: string } }>;\n\t\t\t})\n\t\t: {};\n\tconst iosSubmits = Boolean(easJson.submit?.[profile]?.ios?.ascAppId);\n\n\tconst platforms: EasPlatform[] =\n\t\t!options.platform || options.platform === 'all'\n\t\t\t? ['ios', 'android']\n\t\t\t: [options.platform];\n\n\tconst releases: PlatformRelease[] = [];\n\tfor (const platform of platforms) {\n\t\tconst fingerprint = await cli.fingerprint(dir, { platform, profile });\n\t\tconst storeBuild =\n\t\t\toptions.mode === 'build'\n\t\t\t\t? undefined\n\t\t\t\t: await cli.storeBuild(dir, { platform, profile, fingerprint });\n\t\tif (options.mode === 'update' && !storeBuild) {\n\t\t\tthrow new NoStoreBuildForUpdate(platform, profile, fingerprint);\n\t\t}\n\t\tconst release = options.mode ?? (storeBuild ? 'update' : 'build');\n\t\tconst submit = platform === 'android' || iosSubmits;\n\t\treleases.push({\n\t\t\tplatform,\n\t\t\tfingerprint,\n\t\t\t...(storeBuild ? { storeBuild } : {}),\n\t\t\trelease,\n\t\t\tsubmit: release === 'build' && submit,\n\t\t});\n\n\t\tconst why = storeBuild\n\t\t\t? `store build ${storeBuild} has this native code`\n\t\t\t: 'no store build has this native code';\n\t\tif (release === 'update') {\n\t\t\tlogger.log(\n\t\t\t\t`🔁 ${platform}: an update on '${stage.channel}' (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t\t);\n\t\t\tif (options.dryRun) continue;\n\t\t\tawait cli.update(dir, {\n\t\t\t\tplatform,\n\t\t\t\tchannel: stage.channel,\n\t\t\t\tenvironment: stage.environment,\n\t\t\t\tmessage: options.message ?? commitSubject(synced.root, profile),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst where =\n\t\t\tplatform === 'android'\n\t\t\t\t? `submitted to Play's ${stage.android.track} track (${stage.android.releaseStatus})`\n\t\t\t\t: iosSubmits\n\t\t\t\t\t? 'submitted to App Store Connect'\n\t\t\t\t\t: `not submitted: submit.${profile}.ios.ascAppId is not set in eas.json`;\n\t\tlogger.log(\n\t\t\t`📦 ${platform}: a store build of '${profile}', ${where} (${why})${options.dryRun ? ' (dry run)' : ''}`,\n\t\t);\n\t\tif (options.dryRun) continue;\n\t\tawait cli.build(dir, {\n\t\t\tplatform,\n\t\t\tprofile,\n\t\t\tsubmit,\n\t\t\twait: options.wait ?? false,\n\t\t});\n\t}\n\treturn releases;\n}\n\n/** The current commit's subject, for an update's message. */\nfunction commitSubject(root: string, stage: string): string {\n\tconst result = spawnSync('git', ['log', '-1', '--format=%s'], {\n\t\tcwd: root,\n\t\tencoding: 'utf8',\n\t});\n\tconst subject = result.status === 0 ? result.stdout.trim() : '';\n\treturn subject || `gkm publish --stage ${stage}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,SAAS;;AAGf,IAAa,wBAAb,cAA2C,SAAS;CAEzC;CACA;CACA;CAHV,YACC,UACA,OACA,aACC;EACD,MACC,MAAM,SAAS,mBAAmB,MAAM,sCACvB,YAAY,mIAEL,MAAM,cAAc,SAAS,UACtD;EATS,KAAA,WAAA;EACA,KAAA,QAAA;EACA,KAAA,cAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,sBAAb,cAAyC,SAAS;CACjD,cAAc;EACb,MAAM,+DAA+D;EACrE,KAAK,OAAO;CACb;AACD;;;;;;AA6BA,eAAsB,QACrB,SACA,KAC6B;CAC7B,MAAM,SAAS,MAAM,QAAQ,SAAS,GAAG;CACzC,MAAM,MAAM,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM;CACpD,MAAM,EAAE,KAAK,UAAU;CACvB,MAAM,UAAU,MAAM;CAEtB,MAAM,UAAU,WAAW,KAAK,KAAK,UAAU,CAAC,IAC5C,KAAK,MAAM,aAAa,KAAK,KAAK,UAAU,GAAG,MAAM,CAAC,IAGvD,CAAC;CACJ,MAAM,aAAa,QAAQ,QAAQ,SAAS,QAAQ,EAAE,KAAK,QAAQ;CAEnE,MAAM,YACL,CAAC,QAAQ,YAAY,QAAQ,aAAa,QACvC,CAAC,OAAO,SAAS,IACjB,CAAC,QAAQ,QAAQ;CAErB,MAAM,WAA8B,CAAC;CACrC,KAAK,MAAM,YAAY,WAAW;EACjC,MAAM,cAAc,MAAM,IAAI,YAAY,KAAK;GAAE;GAAU;EAAQ,CAAC;EACpE,MAAM,aACL,QAAQ,SAAS,UACd,KAAA,IACA,MAAM,IAAI,WAAW,KAAK;GAAE;GAAU;GAAS;EAAY,CAAC;EAChE,IAAI,QAAQ,SAAS,YAAY,CAAC,YACjC,MAAM,IAAI,sBAAsB,UAAU,SAAS,WAAW;EAE/D,MAAM,UAAU,QAAQ,SAAS,aAAa,WAAW;EACzD,MAAM,SAAS,aAAa,aAAa;EACzC,SAAS,KAAK;GACb;GACA;GACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC;GACA,QAAQ,YAAY,WAAW;EAChC,CAAC;EAED,MAAM,MAAM,aACT,eAAe,WAAW,yBAC1B;EACH,IAAI,YAAY,UAAU;GACzB,OAAO,IACN,MAAM,SAAS,kBAAkB,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,SAAS,eAAe,IAC5F;GACA,IAAI,QAAQ,QAAQ;GACpB,MAAM,IAAI,OAAO,KAAK;IACrB;IACA,SAAS,MAAM;IACf,aAAa,MAAM;IACnB,SAAS,QAAQ,WAAW,cAAc,OAAO,MAAM,OAAO;GAC/D,CAAC;GACD;EACD;EAEA,MAAM,QACL,aAAa,YACV,uBAAuB,MAAM,QAAQ,MAAM,UAAU,MAAM,QAAQ,cAAc,KACjF,aACC,mCACA,yBAAyB,QAAQ;EACtC,OAAO,IACN,MAAM,SAAS,sBAAsB,QAAQ,KAAK,MAAM,IAAI,IAAI,GAAG,QAAQ,SAAS,eAAe,IACpG;EACA,IAAI,QAAQ,QAAQ;EACpB,MAAM,IAAI,MAAM,KAAK;GACpB;GACA;GACA;GACA,MAAM,QAAQ,QAAQ;EACvB,CAAC;CACF;CACA,OAAO;AACR;;AAGA,SAAS,cAAc,MAAc,OAAuB;CAC3D,MAAM,SAAS,UAAU,OAAO;EAAC;EAAO;EAAM;CAAa,GAAG;EAC7D,KAAK;EACL,UAAU;CACX,CAAC;CAED,QADgB,OAAO,WAAW,IAAI,OAAO,OAAO,KAAK,IAAI,OAC3C,uBAAuB;AAC1C"}
@@ -200,8 +200,8 @@ var InvalidTargetOptions = class extends require_errors.GkmError {
200
200
  };
201
201
  /** The built-ins `gkm deploy` deploys through, loaded only when used. */
202
202
  const BUILTIN_LOADERS = {
203
- dokploy: async () => (await Promise.resolve().then(() => require("./dokploy-OqBh8G4g.cjs"))).dokployTarget,
204
- compose: async () => (await Promise.resolve().then(() => require("./compose-ByIbIrzs.cjs")).then((n) => n.compose_exports)).composeTarget(),
203
+ dokploy: async () => (await Promise.resolve().then(() => require("./dokploy-Di7rC2z0.cjs"))).dokployTarget,
204
+ compose: async () => (await Promise.resolve().then(() => require("./compose-CYeMW2_o.cjs")).then((n) => n.compose_exports)).composeTarget(),
205
205
  sst: async () => (await Promise.resolve().then(() => require("./sst-B-Uz4cvm.cjs")).then((n) => n.sst_exports)).sstTarget
206
206
  };
207
207
  /** The target `name` means, in this workspace, for this host. */
@@ -313,4 +313,4 @@ Object.defineProperty(exports, "runtimeOf", {
313
313
  }
314
314
  });
315
315
 
316
- //# sourceMappingURL=resolve-wG49Woex.cjs.map
316
+ //# sourceMappingURL=resolve-DdFKTx8l.cjs.map