@cosmicdrift/kumiko-framework 0.387.0 → 0.388.0

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 (240) hide show
  1. package/AGENTS.md +6 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/legal-pages.md +1 -1
  9. package/agents/examples/prod-boot-options.md +16 -0
  10. package/agents/examples/rate-limiting.md +48 -0
  11. package/agents/examples/record-detail-layout.md +692 -0
  12. package/agents/examples/seed-migration.md +212 -0
  13. package/agents/examples/wizard-form.md +941 -0
  14. package/agents/examples/writeform-section.md +548 -0
  15. package/agents/features.md +19 -2
  16. package/agents/widgets.md +1 -0
  17. package/dist/api/client-error-route.js +8 -2
  18. package/dist/api/index.d.ts +3 -1
  19. package/dist/api/index.js +3 -1
  20. package/dist/api/request-context.d.ts +1 -1
  21. package/dist/api/request-context.js +1 -1
  22. package/dist/api/server-boot-checks.d.ts +10 -0
  23. package/dist/api/server-boot-checks.js +157 -0
  24. package/dist/api/server-event-dispatcher.d.ts +26 -0
  25. package/dist/api/server-event-dispatcher.js +208 -0
  26. package/dist/api/server-extra-routes.d.ts +11 -0
  27. package/dist/api/server-extra-routes.js +203 -0
  28. package/dist/api/server-middleware.d.ts +20 -0
  29. package/dist/api/server-middleware.js +211 -0
  30. package/dist/api/server-routes.d.ts +19 -0
  31. package/dist/api/server-routes.js +293 -0
  32. package/dist/api/server-websocket-routes.d.ts +4 -0
  33. package/dist/api/server-websocket-routes.js +108 -0
  34. package/dist/api/server-wiring.d.ts +32 -0
  35. package/dist/api/server-wiring.js +236 -0
  36. package/dist/api/server.d.ts +10 -15
  37. package/dist/api/server.js +48 -1251
  38. package/dist/api/session-cookies.js +4 -4
  39. package/dist/api/sse-route.js +16 -12
  40. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  41. package/dist/crypto/owner-transition-rekey.js +1 -1
  42. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  43. package/dist/crypto/pii-field-encryption.js +3 -1
  44. package/dist/db/aggregate-sql.d.ts +21 -0
  45. package/dist/db/aggregate-sql.js +155 -0
  46. package/dist/db/closed-connection.d.ts +9 -0
  47. package/dist/db/closed-connection.js +95 -0
  48. package/dist/db/dialect.js +1 -1
  49. package/dist/db/distinct-on.d.ts +1 -1
  50. package/dist/db/driver-row-coercion.d.ts +2 -0
  51. package/dist/db/driver-row-coercion.js +186 -0
  52. package/dist/db/driver-value-preparation.d.ts +16 -0
  53. package/dist/db/driver-value-preparation.js +99 -0
  54. package/dist/db/entity-field-encryption.d.ts +1 -0
  55. package/dist/db/entity-field-encryption.js +1 -1
  56. package/dist/db/entity-table-meta.js +103 -110
  57. package/dist/db/event-store-executor-context.d.ts +2 -1
  58. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  59. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  60. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  61. package/dist/db/event-store-executor-read-filter.js +203 -0
  62. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  63. package/dist/db/event-store-executor-read-paging.js +99 -0
  64. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  65. package/dist/db/event-store-executor-read-rows.js +32 -0
  66. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  67. package/dist/db/event-store-executor-read-search.js +221 -0
  68. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  69. package/dist/db/event-store-executor-read-sort.js +130 -0
  70. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  71. package/dist/db/event-store-executor-read-where.js +81 -0
  72. package/dist/db/event-store-executor-read.d.ts +2 -9
  73. package/dist/db/event-store-executor-read.js +48 -701
  74. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  75. package/dist/db/event-store-executor-write-steps.js +100 -0
  76. package/dist/db/event-store-executor-write.js +395 -499
  77. package/dist/db/index.d.ts +6 -2
  78. package/dist/db/index.js +5 -1
  79. package/dist/db/list-aggregate-labels.d.ts +14 -0
  80. package/dist/db/list-aggregate-labels.js +26 -0
  81. package/dist/db/migrate-generator.js +71 -86
  82. package/dist/db/migrate-runner.js +4 -1
  83. package/dist/db/query.d.ts +2 -15
  84. package/dist/db/query.js +8 -761
  85. package/dist/db/reference-data.js +6 -1
  86. package/dist/db/table-builder.js +113 -166
  87. package/dist/db/table-info.d.ts +10 -0
  88. package/dist/db/table-info.js +45 -0
  89. package/dist/db/temporal-value-guards.d.ts +6 -0
  90. package/dist/db/temporal-value-guards.js +15 -0
  91. package/dist/db/tenant-db-runner.d.ts +1 -0
  92. package/dist/db/tenant-db-runner.js +3 -0
  93. package/dist/db/tenant-db.js +39 -9
  94. package/dist/db/where-clause.d.ts +6 -0
  95. package/dist/db/where-clause.js +236 -0
  96. package/dist/derivatives/delete-stored-file.js +2 -1
  97. package/dist/engine/boot-validator/boot-check.js +1 -1
  98. package/dist/engine/boot-validator/detail-screens.js +2 -2
  99. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  100. package/dist/engine/boot-validator/entity-handler.js +17 -0
  101. package/dist/engine/boot-validator/form-derivations.js +35 -1
  102. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  103. package/dist/engine/boot-validator/index.js +2 -0
  104. package/dist/engine/boot-validator/pii-retention.js +233 -155
  105. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  106. package/dist/engine/boot-validator/screens-actions.js +8 -1
  107. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  108. package/dist/engine/boot-validator/screens-forms.js +2 -6
  109. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  110. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  111. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  112. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  113. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  114. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  115. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  116. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  117. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  118. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  119. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  120. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  121. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  122. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  123. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens-lists.js +12 -9
  125. package/dist/engine/boot-validator/screens.d.ts +0 -2
  126. package/dist/engine/boot-validator/screens.js +99 -860
  127. package/dist/engine/build-app-schema.js +56 -96
  128. package/dist/engine/define-feature.js +9 -2
  129. package/dist/engine/define-handler.js +1 -0
  130. package/dist/engine/entity-handlers.d.ts +13 -0
  131. package/dist/engine/entity-handlers.js +10 -21
  132. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  133. package/dist/engine/entity-list-runtime-options.js +18 -0
  134. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  135. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  136. package/dist/engine/feature-ast/patch.js +4 -182
  137. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  138. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  139. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  140. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  141. package/dist/engine/feature-ast/patterns.d.ts +9 -0
  142. package/dist/engine/feature-ast/patterns.js +43 -51
  143. package/dist/engine/feature-ast/render.js +48 -87
  144. package/dist/engine/feature-entity-handlers.js +3 -0
  145. package/dist/engine/form-derivations.js +18 -7
  146. package/dist/engine/handler-field-references.js +1 -1
  147. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  148. package/dist/engine/project-app-schema-for-roles.js +6 -4
  149. package/dist/engine/registry-ingest.js +1 -1
  150. package/dist/engine/schema-builder.js +228 -212
  151. package/dist/engine/screen-helpers.js +1 -1
  152. package/dist/engine/screen-kind.d.ts +7 -0
  153. package/dist/engine/screen-kind.js +1 -0
  154. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  155. package/dist/engine/validate-projection-allowlist.js +2 -1
  156. package/dist/entrypoint/index.d.ts +4 -1
  157. package/dist/entrypoint/index.js +7 -1
  158. package/dist/errors/docs-url.d.ts +1 -1
  159. package/dist/errors/docs-url.js +22 -2
  160. package/dist/es-ops/context.js +1 -1
  161. package/dist/event-store/admin-api.js +1 -1
  162. package/dist/event-store/event-store.js +10 -4
  163. package/dist/files/bind-pending-file-refs.js +3 -3
  164. package/dist/files/create-file-ref-from-bytes.js +4 -1
  165. package/dist/files/file-handle.js +2 -3
  166. package/dist/files/file-routes.js +11 -8
  167. package/dist/files/resolve-content-type.js +43 -0
  168. package/dist/files/upload-ledger.js +7 -0
  169. package/dist/i18n/required-surface-keys.js +182 -206
  170. package/dist/internal/stack-support.d.ts +2 -1
  171. package/dist/internal/stack-support.js +1 -1
  172. package/dist/jobs/job-runner-context.d.ts +32 -0
  173. package/dist/jobs/job-runner-context.js +305 -0
  174. package/dist/jobs/job-runner-execution.d.ts +7 -0
  175. package/dist/jobs/job-runner-execution.js +160 -0
  176. package/dist/jobs/job-runner-gates.d.ts +19 -0
  177. package/dist/jobs/job-runner-gates.js +198 -0
  178. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  179. package/dist/jobs/job-runner-helpers.js +61 -2
  180. package/dist/jobs/job-runner-scope.d.ts +44 -0
  181. package/dist/jobs/job-runner-scope.js +102 -0
  182. package/dist/jobs/job-runner-types.d.ts +12 -0
  183. package/dist/jobs/job-runner.d.ts +0 -1
  184. package/dist/jobs/job-runner.js +47 -657
  185. package/dist/observability/escape-hatch-report.d.ts +3 -0
  186. package/dist/observability/escape-hatch-report.js +12 -1
  187. package/dist/pipeline/cascade-handler.js +165 -138
  188. package/dist/pipeline/dispatch-query.js +10 -3
  189. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  190. package/dist/pipeline/dispatch-shared.js +58 -710
  191. package/dist/pipeline/dispatch-write.js +296 -189
  192. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  193. package/dist/pipeline/event-dispatcher-delivery.js +3 -5
  194. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  195. package/dist/pipeline/handler-context-auth.js +54 -0
  196. package/dist/pipeline/handler-context-config.d.ts +14 -0
  197. package/dist/pipeline/handler-context-config.js +113 -0
  198. package/dist/pipeline/handler-context-db.d.ts +24 -0
  199. package/dist/pipeline/handler-context-db.js +149 -0
  200. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  201. package/dist/pipeline/handler-context-dispatch.js +89 -0
  202. package/dist/pipeline/handler-context-events.d.ts +43 -0
  203. package/dist/pipeline/handler-context-events.js +237 -0
  204. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  205. package/dist/pipeline/handler-context-notify.js +55 -0
  206. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  207. package/dist/pipeline/handler-context-observability.js +21 -0
  208. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  209. package/dist/pipeline/handler-context-scope.js +88 -0
  210. package/dist/pipeline/notify-scope.js +12 -1
  211. package/dist/pipeline/pre-handler-gates.js +20 -4
  212. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  213. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  214. package/dist/pipeline/projection-rebuild.js +2 -1
  215. package/dist/schema-cli/apply-command.d.ts +2 -0
  216. package/dist/schema-cli/apply-command.js +97 -0
  217. package/dist/schema-cli/baseline-command.d.ts +2 -0
  218. package/dist/schema-cli/baseline-command.js +33 -0
  219. package/dist/schema-cli/generate-command.d.ts +2 -0
  220. package/dist/schema-cli/generate-command.js +90 -0
  221. package/dist/schema-cli/shared.d.ts +39 -0
  222. package/dist/schema-cli/shared.js +95 -0
  223. package/dist/schema-cli/status-command.d.ts +2 -0
  224. package/dist/schema-cli/status-command.js +73 -0
  225. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  226. package/dist/schema-cli/validate-boot-input.js +130 -0
  227. package/dist/schema-cli/validate-command.d.ts +2 -0
  228. package/dist/schema-cli/validate-command.js +130 -0
  229. package/dist/schema-cli.d.ts +2 -18
  230. package/dist/schema-cli.js +47 -604
  231. package/dist/search/meilisearch-adapter.js +2 -1
  232. package/dist/upgrade-cli.js +149 -121
  233. package/package.json +4 -4
  234. package/src/changes.json +310 -0
  235. package/src/scripts/codemod/feature-layout.ts +10 -2
  236. package/src/scripts/codemod/object-form.ts +41 -4
  237. package/src/scripts/codemod/package-merge.ts +58 -8
  238. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  239. package/src/scripts/codemod/testing-injection.ts +122 -42
  240. package/src/scripts/codemod/testing-move.ts +29 -7
@@ -7,212 +7,39 @@
7
7
  // NO-MAGIC-ON-DATA: reads only checked-in artifacts (kumiko/schema.ts →
8
8
  // ENTITY_METAS, kumiko/migrations/*.sql). Never auto-generates at runtime,
9
9
  // never applies on app-boot — apply/baseline are explicit deploy-steps.
10
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
11
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
12
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
13
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
14
- });
15
- }
16
- return path;
17
- };
18
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
19
- import { join, resolve as resolvePath } from "node:path";
20
- import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
21
- import { configureBlindIndexKey, configurePiiSubjectKms, resolveKmsWiringAsync, } from "./crypto/index.js";
22
- import { assertUniqueTableNames, assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
23
- import { isEntityTableMeta } from "./db/table-introspection.js";
24
- import { validateBoot } from "./engine/boot-validator.js";
25
- import { FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, } from "./engine/extension-names.js";
26
- import { createRegistry } from "./engine/registry.js";
27
- import { createEventsTable } from "./event-store/index.js";
28
- import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "./migrations/index.js";
29
- import { createEventConsumerStateTable, createProjectionStateTable } from "./pipeline/index.js";
10
+ import { resolve as resolvePath } from "node:path";
11
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
12
+ import { runApplyCommand } from "./schema-cli/apply-command.js";
13
+ import { runBaselineCommand } from "./schema-cli/baseline-command.js";
14
+ import { runGenerateCommand } from "./schema-cli/generate-command.js";
15
+ import { runStatusCommand } from "./schema-cli/status-command.js";
16
+ import { runValidateCommand } from "./schema-cli/validate-command.js";
30
17
  import { ensureTemporalPolyfill } from "./time/index.js";
31
- const SNAPSHOT_FILENAME = ".snapshot.json";
32
- // `validate` only sees the composed FEATURES. The file provider normally comes from
33
- // runProdApp's `fileStorage` option, which defaults FILE_STORAGE_PROVIDER at boot, so
34
- // an unset variable here is no misconfiguration. The deploy env is checked by the real boot.
35
- function withFileStoragePresence(env) {
36
- return env[FILE_STORAGE_PROVIDER_ENV] === undefined
37
- ? { ...env, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL }
38
- : { ...env };
39
- }
40
- function isStringSet(value) {
41
- return value instanceof Set && [...value].every((entry) => typeof entry === "string");
42
- }
43
- function isNonEmptyStringArray(value) {
44
- return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry !== "");
45
- }
46
- function isBoolean(value) {
47
- return typeof value === "boolean";
48
- }
49
- function validateBootOptionsError(detail) {
50
- return new Error(`kumiko/schema.ts: \`VALIDATE_BOOT_OPTIONS\` ${detail}`);
51
- }
52
- function readOptionalOption(raw, key, isValid, expected) {
53
- const value = Reflect.get(raw, key);
54
- if (value === undefined)
55
- return undefined;
56
- if (!isValid(value))
57
- throw validateBootOptionsError(`\`${key}\` must be ${expected}.`);
58
- return value;
59
- }
60
- // Boundary check for `export const VALIDATE_BOOT_OPTIONS` in kumiko/schema.ts.
61
- // Only the options that change what validateBoot reports are honoured; `env` and
62
- // `entityFieldCipher` are runner-injected and deliberately ignored here.
63
- function parseValidateBootOptions(raw) {
64
- if (raw === undefined)
65
- return {};
66
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
67
- throw validateBootOptionsError("must be an object (ValidateBootOptions).");
68
- }
69
- const navAllowlist = readOptionalOption(raw, "navAllowlist", isStringSet, "a Set<string>");
70
- const navAllowlistExempt = readOptionalOption(raw, "navAllowlistExempt", isStringSet, "a Set<string>");
71
- const warnOnUniqueAccessRoles = readOptionalOption(raw, "warnOnUniqueAccessRoles", isBoolean, "a boolean");
72
- const requiredLocales = readOptionalOption(raw, "requiredLocales", isNonEmptyStringArray, "an array of non-empty strings");
73
- return {
74
- ...(requiredLocales !== undefined && { requiredLocales }),
75
- ...(navAllowlist !== undefined && { navAllowlist }),
76
- ...(navAllowlistExempt !== undefined && { navAllowlistExempt }),
77
- ...(warnOnUniqueAccessRoles !== undefined && { warnOnUniqueAccessRoles }),
78
- };
79
- }
80
- function tableNameHint(entry) {
81
- if (isPlainObject(entry) && typeof entry["tableName"] === "string")
82
- return entry["tableName"];
83
- return "unknown";
84
- }
85
- // Every non-optional top-level property of FeatureDefinition, grouped by the runtime shape the
86
- // boot validators rely on; a hand-built entry missing one would crash far from the cause.
87
- const FEATURE_ARRAY_KEYS = [
88
- "requires",
89
- "optionalRequires",
90
- "configSeeds",
91
- "extensionUsages",
92
- "extensionSelectors",
93
- "bootChecks",
94
- "referenceData",
95
- "configReads",
96
- "authClaimsHooks",
97
- ];
98
- const FEATURE_SET_KEYS = [
99
- "requiredProjections",
100
- "requiredSteps",
101
- "exposedApis",
102
- "usedApis",
103
- ];
104
- const FEATURE_RECORD_KEYS = [
105
- "relations",
106
- "writeHandlers",
107
- "queryHandlers",
108
- "streamHandlers",
109
- "translations",
110
- "configKeys",
111
- "jobs",
112
- "registrarExtensions",
113
- "notifications",
114
- "events",
115
- "eventMigrations",
116
- "handlerEntityMappings",
117
- "metrics",
118
- "secretKeys",
119
- "secretNamespaces",
120
- "projections",
121
- "multiStreamProjections",
122
- "claimKeys",
123
- "screens",
124
- "navs",
125
- "workspaces",
126
- "httpRoutes",
127
- "webSocketRoutes",
128
- "storeTables",
129
- ];
130
- function isFeatureDefinitionShape(entry) {
131
- if (!isPlainObject(entry))
132
- return false;
133
- return (typeof entry["name"] === "string" &&
134
- typeof entry["systemScope"] === "boolean" &&
135
- FEATURE_ARRAY_KEYS.every((key) => Array.isArray(entry[key])) &&
136
- FEATURE_SET_KEYS.every((key) => entry[key] instanceof Set) &&
137
- FEATURE_RECORD_KEYS.every((key) => isPlainObject(entry[key])));
138
- }
139
- // FEATURES comes from the app's schema module; one malformed entry would make
140
- // validateBoot fail with an unrelated error far from the cause, so name the index here.
141
- function featureDefinitionsOf(value) {
142
- if (!Array.isArray(value))
143
- return undefined;
144
- const definitions = [];
145
- for (const [index, entry] of value.entries()) {
146
- if (!isFeatureDefinitionShape(entry)) {
147
- throw new Error(`FEATURES[${index}] is not a FeatureDefinition (expected an object with a string \`name\`)`);
148
- }
149
- definitions.push(entry);
150
- }
151
- return definitions;
152
- }
153
- // Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
154
- // both need ENTITY_METAS (and validate additionally wants FEATURES); this is
155
- // the one place that enforces "ENTITY_METAS must be an array" so the two
156
- // commands can't drift on the check or its wording. Throws — callers that
157
- // want a graceful exit code (validate) catch it themselves.
158
- async function loadSchemaModFromApp(schemaFile) {
159
- // bun imports TS directly — no spawn needed.
160
- const mod = await import(__rewriteRelativeImportExtension(schemaFile));
161
- const entityMetas = mod["ENTITY_METAS"];
162
- if (!Array.isArray(entityMetas)) {
163
- throw new Error(`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`);
164
- }
165
- const validMetas = [];
166
- for (const [index, entry] of entityMetas.entries()) {
167
- if (!isEntityTableMeta(entry)) {
168
- throw new Error(`Schema file ${schemaFile}: ENTITY_METAS[${index}] is not an EntityTableMeta (table: ${tableNameHint(entry)}).`);
169
- }
170
- validMetas.push(entry);
171
- }
172
- assertUniqueTableNames(validMetas);
173
- return {
174
- entityMetas: validMetas,
175
- features: mod["FEATURES"],
176
- validateBootOptions: mod["VALIDATE_BOOT_OPTIONS"],
177
- };
178
- }
179
- function nextSequenceNumber(migrationsDir) {
180
- if (!existsSync(migrationsDir))
181
- return 1;
182
- const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
183
- let max = 0;
184
- for (const f of files) {
185
- const m = f.match(/^(\d+)_/);
186
- if (m) {
187
- const n = Number(m[1]);
188
- if (n > max)
189
- max = n;
190
- }
191
- }
192
- return max + 1;
193
- }
194
- // A table that a migration only drops as a commented DESTRUCTIVE marker and
195
- // that the snapshot no longer has is gone on paper but still in the live DB.
196
- async function reportLeftoverDroppedTables(db, migrationsDir, out) {
197
- const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
198
- const snapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
199
- const leftovers = [];
200
- for (const tableName of findCommentedDropTables(readMigrationSqlTexts(migrationsDir), snapshot)) {
201
- if (await tableExists(db, tableName))
202
- leftovers.push(tableName);
203
- }
204
- // skip: nothing left over, nothing to report
205
- if (leftovers.length === 0)
206
- return;
207
- out.log(" Tables dropped only as a commented DESTRUCTIVE marker but still in the database:");
208
- for (const tableName of leftovers) {
209
- out.log(isRetiredFrameworkTable(tableName)
210
- ? ` ! ${tableName} — retired framework table: the next \`schema generate <name>\` creates the drop`
211
- : ` ! ${tableName} — app table: after a backup, write a new migration with the drop`);
212
- }
18
+ const SCHEMA_SUBCOMMANDS = ["generate", "validate", "apply", "baseline", "status"];
19
+ const SCHEMA_SUBCOMMAND_HANDLERS = {
20
+ generate: runGenerateCommand,
21
+ validate: runValidateCommand,
22
+ apply: runApplyCommand,
23
+ baseline: runBaselineCommand,
24
+ status: runStatusCommand,
25
+ };
26
+ const HELP_ARGS = ["--help", "-h", "help"];
27
+ function printSchemaCliUsage(out) {
28
+ out.log("");
29
+ out.log(" Subcommands:");
30
+ out.log(" generate <name> Write a new migration from the EntityTableMeta diff");
31
+ out.log(" validate Static CI gate (no DB): schema drift + validateBoot");
32
+ out.log(" apply Apply pending checked-in SQL files");
33
+ out.log(" baseline Mark checked-in migrations as applied (no SQL run)");
34
+ out.log(" status List applied vs pending");
213
35
  out.log("");
36
+ return 0;
37
+ }
38
+ function reportUnknownSubcommand(out, sub) {
39
+ out.err(` Unknown subcommand: "${sub}"`);
40
+ out.err(` Allowed: ${SCHEMA_SUBCOMMANDS.join(" | ")}`);
41
+ return 1;
214
42
  }
215
- const SCHEMA_SUBCOMMANDS = ["generate", "validate", "apply", "baseline", "status"];
216
43
  /**
217
44
  * Runs a schema-CLI subcommand. `appCwd` is the app workspace root (where
218
45
  * `kumiko/schema.ts` + `kumiko/migrations/` live). Returns a process exit code.
@@ -232,406 +59,22 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
232
59
  // migration, since the crashed process still records the migration as
233
60
  // applied and the rebuild is never retried.
234
61
  await ensureTemporalPolyfill();
235
- // `--help`/`-h`/`help` fall through to the same "no subcommand" usage
236
- // branch below (undefined) — a real request for help, not a typo that
237
- // should fail loud like an unrecognized subcommand does.
238
62
  const rawSub = argv[0];
239
- const sub = rawSub === "--help" || rawSub === "-h" || rawSub === "help" ? undefined : rawSub;
240
- const schemaFile = resolvePath(appCwd, "kumiko/schema.ts");
241
- const migrationsDir = resolvePath(appCwd, "kumiko/migrations");
242
- switch (sub) {
243
- case "generate": {
244
- const name = argv[1];
245
- if (name === "--help" || name === "-h") {
246
- out.log(" Usage: schema generate <name>");
247
- return 0;
248
- }
249
- if (!name) {
250
- out.err(" Usage: schema generate <name>");
251
- return 1;
252
- }
253
- // name lands unescaped in `${seq}_${name}.sql` (generateMigration) —
254
- // shared allowlist blocks flag-like names, path traversal, and
255
- // ENAMETOOLONG-length names.
256
- try {
257
- assertValidMigrationName(name);
258
- }
259
- catch (e) {
260
- const msg = e instanceof Error ? e.message : String(e);
261
- out.err(` ${msg}`);
262
- out.err(" Usage: schema generate <name>");
263
- return 1;
264
- }
265
- // Trailing args (e.g. a typo'd `--dry-run` — there is no such flag) were
266
- // silently ignored and the migration got written anyway. Reject them
267
- // before any IO instead of pretending they did something.
268
- if (argv.length > 2) {
269
- out.err(` Unrecognized argument(s): ${argv.slice(2).join(" ")}`);
270
- out.err(" Usage: schema generate <name>");
271
- return 1;
272
- }
273
- if (!existsSync(schemaFile)) {
274
- out.err(` ${schemaFile} fehlt.`);
275
- out.err(" App-Convention: kumiko/schema.ts mit");
276
- out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
277
- return 1;
278
- }
279
- let metas;
280
- try {
281
- ({ entityMetas: metas } = await loadSchemaModFromApp(schemaFile));
282
- }
283
- catch (e) {
284
- out.err(` ${e instanceof Error ? e.message : String(e)}`);
285
- return 1;
286
- }
287
- const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
288
- const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
289
- const result = generateMigration({
290
- metas,
291
- prevSnapshot,
292
- name,
293
- sequenceNumber: nextSequenceNumber(migrationsDir),
294
- priorMigrationsSql: readMigrationSqlTexts(migrationsDir),
295
- });
296
- const isEmpty = result.diff.newTables.length === 0 &&
297
- result.diff.changedTables.length === 0 &&
298
- result.diff.droppedTables.length === 0 &&
299
- result.retiredDrops.length === 0;
300
- if (isEmpty) {
301
- out.log(" No schema changes detected — kein neues Migration-File geschrieben.");
302
- return 0;
303
- }
304
- if (!existsSync(migrationsDir))
305
- mkdirSync(migrationsDir, { recursive: true });
306
- writeFileSync(join(migrationsDir, result.filename), result.sqlContent);
307
- writeSnapshotJson(snapshotPath, result.snapshot);
308
- // Rebuild markers are written only for incremental migrations — the initial
309
- // migration (prevSnapshot===null) only creates tables, so there are no historical
310
- // events to replay.
311
- const rebuildTables = prevSnapshot === null ? [] : rebuildTablesFromDiff(result.diff);
312
- writeRebuildMarker(migrationsDir, result.filename, rebuildTables);
313
- out.log("");
314
- out.log(` ✓ ${result.filename}`);
315
- out.log(` new tables: ${result.diff.newTables.length}, changed: ${result.diff.changedTables.length}, dropped: ${result.diff.droppedTables.length}, retired framework tables dropped: ${result.retiredDrops.length}`);
316
- if (rebuildTables.length > 0) {
317
- out.log(` rebuild-marker: ${result.filename.replace(/\.sql$/, ".rebuild.json")} (${rebuildTables.length} table(s))`);
318
- }
319
- out.log("");
320
- out.log(" Review + ggf. hand-edit + git add + commit. Apply via: schema apply");
321
- out.log("");
322
- return 0;
323
- }
324
- case "validate": {
325
- // Static, DB-free boot-blocking checks for CI — catches "this won't boot"
326
- // before deploy. Three layers, no database:
327
- // 1. schema drift: would `generate` write a migration? (= an entity was
328
- // added/changed but never generated → missing table → prod 500)
329
- // 2. boot validity: validateBoot over the composed FEATURES (QN/screen/
330
- // nav/role refs). Runs only if kumiko/schema.ts exports FEATURES;
331
- // honours its optional VALIDATE_BOOT_OPTIONS export.
332
- // 3. migration-content drift: replay every committed *.sql file and
333
- // diff the result against .snapshot.json — catches a migration
334
- // file whose SQL body doesn't match what its own snapshot entry
335
- // claims (e.g. an accidental copy-paste from an earlier file).
336
- // Layer 1 alone misses this: it only compares ENTITY_METAS to the
337
- // snapshot, never the snapshot to the SQL that's supposed to have
338
- // produced it.
339
- // The DB-level gate (assertKumikoSchemaCurrent) stays at boot/deploy.
340
- if (!existsSync(schemaFile)) {
341
- out.err(` ${schemaFile} fehlt.`);
342
- out.err(" App-Convention: kumiko/schema.ts mit");
343
- out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
344
- return 1;
345
- }
346
- let schemaMod;
347
- try {
348
- schemaMod = await loadSchemaModFromApp(schemaFile);
349
- }
350
- catch (e) {
351
- out.err(` ${e instanceof Error ? e.message : String(e)}`);
352
- return 1;
353
- }
354
- let ok = true;
355
- // 1. Schema drift — compute the diff, never write.
356
- const metas = schemaMod.entityMetas;
357
- const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
358
- const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
359
- const drift = generateMigration({
360
- metas,
361
- prevSnapshot,
362
- name: "validate",
363
- sequenceNumber: nextSequenceNumber(migrationsDir),
364
- priorMigrationsSql: readMigrationSqlTexts(migrationsDir),
365
- });
366
- const pendingTables = [
367
- ...drift.diff.newTables.map((t) => t.tableName),
368
- ...drift.diff.changedTables.map((t) => t.tableName),
369
- ...drift.diff.droppedTables,
370
- ...drift.retiredDrops.map((t) => t.tableName),
371
- ];
372
- if (pendingTables.length === 0) {
373
- out.log(" ✓ schema: migrations match the entity definitions");
374
- }
375
- else {
376
- ok = false;
377
- out.err(" ✗ schema drift: entity definitions are ahead of kumiko/migrations.");
378
- out.err(` pending — new: ${drift.diff.newTables.length}, changed: ${drift.diff.changedTables.length}, dropped: ${drift.diff.droppedTables.length}`);
379
- out.err(` tables: ${pendingTables.join(", ")}`);
380
- out.err(" Fix: `kumiko-schema generate <name>`, then commit the migration.");
381
- }
382
- // 2. Boot validity — needs the composed feature set.
383
- let bootFeatures;
384
- let bootFeaturesError;
385
- try {
386
- bootFeatures = featureDefinitionsOf(schemaMod.features);
387
- }
388
- catch (e) {
389
- bootFeaturesError = e instanceof Error ? e.message : String(e);
390
- }
391
- if (bootFeaturesError !== undefined) {
392
- ok = false;
393
- out.err(` ✗ boot: ${bootFeaturesError}`);
394
- }
395
- else if (bootFeatures) {
396
- try {
397
- validateBoot(bootFeatures, {
398
- ...parseValidateBootOptions(schemaMod.validateBootOptions),
399
- env: withFileStoragePresence(process.env),
400
- });
401
- out.log(" ✓ boot: feature configuration is valid");
402
- }
403
- catch (e) {
404
- ok = false;
405
- out.err(` ✗ boot: ${e instanceof Error ? e.message : String(e)}`);
406
- }
407
- }
408
- else {
409
- out.log(" · boot: skipped — add `export const FEATURES = composeFeatures(APP_FEATURES, { includeBundled: true })` to kumiko/schema.ts to enable validateBoot.");
410
- }
411
- // 3. Migration-content drift — replay the committed *.sql files and
412
- // diff the reconstructed schema against .snapshot.json.
413
- if (existsSync(migrationsDir) && prevSnapshot !== null) {
414
- try {
415
- const replayed = replayMigrationsDir(migrationsDir);
416
- const mismatches = diffReplayAgainstSnapshot(replayed, prevSnapshot);
417
- if (mismatches.length === 0) {
418
- out.log(" ✓ migrations: table/column names match .snapshot.json");
419
- }
420
- else {
421
- ok = false;
422
- out.err(" ✗ migration-content drift: committed *.sql files don't produce .snapshot.json.");
423
- for (const m of mismatches) {
424
- out.err(` ${m.tableName} (${m.kind}): ${m.detail}`);
425
- }
426
- if (mismatches.some((m) => m.kind === "unexpected-table")) {
427
- out.err(" Fix (unexpected-table): build a meta via `defineUnmanagedTable()` " +
428
- "from `@cosmicdrift/kumiko-framework/db`, then `r.storeTable(meta, { reason: ... })` " +
429
- "inside a feature — this adds it to ENTITY_METAS immediately; the .snapshot.json " +
430
- "only picks it up on the NEXT `kumiko-schema generate` run, which you still need " +
431
- "to run and commit. `table()` returns a query handle, not a storeTable()-compatible " +
432
- "meta — don't pass its result to storeTable(). See the bundled `jobs` feature's " +
433
- "job-run-log store table for the pattern.");
434
- }
435
- if (mismatches.some((m) => m.kind !== "unexpected-table")) {
436
- out.err(" Fix (missing-table/column-drift): a migration file's body doesn't match " +
437
- "what it (or the snapshot) claims — hand-fix the file, or ship a corrective " +
438
- "migration if it's already applied in prod.");
439
- }
440
- }
441
- }
442
- catch (e) {
443
- // replayMigrationsDir fail-loud's on a table-DDL statement it can't
444
- // parse — that must surface as a normal ✗ line like every other
445
- // check here, not a raw stack trace that kills the process before
446
- // any later check (or the Fix: help) ever runs.
447
- ok = false;
448
- out.err(` ✗ migration-content: ${e instanceof Error ? e.message : String(e)}`);
449
- out.err(" Fix: a statement in a migration file isn't recognizable to the replay parser — extend the parser's recognized patterns, or reword the migration to one it understands.");
450
- }
451
- }
452
- return ok ? 0 : 1;
453
- }
454
- case "apply": {
455
- const dbUrl = process.env["DATABASE_URL"];
456
- if (!dbUrl) {
457
- out.err(" DATABASE_URL not set.");
458
- return 1;
459
- }
460
- if (!existsSync(migrationsDir)) {
461
- out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
462
- return 1;
463
- }
464
- const { db, close } = createDbConnection({ url: dbUrl });
465
- let wiring;
466
- try {
467
- const result = await runMigrationsFromDir(db, migrationsDir);
468
- // Framework infrastructure tables (event store + pipeline state) are not captured by
469
- // `generate`, which handles only entity read tables. Existing databases have them
470
- // from the legacy-drizzle foundation; without them, a greenfield database (the
471
- // first app without a cutover) would lack kumiko_events and runProdApp would fail
472
- // during boot. All three are idempotent (tableExists-gated), so they are no-ops for
473
- // existing databases.
474
- await createEventsTable(db);
475
- await createEventConsumerStateTable(db);
476
- await createProjectionStateTable(db);
477
- out.log("");
478
- if (result.applied.length === 0) {
479
- out.log(` ✓ All ${result.skipped.length} migrations already applied.`);
480
- }
481
- else {
482
- out.log(` ✓ Applied ${result.applied.length}:`);
483
- for (const id of result.applied)
484
- out.log(` + ${id}`);
485
- if (result.skipped.length > 0)
486
- out.log(` (${result.skipped.length} already applied)`);
487
- }
488
- out.log("");
489
- // Projection-rebuild for tables a freshly applied migration changed
490
- // (marker NNNN_<name>.rebuild.json from `generate`). Needs the composed
491
- // feature set → only when the caller passed `features` (the app bin);
492
- // the dev CLI omits it and applies migrations only. Runs unconditionally
493
- // (not gated on result.applied.length > 0): a persistent queue
494
- // (kumiko_pending_rebuilds) survives a failed/crashed rebuild from an
495
- // earlier apply, so a later apply with zero new migrations still must
496
- // retry it — otherwise a failed rebuild is silently never retried,
497
- // since the migration itself is already tracked applied.
498
- if (options.features) {
499
- // A rebuild replays applyEntityEvent, which computes bidx columns from
500
- // configureBlindIndexKey/configurePiiSubjectKms — both boot-injected by
501
- // run{Prod,Dev}App but never by this standalone CLI (the migrate-db
502
- // initContainer). Without wiring them here, a rebuild would silently
503
- // null out bidx columns and, for ciphertext, throw only once it hits
504
- // the row.
505
- wiring = await resolveKmsWiringAsync(process.env, {
506
- logPrefix: "[kumiko schema apply]",
507
- log: out.log,
508
- slots: options.kmsSlots,
509
- });
510
- if ("kms" in wiring) {
511
- configurePiiSubjectKms(wiring.kms);
512
- configureBlindIndexKey(wiring.blindIndexKey);
513
- }
514
- const thisRunTables = await queueRebuildsFromMarkers(db, {
515
- migrationsDir,
516
- appliedIds: result.applied,
517
- });
518
- const registry = createRegistry(options.features);
519
- const rebuildRun = await runPendingRebuilds(db, registry, { thisRunTables });
520
- reportPendingRebuildRun(rebuildRun, out.log);
521
- }
522
- return 0;
523
- }
524
- catch (e) {
525
- out.err("");
526
- out.err(` ✗ ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`);
527
- out.err("");
528
- return 1;
529
- }
530
- finally {
531
- await wiring?.close();
532
- await close();
533
- }
534
- }
535
- case "baseline": {
536
- // Adopt an existing DB: mark all checked-in migrations as applied WITHOUT
537
- // running their SQL (prod tables already exist — cutover from the legacy
538
- // drizzle system). Afterwards the boot-gate is drift-free.
539
- const dbUrl = process.env["DATABASE_URL"];
540
- if (!dbUrl) {
541
- out.err(" DATABASE_URL not set.");
542
- return 1;
543
- }
544
- if (!existsSync(migrationsDir)) {
545
- out.err(` ${migrationsDir} fehlt — erst schema generate <name>.`);
546
- return 1;
547
- }
548
- const { db, close } = createDbConnection({ url: dbUrl });
549
- try {
550
- const result = await baselineMigrations(db, loadMigrationsFromDir(migrationsDir));
551
- out.log("");
552
- out.log(` ✓ Marked ${result.marked.length} migration(s) as applied (no SQL run):`);
553
- for (const id of result.marked)
554
- out.log(` + ${id}`);
555
- if (result.alreadyTracked.length > 0) {
556
- out.log(` (${result.alreadyTracked.length} already tracked)`);
557
- }
558
- out.log("");
559
- return 0;
560
- }
561
- catch (e) {
562
- out.err(` ✗ ${e instanceof Error ? e.message : String(e)}`);
563
- return 1;
564
- }
565
- finally {
566
- await close();
567
- }
568
- }
569
- case "status": {
570
- const dbUrl = process.env["DATABASE_URL"];
571
- if (!dbUrl) {
572
- out.err(" DATABASE_URL not set.");
573
- return 1;
574
- }
575
- if (!existsSync(migrationsDir)) {
576
- out.log(" Kein kumiko/migrations/ — App ist noch auf dem alten drizzle-Pfad.");
577
- return 0;
578
- }
579
- const local = loadMigrationsFromDir(migrationsDir);
580
- const { db, close } = createDbConnection({ url: dbUrl });
581
- try {
582
- // On a fresh database where `kumiko schema apply` has never run, the tracking table is
583
- // absent, which means "nothing applied". Connection and permission errors must not
584
- // be swallowed (false pending results would hide the problem) — tableExists checks
585
- // only for existence, while all other errors propagate.
586
- const trackingExists = await tableExists(db, "_kumiko_migrations");
587
- const applied = trackingExists
588
- ? new Set((await fetchAppliedMigrations(db)).map((a) => a.id))
589
- : new Set();
590
- out.log("");
591
- out.log(` ${local.length} migrations in ${migrationsDir}:`);
592
- for (const m of local)
593
- out.log(` ${applied.has(m.id) ? "✓" : " "} ${m.id}`);
594
- const pending = local.filter((m) => !applied.has(m.id)).length;
595
- out.log("");
596
- out.log(` ${applied.size} applied, ${pending} pending.`);
597
- out.log("");
598
- await reportLeftoverDroppedTables(db, migrationsDir, out);
599
- return pending === 0 ? 0 : 1;
600
- }
601
- catch (e) {
602
- // Message only, never dbUrl: it can carry the password.
603
- out.err(` ✗ schema status failed: ${e instanceof Error ? e.message : String(e)}`);
604
- out.err(' Check that DATABASE_URL is reachable and the database exists (the scaffold\'s docker-compose creates "app").');
605
- return 1;
606
- }
607
- finally {
608
- try {
609
- await close();
610
- }
611
- catch {
612
- // The connection already failed and was reported above; a failing close must not throw again.
613
- }
614
- }
615
- }
616
- default: {
617
- // No subcommand (`kumiko schema`) is a help request → usage + exit 0.
618
- // An unrecognized one (typo, e.g. `schema cutover`) must fail loud —
619
- // silently exiting 0 here let a deploy's migrate step print help and
620
- // move on without ever running a migration.
621
- if (sub !== undefined) {
622
- out.err(` Unbekanntes Subcommand: "${sub}"`);
623
- out.err(` Erlaubt: ${SCHEMA_SUBCOMMANDS.join(" | ")}`);
624
- return 1;
625
- }
626
- out.log("");
627
- out.log(" Subcommands:");
628
- out.log(" generate <name> Schreibe neue Migration aus EntityTableMeta-Diff");
629
- out.log(" validate Static CI-Gate (kein DB): schema-drift + validateBoot");
630
- out.log(" apply Applied pending checked-in SQL-Files");
631
- out.log(" baseline Markiere checked-in Migrations als applied (kein SQL-Run)");
632
- out.log(" status Liste applied vs pending");
633
- out.log("");
634
- return 0;
635
- }
636
- }
63
+ // No subcommand (`kumiko schema`) is a help request → usage + exit 0.
64
+ // `--help`/`-h`/`help` are the same — a real request for help, not a typo
65
+ // that should fail loud like an unrecognized subcommand does.
66
+ if (rawSub === undefined || isOneOf(HELP_ARGS, rawSub))
67
+ return printSchemaCliUsage(out);
68
+ // An unrecognized subcommand (typo, e.g. `schema cutover`) must fail loud —
69
+ // silently exiting 0 here let a deploy's migrate step print help and
70
+ // move on without ever running a migration.
71
+ if (!isOneOf(SCHEMA_SUBCOMMANDS, rawSub))
72
+ return reportUnknownSubcommand(out, rawSub);
73
+ return SCHEMA_SUBCOMMAND_HANDLERS[rawSub]({
74
+ argv,
75
+ out,
76
+ options,
77
+ schemaFile: resolvePath(appCwd, "kumiko/schema.ts"),
78
+ migrationsDir: resolvePath(appCwd, "kumiko/migrations"),
79
+ });
637
80
  }