@tailor-platform/sdk 2.0.1 → 2.2.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 (92) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/{dist/cli/index.mjs → bin/tailor.mjs} +1 -1
  3. package/dist/{application-CM6hKnSK.mjs → application-BpLeawBg.mjs} +126 -28
  4. package/dist/application-BpLeawBg.mjs.map +1 -0
  5. package/dist/application-BxLLiLsr.mjs +3 -0
  6. package/dist/cli/commands/deploy/tailordb/index.d.mts +2 -1
  7. package/dist/cli/commands/deploy/types.d.mts +2 -1
  8. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +11 -4
  9. package/dist/cli/commands/tailordb/migrate/file-state.d.mts +1 -0
  10. package/dist/cli/commands/tailordb/migrate/schema-checks.d.mts +2 -2
  11. package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +8 -1
  12. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +5 -3
  13. package/dist/cli/lib.d.mts +4 -4
  14. package/dist/cli/lib.mjs +3 -482
  15. package/dist/cli/lib.mjs.map +1 -1
  16. package/dist/cli/main.mjs +997 -202
  17. package/dist/cli/main.mjs.map +1 -1
  18. package/dist/completion/zsh-worker.zsh +145 -2
  19. package/dist/configure/config/types.d.mts +12 -1
  20. package/dist/configure/index.d.mts +3 -3
  21. package/dist/configure/services/index.d.mts +3 -2
  22. package/dist/configure/services/resolver/index.d.mts +1 -1
  23. package/dist/configure/services/resolver/resolver.d.mts +1 -1
  24. package/dist/configure/services/tailordb/index.d.mts +2 -1
  25. package/dist/configure/services/tailordb/schema.d.mts +46 -46
  26. package/dist/configure/services/tailordb/types.d.mts +30 -4
  27. package/dist/{crashreport-B5LqCTfF.mjs → crashreport-CIIfLnoO.mjs} +2 -2
  28. package/dist/{crashreport-B5LqCTfF.mjs.map → crashreport-CIIfLnoO.mjs.map} +1 -1
  29. package/dist/{crashreport-eI7ty7ok.mjs → crashreport-CINZ5dVN.mjs} +1 -1
  30. package/dist/errors-CWj21238.mjs +75 -0
  31. package/dist/errors-CWj21238.mjs.map +1 -0
  32. package/dist/{globals-TfAVItuK.mjs → globals-B2nlxBrz.mjs} +2 -10
  33. package/dist/globals-B2nlxBrz.mjs.map +1 -0
  34. package/dist/kysely/index.d.mts +52 -5
  35. package/dist/kysely/index.mjs +7 -2
  36. package/dist/kysely/index.mjs.map +1 -1
  37. package/dist/{logger-CXQq9YIp.mjs → logger-DSdExfor.mjs} +83 -42
  38. package/dist/logger-DSdExfor.mjs.map +1 -0
  39. package/dist/plugin/builtin/seed/index.mjs +1 -1
  40. package/dist/{register-ts-hook-LYV7zH-e.mjs → register-ts-hook-CTth1eqj.mjs} +1587 -272
  41. package/dist/register-ts-hook-CTth1eqj.mjs.map +1 -0
  42. package/dist/runtime/globals.d.mts +0 -7
  43. package/dist/runtime/workflow.d.mts +2 -7
  44. package/dist/schema--xYWRGfe.mjs.map +1 -1
  45. package/dist/seed/index.d.mts +52 -3
  46. package/dist/seed/index.mjs +200 -44
  47. package/dist/seed/index.mjs.map +1 -1
  48. package/dist/{seed-B-yckgQY.mjs → seed-xqM9WIHP.mjs} +8 -8
  49. package/dist/seed-xqM9WIHP.mjs.map +1 -0
  50. package/dist/{service-B5WynBHQ.mjs → service-CCwl3Avt.mjs} +3 -3
  51. package/dist/{service-B5WynBHQ.mjs.map → service-CCwl3Avt.mjs.map} +1 -1
  52. package/dist/service-_XmjSEGr.mjs +3 -0
  53. package/dist/{service-k1xy_A5G.mjs → service-goqlJJgA.mjs} +2 -2
  54. package/dist/{service-k1xy_A5G.mjs.map → service-goqlJJgA.mjs.map} +1 -1
  55. package/dist/{service_pb-D5--My_M.mjs → service_pb-BEWj1pOV.mjs} +2 -2
  56. package/dist/{service_pb-D5--My_M.mjs.map → service_pb-BEWj1pOV.mjs.map} +1 -1
  57. package/dist/{service_pb-i8CmnQP4.mjs → service_pb-BjlhvK9E.mjs} +1 -1
  58. package/dist/utils/test/index.d.mts +9 -3
  59. package/dist/utils/test/index.mjs +49 -25
  60. package/dist/utils/test/index.mjs.map +1 -1
  61. package/dist/vitest/environment.mjs +1 -1
  62. package/dist/vitest/index.d.mts +2 -1
  63. package/dist/vitest/index.mjs +84 -2
  64. package/dist/vitest/index.mjs.map +1 -1
  65. package/dist/vitest/mocks/file.d.mts +1 -1
  66. package/dist/vitest/pglite-kysely.d.mts +43 -0
  67. package/dist/vitest/setup.mjs +1 -1
  68. package/dist/workflow-Bamae_Yc.mjs.map +1 -1
  69. package/dist/{workspace_resource_pb-DXnO2AT-.mjs → workspace_resource_pb-HNIL_Vfj.mjs} +10 -2
  70. package/dist/{workspace_resource_pb-DXnO2AT-.mjs.map → workspace_resource_pb-HNIL_Vfj.mjs.map} +1 -1
  71. package/docs/cli/tailordb.md +87 -22
  72. package/docs/cli-reference.md +13 -11
  73. package/docs/configuration.md +4 -1
  74. package/docs/migration/v2.md +1 -1
  75. package/docs/plugin/custom.md +48 -48
  76. package/docs/plugin/index.md +24 -24
  77. package/docs/services/auth.md +10 -10
  78. package/docs/services/executor.md +1 -1
  79. package/docs/services/resolver.md +34 -2
  80. package/docs/services/tailordb-migration.md +375 -76
  81. package/docs/services/tailordb.md +28 -28
  82. package/docs/testing.md +3 -1
  83. package/package.json +13 -8
  84. package/dist/application-CM6hKnSK.mjs.map +0 -1
  85. package/dist/application-CXNaUNhv.mjs +0 -3
  86. package/dist/errors-D7583Zz7.mjs +0 -58
  87. package/dist/errors-D7583Zz7.mjs.map +0 -1
  88. package/dist/globals-TfAVItuK.mjs.map +0 -1
  89. package/dist/logger-CXQq9YIp.mjs.map +0 -1
  90. package/dist/register-ts-hook-LYV7zH-e.mjs.map +0 -1
  91. package/dist/seed-B-yckgQY.mjs.map +0 -1
  92. package/dist/service-M7OYziQP.mjs +0 -3
package/dist/cli/main.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { r as parseInputFields } from "../schema--xYWRGfe.mjs";
3
- import { At as PATScope, Ct as AuthInvokerSchema, E as CustomDomainStatus, W as FunctionExecution_Type } from "../workspace_resource_pb-DXnO2AT-.mjs";
3
+ import { At as PATScope, Ct as AuthInvokerSchema, E as CustomDomainStatus, Ft as CloneOperationStatus, W as FunctionExecution_Type } from "../workspace_resource_pb-HNIL_Vfj.mjs";
4
4
  import { t as assertDefined } from "../assert-DBxo8jPo.mjs";
5
- import { n as logger, r as styles } from "../logger-CXQq9YIp.mjs";
6
- import { $n as hasUserTokenEntry, $t as ensureConfigId, An as loadDiff, Bn as sdkAppIdLabelKey, C as healthCommand, Cn as createSnapshotFromLocalTypes, Cr as resolveMachineUserInputSource, Dn as getMigrationFilePath, E as resumeCommand, Et as triggerCommand, F as writeDbTypesFile, Fn as formatMigrationDiff, Ft as startCommand, G as removeCommand$1, H as extractOwnedNamespaces, Hn as sdkVersionLabelKey, Ht as functionExecutionStatusToString, I as getConfiguredEditorCommand, In as hasChanges, Jn as assertWritable, K as updateCommand$3, Kn as prompt, Kt as deployFromCLI, L as openInConfiguredEditor, Ln as getNamespacesWithMigrations, Lt as getCommand$6, Mn as formatMigrationNumber, Mt as jobsCommand, N as generateCommand$1, Nn as parseMigrationNumberArg, O as listCommand$14, On as getMigrationFiles, Ot as listCommand$8, P as generateMigrationScript, Pn as formatDiffSummary, Q as getCommand$5, Qn as hasAnyUserTokenEntry, Qt as workspaceNameSchema, R as deployCommand$1, Rn as selectTargetNamespace, Sr as paginationArgs, St as getCommand$2, Tr as workspaceArgs, Tt as webhookCommand, U as logBetaWarning, Un as writeMetadataLabels, Ut as getCommand$1, V as showCommand, Vn as sdkNameLabelKey, Wn as PluginManager, X as listCommand$13, Xn as deleteUserTokens, Y as treeCommand, Yn as loadConfig, Yt as createCommand$4, Zn as fetchLatestToken, Zt as formatKeyValueTable, _r as confirmationArgs, a as queryCommand, an as toTailorDBDeployInput, ar as loadStoredUserTokens, bn as assertValidMigrationFiles, br as multiConfigArg, bt as listCommand$9, c as removeCommand, cn as handleOptionalToRequiredError, cr as readPlatformConfig, ct as createCommand$3, dn as compareSnapshotWithRemote, dr as resolveUserTokenKey, en as executeScript, er as loadAccessToken, et as updateCommand$2, f as inviteCommand, fn as generateAllTypeManifestsFromSnapshot, fr as saveUserTokens, ft as getCommand$3, g as getCommand$7, gn as INITIAL_SCHEMA_NUMBER, gr as configArg, gt as listCommand$10, hr as commonArgs, ht as tokenCommand$1, in as logRemoteDriftGuidance, ir as loadPlatformClientConfig, it as getCommand$4, j as truncateCommand, jn as reconstructSnapshotFromMigrations, ln as parseMigrationLabelNumber, lr as removeLegacyUserAlias, m as restoreCommand, mr as defineAppCommand, nr as loadConsoleBaseUrl, nt as listCommand$12, o as updateCommand$4, on as verifyRemoteSchema, or as loadWorkspaceId, ot as deleteCommand$4, pn as protoGqlPermission, pr as writePlatformConfig, qn as apiCall, qt as listCommand$17, r as serializeError, rn as checkMigrationDiffs, rr as loadMachineUserName, sn as MIGRATION_LABEL_KEY, sr as platformConfigFromProfile, t as registerTsHook, tr as loadConfigPath, u as listCommand$16, un as sanitizeMigrationLabel, ur as resolveConfigUser, ut as listCommand$11, v as deleteCommand$5, vr as deploymentArgs, vt as generate, w as waitCommand, wn as formatSchemaDrifts, wr as toPageDirection, x as listCommand$15, xn as compareLocalTypesWithSnapshot, xr as pagedLogArgs, yn as assertMigrationNumberExists, yr as isVerbose, zn as resourceTrn, zt as executionsCommand } from "../register-ts-hook-LYV7zH-e.mjs";
7
- import { C as createLogLevelTreeshakeOptions, F as fetchAllTolerant, G as initOperatorClient, H as getOrNull, K as isDefaultPlatform, L as fetchPaged, M as closeConnectionPool, N as defaultPlatformBaseUrl, P as fetchAll, R as fetchPlatformMachineUserToken, S as composeFunctionTreeshakeOptions, T as createBundleLog, U as getPlatformBaseUrl, V as getOAuth2ClientId, W as initOAuth2Client, a as resolveInlineSourcemap, b as resolveTSConfigWithFallback, c as WorkflowJobSchema, d as INVOKER_EXPR, h as assertUniqueLocalTailorDBTypeNames, k as getDistDir, l as ResolverSchema, m as buildResolverPermissionAndInputCheckExpr, v as createGeneratedEntryResolverPlugin, w as resolveBundleLogLevel, x as platformBundleDefinePlugin, y as createTsconfigPathsPlugin, z as fetchUserInfo } from "../application-CM6hKnSK.mjs";
8
- import { i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-k1xy_A5G.mjs";
5
+ import { n as logger, r as styles } from "../logger-DSdExfor.mjs";
6
+ import { $ as treeCommand, $n as formatDiffSummary, An as MIGRATION_NUMBER_PATTERN, Ar as removeLegacyUserAlias, B as MIGRATION_REVIEW_REQUIRED_MARKER, Br as isVerbose, Bt as getCommand$6, C as healthCommand, Cn as captureFileState, Cr as loadConsoleBaseUrl, Ct as listCommand$9, Dr as loadWorkspaceId, E as resumeCommand, En as DIFF_FILE_NAME, Er as loadStoredUserTokens, F as chunkSeedData, Fn as compareLocalTypesWithSnapshot, Fr as defineAppCommand, Ft as jobsCommand, G as showCommand, Gn as loadSnapshot, Gr as toPageDirection, Gt as functionExecutionStatusToString, H as deployCommand$1, Hn as getMigrationFiles, Hr as pagedLogArgs, Ht as executionsCommand, I as bundleSeedScript, In as compareSnapshots, Ir as commonArgs, Jn as MIGRATION_HISTORY_LABEL_KEY, K as extractOwnedNamespaces, Kn as normalizeSchemaSnapshot, Kr as workspaceArgs, Kt as getCommand$1, Ln as createSnapshotFromLocalTypes, Lr as configArg, M as loadSeedContext, Mn as UnsupportedMigrationFileVersionError, Mr as resolveUserTokenKey, N as loadApplicationNamespaces, Nn as assertMigrationNumberExists, Nr as saveUserTokens, O as listCommand$14, On as MIGRATE_FILE_NAME, Or as platformConfigFromProfile, Ot as webhookCommand, Pn as assertValidMigrationFiles, Pr as writePlatformConfig, Qn as sanitizeMigrationLabel, Qt as listCommand$17, R as generateCommand$1, Rn as formatSchemaDrifts, Rr as confirmationArgs, Rt as startCommand, Sn as protoGqlPermission, Sr as loadConfigPath, St as generate, Tn as DB_TYPES_FILE_NAME, Tr as loadPlatformClientConfig, Tt as getCommand$2, Ur as paginationArgs, V as generateSchemaFile, Vn as getMigrationFilePath, Vr as multiConfigArg, Wn as loadDiff, Wr as resolveMachineUserInputSource, X as updateCommand$3, Xn as createMigrationHistoryId, Xt as deployMigrationTestBaseline, Y as removeCommand$1, Yn as MIGRATION_LABEL_KEY, Yt as deployFromCLI, Zn as handleOptionalToRequiredError, Zt as deployMigrationTestTarget, _n as verifyRemoteSchema, _r as deleteUserTokens, a as queryCommand, an as ensureConfigId, ar as sdkAppIdLabelKey, bn as compareSnapshotWithRemote, br as hasUserTokenEntry, bt as listCommand$10, c as removeCommand, cn as executeScript, cr as writeMetadataLabels, ct as getCommand$4, dn as bundleMigrationScript, dr as formatMigrationNumber, en as createCommand$4, er as formatMigrationDiff, et as listCommand$13, f as inviteCommand, fn as checkMigrationDiffs, fr as parseMigrationNumberArg, ft as createCommand$3, g as getCommand$7, gn as toTailorDBDeployInput, gr as loadConfig, gt as getCommand$3, hn as logRemoteDriftGuidance, hr as assertWritable, in as workspaceNameSchema, ir as resourceTrn, it as updateCommand$2, j as truncateCommand, jn as SCHEMA_FILE_NAME, jr as resolveConfigUser, jt as listCommand$8, kr as readPlatformConfig, kt as triggerCommand, lr as PluginManager, m as restoreCommand, mn as formatRemoteVerificationResults, mr as apiCall, mt as listCommand$11, nr as getNamespacesWithMigrations, nt as getCommand$5, o as updateCommand$4, on as getMigrationMachineUser, or as sdkNameLabelKey, ot as listCommand$12, pn as fetchRemoteSchemaSnapshot, pr as prompt, q as logBetaWarning, qn as reconstructSnapshotFromMigrations, r as serializeError, rn as formatKeyValueTable, rr as selectTargetNamespace, sn as updateMigrationLabel, sr as sdkVersionLabelKey, t as registerTsHook, tn as createValidatedWorkspaceWithClient, tr as hasChanges, u as listCommand$16, un as formatMigrationScriptCommand, ut as deleteCommand$4, v as deleteCommand$5, vn as fetchRemoteMigrationNumber, vr as fetchLatestToken, w as waitCommand, wn as captureMigrationFileState, wr as loadMachineUserName, x as listCommand$15, xn as generateAllTypeManifestsFromSnapshot, xr as loadAccessToken, yn as fetchRemoteMigrationState, yr as hasAnyUserTokenEntry, yt as tokenCommand$1, z as scriptCommand, zn as getLatestMigrationNumber, zr as deploymentArgs } from "../register-ts-hook-CTth1eqj.mjs";
7
+ import { A as getDistDir, B as fetchUserInfo, C as composeFunctionTreeshakeOptions, E as createBundleLog, F as fetchAll, G as initOAuth2Client, H as getOAuth2ClientId, I as fetchAllTolerant, K as initOperatorClient, N as closeConnectionPool, P as defaultPlatformBaseUrl, R as fetchPaged, S as platformBundleDefinePlugin, T as resolveBundleLogLevel, U as getOrNull, W as getPlatformBaseUrl, a as resolveInlineSourcemap, b as createTsconfigPathsPlugin, c as WorkflowJobSchema, f as INVOKER_EXPR, g as assertUniqueLocalTailorDBTypeNames, h as buildResolverPermissionAndInputCheckExpr, l as resolveResolverDefaultPermissionForFile, q as isDefaultPlatform, u as ResolverSchema, w as createLogLevelTreeshakeOptions, x as resolveTSConfigWithFallback, y as createGeneratedEntryResolverPlugin, z as fetchPlatformMachineUserToken } from "../application-BpLeawBg.mjs";
8
+ import { h as loadFilesWithIgnores, i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-goqlJJgA.mjs";
9
9
  import { t as multiline } from "../multiline-sfHpTZZK.mjs";
10
10
  import { t as readPackageJson } from "../package-json-8b0O9TlX.mjs";
11
11
  import { t as userAgent } from "../user-agent-Bgsszb5I.mjs";
12
- import { n as isCLIError, t as createCLIError } from "../errors-D7583Zz7.mjs";
13
- import { t as OperatorService } from "../service_pb-D5--My_M.mjs";
14
- import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-B5LqCTfF.mjs";
12
+ import { i as isCLIError, r as formatNextAction, t as createCLIError } from "../errors-CWj21238.mjs";
13
+ import { t as OperatorService } from "../service_pb-BEWj1pOV.mjs";
14
+ import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-CIIfLnoO.mjs";
15
15
  import * as path from "pathe";
16
16
  import { dirname, resolve } from "pathe";
17
17
  import { resolvePackageJSON } from "pkg-types";
@@ -27,7 +27,7 @@ import { pathToFileURL } from "node:url";
27
27
  import { generateCodeVerifier } from "@badgateway/oauth2-client";
28
28
  import { Code, ConnectError } from "@connectrpc/connect";
29
29
  import * as crypto from "node:crypto";
30
- import { createHash } from "node:crypto";
30
+ import { createHash, randomBytes } from "node:crypto";
31
31
  import * as http from "node:http";
32
32
  import open from "open";
33
33
  import * as rolldown from "rolldown";
@@ -1498,7 +1498,7 @@ function composeExecutionErrorString(error) {
1498
1498
  * @returns Formatted fallback string for display
1499
1499
  */
1500
1500
  function formatExecutionErrorFallback(error) {
1501
- const [headerLine, ...frameLines] = composeExecutionErrorString(error).split("\n");
1501
+ const [headerLine = "", ...frameLines] = composeExecutionErrorString(error).split("\n");
1502
1502
  return [` ${styles.error(headerLine)}`, ...frameLines.map((line) => ` ${styles.dim(line)}`)].join("\n");
1503
1503
  }
1504
1504
  /**
@@ -1694,7 +1694,14 @@ async function bundleForTestRun(options) {
1694
1694
  const baseName = `test-run--${detected.name}`;
1695
1695
  const scriptName = `${baseName}.js`;
1696
1696
  const entryPath = path.join(outputDir, `${baseName}.entry.js`);
1697
- const entryContent = generateEntry(detected, sourceFile, env, machineUser, workspaceId);
1697
+ const entryContent = generateEntry({
1698
+ detected,
1699
+ sourceFile,
1700
+ env,
1701
+ machineUser,
1702
+ workspaceId,
1703
+ defaultPermission: options.defaultPermission
1704
+ });
1698
1705
  fs.writeFileSync(entryPath, entryContent);
1699
1706
  const tsconfig = await resolveTSConfigWithFallback(baseDir);
1700
1707
  const bundleLog = createBundleLog({ tsconfig });
@@ -1725,14 +1732,11 @@ async function bundleForTestRun(options) {
1725
1732
  }
1726
1733
  /**
1727
1734
  * Generate entry file content based on the detected function type.
1728
- * @param detected - Detected function info
1729
- * @param sourceFile - Absolute path to the source file
1730
- * @param env - Environment variables for workflow job bundles
1731
- * @param machineUser - Resolved machine user info
1732
- * @param workspaceId - Workspace ID
1735
+ * @param options - Detected function info and the context embedded into the entry
1733
1736
  * @returns Entry file content string
1734
1737
  */
1735
- function generateEntry(detected, sourceFile, env, machineUser, workspaceId) {
1738
+ function generateEntry(options) {
1739
+ const { detected, sourceFile, env, machineUser, workspaceId, defaultPermission } = options;
1736
1740
  const absoluteSourcePath = path.resolve(sourceFile);
1737
1741
  switch (detected.type) {
1738
1742
  case "plain":
@@ -1745,7 +1749,10 @@ function generateEntry(detected, sourceFile, env, machineUser, workspaceId) {
1745
1749
  `;
1746
1750
  case "resolver": {
1747
1751
  const principalExpr = buildMachinePrincipalExpr(machineUser, workspaceId);
1748
- const guardAndInputCheckExpr = buildResolverPermissionAndInputCheckExpr(detected.permission);
1752
+ const guardAndInputCheckExpr = buildResolverPermissionAndInputCheckExpr({
1753
+ permission: detected.permission,
1754
+ defaultPermission
1755
+ });
1749
1756
  return multiline`
1750
1757
  import _internalResolver from "${absoluteSourcePath}";
1751
1758
  import { t } from "@tailor-platform/sdk";
@@ -2027,10 +2034,16 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
2027
2034
  args.arg = void 0;
2028
2035
  }
2029
2036
  logger.info("Bundling...");
2037
+ const baseDir = path.dirname(config.path);
2030
2038
  ({bundledCode, scriptName} = await bundleForTestRun({
2031
2039
  detected,
2032
2040
  sourceFile: filePath,
2033
- baseDir: path.dirname(config.path),
2041
+ baseDir,
2042
+ defaultPermission: detected.type === "resolver" ? resolveResolverDefaultPermissionForFile({
2043
+ config: config.resolver,
2044
+ filePath,
2045
+ baseDir
2046
+ }) : void 0,
2034
2047
  env: config.env ?? {},
2035
2048
  inlineSourcemap: config.inlineSourcemap,
2036
2049
  logLevel: config.logLevel,
@@ -5412,164 +5425,219 @@ const staticwebsiteCommand = defineCommand({
5412
5425
  });
5413
5426
 
5414
5427
  //#endregion
5415
- //#region src/cli/commands/tailordb/migrate/script.ts
5416
- /**
5417
- * Script command for TailorDB migrations
5418
- *
5419
- * Adds a `migrate.ts` (and supporting `db.ts`) template to an existing
5420
- * migration directory. Useful for warning-tier changes where users may
5421
- * want to write a custom data migration even though the change does not
5422
- * automatically require one.
5423
- */
5424
- /**
5425
- * Record in diff.json that a migration requiring a script intentionally has none.
5426
- * @param {MarkScriptSkippedOptions} options - Target migration and skip reason
5427
- * @returns {ScriptSkippedInfo} The recorded acknowledgment
5428
- */
5429
- function markMigrationScriptSkipped(options) {
5430
- const { migrationsDir, migrationNumber, reason } = options;
5431
- const label = formatMigrationNumber(migrationNumber);
5432
- const normalizedReason = reason.trim();
5433
- if (!normalizedReason) throw new Error("Migration script skip reason must not be empty.");
5434
- const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
5435
- if (!fs.existsSync(diffPath)) throw new Error(`Migration ${label} not found in ${migrationsDir}. Expected ${diffPath}.`);
5436
- const diff = loadDiff(diffPath);
5437
- if (!diff.requiresMigrationScript) throw new Error(`Migration ${label} does not require a migration script; nothing to skip.`);
5438
- if (diff.scriptSkipped) throw new Error(`Migration ${label} already has a script skip recorded (${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason}).`);
5439
- const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
5440
- if (fs.existsSync(migratePath)) throw new Error(`Migration script exists at ${migratePath}. Delete migrate.ts first if this migration should run without a script.`);
5441
- const scriptSkipped = {
5442
- reason: normalizedReason,
5443
- acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString()
5444
- };
5445
- const raw = JSON.parse(fs.readFileSync(diffPath, "utf-8"));
5446
- raw.scriptSkipped = scriptSkipped;
5447
- fs.writeFileSync(diffPath, JSON.stringify(raw, null, 2));
5448
- return scriptSkipped;
5449
- }
5450
- /**
5451
- * Remove a script skip acknowledgment after a real migration script is created.
5452
- * @param diffPath - Migration diff file to update
5453
- */
5454
- function clearMigrationScriptSkipped(diffPath) {
5455
- const raw = JSON.parse(fs.readFileSync(diffPath, "utf-8"));
5456
- if (!Object.hasOwn(raw, "scriptSkipped")) return;
5457
- delete raw.scriptSkipped;
5458
- fs.writeFileSync(diffPath, JSON.stringify(raw, null, 2));
5428
+ //#region src/cli/commands/tailordb/migrate/rebaseline.ts
5429
+ const MIGRATION_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
5430
+ SCHEMA_FILE_NAME,
5431
+ DIFF_FILE_NAME,
5432
+ MIGRATE_FILE_NAME,
5433
+ DB_TYPES_FILE_NAME
5434
+ ]);
5435
+ async function activateBaseline(migrationsDir, snapshot, namespace, rebaseline) {
5436
+ const resolvedMigrationsDir = await fsPromises.realpath(migrationsDir);
5437
+ const parentDir = path.dirname(resolvedMigrationsDir);
5438
+ const baseName = path.basename(resolvedMigrationsDir);
5439
+ const stagingDir = await fsPromises.mkdtemp(path.join(parentDir, `.${baseName}-rebaseline-`));
5440
+ const backupDir = `${stagingDir}-previous`;
5441
+ let movedLiveDirectory = false;
5442
+ let activatedStagingDirectory = false;
5443
+ try {
5444
+ await fsPromises.cp(resolvedMigrationsDir, stagingDir, { recursive: true });
5445
+ for (const entry of await fsPromises.readdir(stagingDir, { withFileTypes: true })) {
5446
+ if (!entry.isDirectory() || !MIGRATION_NUMBER_PATTERN.test(entry.name)) continue;
5447
+ const entryPath = path.join(stagingDir, entry.name);
5448
+ if (!(await fsPromises.readdir(entryPath)).some((childName) => MIGRATION_ARTIFACT_NAMES.has(childName))) continue;
5449
+ await fsPromises.rm(entryPath, {
5450
+ recursive: true,
5451
+ force: true
5452
+ });
5453
+ }
5454
+ loadSnapshot((await generateSchemaFile({
5455
+ ...snapshot,
5456
+ namespace,
5457
+ version: 2,
5458
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5459
+ rebaseline
5460
+ }, stagingDir, 0)).filePath);
5461
+ await fsPromises.rename(resolvedMigrationsDir, backupDir);
5462
+ movedLiveDirectory = true;
5463
+ try {
5464
+ await fsPromises.rename(stagingDir, resolvedMigrationsDir);
5465
+ activatedStagingDirectory = true;
5466
+ } catch (error) {
5467
+ await fsPromises.rename(backupDir, resolvedMigrationsDir);
5468
+ movedLiveDirectory = false;
5469
+ throw error;
5470
+ }
5471
+ try {
5472
+ await fsPromises.rm(backupDir, {
5473
+ recursive: true,
5474
+ force: true
5475
+ });
5476
+ } catch (error) {
5477
+ logger.warn(`The previous migration directory remains at ${backupDir}: ${error instanceof Error ? error.message : String(error)}`);
5478
+ }
5479
+ movedLiveDirectory = false;
5480
+ } finally {
5481
+ if (fs.existsSync(stagingDir)) await fsPromises.rm(stagingDir, {
5482
+ recursive: true,
5483
+ force: true
5484
+ });
5485
+ if (!activatedStagingDirectory && movedLiveDirectory && fs.existsSync(backupDir)) await fsPromises.rename(backupDir, resolvedMigrationsDir);
5486
+ }
5459
5487
  }
5460
- /**
5461
- * Add a migrate.ts template to an existing migration directory.
5462
- * @param {ScriptOptions} options - Command options
5463
- */
5464
- async function script(options) {
5488
+ async function rebaseline(options) {
5465
5489
  logBetaWarning("tailordb migration");
5466
- const migrationNumber = parseMigrationNumberArg(options.number);
5467
- if (migrationNumber === 0) throw new Error(`Migration ${options.number} is the initial schema snapshot and cannot have a migration script.`);
5468
- const { config } = await loadConfig(options.configPath);
5469
- const namespacesWithMigrations = getNamespacesWithMigrations(config, path.dirname(config.path));
5470
- if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
5471
- const targetNamespace = resolveTargetNamespace(namespacesWithMigrations, options.namespace);
5472
- const { migrationsDir } = assertDefined(namespacesWithMigrations.find((ns) => ns.namespace === targetNamespace), "namespace with migrations not found");
5473
- if (options.noScript) {
5474
- const reason = options.reason?.trim();
5475
- if (!reason) throw new Error("--reason is required with --no-script.");
5476
- const scriptSkipped = markMigrationScriptSkipped({
5477
- migrationsDir,
5478
- migrationNumber,
5479
- reason
5480
- });
5481
- logger.success(`Recorded that migration ${styles.bold(options.number)} in namespace ${styles.bold(targetNamespace)} intentionally has no migration script`);
5482
- logger.info(` Reason: ${scriptSkipped.reason}`);
5483
- logger.info(` Diff file: ${getMigrationFilePath(migrationsDir, migrationNumber, "diff")}`);
5484
- return;
5490
+ const { config, plugins } = await loadConfig(options.configPath);
5491
+ const configDir = path.dirname(config.path);
5492
+ const target = selectTargetNamespace(getNamespacesWithMigrations(config, configDir), options.namespace);
5493
+ const generateCommand = formatNextAction({
5494
+ command: "tailor",
5495
+ args: [
5496
+ "tailordb",
5497
+ "migration",
5498
+ "generate",
5499
+ "--config",
5500
+ config.path
5501
+ ]
5502
+ });
5503
+ assertValidMigrationFiles(target.migrationsDir, target.namespace);
5504
+ const latestSnapshot = reconstructSnapshotFromMigrations(target.migrationsDir);
5505
+ if (!latestSnapshot) throw new Error(`No migration history found for namespace "${target.namespace}". Run ${generateCommand} first.`);
5506
+ const latestMigration = getLatestMigrationNumber(target.migrationsDir);
5507
+ const currentHistoryId = reconstructSnapshotFromMigrations(target.migrationsDir, 0)?.rebaseline?.historyId ?? null;
5508
+ const initialFileState = captureMigrationFileState([target])[target.namespace];
5509
+ const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
5510
+ const { defineApplication } = await import("../application-BxLLiLsr.mjs");
5511
+ const application = defineApplication({
5512
+ config,
5513
+ pluginManager
5514
+ });
5515
+ for (const service of application.tailorDBServices) {
5516
+ await service.loadTypes();
5517
+ await service.processNamespacePlugins();
5485
5518
  }
5486
- if (options.reason !== void 0) throw new Error("--reason can only be used together with --no-script.");
5487
- const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
5488
- if (!fs.existsSync(diffPath)) throw new Error(`Migration ${options.number} not found in ${migrationsDir}. Expected ${diffPath}.`);
5489
- const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
5490
- if (fs.existsSync(migratePath)) throw new Error(`Migration script already exists at ${migratePath}.`);
5491
- const diff = loadDiff(diffPath);
5492
- const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir, migrationNumber - 1);
5493
- if (!previousSnapshot) throw new Error(`Could not reconstruct previous schema for migration ${options.number}. Make sure migration ${0} exists.`);
5494
- const scriptContent = generateMigrationScript(diff);
5495
- await fsPromises.writeFile(migratePath, scriptContent);
5496
- await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff);
5497
- clearMigrationScriptSkipped(diffPath);
5498
- logger.success(`Added migration script for migration ${styles.bold(options.number)} in namespace ${styles.bold(targetNamespace)}`);
5499
- logger.info(` Migration script: ${migratePath}`);
5500
- logger.info(` DB types: ${getMigrationFilePath(migrationsDir, migrationNumber, "db")}`);
5519
+ const targetService = application.tailorDBServices.find((service) => service.namespace === target.namespace);
5520
+ if (!targetService) throw new Error(`No TailorDB service found for namespace "${target.namespace}"`);
5521
+ const assertLocalTypesReady = () => {
5522
+ const currentSnapshot = createSnapshotFromLocalTypes(targetService.types, target.namespace);
5523
+ const localDiff = compareLocalTypesWithSnapshot(latestSnapshot, currentSnapshot.types, target.namespace);
5524
+ if (hasChanges(localDiff)) {
5525
+ logger.error(`Migration history does not reproduce the current local schema for namespace ${styles.bold(target.namespace)}:`);
5526
+ logger.log(formatMigrationDiff(localDiff));
5527
+ throw new Error(`Refusing to re-baseline: the migration history must reproduce the current local schema. Run ${generateCommand} first.`);
5528
+ }
5529
+ };
5530
+ const localSourceFiles = () => [config.path, ...loadFilesWithIgnores(targetService.config, configDir)];
5531
+ assertLocalTypesReady();
5532
+ const initialLocalFileState = captureFileState(localSourceFiles());
5533
+ const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
5534
+ const workspaceId = await loadWorkspaceId({
5535
+ workspaceId: options.workspaceId,
5536
+ profile: options.profile
5537
+ });
5538
+ const remoteContextArgs = [
5539
+ "--config",
5540
+ config.path,
5541
+ "--workspace-id",
5542
+ workspaceId,
5543
+ ...options.profile ? ["--profile", options.profile] : []
5544
+ ];
5545
+ const setBaselineCommand = formatNextAction({
5546
+ command: "tailor",
5547
+ args: [
5548
+ "tailordb",
5549
+ "migration",
5550
+ "set",
5551
+ "0",
5552
+ "--namespace",
5553
+ target.namespace,
5554
+ ...remoteContextArgs
5555
+ ]
5556
+ });
5557
+ const deployCommand = formatNextAction({
5558
+ command: "tailor",
5559
+ args: ["deploy", ...remoteContextArgs]
5560
+ });
5561
+ const assertConnectedWorkspaceReady = async (expectedHistoryId, phase) => {
5562
+ const remoteState = await fetchRemoteMigrationState(client, resourceTrn(workspaceId, "tailordb", target.namespace));
5563
+ const remoteMigration = remoteState.number;
5564
+ if (remoteMigration !== latestMigration) {
5565
+ const actual = remoteMigration === null ? "<unset>" : formatMigrationNumber(remoteMigration);
5566
+ throw new Error(`The connected workspace must be at the latest migration ${formatMigrationNumber(latestMigration)} before re-baselining; current checkpoint is ${actual}.`);
5567
+ }
5568
+ if (remoteState.historyIdInvalid) throw new Error("Refusing to re-baseline: the connected workspace has an invalid migration history marker.");
5569
+ if (remoteState.historyId !== expectedHistoryId) throw new Error(phase === "confirmation" ? "The connected workspace migration history changed while waiting for confirmation. Run the command again." : "Refusing to re-baseline: the connected workspace does not match the local migration history.");
5570
+ const remoteResults = await verifyRemoteSchema(client, workspaceId, [target], config, [toTailorDBDeployInput(targetService)]);
5571
+ const remoteResult = remoteResults[0];
5572
+ if (!remoteResult || remoteResult.remoteMigrationNumber !== latestMigration || remoteResult.hasDrift || remoteResult.checkpointMissingLocal) {
5573
+ if (remoteResult?.hasDrift) {
5574
+ logger.error("Remote schema drift detected:");
5575
+ logger.log(formatRemoteVerificationResults(remoteResults));
5576
+ }
5577
+ throw new Error("Refusing to re-baseline: the connected workspace remote schema must match the latest migration.");
5578
+ }
5579
+ };
5580
+ await assertConnectedWorkspaceReady(currentHistoryId, "initial");
5501
5581
  logger.newline();
5502
- logger.log("Edit the script to implement your data migration logic.");
5503
- logger.log("It will be executed by 'tailor deploy' between Pre and Post phases.");
5504
- const editor = getConfiguredEditorCommand();
5505
- if (!editor) return;
5582
+ logger.warn(`This will replace the migration history for ${styles.bold(target.namespace)}.`);
5583
+ logger.log(` Latest migration: ${formatMigrationNumber(latestMigration)}`);
5584
+ logger.log(" New history: 0000/schema.json only");
5585
+ logger.log(" migrate.ts and db.ts files will disappear from the working tree.");
5586
+ logger.log(" Committed migration files will remain in Git history. Preserve any uncommitted files before continuing.");
5587
+ logger.log(" Every other environment must already be at the latest migration.");
5588
+ logger.log(" The connected workspace checkpoint will be reset to 0000.");
5506
5589
  logger.newline();
5507
- logger.info(`Opening ${path.basename(migratePath)} in ${editor}...`);
5508
- try {
5509
- await openInConfiguredEditor(migratePath);
5510
- } catch {
5511
- return;
5512
- }
5513
- }
5514
- function resolveTargetNamespace(namespacesWithMigrations, requested) {
5515
- if (requested) {
5516
- if (!namespacesWithMigrations.some((ns) => ns.namespace === requested)) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
5517
- return requested;
5590
+ if (!options.yes) {
5591
+ if (!await prompt.confirm({
5592
+ message: "Re-baseline this migration history and reset the connected workspace checkpoint?",
5593
+ default: false
5594
+ })) {
5595
+ logger.info("Operation cancelled.");
5596
+ return;
5597
+ }
5518
5598
  }
5519
- if (namespacesWithMigrations.length === 1) {
5520
- const [ns] = namespacesWithMigrations;
5521
- return assertDefined(ns, "namespace with migrations missing").namespace;
5599
+ assertValidMigrationFiles(target.migrationsDir, target.namespace);
5600
+ if (captureMigrationFileState([target])[target.namespace] !== initialFileState) throw new Error("Migration files changed while waiting for confirmation. Run the command again.");
5601
+ if (captureFileState(localSourceFiles()) !== initialLocalFileState) throw new Error("Local TailorDB type or config files changed. Run the command again.");
5602
+ assertLocalTypesReady();
5603
+ await assertConnectedWorkspaceReady(currentHistoryId, "confirmation");
5604
+ const rebaselineMarker = {
5605
+ historyId: createMigrationHistoryId(),
5606
+ replacedHistoryId: currentHistoryId,
5607
+ replacedLatestMigration: latestMigration
5608
+ };
5609
+ await activateBaseline(target.migrationsDir, latestSnapshot, target.namespace, rebaselineMarker);
5610
+ try {
5611
+ await updateMigrationLabel(client, workspaceId, target.namespace, 0, rebaselineMarker.historyId);
5612
+ } catch (error) {
5613
+ throw new Error(`The local migration history was re-baselined, but the connected workspace checkpoint could not be updated. Run ${setBaselineCommand}, or run ${deployCommand} with schema checks enabled after resolving the connection error.`, { cause: error });
5522
5614
  }
5523
- throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
5615
+ logger.success(`Re-baselined ${styles.bold(target.namespace)} and reset the connected workspace checkpoint to 0000.`);
5524
5616
  }
5525
- const scriptCommand = defineAppCommand({
5526
- name: "script",
5527
- description: "Add a migration script (migrate.ts) template to an existing migration directory, or record with --no-script that a migration intentionally has none.",
5617
+ const rebaselineCommand = defineAppCommand({
5618
+ name: "rebaseline",
5619
+ description: "Collapse the full migration history into a new 0000 baseline.",
5620
+ notes: `Re-baselining removes migrations after 0000 from the working tree, records a new migration history ID, and resets the connected workspace checkpoint without changing its schema or data. Every environment must already have applied the latest migration before you run this command.`,
5528
5621
  args: z.strictObject({
5529
- ...configArg,
5530
- number: arg(z.string(), {
5531
- positional: true,
5532
- description: "Migration number to add a script to (e.g., 0001 or 1)"
5533
- }),
5622
+ ...deploymentArgs,
5623
+ ...confirmationArgs,
5534
5624
  namespace: arg(z.string().optional(), {
5535
5625
  alias: "n",
5536
5626
  description: "Target TailorDB namespace (required if multiple namespaces exist)"
5537
- }),
5538
- "no-script": arg(z.boolean().optional(), { description: "Record that this migration intentionally runs without a migration script (requires --reason)" }),
5539
- reason: arg(z.string().optional(), { description: "Reason why no migration script is needed (used with --no-script)" })
5627
+ })
5540
5628
  }),
5541
5629
  run: async (args) => {
5542
- await script({
5630
+ await assertWritable({ profile: args.profile });
5631
+ await rebaseline({
5543
5632
  configPath: args.config,
5544
- number: args.number,
5545
5633
  namespace: args.namespace,
5546
- noScript: args["no-script"],
5547
- reason: args.reason
5634
+ yes: args.yes,
5635
+ workspaceId: args["workspace-id"],
5636
+ profile: args.profile
5548
5637
  });
5549
5638
  }
5550
5639
  });
5551
5640
 
5552
- //#endregion
5553
- //#region src/cli/commands/tailordb/migrate/remote-state.ts
5554
- /**
5555
- * Fetch the namespace's current migration number from its metadata labels.
5556
- *
5557
- * Only a namespace that has not been deployed yet reads as "no current
5558
- * migration". Every other lookup failure must propagate: treating it as
5559
- * unset would misreport a transient error as "no checkpoint", showing
5560
- * every migration as pending to callers.
5561
- * @param client - Operator client
5562
- * @param trn - Namespace TRN
5563
- * @returns Parsed current migration number, or null when unset or unparseable
5564
- */
5565
- async function fetchRemoteMigrationNumber(client, trn) {
5566
- const label = (await getOrNull(async () => {
5567
- const { metadata } = await client.getMetadata({ trn });
5568
- return metadata;
5569
- }))?.labels[MIGRATION_LABEL_KEY];
5570
- return label ? parseMigrationLabelNumber(label) : null;
5571
- }
5572
-
5573
5641
  //#endregion
5574
5642
  //#region src/cli/commands/tailordb/migrate/set.ts
5575
5643
  /**
@@ -5584,18 +5652,24 @@ async function set(options) {
5584
5652
  const targetNamespace = target.namespace;
5585
5653
  assertValidMigrationFiles(target.migrationsDir, targetNamespace);
5586
5654
  assertMigrationNumberExists(target.migrationsDir, migrationNumber);
5655
+ const historyId = reconstructSnapshotFromMigrations(target.migrationsDir, 0)?.rebaseline?.historyId;
5587
5656
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
5588
5657
  const trn = resourceTrn(await loadWorkspaceId({
5589
5658
  workspaceId: options.workspaceId,
5590
5659
  profile: options.profile
5591
5660
  }), "tailordb", targetNamespace);
5592
- const current = await fetchRemoteMigrationNumber(client, trn);
5661
+ const currentState = await fetchRemoteMigrationState(client, trn);
5662
+ const current = currentState.number;
5593
5663
  const currentMigration = current ?? 0;
5664
+ const currentHistoryId = currentState.historyIdInvalid ? "<invalid>" : currentState.historyId ?? "<unset>";
5665
+ const newHistoryId = historyId ?? "<unset>";
5594
5666
  logger.newline();
5595
- logger.warn("This operation will change the migration checkpoint.");
5667
+ logger.warn("This operation will change TailorDB migration state metadata.");
5596
5668
  logger.log(`Namespace: ${styles.bold(targetNamespace)}`);
5597
5669
  logger.log(`Current migration: ${current === null ? "<unset>" : styles.bold(formatMigrationNumber(current))}`);
5598
5670
  logger.log(`New migration: ${styles.bold(formatMigrationNumber(migrationNumber))}`);
5671
+ logger.log(`Current migration history ID: ${styles.bold(currentHistoryId)}`);
5672
+ logger.log(`New migration history ID: ${styles.bold(newHistoryId)}`);
5599
5673
  logger.newline();
5600
5674
  if (migrationNumber < currentMigration) {
5601
5675
  logger.warn(`Setting migration number backwards (${formatMigrationNumber(currentMigration)} → ${formatMigrationNumber(migrationNumber)}) will cause previous migrations to be re-executed on next apply.`);
@@ -5606,7 +5680,7 @@ async function set(options) {
5606
5680
  }
5607
5681
  if (!options.yes) {
5608
5682
  if (!await prompt.confirm({
5609
- message: "Continue with migration checkpoint update?",
5683
+ message: "Continue with migration checkpoint and history ID update?",
5610
5684
  default: false
5611
5685
  })) {
5612
5686
  logger.info("Operation cancelled.");
@@ -5616,7 +5690,11 @@ async function set(options) {
5616
5690
  }
5617
5691
  await writeMetadataLabels(client, {
5618
5692
  trn,
5619
- labels: { [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber) }
5693
+ labels: {
5694
+ [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(migrationNumber),
5695
+ ...historyId ? { [MIGRATION_HISTORY_LABEL_KEY]: historyId } : {}
5696
+ },
5697
+ remove: historyId ? void 0 : [MIGRATION_HISTORY_LABEL_KEY]
5620
5698
  });
5621
5699
  logger.success(`Migration checkpoint set to ${styles.bold(formatMigrationNumber(migrationNumber))} for namespace ${styles.bold(targetNamespace)}`);
5622
5700
  }
@@ -5662,17 +5740,67 @@ async function collectMigrationStatuses(options) {
5662
5740
  if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
5663
5741
  const targetNamespaces = options.namespace ? namespacesWithMigrations.filter((ns) => ns.namespace === options.namespace) : namespacesWithMigrations;
5664
5742
  if (targetNamespaces.length === 0) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
5743
+ const localStates = /* @__PURE__ */ new Map();
5744
+ const localFailures = /* @__PURE__ */ new Map();
5745
+ for (const { namespace, migrationsDir } of targetNamespaces) try {
5746
+ const migrationFiles = getMigrationFiles(migrationsDir);
5747
+ const descriptions = /* @__PURE__ */ new Map();
5748
+ let historyId = null;
5749
+ for (const file of migrationFiles) {
5750
+ if (file.type === "schema") {
5751
+ const snapshot = loadSnapshot(file.path);
5752
+ if (file.number === 0) historyId = snapshot.rebaseline?.historyId ?? null;
5753
+ continue;
5754
+ }
5755
+ try {
5756
+ const diff = loadDiff(file.path);
5757
+ if (diff.description) descriptions.set(file.number, diff.description);
5758
+ } catch (error) {
5759
+ if (error instanceof UnsupportedMigrationFileVersionError) throw error;
5760
+ }
5761
+ }
5762
+ localStates.set(namespace, {
5763
+ migrationFiles,
5764
+ descriptions,
5765
+ historyId
5766
+ });
5767
+ } catch (error) {
5768
+ localFailures.set(namespace, error instanceof Error ? error.message : String(error));
5769
+ }
5770
+ if (localStates.size === 0) return targetNamespaces.map(({ namespace }) => ({
5771
+ status: "error",
5772
+ namespace,
5773
+ error: localFailures.get(namespace) ?? "Failed to read local migration history."
5774
+ }));
5665
5775
  const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
5666
5776
  const workspaceId = await loadWorkspaceId({
5667
5777
  workspaceId: options.workspaceId,
5668
5778
  profile: options.profile
5669
5779
  });
5670
5780
  const rows = [];
5671
- for (const { namespace, migrationsDir } of targetNamespaces) {
5781
+ for (const { namespace } of targetNamespaces) {
5782
+ if (localFailures.has(namespace)) {
5783
+ rows.push({
5784
+ status: "error",
5785
+ namespace,
5786
+ error: localFailures.get(namespace) ?? "Failed to read local migration history."
5787
+ });
5788
+ continue;
5789
+ }
5790
+ const localState = localStates.get(namespace);
5791
+ if (!localState) {
5792
+ rows.push({
5793
+ status: "error",
5794
+ namespace,
5795
+ error: "Failed to read local migration history."
5796
+ });
5797
+ continue;
5798
+ }
5799
+ const { migrationFiles, descriptions, historyId: localHistoryId } = localState;
5672
5800
  const trn = resourceTrn(workspaceId, "tailordb", namespace);
5673
- let current;
5801
+ let remoteState;
5674
5802
  try {
5675
- current = await fetchRemoteMigrationNumber(client, trn);
5803
+ remoteState = await fetchRemoteMigrationState(client, trn);
5676
5804
  } catch (error) {
5677
5805
  rows.push({
5678
5806
  status: "error",
@@ -5681,19 +5809,28 @@ async function collectMigrationStatuses(options) {
5681
5809
  });
5682
5810
  continue;
5683
5811
  }
5684
- const currentMigration = current ?? 0;
5685
- const pendingMigrations = getMigrationFiles(migrationsDir).map((f) => f.number).filter((n, i, arr) => arr.indexOf(n) === i).toSorted((a, b) => a - b).filter((n) => n > currentMigration).map((num) => {
5686
- const diffPath = getMigrationFilePath(migrationsDir, num, "diff");
5687
- let description;
5688
- if (fs.existsSync(diffPath)) try {
5689
- description = loadDiff(diffPath).description;
5690
- } catch {}
5691
- return {
5692
- number: num,
5693
- label: formatMigrationNumber(num),
5694
- ...description ? { description } : {}
5695
- };
5696
- });
5812
+ const currentMigration = remoteState.number ?? 0;
5813
+ if (remoteState.historyIdInvalid) {
5814
+ rows.push({
5815
+ status: "error",
5816
+ namespace,
5817
+ error: "Remote migration history ID is invalid."
5818
+ });
5819
+ continue;
5820
+ }
5821
+ if ((remoteState.number !== null || remoteState.historyId !== null) && remoteState.historyId !== localHistoryId) {
5822
+ rows.push({
5823
+ status: "error",
5824
+ namespace,
5825
+ error: `Remote migration history ID ${remoteState.historyId ?? "<unset>"} does not match local migration history ID ${localHistoryId ?? "<unset>"}.`
5826
+ });
5827
+ continue;
5828
+ }
5829
+ const pendingMigrations = migrationFiles.map((f) => f.number).filter((n, i, arr) => arr.indexOf(n) === i).toSorted((a, b) => a - b).filter((n) => n > currentMigration).map((num) => ({
5830
+ number: num,
5831
+ label: formatMigrationNumber(num),
5832
+ ...descriptions.has(num) ? { description: descriptions.get(num) } : {}
5833
+ }));
5697
5834
  rows.push({
5698
5835
  status: "ok",
5699
5836
  namespace,
@@ -5709,7 +5846,7 @@ function printMigrationStatuses(rows) {
5709
5846
  logger.newline();
5710
5847
  logger.info(`Namespace: ${styles.bold(row.namespace)}`);
5711
5848
  if (isStatusFailure(row)) {
5712
- logger.error(` Failed to read migration state: ${row.error}`);
5849
+ logger.error(` Migration status error: ${row.error}`);
5713
5850
  continue;
5714
5851
  }
5715
5852
  logger.log(` Current migration: ${styles.bold(row.currentMigrationLabel)}`);
@@ -5733,13 +5870,13 @@ async function status(options) {
5733
5870
  const failures = rows.filter(isStatusFailure);
5734
5871
  if (failures.length > 0) {
5735
5872
  const namespaces = failures.map((f) => f.namespace).join(", ");
5736
- throw new Error(`Failed to read migration state for ${failures.length} namespace${failures.length === 1 ? "" : "s"}: ${namespaces}`);
5873
+ throw new Error(`Migration status check failed for ${failures.length} namespace${failures.length === 1 ? "" : "s"}: ${namespaces}`);
5737
5874
  }
5738
5875
  }
5739
5876
  const statusCommand = defineAppCommand({
5740
5877
  name: "status",
5741
5878
  description: "Show the current migration status for TailorDB namespaces, including applied and pending migrations.",
5742
- notes: "Metadata lookup failures (authentication, permission, or network errors) are reported per namespace and make the command exit non-zero; only a not-yet-deployed namespace is treated as having no applied migrations.",
5879
+ notes: "Every local migration file is checked for a compatible format version, and deployed migration history IDs must match the local baseline. Compatibility errors, history mismatches, and metadata lookup failures are reported per namespace and make the command exit non-zero; only a not-yet-deployed namespace is treated as having no applied migrations.",
5743
5880
  args: z.strictObject({
5744
5881
  ...deploymentArgs,
5745
5882
  namespace: arg(z.string().optional(), {
@@ -5808,7 +5945,7 @@ async function fetchRemoteTypes(client, workspaceId, namespace) {
5808
5945
  async function assertMigrationsReproduceLocalTypes(loaded, target) {
5809
5946
  const { config, plugins } = loaded;
5810
5947
  const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
5811
- const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-CXNaUNhv.mjs");
5948
+ const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-BxLLiLsr.mjs");
5812
5949
  const application = defineApplication({
5813
5950
  config,
5814
5951
  pluginManager
@@ -5820,7 +5957,7 @@ async function assertMigrationsReproduceLocalTypes(loaded, target) {
5820
5957
  await service.processNamespacePlugins();
5821
5958
  }
5822
5959
  const pluginExecutorFiles = generatePluginFilesIfNeeded(pluginManager, application.tailorDBServices, config.path);
5823
- const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-M7OYziQP.mjs")).createExecutorService({
5960
+ const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-_XmjSEGr.mjs")).createExecutorService({
5824
5961
  config: { files: [] },
5825
5962
  baseDir: path.dirname(config.path)
5826
5963
  }) : void 0);
@@ -5965,7 +6102,11 @@ async function sync(options) {
5965
6102
  })));
5966
6103
  await writeMetadataLabels(client, {
5967
6104
  trn,
5968
- labels: { [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(targetVersion) }
6105
+ labels: {
6106
+ [MIGRATION_LABEL_KEY]: sanitizeMigrationLabel(targetVersion),
6107
+ ...snapshot.rebaseline?.historyId ? { [MIGRATION_HISTORY_LABEL_KEY]: snapshot.rebaseline.historyId } : {}
6108
+ },
6109
+ remove: snapshot.rebaseline?.historyId ? void 0 : [MIGRATION_HISTORY_LABEL_KEY]
5969
6110
  });
5970
6111
  logger.success(`Synced namespace ${styles.bold(target.namespace)} to migration ${styles.bold(formatMigrationNumber(targetVersion))}.`);
5971
6112
  if (targetVersion < latest) {
@@ -6001,23 +6142,634 @@ const syncCommand = defineAppCommand({
6001
6142
  }
6002
6143
  });
6003
6144
 
6145
+ //#endregion
6146
+ //#region src/cli/commands/tailordb/migrate/test-runtime.ts
6147
+ const CLONE_POLL_INTERVAL = 1e3;
6148
+ const CLONE_TIMEOUT = 300 * 1e3;
6149
+ function delay(milliseconds) {
6150
+ return milliseconds > 0 ? new Promise((resolve) => setTimeout(resolve, milliseconds)) : Promise.resolve();
6151
+ }
6152
+ function assertSeedDataDirectory(dataDir) {
6153
+ let stats;
6154
+ try {
6155
+ stats = fs.statSync(dataDir);
6156
+ } catch (error) {
6157
+ if (error.code === "ENOENT") throw new Error(`Seed data directory not found: ${dataDir}. Run 'tailor generate' before testing migrations.`, { cause: error });
6158
+ throw error;
6159
+ }
6160
+ if (!stats.isDirectory()) throw new Error(`Seed data path is not a directory: ${dataDir}`);
6161
+ }
6162
+ function assertAssertionScript(assertionPath) {
6163
+ const resolvedPath = path.resolve(assertionPath);
6164
+ let stats;
6165
+ try {
6166
+ stats = fs.statSync(resolvedPath);
6167
+ } catch (error) {
6168
+ if (error.code === "ENOENT") throw new Error(`Migration assertion script not found: ${resolvedPath}`, { cause: error });
6169
+ throw error;
6170
+ }
6171
+ if (!stats.isFile()) throw new Error(`Migration assertion path is not a file: ${resolvedPath}`);
6172
+ }
6173
+ /**
6174
+ * Require a designated clone target to share the source workspace's region.
6175
+ * @param sourceRegion - Source workspace region
6176
+ * @param targetRegion - Target workspace region
6177
+ */
6178
+ function assertCloneTargetRegion(sourceRegion, targetRegion) {
6179
+ if (sourceRegion !== targetRegion) throw new Error(`Clone mode requires source and target workspaces in the same region (source: ${sourceRegion}, target: ${targetRegion}).`);
6180
+ }
6181
+ /**
6182
+ * Require the migration test target workspace to differ from the source workspace.
6183
+ * @param sourceWorkspaceId - Source workspace ID
6184
+ * @param targetWorkspaceId - Designated target workspace ID, if any
6185
+ */
6186
+ function assertTargetWorkspaceDiffers(sourceWorkspaceId, targetWorkspaceId) {
6187
+ if (targetWorkspaceId?.toLowerCase() === sourceWorkspaceId.toLowerCase()) throw new Error("The migration test target workspace must differ from the source workspace.");
6188
+ }
6189
+ /**
6190
+ * Resolve the namespace an assertion script runs against.
6191
+ * @param pendingNamespaces - Namespaces with pending migrations
6192
+ * @param assertionNamespace - Explicit namespace from `--assert-namespace`, if any
6193
+ * @returns Resolved assertion namespace
6194
+ */
6195
+ function resolveAssertionNamespace(pendingNamespaces, assertionNamespace) {
6196
+ const namespace = assertionNamespace ?? (pendingNamespaces.length === 1 ? pendingNamespaces[0] : void 0);
6197
+ if (!namespace) throw new Error("--assert-namespace is required when pending migrations span multiple namespaces.");
6198
+ if (!pendingNamespaces.includes(namespace)) throw new Error(`Assertion namespace "${namespace}" has no pending migrations.`);
6199
+ return namespace;
6200
+ }
6201
+ /**
6202
+ * Build the schema overrides used before migration-test data is loaded.
6203
+ * @param options - Source workspace, local inputs, and migration baselines
6204
+ * @returns Schema snapshots keyed by namespace
6205
+ */
6206
+ async function createMigrationTestBaselineSnapshots(options) {
6207
+ const snapshots = new Map([...options.baselines].map(([namespace, baseline]) => [namespace, baseline.snapshot]));
6208
+ if (options.dataMode !== "clone") return snapshots;
6209
+ const unmigratedInputs = options.inputs.filter((input) => !snapshots.has(input.namespace));
6210
+ const remoteSnapshots = await Promise.all(unmigratedInputs.map((input) => fetchRemoteSchemaSnapshot(options.client, options.workspaceId, input.namespace)));
6211
+ unmigratedInputs.forEach((input, index) => {
6212
+ snapshots.set(input.namespace, assertDefined(remoteSnapshots[index], `remote snapshot missing for "${input.namespace}"`));
6213
+ });
6214
+ return snapshots;
6215
+ }
6216
+ /**
6217
+ * Wait for an application-data clone operation to reach a terminal state.
6218
+ * @param client - Operator client
6219
+ * @param options - Clone operation identifiers and polling limits
6220
+ */
6221
+ async function waitForCloneApplicationData(client, options) {
6222
+ const pollInterval = options.pollInterval ?? CLONE_POLL_INTERVAL;
6223
+ const timeout = options.timeout ?? CLONE_TIMEOUT;
6224
+ const deadline = Date.now() + timeout;
6225
+ while (Date.now() <= deadline) {
6226
+ const operation = await client.getCloneApplicationDataOperation({
6227
+ sourceWorkspaceId: options.sourceWorkspaceId,
6228
+ targetWorkspaceId: options.targetWorkspaceId,
6229
+ operationId: options.operationId
6230
+ });
6231
+ if (operation.status === CloneOperationStatus.COMPLETED) return;
6232
+ if (operation.status === CloneOperationStatus.FAILED) throw new Error(`Application data clone failed: ${operation.errorMessage || "unknown platform error"}`);
6233
+ if (operation.status !== CloneOperationStatus.PENDING && operation.status !== CloneOperationStatus.PROCESSING) throw new Error(`Application data clone returned unexpected status ${operation.status}.`);
6234
+ await delay(pollInterval);
6235
+ }
6236
+ throw new Error(`Application data clone timed out after ${Math.round(timeout / 1e3)} seconds.`);
6237
+ }
6238
+ /**
6239
+ * Load generated JSONL seed rows for the types in a baseline snapshot.
6240
+ * @param dataDir - Directory containing `<Type>.jsonl` files
6241
+ * @param typeNames - Baseline type names to load
6242
+ * @param snapshot - Baseline schema used to remove fields introduced by pending migrations
6243
+ * @returns Seed rows keyed by type name
6244
+ */
6245
+ function loadSnapshotSeedData(dataDir, typeNames, snapshot) {
6246
+ const data = {};
6247
+ for (const typeName of typeNames) {
6248
+ const snapshotType = snapshot?.types[typeName];
6249
+ const jsonlPath = path.join(dataDir, `${typeName}.jsonl`);
6250
+ let content;
6251
+ try {
6252
+ content = fs.readFileSync(jsonlPath, "utf8").trim();
6253
+ } catch (error) {
6254
+ if (error.code === "ENOENT") {
6255
+ data[typeName] = [];
6256
+ continue;
6257
+ }
6258
+ throw error;
6259
+ }
6260
+ data[typeName] = content ? content.split("\n").map((line, index) => {
6261
+ let value;
6262
+ try {
6263
+ value = JSON.parse(line);
6264
+ } catch (error) {
6265
+ throw new Error(`Invalid JSON in ${jsonlPath} at line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
6266
+ }
6267
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid seed row in ${jsonlPath} at line ${index + 1}: expected a JSON object`);
6268
+ const row = value;
6269
+ return snapshotType ? projectSeedObject(row, snapshotType.fields, /* @__PURE__ */ new Set(["id"])) : row;
6270
+ }) : [];
6271
+ }
6272
+ return data;
6273
+ }
6274
+ function projectSeedObject(value, fields, implicitFields = /* @__PURE__ */ new Set()) {
6275
+ return Object.fromEntries(Object.entries(value).flatMap(([fieldName, fieldValue]) => {
6276
+ const field = fields[fieldName];
6277
+ if (!field) return implicitFields.has(fieldName) ? [[fieldName, fieldValue]] : [];
6278
+ return [[fieldName, projectSeedValue(fieldValue, field)]];
6279
+ }));
6280
+ }
6281
+ function projectSeedValue(value, field) {
6282
+ const fields = field.fields;
6283
+ if (!fields) return value;
6284
+ if (field.array) {
6285
+ if (!Array.isArray(value)) return value;
6286
+ return value.map((entry) => isJsonObject(entry) ? projectSeedObject(entry, fields) : entry);
6287
+ }
6288
+ return isJsonObject(value) ? projectSeedObject(value, fields) : value;
6289
+ }
6290
+ function isJsonObject(value) {
6291
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6292
+ }
6293
+ /**
6294
+ * Derive insertion order and self-referencing types from a baseline snapshot.
6295
+ * @param snapshot - Baseline schema snapshot
6296
+ * @returns Dependency-ordered type names and self-referencing types
6297
+ */
6298
+ function sortSeedTypesForSnapshot(snapshot) {
6299
+ const typeNames = Object.keys(snapshot.types);
6300
+ const available = new Set(typeNames);
6301
+ const dependencies = /* @__PURE__ */ new Map();
6302
+ const selfRefTypes = [];
6303
+ for (const [typeName, type] of Object.entries(snapshot.types)) {
6304
+ const referenced = /* @__PURE__ */ new Set();
6305
+ for (const field of Object.values(type.fields)) {
6306
+ const target = field.foreignKeyType;
6307
+ if (!target) continue;
6308
+ if (target === typeName) selfRefTypes.push(typeName);
6309
+ else if (available.has(target)) referenced.add(target);
6310
+ }
6311
+ dependencies.set(typeName, [...referenced]);
6312
+ }
6313
+ const visited = /* @__PURE__ */ new Set();
6314
+ const order = [];
6315
+ const visit = (typeName) => {
6316
+ if (visited.has(typeName)) return;
6317
+ visited.add(typeName);
6318
+ for (const dependency of dependencies.get(typeName) ?? []) visit(dependency);
6319
+ order.push(typeName);
6320
+ };
6321
+ for (const typeName of typeNames) visit(typeName);
6322
+ return {
6323
+ order,
6324
+ selfRefTypes
6325
+ };
6326
+ }
6327
+ function temporaryWorkspaceName() {
6328
+ return `migration-test-${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14)}-${randomBytes(3).toString("hex")}`;
6329
+ }
6330
+ function stateOrThrow(state) {
6331
+ return assertDefined(state, "migration test runtime was used before preparation");
6332
+ }
6333
+ /**
6334
+ * Require the source workspace to still match the prepared baselines.
6335
+ *
6336
+ * The source is verified during preparation, but the baseline deploy runs in
6337
+ * between; a source deploy in that window would make the clone diverge from
6338
+ * the already-deployed baseline schemas.
6339
+ * @param state - Prepared migration test runtime state
6340
+ * @param prepared - Baselines captured during preparation
6341
+ * @param sourceWorkspaceId - Source workspace ID
6342
+ */
6343
+ async function assertSourceBaselineFresh(state, prepared, sourceWorkspaceId) {
6344
+ const loaded = state.loaded;
6345
+ const namespaces = getNamespacesWithMigrations(loaded.config, path.dirname(loaded.config.path));
6346
+ const inputs = loaded.application.tailorDBServices.map(toTailorDBDeployInput);
6347
+ const invalidRemote = (await verifyRemoteSchema(state.client, sourceWorkspaceId, namespaces, loaded.config, inputs)).find((check) => check.hasDrift || check.checkpointMissingLocal || check.checkpointRepair);
6348
+ if (invalidRemote) throw new Error(`Source namespace "${invalidRemote.namespace}" changed after migration test preparation. Run the migration test again.`);
6349
+ for (const namespace of namespaces) {
6350
+ const baseline = prepared.baselines.get(namespace.namespace);
6351
+ if (!baseline) continue;
6352
+ const migrationNumber = await fetchRemoteMigrationNumber(state.client, resourceTrn(sourceWorkspaceId, "tailordb", namespace.namespace));
6353
+ if (migrationNumber !== baseline.migrationNumber) throw new Error(`Source namespace "${namespace.namespace}" moved from migration ${baseline.migrationNumber} to ${migrationNumber ?? "none"} after migration test preparation. Run the migration test again.`);
6354
+ }
6355
+ const unmigratedInputs = inputs.filter((input) => !prepared.baselines.has(input.namespace));
6356
+ for (const input of unmigratedInputs) {
6357
+ const snapshot = prepared.baselineSnapshots.get(input.namespace);
6358
+ if (!snapshot) continue;
6359
+ if (compareSnapshots(snapshot, await fetchRemoteSchemaSnapshot(state.client, sourceWorkspaceId, input.namespace)).changes.length > 0) throw new Error(`Source namespace "${input.namespace}" schema changed after migration test preparation. Run the migration test again.`);
6360
+ }
6361
+ }
6362
+ /**
6363
+ * Delete the target workspace's user profile config when one exists.
6364
+ *
6365
+ * A retained target may carry a user profile config referencing types the
6366
+ * baseline deploy replaces; deleting it up front lets the baseline's planAuth
6367
+ * see no existing config instead of reordering delete phases.
6368
+ * @param state - Prepared migration test runtime state
6369
+ * @param targetWorkspaceId - Target workspace ID
6370
+ */
6371
+ async function deleteExistingUserProfileConfig(state, targetWorkspaceId) {
6372
+ const auth = state.loaded.application.authService;
6373
+ if (!auth) return;
6374
+ const namespaceName = auth.config.name;
6375
+ if (!await getOrNull(() => state.client.getUserProfileConfig({
6376
+ workspaceId: targetWorkspaceId,
6377
+ namespaceName
6378
+ }))) return;
6379
+ await state.client.deleteUserProfileConfig({
6380
+ workspaceId: targetWorkspaceId,
6381
+ namespaceName
6382
+ });
6383
+ }
6384
+ function migrationConfig(loaded, namespace) {
6385
+ return (loaded.config.db?.[namespace])?.migration;
6386
+ }
6387
+ function authExecutionContext(state, namespace, explicitMachineUser) {
6388
+ const auth = state.loaded.application.authService;
6389
+ if (!auth) throw new Error("Auth configuration is required to execute migration test scripts.");
6390
+ const machineUserName = explicitMachineUser ?? getMigrationMachineUser(migrationConfig(state.loaded, namespace), auth.config.machineUsers ? Object.keys(auth.config.machineUsers) : void 0);
6391
+ if (!machineUserName) throw new Error(`No machine user is available for namespace "${namespace}". Pass --machine-user or configure one in auth or db.${namespace}.migration.`);
6392
+ return {
6393
+ authNamespace: auth.config.name,
6394
+ machineUserName
6395
+ };
6396
+ }
6397
+ async function prepareMigrationTest(options) {
6398
+ const loaded = await loadApplicationNamespaces({ configPath: options.configPath });
6399
+ const configDir = path.dirname(loaded.config.path);
6400
+ const namespaces = getNamespacesWithMigrations(loaded.config, configDir);
6401
+ if (namespaces.length === 0) throw new Error("No TailorDB services with migrations configuration found.");
6402
+ for (const namespace of namespaces) assertValidMigrationFiles(namespace.migrationsDir, namespace.namespace);
6403
+ let seedContext;
6404
+ if (options.data === "seed") {
6405
+ seedContext = await loadSeedContext({ configPath: loaded.config.path });
6406
+ assertSeedDataDirectory(path.join(seedContext.distPath, "data"));
6407
+ }
6408
+ if (options.assertionPath) assertAssertionScript(options.assertionPath);
6409
+ const inputs = loaded.application.tailorDBServices.map(toTailorDBDeployInput);
6410
+ const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }), await loadPlatformClientConfig({ profile: options.profile }));
6411
+ const sourceWorkspaceId = await loadWorkspaceId({
6412
+ workspaceId: options.workspaceId,
6413
+ profile: options.profile
6414
+ });
6415
+ assertTargetWorkspaceDiffers(sourceWorkspaceId, options.targetWorkspaceId);
6416
+ const [workspaceResponse, applicationResponse, targetWorkspaceResponse] = await Promise.all([
6417
+ client.getWorkspace({ workspaceId: sourceWorkspaceId }),
6418
+ client.getApplication({
6419
+ workspaceId: sourceWorkspaceId,
6420
+ applicationName: loaded.config.name
6421
+ }),
6422
+ options.data === "clone" && options.targetWorkspaceId ? client.getWorkspace({ workspaceId: options.targetWorkspaceId }) : Promise.resolve(void 0)
6423
+ ]);
6424
+ const sourceWorkspace = assertDefined(workspaceResponse.workspace, `source workspace "${sourceWorkspaceId}" not found`);
6425
+ assertDefined(applicationResponse.application, `application "${loaded.config.name}" not found in source workspace`);
6426
+ const designatedTarget = targetWorkspaceResponse ? assertDefined(targetWorkspaceResponse.workspace, `target workspace "${options.targetWorkspaceId}" not found`) : void 0;
6427
+ const remoteChecks = await verifyRemoteSchema(client, sourceWorkspaceId, namespaces, loaded.config, inputs);
6428
+ const invalidRemote = remoteChecks.find((check) => check.hasDrift || check.checkpointMissingLocal);
6429
+ if (invalidRemote) throw new Error(`Source namespace "${invalidRemote.namespace}" does not match its migration checkpoint. Run 'tailor tailordb migration validate' first.`);
6430
+ const pendingRepair = remoteChecks.find((check) => check.checkpointRepair);
6431
+ if (pendingRepair) throw new Error(`Source namespace "${pendingRepair.namespace}" has a pending migration checkpoint repair. Run 'tailor deploy' against the source workspace first.`);
6432
+ const baselines = /* @__PURE__ */ new Map();
6433
+ const targetSnapshots = /* @__PURE__ */ new Map();
6434
+ const pendingNamespaces = [];
6435
+ for (const namespace of namespaces) {
6436
+ const migrationNumber = await fetchRemoteMigrationNumber(client, resourceTrn(sourceWorkspaceId, "tailordb", namespace.namespace));
6437
+ if (migrationNumber === null) throw new Error(`Source namespace "${namespace.namespace}" has no sdk-migration checkpoint. Deploy or set its migration checkpoint before testing.`);
6438
+ const latest = getLatestMigrationNumber(namespace.migrationsDir);
6439
+ if (migrationNumber > latest) throw new Error(`Source namespace "${namespace.namespace}" is at migration ${migrationNumber}, but the local history ends at ${latest}.`);
6440
+ const snapshot = reconstructSnapshotFromMigrations(namespace.migrationsDir, migrationNumber);
6441
+ if (!snapshot) throw new Error(`No migration baseline snapshot found for namespace "${namespace.namespace}".`);
6442
+ baselines.set(namespace.namespace, {
6443
+ migrationNumber,
6444
+ snapshot,
6445
+ historyId: snapshot.rebaseline?.historyId ?? null
6446
+ });
6447
+ const targetSnapshot = reconstructSnapshotFromMigrations(namespace.migrationsDir, latest);
6448
+ if (!targetSnapshot) throw new Error(`No target migration snapshot found for namespace "${namespace.namespace}".`);
6449
+ targetSnapshots.set(namespace.namespace, targetSnapshot);
6450
+ if (migrationNumber < latest) pendingNamespaces.push(namespace.namespace);
6451
+ }
6452
+ if (pendingNamespaces.length === 0) throw new Error("No pending TailorDB migrations found in the source workspace.");
6453
+ const baselineSnapshots = await createMigrationTestBaselineSnapshots({
6454
+ client,
6455
+ workspaceId: sourceWorkspaceId,
6456
+ dataMode: options.data,
6457
+ inputs,
6458
+ baselines
6459
+ });
6460
+ const prepared = {
6461
+ sourceWorkspaceId,
6462
+ sourceApplicationName: loaded.config.name,
6463
+ temporaryWorkspace: {
6464
+ name: temporaryWorkspaceName(),
6465
+ region: sourceWorkspace.region,
6466
+ ...sourceWorkspace.organizationId ? { organizationId: sourceWorkspace.organizationId } : {},
6467
+ ...sourceWorkspace.folderId ? { folderId: sourceWorkspace.folderId } : {}
6468
+ },
6469
+ baselines,
6470
+ baselineSnapshots,
6471
+ targetSnapshots,
6472
+ pendingNamespaces,
6473
+ ...designatedTarget ? { designatedTarget: {
6474
+ id: designatedTarget.id,
6475
+ region: designatedTarget.region
6476
+ } } : {}
6477
+ };
6478
+ return {
6479
+ state: {
6480
+ client,
6481
+ loaded,
6482
+ options,
6483
+ ...seedContext ? { seedContext } : {}
6484
+ },
6485
+ prepared
6486
+ };
6487
+ }
6488
+ /**
6489
+ * Create the platform operations used by `tailordb migration test`.
6490
+ * @returns Migration test dependencies backed by the Operator API
6491
+ */
6492
+ function createMigrationTestDependencies() {
6493
+ let runtimeState;
6494
+ return {
6495
+ prepare: async (options) => {
6496
+ const { state, prepared } = await prepareMigrationTest(options);
6497
+ runtimeState = state;
6498
+ return prepared;
6499
+ },
6500
+ createWorkspace: async (prepared) => {
6501
+ const workspace = await createValidatedWorkspaceWithClient(stateOrThrow(runtimeState).client, prepared.temporaryWorkspace);
6502
+ logger.info(`Created temporary workspace ${workspace.name} (${workspace.id}).`);
6503
+ return {
6504
+ id: workspace.id,
6505
+ name: workspace.name
6506
+ };
6507
+ },
6508
+ deployBaseline: async ({ prepared, targetWorkspaceId }) => {
6509
+ const state = stateOrThrow(runtimeState);
6510
+ await deleteExistingUserProfileConfig(state, targetWorkspaceId);
6511
+ await deployMigrationTestBaseline({
6512
+ configPath: state.loaded.config.path,
6513
+ profile: state.options.profile,
6514
+ workspaceId: targetWorkspaceId,
6515
+ yes: true
6516
+ }, prepared.baselines, prepared.baselineSnapshots);
6517
+ for (const [namespace, baseline] of prepared.baselines) await updateMigrationLabel(state.client, targetWorkspaceId, namespace, baseline.migrationNumber, baseline.historyId ?? void 0);
6518
+ },
6519
+ seedData: async ({ prepared, targetWorkspaceId }) => {
6520
+ const state = stateOrThrow(runtimeState);
6521
+ const seedContext = assertDefined(state.seedContext, "seed context missing after migration test preparation");
6522
+ const dataDir = path.join(seedContext.distPath, "data");
6523
+ assertSeedDataDirectory(dataDir);
6524
+ const seedSnapshots = new Map(state.loaded.application.tailorDBServices.map((service) => {
6525
+ const baseline = prepared.baselines.get(service.namespace);
6526
+ const input = toTailorDBDeployInput(service);
6527
+ return [service.namespace, baseline?.snapshot ?? normalizeSchemaSnapshot({
6528
+ version: 1,
6529
+ namespace: service.namespace,
6530
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6531
+ types: input.types
6532
+ })];
6533
+ }));
6534
+ for (const [namespace, snapshot] of seedSnapshots) {
6535
+ const { order, selfRefTypes } = sortSeedTypesForSnapshot(snapshot);
6536
+ const data = loadSnapshotSeedData(dataDir, order, snapshot);
6537
+ const typesWithData = order.filter((typeName) => (data[typeName]?.length ?? 0) > 0);
6538
+ if (typesWithData.length === 0) continue;
6539
+ const bundled = await bundleSeedScript(namespace, typesWithData, path.dirname(state.loaded.config.path));
6540
+ const chunks = chunkSeedData({
6541
+ data,
6542
+ order,
6543
+ codeByteSize: new TextEncoder().encode(bundled.bundledCode).length
6544
+ });
6545
+ const auth = authExecutionContext(state, namespace, state.options.machineUser ?? seedContext.machineUserName);
6546
+ for (const chunk of chunks) {
6547
+ const execution = await executeScript({
6548
+ client: state.client,
6549
+ workspaceId: targetWorkspaceId,
6550
+ name: `migration-test-seed-${namespace}.ts`,
6551
+ code: bundled.bundledCode,
6552
+ arg: {
6553
+ data: chunk.data,
6554
+ order: chunk.order,
6555
+ selfRefTypes,
6556
+ upsert: false
6557
+ },
6558
+ invoker: {
6559
+ namespace: auth.authNamespace,
6560
+ machineUserName: auth.machineUserName
6561
+ }
6562
+ });
6563
+ if (!execution.success) throw new Error(execution.error ?? `Seed execution failed for namespace "${namespace}".`);
6564
+ if (JSON.parse(execution.result || "{}").success !== true) throw new Error(`Seed execution reported failure for namespace "${namespace}".`);
6565
+ }
6566
+ }
6567
+ },
6568
+ cloneData: async ({ prepared, sourceWorkspaceId, targetWorkspaceId, applicationName }) => {
6569
+ const state = stateOrThrow(runtimeState);
6570
+ await assertSourceBaselineFresh(state, prepared, sourceWorkspaceId);
6571
+ logger.warn("Clone mode copies TailorDB records only; IdP users and file blobs are not copied.");
6572
+ let operationId;
6573
+ try {
6574
+ operationId = (await state.client.cloneApplicationData({
6575
+ sourceWorkspaceId,
6576
+ sourceApplicationName: applicationName,
6577
+ targetWorkspaceId,
6578
+ targetApplicationName: applicationName
6579
+ })).operationId;
6580
+ } catch (error) {
6581
+ if (error instanceof ConnectError && error.code === Code.Unimplemented) throw new Error("Application data clone is not enabled for this platform. Retry with '--data seed'.", { cause: error });
6582
+ throw error;
6583
+ }
6584
+ if (!operationId) throw new Error("Application data clone returned no operation ID.");
6585
+ await waitForCloneApplicationData(state.client, {
6586
+ sourceWorkspaceId,
6587
+ targetWorkspaceId,
6588
+ operationId
6589
+ });
6590
+ },
6591
+ deployMigrations: async ({ prepared, targetWorkspaceId }) => {
6592
+ const state = stateOrThrow(runtimeState);
6593
+ await deployMigrationTestTarget({
6594
+ configPath: state.loaded.config.path,
6595
+ profile: state.options.profile,
6596
+ workspaceId: targetWorkspaceId,
6597
+ yes: true
6598
+ }, prepared.targetSnapshots);
6599
+ },
6600
+ runAssertion: async ({ prepared, targetWorkspaceId, assertionPath, assertionNamespace, machineUser }) => {
6601
+ const state = stateOrThrow(runtimeState);
6602
+ const namespace = resolveAssertionNamespace(prepared.pendingNamespaces, assertionNamespace);
6603
+ const auth = authExecutionContext(state, namespace, machineUser);
6604
+ const bundled = await bundleMigrationScript(assertionPath, namespace, 0, state.loaded.config.env ?? {}, path.dirname(state.loaded.config.path));
6605
+ const execution = await executeScript({
6606
+ client: state.client,
6607
+ workspaceId: targetWorkspaceId,
6608
+ name: `migration-test-assert-${namespace}.ts`,
6609
+ code: bundled.bundledCode,
6610
+ invoker: {
6611
+ namespace: auth.authNamespace,
6612
+ machineUserName: auth.machineUserName
6613
+ }
6614
+ });
6615
+ if (!execution.success) throw new Error(execution.error ?? `Migration assertion failed for namespace "${namespace}".`);
6616
+ },
6617
+ deleteWorkspace: async (workspaceId) => {
6618
+ await stateOrThrow(runtimeState).client.deleteWorkspace({ workspaceId });
6619
+ }
6620
+ };
6621
+ }
6622
+
6623
+ //#endregion
6624
+ //#region src/cli/commands/tailordb/migrate/test.ts
6625
+ /**
6626
+ * Run pending migrations against an isolated target workspace.
6627
+ * @param options - Migration test options
6628
+ * @param dependencies - Runtime operations used by the migration test
6629
+ * @returns Migration test result
6630
+ */
6631
+ async function runMigrationTest(options, dependencies = createMigrationTestDependencies()) {
6632
+ if (options.targetWorkspaceId && !options.yes) throw new Error("--target-workspace-id requires --yes because the target may be overwritten.");
6633
+ if (options.keep && options.targetWorkspaceId) throw new Error("--keep applies only to automatically created workspaces; a designated target is always retained.");
6634
+ if (options.assertionNamespace && !options.assertionPath) throw new Error("--assert is required when --assert-namespace is provided.");
6635
+ const prepared = await dependencies.prepare(options);
6636
+ assertTargetWorkspaceDiffers(prepared.sourceWorkspaceId, options.targetWorkspaceId);
6637
+ if (options.assertionPath) resolveAssertionNamespace(prepared.pendingNamespaces, options.assertionNamespace);
6638
+ if (options.data === "clone" && options.targetWorkspaceId) {
6639
+ const target = assertDefined(prepared.designatedTarget, "designated clone target was not loaded during preparation");
6640
+ assertCloneTargetRegion(prepared.temporaryWorkspace.region, target.region);
6641
+ }
6642
+ const temporary = options.targetWorkspaceId === void 0;
6643
+ const workspace = temporary ? await dependencies.createWorkspace(prepared) : {
6644
+ id: assertDefined(options.targetWorkspaceId, "designated target workspace missing"),
6645
+ name: void 0
6646
+ };
6647
+ let primaryError;
6648
+ let failed = false;
6649
+ let deleted = false;
6650
+ try {
6651
+ const target = {
6652
+ prepared,
6653
+ targetWorkspaceId: workspace.id
6654
+ };
6655
+ await dependencies.deployBaseline(target);
6656
+ const dataTarget = {
6657
+ ...target,
6658
+ sourceWorkspaceId: prepared.sourceWorkspaceId,
6659
+ applicationName: prepared.sourceApplicationName
6660
+ };
6661
+ if (options.data === "clone") await dependencies.cloneData(dataTarget);
6662
+ else await dependencies.seedData(dataTarget);
6663
+ await dependencies.deployMigrations(target);
6664
+ if (options.assertionPath) await dependencies.runAssertion({
6665
+ ...target,
6666
+ assertionPath: options.assertionPath,
6667
+ assertionNamespace: options.assertionNamespace,
6668
+ machineUser: options.machineUser
6669
+ });
6670
+ } catch (error) {
6671
+ primaryError = error;
6672
+ failed = true;
6673
+ }
6674
+ if (temporary && options.keep) logger.info(`Keeping temporary workspace ${workspace.name ?? workspace.id}.`);
6675
+ else if (temporary) try {
6676
+ await dependencies.deleteWorkspace(workspace.id);
6677
+ deleted = true;
6678
+ } catch (cleanupError) {
6679
+ if (!failed) throw new Error(`Failed to delete temporary workspace ${workspace.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, { cause: cleanupError });
6680
+ logger.warn(`Failed to delete temporary workspace ${workspace.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
6681
+ }
6682
+ if (failed) throw primaryError;
6683
+ return {
6684
+ success: true,
6685
+ workspaceId: workspace.id,
6686
+ ...workspace.name ? { workspaceName: workspace.name } : {},
6687
+ temporary,
6688
+ deleted,
6689
+ data: options.data,
6690
+ pendingNamespaces: prepared.pendingNamespaces
6691
+ };
6692
+ }
6693
+ async function migrationTest(options) {
6694
+ logBetaWarning("tailordb migration");
6695
+ const result = await runMigrationTest(options);
6696
+ if (options.json) {
6697
+ logger.out(result);
6698
+ return;
6699
+ }
6700
+ logger.success(`Pending migrations completed in workspace ${result.workspaceName ?? result.workspaceId}.`);
6701
+ }
6702
+ const testCommand = defineAppCommand({
6703
+ name: "test",
6704
+ description: "Test pending migrations with seed fixtures or cloned data in a temporary workspace.",
6705
+ notes: "The source workspace is read-only. Without --target-workspace-id, the command creates a workspace in the source workspace's region and deletes it after success or failure; pass --keep to retain it for inspection. A designated target is retained and requires --yes. Clone mode copies TailorDB records only; it does not copy IdP users or file blobs.",
6706
+ args: z.strictObject({
6707
+ ...deploymentArgs,
6708
+ yes: arg(z.boolean().default(false), {
6709
+ alias: "y",
6710
+ description: "Acknowledge that a designated target workspace may be overwritten"
6711
+ }),
6712
+ data: arg(z.enum(["seed", "clone"]).default("seed"), { description: "Data source for the migration test (seed or clone)" }),
6713
+ "target-workspace-id": arg(z.uuid().optional(), { description: "Existing throwaway workspace to retain after the test (requires --yes)" }),
6714
+ keep: arg(z.boolean().default(false), { description: "Keep the automatically created workspace after the test" }),
6715
+ assert: arg(z.string().optional(), {
6716
+ description: "Path to a TypeScript assertion script to run after migrations",
6717
+ completion: {
6718
+ type: "file",
6719
+ extensions: ["ts"]
6720
+ }
6721
+ }),
6722
+ "assert-namespace": arg(z.string().optional(), { description: "TailorDB namespace exposed to the assertion script" }),
6723
+ "machine-user": arg(z.string().optional(), { description: "Machine user for seed and assertion script execution" })
6724
+ }),
6725
+ run: async (args) => {
6726
+ await assertWritable({ profile: args.profile });
6727
+ await migrationTest({
6728
+ configPath: args.config,
6729
+ workspaceId: args["workspace-id"],
6730
+ profile: args.profile,
6731
+ data: args.data,
6732
+ targetWorkspaceId: args["target-workspace-id"],
6733
+ keep: args.keep,
6734
+ assertionPath: args.assert,
6735
+ assertionNamespace: args["assert-namespace"],
6736
+ machineUser: args["machine-user"],
6737
+ yes: args.yes,
6738
+ json: logger.jsonMode
6739
+ });
6740
+ }
6741
+ });
6742
+
6004
6743
  //#endregion
6005
6744
  //#region src/cli/commands/tailordb/migrate/validate.ts
6006
6745
  /**
6007
- * Assert that every migration in the local history whose diff requires a data
6008
- * migration script has a migrate.ts on disk or an explicit skip acknowledgment
6746
+ * Walk the local migration history once to assert that every required
6747
+ * migration script exists and generated normalization logic has been
6748
+ * reviewed, and to collect migrations with data-loss warnings that have
6749
+ * neither a migrate.ts nor a recorded --no-script acknowledgment
6009
6750
  * @param {string} migrationsDir - Migrations directory path
6010
6751
  * @param {string} namespace - TailorDB namespace (for error messages)
6752
+ * @returns {UnacknowledgedWarningMigration[]} Unacknowledged warning migrations in the local history
6011
6753
  */
6012
- function assertRequiredMigrationScripts(migrationsDir, namespace) {
6754
+ function assertMigrationScriptsReady(migrationsDir, namespace) {
6013
6755
  const missing = [];
6756
+ const unreviewed = [];
6757
+ const unacknowledgedWarnings = [];
6014
6758
  for (const file of getMigrationFiles(migrationsDir)) {
6015
6759
  if (file.type !== "diff") continue;
6016
6760
  const diff = loadDiff(file.path);
6017
- if (!diff.requiresMigrationScript || diff.scriptSkipped) continue;
6018
- if (!fs.existsSync(getMigrationFilePath(migrationsDir, file.number, "migrate"))) missing.push(file.number);
6761
+ const migrateFilePath = getMigrationFilePath(migrationsDir, file.number, "migrate");
6762
+ const hasScript = fs.existsSync(migrateFilePath);
6763
+ if (diff.requiresMigrationScript && !diff.scriptSkipped && !hasScript) missing.push(file.number);
6764
+ if (hasScript && fs.readFileSync(migrateFilePath, "utf8").includes("TODO(tailor-migration-review)")) unreviewed.push(file.number);
6765
+ if (diff.hasWarnings && !diff.scriptSkipped && !hasScript) unacknowledgedWarnings.push({
6766
+ migrationNumber: file.number,
6767
+ warnings: diff.warnings
6768
+ });
6019
6769
  }
6020
6770
  if (missing.length > 0) throw new Error(`Migration(s) ${missing.map(formatMigrationNumber).join(", ")} in namespace "${namespace}" require a migration script but have no migrate.ts. Add one with 'tailor tailordb migration script <number>', or record that no script is needed with 'tailor tailordb migration script <number> --no-script --reason "..."'.`);
6771
+ if (unreviewed.length > 0) throw new Error(`Migration(s) ${unreviewed.map(formatMigrationNumber).join(", ")} in namespace "${namespace}" contain generated normalization logic that still requires review in migrate.ts. Review each ${MIGRATION_REVIEW_REQUIRED_MARKER} marker, then remove the marker and its associated \`never\` annotation.`);
6772
+ return unacknowledgedWarnings;
6021
6773
  }
6022
6774
  /**
6023
6775
  * Build per-namespace reports from the checks that completed
@@ -6025,7 +6777,7 @@ function assertRequiredMigrationScripts(migrationsDir, namespace) {
6025
6777
  * @returns {NamespaceValidationReport[]} Validation reports per namespace
6026
6778
  */
6027
6779
  function buildValidationReports(options) {
6028
- const { targetNamespaces, migrationFileErrors, localResults, remoteResults } = options;
6780
+ const { targetNamespaces, migrationFileErrors, localResults, remoteResults, unacknowledgedWarnings } = options;
6029
6781
  return targetNamespaces.map((target) => {
6030
6782
  const fileError = migrationFileErrors.get(target.namespace);
6031
6783
  if (fileError !== void 0) return {
@@ -6055,14 +6807,25 @@ function buildValidationReports(options) {
6055
6807
  hasDrift: remote.hasDrift,
6056
6808
  drifts: remote.drifts,
6057
6809
  ...remote.skipped ? { skipped: remote.skipped } : {},
6058
- ...checkpointMissingLocal ? { checkpointMissingLocal: true } : {}
6810
+ ...checkpointMissingLocal ? { checkpointMissingLocal: true } : {},
6811
+ ...remote.checkpointRepair ? { checkpointRepair: remote.checkpointRepair } : {}
6059
6812
  };
6813
+ const candidates = unacknowledgedWarnings?.get(target.namespace);
6814
+ let warningAcknowledgments;
6815
+ if (candidates !== void 0) {
6816
+ const missing = candidates.filter((candidate) => candidate.migrationNumber > remote.remoteMigrationNumber);
6817
+ warningAcknowledgments = {
6818
+ valid: missing.length === 0,
6819
+ missing
6820
+ };
6821
+ }
6060
6822
  return {
6061
6823
  namespace: target.namespace,
6062
- valid: !localSchema.hasDiff && !remoteSchema.hasDrift && !checkpointMissingLocal,
6824
+ valid: !localSchema.hasDiff && !remoteSchema.hasDrift && !checkpointMissingLocal && warningAcknowledgments?.valid !== false,
6063
6825
  migrationFiles: { valid: true },
6064
6826
  localSchema,
6065
- remoteSchema
6827
+ remoteSchema,
6828
+ ...warningAcknowledgments ? { warningAcknowledgments } : {}
6066
6829
  };
6067
6830
  });
6068
6831
  }
@@ -6078,7 +6841,7 @@ async function collectValidationReports(options) {
6078
6841
  const targetNamespaces = options.namespace ? namespacesWithMigrations.filter((ns) => ns.namespace === options.namespace) : namespacesWithMigrations;
6079
6842
  if (targetNamespaces.length === 0) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
6080
6843
  const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
6081
- const { defineApplication } = await import("../application-CXNaUNhv.mjs");
6844
+ const { defineApplication } = await import("../application-BxLLiLsr.mjs");
6082
6845
  const application = defineApplication({
6083
6846
  config,
6084
6847
  pluginManager
@@ -6093,10 +6856,12 @@ async function collectValidationReports(options) {
6093
6856
  const typesByNamespace = new Map(tailorDBInputs.map((input) => [input.namespace, input.types]));
6094
6857
  const migrationFileErrors = /* @__PURE__ */ new Map();
6095
6858
  const checkableNamespaces = [];
6859
+ const unacknowledgedWarnings = options.strict ? /* @__PURE__ */ new Map() : void 0;
6096
6860
  for (const target of targetNamespaces) try {
6097
6861
  assertValidMigrationFiles(target.migrationsDir, target.namespace);
6098
6862
  reconstructSnapshotFromMigrations(target.migrationsDir);
6099
- assertRequiredMigrationScripts(target.migrationsDir, target.namespace);
6863
+ const namespaceWarnings = assertMigrationScriptsReady(target.migrationsDir, target.namespace);
6864
+ unacknowledgedWarnings?.set(target.namespace, namespaceWarnings);
6100
6865
  checkableNamespaces.push(target);
6101
6866
  } catch (error) {
6102
6867
  migrationFileErrors.set(target.namespace, error instanceof Error ? error.message : String(error));
@@ -6104,7 +6869,8 @@ async function collectValidationReports(options) {
6104
6869
  const localReportOptions = {
6105
6870
  targetNamespaces,
6106
6871
  migrationFileErrors,
6107
- localResults: await checkMigrationDiffs(typesByNamespace, checkableNamespaces)
6872
+ localResults: await checkMigrationDiffs(typesByNamespace, checkableNamespaces),
6873
+ ...unacknowledgedWarnings ? { unacknowledgedWarnings } : {}
6108
6874
  };
6109
6875
  if (checkableNamespaces.length === 0) return { reports: buildValidationReports(localReportOptions) };
6110
6876
  let remoteResults;
@@ -6143,6 +6909,7 @@ function printValidationReports(reports) {
6143
6909
  else logger.log(` Local schema: ${styles.success("OK")}`);
6144
6910
  const remote = report.remoteSchema;
6145
6911
  if (remote?.skipped === "check_failed") logger.log(` Remote schema: ${styles.error("not checked")}`);
6912
+ else if (remote?.checkpointRepair) logger.log(` Remote schema: ${styles.success("OK")} (next deploy will reset the checkpoint to 0000 from ${formatMigrationNumber(remote.checkpointRepair.from)})`);
6146
6913
  else if (remote?.checkpointMissingLocal) logger.log(` Remote schema: ${styles.error(`remote migration ${formatMigrationNumber(remote.remoteMigrationNumber)} is not in the local migration history`)}`);
6147
6914
  else if (remote?.hasDrift) {
6148
6915
  logger.log(` Remote schema: ${styles.error("drift detected")} (remote migration: ${formatMigrationNumber(remote.remoteMigrationNumber)})`);
@@ -6151,14 +6918,36 @@ function printValidationReports(reports) {
6151
6918
  else if (remote?.skipped === "no_migration_label") logger.log(` Remote schema: ${styles.dim("skipped (deployed namespace has no migration state)")}`);
6152
6919
  else if (remote?.skipped === "no_snapshot") logger.log(` Remote schema: ${styles.dim(`skipped (no local snapshot for remote migration ${formatMigrationNumber(remote.remoteMigrationNumber)})`)}`);
6153
6920
  else if (remote) logger.log(` Remote schema: ${styles.success("OK")} (remote migration: ${formatMigrationNumber(remote.remoteMigrationNumber)})`);
6921
+ const acknowledgments = report.warningAcknowledgments;
6922
+ if (acknowledgments) if (acknowledgments.valid) logger.log(` Warning acknowledgments: ${styles.success("OK")}`);
6923
+ else {
6924
+ logger.log(` Warning acknowledgments: ${styles.error("missing for pending migration(s) with possible data loss")}`);
6925
+ for (const migration of acknowledgments.missing) {
6926
+ const label = formatMigrationNumber(migration.migrationNumber);
6927
+ for (const warning of migration.warnings) {
6928
+ const field = warning.fieldName ? `.${warning.fieldName}` : "";
6929
+ logger.log(` ${label}: ${warning.typeName}${field}: ${warning.reason}`);
6930
+ }
6931
+ }
6932
+ }
6154
6933
  }
6155
6934
  logger.newline();
6156
6935
  }
6157
- function printResolutionHints(reports) {
6936
+ function printResolutionHints(reports, configPath) {
6158
6937
  if (reports.some((r) => r.localSchema?.hasDiff && !r.localSchema.diff)) logger.info("Run 'tailor tailordb migration generate' to create the initial snapshot.");
6159
6938
  if (reports.some((r) => r.localSchema?.diff)) logger.info("Run 'tailor tailordb migration generate' to create migration files.");
6160
6939
  if (reports.some((r) => r.remoteSchema?.checkpointMissingLocal)) logger.info("The remote migration checkpoint is ahead of the local history. Pull the latest migration files, or run 'tailor tailordb migration status' to compare.");
6161
6940
  if (reports.some((r) => r.remoteSchema?.hasDrift)) logRemoteDriftGuidance();
6941
+ const missingAcknowledgments = reports.filter((r) => r.warningAcknowledgments && !r.warningAcknowledgments.valid);
6942
+ if (missingAcknowledgments.length > 0) {
6943
+ logger.info("For each migration listed, either add a data migration script with 'tailor tailordb migration script <number>' or record why none is needed:");
6944
+ for (const report of missingAcknowledgments) for (const migration of report.warningAcknowledgments?.missing ?? []) logger.info(` ${formatMigrationScriptCommand({
6945
+ migrationNumber: migration.migrationNumber,
6946
+ namespace: report.namespace,
6947
+ configPath,
6948
+ noScript: true
6949
+ })}`, { mode: "plain" });
6950
+ }
6162
6951
  }
6163
6952
  /**
6164
6953
  * Validate migration files and schema state without deploying
@@ -6173,20 +6962,21 @@ async function validate(options) {
6173
6962
  else {
6174
6963
  printValidationReports(reports);
6175
6964
  if (invalidCount === 0 && !("remoteError" in collected)) logger.success("All migration validation checks passed.");
6176
- else printResolutionHints(reports);
6965
+ else printResolutionHints(reports, options.configPath);
6177
6966
  }
6178
6967
  if ("remoteError" in collected) throw collected.remoteError;
6179
6968
  if (invalidCount > 0) throw new Error(`Migration validation failed for ${invalidCount} namespace(s)`);
6180
6969
  }
6181
6970
  const validateCommand = defineAppCommand({
6182
6971
  name: "validate",
6183
- description: "Validate the full migration history and detect schema drift (local types vs. migration snapshot, remote schema vs. migration checkpoint) without deploying. This includes the migration and schema-drift checks used by 'deploy' and exits with a non-zero code when issues are found.",
6972
+ description: "Validate the full migration history, unreviewed generated migration scripts, and schema drift (local types vs. migration snapshot, remote schema vs. migration checkpoint) without deploying. This includes the migration and schema-drift checks used by 'deploy' and exits with a non-zero code when issues are found.",
6184
6973
  args: z.strictObject({
6185
6974
  ...deploymentArgs,
6186
6975
  namespace: arg(z.string().optional(), {
6187
6976
  alias: "n",
6188
6977
  description: "Target TailorDB namespace (validates all namespaces if not specified)"
6189
- })
6978
+ }),
6979
+ strict: arg(z.boolean().default(false), { description: "Also fail when a pending migration can drop data without an acknowledgment" })
6190
6980
  }),
6191
6981
  run: async (args) => {
6192
6982
  await validate({
@@ -6194,7 +6984,8 @@ const validateCommand = defineAppCommand({
6194
6984
  namespace: args.namespace,
6195
6985
  workspaceId: args["workspace-id"],
6196
6986
  profile: args.profile,
6197
- json: logger.jsonMode
6987
+ json: logger.jsonMode,
6988
+ strict: args.strict
6198
6989
  });
6199
6990
  }
6200
6991
  });
@@ -6206,10 +6997,12 @@ const validateCommand = defineAppCommand({
6206
6997
  *
6207
6998
  * Subcommands:
6208
6999
  * - generate: Generate migration files from schema differences
7000
+ * - rebaseline: Collapse the full migration history into a new baseline
6209
7001
  * - script: Add a migrate.ts template to an existing migration
6210
7002
  * - set: Set migration checkpoint to a specific number
6211
7003
  * - status: Show migration status for TailorDB namespaces
6212
7004
  * - sync: Sync remote TailorDB schema to a specific migration snapshot
7005
+ * - test: Test pending migrations in an isolated workspace
6213
7006
  * - validate: Validate migration files and detect schema drift without deploying
6214
7007
  */
6215
7008
  const migrationCommand = defineCommand({
@@ -6217,10 +7010,12 @@ const migrationCommand = defineCommand({
6217
7010
  description: "Manage TailorDB schema migrations.",
6218
7011
  subCommands: {
6219
7012
  generate: generateCommand$1,
7013
+ rebaseline: rebaselineCommand,
6220
7014
  script: scriptCommand,
6221
7015
  set: setCommand,
6222
7016
  status: statusCommand,
6223
7017
  sync: syncCommand,
7018
+ test: testCommand,
6224
7019
  validate: validateCommand
6225
7020
  }
6226
7021
  });
@@ -6256,7 +7051,7 @@ const upgradeCommand = defineAppCommand({
6256
7051
  run: async (args) => {
6257
7052
  const { initTelemetry } = await import("../telemetry-CkbkeJxl.mjs");
6258
7053
  await initTelemetry();
6259
- const { upgrade } = await import("../service-B5WynBHQ.mjs");
7054
+ const { upgrade } = await import("../service-CCwl3Avt.mjs");
6260
7055
  await upgrade({
6261
7056
  from: args.from,
6262
7057
  dryRun: args["dry-run"],
@@ -6763,7 +7558,7 @@ runMain(mainCommand, {
6763
7558
  if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
6764
7559
  } else logger.error(`Unknown error: ${error}`);
6765
7560
  if (!isCLIError(error) && (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError)) {
6766
- const { reportCrash } = await import("../crashreport-eI7ty7ok.mjs");
7561
+ const { reportCrash } = await import("../crashreport-CINZ5dVN.mjs");
6767
7562
  await reportCrash(error, "handledError");
6768
7563
  }
6769
7564
  }