@tailor-platform/sdk 1.73.2 → 1.74.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 (67) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/application-BndtMTFY.mjs +3 -0
  3. package/dist/{application-Bb9NNp5m.mjs → application-DwmoI6ZQ.mjs} +231 -153
  4. package/dist/application-DwmoI6ZQ.mjs.map +1 -0
  5. package/dist/cli/commands/deploy/apply-phases.d.mts +1 -0
  6. package/dist/cli/commands/executor/trigger.d.mts +1 -1
  7. package/dist/cli/index.mjs +102 -13
  8. package/dist/cli/index.mjs.map +1 -1
  9. package/dist/cli/lib.d.mts +2 -2
  10. package/dist/cli/lib.mjs +2 -2
  11. package/dist/cli/shared/client.d.mts +1 -1
  12. package/dist/completion/zsh-worker.zsh +28 -3
  13. package/dist/{repl-editor-BmQOyZJd.mjs → repl-editor-BCozyiNq.mjs} +3 -2
  14. package/dist/{repl-editor-BmQOyZJd.mjs.map → repl-editor-BCozyiNq.mjs.map} +1 -1
  15. package/dist/{runtime-B8O6JFS2.mjs → runtime-Zkg3SUwi.mjs} +1974 -1217
  16. package/dist/runtime-Zkg3SUwi.mjs.map +1 -0
  17. package/dist/{service-DiEtAsvc.mjs → service-BCRJ-3NV.mjs} +18 -1
  18. package/dist/{service-DiEtAsvc.mjs.map → service-BCRJ-3NV.mjs.map} +1 -1
  19. package/dist/service-CPMlIg3X.mjs +3 -0
  20. package/dist/tailor-proto/src/tailor/v1/aigateway_pb.d.mts +2 -2
  21. package/dist/tailor-proto/src/tailor/v1/aigateway_resource_pb.d.mts +1 -1
  22. package/dist/tailor-proto/src/tailor/v1/application_pb.d.mts +1 -1
  23. package/dist/tailor-proto/src/tailor/v1/application_resource_pb.d.mts +1 -1
  24. package/dist/tailor-proto/src/tailor/v1/auth_pb.d.mts +1 -1
  25. package/dist/tailor-proto/src/tailor/v1/auth_resource_pb.d.mts +1 -1
  26. package/dist/tailor-proto/src/tailor/v1/events_pb.d.mts +1 -1
  27. package/dist/tailor-proto/src/tailor/v1/events_resource_pb.d.mts +1 -1
  28. package/dist/tailor-proto/src/tailor/v1/executor_pb.d.mts +1 -1
  29. package/dist/tailor-proto/src/tailor/v1/executor_resource_pb.d.mts +1 -1
  30. package/dist/tailor-proto/src/tailor/v1/function_pb.d.mts +1 -1
  31. package/dist/tailor-proto/src/tailor/v1/function_registry_pb.d.mts +1 -1
  32. package/dist/tailor-proto/src/tailor/v1/function_resource_pb.d.mts +1 -1
  33. package/dist/tailor-proto/src/tailor/v1/http_adapter_resource_pb.d.mts +1 -1
  34. package/dist/tailor-proto/src/tailor/v1/idp_pb.d.mts +1 -1
  35. package/dist/tailor-proto/src/tailor/v1/idp_resource_pb.d.mts +1 -1
  36. package/dist/tailor-proto/src/tailor/v1/metadata_pb.d.mts +1 -1
  37. package/dist/tailor-proto/src/tailor/v1/metadata_resource_pb.d.mts +1 -1
  38. package/dist/tailor-proto/src/tailor/v1/meter_pb.d.mts +1 -1
  39. package/dist/tailor-proto/src/tailor/v1/pipeline_pb.d.mts +1 -1
  40. package/dist/tailor-proto/src/tailor/v1/pipeline_resource_pb.d.mts +1 -1
  41. package/dist/tailor-proto/src/tailor/v1/resource_pb.d.mts +1 -1
  42. package/dist/tailor-proto/src/tailor/v1/secret_manager_pb.d.mts +1 -1
  43. package/dist/tailor-proto/src/tailor/v1/secret_manager_resource_pb.d.mts +1 -1
  44. package/dist/tailor-proto/src/tailor/v1/service_pb.d.mts +2 -2
  45. package/dist/tailor-proto/src/tailor/v1/stateflow_pb.d.mts +1 -1
  46. package/dist/tailor-proto/src/tailor/v1/stateflow_resource_pb.d.mts +1 -1
  47. package/dist/tailor-proto/src/tailor/v1/staticwebsite_pb.d.mts +1 -1
  48. package/dist/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.d.mts +1 -1
  49. package/dist/tailor-proto/src/tailor/v1/tailordb_pb.d.mts +1 -1
  50. package/dist/tailor-proto/src/tailor/v1/tailordb_resource_pb.d.mts +1 -1
  51. package/dist/tailor-proto/src/tailor/v1/telemetryrouter_pb.d.mts +1 -1
  52. package/dist/tailor-proto/src/tailor/v1/telemetryrouter_resource_pb.d.mts +1 -1
  53. package/dist/tailor-proto/src/tailor/v1/workflow_pb.d.mts +1 -1
  54. package/dist/tailor-proto/src/tailor/v1/workflow_resource_pb.d.mts +1 -1
  55. package/dist/tailor-proto/src/tailor/v1/workspace_pb.d.mts +2 -2
  56. package/dist/tailor-proto/src/tailor/v1/workspace_resource_pb.d.mts +1 -1
  57. package/dist/vitest/index.mjs +2 -1
  58. package/dist/vitest/index.mjs.map +1 -1
  59. package/docs/cli/application.md +21 -11
  60. package/docs/cli/setup.md +25 -0
  61. package/docs/cli-reference.md +1 -0
  62. package/docs/services/tailordb.md +23 -0
  63. package/package.json +8 -8
  64. package/dist/application-BXdCl6bp.mjs +0 -3
  65. package/dist/application-Bb9NNp5m.mjs.map +0 -1
  66. package/dist/runtime-B8O6JFS2.mjs.map +0 -1
  67. package/dist/service-BXmdFwSH.mjs +0 -3
@@ -2,8 +2,8 @@ import { t as db } from "./schema-DAdZSFOd.mjs";
2
2
  import { $ as CreateUserProfileConfigRequestSchema, A as UpdatePipelineServiceRequestSchema, At as Condition_Operator, B as UpdateExecutorExecutorRequestSchema, Ct as UserProfileProviderConfig_UserProfileProviderType, D as CreatePipelineResolverRequestSchema, Dt as ApplicationSchemaUpdateAttemptStatus, E as UpdateSecretManagerSecretRequestSchema, Et as UpdateApplicationRequestSchema, F as IdPPermissionOperator, G as CreateAuthHookRequestSchema, H as ExecutorTargetType, I as IdPPermissionPermit, J as CreateAuthOAuth2ClientRequestSchema, K as CreateAuthIDPConfigRequestSchema, L as FunctionExecution_Status, M as CreateIdPServiceRequestSchema, Mt as PageDirection, N as UpdateIdPServiceRequestSchema, O as CreatePipelineServiceRequestSchema, Ot as Subgraph_ServiceType, P as IdPLang, Q as CreateTenantConfigRequestSchema, S as UpdateStaticWebsiteRequestSchema, St as TenantProviderConfig_TenantProviderType, T as CreateSecretManagerVaultRequestSchema, Tt as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, U as ExecutorTriggerType, V as ExecutorJobStatus, W as CreateAuthConnectionRequestSchema, X as CreateAuthSCIMResourceRequestSchema, Y as CreateAuthSCIMConfigRequestSchema, Z as CreateAuthServiceRequestSchema, _ as TailorDBType_Permission_Operator, _t as AuthSCIMAttribute_Mutability, a as WorkspacePlatformUserRole, at as UpdateAuthSCIMConfigRequestSchema, b as AddCustomDomainRequestSchema, bt as AuthSCIMConfig_AuthorizationType, c as UpdateWorkflowRequestSchema, ct as UpdateTenantConfigRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as AuthConnection_Type, et as UpdateAuthConnectionRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as AuthHookPoint, g as TailorDBGQLPermission_Permit, gt as AuthOAuth2Client_GrantType, h as TailorDBGQLPermission_Operator, ht as AuthOAuth2Client_ClientType, it as UpdateAuthOAuth2ClientRequestSchema, j as PipelineResolver_OperationType, jt as FilterSchema, k as UpdatePipelineResolverRequestSchema, kt as ConditionSchema, l as WorkflowExecution_Status, lt as UpdateUserProfileConfigRequestSchema, m as TailorDBGQLPermission_Action, mt as AuthInvokerSchema, nt as UpdateAuthIDPConfigRequestSchema, o as CreateWorkflowJobFunctionRequestSchema, ot as UpdateAuthSCIMResourceRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as AuthIDPConfig_AuthType, q as CreateAuthMachineUserRequestSchema, rt as UpdateAuthMachineUserRequestSchema, s as CreateWorkflowRequestSchema, st as UpdateAuthServiceRequestSchema, t as OperatorService, tt as UpdateAuthHookRequestSchema, u as WorkflowJobExecution_Status, ut as AuthConnection_Status, v as TailorDBType_Permission_Permit, vt as AuthSCIMAttribute_Type, w as CreateSecretManagerSecretRequestSchema, wt as CreateApplicationRequestSchema, x as CreateStaticWebsiteRequestSchema, y as TailorDBType_PermitAction, yt as AuthSCIMAttribute_Uniqueness, z as CreateExecutorExecutorRequestSchema } from "./service_pb-CIrhGwHk.mjs";
3
3
  import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
4
4
  import { a as parseBoolean, i as symbols, n as logger, r as styles, t as CIPromptError } from "./logger-BEiZZ3qT.mjs";
5
- import { $ as initOperatorClient, A as loadAccessToken, G as fetchMachineUserToken, I as loadWorkspaceId, K as fetchPaged, L as platformConfigFromProfile, N as loadMachineUserName, P as loadPlatformClientConfig, R as readPlatformConfig, S as getDistDir, T as loadConfig, V as writePlatformConfig, W as fetchAll, X as getOAuth2ClientId, Y as getConsoleBaseUrl, Z as getPlatformBaseUrl, b as hasGenerationHooks, d as assertUniqueLocalTailorDBTypeNames, et as isDefaultPlatform, f as assertUniqueTailorDBTypeNamesWithExternal, h as platformBundleDefinePlugin, j as loadConfigPath, k as hasUserTokenEntry, l as buildExecutorArgsExpr, m as stringifyFunction, n as generatePluginFilesIfNeeded, nt as byName, p as TailorDBTypeSchema, r as loadApplication, s as HTTP_METHODS, t as defineApplication, tt as resolveStaticWebsiteUrls, u as buildResolverOperationHookExpr, w as hashFile, x as createBundleCache, y as getPluginGenerationDependencies } from "./application-Bb9NNp5m.mjs";
6
- import { o as loadFilesWithIgnores, t as createExecutorService } from "./service-DiEtAsvc.mjs";
5
+ import { A as hasUserTokenEntry, C as getDistDir, E as loadConfig, F as loadPlatformClientConfig, G as fetchAll, H as writePlatformConfig, K as fetchMachineUserToken, L as loadWorkspaceId, M as loadConfigPath, P as loadMachineUserName, Q as getPlatformBaseUrl, R as platformConfigFromProfile, S as createBundleCache, T as hashFile, X as getConsoleBaseUrl, Z as getOAuth2ClientId, b as getPluginGenerationDependencies, c as HTTP_METHODS, d as buildResolverOperationHookExpr, et as initOperatorClient, f as assertUniqueLocalTailorDBTypeNames, g as platformBundleDefinePlugin, h as stringifyFunction, i as resolverBundleKey, j as loadAccessToken, m as TailorDBTypeSchema, n as generatePluginFilesIfNeeded, nt as resolveStaticWebsiteUrls, p as assertUniqueTailorDBTypeNamesWithExternal, q as fetchPaged, r as loadApplication, rt as byName, t as defineApplication, tt as isDefaultPlatform, u as buildExecutorArgsExpr, w as hashContent, x as hasGenerationHooks, z as readPlatformConfig } from "./application-DwmoI6ZQ.mjs";
6
+ import { o as loadFilesWithIgnores, t as createExecutorService } from "./service-BCRJ-3NV.mjs";
7
7
  import { t as multiline } from "./multiline-sfHpTZZK.mjs";
8
8
  import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
9
9
  import { i as userAgent } from "./secret-file-VSVGy1V0.mjs";
@@ -71,7 +71,9 @@ const durationArg = z.string().refine((val) => durationPattern.test(val), { mess
71
71
  */
72
72
  function parseDuration(duration) {
73
73
  const match = assertDefined(duration.match(durationPattern), `invalid duration format: ${duration}`);
74
- return parseInt(assertDefined(match[1], "duration digits group missing"), 10) * unitToMs[assertDefined(match[2], "duration unit group missing")];
74
+ const value = parseInt(assertDefined(match[1], "duration digits group missing"), 10);
75
+ const unit = assertDefined(match[2], "duration unit group missing");
76
+ return value * unitToMs[unit];
75
77
  }
76
78
  /**
77
79
  * Schema for positive integer validation (from string input)
@@ -197,6 +199,18 @@ const configArg = { config: arg(z.string().default("tailor.config.ts"), {
197
199
  }
198
200
  }) };
199
201
  /**
202
+ * Shared config arg for commands that accept one or more comma-separated config file paths
203
+ */
204
+ const multiConfigArg = { config: arg(z.string().default("tailor.config.ts"), {
205
+ alias: "c",
206
+ description: "Path to SDK config file. Use comma-separated paths to deploy multiple apps together.",
207
+ env: "TAILOR_PLATFORM_SDK_CONFIG_PATH",
208
+ completion: {
209
+ type: "file",
210
+ extensions: ["ts"]
211
+ }
212
+ }) };
213
+ /**
200
214
  * Arguments for commands that interact with deployed resources (includes config)
201
215
  */
202
216
  const deploymentArgs = {
@@ -965,7 +979,7 @@ function createCacheStore(config) {
965
979
  return path.join(config.cacheDir, BUNDLES_DIR);
966
980
  }
967
981
  function bundlePath(cacheKey) {
968
- return path.join(bundlesDir(), `${cacheKey.replaceAll(":", "_")}.js`);
982
+ return path.join(bundlesDir(), `${hashContent(cacheKey)}.js`);
969
983
  }
970
984
  function loadManifest() {
971
985
  try {
@@ -1085,12 +1099,20 @@ function createCacheManager(options) {
1085
1099
  enabled: true,
1086
1100
  bundleCache: createBundleCache(store),
1087
1101
  finalize() {
1088
- const manifest = store.getCurrentManifest() ?? {
1102
+ const currentManifest = store.getCurrentManifest() ?? {
1089
1103
  version: 1,
1090
1104
  sdkVersion: options.sdkVersion,
1091
1105
  lockfileHash: options.lockfileHash,
1092
1106
  entries: {}
1093
1107
  };
1108
+ const latestManifest = store.loadManifest();
1109
+ const manifest = latestManifest?.sdkVersion === options.sdkVersion && latestManifest.lockfileHash === options.lockfileHash ? {
1110
+ ...latestManifest,
1111
+ entries: {
1112
+ ...latestManifest.entries,
1113
+ ...currentManifest.entries
1114
+ }
1115
+ } : currentManifest;
1094
1116
  manifest.sdkVersion = options.sdkVersion;
1095
1117
  manifest.lockfileHash = options.lockfileHash;
1096
1118
  store.saveManifest(manifest);
@@ -1894,6 +1916,210 @@ function trackRemainingResourceOwner(params) {
1894
1916
  return owned;
1895
1917
  }
1896
1918
 
1919
+ //#endregion
1920
+ //#region src/cli/commands/deploy/staticwebsite.ts
1921
+ /**
1922
+ * Apply static website changes for the given phase.
1923
+ * @param client - Operator client instance
1924
+ * @param result - Planned static website changes
1925
+ * @param phase - Apply phase
1926
+ * @returns Promise that resolves when static websites are applied
1927
+ */
1928
+ async function applyStaticWebsite(client, result, phase = "create-update") {
1929
+ const { changeSet, customDomainChangeSet } = result;
1930
+ if (phase === "create-update") {
1931
+ await Promise.all([...changeSet.creates.map(async (create) => {
1932
+ await client.createStaticWebsite(create.request);
1933
+ await client.setMetadata(create.metaRequest);
1934
+ }), ...changeSet.updates.map(async (update) => {
1935
+ await client.updateStaticWebsite(update.request);
1936
+ await client.setMetadata(update.metaRequest);
1937
+ })]);
1938
+ await Promise.all([...customDomainChangeSet.creates.map(async (add) => {
1939
+ await client.addCustomDomain(add.request);
1940
+ await client.setMetadata(add.metaRequest);
1941
+ }), ...customDomainChangeSet.deletes.map((del) => client.removeCustomDomain(del.request))]);
1942
+ } else await Promise.all(changeSet.deletes.map((del) => client.deleteStaticWebsite(del.request)));
1943
+ }
1944
+ function customDomainTrn(workspaceId, websiteName, domain) {
1945
+ return `trn:v1:workspace:${workspaceId}:staticwebsite:${websiteName}:custom_domain:${domain}`;
1946
+ }
1947
+ function normalizeComparableStaticWebsiteShape(input) {
1948
+ return {
1949
+ description: input.description,
1950
+ allowedIpAddresses: input.allowedIpAddresses.toSorted()
1951
+ };
1952
+ }
1953
+ function normalizeComparableStaticWebsite(input) {
1954
+ return normalizeComparableStaticWebsiteShape({
1955
+ description: input.description || "",
1956
+ allowedIpAddresses: [...input.allowedIpAddresses || []]
1957
+ });
1958
+ }
1959
+ function areStaticWebsitesEqual(existing, desired) {
1960
+ return areNormalizedEqual(normalizeComparableStaticWebsite(existing), normalizeComparableStaticWebsite(desired));
1961
+ }
1962
+ /**
1963
+ * Static website names expected to exist after this deploy run.
1964
+ * @param context - Planning context
1965
+ * @returns Names from the deploy-run scope when set, otherwise the application's own websites
1966
+ */
1967
+ function expectedLocalStaticWebsiteNames(context) {
1968
+ return context.expectedLocalStaticWebsiteNames ?? new Set(context.application.staticWebsiteServices.map((website) => website.name));
1969
+ }
1970
+ /**
1971
+ * Plan static website changes based on current and desired state.
1972
+ * @param context - Planning context
1973
+ * @returns Planned changes
1974
+ */
1975
+ async function planStaticWebsite(context) {
1976
+ const { client, workspaceId, application, forRemoval } = context;
1977
+ const changeSet = createChangeSet("StaticWebsites");
1978
+ const customDomainChangeSet = createChangeSet("CustomDomains");
1979
+ const conflicts = [];
1980
+ const unmanaged = [];
1981
+ const resourceOwners = /* @__PURE__ */ new Set();
1982
+ const existingWebsites = await fetchExistingResourcesWithLabels({
1983
+ client,
1984
+ fetchPage: async (pageToken, pageSize) => {
1985
+ const { staticwebsites, nextPageToken } = await client.listStaticWebsites({
1986
+ workspaceId,
1987
+ pageToken,
1988
+ pageSize
1989
+ });
1990
+ return [staticwebsites, nextPageToken];
1991
+ },
1992
+ getName: (resource) => resource.name,
1993
+ getTrn: (name) => resourceTrn(workspaceId, "staticwebsite", name)
1994
+ });
1995
+ const ownedWebsiteNames = /* @__PURE__ */ new Set();
1996
+ const staticWebsiteServices = forRemoval ? [] : application.staticWebsiteServices;
1997
+ for (const websiteService of staticWebsiteServices) {
1998
+ const config = websiteService;
1999
+ const name = websiteService.name;
2000
+ const existing = existingWebsites[name];
2001
+ const metaRequest = await buildMetaRequest({
2002
+ trn: resourceTrn(workspaceId, "staticwebsite", name),
2003
+ appName: application.name,
2004
+ appId: application.id
2005
+ });
2006
+ const desired = normalizeComparableStaticWebsite(config);
2007
+ const request = {
2008
+ workspaceId,
2009
+ staticwebsite: {
2010
+ name,
2011
+ description: config.description || "",
2012
+ allowedIpAddresses: config.allowedIpAddresses || []
2013
+ }
2014
+ };
2015
+ if (existing) {
2016
+ const owned = trackDesiredResourceOwnership({
2017
+ labels: existing.allLabels,
2018
+ ownerLabel: existing.label,
2019
+ appName: application.name,
2020
+ appId: application.id,
2021
+ resourceType: "StaticWebsite",
2022
+ resourceName: name,
2023
+ conflicts,
2024
+ unmanaged
2025
+ });
2026
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areStaticWebsitesEqual(existing.resource, desired)) changeSet.unchanged.push({ name });
2027
+ else changeSet.updates.push({
2028
+ name,
2029
+ request,
2030
+ metaRequest
2031
+ });
2032
+ if (owned) ownedWebsiteNames.add(name);
2033
+ delete existingWebsites[name];
2034
+ } else {
2035
+ changeSet.creates.push({
2036
+ name,
2037
+ request,
2038
+ metaRequest
2039
+ });
2040
+ ownedWebsiteNames.add(name);
2041
+ }
2042
+ }
2043
+ Object.entries(existingWebsites).forEach(([name]) => {
2044
+ const entry = existingWebsites[name];
2045
+ const label = entry?.label;
2046
+ if (trackRemainingResourceOwner({
2047
+ labels: entry?.allLabels,
2048
+ ownerLabel: label,
2049
+ appName: application.name,
2050
+ appId: application.id,
2051
+ resourceOwners
2052
+ })) changeSet.deletes.push({
2053
+ name,
2054
+ request: {
2055
+ workspaceId,
2056
+ name
2057
+ }
2058
+ });
2059
+ });
2060
+ const desiredDomainsByWebsite = /* @__PURE__ */ new Map();
2061
+ for (const service of staticWebsiteServices) if (service.customDomains !== void 0 && ownedWebsiteNames.has(service.name)) desiredDomainsByWebsite.set(service.name, service.customDomains);
2062
+ const existingDomainsByWebsite = /* @__PURE__ */ new Map();
2063
+ const websitesToFetchDomains = [...ownedWebsiteNames].filter((name) => !changeSet.creates.some((c) => c.name === name));
2064
+ await Promise.all(websitesToFetchDomains.map(async (name) => {
2065
+ try {
2066
+ const { customDomains } = await client.listCustomDomains({
2067
+ workspaceId,
2068
+ staticWebsiteName: name
2069
+ });
2070
+ const domainsWithLabels = await Promise.all(customDomains.map(async (d) => {
2071
+ const { metadata } = await client.getMetadata({ trn: customDomainTrn(workspaceId, name, d.domain) });
2072
+ return {
2073
+ domain: d.domain,
2074
+ allLabels: metadata?.labels
2075
+ };
2076
+ }));
2077
+ existingDomainsByWebsite.set(name, domainsWithLabels);
2078
+ } catch (error) {
2079
+ if (error instanceof ConnectError && error.code === Code.NotFound) return;
2080
+ throw error;
2081
+ }
2082
+ }));
2083
+ for (const name of ownedWebsiteNames) {
2084
+ const desired = new Set(desiredDomainsByWebsite.get(name) ?? []);
2085
+ const existingDomains = existingDomainsByWebsite.get(name) ?? [];
2086
+ const existingSet = new Set(existingDomains.map((d) => d.domain));
2087
+ const sdkOwnedDomains = new Set(existingDomains.filter((d) => isOwnedByApp(d.allLabels, application.name, application.id)).map((d) => d.domain));
2088
+ for (const domain of desired) if (!existingSet.has(domain)) {
2089
+ const metaRequest = await buildMetaRequest({
2090
+ trn: customDomainTrn(workspaceId, name, domain),
2091
+ appName: application.name,
2092
+ appId: application.id
2093
+ });
2094
+ customDomainChangeSet.creates.push({
2095
+ name: domain,
2096
+ request: {
2097
+ workspaceId,
2098
+ staticWebsiteName: name,
2099
+ domain
2100
+ },
2101
+ metaRequest
2102
+ });
2103
+ } else customDomainChangeSet.unchanged.push({ name: domain });
2104
+ if (desiredDomainsByWebsite.has(name)) {
2105
+ for (const domain of sdkOwnedDomains) if (!desired.has(domain)) customDomainChangeSet.deletes.push({
2106
+ name: domain,
2107
+ request: {
2108
+ workspaceId,
2109
+ domain
2110
+ }
2111
+ });
2112
+ }
2113
+ }
2114
+ return {
2115
+ changeSet,
2116
+ customDomainChangeSet,
2117
+ conflicts,
2118
+ unmanaged,
2119
+ resourceOwners
2120
+ };
2121
+ }
2122
+
1897
2123
  //#endregion
1898
2124
  //#region src/cli/commands/deploy/aigateway.ts
1899
2125
  /**
@@ -1956,7 +2182,7 @@ async function planAIGateway(context) {
1956
2182
  getTrn: (name) => resourceTrn(workspaceId, "aigateway", name)
1957
2183
  });
1958
2184
  const aiGatewayServices = forRemoval ? [] : application.aiGatewayServices;
1959
- const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
2185
+ const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
1960
2186
  for (const gatewayService of aiGatewayServices) {
1961
2187
  const config = gatewayService;
1962
2188
  const name = gatewayService.name;
@@ -2156,23 +2382,26 @@ async function planApplication(context, httpAdapterBuildResult) {
2156
2382
  if (idProvider) authIdpConfigName = idProvider.name;
2157
2383
  } else if (application.config.auth) {
2158
2384
  authNamespace = application.config.auth.name;
2159
- const idpConfigs = await fetchAll(async (pageToken, maxPageSize) => {
2160
- try {
2161
- const { idpConfigs, nextPageToken } = await client.listAuthIDPConfigs({
2162
- workspaceId,
2163
- namespaceName: assertDefined(authNamespace, "authNamespace must be set before listing IDP configs"),
2164
- pageToken,
2165
- pageSize: maxPageSize
2166
- });
2167
- return [idpConfigs, nextPageToken];
2168
- } catch (error) {
2169
- if (error instanceof ConnectError && error.code === Code.NotFound) return [[], ""];
2170
- throw error;
2385
+ if (context.externalAuthIdpConfigNames?.has(authNamespace)) authIdpConfigName = context.externalAuthIdpConfigNames.get(authNamespace);
2386
+ else {
2387
+ const idpConfigs = await fetchAll(async (pageToken, maxPageSize) => {
2388
+ try {
2389
+ const { idpConfigs, nextPageToken } = await client.listAuthIDPConfigs({
2390
+ workspaceId,
2391
+ namespaceName: assertDefined(authNamespace, "authNamespace must be set before listing IDP configs"),
2392
+ pageToken,
2393
+ pageSize: maxPageSize
2394
+ });
2395
+ return [idpConfigs, nextPageToken];
2396
+ } catch (error) {
2397
+ if (error instanceof ConnectError && error.code === Code.NotFound) return [[], ""];
2398
+ throw error;
2399
+ }
2400
+ });
2401
+ if (idpConfigs.length > 0) {
2402
+ const [firstConfig] = idpConfigs;
2403
+ if (firstConfig) authIdpConfigName = firstConfig.name;
2171
2404
  }
2172
- });
2173
- if (idpConfigs.length > 0) {
2174
- const [firstConfig] = idpConfigs;
2175
- if (firstConfig) authIdpConfigName = firstConfig.name;
2176
2405
  }
2177
2406
  }
2178
2407
  const metaRequest = await buildMetaRequest({
@@ -2180,7 +2409,7 @@ async function planApplication(context, httpAdapterBuildResult) {
2180
2409
  appName: application.name,
2181
2410
  appId: application.id
2182
2411
  });
2183
- const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
2412
+ const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
2184
2413
  const resolvedCors = await resolveStaticWebsiteUrls(client, workspaceId, application.config.cors, "CORS", { expectedLocalNames: expectedLocalWebsites });
2185
2414
  const httpAdapters = buildHttpAdapters(application, httpAdapterBuildResult);
2186
2415
  const desired = normalizeComparableApplication(application, authNamespace, authIdpConfigName, resolvedCors, httpAdapters);
@@ -2455,14 +2684,15 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
2455
2684
  appId
2456
2685
  });
2457
2686
  if (existing) {
2458
- if (!isOwnedByApp(existing.allLabels, appName, appId)) if (existing.label) conflicts.push({
2687
+ trackDesiredResourceOwnership({
2688
+ labels: existing.allLabels,
2689
+ ownerLabel: existing.label,
2690
+ appName,
2691
+ appId,
2459
2692
  resourceType: "Auth connection",
2460
2693
  resourceName: name,
2461
- currentOwner: existing.label
2462
- });
2463
- else unmanaged.push({
2464
- resourceType: "Auth connection",
2465
- resourceName: name
2694
+ conflicts,
2695
+ unmanaged
2466
2696
  });
2467
2697
  const secretChanged = hashValue(config.clientSecret) !== state.connections?.[name];
2468
2698
  const updateMask = buildUpdateMask(existing.resource, config, secretChanged);
@@ -2488,12 +2718,13 @@ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
2488
2718
  }
2489
2719
  for (const [name, entry] of Object.entries(existingConnections)) {
2490
2720
  if (!entry) continue;
2491
- const owned = isOwnedByApp(entry.allLabels, appName, appId);
2492
- if (entry.label && !owned) {
2493
- resourceOwners.add(entry.label);
2494
- continue;
2495
- }
2496
- if (owned) changeSet.deletes.push({
2721
+ if (trackRemainingResourceOwner({
2722
+ labels: entry.allLabels,
2723
+ ownerLabel: entry.label,
2724
+ appName,
2725
+ appId,
2726
+ resourceOwners
2727
+ })) changeSet.deletes.push({
2497
2728
  name,
2498
2729
  request: {
2499
2730
  workspaceId,
@@ -2656,9 +2887,9 @@ function authHookFunctionName(authName, hookPoint) {
2656
2887
  function collectFunctionEntries(application, workflowJobs, bundledScripts) {
2657
2888
  const entries = [];
2658
2889
  for (const app of application.applications) for (const pipeline of app.resolverServices) for (const resolver of Object.values(pipeline.resolvers)) {
2659
- const content = bundledScripts.resolvers.get(resolver.name);
2890
+ const content = bundledScripts.resolvers.get(resolverBundleKey(pipeline.namespace, resolver.name));
2660
2891
  if (!content) {
2661
- logger.warn(`Bundled code not found for resolver: ${resolver.name}`);
2892
+ logger.warn(`Bundled code not found for resolver: ${pipeline.namespace}/${resolver.name}`);
2662
2893
  continue;
2663
2894
  }
2664
2895
  entries.push({
@@ -2762,15 +2993,15 @@ async function planFunctionRegistry(client, workspaceId, appName, appId, entries
2762
2993
  appId
2763
2994
  });
2764
2995
  if (existing) {
2765
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
2766
- if (!owned) if (!existing.label) unmanaged.push({
2767
- resourceType: "Function registry",
2768
- resourceName: entry.name
2769
- });
2770
- else conflicts.push({
2996
+ const owned = trackDesiredResourceOwnership({
2997
+ labels: existing.allLabels,
2998
+ ownerLabel: existing.label,
2999
+ appName,
3000
+ appId,
2771
3001
  resourceType: "Function registry",
2772
3002
  resourceName: entry.name,
2773
- currentOwner: existing.label
3003
+ conflicts,
3004
+ unmanaged
2774
3005
  });
2775
3006
  if (existing.resource.contentHash === entry.contentHash && owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: entry.name });
2776
3007
  else changeSet.updates.push({
@@ -2787,10 +3018,13 @@ async function planFunctionRegistry(client, workspaceId, appName, appId, entries
2787
3018
  }
2788
3019
  for (const [name, existing] of Object.entries(existingMap)) {
2789
3020
  if (!existing) continue;
2790
- const label = existing.label;
2791
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
2792
- if (label && !owned) resourceOwners.add(label);
2793
- if (owned) changeSet.deletes.push({
3021
+ if (trackRemainingResourceOwner({
3022
+ labels: existing.allLabels,
3023
+ ownerLabel: existing.label,
3024
+ appName,
3025
+ appId,
3026
+ resourceOwners
3027
+ })) changeSet.deletes.push({
2794
3028
  name,
2795
3029
  workspaceId
2796
3030
  });
@@ -3314,7 +3548,7 @@ async function applyIdP(client, result, phase = "create-update") {
3314
3548
  async function planIdP(context) {
3315
3549
  const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets } = context;
3316
3550
  const idps = forRemoval ? [] : application.idpServices;
3317
- const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
3551
+ const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
3318
3552
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set(), expectedLocalWebsites);
3319
3553
  return {
3320
3554
  changeSet: {
@@ -3484,17 +3718,16 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
3484
3718
  permission: protoPermission
3485
3719
  };
3486
3720
  if (existing) {
3487
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
3488
- if (!owned) if (!existing.label) unmanaged.push({
3489
- resourceType: "IdP service",
3490
- resourceName: idp.name
3491
- });
3492
- else conflicts.push({
3721
+ if (trackDesiredResourceOwnership({
3722
+ labels: existing.allLabels,
3723
+ ownerLabel: existing.label,
3724
+ appName,
3725
+ appId,
3493
3726
  resourceType: "IdP service",
3494
3727
  resourceName: idp.name,
3495
- currentOwner: existing.label
3496
- });
3497
- if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({ name: namespaceName });
3728
+ conflicts,
3729
+ unmanaged
3730
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({ name: namespaceName });
3498
3731
  else changeSet.updates.push({
3499
3732
  name: namespaceName,
3500
3733
  request,
@@ -3509,10 +3742,13 @@ async function planServices$3(client, workspaceId, appName, appId, idps, idpUser
3509
3742
  }
3510
3743
  Object.entries(existingServices).forEach(([namespaceName]) => {
3511
3744
  const entry = existingServices[namespaceName];
3512
- const label = entry?.label;
3513
- const owned = isOwnedByApp(entry?.allLabels, appName, appId);
3514
- if (label && !owned) resourceOwners.add(label);
3515
- if (owned) changeSet.deletes.push({
3745
+ if (trackRemainingResourceOwner({
3746
+ labels: entry?.allLabels,
3747
+ ownerLabel: entry?.label,
3748
+ appName,
3749
+ appId,
3750
+ resourceOwners
3751
+ })) changeSet.deletes.push({
3516
3752
  name: namespaceName,
3517
3753
  request: {
3518
3754
  workspaceId,
@@ -3701,7 +3937,7 @@ function protoIdPOperand(operand) {
3701
3937
  */
3702
3938
  async function applyAuth(client, result, phase = "create-update") {
3703
3939
  const { changeSet } = result;
3704
- if (phase === "create-update") {
3940
+ const applyServices = async () => {
3705
3941
  await Promise.all([...changeSet.service.creates.map(async (create) => {
3706
3942
  await client.createAuthService(create.request);
3707
3943
  await client.setMetadata(create.metaRequest);
@@ -3709,6 +3945,11 @@ async function applyAuth(client, result, phase = "create-update") {
3709
3945
  await client.updateAuthService(update.request);
3710
3946
  await client.setMetadata(update.metaRequest);
3711
3947
  })]);
3948
+ };
3949
+ const applyMachineUsers = async () => {
3950
+ await Promise.all([...changeSet.machineUser.creates.map((create) => client.createAuthMachineUser(create.request)), ...changeSet.machineUser.updates.map((update) => client.updateAuthMachineUser(update.request))]);
3951
+ };
3952
+ const applyCreateUpdateDependents = async () => {
3712
3953
  await applyAuthConnections(client, { changeSet: changeSet.connection }, "create-update");
3713
3954
  await Promise.all([...changeSet.idpConfig.creates.map(async (create) => {
3714
3955
  if (create.idpConfig.kind === "BuiltInIdP") assertDefined(create.request.idpConfig, "request missing idpConfig").config = await protoBuiltinIdPConfig(client, assertDefined(create.request.workspaceId, "request missing workspaceId"), create.idpConfig);
@@ -3719,7 +3960,6 @@ async function applyAuth(client, result, phase = "create-update") {
3719
3960
  })]);
3720
3961
  await Promise.all([...changeSet.userProfileConfig.creates.map((create) => client.createUserProfileConfig(create.request)), ...changeSet.userProfileConfig.updates.map((update) => client.updateUserProfileConfig(update.request))]);
3721
3962
  await Promise.all([...changeSet.tenantConfig.creates.map((create) => client.createTenantConfig(create.request)), ...changeSet.tenantConfig.updates.map((update) => client.updateTenantConfig(update.request))]);
3722
- await Promise.all([...changeSet.machineUser.creates.map((create) => client.createAuthMachineUser(create.request)), ...changeSet.machineUser.updates.map((update) => client.updateAuthMachineUser(update.request))]);
3723
3963
  await Promise.all([...changeSet.authHook.creates.map((create) => client.createAuthHook(create.request)), ...changeSet.authHook.updates.map((update) => client.updateAuthHook(update.request))]);
3724
3964
  await Promise.all([...changeSet.oauth2Client.creates.map(async (create) => {
3725
3965
  const oauth2Client = assertDefined(create.request.oauth2Client, "request missing oauth2Client");
@@ -3738,7 +3978,13 @@ async function applyAuth(client, result, phase = "create-update") {
3738
3978
  }
3739
3979
  await Promise.all([...changeSet.scim.creates.map((create) => client.createAuthSCIMConfig(create.request)), ...changeSet.scim.updates.map((update) => client.updateAuthSCIMConfig(update.request))]);
3740
3980
  await Promise.all([...changeSet.scimResource.creates.map((create) => client.createAuthSCIMResource(create.request)), ...changeSet.scimResource.updates.map((update) => client.updateAuthSCIMResource(update.request))]);
3741
- } else if (phase === "delete-resources") {
3981
+ };
3982
+ if (phase === "create-update-prerequisites" || phase === "create-update") {
3983
+ await applyServices();
3984
+ await applyMachineUsers();
3985
+ }
3986
+ if (phase === "create-update-dependents" || phase === "create-update") await applyCreateUpdateDependents();
3987
+ if (phase === "delete-resources") {
3742
3988
  await Promise.all(changeSet.scimResource.deletes.map((del) => client.deleteAuthSCIMResource(del.request)));
3743
3989
  await Promise.all(changeSet.scim.deletes.map((del) => client.deleteAuthSCIMConfig(del.request)));
3744
3990
  await Promise.all(changeSet.oauth2Client.deletes.map((del) => client.deleteAuthOAuth2Client(del.request)));
@@ -3748,7 +3994,7 @@ async function applyAuth(client, result, phase = "create-update") {
3748
3994
  await Promise.all(changeSet.userProfileConfig.deletes.map((del) => client.deleteUserProfileConfig(del.request)));
3749
3995
  await Promise.all(changeSet.idpConfig.deletes.map((del) => client.deleteAuthIDPConfig(del.request)));
3750
3996
  await applyAuthConnections(client, { changeSet: changeSet.connection }, "delete-resources");
3751
- } else await Promise.all(changeSet.service.deletes.map((del) => client.deleteAuthService(del.request)));
3997
+ } else if (phase === "delete-services") await Promise.all(changeSet.service.deletes.map((del) => client.deleteAuthService(del.request)));
3752
3998
  }
3753
3999
  /**
3754
4000
  * Plan auth-related changes based on current and desired state.
@@ -3764,7 +4010,7 @@ async function planAuth(context) {
3764
4010
  }
3765
4011
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$2(client, workspaceId, application.name, application.id, auths, forceApplyAll);
3766
4012
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
3767
- const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
4013
+ const expectedLocalWebsites = expectedLocalStaticWebsiteNames(context);
3768
4014
  const [idpConfigChangeSet, userProfileConfigChangeSet, tenantConfigChangeSet, machineUserChangeSet, authHookChangeSet, oauth2ClientChangeSet, scimChangeSet, scimResourceChangeSet, connectionResult] = await Promise.all([
3769
4015
  planIdPConfigs(client, workspaceId, auths, deletedServices, forceApplyAll),
3770
4016
  planUserProfileConfigs(client, workspaceId, auths, deletedServices, forceApplyAll),
@@ -3826,15 +4072,15 @@ async function planServices$2(client, workspaceId, appName, appId, auths, forceA
3826
4072
  publishSessionEvents: config.publishSessionEvents
3827
4073
  };
3828
4074
  if (existing) {
3829
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
3830
- if (!owned) if (!existing.label) unmanaged.push({
3831
- resourceType: "Auth service",
3832
- resourceName: config.name
3833
- });
3834
- else conflicts.push({
4075
+ const owned = trackDesiredResourceOwnership({
4076
+ labels: existing.allLabels,
4077
+ ownerLabel: existing.label,
4078
+ appName,
4079
+ appId,
3835
4080
  resourceType: "Auth service",
3836
4081
  resourceName: config.name,
3837
- currentOwner: existing.label
4082
+ conflicts,
4083
+ unmanaged
3838
4084
  });
3839
4085
  if (!forceApplyAll && existing.resource.publishSessionEvents === (config.publishSessionEvents ?? false) && owned) changeSet.unchanged.push({ name: config.name });
3840
4086
  else changeSet.updates.push({
@@ -3851,10 +4097,13 @@ async function planServices$2(client, workspaceId, appName, appId, auths, forceA
3851
4097
  }
3852
4098
  Object.entries(existingServices).forEach(([namespaceName]) => {
3853
4099
  const entry = existingServices[namespaceName];
3854
- const label = entry?.label;
3855
- const owned = isOwnedByApp(entry?.allLabels, appName, appId);
3856
- if (label && !owned) resourceOwners.add(label);
3857
- if (owned) changeSet.deletes.push({
4100
+ if (trackRemainingResourceOwner({
4101
+ labels: entry?.allLabels,
4102
+ ownerLabel: entry?.label,
4103
+ appName,
4104
+ appId,
4105
+ resourceOwners
4106
+ })) changeSet.deletes.push({
3858
4107
  name: namespaceName,
3859
4108
  request: {
3860
4109
  workspaceId,
@@ -4819,509 +5068,310 @@ async function planAuthHooks(client, workspaceId, auths, deletedServices, forceA
4819
5068
  }
4820
5069
 
4821
5070
  //#endregion
4822
- //#region src/cli/commands/deploy/config-id-injector.ts
4823
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4824
- function findDefineConfigCalls(node, results) {
4825
- if (!node || typeof node !== "object") return;
4826
- const n = node;
4827
- if (n.type === "CallExpression") {
4828
- const ce = n;
4829
- if (ce.callee.type === "Identifier" && ce.callee.name === "defineConfig") {
4830
- const arg = ce.arguments[0];
4831
- const configObj = arg && arg.type === "ObjectExpression" ? arg : null;
4832
- results.push({
4833
- callExpr: ce,
4834
- configObj
4835
- });
4836
- }
4837
- }
4838
- for (const key of Object.keys(n)) {
4839
- const child = n[key];
4840
- if (Array.isArray(child)) for (const c of child) findDefineConfigCalls(c, results);
4841
- else if (child && typeof child === "object") findDefineConfigCalls(child, results);
5071
+ //#region src/cli/commands/deploy/auth-invoker.ts
5072
+ /**
5073
+ * Normalize an authInvoker value to the object form required by the proto payload.
5074
+ *
5075
+ * Accepts either:
5076
+ * - `undefined` returns undefined
5077
+ * - a plain string (machine user name) — expands to `{ namespace, machineUserName }` using `authNamespace`
5078
+ * - an object `{ namespace, machineUserName }` — returned as-is
5079
+ * @param authInvoker - String machine user name or object form
5080
+ * @param authNamespace - Auth service namespace (required when authInvoker is a string)
5081
+ * @param context - Contextual label used in error messages (e.g. `resolver "foo"`)
5082
+ * @returns Object form of auth invoker, or undefined
5083
+ */
5084
+ function normalizeAuthInvoker(authInvoker, authNamespace, context) {
5085
+ if (authInvoker === void 0) return void 0;
5086
+ if (typeof authInvoker === "string") {
5087
+ if (!authNamespace) throw new Error(`${context} uses a string authInvoker ("${authInvoker}"), but no Auth service is configured. Configure an Auth service or use the object form { namespace, machineUserName }.`);
5088
+ return {
5089
+ namespace: authNamespace,
5090
+ machineUserName: authInvoker
5091
+ };
4842
5092
  }
5093
+ return authInvoker;
4843
5094
  }
4844
- function findIdProperty(obj) {
4845
- for (const prop of obj.properties) {
4846
- if (prop.type !== "Property") continue;
4847
- if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? prop.key.value : null) === "id") return prop;
4848
- }
4849
- return null;
5095
+
5096
+ //#endregion
5097
+ //#region src/cli/commands/deploy/executor.ts
5098
+ /**
5099
+ * Apply executor-related changes for the given phase.
5100
+ * @param client - Operator client instance
5101
+ * @param result - Planned executor changes
5102
+ * @param phase - Apply phase (defaults to "create-update")
5103
+ * @returns Promise that resolves when executors are applied
5104
+ */
5105
+ async function applyExecutor(client, result, phase = "create-update") {
5106
+ const { changeSet } = result;
5107
+ if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
5108
+ await client.createExecutorExecutor(create.request);
5109
+ await client.setMetadata(create.metaRequest);
5110
+ }), ...changeSet.updates.map(async (update) => {
5111
+ await client.updateExecutorExecutor(update.request);
5112
+ await client.setMetadata(update.metaRequest);
5113
+ })]);
5114
+ else await Promise.all(changeSet.deletes.map((del) => client.deleteExecutorExecutor(del.request)));
4850
5115
  }
4851
5116
  /**
4852
- * Ensure `tailor.config.ts` has an `id` property on the `defineConfig({...})`
4853
- * argument. Generates a UUID when missing and writes it back to the file.
4854
- * Returns null when the file does not contain a `defineConfig()` call (e.g.
4855
- * a wrapper that re-exports another config).
4856
- * @param configPath - Absolute path to the config file
4857
- * @returns Resolved id and whether it was newly injected, or null if skipped
5117
+ * Plan executor-related changes based on current and desired state.
5118
+ * @param context - Planning context
5119
+ * @returns Planned changes
4858
5120
  */
4859
- async function ensureConfigId(configPath) {
4860
- const source = await fs$1.promises.readFile(configPath, "utf-8");
4861
- const { program } = parseSync(configPath, source);
4862
- const calls = [];
4863
- findDefineConfigCalls(program, calls);
4864
- if (calls.length === 0) return null;
4865
- if (calls.length > 1) throw new Error(`Multiple defineConfig() calls found in ${configPath}. Only one is supported.`);
4866
- const { configObj } = assertDefined(calls[0], "defineConfig call site missing");
4867
- if (!configObj) throw new Error(`defineConfig() argument must be an inline object literal in ${configPath} so the SDK can manage the 'id' field.`);
4868
- const idProp = findIdProperty(configObj);
4869
- if (idProp) {
4870
- const value = idProp.value;
4871
- if (value.type !== "Literal") throw new Error(`'id' field in ${configPath} must be a string literal. To use this config for a separate app, delete it.`);
4872
- const literalValue = value.value;
4873
- if (typeof literalValue !== "string" || literalValue === "") throw new Error(`'id' field in ${configPath} must be a non-empty string literal. To use this config for a separate app, delete it.`);
4874
- if (!uuidRegex.test(literalValue)) throw new Error(`'id' field in ${configPath} must be a UUID. To use this config for a separate app, delete it.`);
4875
- return {
4876
- id: literalValue,
4877
- injected: false
4878
- };
5121
+ async function planExecutor(context) {
5122
+ const { client, workspaceId, application, forRemoval } = context;
5123
+ const changeSet = createChangeSet("Executors");
5124
+ const conflicts = [];
5125
+ const unmanaged = [];
5126
+ const resourceOwners = /* @__PURE__ */ new Set();
5127
+ const existingExecutors = await fetchExistingResourcesWithLabels({
5128
+ client,
5129
+ fetchPage: async (pageToken, pageSize) => {
5130
+ const { executors, nextPageToken } = await client.listExecutorExecutors({
5131
+ workspaceId,
5132
+ pageToken,
5133
+ pageSize
5134
+ });
5135
+ return [executors, nextPageToken];
5136
+ },
5137
+ getName: (resource) => resource.name,
5138
+ getTrn: (name) => resourceTrn(workspaceId, "executor", name)
5139
+ });
5140
+ const executors = forRemoval ? {} : await application.executorService?.loadExecutors() ?? {};
5141
+ for (const executor of Object.values(executors)) {
5142
+ const existing = existingExecutors[executor.name];
5143
+ const metaRequest = await buildMetaRequest({
5144
+ trn: resourceTrn(workspaceId, "executor", executor.name),
5145
+ appName: application.name,
5146
+ appId: application.id
5147
+ });
5148
+ const desiredExecutor = protoExecutor(context, executor);
5149
+ if (existing) {
5150
+ if (trackDesiredResourceOwnership({
5151
+ labels: existing.allLabels,
5152
+ ownerLabel: existing.label,
5153
+ appName: application.name,
5154
+ appId: application.id,
5155
+ resourceType: "Executor",
5156
+ resourceName: executor.name,
5157
+ conflicts,
5158
+ unmanaged
5159
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areExecutorsEqual(existing.resource, desiredExecutor)) changeSet.unchanged.push({ name: executor.name });
5160
+ else changeSet.updates.push({
5161
+ name: executor.name,
5162
+ request: {
5163
+ workspaceId,
5164
+ executor: desiredExecutor
5165
+ },
5166
+ metaRequest
5167
+ });
5168
+ delete existingExecutors[executor.name];
5169
+ } else changeSet.creates.push({
5170
+ name: executor.name,
5171
+ request: {
5172
+ workspaceId,
5173
+ executor: desiredExecutor
5174
+ },
5175
+ metaRequest
5176
+ });
4879
5177
  }
4880
- const id = crypto.randomUUID();
4881
- const newSource = insertIdProperty(source, configObj, id);
4882
- await fs$1.promises.writeFile(configPath, newSource, "utf-8");
4883
- logger.info(`Generated app id and wrote to ${configPath}: ${id}`);
5178
+ Object.entries(existingExecutors).forEach(([name]) => {
5179
+ const entry = existingExecutors[name];
5180
+ const label = entry?.label;
5181
+ if (trackRemainingResourceOwner({
5182
+ labels: entry?.allLabels,
5183
+ ownerLabel: label,
5184
+ appName: application.name,
5185
+ appId: application.id,
5186
+ resourceOwners
5187
+ })) changeSet.deletes.push({
5188
+ name,
5189
+ request: {
5190
+ workspaceId,
5191
+ name
5192
+ }
5193
+ });
5194
+ });
4884
5195
  return {
4885
- id,
4886
- injected: true
5196
+ changeSet,
5197
+ conflicts,
5198
+ unmanaged,
5199
+ resourceOwners
4887
5200
  };
4888
5201
  }
5202
+ function isFunctionBackedExecutor(executor) {
5203
+ return executor?.targetType === ExecutorTargetType.FUNCTION || executor?.targetType === ExecutorTargetType.JOB_FUNCTION;
5204
+ }
4889
5205
  /**
4890
- * Read the resolved `defineConfig({...})` `id` from a config file without
4891
- * mutating it. Returns null when the file has no inline `defineConfig()` call
4892
- * (wrapper/re-export config), and `{ id: null }` when the call exists but has
4893
- * no usable `id` property.
4894
- * @param configPath - Absolute path to the config file
4895
- * @returns The existing id (or null when absent), or null for wrapper configs
5206
+ * Build desired executor configs keyed by executor name from create/update changes.
5207
+ * @param changeSet - Executor create/update changes
5208
+ * @returns Executor configs keyed by name
4896
5209
  */
4897
- async function readConfigId(configPath) {
4898
- const { program } = parseSync(configPath, await fs$1.promises.readFile(configPath, "utf-8"));
4899
- const calls = [];
4900
- findDefineConfigCalls(program, calls);
4901
- if (calls.length === 0) return null;
4902
- if (calls.length > 1) throw new Error(`Multiple defineConfig() calls found in ${configPath}. Only one is supported.`);
4903
- const { configObj } = assertDefined(calls[0], "defineConfig call site missing");
4904
- if (!configObj) throw new Error(`defineConfig() argument must be an inline object literal in ${configPath} so the SDK can manage the 'id' field.`);
4905
- const idProp = findIdProperty(configObj);
4906
- if (!idProp || idProp.value.type !== "Literal") return { id: null };
4907
- const value = idProp.value.value;
4908
- return { id: typeof value === "string" && value !== "" ? value : null };
5210
+ function buildPlannedExecutorsByName(changeSet) {
5211
+ return Object.fromEntries([...changeSet.creates, ...changeSet.updates].map((item) => [item.name, item.request.executor]));
4909
5212
  }
4910
5213
  /**
4911
- * Read-only CI check: the config must already carry a valid app id.
4912
- * Wrapper/re-export configs (no inline defineConfig call) are exempt,
4913
- * mirroring the local behavior where {@link ensureConfigId} no-ops.
4914
- * @param configPath - Absolute path to the config file
5214
+ * Format executor changes for grouped dry-run display.
5215
+ * @param changeSet - Executor changes
5216
+ * @param executors - Desired executor configs keyed by name
5217
+ * @param functionRegistryExecutorChanges - Related function registry changes for executors
5218
+ * @returns Display entries for executor output
4915
5219
  */
4916
- async function assertConfigIdInCI(configPath) {
4917
- const result = await readConfigId(configPath);
4918
- if (result === null) return;
4919
- if (!result.id) throw new Error("tailor.config.ts is missing an 'id'. CI does not auto-generate one (each run would be treated as a separate app and break resource ownership). Run 'tailor-sdk setup' or 'tailor-sdk apply' locally and commit the injected id.");
4920
- if (!uuidRegex.test(result.id)) throw new Error(`'id' in ${configPath} must be a UUID. To use this config for a separate app, delete it.`);
5220
+ function formatExecutorChangeEntries(changeSet, executors, functionRegistryExecutorChanges) {
5221
+ return formatChangeEntriesWithFunctionRegistry("executor", changeSet, functionRegistryExecutorChanges, (item, action) => {
5222
+ if (action === "delete") return [executorFunctionName(item.name)];
5223
+ const executor = executors[item.name];
5224
+ return executor && isFunctionBackedExecutor(executor) ? [executorFunctionName(item.name)] : [];
5225
+ });
4921
5226
  }
4922
- /**
4923
- * Ensure the config has an app id for a deploy run.
4924
- *
4925
- * Locally, the id is auto-injected when missing (via {@link ensureConfigId}).
4926
- * In CI, the id is never auto-injected — a missing id is a hard error, because
4927
- * generating one per run would create a fresh app each time and break resource
4928
- * ownership. CI dry-runs (plan) perform the same check read-only, so a
4929
- * forgotten id fails at PR time instead of at deploy. Ephemeral pipelines that
4930
- * intentionally deploy a fresh app per run (such as e2e harnesses) can opt
4931
- * back into injection with `TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION=true`.
4932
- * Local dry-run and build-only flows skip both injection and the check (no
4933
- * on-disk side effects are expected, and build-only never talks to the
4934
- * platform).
4935
- * @param obj - Inputs
4936
- * @param obj.configPath - Absolute path to the config file
4937
- * @param obj.dryRun - Whether this is a dry-run
4938
- * @param obj.buildOnly - Whether this is a build-only run
4939
- */
4940
- async function ensureConfigIdForDeploy(obj) {
4941
- const { configPath, dryRun, buildOnly } = obj;
4942
- if (buildOnly) return;
4943
- const allowCIInjection = parseBoolean(process.env.TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION) === true;
4944
- const strictCI = isCI && !allowCIInjection;
4945
- if (dryRun) {
4946
- if (strictCI) await assertConfigIdInCI(configPath);
4947
- return;
4948
- }
4949
- if (strictCI) {
4950
- await assertConfigIdInCI(configPath);
4951
- return;
4952
- }
4953
- await ensureConfigId(configPath);
5227
+ function normalizeComparableExecutor(executor) {
5228
+ const normalized = normalizeProtoConfig(executor);
5229
+ const webhookHeaders = normalized.targetConfig?.config?.case === "webhook" ? (normalized.targetConfig.config.value.headers ?? []).toSorted((left, right) => (left.key ?? "").localeCompare(right.key ?? "")) : void 0;
5230
+ const triggerConfig = normalized.triggerConfig?.config?.case === "incomingWebhook" ? {
5231
+ ...normalized.triggerConfig,
5232
+ config: {
5233
+ ...normalized.triggerConfig.config,
5234
+ value: {
5235
+ ...normalized.triggerConfig.config.value,
5236
+ secret: void 0
5237
+ }
5238
+ }
5239
+ } : normalized.triggerConfig?.config?.case === "event" ? {
5240
+ ...normalized.triggerConfig,
5241
+ config: {
5242
+ ...normalized.triggerConfig.config,
5243
+ value: {
5244
+ ...normalized.triggerConfig.config.value,
5245
+ eventType: void 0
5246
+ }
5247
+ }
5248
+ } : normalized.triggerConfig;
5249
+ return {
5250
+ name: normalized.name,
5251
+ description: normalized.description ?? "",
5252
+ disabled: normalized.disabled ?? false,
5253
+ triggerType: normalized.triggerType,
5254
+ triggerConfig,
5255
+ targetType: normalized.targetType,
5256
+ targetConfig: normalized.targetConfig?.config?.case === "webhook" ? {
5257
+ ...normalized.targetConfig,
5258
+ config: {
5259
+ ...normalized.targetConfig.config,
5260
+ value: {
5261
+ ...normalized.targetConfig.config.value,
5262
+ headers: webhookHeaders
5263
+ }
5264
+ }
5265
+ } : normalized.targetConfig?.config?.case === "function" ? {
5266
+ ...normalized.targetConfig,
5267
+ config: {
5268
+ ...normalized.targetConfig.config,
5269
+ value: {
5270
+ ...normalized.targetConfig.config.value,
5271
+ script: void 0
5272
+ }
5273
+ }
5274
+ } : normalized.targetConfig
5275
+ };
4954
5276
  }
4955
- const idComment = "// SDK-managed app id — do not edit, except when copying this config to a separate app.";
4956
- function insertIdProperty(source, configObj, id) {
4957
- const idLiteral = `id: ${JSON.stringify(id)}`;
4958
- if (configObj.properties.length > 0) {
4959
- const firstProp = assertDefined(configObj.properties[0], "first property missing");
4960
- const lineStart = source.lastIndexOf("\n", firstProp.start - 1) + 1;
4961
- const indent = source.slice(lineStart, firstProp.start);
4962
- if (!/^[\t ]*$/.test(indent)) return source.slice(0, firstProp.start) + `${idLiteral}, ` + source.slice(firstProp.start);
4963
- const insertion = `${idComment}\n${indent}${idLiteral},\n${indent}`;
4964
- return source.slice(0, firstProp.start) + insertion + source.slice(firstProp.start);
4965
- }
4966
- const openBracePos = configObj.start + 1;
4967
- const braceLineStart = source.lastIndexOf("\n", configObj.start) + 1;
4968
- const baseIndent = source.slice(braceLineStart).match(/^[\t ]*/)?.[0] ?? "";
4969
- const innerIndent = `${baseIndent} `;
4970
- const insertion = `\n${innerIndent}${idComment}\n${innerIndent}${idLiteral},\n${baseIndent}`;
4971
- return source.slice(0, openBracePos) + insertion + source.slice(openBracePos);
5277
+ function areExecutorsEqual(existing, desired) {
5278
+ return areNormalizedEqual(normalizeComparableExecutor(existing), normalizeComparableExecutor(desired));
4972
5279
  }
4973
-
4974
- //#endregion
4975
- //#region src/cli/commands/deploy/confirm.ts
4976
5280
  /**
4977
- * Confirm reassignment of resources when owner conflicts are detected.
4978
- * Splits into two scenarios: id regeneration (same sdk-name, different
4979
- * sdk-app-id) and name mismatch (different sdk-name). Each gets its own
4980
- * prompt because the user-facing meaning is different.
4981
- * @param conflicts - Detected owner conflicts
4982
- * @param appName - Target application name
4983
- * @param yes - Whether to auto-confirm without prompting
4984
- * @returns Promise that resolves when confirmation completes
5281
+ * Resolve a resource's namespace from same-run peer configs, throwing when the
5282
+ * name is claimed by more than one namespace in the deploy run.
5283
+ * @param sameRunNamespaces - Namespaces keyed by resource name across the deploy run
5284
+ * @param resourceName - Resource name to resolve
5285
+ * @param resourceLabel - Resource label used in error messages
5286
+ * @returns The owning namespace, or undefined when the name is unknown
4985
5287
  */
4986
- async function confirmOwnerConflict(conflicts, appName, yes) {
4987
- if (conflicts.length === 0) return;
4988
- const idRegenerated = conflicts.filter((c) => c.currentOwner === appName);
4989
- const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
4990
- if (idRegenerated.length > 0) await confirmIdRegeneration(idRegenerated, appName, yes);
4991
- if (nameMismatches.length > 0) await confirmNameMismatch(nameMismatches, appName, yes);
4992
- }
4993
- async function confirmIdRegeneration(conflicts, appName, yes) {
4994
- logger.warn(`Application id was regenerated for "${appName}":`);
4995
- logger.log(" These resources still carry the previous id.");
4996
- logger.newline();
4997
- logger.log(` ${styles.info("Resources")}:`);
4998
- for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
4999
- if (yes) {
5000
- logger.success("Re-tagging resources with the new id (--yes flag specified)...", { mode: "plain" });
5001
- return;
5002
- }
5003
- if (!await prompt.confirm({
5004
- message: `Re-tag these resources with the new id for "${appName}"?\n${styles.dim("(The id in tailor.config.ts was removed since the previous deploy, so a new one was generated)")}`,
5005
- default: false
5006
- })) throw new Error(multiline`
5007
- Apply cancelled. Resources remain tagged with the previous id.
5008
- To override, run again and confirm, or use --yes flag.
5009
- `);
5010
- }
5011
- async function confirmNameMismatch(conflicts, appName, yes) {
5012
- const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
5013
- logger.warn("Application name mismatch detected:");
5014
- logger.log(` ${styles.warning("Current application(s)")}: ${currentOwners.map((o) => styles.bold(`"${o}"`)).join(", ")}`);
5015
- logger.log(` ${styles.success("New application")}: ${styles.bold(`"${appName}"`)}`);
5016
- logger.newline();
5017
- logger.log(` ${styles.info("Resources")}:`);
5018
- for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
5019
- if (yes) {
5020
- logger.success("Updating resources (--yes flag specified)...", { mode: "plain" });
5021
- return;
5022
- }
5023
- const promptMessage = currentOwners.length === 1 ? `Update these resources to be managed by "${appName}"?\n${styles.dim("(Common when renaming your application)")}` : `Update these resources to be managed by "${appName}"?`;
5024
- if (!await prompt.confirm({
5025
- message: promptMessage,
5026
- default: false
5027
- })) throw new Error(multiline`
5028
- Apply cancelled. Resources remain managed by their current applications.
5029
- To override, run again and confirm, or use --yes flag.
5030
- `);
5288
+ function resolveSameRunNamespace(sameRunNamespaces, resourceName, resourceLabel) {
5289
+ if (!sameRunNamespaces?.has(resourceName)) return;
5290
+ const namespace = sameRunNamespaces.get(resourceName);
5291
+ if (!namespace) throw new Error(`${resourceLabel} "${resourceName}" is defined in multiple namespaces in this deploy run. Move the trigger to the application that owns it or use unique names.`);
5292
+ return namespace;
5031
5293
  }
5032
5294
  /**
5033
- * Confirm allowing tailor-sdk to manage previously unmanaged resources.
5034
- * @param resources - Unmanaged resources
5035
- * @param appName - Target application name
5036
- * @param yes - Whether to auto-confirm without prompting
5037
- * @returns Promise that resolves when confirmation completes
5295
+ * Find the local TailorDB namespace that declares the given type.
5296
+ * @param application - Loaded application
5297
+ * @param typeName - TailorDB type name to look up
5298
+ * @returns The declaring namespace, or undefined when no local service has it
5038
5299
  */
5039
- async function confirmUnmanagedResources(resources, appName, yes) {
5040
- if (resources.length === 0) return;
5041
- logger.warn("Existing resources not tracked by tailor-sdk were found:");
5042
- logger.log(` ${styles.info("Resources")}:`);
5043
- for (const r of resources) logger.log(` • ${styles.bold(r.resourceType)} ${styles.info(`"${r.resourceName}"`)}`);
5044
- logger.newline();
5045
- logger.log(" These resources may have been created by older SDK versions, Terraform, or CUE.");
5046
- logger.log(" To continue, confirm that tailor-sdk should manage them.");
5047
- logger.log(" If they are managed by another tool (e.g., Terraform), cancel and manage them there instead.");
5048
- if (yes) {
5049
- logger.success(`Adding to "${appName}" (--yes flag specified)...`, { mode: "plain" });
5050
- return;
5051
- }
5052
- if (!await prompt.confirm({
5053
- message: `Allow tailor-sdk to manage these resources for "${appName}"?`,
5054
- default: false
5055
- })) throw new Error(multiline`
5056
- Apply cancelled. Resources remain unmanaged.
5057
- To override, run again and confirm, or use --yes flag.
5058
- `);
5300
+ function findTailorDBNamespace(application, typeName) {
5301
+ return application.tailorDBServices.find((service) => Object.hasOwn(service.types, typeName))?.namespace;
5059
5302
  }
5060
5303
  /**
5061
- * Confirm deletion of important resources.
5062
- * @param resources - Resources scheduled for deletion
5063
- * @param yes - Whether to auto-confirm without prompting
5064
- * @returns Promise that resolves when confirmation completes
5065
- */
5066
- async function confirmImportantResourceDeletion(resources, yes) {
5067
- if (resources.length === 0) return;
5068
- logger.warn("The following resources will be deleted:");
5069
- logger.log(` ${styles.info("Resources")}:`);
5070
- for (const r of resources) logger.log(` • ${styles.bold(r.resourceType)} ${styles.error(`"${r.resourceName}"`)}`);
5071
- logger.newline();
5072
- logger.log(styles.warning(" Deleting these resources will permanently remove all associated data."));
5073
- if (yes) {
5074
- logger.success("Deleting resources (--yes flag specified)...", { mode: "plain" });
5075
- return;
5076
- }
5077
- if (!await prompt.confirm({
5078
- message: "Are you sure you want to delete these resources?",
5079
- default: false
5080
- })) throw new Error(multiline`
5081
- Apply cancelled. Resources will not be deleted.
5082
- To override, run again and confirm, or use --yes flag.
5083
- `);
5304
+ * Find the local resolver namespace that declares the given resolver.
5305
+ * @param application - Loaded application
5306
+ * @param resolverName - Resolver name to look up
5307
+ * @returns The declaring namespace, or undefined when no local service has it
5308
+ */
5309
+ function findResolverNamespace(application, resolverName) {
5310
+ return application.resolverServices.find((service) => Object.values(service.resolvers).some((resolver) => resolver.name === resolverName))?.namespace;
5311
+ }
5312
+ function resolveNamespace(params) {
5313
+ const { resourceLabel, resourceName, localNamespaces, findLocalNamespace, sameRunNamespaces } = params;
5314
+ const localNamespace = findLocalNamespace();
5315
+ if (localNamespace !== void 0) return localNamespace;
5316
+ const sameRunNamespace = resolveSameRunNamespace(sameRunNamespaces, resourceName, resourceLabel);
5317
+ if (sameRunNamespace !== void 0) return sameRunNamespace;
5318
+ const sameRunAvailableNamespaces = sameRunNamespaces ? [...new Set([...sameRunNamespaces.values()].filter((value) => value !== void 0))] : [];
5319
+ const availableNamespaces = [...localNamespaces, ...sameRunAvailableNamespaces];
5320
+ throw new Error(`${resourceLabel} "${resourceName}" not found in any namespace. Available namespaces: ${availableNamespaces.join(", ")}`);
5321
+ }
5322
+ function resolveTailorDBNamespace(application, typeName, sameRunNamespaces) {
5323
+ return resolveNamespace({
5324
+ resourceLabel: "TailorDB type",
5325
+ resourceName: typeName,
5326
+ localNamespaces: application.tailorDBServices.map((service) => service.namespace),
5327
+ findLocalNamespace: () => findTailorDBNamespace(application, typeName),
5328
+ sameRunNamespaces
5329
+ });
5330
+ }
5331
+ function resolveResolverNamespace(application, resolverName, sameRunNamespaces) {
5332
+ return resolveNamespace({
5333
+ resourceLabel: "Resolver",
5334
+ resourceName: resolverName,
5335
+ localNamespaces: application.resolverServices.map((service) => service.namespace),
5336
+ findLocalNamespace: () => findResolverNamespace(application, resolverName),
5337
+ sameRunNamespaces
5338
+ });
5084
5339
  }
5085
-
5086
- //#endregion
5087
- //#region src/cli/commands/deploy/auth-invoker.ts
5088
5340
  /**
5089
- * Normalize an authInvoker value to the object form required by the proto payload.
5090
- *
5091
- * Accepts either:
5092
- * - `undefined` returns undefined
5093
- * - a plain string (machine user name) — expands to `{ namespace, machineUserName }` using `authNamespace`
5094
- * - an object `{ namespace, machineUserName }` — returned as-is
5095
- * @param authInvoker - String machine user name or object form
5096
- * @param authNamespace - Auth service namespace (required when authInvoker is a string)
5097
- * @param context - Contextual label used in error messages (e.g. `resolver "foo"`)
5098
- * @returns Object form of auth invoker, or undefined
5341
+ * Collect IdP names declared by the application, including external IdP
5342
+ * subgraphs.
5343
+ * @param application - Loaded application
5344
+ * @returns IdP names visible from the application config
5099
5345
  */
5100
- function normalizeAuthInvoker(authInvoker, authNamespace, context) {
5101
- if (authInvoker === void 0) return void 0;
5102
- if (typeof authInvoker === "string") {
5103
- if (!authNamespace) throw new Error(`${context} uses a string authInvoker ("${authInvoker}"), but no Auth service is configured. Configure an Auth service or use the object form { namespace, machineUserName }.`);
5104
- return {
5105
- namespace: authNamespace,
5106
- machineUserName: authInvoker
5107
- };
5346
+ function collectApplicationIdpNames(application) {
5347
+ const names = new Set(application.idpServices.map((idp) => idp.name));
5348
+ for (const subgraph of application.subgraphs) if (subgraph.Type === "idp") names.add(subgraph.Name);
5349
+ return names;
5350
+ }
5351
+ function resolveIdpNamespace(application, executorName, idpName, sameRunIdpNames) {
5352
+ const localIdpNames = collectApplicationIdpNames(application);
5353
+ if (idpName !== void 0) {
5354
+ const candidateNames = sameRunIdpNames ?? localIdpNames;
5355
+ if (!candidateNames.has(idpName)) {
5356
+ const available = [...candidateNames].join(", ");
5357
+ throw new Error(`Executor "${executorName}" specifies IdP "${idpName}" in its idpUser trigger, but no IdP with that name is configured. Available IdPs: ${available}`);
5358
+ }
5359
+ return idpName;
5108
5360
  }
5109
- return authInvoker;
5110
- }
5111
-
5112
- //#endregion
5113
- //#region src/cli/commands/deploy/executor.ts
5114
- /**
5115
- * Apply executor-related changes for the given phase.
5116
- * @param client - Operator client instance
5117
- * @param result - Planned executor changes
5118
- * @param phase - Apply phase (defaults to "create-update")
5119
- * @returns Promise that resolves when executors are applied
5120
- */
5121
- async function applyExecutor(client, result, phase = "create-update") {
5122
- const { changeSet } = result;
5123
- if (phase === "create-update") await Promise.all([...changeSet.creates.map(async (create) => {
5124
- await client.createExecutorExecutor(create.request);
5125
- await client.setMetadata(create.metaRequest);
5126
- }), ...changeSet.updates.map(async (update) => {
5127
- await client.updateExecutorExecutor(update.request);
5128
- await client.setMetadata(update.metaRequest);
5129
- })]);
5130
- else await Promise.all(changeSet.deletes.map((del) => client.deleteExecutorExecutor(del.request)));
5131
- }
5132
- /**
5133
- * Plan executor-related changes based on current and desired state.
5134
- * @param context - Planning context
5135
- * @returns Planned changes
5136
- */
5137
- async function planExecutor(context) {
5138
- const { client, workspaceId, application, forRemoval } = context;
5139
- const changeSet = createChangeSet("Executors");
5140
- const conflicts = [];
5141
- const unmanaged = [];
5142
- const resourceOwners = /* @__PURE__ */ new Set();
5143
- const existingExecutors = await fetchExistingResourcesWithLabels({
5144
- client,
5145
- fetchPage: async (pageToken, pageSize) => {
5146
- const { executors, nextPageToken } = await client.listExecutorExecutors({
5147
- workspaceId,
5148
- pageToken,
5149
- pageSize
5150
- });
5151
- return [executors, nextPageToken];
5152
- },
5153
- getName: (resource) => resource.name,
5154
- getTrn: (name) => resourceTrn(workspaceId, "executor", name)
5155
- });
5156
- const executors = forRemoval ? {} : await application.executorService?.loadExecutors() ?? {};
5157
- for (const executor of Object.values(executors)) {
5158
- const existing = existingExecutors[executor.name];
5159
- const metaRequest = await buildMetaRequest({
5160
- trn: resourceTrn(workspaceId, "executor", executor.name),
5161
- appName: application.name,
5162
- appId: application.id
5163
- });
5164
- const desiredExecutor = protoExecutor(application, executor);
5165
- if (existing) {
5166
- if (trackDesiredResourceOwnership({
5167
- labels: existing.allLabels,
5168
- ownerLabel: existing.label,
5169
- appName: application.name,
5170
- appId: application.id,
5171
- resourceType: "Executor",
5172
- resourceName: executor.name,
5173
- conflicts,
5174
- unmanaged
5175
- }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areExecutorsEqual(existing.resource, desiredExecutor)) changeSet.unchanged.push({ name: executor.name });
5176
- else changeSet.updates.push({
5177
- name: executor.name,
5178
- request: {
5179
- workspaceId,
5180
- executor: desiredExecutor
5181
- },
5182
- metaRequest
5183
- });
5184
- delete existingExecutors[executor.name];
5185
- } else changeSet.creates.push({
5186
- name: executor.name,
5187
- request: {
5188
- workspaceId,
5189
- executor: desiredExecutor
5190
- },
5191
- metaRequest
5192
- });
5193
- }
5194
- Object.entries(existingExecutors).forEach(([name]) => {
5195
- const entry = existingExecutors[name];
5196
- const label = entry?.label;
5197
- if (trackRemainingResourceOwner({
5198
- labels: entry?.allLabels,
5199
- ownerLabel: label,
5200
- appName: application.name,
5201
- appId: application.id,
5202
- resourceOwners
5203
- })) changeSet.deletes.push({
5204
- name,
5205
- request: {
5206
- workspaceId,
5207
- name
5208
- }
5209
- });
5210
- });
5211
- return {
5212
- changeSet,
5213
- conflicts,
5214
- unmanaged,
5215
- resourceOwners
5216
- };
5217
- }
5218
- function isFunctionBackedExecutor(executor) {
5219
- return executor?.targetType === ExecutorTargetType.FUNCTION || executor?.targetType === ExecutorTargetType.JOB_FUNCTION;
5220
- }
5221
- /**
5222
- * Build desired executor configs keyed by executor name from create/update changes.
5223
- * @param changeSet - Executor create/update changes
5224
- * @returns Executor configs keyed by name
5225
- */
5226
- function buildPlannedExecutorsByName(changeSet) {
5227
- return Object.fromEntries([...changeSet.creates, ...changeSet.updates].map((item) => [item.name, item.request.executor]));
5228
- }
5229
- /**
5230
- * Format executor changes for grouped dry-run display.
5231
- * @param changeSet - Executor changes
5232
- * @param executors - Desired executor configs keyed by name
5233
- * @param functionRegistryExecutorChanges - Related function registry changes for executors
5234
- * @returns Display entries for executor output
5235
- */
5236
- function formatExecutorChangeEntries(changeSet, executors, functionRegistryExecutorChanges) {
5237
- return formatChangeEntriesWithFunctionRegistry("executor", changeSet, functionRegistryExecutorChanges, (item, action) => {
5238
- if (action === "delete") return [executorFunctionName(item.name)];
5239
- const executor = executors[item.name];
5240
- return executor && isFunctionBackedExecutor(executor) ? [executorFunctionName(item.name)] : [];
5241
- });
5242
- }
5243
- function normalizeComparableExecutor(executor) {
5244
- const normalized = normalizeProtoConfig(executor);
5245
- const webhookHeaders = normalized.targetConfig?.config?.case === "webhook" ? (normalized.targetConfig.config.value.headers ?? []).toSorted((left, right) => (left.key ?? "").localeCompare(right.key ?? "")) : void 0;
5246
- const triggerConfig = normalized.triggerConfig?.config?.case === "incomingWebhook" ? {
5247
- ...normalized.triggerConfig,
5248
- config: {
5249
- ...normalized.triggerConfig.config,
5250
- value: {
5251
- ...normalized.triggerConfig.config.value,
5252
- secret: void 0
5253
- }
5254
- }
5255
- } : normalized.triggerConfig?.config?.case === "event" ? {
5256
- ...normalized.triggerConfig,
5257
- config: {
5258
- ...normalized.triggerConfig.config,
5259
- value: {
5260
- ...normalized.triggerConfig.config.value,
5261
- eventType: void 0
5262
- }
5263
- }
5264
- } : normalized.triggerConfig;
5265
- return {
5266
- name: normalized.name,
5267
- description: normalized.description ?? "",
5268
- disabled: normalized.disabled ?? false,
5269
- triggerType: normalized.triggerType,
5270
- triggerConfig,
5271
- targetType: normalized.targetType,
5272
- targetConfig: normalized.targetConfig?.config?.case === "webhook" ? {
5273
- ...normalized.targetConfig,
5274
- config: {
5275
- ...normalized.targetConfig.config,
5276
- value: {
5277
- ...normalized.targetConfig.config.value,
5278
- headers: webhookHeaders
5279
- }
5280
- }
5281
- } : normalized.targetConfig?.config?.case === "function" ? {
5282
- ...normalized.targetConfig,
5283
- config: {
5284
- ...normalized.targetConfig.config,
5285
- value: {
5286
- ...normalized.targetConfig.config.value,
5287
- script: void 0
5288
- }
5289
- }
5290
- } : normalized.targetConfig
5291
- };
5292
- }
5293
- function areExecutorsEqual(existing, desired) {
5294
- return areNormalizedEqual(normalizeComparableExecutor(existing), normalizeComparableExecutor(desired));
5295
- }
5296
- function resolveTailorDBNamespace(application, typeName) {
5297
- for (const service of application.tailorDBServices) if (Object.hasOwn(service.types, typeName)) return service.namespace;
5298
- throw new Error(`TailorDB type "${typeName}" not found in any namespace. Available namespaces: ${application.tailorDBServices.map((s) => s.namespace).join(", ")}`);
5299
- }
5300
- function resolveResolverNamespace(application, resolverName) {
5301
- for (const service of application.resolverServices) if (Object.values(service.resolvers).some((r) => r.name === resolverName)) return service.namespace;
5302
- throw new Error(`Resolver "${resolverName}" not found in any namespace. Available namespaces: ${application.resolverServices.map((s) => s.namespace).join(", ")}`);
5303
- }
5304
- function resolveIdpNamespace(application, executorName, idpName) {
5305
- if (application.idpServices.length === 0) throw new Error(`Executor "${executorName}" uses an idpUser trigger but no IdP is configured.`);
5306
- if (idpName !== void 0) {
5307
- const found = application.idpServices.find((idp) => idp.name === idpName);
5308
- if (!found) {
5309
- const available = application.idpServices.map((idp) => idp.name).join(", ");
5310
- throw new Error(`Executor "${executorName}" specifies IdP "${idpName}" in its idpUser trigger, but no IdP with that name is configured. Available IdPs: ${available}`);
5311
- }
5312
- return found.name;
5313
- }
5314
- if (application.idpServices.length > 1) {
5315
- const available = application.idpServices.map((idp) => idp.name).join(", ");
5316
- throw new Error(`Executor "${executorName}" uses an idpUser trigger but the project defines multiple IdPs (${available}). Specify which IdP to subscribe to via the trigger's "idp" option.`);
5317
- }
5318
- return assertDefined(application.idpServices[0], "idp service missing").name;
5361
+ if (localIdpNames.size === 0) throw new Error(`Executor "${executorName}" uses an idpUser trigger but no IdP is configured.`);
5362
+ if (localIdpNames.size > 1) {
5363
+ const available = [...localIdpNames].join(", ");
5364
+ throw new Error(`Executor "${executorName}" uses an idpUser trigger but the project defines multiple IdPs (${available}). Specify which IdP to subscribe to via the trigger's "idp" option.`);
5365
+ }
5366
+ return assertDefined([...localIdpNames][0], "idp service missing");
5319
5367
  }
5320
5368
  function resolveAuthNamespace(application) {
5321
- if (!application.authService) throw new Error("No Auth service configured");
5322
- return application.authService.config.name;
5369
+ const authNamespace = application.authService?.config.name ?? application.config.auth?.name;
5370
+ if (!authNamespace) throw new Error("No Auth service configured");
5371
+ return authNamespace;
5323
5372
  }
5324
- function protoExecutor(application, executor) {
5373
+ function protoExecutor(context, executor) {
5374
+ const { application } = context;
5325
5375
  const appName = application.name;
5326
5376
  const env = application.env;
5327
5377
  const trigger = executor.trigger;
@@ -5351,7 +5401,7 @@ function protoExecutor(application, executor) {
5351
5401
  case: "tailordb",
5352
5402
  value: {
5353
5403
  eventTypes: trigger.events,
5354
- namespaceName: resolveTailorDBNamespace(application, trigger.typeName),
5404
+ namespaceName: resolveTailorDBNamespace(application, trigger.typeName, context.tailorDBTypeNamespaces),
5355
5405
  typeName: trigger.typeName,
5356
5406
  ...trigger.condition ? { condition: { expr: `(${stringifyFunction(trigger.condition)})(${argsExpr})` } } : {}
5357
5407
  }
@@ -5363,7 +5413,7 @@ function protoExecutor(application, executor) {
5363
5413
  case: "pipeline",
5364
5414
  value: {
5365
5415
  eventTypes: ["pipeline.resolver.executed"],
5366
- namespaceName: resolveResolverNamespace(application, trigger.resolverName),
5416
+ namespaceName: resolveResolverNamespace(application, trigger.resolverName, context.resolverNamespaces),
5367
5417
  resolverName: trigger.resolverName,
5368
5418
  ...trigger.condition ? { condition: { expr: `(${stringifyFunction(trigger.condition)})(${argsExpr})` } } : {}
5369
5419
  }
@@ -5385,7 +5435,7 @@ function protoExecutor(application, executor) {
5385
5435
  case: "idp",
5386
5436
  value: {
5387
5437
  eventTypes: trigger.events,
5388
- namespaceName: resolveIdpNamespace(application, executor.name, trigger.idp)
5438
+ namespaceName: resolveIdpNamespace(application, executor.name, trigger.idp, context.idpNames)
5389
5439
  }
5390
5440
  });
5391
5441
  break;
@@ -5560,7 +5610,8 @@ async function planPipeline(context) {
5560
5610
  }
5561
5611
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
5562
5612
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$1(client, workspaceId, application.name, application.id, pipelines);
5563
- const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, serviceChangeSet.deletes.map((del) => del.name), application.env, application.authService?.config.name, forceApplyAll);
5613
+ const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
5614
+ const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, context.executorUsedResolvers ?? /* @__PURE__ */ new Set(), deletedServices, application.env, application.authService?.config.name, forceApplyAll);
5564
5615
  return {
5565
5616
  changeSet: {
5566
5617
  service: serviceChangeSet,
@@ -5597,17 +5648,16 @@ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
5597
5648
  appId
5598
5649
  });
5599
5650
  if (existing) {
5600
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
5601
- if (!owned) if (!existing.label) unmanaged.push({
5602
- resourceType: "Pipeline service",
5603
- resourceName: pipeline.namespace
5604
- });
5605
- else conflicts.push({
5651
+ if (trackDesiredResourceOwnership({
5652
+ labels: existing.allLabels,
5653
+ ownerLabel: existing.label,
5654
+ appName,
5655
+ appId,
5606
5656
  resourceType: "Pipeline service",
5607
5657
  resourceName: pipeline.namespace,
5608
- currentOwner: existing.label
5609
- });
5610
- if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: pipeline.namespace });
5658
+ conflicts,
5659
+ unmanaged
5660
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: pipeline.namespace });
5611
5661
  else changeSet.updates.push({
5612
5662
  name: pipeline.namespace,
5613
5663
  request: {
@@ -5628,10 +5678,13 @@ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
5628
5678
  }
5629
5679
  Object.entries(existingServices).forEach(([namespaceName]) => {
5630
5680
  const entry = existingServices[namespaceName];
5631
- const label = entry?.label;
5632
- const owned = isOwnedByApp(entry?.allLabels, appName, appId);
5633
- if (label && !owned) resourceOwners.add(label);
5634
- if (owned) changeSet.deletes.push({
5681
+ if (trackRemainingResourceOwner({
5682
+ labels: entry?.allLabels,
5683
+ ownerLabel: entry?.label,
5684
+ appName,
5685
+ appId,
5686
+ resourceOwners
5687
+ })) changeSet.deletes.push({
5635
5688
  name: namespaceName,
5636
5689
  request: {
5637
5690
  workspaceId,
@@ -5646,7 +5699,7 @@ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
5646
5699
  resourceOwners
5647
5700
  };
5648
5701
  }
5649
- async function planResolvers(client, workspaceId, pipelines, executors, deletedServices, env, authNamespace, forceApplyAll = false) {
5702
+ async function planResolvers(client, workspaceId, pipelines, executors, initialExecutorUsedResolvers, deletedServices, env, authNamespace, forceApplyAll = false) {
5650
5703
  const changeSet = createChangeSet("Pipeline resolvers");
5651
5704
  const fetchResolvers = (namespaceName) => {
5652
5705
  return fetchAll(async (pageToken, maxPageSize) => {
@@ -5664,7 +5717,7 @@ async function planResolvers(client, workspaceId, pipelines, executors, deletedS
5664
5717
  }
5665
5718
  });
5666
5719
  };
5667
- const executorUsedResolvers = /* @__PURE__ */ new Set();
5720
+ const executorUsedResolvers = new Set(initialExecutorUsedResolvers);
5668
5721
  for (const executor of executors) if (executor.trigger.kind === "resolverExecuted") executorUsedResolvers.add(executor.trigger.resolverName);
5669
5722
  for (const pipeline of pipelines) for (const resolver of Object.values(pipeline.resolvers)) if (executorUsedResolvers.has(resolver.name) && resolver.publishEvents === false) throw new Error(`Resolver "${resolver.name}" has publishEvents set to false, but it is used by an executor with a resolverExecuted trigger. Either remove the publishEvents: false setting or remove the executor trigger for this resolver.`);
5670
5723
  for (const pipeline of pipelines) {
@@ -5925,17 +5978,16 @@ async function planSecretManager(context) {
5925
5978
  appName: application.name,
5926
5979
  appId: application.id
5927
5980
  });
5928
- const owned = isOwnedByApp(existing.allLabels, application.name, application.id);
5929
- if (!owned) if (!existing.label) unmanaged.push({
5930
- resourceType: "Secret Manager vault",
5931
- resourceName: vaultName
5932
- });
5933
- else conflicts.push({
5981
+ if (trackDesiredResourceOwnership({
5982
+ labels: existing.allLabels,
5983
+ ownerLabel: existing.label,
5984
+ appName: application.name,
5985
+ appId: application.id,
5934
5986
  resourceType: "Secret Manager vault",
5935
5987
  resourceName: vaultName,
5936
- currentOwner: existing.label
5937
- });
5938
- if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) vaultChangeSet.unchanged.push({ name: vaultName });
5988
+ conflicts,
5989
+ unmanaged
5990
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) vaultChangeSet.unchanged.push({ name: vaultName });
5939
5991
  else vaultChangeSet.updates.push({
5940
5992
  name: vaultName,
5941
5993
  workspaceId
@@ -5995,10 +6047,13 @@ async function planSecretManager(context) {
5995
6047
  }));
5996
6048
  for (const [name, entry] of Object.entries(existingVaults)) {
5997
6049
  if (!entry) continue;
5998
- const label = entry.label;
5999
- const owned = isOwnedByApp(entry.allLabels, application.name, application.id);
6000
- if (label && !owned) resourceOwners.add(label);
6001
- if (owned) {
6050
+ if (trackRemainingResourceOwner({
6051
+ labels: entry.allLabels,
6052
+ ownerLabel: entry.label,
6053
+ appName: application.name,
6054
+ appId: application.id,
6055
+ resourceOwners
6056
+ })) {
6002
6057
  const secrets = await fetchAll(async (pageToken, maxPageSize) => {
6003
6058
  try {
6004
6059
  const { secrets, nextPageToken } = await client.listSecretManagerSecrets({
@@ -6097,213 +6152,17 @@ async function applySecretManager(client, result, phase = "create-update", appli
6097
6152
  }
6098
6153
 
6099
6154
  //#endregion
6100
- //#region src/cli/commands/deploy/staticwebsite.ts
6155
+ //#region src/cli/commands/tailordb/migrate/config.ts
6101
6156
  /**
6102
- * Apply static website changes for the given phase.
6103
- * @param client - Operator client instance
6104
- * @param result - Planned static website changes
6105
- * @param phase - Apply phase
6106
- * @returns Promise that resolves when static websites are applied
6157
+ * Migration configuration utilities
6107
6158
  */
6108
- async function applyStaticWebsite(client, result, phase = "create-update") {
6109
- const { changeSet, customDomainChangeSet } = result;
6110
- if (phase === "create-update") {
6111
- await Promise.all([...changeSet.creates.map(async (create) => {
6112
- await client.createStaticWebsite(create.request);
6113
- await client.setMetadata(create.metaRequest);
6114
- }), ...changeSet.updates.map(async (update) => {
6115
- await client.updateStaticWebsite(update.request);
6116
- await client.setMetadata(update.metaRequest);
6117
- })]);
6118
- await Promise.all([...customDomainChangeSet.creates.map(async (add) => {
6119
- await client.addCustomDomain(add.request);
6120
- await client.setMetadata(add.metaRequest);
6121
- }), ...customDomainChangeSet.deletes.map((del) => client.removeCustomDomain(del.request))]);
6122
- } else await Promise.all(changeSet.deletes.map((del) => client.deleteStaticWebsite(del.request)));
6123
- }
6124
- function customDomainTrn(workspaceId, websiteName, domain) {
6125
- return `trn:v1:workspace:${workspaceId}:staticwebsite:${websiteName}:custom_domain:${domain}`;
6126
- }
6127
- function normalizeComparableStaticWebsiteShape(input) {
6128
- return {
6129
- description: input.description,
6130
- allowedIpAddresses: input.allowedIpAddresses.toSorted()
6131
- };
6132
- }
6133
- function normalizeComparableStaticWebsite(input) {
6134
- return normalizeComparableStaticWebsiteShape({
6135
- description: input.description || "",
6136
- allowedIpAddresses: [...input.allowedIpAddresses || []]
6137
- });
6138
- }
6139
- function areStaticWebsitesEqual(existing, desired) {
6140
- return areNormalizedEqual(normalizeComparableStaticWebsite(existing), normalizeComparableStaticWebsite(desired));
6141
- }
6142
- /**
6143
- * Plan static website changes based on current and desired state.
6144
- * @param context - Planning context
6145
- * @returns Planned changes
6146
- */
6147
- async function planStaticWebsite(context) {
6148
- const { client, workspaceId, application, forRemoval } = context;
6149
- const changeSet = createChangeSet("StaticWebsites");
6150
- const customDomainChangeSet = createChangeSet("CustomDomains");
6151
- const conflicts = [];
6152
- const unmanaged = [];
6153
- const resourceOwners = /* @__PURE__ */ new Set();
6154
- const existingWebsites = await fetchExistingResourcesWithLabels({
6155
- client,
6156
- fetchPage: async (pageToken, pageSize) => {
6157
- const { staticwebsites, nextPageToken } = await client.listStaticWebsites({
6158
- workspaceId,
6159
- pageToken,
6160
- pageSize
6161
- });
6162
- return [staticwebsites, nextPageToken];
6163
- },
6164
- getName: (resource) => resource.name,
6165
- getTrn: (name) => resourceTrn(workspaceId, "staticwebsite", name)
6166
- });
6167
- const ownedWebsiteNames = /* @__PURE__ */ new Set();
6168
- const staticWebsiteServices = forRemoval ? [] : application.staticWebsiteServices;
6169
- for (const websiteService of staticWebsiteServices) {
6170
- const config = websiteService;
6171
- const name = websiteService.name;
6172
- const existing = existingWebsites[name];
6173
- const metaRequest = await buildMetaRequest({
6174
- trn: resourceTrn(workspaceId, "staticwebsite", name),
6175
- appName: application.name,
6176
- appId: application.id
6177
- });
6178
- const desired = normalizeComparableStaticWebsite(config);
6179
- const request = {
6180
- workspaceId,
6181
- staticwebsite: {
6182
- name,
6183
- description: config.description || "",
6184
- allowedIpAddresses: config.allowedIpAddresses || []
6185
- }
6186
- };
6187
- if (existing) {
6188
- const owned = trackDesiredResourceOwnership({
6189
- labels: existing.allLabels,
6190
- ownerLabel: existing.label,
6191
- appName: application.name,
6192
- appId: application.id,
6193
- resourceType: "StaticWebsite",
6194
- resourceName: name,
6195
- conflicts,
6196
- unmanaged
6197
- });
6198
- if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areStaticWebsitesEqual(existing.resource, desired)) changeSet.unchanged.push({ name });
6199
- else changeSet.updates.push({
6200
- name,
6201
- request,
6202
- metaRequest
6203
- });
6204
- if (owned) ownedWebsiteNames.add(name);
6205
- delete existingWebsites[name];
6206
- } else {
6207
- changeSet.creates.push({
6208
- name,
6209
- request,
6210
- metaRequest
6211
- });
6212
- ownedWebsiteNames.add(name);
6213
- }
6214
- }
6215
- Object.entries(existingWebsites).forEach(([name]) => {
6216
- const entry = existingWebsites[name];
6217
- const label = entry?.label;
6218
- if (trackRemainingResourceOwner({
6219
- labels: entry?.allLabels,
6220
- ownerLabel: label,
6221
- appName: application.name,
6222
- appId: application.id,
6223
- resourceOwners
6224
- })) changeSet.deletes.push({
6225
- name,
6226
- request: {
6227
- workspaceId,
6228
- name
6229
- }
6230
- });
6231
- });
6232
- const desiredDomainsByWebsite = /* @__PURE__ */ new Map();
6233
- for (const service of staticWebsiteServices) if (service.customDomains !== void 0 && ownedWebsiteNames.has(service.name)) desiredDomainsByWebsite.set(service.name, service.customDomains);
6234
- const existingDomainsByWebsite = /* @__PURE__ */ new Map();
6235
- const websitesToFetchDomains = [...ownedWebsiteNames].filter((name) => !changeSet.creates.some((c) => c.name === name));
6236
- await Promise.all(websitesToFetchDomains.map(async (name) => {
6237
- try {
6238
- const { customDomains } = await client.listCustomDomains({
6239
- workspaceId,
6240
- staticWebsiteName: name
6241
- });
6242
- const domainsWithLabels = await Promise.all(customDomains.map(async (d) => {
6243
- const { metadata } = await client.getMetadata({ trn: customDomainTrn(workspaceId, name, d.domain) });
6244
- return {
6245
- domain: d.domain,
6246
- allLabels: metadata?.labels
6247
- };
6248
- }));
6249
- existingDomainsByWebsite.set(name, domainsWithLabels);
6250
- } catch (error) {
6251
- if (error instanceof ConnectError && error.code === Code.NotFound) return;
6252
- throw error;
6253
- }
6254
- }));
6255
- for (const name of ownedWebsiteNames) {
6256
- const desired = new Set(desiredDomainsByWebsite.get(name) ?? []);
6257
- const existingDomains = existingDomainsByWebsite.get(name) ?? [];
6258
- const existingSet = new Set(existingDomains.map((d) => d.domain));
6259
- const sdkOwnedDomains = new Set(existingDomains.filter((d) => isOwnedByApp(d.allLabels, application.name, application.id)).map((d) => d.domain));
6260
- for (const domain of desired) if (!existingSet.has(domain)) {
6261
- const metaRequest = await buildMetaRequest({
6262
- trn: customDomainTrn(workspaceId, name, domain),
6263
- appName: application.name,
6264
- appId: application.id
6265
- });
6266
- customDomainChangeSet.creates.push({
6267
- name: domain,
6268
- request: {
6269
- workspaceId,
6270
- staticWebsiteName: name,
6271
- domain
6272
- },
6273
- metaRequest
6274
- });
6275
- } else customDomainChangeSet.unchanged.push({ name: domain });
6276
- if (desiredDomainsByWebsite.has(name)) {
6277
- for (const domain of sdkOwnedDomains) if (!desired.has(domain)) customDomainChangeSet.deletes.push({
6278
- name: domain,
6279
- request: {
6280
- workspaceId,
6281
- domain
6282
- }
6283
- });
6284
- }
6285
- }
6286
- return {
6287
- changeSet,
6288
- customDomainChangeSet,
6289
- conflicts,
6290
- unmanaged,
6291
- resourceOwners
6292
- };
6293
- }
6294
-
6295
- //#endregion
6296
- //#region src/cli/commands/tailordb/migrate/config.ts
6297
- /**
6298
- * Migration configuration utilities
6299
- */
6300
- function hasMigrationConfig(dbConfig) {
6301
- if (typeof dbConfig !== "object" || dbConfig === null) return false;
6302
- if (!("migration" in dbConfig)) return false;
6303
- const migration = dbConfig.migration;
6304
- if (typeof migration !== "object" || migration === null) return false;
6305
- if (!("directory" in migration)) return false;
6306
- return typeof migration.directory === "string";
6159
+ function hasMigrationConfig(dbConfig) {
6160
+ if (typeof dbConfig !== "object" || dbConfig === null) return false;
6161
+ if (!("migration" in dbConfig)) return false;
6162
+ const migration = dbConfig.migration;
6163
+ if (typeof migration !== "object" || migration === null) return false;
6164
+ if (!("directory" in migration)) return false;
6165
+ return typeof migration.directory === "string";
6307
6166
  }
6308
6167
  /**
6309
6168
  * Get namespaces that have migrations configured
@@ -6919,42 +6778,56 @@ function copySnapshotRecord(record) {
6919
6778
  return copy;
6920
6779
  }
6921
6780
  /**
6922
- * Normalize a snapshot field in place so the snapshot becomes the canonical
6923
- * form for comparison. Currently fills in the platform default decimal scale
6924
- * when omitted, which avoids false drift between local schemas (where scale
6925
- * may be omitted) and the platform (which always materializes a scale).
6781
+ * Normalize a snapshot field into the canonical form for comparison, returning
6782
+ * a new object rather than mutating the input. Currently fills in the platform
6783
+ * default decimal scale when omitted, which avoids false drift between local
6784
+ * schemas (where scale may be omitted) and the platform (which always
6785
+ * materializes a scale).
6926
6786
  * @param {SnapshotFieldConfig} field - Field configuration to normalize
6927
- * @returns The same field object after normalization
6787
+ * @returns {SnapshotFieldConfig} A new, normalized field object
6928
6788
  */
6929
6789
  function normalizeSnapshotField(field) {
6930
- if (field.type === "decimal" && field.scale === void 0) field.scale = 6;
6931
- if (field.fields) for (const nested of Object.values(field.fields)) normalizeSnapshotField(nested);
6932
- return field;
6790
+ const fields = field.fields ? Object.fromEntries(Object.entries(field.fields).map(([name, nested]) => [name, normalizeSnapshotField(nested)])) : void 0;
6791
+ return {
6792
+ ...field,
6793
+ ...field.type === "decimal" && field.scale === void 0 && { scale: 6 },
6794
+ ...fields && { fields }
6795
+ };
6933
6796
  }
6934
6797
  /**
6935
- * Normalize a snapshot type in place to the canonical comparison shape.
6936
- * Currently fills:
6798
+ * Normalize a snapshot type into the canonical comparison shape, returning a
6799
+ * new object rather than mutating the input. Currently fills:
6937
6800
  * - `pluralForm` via inflection when missing (legacy snapshots written
6938
6801
  * before `pluralForm` became required may omit it)
6939
6802
  * - per-field `scale` defaults via {@link normalizeSnapshotField}
6940
6803
  *
6941
6804
  * Idempotent — safe to call multiple times on the same input.
6942
6805
  * @param {TailorDBSnapshotType} type - Snapshot type to normalize
6943
- * @returns The same snapshot type object after normalization
6806
+ * @returns {TailorDBSnapshotType} A new, normalized snapshot type object
6944
6807
  */
6945
6808
  function normalizeSnapshotType(type) {
6946
- if (!type.pluralForm) type.pluralForm = inflection.pluralize(type.name);
6947
- for (const field of Object.values(type.fields)) normalizeSnapshotField(field);
6948
- return type;
6809
+ const pluralForm = type.pluralForm || inflection.pluralize(type.name);
6810
+ const fields = createSnapshotRecord();
6811
+ for (const [fieldName, field] of Object.entries(type.fields)) fields[fieldName] = normalizeSnapshotField(field);
6812
+ return {
6813
+ ...type,
6814
+ pluralForm,
6815
+ fields
6816
+ };
6949
6817
  }
6950
6818
  /**
6951
- * Normalize a schema snapshot in place to the canonical comparison shape.
6819
+ * Normalize a schema snapshot into the canonical comparison shape, returning a
6820
+ * new object rather than mutating the input.
6952
6821
  * @param {SchemaSnapshot} snapshot - Schema snapshot to normalize
6953
- * @returns The same schema snapshot object branded as normalized
6822
+ * @returns {NormalizedSchemaSnapshot} A new schema snapshot object branded as normalized
6954
6823
  */
6955
6824
  function normalizeSchemaSnapshot(snapshot) {
6956
- for (const type of Object.values(snapshot.types)) normalizeSnapshotType(type);
6957
- return snapshot;
6825
+ const types = createSnapshotRecord();
6826
+ for (const [typeName, type] of Object.entries(snapshot.types)) types[typeName] = normalizeSnapshotType(type);
6827
+ return {
6828
+ ...snapshot,
6829
+ types
6830
+ };
6958
6831
  }
6959
6832
  /**
6960
6833
  * Validate that a migration number follows the expected format (4-digit number)
@@ -7037,8 +6910,7 @@ function createSnapshotFieldConfig(field) {
7037
6910
  config.fields = {};
7038
6911
  for (const [nestedName, nestedConfig] of Object.entries(field.config.fields)) config.fields[nestedName] = createSnapshotFieldConfigFromOperatorConfig(nestedConfig);
7039
6912
  }
7040
- normalizeSnapshotField(config);
7041
- return config;
6913
+ return normalizeSnapshotField(config);
7042
6914
  }
7043
6915
  /**
7044
6916
  * Create a snapshot field config from an OperatorFieldConfig (for nested fields)
@@ -7083,8 +6955,7 @@ function createSnapshotFieldConfigFromOperatorConfig(fieldConfig) {
7083
6955
  config.fields = {};
7084
6956
  for (const [nestedName, nestedConfig] of Object.entries(fieldConfig.fields)) config.fields[nestedName] = createSnapshotFieldConfigFromOperatorConfig(nestedConfig);
7085
6957
  }
7086
- normalizeSnapshotField(config);
7087
- return config;
6958
+ return normalizeSnapshotField(config);
7088
6959
  }
7089
6960
  /**
7090
6961
  * Create a snapshot type from a parsed type
@@ -10301,7 +10172,7 @@ async function planTailorDB(context) {
10301
10172
  tailordbs.push(toTailorDBDeployInput(tailordb));
10302
10173
  }
10303
10174
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
10304
- const executorUsedTypes = /* @__PURE__ */ new Set();
10175
+ const executorUsedTypes = new Set(context.executorUsedTailorDBTypes ?? []);
10305
10176
  for (const executor of executors) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
10306
10177
  const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
10307
10178
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
@@ -10406,17 +10277,16 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
10406
10277
  existingLabels: existing?.allLabels
10407
10278
  });
10408
10279
  if (existing) {
10409
- const owned = isOwnedByApp(existing.allLabels, appName, appId);
10410
- if (!owned) if (!existing.label) unmanaged.push({
10411
- resourceType: "TailorDB service",
10412
- resourceName: tailordb.namespace
10413
- });
10414
- else conflicts.push({
10280
+ if (trackDesiredResourceOwnership({
10281
+ labels: existing.allLabels,
10282
+ ownerLabel: existing.label,
10283
+ appName,
10284
+ appId,
10415
10285
  resourceType: "TailorDB service",
10416
10286
  resourceName: tailordb.namespace,
10417
- currentOwner: existing.label
10418
- });
10419
- if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areTailorDBServicesEqual(existing.resource, tailordb)) changeSet.unchanged.push({ name: tailordb.namespace });
10287
+ conflicts,
10288
+ unmanaged
10289
+ }) && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areTailorDBServicesEqual(existing.resource, tailordb)) changeSet.unchanged.push({ name: tailordb.namespace });
10420
10290
  else changeSet.updates.push({
10421
10291
  name: tailordb.namespace,
10422
10292
  metaRequest
@@ -10434,10 +10304,13 @@ async function planServices(client, workspaceId, appName, appId, tailordbs) {
10434
10304
  }
10435
10305
  Object.entries(existingServices).forEach(([namespaceName]) => {
10436
10306
  const entry = existingServices[namespaceName];
10437
- const label = entry?.label;
10438
- const owned = isOwnedByApp(entry?.allLabels, appName, appId);
10439
- if (label && !owned) resourceOwners.add(label);
10440
- if (owned) changeSet.deletes.push({
10307
+ if (trackRemainingResourceOwner({
10308
+ labels: entry?.allLabels,
10309
+ ownerLabel: entry?.label,
10310
+ appName,
10311
+ appId,
10312
+ resourceOwners
10313
+ })) changeSet.deletes.push({
10441
10314
  name: namespaceName,
10442
10315
  request: {
10443
10316
  workspaceId,
@@ -11090,19 +10963,340 @@ function getExistingWorkflowJobNames(existing) {
11090
10963
  if (existing.mainJobFunctionName) jobNames.add(existing.mainJobFunctionName);
11091
10964
  return [...jobNames].toSorted();
11092
10965
  }
11093
- function normalizeRetryPolicyForCompare(policy) {
11094
- return {
11095
- maxRetries: policy.maxRetries,
11096
- backoffMultiplier: policy.backoffMultiplier,
11097
- initialBackoff: {
11098
- seconds: String(policy.initialBackoff.seconds),
11099
- nanos: policy.initialBackoff.nanos
11100
- },
11101
- maxBackoff: {
11102
- seconds: String(policy.maxBackoff.seconds),
11103
- nanos: policy.maxBackoff.nanos
11104
- }
11105
- };
10966
+ function normalizeRetryPolicyForCompare(policy) {
10967
+ return {
10968
+ maxRetries: policy.maxRetries,
10969
+ backoffMultiplier: policy.backoffMultiplier,
10970
+ initialBackoff: {
10971
+ seconds: String(policy.initialBackoff.seconds),
10972
+ nanos: policy.initialBackoff.nanos
10973
+ },
10974
+ maxBackoff: {
10975
+ seconds: String(policy.maxBackoff.seconds),
10976
+ nanos: policy.maxBackoff.nanos
10977
+ }
10978
+ };
10979
+ }
10980
+
10981
+ //#endregion
10982
+ //#region src/cli/commands/deploy/apply-phases.ts
10983
+ /**
10984
+ * Apply planned deploy changes for one or more applications.
10985
+ * @param client - Operator client instance
10986
+ * @param workspaceId - Target workspace ID
10987
+ * @param deployments - Planned deployments to apply
10988
+ */
10989
+ async function applyDeploymentPlans(client, workspaceId, deployments) {
10990
+ const forEachDeployment = async (apply) => {
10991
+ for (const deployment of deployments) await apply(deployment);
10992
+ };
10993
+ await withSpan("apply.createUpdateServices", async () => {
10994
+ await forEachDeployment((d) => applySecretManager(client, d.secretManager, "create-update", d.application));
10995
+ await forEachDeployment((d) => applyFunctionRegistry(client, workspaceId, d.functionRegistry, "create-update"));
10996
+ await forEachDeployment((d) => applyStaticWebsite(client, d.staticWebsite, "create-update"));
10997
+ await forEachDeployment((d) => applyAIGateway(client, d.aiGateway, "create-update"));
10998
+ await forEachDeployment((d) => applyIdP(client, d.idp, "create-update"));
10999
+ await forEachDeployment((d) => applyAuth(client, d.auth, "create-update-prerequisites"));
11000
+ await forEachDeployment((d) => applyTailorDB(client, d.tailorDB, "create-update"));
11001
+ await forEachDeployment((d) => applyAuth(client, d.auth, "create-update-dependents"));
11002
+ await forEachDeployment((d) => applyPipeline(client, d.pipeline, "create-update"));
11003
+ });
11004
+ await withSpan("apply.deleteSubgraphResources", async () => {
11005
+ await forEachDeployment((d) => applyPipeline(client, d.pipeline, "delete-resources"));
11006
+ await forEachDeployment((d) => applyAuth(client, d.auth, "delete-resources"));
11007
+ await forEachDeployment((d) => applyIdP(client, d.idp, "delete-resources"));
11008
+ });
11009
+ await withSpan("apply.createUpdateApplication", async () => {
11010
+ await forEachDeployment((d) => applyApplication(client, d.app, "create-update"));
11011
+ });
11012
+ await withSpan("apply.createUpdateDependentServices", async () => {
11013
+ await forEachDeployment((d) => applyExecutor(client, d.executor, "create-update"));
11014
+ await forEachDeployment((d) => applyWorkflow(client, d.workflow, "create-update"));
11015
+ });
11016
+ await withSpan("apply.deleteDependentServices", async () => {
11017
+ await forEachDeployment((d) => applyWorkflow(client, d.workflow, "delete"));
11018
+ await forEachDeployment((d) => applyExecutor(client, d.executor, "delete"));
11019
+ await forEachDeployment((d) => applyStaticWebsite(client, d.staticWebsite, "delete"));
11020
+ await forEachDeployment((d) => applyAIGateway(client, d.aiGateway, "delete"));
11021
+ await forEachDeployment((d) => applySecretManager(client, d.secretManager, "delete", d.application));
11022
+ });
11023
+ await withSpan("apply.deleteApplication", async () => {
11024
+ await forEachDeployment((d) => applyApplication(client, d.app, "delete"));
11025
+ });
11026
+ await withSpan("apply.deleteSubgraphServices", async () => {
11027
+ await forEachDeployment((d) => applyPipeline(client, d.pipeline, "delete-services"));
11028
+ await forEachDeployment((d) => applyAuth(client, d.auth, "delete-services"));
11029
+ await forEachDeployment((d) => applyIdP(client, d.idp, "delete-services"));
11030
+ await forEachDeployment((d) => applyTailorDB(client, d.tailorDB, "delete-services"));
11031
+ });
11032
+ await withSpan("apply.cleanup", async () => {
11033
+ await forEachDeployment((d) => applyFunctionRegistry(client, workspaceId, d.functionRegistry, "delete"));
11034
+ });
11035
+ }
11036
+
11037
+ //#endregion
11038
+ //#region src/cli/commands/deploy/config-id-injector.ts
11039
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11040
+ function findDefineConfigCalls(node, results) {
11041
+ if (!node || typeof node !== "object") return;
11042
+ const n = node;
11043
+ if (n.type === "CallExpression") {
11044
+ const ce = n;
11045
+ if (ce.callee.type === "Identifier" && ce.callee.name === "defineConfig") {
11046
+ const arg = ce.arguments[0];
11047
+ const configObj = arg && arg.type === "ObjectExpression" ? arg : null;
11048
+ results.push({
11049
+ callExpr: ce,
11050
+ configObj
11051
+ });
11052
+ }
11053
+ }
11054
+ for (const key of Object.keys(n)) {
11055
+ const child = n[key];
11056
+ if (Array.isArray(child)) for (const c of child) findDefineConfigCalls(c, results);
11057
+ else if (child && typeof child === "object") findDefineConfigCalls(child, results);
11058
+ }
11059
+ }
11060
+ function findIdProperty(obj) {
11061
+ for (const prop of obj.properties) {
11062
+ if (prop.type !== "Property") continue;
11063
+ if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? prop.key.value : null) === "id") return prop;
11064
+ }
11065
+ return null;
11066
+ }
11067
+ /**
11068
+ * Ensure `tailor.config.ts` has an `id` property on the `defineConfig({...})`
11069
+ * argument. Generates a UUID when missing and writes it back to the file.
11070
+ * Returns null when the file does not contain a `defineConfig()` call (e.g.
11071
+ * a wrapper that re-exports another config).
11072
+ * @param configPath - Absolute path to the config file
11073
+ * @returns Resolved id and whether it was newly injected, or null if skipped
11074
+ */
11075
+ async function ensureConfigId(configPath) {
11076
+ const source = await fs$1.promises.readFile(configPath, "utf-8");
11077
+ const { program } = parseSync(configPath, source);
11078
+ const calls = [];
11079
+ findDefineConfigCalls(program, calls);
11080
+ if (calls.length === 0) return null;
11081
+ if (calls.length > 1) throw new Error(`Multiple defineConfig() calls found in ${configPath}. Only one is supported.`);
11082
+ const { configObj } = assertDefined(calls[0], "defineConfig call site missing");
11083
+ if (!configObj) throw new Error(`defineConfig() argument must be an inline object literal in ${configPath} so the SDK can manage the 'id' field.`);
11084
+ const idProp = findIdProperty(configObj);
11085
+ if (idProp) {
11086
+ const value = idProp.value;
11087
+ if (value.type !== "Literal") throw new Error(`'id' field in ${configPath} must be a string literal. To use this config for a separate app, delete it.`);
11088
+ const literalValue = value.value;
11089
+ if (typeof literalValue !== "string" || literalValue === "") throw new Error(`'id' field in ${configPath} must be a non-empty string literal. To use this config for a separate app, delete it.`);
11090
+ if (!uuidRegex.test(literalValue)) throw new Error(`'id' field in ${configPath} must be a UUID. To use this config for a separate app, delete it.`);
11091
+ return {
11092
+ id: literalValue,
11093
+ injected: false
11094
+ };
11095
+ }
11096
+ const id = crypto.randomUUID();
11097
+ const newSource = insertIdProperty(source, configObj, id);
11098
+ await fs$1.promises.writeFile(configPath, newSource, "utf-8");
11099
+ logger.info(`Generated app id and wrote to ${configPath}: ${id}`);
11100
+ return {
11101
+ id,
11102
+ injected: true
11103
+ };
11104
+ }
11105
+ /**
11106
+ * Read the resolved `defineConfig({...})` `id` from a config file without
11107
+ * mutating it. Returns null when the file has no inline `defineConfig()` call
11108
+ * (wrapper/re-export config), and `{ id: null }` when the call exists but has
11109
+ * no usable `id` property.
11110
+ * @param configPath - Absolute path to the config file
11111
+ * @returns The existing id (or null when absent), or null for wrapper configs
11112
+ */
11113
+ async function readConfigId(configPath) {
11114
+ const { program } = parseSync(configPath, await fs$1.promises.readFile(configPath, "utf-8"));
11115
+ const calls = [];
11116
+ findDefineConfigCalls(program, calls);
11117
+ if (calls.length === 0) return null;
11118
+ if (calls.length > 1) throw new Error(`Multiple defineConfig() calls found in ${configPath}. Only one is supported.`);
11119
+ const { configObj } = assertDefined(calls[0], "defineConfig call site missing");
11120
+ if (!configObj) throw new Error(`defineConfig() argument must be an inline object literal in ${configPath} so the SDK can manage the 'id' field.`);
11121
+ const idProp = findIdProperty(configObj);
11122
+ if (!idProp || idProp.value.type !== "Literal") return { id: null };
11123
+ const value = idProp.value.value;
11124
+ return { id: typeof value === "string" && value !== "" ? value : null };
11125
+ }
11126
+ /**
11127
+ * Read-only CI check: the config must already carry a valid app id.
11128
+ * Wrapper/re-export configs (no inline defineConfig call) are exempt,
11129
+ * mirroring the local behavior where {@link ensureConfigId} no-ops.
11130
+ * @param configPath - Absolute path to the config file
11131
+ */
11132
+ async function assertConfigIdInCI(configPath) {
11133
+ const result = await readConfigId(configPath);
11134
+ if (result === null) return;
11135
+ if (!result.id) throw new Error("tailor.config.ts is missing an 'id'. CI does not auto-generate one (each run would be treated as a separate app and break resource ownership). Run 'tailor-sdk setup' or 'tailor-sdk apply' locally and commit the injected id.");
11136
+ if (!uuidRegex.test(result.id)) throw new Error(`'id' in ${configPath} must be a UUID. To use this config for a separate app, delete it.`);
11137
+ }
11138
+ /**
11139
+ * Ensure the config has an app id for a deploy run.
11140
+ *
11141
+ * Locally, the id is auto-injected when missing (via {@link ensureConfigId}).
11142
+ * In CI, the id is never auto-injected — a missing id is a hard error, because
11143
+ * generating one per run would create a fresh app each time and break resource
11144
+ * ownership. CI dry-runs (plan) perform the same check read-only, so a
11145
+ * forgotten id fails at PR time instead of at deploy. Ephemeral pipelines that
11146
+ * intentionally deploy a fresh app per run (such as e2e harnesses) can opt
11147
+ * back into injection with `TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION=true`.
11148
+ * Local dry-run and build-only flows skip both injection and the check (no
11149
+ * on-disk side effects are expected, and build-only never talks to the
11150
+ * platform).
11151
+ * @param obj - Inputs
11152
+ * @param obj.configPath - Absolute path to the config file
11153
+ * @param obj.dryRun - Whether this is a dry-run
11154
+ * @param obj.buildOnly - Whether this is a build-only run
11155
+ */
11156
+ async function ensureConfigIdForDeploy(obj) {
11157
+ const { configPath, dryRun, buildOnly } = obj;
11158
+ if (buildOnly) return;
11159
+ const allowCIInjection = parseBoolean(process.env.TAILOR_PLATFORM_SDK_ALLOW_CI_ID_INJECTION) === true;
11160
+ const strictCI = isCI && !allowCIInjection;
11161
+ if (dryRun) {
11162
+ if (strictCI) await assertConfigIdInCI(configPath);
11163
+ return;
11164
+ }
11165
+ if (strictCI) {
11166
+ await assertConfigIdInCI(configPath);
11167
+ return;
11168
+ }
11169
+ await ensureConfigId(configPath);
11170
+ }
11171
+ const idComment = "// SDK-managed app id — do not edit, except when copying this config to a separate app.";
11172
+ function insertIdProperty(source, configObj, id) {
11173
+ const idLiteral = `id: ${JSON.stringify(id)}`;
11174
+ if (configObj.properties.length > 0) {
11175
+ const firstProp = assertDefined(configObj.properties[0], "first property missing");
11176
+ const lineStart = source.lastIndexOf("\n", firstProp.start - 1) + 1;
11177
+ const indent = source.slice(lineStart, firstProp.start);
11178
+ if (!/^[\t ]*$/.test(indent)) return source.slice(0, firstProp.start) + `${idLiteral}, ` + source.slice(firstProp.start);
11179
+ const insertion = `${idComment}\n${indent}${idLiteral},\n${indent}`;
11180
+ return source.slice(0, firstProp.start) + insertion + source.slice(firstProp.start);
11181
+ }
11182
+ const openBracePos = configObj.start + 1;
11183
+ const braceLineStart = source.lastIndexOf("\n", configObj.start) + 1;
11184
+ const baseIndent = source.slice(braceLineStart).match(/^[\t ]*/)?.[0] ?? "";
11185
+ const innerIndent = `${baseIndent} `;
11186
+ const insertion = `\n${innerIndent}${idComment}\n${innerIndent}${idLiteral},\n${baseIndent}`;
11187
+ return source.slice(0, openBracePos) + insertion + source.slice(openBracePos);
11188
+ }
11189
+
11190
+ //#endregion
11191
+ //#region src/cli/commands/deploy/confirm.ts
11192
+ /**
11193
+ * Confirm reassignment of resources when owner conflicts are detected.
11194
+ * Splits into two scenarios: id regeneration (same sdk-name, different
11195
+ * sdk-app-id) and name mismatch (different sdk-name). Each gets its own
11196
+ * prompt because the user-facing meaning is different.
11197
+ * @param conflicts - Detected owner conflicts
11198
+ * @param appName - Target application name
11199
+ * @param yes - Whether to auto-confirm without prompting
11200
+ * @returns Promise that resolves when confirmation completes
11201
+ */
11202
+ async function confirmOwnerConflict(conflicts, appName, yes) {
11203
+ if (conflicts.length === 0) return;
11204
+ const idRegenerated = conflicts.filter((c) => c.currentOwner === appName);
11205
+ const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
11206
+ if (idRegenerated.length > 0) await confirmIdRegeneration(idRegenerated, appName, yes);
11207
+ if (nameMismatches.length > 0) await confirmNameMismatch(nameMismatches, appName, yes);
11208
+ }
11209
+ async function confirmIdRegeneration(conflicts, appName, yes) {
11210
+ logger.warn(`Application id was regenerated for "${appName}":`);
11211
+ logger.log(" These resources still carry the previous id.");
11212
+ logger.newline();
11213
+ logger.log(` ${styles.info("Resources")}:`);
11214
+ for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
11215
+ if (yes) {
11216
+ logger.success("Re-tagging resources with the new id (--yes flag specified)...", { mode: "plain" });
11217
+ return;
11218
+ }
11219
+ if (!await prompt.confirm({
11220
+ message: `Re-tag these resources with the new id for "${appName}"?\n${styles.dim("(The id in tailor.config.ts was removed since the previous deploy, so a new one was generated)")}`,
11221
+ default: false
11222
+ })) throw new Error(multiline`
11223
+ Apply cancelled. Resources remain tagged with the previous id.
11224
+ To override, run again and confirm, or use --yes flag.
11225
+ `);
11226
+ }
11227
+ async function confirmNameMismatch(conflicts, appName, yes) {
11228
+ const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
11229
+ logger.warn("Application name mismatch detected:");
11230
+ logger.log(` ${styles.warning("Current application(s)")}: ${currentOwners.map((o) => styles.bold(`"${o}"`)).join(", ")}`);
11231
+ logger.log(` ${styles.success("New application")}: ${styles.bold(`"${appName}"`)}`);
11232
+ logger.newline();
11233
+ logger.log(` ${styles.info("Resources")}:`);
11234
+ for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
11235
+ if (yes) {
11236
+ logger.success("Updating resources (--yes flag specified)...", { mode: "plain" });
11237
+ return;
11238
+ }
11239
+ const promptMessage = currentOwners.length === 1 ? `Update these resources to be managed by "${appName}"?\n${styles.dim("(Common when renaming your application)")}` : `Update these resources to be managed by "${appName}"?`;
11240
+ if (!await prompt.confirm({
11241
+ message: promptMessage,
11242
+ default: false
11243
+ })) throw new Error(multiline`
11244
+ Apply cancelled. Resources remain managed by their current applications.
11245
+ To override, run again and confirm, or use --yes flag.
11246
+ `);
11247
+ }
11248
+ /**
11249
+ * Confirm allowing tailor-sdk to manage previously unmanaged resources.
11250
+ * @param resources - Unmanaged resources
11251
+ * @param appName - Target application name
11252
+ * @param yes - Whether to auto-confirm without prompting
11253
+ * @returns Promise that resolves when confirmation completes
11254
+ */
11255
+ async function confirmUnmanagedResources(resources, appName, yes) {
11256
+ if (resources.length === 0) return;
11257
+ logger.warn("Existing resources not tracked by tailor-sdk were found:");
11258
+ logger.log(` ${styles.info("Resources")}:`);
11259
+ for (const r of resources) logger.log(` • ${styles.bold(r.resourceType)} ${styles.info(`"${r.resourceName}"`)}`);
11260
+ logger.newline();
11261
+ logger.log(" These resources may have been created by older SDK versions, Terraform, or CUE.");
11262
+ logger.log(" To continue, confirm that tailor-sdk should manage them.");
11263
+ logger.log(" If they are managed by another tool (e.g., Terraform), cancel and manage them there instead.");
11264
+ if (yes) {
11265
+ logger.success(`Adding to "${appName}" (--yes flag specified)...`, { mode: "plain" });
11266
+ return;
11267
+ }
11268
+ if (!await prompt.confirm({
11269
+ message: `Allow tailor-sdk to manage these resources for "${appName}"?`,
11270
+ default: false
11271
+ })) throw new Error(multiline`
11272
+ Apply cancelled. Resources remain unmanaged.
11273
+ To override, run again and confirm, or use --yes flag.
11274
+ `);
11275
+ }
11276
+ /**
11277
+ * Confirm deletion of important resources.
11278
+ * @param resources - Resources scheduled for deletion
11279
+ * @param yes - Whether to auto-confirm without prompting
11280
+ * @returns Promise that resolves when confirmation completes
11281
+ */
11282
+ async function confirmImportantResourceDeletion(resources, yes) {
11283
+ if (resources.length === 0) return;
11284
+ logger.warn("The following resources will be deleted:");
11285
+ logger.log(` ${styles.info("Resources")}:`);
11286
+ for (const r of resources) logger.log(` • ${styles.bold(r.resourceType)} ${styles.error(`"${r.resourceName}"`)}`);
11287
+ logger.newline();
11288
+ logger.log(styles.warning(" Deleting these resources will permanently remove all associated data."));
11289
+ if (yes) {
11290
+ logger.success("Deleting resources (--yes flag specified)...", { mode: "plain" });
11291
+ return;
11292
+ }
11293
+ if (!await prompt.confirm({
11294
+ message: "Are you sure you want to delete these resources?",
11295
+ default: false
11296
+ })) throw new Error(multiline`
11297
+ Apply cancelled. Resources will not be deleted.
11298
+ To override, run again and confirm, or use --yes flag.
11299
+ `);
11106
11300
  }
11107
11301
 
11108
11302
  //#endregion
@@ -11336,17 +11530,126 @@ async function validatePlan(input) {
11336
11530
  */
11337
11531
  function collectIdpUserTriggerTargets(application) {
11338
11532
  const targets = /* @__PURE__ */ new Set();
11339
- const idps = application.idpServices;
11533
+ const idps = collectApplicationIdpNames(application);
11340
11534
  for (const executor of Object.values(application.executorService?.executors ?? {})) {
11341
11535
  if (executor.trigger.kind !== "idpUser") continue;
11342
11536
  if (executor.trigger.idp != null) targets.add(executor.trigger.idp);
11343
- else if (idps.length === 1) {
11537
+ else if (idps.size === 1) {
11344
11538
  const [idp] = idps;
11345
- if (idp) targets.add(idp.name);
11539
+ if (idp) targets.add(idp);
11346
11540
  }
11347
11541
  }
11348
11542
  return targets;
11349
11543
  }
11544
+ function collectDeployIdpUserTriggerTargets(targets) {
11545
+ const triggerTargets = /* @__PURE__ */ new Set();
11546
+ for (const target of targets) for (const idpName of collectIdpUserTriggerTargets(target.application)) triggerTargets.add(idpName);
11547
+ return triggerTargets;
11548
+ }
11549
+ function collectExecutorUsedTailorDBTypes(owner, targets) {
11550
+ const usedTypes = /* @__PURE__ */ new Set();
11551
+ const ownerNamespaces = new Set(owner.application.tailorDBServices.map((service) => service.namespace));
11552
+ const applications = targets.map((target) => target.application);
11553
+ for (const target of targets) {
11554
+ const visibleNamespaces = collectVisibleTailorDBTypeNamespaces(target.application, applications);
11555
+ for (const executor of Object.values(target.application.executorService?.executors ?? {})) if (executor.trigger.kind === "tailordb") {
11556
+ const namespace = findTailorDBNamespace(target.application, executor.trigger.typeName) ?? resolveSameRunNamespace(visibleNamespaces, executor.trigger.typeName, "TailorDB type");
11557
+ if (namespace && ownerNamespaces.has(namespace)) usedTypes.add(executor.trigger.typeName);
11558
+ }
11559
+ }
11560
+ return usedTypes;
11561
+ }
11562
+ function collectExecutorUsedResolvers(owner, targets) {
11563
+ const usedResolvers = /* @__PURE__ */ new Set();
11564
+ const ownerNamespaces = new Set(owner.application.resolverServices.map((service) => service.namespace));
11565
+ const applications = targets.map((target) => target.application);
11566
+ for (const target of targets) {
11567
+ const visibleNamespaces = collectVisibleResolverNamespaces(target.application, applications);
11568
+ for (const executor of Object.values(target.application.executorService?.executors ?? {})) if (executor.trigger.kind === "resolverExecuted") {
11569
+ const namespace = findResolverNamespace(target.application, executor.trigger.resolverName) ?? resolveSameRunNamespace(visibleNamespaces, executor.trigger.resolverName, "Resolver");
11570
+ if (namespace && ownerNamespaces.has(namespace)) usedResolvers.add(executor.trigger.resolverName);
11571
+ }
11572
+ }
11573
+ return usedResolvers;
11574
+ }
11575
+ function collectExpectedLocalStaticWebsiteNames(targets) {
11576
+ const websiteNames = /* @__PURE__ */ new Set();
11577
+ for (const target of targets) for (const website of target.application.staticWebsiteServices) websiteNames.add(website.name);
11578
+ return websiteNames;
11579
+ }
11580
+ function addPossiblyAmbiguousNamespace(namespaces, key, namespace) {
11581
+ if (namespaces.has(key)) {
11582
+ if (namespaces.get(key) !== namespace) namespaces.set(key, void 0);
11583
+ return;
11584
+ }
11585
+ namespaces.set(key, namespace);
11586
+ }
11587
+ function collectVisibleResources(params) {
11588
+ const { application, applications, visibleKeysOf, resourcesOf, createResult, addResource } = params;
11589
+ const visibleKeys = visibleKeysOf(application);
11590
+ const result = createResult();
11591
+ for (const candidate of applications) for (const resource of resourcesOf(candidate)) {
11592
+ if (!visibleKeys.has(resource.visibilityKey)) continue;
11593
+ addResource(result, resource.resourceKey, resource.visibilityKey);
11594
+ }
11595
+ return result;
11596
+ }
11597
+ function collectApplicationTailorDBNamespaces(application) {
11598
+ return /* @__PURE__ */ new Set([...application.tailorDBServices.map((service) => service.namespace), ...application.externalTailorDBNamespaces]);
11599
+ }
11600
+ function* tailorDBTypeResources(application) {
11601
+ for (const service of application.tailorDBServices) for (const typeName of Object.keys(service.types)) yield {
11602
+ visibilityKey: service.namespace,
11603
+ resourceKey: typeName
11604
+ };
11605
+ }
11606
+ function collectVisibleTailorDBTypeNamespaces(application, applications) {
11607
+ return collectVisibleResources({
11608
+ application,
11609
+ applications,
11610
+ visibleKeysOf: collectApplicationTailorDBNamespaces,
11611
+ resourcesOf: tailorDBTypeResources,
11612
+ createResult: () => /* @__PURE__ */ new Map(),
11613
+ addResource: addPossiblyAmbiguousNamespace
11614
+ });
11615
+ }
11616
+ function collectApplicationResolverNamespaces(application) {
11617
+ return new Set(application.subgraphs.filter((subgraph) => subgraph.Type === "pipeline").map((subgraph) => subgraph.Name));
11618
+ }
11619
+ function* idpNameResources(application) {
11620
+ for (const name of collectApplicationIdpNames(application)) yield {
11621
+ visibilityKey: name,
11622
+ resourceKey: name
11623
+ };
11624
+ }
11625
+ function collectVisibleIdpNames(application, applications) {
11626
+ return collectVisibleResources({
11627
+ application,
11628
+ applications,
11629
+ visibleKeysOf: collectApplicationIdpNames,
11630
+ resourcesOf: idpNameResources,
11631
+ createResult: () => /* @__PURE__ */ new Set(),
11632
+ addResource: (names, name) => {
11633
+ names.add(name);
11634
+ }
11635
+ });
11636
+ }
11637
+ function* resolverResources(application) {
11638
+ for (const service of application.resolverServices) for (const resolver of Object.values(service.resolvers)) yield {
11639
+ visibilityKey: service.namespace,
11640
+ resourceKey: resolver.name
11641
+ };
11642
+ }
11643
+ function collectVisibleResolverNamespaces(application, applications) {
11644
+ return collectVisibleResources({
11645
+ application,
11646
+ applications,
11647
+ visibleKeysOf: collectApplicationResolverNamespaces,
11648
+ resourcesOf: resolverResources,
11649
+ createResult: () => /* @__PURE__ */ new Map(),
11650
+ addResource: addPossiblyAmbiguousNamespace
11651
+ });
11652
+ }
11350
11653
  async function shouldForceApplyAll(client, workspaceId, application, functionEntries) {
11351
11654
  const desiredLabels = (await buildMetaRequest({
11352
11655
  trn: resourceTrn(workspaceId, "application", application.name),
@@ -11395,27 +11698,19 @@ async function shouldForceApplyAll(client, workspaceId, application, functionEnt
11395
11698
  }
11396
11699
  /**
11397
11700
  * Decide which renamed-away applications should be deleted. Excludes the
11398
- * target itself: id regeneration alone keeps the name unchanged, so deleting
11399
- * by name would destroy the live app.
11701
+ * deploy targets themselves: id regeneration alone keeps the name unchanged,
11702
+ * so deleting by name would destroy a live app.
11400
11703
  * @param params - Inputs for the computation
11401
11704
  * @param params.conflicts - Detected owner conflicts across all services
11402
11705
  * @param params.resourceOwners - App names that still own resources we don't manage
11403
- * @param params.targetAppName - The application currently being deployed
11706
+ * @param params.protectedAppNames - App names that must not be deleted
11404
11707
  * @returns Names of empty old applications that should be deleted
11405
11708
  */
11406
11709
  function computeRenamedAppDeletions(params) {
11407
- const { conflicts, resourceOwners, targetAppName } = params;
11408
- return [...new Set(conflicts.map((c) => c.currentOwner))].filter((owner) => !resourceOwners.has(owner) && owner !== targetAppName);
11710
+ const { conflicts, resourceOwners, protectedAppNames } = params;
11711
+ return [...new Set(conflicts.map((c) => c.currentOwner))].filter((owner) => !resourceOwners.has(owner) && !protectedAppNames.has(owner));
11409
11712
  }
11410
- /**
11411
- * Format and output the plan results, then return a summary of change counts.
11412
- * In JSON dry-run mode a JSON payload is written to stdout. In all other modes
11413
- * the human-readable diff goes to stdout (dry-run) or stderr (apply).
11414
- * @param results - Planned results across all services
11415
- * @param opts - Output options (dry-run mode flag)
11416
- * @returns Aggregated plan summary counts
11417
- */
11418
- function printPlanResults(results, opts) {
11713
+ function buildPlanReport(results) {
11419
11714
  const executorEntries = formatExecutorChangeEntries(results.executor.changeSet, buildPlannedExecutorsByName(results.executor.changeSet), results.functionRegistry.executorFunctionChanges);
11420
11715
  const resolverEntries = formatResolverChangeEntries(results.pipeline.changeSet.resolver, results.functionRegistry.resolverFunctionChanges);
11421
11716
  const workflowEntries = formatWorkflowChangeEntries(results.workflow.changeSet, results.functionRegistry.workflowJobChanges);
@@ -11482,132 +11777,808 @@ function printPlanResults(results, opts) {
11482
11777
  ...results.workflow.conflicts,
11483
11778
  ...results.secretManager.conflicts
11484
11779
  ];
11485
- if (logger.jsonMode && opts?.dryRun) {
11486
- const changes = [
11487
- ...allDisplayEntries,
11488
- ...tailorDBServiceActions.map(({ action, name }) => ({
11489
- action,
11490
- name,
11491
- labels: ["tailorDB"],
11492
- namespace: void 0
11493
- })),
11494
- ...pipelineServiceActions.map(({ action, name }) => ({
11495
- action,
11496
- name,
11497
- labels: ["pipeline"],
11498
- namespace: void 0
11499
- })),
11500
- ...idpServiceActions.map(({ action, name }) => ({
11501
- action,
11502
- name,
11503
- labels: ["idp"],
11504
- namespace: void 0
11505
- })),
11506
- ...authServiceActions.map(({ action, name }) => ({
11507
- action,
11508
- name,
11509
- labels: ["auth"],
11510
- namespace: void 0
11511
- })),
11512
- ...formatChangeSetEntries(otherFunctionRegistryChanges),
11513
- ...formatChangeSetEntries(results.staticWebsite.changeSet, ["staticWebsite"]),
11514
- ...formatChangeSetEntries(results.staticWebsite.customDomainChangeSet, ["customDomain"]),
11515
- ...formatChangeSetEntries(results.aiGateway.changeSet, ["aiGateway"]),
11516
- ...formatChangeSetEntries(results.app, ["application"]),
11517
- ...formatChangeSetEntries(results.secretManager.vaultChangeSet, ["vault"]),
11518
- ...formatChangeSetEntries(results.secretManager.secretChangeSet, ["secret"])
11519
- ].map(({ action, name, labels, namespace }) => ({
11520
- action,
11521
- name,
11522
- labels,
11523
- namespace
11524
- }));
11525
- const warnings = [...allUnmanaged.map(({ resourceType, resourceName }) => ({
11526
- type: "unmanaged",
11527
- resourceType,
11528
- name: resourceName
11529
- })), ...results.secretManager.skippedSecrets.map((name) => ({
11530
- type: "skippedSecret",
11531
- resourceType: "secret",
11532
- name
11533
- }))];
11534
- const conflicts = allConflicts.map(({ resourceType, resourceName, currentOwner }) => ({
11535
- resourceType,
11536
- name: resourceName,
11537
- currentOwner
11538
- }));
11539
- logger.out({
11540
- summary,
11541
- changes,
11542
- warnings,
11543
- conflicts
11544
- });
11545
- return summary;
11546
- }
11547
- const allLines = [
11548
- ...buildGroupedDisplayLines(results.functionRegistry.changeSet.title, formatChangeSetEntries(otherFunctionRegistryChanges)),
11549
- ...results.staticWebsite.changeSet.lines(),
11550
- ...results.staticWebsite.customDomainChangeSet.lines(),
11551
- ...results.aiGateway.changeSet.lines(),
11552
- ...results.app.lines(),
11553
- ...buildGroupedDisplayLines("TailorDB", tailorDBEntries, tailorDBServiceActions),
11554
- ...buildGroupedDisplayLines("Resolver", pipelineEntries, pipelineServiceActions),
11555
- ...buildGroupedDisplayLines("Executor", executorEntries),
11556
- ...buildGroupedDisplayLines("Workflow", workflowEntries),
11557
- ...buildGroupedDisplayLines("IdP", idpEntries, idpServiceActions),
11558
- ...buildGroupedDisplayLines("Auth", authEntries, authServiceActions),
11559
- ...results.secretManager.vaultChangeSet.lines(),
11560
- ...results.secretManager.secretChangeSet.lines()
11780
+ const changes = [
11781
+ ...allDisplayEntries,
11782
+ ...tailorDBServiceActions.map(({ action, name }) => ({
11783
+ action,
11784
+ name,
11785
+ labels: ["tailorDB"],
11786
+ namespace: void 0
11787
+ })),
11788
+ ...pipelineServiceActions.map(({ action, name }) => ({
11789
+ action,
11790
+ name,
11791
+ labels: ["pipeline"],
11792
+ namespace: void 0
11793
+ })),
11794
+ ...idpServiceActions.map(({ action, name }) => ({
11795
+ action,
11796
+ name,
11797
+ labels: ["idp"],
11798
+ namespace: void 0
11799
+ })),
11800
+ ...authServiceActions.map(({ action, name }) => ({
11801
+ action,
11802
+ name,
11803
+ labels: ["auth"],
11804
+ namespace: void 0
11805
+ })),
11806
+ ...formatChangeSetEntries(otherFunctionRegistryChanges),
11807
+ ...formatChangeSetEntries(results.staticWebsite.changeSet, ["staticWebsite"]),
11808
+ ...formatChangeSetEntries(results.staticWebsite.customDomainChangeSet, ["customDomain"]),
11809
+ ...formatChangeSetEntries(results.aiGateway.changeSet, ["aiGateway"]),
11810
+ ...formatChangeSetEntries(results.app, ["application"]),
11811
+ ...formatChangeSetEntries(results.secretManager.vaultChangeSet, ["vault"]),
11812
+ ...formatChangeSetEntries(results.secretManager.secretChangeSet, ["secret"])
11813
+ ].map(({ action, name, labels, namespace }) => ({
11814
+ action,
11815
+ name,
11816
+ labels,
11817
+ namespace
11818
+ }));
11819
+ const warnings = [...allUnmanaged.map(({ resourceType, resourceName }) => ({
11820
+ type: "unmanaged",
11821
+ resourceType,
11822
+ name: resourceName
11823
+ })), ...results.secretManager.skippedSecrets.map((name) => ({
11824
+ type: "skippedSecret",
11825
+ resourceType: "secret",
11826
+ name
11827
+ }))];
11828
+ const conflicts = allConflicts.map(({ resourceType, resourceName, currentOwner }) => ({
11829
+ resourceType,
11830
+ name: resourceName,
11831
+ currentOwner
11832
+ }));
11833
+ const allLines = [
11834
+ ...buildGroupedDisplayLines(results.functionRegistry.changeSet.title, formatChangeSetEntries(otherFunctionRegistryChanges)),
11835
+ ...results.staticWebsite.changeSet.lines(),
11836
+ ...results.staticWebsite.customDomainChangeSet.lines(),
11837
+ ...results.aiGateway.changeSet.lines(),
11838
+ ...results.app.lines(),
11839
+ ...buildGroupedDisplayLines("TailorDB", tailorDBEntries, tailorDBServiceActions),
11840
+ ...buildGroupedDisplayLines("Resolver", pipelineEntries, pipelineServiceActions),
11841
+ ...buildGroupedDisplayLines("Executor", executorEntries),
11842
+ ...buildGroupedDisplayLines("Workflow", workflowEntries),
11843
+ ...buildGroupedDisplayLines("IdP", idpEntries, idpServiceActions),
11844
+ ...buildGroupedDisplayLines("Auth", authEntries, authServiceActions),
11845
+ ...results.secretManager.vaultChangeSet.lines(),
11846
+ ...results.secretManager.secretChangeSet.lines()
11847
+ ];
11848
+ if (allUnmanaged.length > 0) {
11849
+ allLines.push(styles.bold("Unmanaged resources (not in config):"));
11850
+ for (const { resourceType, resourceName } of allUnmanaged) allLines.push(` ${styles.warning("")} ${styles.bold(resourceType)} "${resourceName}"`);
11851
+ }
11852
+ if (results.secretManager.skippedSecrets.length > 0) {
11853
+ allLines.push(styles.bold("Secret Manager secrets (skipped - no value provided):"));
11854
+ for (const name of results.secretManager.skippedSecrets) allLines.push(` ${styles.dim("○")} ${name}`);
11855
+ }
11856
+ if (allConflicts.length > 0) {
11857
+ allLines.push(styles.bold("Owner conflicts (will require confirmation on apply):"));
11858
+ for (const { resourceType, resourceName, currentOwner } of allConflicts) allLines.push(` ${styles.warning("!")} ${styles.bold(resourceType)} "${resourceName}" — owned by "${currentOwner}"`);
11859
+ }
11860
+ allLines.push(formatPlanSummary(summary));
11861
+ return {
11862
+ summary,
11863
+ json: {
11864
+ summary,
11865
+ changes,
11866
+ warnings,
11867
+ conflicts
11868
+ },
11869
+ lines: allLines
11870
+ };
11871
+ }
11872
+ /**
11873
+ * Format and output the plan results, then return a summary of change counts.
11874
+ * In JSON dry-run mode a JSON payload is written to stdout. In all other modes
11875
+ * the human-readable diff goes to stdout (dry-run) or stderr (apply).
11876
+ * @param results - Planned results across all services
11877
+ * @param opts - Output options (dry-run mode flag)
11878
+ * @returns Aggregated plan summary counts
11879
+ */
11880
+ function printPlanResults(results, opts) {
11881
+ const report = buildPlanReport(results);
11882
+ if (logger.jsonMode && opts?.dryRun) {
11883
+ logger.out(report.json);
11884
+ return report.summary;
11885
+ }
11886
+ const output = report.lines.join("\n");
11887
+ if (opts?.dryRun) logger.out(output);
11888
+ else logger.log(output);
11889
+ return report.summary;
11890
+ }
11891
+ /**
11892
+ * Summarize plan counts from display entries, service actions, and non-grouped changesets.
11893
+ * @param results - Planned apply results
11894
+ * @param displayEntries - All grouped display entries across sections
11895
+ * @param serviceActions - All service-level namespace actions
11896
+ * @returns Aggregated plan summary
11897
+ */
11898
+ function summarizePlanResults(results, displayEntries, serviceActions) {
11899
+ const summary = {
11900
+ create: 0,
11901
+ update: 0,
11902
+ delete: 0,
11903
+ replace: 0
11904
+ };
11905
+ for (const entry of displayEntries) summary[entry.action] += 1;
11906
+ for (const sa of serviceActions) summary[sa.action] += 1;
11907
+ const { otherChanges } = splitFunctionRegistryChanges(results.functionRegistry.changeSet);
11908
+ const nonGrouped = summarizeChangeSets([
11909
+ otherChanges,
11910
+ results.staticWebsite.changeSet,
11911
+ results.staticWebsite.customDomainChangeSet,
11912
+ results.aiGateway.changeSet,
11913
+ results.app,
11914
+ results.secretManager.vaultChangeSet,
11915
+ results.secretManager.secretChangeSet
11916
+ ]);
11917
+ summary.create += nonGrouped.create;
11918
+ summary.update += nonGrouped.update;
11919
+ summary.delete += nonGrouped.delete;
11920
+ summary.replace += nonGrouped.replace;
11921
+ return summary;
11922
+ }
11923
+ /**
11924
+ * Parse the deploy config option into one or more config paths.
11925
+ * @param configPath - Raw `--config` option value
11926
+ * @returns Config paths, or one undefined entry to preserve default config lookup
11927
+ */
11928
+ function parseDeployConfigPaths(configPath) {
11929
+ const rawConfigPath = configPath ?? process.env.TAILOR_PLATFORM_SDK_CONFIG_PATH;
11930
+ if (rawConfigPath === void 0) return [void 0];
11931
+ const configPaths = rawConfigPath.split(",").map((entry) => entry.trim());
11932
+ if (configPaths.some((entry) => entry.length === 0)) throw new Error("--config must contain one or more non-empty config paths.");
11933
+ return configPaths;
11934
+ }
11935
+ async function buildDeploymentTarget(params) {
11936
+ const { configPath, dryRun, buildOnly, noCache, packageVersion, cacheDir } = params;
11937
+ const { config, plugins } = await withSpan("build.loadConfig", async () => {
11938
+ const foundPath = loadConfigPath(configPath);
11939
+ if (foundPath) {
11940
+ const resolvedPath = path.resolve(process.cwd(), foundPath);
11941
+ if (fs$1.existsSync(resolvedPath)) await ensureConfigIdForDeploy({
11942
+ configPath: resolvedPath,
11943
+ dryRun,
11944
+ buildOnly
11945
+ });
11946
+ }
11947
+ return loadConfig(configPath);
11948
+ });
11949
+ const configDir = path.dirname(config.path);
11950
+ const lockfilePath = findUpSync("pnpm-lock.yaml", { cwd: configDir }) ?? findUpSync("package-lock.json", { cwd: configDir }) ?? findUpSync("yarn.lock", { cwd: configDir }) ?? findUpSync("bun.lock", { cwd: configDir });
11951
+ const cacheManager = createCacheManager({
11952
+ enabled: !noCache,
11953
+ cacheDir,
11954
+ sdkVersion: packageVersion,
11955
+ lockfileHash: lockfilePath ? hashFile(lockfilePath) : void 0
11956
+ });
11957
+ let pluginManager;
11958
+ if (plugins.length > 0) pluginManager = new PluginManager(plugins);
11959
+ await withSpan("build.generateUserTypes", () => generateUserTypes({
11960
+ config,
11961
+ configPath: config.path
11962
+ }));
11963
+ let application;
11964
+ let workflowBuildResult;
11965
+ let httpAdapterBuildResult;
11966
+ let bundledScripts;
11967
+ try {
11968
+ const result = await withSpan("build.loadApplication", () => loadApplication({
11969
+ config,
11970
+ pluginManager,
11971
+ bundleCache: cacheManager.bundleCache
11972
+ }));
11973
+ application = result.application;
11974
+ workflowBuildResult = result.workflowBuildResult;
11975
+ httpAdapterBuildResult = result.httpAdapterBuildResult;
11976
+ bundledScripts = result.bundledScripts;
11977
+ } finally {
11978
+ cacheManager.finalize();
11979
+ }
11980
+ return {
11981
+ config,
11982
+ application,
11983
+ workflowBuildResult,
11984
+ httpAdapterBuildResult,
11985
+ bundledScripts
11986
+ };
11987
+ }
11988
+ async function buildDeploymentTargets(params) {
11989
+ const { configPaths, buildTarget = buildDeploymentTarget, ...targetParams } = params;
11990
+ return Promise.all(configPaths.map((configPath) => buildTarget({
11991
+ ...targetParams,
11992
+ configPath
11993
+ })));
11994
+ }
11995
+ function setBundledScript(target, name, code, kind) {
11996
+ if (target.has(name)) throw new Error(`Duplicate ${kind} bundle name "${name}" across config files.`);
11997
+ target.set(name, code);
11998
+ }
11999
+ function addBundledScripts(target, source, kind) {
12000
+ for (const [name, code] of source) setBundledScript(target, name, code, kind);
12001
+ }
12002
+ function collectDuplicateBundleNames(targets, select) {
12003
+ const seen = /* @__PURE__ */ new Set();
12004
+ const duplicates = /* @__PURE__ */ new Set();
12005
+ for (const target of targets) for (const name of select(target).keys()) {
12006
+ if (seen.has(name)) duplicates.add(name);
12007
+ seen.add(name);
12008
+ }
12009
+ return duplicates;
12010
+ }
12011
+ function addResolverBundledScripts(target, source, duplicateNames) {
12012
+ const consumedNames = /* @__PURE__ */ new Set();
12013
+ for (const service of source.application.resolverServices) for (const resolver of Object.values(service.resolvers)) {
12014
+ const code = source.bundledScripts.resolvers.get(resolver.name);
12015
+ if (code === void 0) continue;
12016
+ setBundledScript(target, duplicateNames.has(resolver.name) ? resolverFunctionName(service.namespace, resolver.name) : resolver.name, code, "resolver");
12017
+ consumedNames.add(resolver.name);
12018
+ }
12019
+ for (const [name, code] of source.bundledScripts.resolvers) if (!consumedNames.has(name)) setBundledScript(target, name, code, "resolver");
12020
+ }
12021
+ /**
12022
+ * Merge per-config bundled scripts into one build-only result.
12023
+ * @param targets - Built deployment targets to merge
12024
+ * @returns Combined bundled scripts across all targets
12025
+ */
12026
+ function mergeBundledScripts(targets) {
12027
+ const bundledScripts = {
12028
+ resolvers: /* @__PURE__ */ new Map(),
12029
+ executors: /* @__PURE__ */ new Map(),
12030
+ workflowJobs: /* @__PURE__ */ new Map(),
12031
+ authHooks: /* @__PURE__ */ new Map()
12032
+ };
12033
+ const duplicateResolverNames = collectDuplicateBundleNames(targets, (target) => target.bundledScripts.resolvers);
12034
+ for (const target of targets) {
12035
+ addResolverBundledScripts(bundledScripts.resolvers, target, duplicateResolverNames);
12036
+ addBundledScripts(bundledScripts.executors, target.bundledScripts.executors, "executor");
12037
+ addBundledScripts(bundledScripts.workflowJobs, target.bundledScripts.workflowJobs, "workflow job");
12038
+ addBundledScripts(bundledScripts.authHooks, target.bundledScripts.authHooks, "auth hook");
12039
+ }
12040
+ return bundledScripts;
12041
+ }
12042
+ /**
12043
+ * Reject resource names that collide across configs. Each checked resource
12044
+ * type is workspace-global (its TRN is not qualified by namespace or app), so
12045
+ * a duplicate name would make two configs target the same platform resource:
12046
+ * one config's apply could overwrite the other's, or a fresh create could be
12047
+ * attempted twice. Resolver and auth-hook function names are namespace- or
12048
+ * app-qualified and are intentionally excluded.
12049
+ * @param targets - Built deployment targets to check
12050
+ */
12051
+ function assertUniqueGlobalResourceNames(targets) {
12052
+ for (const check of DEPLOY_MANAGED_RESOURCE_DEFINITIONS) {
12053
+ const seen = /* @__PURE__ */ new Set();
12054
+ for (const target of targets) for (const name of check.namesOf(target)) {
12055
+ if (seen.has(name)) throw new Error(`Duplicate ${check.resourceLabel} name "${name}" across config files. ${check.resourceLabel} names must be unique across all configs in a single deploy.`);
12056
+ seen.add(name);
12057
+ }
12058
+ }
12059
+ }
12060
+ function collectPlannedExternalTailorDBServices(target, targets) {
12061
+ const externalNamespaces = new Set(target.application.externalTailorDBNamespaces);
12062
+ if (externalNamespaces.size === 0) return [];
12063
+ return targets.flatMap((candidate) => candidate.application.tailorDBServices.filter((service) => externalNamespaces.has(service.namespace)));
12064
+ }
12065
+ function collectExternalAuthIdpConfigNames(targets) {
12066
+ const idpConfigNames = /* @__PURE__ */ new Map();
12067
+ for (const target of targets) {
12068
+ const authService = target.application.authService;
12069
+ if (!authService) continue;
12070
+ const { name } = authService.config;
12071
+ const idpConfigName = authService.config.idProvider?.name;
12072
+ if (idpConfigNames.has(name) && idpConfigNames.get(name) !== idpConfigName) throw new Error(`Auth namespace "${name}" is defined by multiple config files with different IdP configs. Auth namespace names must be unique across all configs in a single deploy.`);
12073
+ idpConfigNames.set(name, idpConfigName);
12074
+ }
12075
+ return idpConfigNames;
12076
+ }
12077
+ function collectDeployRunPlanInputs(targets) {
12078
+ return {
12079
+ idpUserTriggerTargets: collectDeployIdpUserTriggerTargets(targets),
12080
+ expectedLocalStaticWebsiteNames: collectExpectedLocalStaticWebsiteNames(targets),
12081
+ externalAuthIdpConfigNames: collectExternalAuthIdpConfigNames(targets)
12082
+ };
12083
+ }
12084
+ async function planDeploymentTarget(params) {
12085
+ const { target, targets, runInputs, client, workspaceId, noSchemaCheck } = params;
12086
+ const { config, application, workflowBuildResult, httpAdapterBuildResult, bundledScripts } = target;
12087
+ await withSpan("plan.validateTailorDBTypeNames", () => assertUniqueTailorDBTypeNamesWithExternal({
12088
+ client,
12089
+ workspaceId,
12090
+ tailorDBServices: application.tailorDBServices,
12091
+ externalTailorDBNamespaces: application.externalTailorDBNamespaces,
12092
+ plannedExternalTailorDBServices: collectPlannedExternalTailorDBServices(target, targets)
12093
+ }));
12094
+ const workflowService = application.workflowService;
12095
+ const functionEntries = collectFunctionEntries(application, filterBundledWorkflowJobs(workflowService?.jobs ?? [], workflowBuildResult?.usedJobNames ?? []), bundledScripts);
12096
+ const forceApplyAll = await withSpan("plan.detectSdkVersionChange", () => shouldForceApplyAll(client, workspaceId, application, functionEntries));
12097
+ return withSpan("plan", async () => {
12098
+ const applications = targets.map((target) => target.application);
12099
+ const tailorDBTypeNamespaces = collectVisibleTailorDBTypeNamespaces(application, applications);
12100
+ const resolverNamespaces = collectVisibleResolverNamespaces(application, applications);
12101
+ const idpNames = collectVisibleIdpNames(application, applications);
12102
+ const ctx = {
12103
+ client,
12104
+ workspaceId,
12105
+ application,
12106
+ forRemoval: false,
12107
+ config,
12108
+ noSchemaCheck,
12109
+ forceApplyAll,
12110
+ ...runInputs,
12111
+ executorUsedTailorDBTypes: collectExecutorUsedTailorDBTypes(target, targets),
12112
+ executorUsedResolvers: collectExecutorUsedResolvers(target, targets),
12113
+ tailorDBTypeNamespaces,
12114
+ resolverNamespaces,
12115
+ idpNames
12116
+ };
12117
+ const functionRegistry = await withSpan("plan.functionRegistry", () => planFunctionRegistry(client, workspaceId, application.name, application.id, functionEntries));
12118
+ const unchangedWorkflowJobs = new Set(functionRegistry.changeSet.unchanged.filter((entry) => entry.name.startsWith(WORKFLOW_PREFIX)).map((entry) => entry.name.slice(WORKFLOW_PREFIX.length)));
12119
+ const [tailorDB, staticWebsite, aiGateway, idp, auth, pipeline, app, executor, workflow, secretManager] = await Promise.all([
12120
+ withSpan("plan.tailorDB", () => planTailorDB(ctx)),
12121
+ withSpan("plan.staticWebsite", () => planStaticWebsite(ctx)),
12122
+ withSpan("plan.aiGateway", () => planAIGateway(ctx)),
12123
+ withSpan("plan.idp", () => planIdP(ctx)),
12124
+ withSpan("plan.auth", () => planAuth(ctx)),
12125
+ withSpan("plan.pipeline", () => planPipeline(ctx)),
12126
+ withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
12127
+ withSpan("plan.executor", () => planExecutor(ctx)),
12128
+ withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
12129
+ withSpan("plan.secretManager", () => planSecretManager(ctx))
12130
+ ]);
12131
+ return {
12132
+ application,
12133
+ functionRegistry,
12134
+ tailorDB,
12135
+ staticWebsite,
12136
+ aiGateway,
12137
+ idp,
12138
+ auth,
12139
+ pipeline,
12140
+ app,
12141
+ executor,
12142
+ workflow,
12143
+ secretManager
12144
+ };
12145
+ });
12146
+ }
12147
+ async function planDeploymentTargets(params) {
12148
+ const { targets, planTarget = planDeploymentTarget, ...planParams } = params;
12149
+ return Promise.all(targets.map((target) => planTarget({
12150
+ ...planParams,
12151
+ target,
12152
+ targets
12153
+ })));
12154
+ }
12155
+ function deploymentPlanResults(deployment) {
12156
+ const { application: _application, ...results } = deployment;
12157
+ return results;
12158
+ }
12159
+ function collectOwnerConflicts(results) {
12160
+ return [
12161
+ ...results.functionRegistry.conflicts,
12162
+ ...results.tailorDB.conflicts,
12163
+ ...results.staticWebsite.conflicts,
12164
+ ...results.aiGateway.conflicts,
12165
+ ...results.idp.conflicts,
12166
+ ...results.auth.conflicts,
12167
+ ...results.pipeline.conflicts,
12168
+ ...results.executor.conflicts,
12169
+ ...results.workflow.conflicts,
12170
+ ...results.secretManager.conflicts
12171
+ ];
12172
+ }
12173
+ function collectUnmanagedResources(results) {
12174
+ return [
12175
+ ...results.functionRegistry.unmanaged,
12176
+ ...results.tailorDB.unmanaged,
12177
+ ...results.staticWebsite.unmanaged,
12178
+ ...results.aiGateway.unmanaged,
12179
+ ...results.idp.unmanaged,
12180
+ ...results.auth.unmanaged,
12181
+ ...results.pipeline.unmanaged,
12182
+ ...results.executor.unmanaged,
12183
+ ...results.workflow.unmanaged,
12184
+ ...results.secretManager.unmanaged
11561
12185
  ];
11562
- if (allUnmanaged.length > 0) {
11563
- allLines.push(styles.bold("Unmanaged resources (not in config):"));
11564
- for (const { resourceType, resourceName } of allUnmanaged) allLines.push(` ${styles.warning("⚠")} ${styles.bold(resourceType)} "${resourceName}"`);
12186
+ }
12187
+ function collectResourceOwners(results) {
12188
+ return /* @__PURE__ */ new Set([
12189
+ ...results.functionRegistry.resourceOwners,
12190
+ ...results.tailorDB.resourceOwners,
12191
+ ...results.staticWebsite.resourceOwners,
12192
+ ...results.aiGateway.resourceOwners,
12193
+ ...results.idp.resourceOwners,
12194
+ ...results.auth.resourceOwners,
12195
+ ...results.pipeline.resourceOwners,
12196
+ ...results.executor.resourceOwners,
12197
+ ...results.workflow.resourceOwners,
12198
+ ...results.secretManager.resourceOwners
12199
+ ]);
12200
+ }
12201
+ function collectImportantResourceDeletions(results) {
12202
+ const importantDeletions = [];
12203
+ for (const del of results.tailorDB.changeSet.type.deletes) importantDeletions.push({
12204
+ resourceType: "TailorDB type",
12205
+ resourceName: del.name
12206
+ });
12207
+ for (const del of results.staticWebsite.changeSet.deletes) importantDeletions.push({
12208
+ resourceType: "StaticWebsite",
12209
+ resourceName: del.name
12210
+ });
12211
+ for (const del of results.aiGateway.changeSet.deletes) importantDeletions.push({
12212
+ resourceType: "AIGateway",
12213
+ resourceName: del.name
12214
+ });
12215
+ for (const del of results.auth.changeSet.oauth2Client.deletes) importantDeletions.push({
12216
+ resourceType: "OAuth2 client",
12217
+ resourceName: del.name
12218
+ });
12219
+ for (const replace of results.auth.changeSet.oauth2Client.replaces) importantDeletions.push({
12220
+ resourceType: "OAuth2 client (client type change)",
12221
+ resourceName: replace.name
12222
+ });
12223
+ for (const del of results.auth.changeSet.connection.deletes) importantDeletions.push({
12224
+ resourceType: "Auth connection",
12225
+ resourceName: del.name
12226
+ });
12227
+ for (const del of results.secretManager.vaultChangeSet.deletes) importantDeletions.push({
12228
+ resourceType: "Secret Manager vault",
12229
+ resourceName: del.name
12230
+ });
12231
+ for (const del of results.secretManager.secretChangeSet.deletes) importantDeletions.push({
12232
+ resourceType: "Secret Manager secret",
12233
+ resourceName: del.name
12234
+ });
12235
+ return importantDeletions;
12236
+ }
12237
+ function readResourceField(item, field) {
12238
+ const itemRecord = item;
12239
+ for (const requestField of [
12240
+ "request",
12241
+ "deleteRequest",
12242
+ "createRequest"
12243
+ ]) {
12244
+ const request = itemRecord[requestField];
12245
+ if (request && typeof request === "object" && field in request) {
12246
+ const value = request[field];
12247
+ return value == null ? void 0 : String(value);
12248
+ }
11565
12249
  }
11566
- if (results.secretManager.skippedSecrets.length > 0) {
11567
- allLines.push(styles.bold("Secret Manager secrets (skipped - no value provided):"));
11568
- for (const name of results.secretManager.skippedSecrets) allLines.push(` ${styles.dim("○")} ${name}`);
12250
+ const value = itemRecord[field];
12251
+ return value == null ? void 0 : String(value);
12252
+ }
12253
+ function managedResourceName(group, item) {
12254
+ if (group.getName) return group.getName(item);
12255
+ return item.name;
12256
+ }
12257
+ function managedResourceKey(group, item) {
12258
+ const namespace = group.namespaceFields?.map((field) => readResourceField(item, field)).find((value) => value !== void 0);
12259
+ const name = managedResourceName(group, item);
12260
+ return namespace !== void 0 ? `${group.resourceType}:${namespace}:${name}` : `${group.resourceType}:${name}`;
12261
+ }
12262
+ function managedNamespaceOwnerKey(group, item) {
12263
+ if (!group.namespaceOwnerResourceType) return;
12264
+ const namespace = group.namespaceFields?.map((field) => readResourceField(item, field)).find((value) => value !== void 0);
12265
+ return namespace !== void 0 ? `${group.namespaceOwnerResourceType}:${namespace}` : void 0;
12266
+ }
12267
+ function addManagedResourceClaims(claims, group, item) {
12268
+ claims.add(managedResourceKey(group, item));
12269
+ const namespaceOwnerKey = managedNamespaceOwnerKey(group, item);
12270
+ if (namespaceOwnerKey) claims.add(namespaceOwnerKey);
12271
+ }
12272
+ function isManagedResourceClaimed(claims, group, item) {
12273
+ if (claims.has(managedResourceKey(group, item))) return true;
12274
+ const namespaceOwnerKey = managedNamespaceOwnerKey(group, item);
12275
+ return namespaceOwnerKey ? claims.has(namespaceOwnerKey) : false;
12276
+ }
12277
+ function retainDeletesNotClaimed(group, otherClaims) {
12278
+ let writeIndex = 0;
12279
+ for (const item of group.changeSet.deletes) {
12280
+ if (isManagedResourceClaimed(otherClaims, group, item)) {
12281
+ logger.debug(`Skipping delete of ${managedResourceKey(group, item)}: still managed by another config in this deploy.`);
12282
+ continue;
12283
+ }
12284
+ group.changeSet.deletes[writeIndex] = item;
12285
+ writeIndex += 1;
11569
12286
  }
11570
- if (allConflicts.length > 0) {
11571
- allLines.push(styles.bold("Owner conflicts (will require confirmation on apply):"));
11572
- for (const { resourceType, resourceName, currentOwner } of allConflicts) allLines.push(` ${styles.warning("!")} ${styles.bold(resourceType)} "${resourceName}" — owned by "${currentOwner}"`);
12287
+ group.changeSet.deletes.length = writeIndex;
12288
+ }
12289
+ function workflowJobFunctionItems(items) {
12290
+ const jobNames = /* @__PURE__ */ new Set();
12291
+ for (const item of items) {
12292
+ const usedJobNames = item.usedJobNames;
12293
+ if (!Array.isArray(usedJobNames)) continue;
12294
+ for (const jobName of usedJobNames) if (typeof jobName === "string") jobNames.add(jobName);
11573
12295
  }
11574
- allLines.push(formatPlanSummary(summary));
11575
- const output = allLines.join("\n");
11576
- if (opts?.dryRun) logger.out(output);
11577
- else logger.log(output);
11578
- return summary;
12296
+ return [...jobNames].map((name) => ({ name }));
11579
12297
  }
11580
- /**
11581
- * Summarize plan counts from display entries, service actions, and non-grouped changesets.
11582
- * @param results - Planned apply results
11583
- * @param displayEntries - All grouped display entries across sections
11584
- * @param serviceActions - All service-level namespace actions
11585
- * @returns Aggregated plan summary
11586
- */
11587
- function summarizePlanResults(results, displayEntries, serviceActions) {
11588
- const summary = {
12298
+ function workflowJobFunctionResourceGroup(results) {
12299
+ return {
12300
+ changeSet: {
12301
+ creates: workflowJobFunctionItems(results.workflow.changeSet.creates),
12302
+ updates: workflowJobFunctionItems(results.workflow.changeSet.updates),
12303
+ replaces: workflowJobFunctionItems(results.workflow.changeSet.replaces),
12304
+ unchanged: [...results.workflow.unchangedWorkflowJobNames].map((name) => ({ name })),
12305
+ deletes: results.workflow.jobFunctionDeletes
12306
+ },
12307
+ resourceType: "workflow_job_function",
12308
+ getName: (item) => "jobFunctionName" in item ? item.jobFunctionName : item.name
12309
+ };
12310
+ }
12311
+ const MANAGED_RESOURCE_NAMESPACE_FIELDS = [
12312
+ "namespaceName",
12313
+ "authNamespace",
12314
+ "vaultName",
12315
+ "staticWebsiteName"
12316
+ ];
12317
+ const DEPLOY_MANAGED_RESOURCE_DEFINITIONS = [
12318
+ {
12319
+ resourceLabel: "Application",
12320
+ resourceType: "application",
12321
+ namesOf: (target) => [target.application.name],
12322
+ changeSetOf: (results) => results.app
12323
+ },
12324
+ {
12325
+ resourceLabel: "Executor",
12326
+ resourceType: "executor",
12327
+ namesOf: (target) => Object.values(target.application.executorService?.executors ?? {}).map((executor) => executor.name),
12328
+ changeSetOf: (results) => results.executor.changeSet
12329
+ },
12330
+ {
12331
+ resourceLabel: "Workflow job",
12332
+ resourceType: "workflow_job_function",
12333
+ namesOf: (target) => target.bundledScripts.workflowJobs.keys(),
12334
+ changeSetOf: (results) => workflowJobFunctionResourceGroup(results).changeSet,
12335
+ getName: (item) => "jobFunctionName" in item ? item.jobFunctionName : item.name
12336
+ },
12337
+ {
12338
+ resourceLabel: "Workflow",
12339
+ resourceType: "workflow",
12340
+ namesOf: (target) => Object.values(target.application.workflowService?.workflows ?? {}).map((workflow) => workflow.name),
12341
+ changeSetOf: (results) => results.workflow.changeSet
12342
+ },
12343
+ {
12344
+ resourceLabel: "Auth connection",
12345
+ resourceType: "auth.connection",
12346
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12347
+ namespaceOwnerResourceType: "auth.service",
12348
+ namesOf: (target) => Object.keys(target.application.authService?.connections ?? {}),
12349
+ changeSetOf: (results) => results.auth.changeSet.connection
12350
+ },
12351
+ {
12352
+ resourceLabel: "StaticWebsite",
12353
+ resourceType: "staticwebsite",
12354
+ namesOf: (target) => target.application.staticWebsiteServices.map((service) => service.name),
12355
+ changeSetOf: (results) => results.staticWebsite.changeSet
12356
+ },
12357
+ {
12358
+ resourceLabel: "TailorDB namespace",
12359
+ resourceType: "tailordb.service",
12360
+ namesOf: (target) => target.application.tailorDBServices.map((service) => service.namespace),
12361
+ changeSetOf: (results) => results.tailorDB.changeSet.service
12362
+ },
12363
+ {
12364
+ resourceLabel: "Auth namespace",
12365
+ resourceType: "auth.service",
12366
+ namesOf: (target) => {
12367
+ const name = target.application.authService?.config.name;
12368
+ return name === void 0 ? [] : [name];
12369
+ },
12370
+ changeSetOf: (results) => results.auth.changeSet.service
12371
+ },
12372
+ {
12373
+ resourceLabel: "IdP namespace",
12374
+ resourceType: "idp.service",
12375
+ namesOf: (target) => target.application.idpServices.map((idp) => idp.name),
12376
+ changeSetOf: (results) => results.idp.changeSet.service
12377
+ },
12378
+ {
12379
+ resourceLabel: "Resolver namespace",
12380
+ resourceType: "pipeline.service",
12381
+ namesOf: (target) => target.application.resolverServices.map((service) => service.namespace),
12382
+ changeSetOf: (results) => results.pipeline.changeSet.service
12383
+ },
12384
+ {
12385
+ resourceLabel: "AIGateway",
12386
+ resourceType: "aigateway",
12387
+ namesOf: (target) => target.application.aiGatewayServices.map((service) => service.name),
12388
+ changeSetOf: (results) => results.aiGateway.changeSet
12389
+ },
12390
+ {
12391
+ resourceLabel: "Secret Manager vault",
12392
+ resourceType: "secret.vault",
12393
+ namesOf: (target) => target.application.secrets.map((vault) => vault.vaultName),
12394
+ changeSetOf: (results) => results.secretManager.vaultChangeSet
12395
+ }
12396
+ ];
12397
+ function managedResourceGroupFromDefinition(definition, results) {
12398
+ const { changeSetOf, namesOf: _namesOf, resourceLabel: _resourceLabel, ...group } = definition;
12399
+ return {
12400
+ ...group,
12401
+ changeSet: changeSetOf(results)
12402
+ };
12403
+ }
12404
+ function managedResourceGroups(results) {
12405
+ return [
12406
+ {
12407
+ changeSet: results.functionRegistry.changeSet,
12408
+ resourceType: "function_registry"
12409
+ },
12410
+ ...DEPLOY_MANAGED_RESOURCE_DEFINITIONS.map((definition) => managedResourceGroupFromDefinition(definition, results)),
12411
+ {
12412
+ changeSet: results.tailorDB.changeSet.type,
12413
+ resourceType: "tailordb.type",
12414
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12415
+ namespaceOwnerResourceType: "tailordb.service"
12416
+ },
12417
+ {
12418
+ changeSet: results.tailorDB.changeSet.gqlPermission,
12419
+ resourceType: "tailordb.gql_permission",
12420
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12421
+ namespaceOwnerResourceType: "tailordb.service"
12422
+ },
12423
+ {
12424
+ changeSet: results.staticWebsite.customDomainChangeSet,
12425
+ resourceType: "staticwebsite.custom_domain",
12426
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12427
+ namespaceOwnerResourceType: "staticwebsite"
12428
+ },
12429
+ {
12430
+ changeSet: results.idp.changeSet.client,
12431
+ resourceType: "idp.client",
12432
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12433
+ namespaceOwnerResourceType: "idp.service"
12434
+ },
12435
+ {
12436
+ changeSet: results.auth.changeSet.idpConfig,
12437
+ resourceType: "auth.idp_config",
12438
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12439
+ namespaceOwnerResourceType: "auth.service"
12440
+ },
12441
+ {
12442
+ changeSet: results.auth.changeSet.userProfileConfig,
12443
+ resourceType: "auth.user_profile_config",
12444
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12445
+ namespaceOwnerResourceType: "auth.service"
12446
+ },
12447
+ {
12448
+ changeSet: results.auth.changeSet.tenantConfig,
12449
+ resourceType: "auth.tenant_config",
12450
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12451
+ namespaceOwnerResourceType: "auth.service"
12452
+ },
12453
+ {
12454
+ changeSet: results.auth.changeSet.machineUser,
12455
+ resourceType: "auth.machine_user",
12456
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12457
+ namespaceOwnerResourceType: "auth.service"
12458
+ },
12459
+ {
12460
+ changeSet: results.auth.changeSet.oauth2Client,
12461
+ resourceType: "auth.oauth2_client",
12462
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12463
+ namespaceOwnerResourceType: "auth.service"
12464
+ },
12465
+ {
12466
+ changeSet: results.auth.changeSet.authHook,
12467
+ resourceType: "auth.hook",
12468
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12469
+ namespaceOwnerResourceType: "auth.service"
12470
+ },
12471
+ {
12472
+ changeSet: results.auth.changeSet.scim,
12473
+ resourceType: "auth.scim",
12474
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12475
+ namespaceOwnerResourceType: "auth.service"
12476
+ },
12477
+ {
12478
+ changeSet: results.auth.changeSet.scimResource,
12479
+ resourceType: "auth.scim_resource",
12480
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12481
+ namespaceOwnerResourceType: "auth.service"
12482
+ },
12483
+ {
12484
+ changeSet: results.pipeline.changeSet.resolver,
12485
+ resourceType: "pipeline.resolver",
12486
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12487
+ namespaceOwnerResourceType: "pipeline.service"
12488
+ },
12489
+ {
12490
+ changeSet: results.secretManager.secretChangeSet,
12491
+ resourceType: "secret.secret",
12492
+ namespaceFields: MANAGED_RESOURCE_NAMESPACE_FIELDS,
12493
+ namespaceOwnerResourceType: "secret.vault"
12494
+ }
12495
+ ];
12496
+ }
12497
+ function dropCrossDeploymentManagedDeletes(deployments) {
12498
+ const groupsByDeployment = deployments.map((deployment) => managedResourceGroups(deploymentPlanResults(deployment)));
12499
+ const claimsByDeployment = groupsByDeployment.map((groups) => groups.reduce((claims, group) => {
12500
+ for (const item of [
12501
+ ...group.changeSet.creates,
12502
+ ...group.changeSet.updates,
12503
+ ...group.changeSet.replaces,
12504
+ ...group.changeSet.unchanged
12505
+ ]) addManagedResourceClaims(claims, group, item);
12506
+ return claims;
12507
+ }, /* @__PURE__ */ new Set()));
12508
+ groupsByDeployment.forEach((groups, deploymentIndex) => {
12509
+ const otherClaims = /* @__PURE__ */ new Set();
12510
+ claimsByDeployment.forEach((claims, claimIndex) => {
12511
+ if (claimIndex === deploymentIndex) return;
12512
+ for (const claim of claims) otherClaims.add(claim);
12513
+ });
12514
+ for (const group of groups) retainDeletesNotClaimed(group, otherClaims);
12515
+ });
12516
+ }
12517
+ function collectDeploymentResourceOwners(deployments) {
12518
+ const owners = /* @__PURE__ */ new Set();
12519
+ for (const deployment of deployments) for (const owner of collectResourceOwners(deploymentPlanResults(deployment))) owners.add(owner);
12520
+ return owners;
12521
+ }
12522
+ async function confirmDeploymentPlans(params) {
12523
+ const { deployments, yes } = params;
12524
+ const targetAppNames = new Set(deployments.map((deployment) => deployment.application.name));
12525
+ const resourceOwners = collectDeploymentResourceOwners(deployments);
12526
+ const scheduledRenamedAppDeletes = /* @__PURE__ */ new Set();
12527
+ const importantDeletions = [];
12528
+ for (const deployment of deployments) {
12529
+ const results = deploymentPlanResults(deployment);
12530
+ const conflicts = collectOwnerConflicts(results);
12531
+ await confirmOwnerConflict(conflicts, deployment.application.name, yes);
12532
+ await confirmUnmanagedResources(collectUnmanagedResources(results), deployment.application.name, yes);
12533
+ importantDeletions.push(...collectImportantResourceDeletions(results));
12534
+ const emptyApps = computeRenamedAppDeletions({
12535
+ conflicts,
12536
+ resourceOwners,
12537
+ protectedAppNames: targetAppNames
12538
+ });
12539
+ for (const emptyApp of emptyApps) {
12540
+ if (scheduledRenamedAppDeletes.has(emptyApp)) continue;
12541
+ scheduledRenamedAppDeletes.add(emptyApp);
12542
+ deployment.app.deletes.push({
12543
+ name: emptyApp,
12544
+ request: {
12545
+ workspaceId: deployment.tailorDB.context.workspaceId,
12546
+ applicationName: emptyApp
12547
+ }
12548
+ });
12549
+ }
12550
+ }
12551
+ await confirmImportantResourceDeletion(importantDeletions, yes);
12552
+ }
12553
+ function sumPlanSummaries(summaries) {
12554
+ return summaries.reduce((acc, summary) => ({
12555
+ create: acc.create + summary.create,
12556
+ update: acc.update + summary.update,
12557
+ delete: acc.delete + summary.delete,
12558
+ replace: acc.replace + summary.replace
12559
+ }), {
11589
12560
  create: 0,
11590
12561
  update: 0,
11591
12562
  delete: 0,
11592
12563
  replace: 0
11593
- };
11594
- for (const entry of displayEntries) summary[entry.action] += 1;
11595
- for (const sa of serviceActions) summary[sa.action] += 1;
11596
- const { otherChanges } = splitFunctionRegistryChanges(results.functionRegistry.changeSet);
11597
- const nonGrouped = summarizeChangeSets([
11598
- otherChanges,
11599
- results.staticWebsite.changeSet,
11600
- results.staticWebsite.customDomainChangeSet,
11601
- results.aiGateway.changeSet,
11602
- results.app,
11603
- results.secretManager.vaultChangeSet,
11604
- results.secretManager.secretChangeSet
11605
- ]);
11606
- summary.create += nonGrouped.create;
11607
- summary.update += nonGrouped.update;
11608
- summary.delete += nonGrouped.delete;
11609
- summary.replace += nonGrouped.replace;
11610
- return summary;
12564
+ });
12565
+ }
12566
+ function printDeploymentPlans(deployments, opts) {
12567
+ if (logger.jsonMode && opts?.dryRun) {
12568
+ const reports = deployments.map((deployment) => buildPlanReport(deploymentPlanResults(deployment)));
12569
+ const summary = sumPlanSummaries(reports.map((report) => report.summary));
12570
+ logger.out({
12571
+ summary,
12572
+ changes: reports.flatMap((report) => report.json.changes),
12573
+ warnings: reports.flatMap((report) => report.json.warnings),
12574
+ conflicts: reports.flatMap((report) => report.json.conflicts)
12575
+ });
12576
+ return summary;
12577
+ }
12578
+ return sumPlanSummaries(deployments.map((deployment) => printPlanResults(deploymentPlanResults(deployment), opts)));
12579
+ }
12580
+ async function validateDeploymentPlans(deployments) {
12581
+ for (const deployment of deployments) await validatePlan(deploymentPlanResults(deployment));
11611
12582
  }
11612
12583
  /**
11613
12584
  * Deploy the configured application to the Tailor platform.
@@ -11617,21 +12588,10 @@ function summarizePlanResults(results, displayEntries, serviceActions) {
11617
12588
  async function deploy(options) {
11618
12589
  return withSpan("deploy", async (rootSpan) => {
11619
12590
  rootSpan.setAttribute("deploy.dry_run", options?.dryRun ?? false);
11620
- const { config, application, workflowBuildResult, httpAdapterBuildResult, bundledScripts, buildOnly } = await withSpan("build", async () => {
12591
+ const configPaths = parseDeployConfigPaths(options?.configPath);
12592
+ const { targets, buildOnly } = await withSpan("build", async () => {
11621
12593
  const dryRun = options?.dryRun ?? false;
11622
12594
  const buildOnly = options?.buildOnly ?? parseBoolean(process.env.TAILOR_PLATFORM_SDK_BUILD_ONLY) === true;
11623
- const { config, plugins } = await withSpan("build.loadConfig", async () => {
11624
- const foundPath = loadConfigPath(options?.configPath);
11625
- if (foundPath) {
11626
- const resolvedPath = path.resolve(process.cwd(), foundPath);
11627
- if (fs$1.existsSync(resolvedPath)) await ensureConfigIdForDeploy({
11628
- configPath: resolvedPath,
11629
- dryRun,
11630
- buildOnly
11631
- });
11632
- }
11633
- return loadConfig(options?.configPath);
11634
- });
11635
12595
  const noCache = options?.noCache ?? false;
11636
12596
  const packageJson = await readPackageJson();
11637
12597
  const cacheDir = path.resolve(getDistDir(), "cache");
@@ -11642,256 +12602,51 @@ async function deploy(options) {
11642
12602
  });
11643
12603
  logger.info("Bundle cache cleaned");
11644
12604
  }
11645
- const configDir = path.dirname(config.path);
11646
- const lockfilePath = findUpSync("pnpm-lock.yaml", { cwd: configDir }) ?? findUpSync("package-lock.json", { cwd: configDir }) ?? findUpSync("yarn.lock", { cwd: configDir }) ?? findUpSync("bun.lock", { cwd: configDir });
11647
- const cacheManager = createCacheManager({
11648
- enabled: !noCache,
11649
- cacheDir,
11650
- sdkVersion: packageJson.version ?? "unknown",
11651
- lockfileHash: lockfilePath ? hashFile(lockfilePath) : void 0
11652
- });
11653
- let pluginManager;
11654
- if (plugins.length > 0) pluginManager = new PluginManager(plugins);
11655
- await withSpan("build.generateUserTypes", () => generateUserTypes({
11656
- config,
11657
- configPath: config.path
11658
- }));
11659
- let application;
11660
- let workflowBuildResult;
11661
- let httpAdapterBuildResult;
11662
- let bundledScripts;
11663
- try {
11664
- const result = await withSpan("build.loadApplication", () => loadApplication({
11665
- config,
11666
- pluginManager,
11667
- bundleCache: cacheManager.bundleCache
11668
- }));
11669
- application = result.application;
11670
- workflowBuildResult = result.workflowBuildResult;
11671
- httpAdapterBuildResult = result.httpAdapterBuildResult;
11672
- bundledScripts = result.bundledScripts;
11673
- } finally {
11674
- cacheManager.finalize();
11675
- }
11676
12605
  return {
11677
- config,
11678
- plugins,
11679
- application,
11680
- workflowBuildResult,
11681
- httpAdapterBuildResult,
11682
- bundledScripts,
11683
- dryRun,
12606
+ targets: await buildDeploymentTargets({
12607
+ configPaths,
12608
+ dryRun,
12609
+ buildOnly,
12610
+ noCache,
12611
+ packageVersion: packageJson.version ?? "unknown",
12612
+ cacheDir
12613
+ }),
11684
12614
  buildOnly
11685
12615
  };
11686
12616
  });
11687
- if (buildOnly) return { bundledScripts };
12617
+ if (buildOnly) return { bundledScripts: mergeBundledScripts(targets) };
12618
+ assertUniqueGlobalResourceNames(targets);
11688
12619
  const client = await initOperatorClient(await loadAccessToken({ profile: options?.profile }));
11689
12620
  const workspaceId = await loadWorkspaceId({
11690
12621
  workspaceId: options?.workspaceId,
11691
12622
  profile: options?.profile
11692
12623
  });
11693
- rootSpan.setAttribute("app.name", application.name);
12624
+ rootSpan.setAttribute("app.name", targets.map((target) => target.application.name).join(","));
11694
12625
  rootSpan.setAttribute("workspace.id", workspaceId);
11695
- await withSpan("plan.validateTailorDBTypeNames", () => assertUniqueTailorDBTypeNamesWithExternal({
12626
+ const deployments = await planDeploymentTargets({
12627
+ targets,
12628
+ runInputs: collectDeployRunPlanInputs(targets),
11696
12629
  client,
11697
12630
  workspaceId,
11698
- tailorDBServices: application.tailorDBServices,
11699
- externalTailorDBNamespaces: application.externalTailorDBNamespaces
11700
- }));
11701
- const workflowService = application.workflowService;
11702
- const functionEntries = collectFunctionEntries(application, filterBundledWorkflowJobs(workflowService?.jobs ?? [], workflowBuildResult?.usedJobNames ?? []), bundledScripts);
12631
+ noSchemaCheck: options?.noSchemaCheck
12632
+ });
11703
12633
  const dryRun = options?.dryRun ?? false;
11704
12634
  const yes = options?.yes ?? false;
11705
- const forceApplyAll = await withSpan("plan.detectSdkVersionChange", () => shouldForceApplyAll(client, workspaceId, application, functionEntries));
11706
- const { functionRegistry, tailorDB, staticWebsite, aiGateway, idp, auth, pipeline, app, executor, workflow, secretManager } = await withSpan("plan", async () => {
11707
- const idpUserTriggerTargets = collectIdpUserTriggerTargets(application);
11708
- const ctx = {
11709
- client,
11710
- workspaceId,
11711
- application,
11712
- forRemoval: false,
11713
- config,
11714
- noSchemaCheck: options?.noSchemaCheck,
11715
- forceApplyAll,
11716
- idpUserTriggerTargets
11717
- };
11718
- const functionRegistry = await withSpan("plan.functionRegistry", () => planFunctionRegistry(client, workspaceId, application.name, application.id, functionEntries));
11719
- const unchangedWorkflowJobs = new Set(functionRegistry.changeSet.unchanged.filter((entry) => entry.name.startsWith(WORKFLOW_PREFIX)).map((entry) => entry.name.slice(WORKFLOW_PREFIX.length)));
11720
- const [tailorDB, staticWebsite, aiGateway, idp, auth, pipeline, app, executor, workflow, secretManager] = await Promise.all([
11721
- withSpan("plan.tailorDB", () => planTailorDB(ctx)),
11722
- withSpan("plan.staticWebsite", () => planStaticWebsite(ctx)),
11723
- withSpan("plan.aiGateway", () => planAIGateway(ctx)),
11724
- withSpan("plan.idp", () => planIdP(ctx)),
11725
- withSpan("plan.auth", () => planAuth(ctx)),
11726
- withSpan("plan.pipeline", () => planPipeline(ctx)),
11727
- withSpan("plan.application", () => planApplication(ctx, httpAdapterBuildResult)),
11728
- withSpan("plan.executor", () => planExecutor(ctx)),
11729
- withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
11730
- withSpan("plan.secretManager", () => planSecretManager(ctx))
11731
- ]);
11732
- return {
11733
- functionRegistry,
11734
- tailorDB,
11735
- staticWebsite,
11736
- aiGateway,
11737
- idp,
11738
- auth,
11739
- pipeline,
11740
- app,
11741
- executor,
11742
- workflow,
11743
- secretManager
11744
- };
11745
- });
12635
+ dropCrossDeploymentManagedDeletes(deployments);
11746
12636
  await withSpan("confirm", async () => {
11747
- const allConflicts = [
11748
- ...functionRegistry.conflicts,
11749
- ...tailorDB.conflicts,
11750
- ...staticWebsite.conflicts,
11751
- ...aiGateway.conflicts,
11752
- ...idp.conflicts,
11753
- ...auth.conflicts,
11754
- ...pipeline.conflicts,
11755
- ...executor.conflicts,
11756
- ...workflow.conflicts,
11757
- ...secretManager.conflicts
11758
- ];
11759
- await confirmOwnerConflict(allConflicts, application.name, yes);
11760
- await confirmUnmanagedResources([
11761
- ...functionRegistry.unmanaged,
11762
- ...tailorDB.unmanaged,
11763
- ...staticWebsite.unmanaged,
11764
- ...aiGateway.unmanaged,
11765
- ...idp.unmanaged,
11766
- ...auth.unmanaged,
11767
- ...pipeline.unmanaged,
11768
- ...executor.unmanaged,
11769
- ...workflow.unmanaged,
11770
- ...secretManager.unmanaged
11771
- ], application.name, yes);
11772
- const importantDeletions = [];
11773
- for (const del of tailorDB.changeSet.type.deletes) importantDeletions.push({
11774
- resourceType: "TailorDB type",
11775
- resourceName: del.name
11776
- });
11777
- for (const del of staticWebsite.changeSet.deletes) importantDeletions.push({
11778
- resourceType: "StaticWebsite",
11779
- resourceName: del.name
11780
- });
11781
- for (const del of aiGateway.changeSet.deletes) importantDeletions.push({
11782
- resourceType: "AIGateway",
11783
- resourceName: del.name
11784
- });
11785
- for (const del of auth.changeSet.oauth2Client.deletes) importantDeletions.push({
11786
- resourceType: "OAuth2 client",
11787
- resourceName: del.name
11788
- });
11789
- for (const replace of auth.changeSet.oauth2Client.replaces) importantDeletions.push({
11790
- resourceType: "OAuth2 client (client type change)",
11791
- resourceName: replace.name
11792
- });
11793
- for (const del of auth.changeSet.connection.deletes) importantDeletions.push({
11794
- resourceType: "Auth connection",
11795
- resourceName: del.name
11796
- });
11797
- for (const del of secretManager.vaultChangeSet.deletes) importantDeletions.push({
11798
- resourceType: "Secret Manager vault",
11799
- resourceName: del.name
11800
- });
11801
- for (const del of secretManager.secretChangeSet.deletes) importantDeletions.push({
11802
- resourceType: "Secret Manager secret",
11803
- resourceName: del.name
11804
- });
11805
- await confirmImportantResourceDeletion(importantDeletions, yes);
11806
- const emptyApps = computeRenamedAppDeletions({
11807
- conflicts: allConflicts,
11808
- resourceOwners: /* @__PURE__ */ new Set([
11809
- ...functionRegistry.resourceOwners,
11810
- ...tailorDB.resourceOwners,
11811
- ...staticWebsite.resourceOwners,
11812
- ...aiGateway.resourceOwners,
11813
- ...idp.resourceOwners,
11814
- ...auth.resourceOwners,
11815
- ...pipeline.resourceOwners,
11816
- ...executor.resourceOwners,
11817
- ...workflow.resourceOwners,
11818
- ...secretManager.resourceOwners
11819
- ]),
11820
- targetAppName: application.name
11821
- });
11822
- for (const emptyApp of emptyApps) app.deletes.push({
11823
- name: emptyApp,
11824
- request: {
11825
- workspaceId,
11826
- applicationName: emptyApp
11827
- }
12637
+ await confirmDeploymentPlans({
12638
+ deployments,
12639
+ yes
11828
12640
  });
11829
12641
  });
11830
- const planSummary = printPlanResults({
11831
- functionRegistry,
11832
- tailorDB,
11833
- staticWebsite,
11834
- aiGateway,
11835
- idp,
11836
- auth,
11837
- pipeline,
11838
- app,
11839
- executor,
11840
- workflow,
11841
- secretManager
11842
- }, { dryRun: options?.dryRun });
12642
+ const planSummary = printDeploymentPlans(deployments, { dryRun: options?.dryRun });
11843
12643
  if (options?.noValidate) logger.warn("Client-side validation skipped (--no-validate).");
11844
- else await validatePlan({
11845
- functionRegistry,
11846
- tailorDB,
11847
- staticWebsite,
11848
- idp,
11849
- auth,
11850
- pipeline,
11851
- app,
11852
- executor,
11853
- workflow,
11854
- secretManager
11855
- });
12644
+ else await validateDeploymentPlans(deployments);
11856
12645
  if (dryRun) {
11857
12646
  logger.info("Dry run enabled. No changes applied.");
11858
12647
  return;
11859
12648
  }
11860
- await withSpan("apply.createUpdateServices", async () => {
11861
- await applySecretManager(client, secretManager, "create-update", application);
11862
- await applyFunctionRegistry(client, workspaceId, functionRegistry, "create-update");
11863
- await applyStaticWebsite(client, staticWebsite, "create-update");
11864
- await applyAIGateway(client, aiGateway, "create-update");
11865
- await applyIdP(client, idp, "create-update");
11866
- await applyAuth(client, auth, "create-update");
11867
- await applyTailorDB(client, tailorDB, "create-update");
11868
- await applyPipeline(client, pipeline, "create-update");
11869
- });
11870
- await withSpan("apply.deleteSubgraphResources", async () => {
11871
- await applyPipeline(client, pipeline, "delete-resources");
11872
- await applyAuth(client, auth, "delete-resources");
11873
- await applyIdP(client, idp, "delete-resources");
11874
- });
11875
- await withSpan("apply.createUpdateApplication", () => applyApplication(client, app, "create-update"));
11876
- await withSpan("apply.createUpdateDependentServices", async () => {
11877
- await applyExecutor(client, executor, "create-update");
11878
- await applyWorkflow(client, workflow, "create-update");
11879
- });
11880
- await withSpan("apply.deleteDependentServices", async () => {
11881
- await applyWorkflow(client, workflow, "delete");
11882
- await applyExecutor(client, executor, "delete");
11883
- await applyStaticWebsite(client, staticWebsite, "delete");
11884
- await applyAIGateway(client, aiGateway, "delete");
11885
- await applySecretManager(client, secretManager, "delete");
11886
- });
11887
- await withSpan("apply.deleteApplication", () => applyApplication(client, app, "delete"));
11888
- await withSpan("apply.deleteSubgraphServices", async () => {
11889
- await applyPipeline(client, pipeline, "delete-services");
11890
- await applyAuth(client, auth, "delete-services");
11891
- await applyIdP(client, idp, "delete-services");
11892
- await applyTailorDB(client, tailorDB, "delete-services");
11893
- });
11894
- await withSpan("apply.cleanup", () => applyFunctionRegistry(client, workspaceId, functionRegistry, "delete"));
12649
+ await applyDeploymentPlans(client, workspaceId, deployments);
11895
12650
  if (logger.jsonMode) logger.out({
11896
12651
  summary: planSummary,
11897
12652
  status: "applied"
@@ -16031,10 +16786,12 @@ function createWorkspaceFolderNameResolver(client) {
16031
16786
  };
16032
16787
  }
16033
16788
  async function workspaceInfoWithFolderName(client, workspace) {
16034
- return workspaceInfo(workspace, await resolveWorkspaceFolderName(client, workspace));
16789
+ const folderName = await resolveWorkspaceFolderName(client, workspace);
16790
+ return workspaceInfo(workspace, folderName);
16035
16791
  }
16036
16792
  async function workspaceDetailsWithFolderName(client, workspace) {
16037
- return workspaceDetails(workspace, await resolveWorkspaceFolderName(client, workspace));
16793
+ const folderName = await resolveWorkspaceFolderName(client, workspace);
16794
+ return workspaceDetails(workspace, folderName);
16038
16795
  }
16039
16796
  async function workspaceInfosWithFolderNames(client, workspaces) {
16040
16797
  const resolveFolderName = createWorkspaceFolderNameResolver(client);
@@ -16705,7 +17462,7 @@ async function generate(options) {
16705
17462
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
16706
17463
  let pluginManager;
16707
17464
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
16708
- const { defineApplication } = await import("./application-BXdCl6bp.mjs");
17465
+ const { defineApplication } = await import("./application-BndtMTFY.mjs");
16709
17466
  const application = defineApplication({
16710
17467
  config,
16711
17468
  pluginManager
@@ -18635,7 +19392,7 @@ async function runRepl(options) {
18635
19392
  const execute = await prepareQueryExecutor(options);
18636
19393
  const historyPath = getReplHistoryPath(options.engine, options.profile, options.workspaceId);
18637
19394
  const validate = createReplValidator(options.engine);
18638
- const { highlightSqlLine, highlightGraphqlLine, replTransform } = await import("./repl-editor-BmQOyZJd.mjs");
19395
+ const { highlightSqlLine, highlightGraphqlLine, replTransform } = await import("./repl-editor-BCozyiNq.mjs");
18639
19396
  const highlight = options.engine === "sql" ? highlightSqlLine : highlightGraphqlLine;
18640
19397
  const prompt = createPrompt({
18641
19398
  prefix: "",
@@ -18969,5 +19726,5 @@ function isDeno() {
18969
19726
  }
18970
19727
 
18971
19728
  //#endregion
18972
- export { getOrganization as $, generateAllTypeManifestsFromSnapshot as $t, listCommand$3 as A, apiCommand as An, getExecutorWaitFailureMessage as At, show as B, paginationArgs as Bn, listWorkflowExecutions as Bt, listCommand$2 as C, getNamespacesWithMigrations as Cn, listWebhookExecutors as Ct, waitWorkflowExecution as D, PluginManager as Dn, listCommand$9 as Dt, waitCommand as E, sdkNameLabelKey as En, triggerExecutor as Et, generateCommand as F, configArg as Fn, startWorkflow as Ft, removeCommand$1 as G, deploy as Gt, extractOwnedNamespaces as H, workspaceArgs as Hn, formatKeyValueTable as Ht, generateMigrationScript as I, confirmationArgs as In, getCommand$5 as It, organizationTree as J, bundleMigrationScript as Jt, updateCommand$1 as K, executeScript as Kt, writeDbTypesFile as L, deploymentArgs as Ln, getWorkflow as Lt, truncate as M, assertWritable as Mn, listExecutorJobs as Mt, truncateCommand as N, defineAppCommand as Nn, watchExecutorJob as Nt, resumeCommand as O, generateUserTypes as On, listExecutors as Ot, generate as P, commonArgs as Pn, startCommand as Pt, getCommand$1 as Q, compareSnapshotWithRemote as Qt, getConfiguredEditorCommand as R, isVerbose as Rn, executionsCommand as Rt, listApps as S, hasChanges as Sn, getFunctionRegistry as St, healthCommand as T, resourceTrn as Tn, triggerCommand as Tt, logBetaWarning as U, getCommand$6 as Ut, showCommand as V, toPageDirection as Vn, functionExecutionStatusToString as Vt, remove as W, getExecutor as Wt, listCommand$4 as X, handleOptionalToRequiredError as Xt, treeCommand as Y, MIGRATION_LABEL_KEY as Yt, listOrganizations as Z, parseMigrationLabelNumber as Zt, getWorkspace as _, reconstructSnapshotFromMigrations as _n, listMachineUsers as _t, updateUser as a, SCHEMA_FILE_NAME as an, getFolder as at, createCommand as b, formatDiffSummary as bn, listFunctionRegistries as bt, listCommand as c, compareSnapshots as cn, createCommand$1 as ct, inviteUser as d, getMigrationDirPath as dn, listOAuth2Clients as dt, protoGqlPermission as en, updateCommand$2 as et, restoreCommand as f, getMigrationFilePath as fn, getCommand$3 as ft, getCommand as g, loadDiff as gn, listCommand$7 as gt, listWorkspaces as h, isValidMigrationNumber as hn, tokenCommand as ht, updateCommand as i, MIGRATE_FILE_NAME as in, getCommand$2 as it, listWorkflows as j, apiCall as jn, jobsCommand as jt, resumeWorkflow as k, prompt as kn, getExecutorJob as kt, listUsers as l, createSnapshotFromLocalTypes as ln, createFolder as lt, listCommand$1 as m, getNextMigrationNumber as mn, getMachineUserToken as mt, query as n, DIFF_FILE_NAME as nn, listCommand$5 as nt, removeCommand as o, assertValidMigrationFiles as on, deleteCommand$1 as ot, restoreWorkspace as p, getMigrationFiles as pn, getOAuth2Client as pt, updateOrganization as q, waitForExecution as qt, queryCommand as r, INITIAL_SCHEMA_NUMBER as rn, listFolders as rt, removeUser as s, compareLocalTypesWithSnapshot as sn, deleteFolder as st, isNativeTypeScriptRuntime as t, DB_TYPES_FILE_NAME as tn, updateFolder as tt, inviteCommand as u, getLatestMigrationNumber as un, listCommand$6 as ut, deleteCommand as v, formatMigrationNumber as vn, generate$1 as vt, getAppHealth as w, ensureConfigId as wn, webhookCommand as wt, createWorkspace as x, formatMigrationDiff as xn, getCommand$4 as xt, deleteWorkspace as y, parseMigrationNumberArg as yn, listCommand$8 as yt, openInConfiguredEditor as z, pagedLogArgs as zn, getWorkflowExecution as zt };
18973
- //# sourceMappingURL=runtime-B8O6JFS2.mjs.map
19729
+ export { getOrganization as $, compareSnapshotWithRemote as $t, listCommand$3 as A, apiCommand as An, getExecutorWaitFailureMessage as At, show as B, pagedLogArgs as Bn, listWorkflowExecutions as Bt, listCommand$2 as C, hasChanges as Cn, listWebhookExecutors as Ct, waitWorkflowExecution as D, PluginManager as Dn, listCommand$9 as Dt, waitCommand as E, sdkNameLabelKey as En, triggerExecutor as Et, generateCommand as F, configArg as Fn, startWorkflow as Ft, removeCommand$1 as G, deploy as Gt, extractOwnedNamespaces as H, toPageDirection as Hn, formatKeyValueTable as Ht, generateMigrationScript as I, confirmationArgs as In, getCommand$5 as It, organizationTree as J, waitForExecution as Jt, updateCommand$1 as K, ensureConfigId as Kt, writeDbTypesFile as L, deploymentArgs as Ln, getWorkflow as Lt, truncate as M, assertWritable as Mn, listExecutorJobs as Mt, truncateCommand as N, defineAppCommand as Nn, watchExecutorJob as Nt, resumeCommand as O, generateUserTypes as On, listExecutors as Ot, generate as P, commonArgs as Pn, startCommand as Pt, getCommand$1 as Q, parseMigrationLabelNumber as Qt, getConfiguredEditorCommand as R, isVerbose as Rn, executionsCommand as Rt, listApps as S, formatMigrationDiff as Sn, getFunctionRegistry as St, healthCommand as T, resourceTrn as Tn, triggerCommand as Tt, logBetaWarning as U, workspaceArgs as Un, getCommand$6 as Ut, showCommand as V, paginationArgs as Vn, functionExecutionStatusToString as Vt, remove as W, getExecutor as Wt, listCommand$4 as X, MIGRATION_LABEL_KEY as Xt, treeCommand as Y, bundleMigrationScript as Yt, listOrganizations as Z, handleOptionalToRequiredError as Zt, getWorkspace as _, loadDiff as _n, listMachineUsers as _t, updateUser as a, MIGRATE_FILE_NAME as an, getFolder as at, createCommand as b, parseMigrationNumberArg as bn, listFunctionRegistries as bt, listCommand as c, compareLocalTypesWithSnapshot as cn, createCommand$1 as ct, inviteUser as d, getLatestMigrationNumber as dn, listOAuth2Clients as dt, generateAllTypeManifestsFromSnapshot as en, updateCommand$2 as et, restoreCommand as f, getMigrationDirPath as fn, getCommand$3 as ft, getCommand as g, isValidMigrationNumber as gn, listCommand$7 as gt, listWorkspaces as h, getNextMigrationNumber as hn, tokenCommand as ht, updateCommand as i, INITIAL_SCHEMA_NUMBER as in, getCommand$2 as it, listWorkflows as j, apiCall as jn, jobsCommand as jt, resumeWorkflow as k, prompt as kn, getExecutorJob as kt, listUsers as l, compareSnapshots as ln, createFolder as lt, listCommand$1 as m, getMigrationFiles as mn, getMachineUserToken as mt, query as n, DB_TYPES_FILE_NAME as nn, listCommand$5 as nt, removeCommand as o, SCHEMA_FILE_NAME as on, deleteCommand$1 as ot, restoreWorkspace as p, getMigrationFilePath as pn, getOAuth2Client as pt, updateOrganization as q, executeScript as qt, queryCommand as r, DIFF_FILE_NAME as rn, listFolders as rt, removeUser as s, assertValidMigrationFiles as sn, deleteFolder as st, isNativeTypeScriptRuntime as t, protoGqlPermission as tn, updateFolder as tt, inviteCommand as u, createSnapshotFromLocalTypes as un, listCommand$6 as ut, deleteCommand as v, reconstructSnapshotFromMigrations as vn, generate$1 as vt, getAppHealth as w, getNamespacesWithMigrations as wn, webhookCommand as wt, createWorkspace as x, formatDiffSummary as xn, getCommand$4 as xt, deleteWorkspace as y, formatMigrationNumber as yn, listCommand$8 as yt, openInConfiguredEditor as z, multiConfigArg as zn, getWorkflowExecution as zt };
19730
+ //# sourceMappingURL=runtime-Zkg3SUwi.mjs.map