{"version":3,"file":"impl-DBK5c6d0.mjs","names":[],"sources":["../src/lib/custom-functions/manifest.ts","../src/commands/custom-functions/push/impl.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\n\nimport { CustomFunctionPayloadType, CustomFunctionType } from '@transcend-io/privacy-types';\nimport type { CustomFunctionConfigInput } from '@transcend-io/sdk';\nimport { decodeCodec, type ObjByString } from '@transcend-io/type-utils';\nimport * as t from 'io-ts';\nimport yaml from 'js-yaml';\nimport { isMap, isScalar, isSeq, parseDocument } from 'yaml';\n\nimport { replaceVariablesInYaml } from '../readTranscendYaml.js';\n\nexport const CustomFunctionManifestEntry = t.intersection([\n  t.type({\n    /** Display name of the custom function — used as the sync key when no `id` is set */\n    name: t.string,\n    /** Path to the TypeScript source file, relative to the manifest */\n    code: t.string,\n  }),\n  t.partial({\n    /**\n     * Custom function ID. Optional, but required to disambiguate when multiple\n     * custom functions share a name. Written back by `push --updateManifest`.\n     */\n    id: t.string,\n    /** Description shown in the Transcend dashboard */\n    description: t.string,\n    /** Custom function type. Defaults to GENERAL */\n    type: t.union([t.literal(CustomFunctionType.Dsr), t.literal(CustomFunctionType.General)]),\n    /** Data silo ID to attach to (required for DSR functions) */\n    'data-silo-id': t.string,\n    /** The Sombra gateway the function belongs to */\n    'sombra-id': t.string,\n    /**\n     * Name of the environment variable holding the internal key of the\n     * function's Sombra gateway. The key itself never lives in the manifest —\n     * it is read from the CLI's process environment at push time. Overrides\n     * `--sombraAuth` for this entry.\n     */\n    'sombra-auth-env': t.string,\n    /** Hosts the function may make network requests to */\n    'allowed-hosts': t.array(t.string),\n    /** Execution timeout in milliseconds */\n    'timeout-ms': t.number,\n    /** Whether the function may import third party modules */\n    'allow-third-party-imports': t.boolean,\n    /** Environment variables to expose to the function */\n    env: t.record(t.string, t.string),\n    /**\n     * Path to a JSON file (relative to the manifest) holding the test payload\n     * to run the function with before pushing. When set, the function is\n     * test-run after signing and only pushed/promoted when the test passes.\n     * Shorthand for a single-item `test-payloads` list.\n     */\n    'test-payload': t.string,\n    /**\n     * Which export to invoke when test-running a DSR function:\n     * `DATA_POINT` invokes the default export, `REQUEST_ENRICHER` invokes the\n     * `enricher` export. Defaults to DATA_POINT. Ignored for GENERAL functions.\n     * Only valid alongside `test-payload`.\n     */\n    'test-payload-type': t.union([\n      t.literal(CustomFunctionPayloadType.DataPoint),\n      t.literal(CustomFunctionPayloadType.RequestEnricher),\n    ]),\n    /**\n     * Test payloads to run the function with before pushing. Every payload\n     * runs and all must pass for the function to be pushed/promoted. DSR\n     * functions should list one payload per export they implement\n     * (`DATA_POINT` for the default export, `REQUEST_ENRICHER` for the\n     * `enricher` export). Mutually exclusive with `test-payload`.\n     */\n    'test-payloads': t.array(\n      t.intersection([\n        t.type({\n          /** Path to the JSON payload file, relative to the manifest */\n          payload: t.string,\n        }),\n        t.partial({\n          /** Which export the payload invokes (DSR only). Defaults to DATA_POINT */\n          'payload-type': t.union([\n            t.literal(CustomFunctionPayloadType.DataPoint),\n            t.literal(CustomFunctionPayloadType.RequestEnricher),\n          ]),\n        }),\n      ]),\n    ),\n  }),\n]);\n\n/** Override type */\nexport type CustomFunctionManifestEntry = t.TypeOf<typeof CustomFunctionManifestEntry>;\n\nexport const CustomFunctionsManifest = t.type({\n  /** The custom functions to sync */\n  functions: t.array(CustomFunctionManifestEntry),\n});\n\n/** Override type */\nexport type CustomFunctionsManifest = t.TypeOf<typeof CustomFunctionsManifest>;\n\n/**\n * Read a custom functions manifest from disk, apply variable substitution,\n * validate its shape, and load each function's source code.\n *\n * @param filePath - Path to the manifest YAML file\n * @param variables - Variables to fill into `<<parameters.x>>` placeholders\n * @returns The custom function configs, with code loaded from disk\n */\n/**\n * A custom function config from the manifest, plus CLI-level settings that\n * are not part of the SDK sync input.\n */\nexport type CustomFunctionManifestConfig = CustomFunctionConfigInput & {\n  /** Env variable name holding the internal key of the function's Sombra gateway */\n  sombraAuthEnv?: string;\n  /** Parsed JSON test payloads to run the function with before pushing */\n  testPayloads?: {\n    /** The parsed JSON payload */\n    payload: object;\n    /** Which export the payload invokes when test-running a DSR function */\n    payloadType?: Exclude<CustomFunctionPayloadType, typeof CustomFunctionPayloadType.Maestro>;\n  }[];\n};\n\nexport function readCustomFunctionsManifest(\n  filePath: string,\n  variables: ObjByString = {},\n): CustomFunctionManifestConfig[] {\n  const fileContents = readFileSync(filePath, 'utf-8');\n\n  const replacedVariables = replaceVariablesInYaml(\n    fileContents,\n    variables,\n    `Also check that there are no extra variables defined in your manifest: ${filePath}`,\n  );\n\n  const manifest = decodeCodec(CustomFunctionsManifest, yaml.load(replacedVariables));\n\n  // IDs must be unique — two entries cannot target the same function\n  const duplicateIds = manifest.functions\n    .map(({ id }) => id)\n    .filter((id, index, ids) => id !== undefined && ids.indexOf(id) !== index);\n  if (duplicateIds.length > 0) {\n    throw new Error(\n      `Duplicate custom function ids in manifest: ${[...new Set(duplicateIds)].join(', ')}`,\n    );\n  }\n\n  // Names may repeat only when every entry sharing the name carries an `id`\n  // to disambiguate; an id-less entry syncs by name and would be ambiguous.\n  const allNames = manifest.functions.map(({ name }) => name);\n  const ambiguousNames = manifest.functions\n    .filter(({ id, name }) => id === undefined && allNames.filter((n) => n === name).length > 1)\n    .map(({ name }) => name);\n  if (ambiguousNames.length > 0) {\n    throw new Error(\n      `Duplicate custom function names in manifest without ids: ${[...new Set(ambiguousNames)].join(', ')}. ` +\n        'Add an `id` field to each duplicated entry to disambiguate.',\n    );\n  }\n\n  const manifestDir = dirname(resolve(filePath));\n\n  /**\n   * Load and parse a JSON test payload file.\n   *\n   * @param entryName - The manifest entry name, for error messages\n   * @param payloadFile - The payload file path, relative to the manifest\n   * @returns The parsed payload object\n   */\n  const loadTestPayload = (entryName: string, payloadFile: string): object => {\n    const testPayloadPath = resolve(manifestDir, payloadFile);\n    if (!existsSync(testPayloadPath)) {\n      throw new Error(\n        `Test payload file for custom function \"${entryName}\" does not exist: ${testPayloadPath}`,\n      );\n    }\n    const rawPayload = readFileSync(testPayloadPath, 'utf-8');\n    try {\n      return JSON.parse(rawPayload);\n    } catch (err) {\n      throw new Error(\n        `Test payload file for custom function \"${entryName}\" is not valid JSON ` +\n          `(${testPayloadPath}): ${(err as Error).message}`,\n      );\n    }\n  };\n\n  return manifest.functions.map((entry) => {\n    const codePath = resolve(manifestDir, entry.code);\n    if (!existsSync(codePath)) {\n      throw new Error(`Code file for custom function \"${entry.name}\" does not exist: ${codePath}`);\n    }\n\n    // `test-payload` is shorthand for a single-item `test-payloads` list\n    if (entry['test-payload'] !== undefined && entry['test-payloads'] !== undefined) {\n      throw new Error(\n        `Custom function \"${entry.name}\" sets both test-payload and test-payloads — ` +\n          'use test-payloads alone to define multiple payloads.',\n      );\n    }\n    if (entry['test-payload-type'] !== undefined && entry['test-payload'] === undefined) {\n      throw new Error(\n        `Custom function \"${entry.name}\" sets test-payload-type without test-payload — ` +\n          'set payload-type per item in test-payloads instead.',\n      );\n    }\n    let testPayloads: CustomFunctionManifestConfig['testPayloads'];\n    if (entry['test-payload'] !== undefined) {\n      testPayloads = [\n        {\n          payload: loadTestPayload(entry.name, entry['test-payload']),\n          ...(entry['test-payload-type'] !== undefined\n            ? { payloadType: entry['test-payload-type'] }\n            : {}),\n        },\n      ];\n    } else if (entry['test-payloads'] !== undefined && entry['test-payloads'].length > 0) {\n      testPayloads = entry['test-payloads'].map((item) => ({\n        payload: loadTestPayload(entry.name, item.payload),\n        ...(item['payload-type'] !== undefined ? { payloadType: item['payload-type'] } : {}),\n      }));\n    }\n\n    return {\n      name: entry.name,\n      code: readFileSync(codePath, 'utf-8'),\n      ...(entry.id !== undefined ? { id: entry.id } : {}),\n      ...(entry.description !== undefined ? { description: entry.description } : {}),\n      ...(entry.type !== undefined ? { type: entry.type } : {}),\n      ...(entry['data-silo-id'] !== undefined ? { dataSiloId: entry['data-silo-id'] } : {}),\n      ...(entry['sombra-id'] !== undefined ? { sombraId: entry['sombra-id'] } : {}),\n      ...(entry['sombra-auth-env'] !== undefined\n        ? { sombraAuthEnv: entry['sombra-auth-env'] }\n        : {}),\n      ...(entry['allowed-hosts'] !== undefined ? { allowedHosts: entry['allowed-hosts'] } : {}),\n      ...(entry['timeout-ms'] !== undefined ? { timeoutMs: entry['timeout-ms'] } : {}),\n      ...(entry['allow-third-party-imports'] !== undefined\n        ? { allowThirdPartyImports: entry['allow-third-party-imports'] }\n        : {}),\n      ...(entry.env !== undefined ? { env: entry.env } : {}),\n      ...(testPayloads !== undefined ? { testPayloads } : {}),\n    };\n  });\n}\n\n/**\n * The IDs assigned to a manifest entry during a push, to write back into the\n * manifest file.\n */\nexport interface CustomFunctionManifestIds {\n  /** Custom function ID */\n  id?: string;\n  /** Data silo (DSR integration) ID, for DSR functions */\n  dataSiloId?: string;\n}\n\n/**\n * Write custom function and data silo IDs back into a manifest file, so\n * future pushes match by ID instead of by (potentially non-unique) name and\n * DSR entries keep pointing at their integration.\n *\n * The raw file text is edited via a comment- and formatting-preserving YAML\n * document, so comments and un-substituted `<<parameters.x>>` placeholders\n * survive untouched. Only entries that are missing the respective key are\n * modified.\n *\n * @param filePath - Path to the manifest YAML file\n * @param idsByIndex - IDs for each manifest entry, by array index (undefined\n *   entries are left unchanged)\n * @returns The number of entries that were updated\n */\nexport function writeCustomFunctionIdsToManifest(\n  filePath: string,\n  idsByIndex: (CustomFunctionManifestIds | undefined)[],\n): number {\n  const document = parseDocument(readFileSync(filePath, 'utf-8'));\n  const functions = document.get('functions');\n  if (!isSeq(functions)) {\n    throw new Error(`Expected a \\`functions\\` list in manifest: ${filePath}`);\n  }\n\n  let updated = 0;\n  functions.items.forEach((item, index) => {\n    const ids = idsByIndex[index];\n    if (!ids || !isMap(item)) {\n      return;\n    }\n    let changed = false;\n    if (ids.id && !item.has('id')) {\n      // Place `id` first so it reads as the entry's key\n      item.items.unshift(document.createPair('id', ids.id));\n      changed = true;\n    }\n    if (ids.dataSiloId && !item.has('data-silo-id')) {\n      // Place `data-silo-id` right after `id` (or first when there is no id)\n      const idIndex = item.items.findIndex((pair) => isScalar(pair.key) && pair.key.value === 'id');\n      item.items.splice(idIndex + 1, 0, document.createPair('data-silo-id', ids.dataSiloId));\n      changed = true;\n    }\n    if (changed) {\n      updated += 1;\n    }\n  });\n\n  if (updated > 0) {\n    writeFileSync(filePath, document.toString());\n  }\n  return updated;\n}\n","import { existsSync } from 'node:fs';\n\nimport { CustomFunctionPayloadType, CustomFunctionType } from '@transcend-io/privacy-types';\nimport {\n  buildTranscendGraphQLClient,\n  createSombraGotInstance,\n  fetchAllCustomFunctions,\n  resolveEffectiveSombraId,\n  resolveExistingCustomFunction,\n  syncCustomFunction,\n  type CustomFunctionSyncResult,\n} from '@transcend-io/sdk';\nimport { mapSeries } from '@transcend-io/utils';\nimport colors from 'colors';\n\nimport type { LocalContext } from '../../../context.js';\nimport { validateTranscendAuth } from '../../../lib/api-keys/index.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport {\n  readCustomFunctionsManifest,\n  writeCustomFunctionIdsToManifest,\n} from '../../../lib/custom-functions/manifest.js';\nimport { parseVariablesFromString } from '../../../lib/helpers/parseVariablesFromString.js';\nimport { logger } from '../../../logger.js';\n\nexport interface CustomFunctionsPushCommandFlags {\n  auth: string;\n  sombraAuth?: string;\n  transcendUrl: string;\n  file: string;\n  variables: string;\n  dryRun: boolean;\n  promote: boolean;\n  force: boolean;\n  skipTests: boolean;\n  updateManifest: boolean;\n  sombraId?: string;\n}\n\nexport async function push(\n  this: LocalContext,\n  {\n    auth,\n    sombraAuth,\n    transcendUrl,\n    file = './transcend-functions.yml',\n    variables,\n    dryRun,\n    promote,\n    force,\n    skipTests,\n    updateManifest,\n    sombraId,\n  }: CustomFunctionsPushCommandFlags,\n): Promise<void> {\n  doneInputValidation(this.process.exit);\n\n  // This command operates on a single Transcend instance\n  const apiKeyOrList = validateTranscendAuth(auth);\n  if (Array.isArray(apiKeyOrList)) {\n    logger.error(\n      colors.red(\n        'transcend custom-functions push does not support a list of API keys — pass a single API key.',\n      ),\n    );\n    this.process.exit(1);\n  }\n  const apiKey = apiKeyOrList as string;\n\n  // Read and validate the manifest\n  if (!existsSync(file)) {\n    logger.error(\n      colors.red(\n        `The manifest file does not exist on disk: ${file}. ` +\n          'You can specify the file path using --file=./transcend-functions.yml',\n      ),\n    );\n    this.process.exit(1);\n  }\n  const vars = parseVariablesFromString(variables);\n  logger.info(colors.magenta(`Reading manifest \"${file}\"...`));\n  const configs = readCustomFunctionsManifest(file, vars);\n  logger.info(colors.green(`Found ${configs.length} custom function(s) in \"${file}\"`));\n\n  const client = buildTranscendGraphQLClient(transcendUrl, apiKey);\n\n  // Fetch existing functions once to diff against\n  const existing = await fetchAllCustomFunctions(client, { logger });\n\n  // Each custom function belongs to a single Sombra gateway whose keys sign\n  // its code, so code must be signed against that specific gateway's customer\n  // ingress. Cache one connection per distinct gateway + internal key across\n  // the run.\n  type SombraGot = Awaited<ReturnType<typeof createSombraGotInstance>>;\n  const sombraByGateway = new Map<string, SombraGot>();\n  const getSombraForGateway = async (\n    gatewaySombraId: string | undefined,\n    sombraApiKey: string | undefined,\n  ): Promise<SombraGot> => {\n    const key = `${gatewaySombraId ?? ''}\\u0000${sombraApiKey ?? ''}`;\n    const cached = sombraByGateway.get(key);\n    if (cached) {\n      return cached;\n    }\n    logger.info(\n      colors.magenta(\n        `Connecting to the ${\n          gatewaySombraId ? `Sombra gateway \"${gatewaySombraId}\"` : 'primary Sombra gateway'\n        } to sign code...`,\n      ),\n    );\n    const sombra = await createSombraGotInstance(transcendUrl, apiKey, {\n      logger,\n      sombraApiKey,\n      ...(gatewaySombraId ? { sombraId: gatewaySombraId } : {}),\n    });\n    sombraByGateway.set(key, sombra);\n    return sombra;\n  };\n\n  /**\n   * Resolve the Sombra internal key for a manifest entry: the env variable\n   * named by `sombra-auth-env` when set (which must be exported), else the\n   * `--sombraAuth` flag.\n   *\n   * @param input - The manifest entry\n   * @returns The internal key to authenticate with, if any\n   */\n  const resolveEntrySombraAuth = (input: {\n    /** Function name, for error messages */\n    name: string;\n    /** Env variable name holding the gateway's internal key */\n    sombraAuthEnv?: string;\n  }): string | undefined => {\n    if (!input.sombraAuthEnv) {\n      return sombraAuth;\n    }\n    const value = process.env[input.sombraAuthEnv];\n    if (!value) {\n      throw new Error(\n        `Custom function \"${input.name}\" sets sombra-auth-env: ${input.sombraAuthEnv}, ` +\n          'but that environment variable is not set. Export it (e.g. from a CI secret) ' +\n          'before pushing.',\n      );\n    }\n    return value;\n  };\n\n  // Sync each function in order\n  const results: { name: string; result?: CustomFunctionSyncResult; error?: Error }[] = [];\n  await mapSeries(configs, async (input) => {\n    try {\n      // Resolve the gateway this function belongs to: manifest sombra-id,\n      // else the existing function's gateway, else --sombraId, else primary.\n      // Also validates manifest-vs-existing gateway mismatches.\n      const effectiveSombraId = resolveEffectiveSombraId(\n        input,\n        resolveExistingCustomFunction(existing, input),\n        sombraId,\n      );\n      const result = await syncCustomFunction(client, {\n        input,\n        sombra: dryRun\n          ? undefined\n          : await getSombraForGateway(effectiveSombraId, resolveEntrySombraAuth(input)),\n        defaultSombraId: sombraId,\n        existing,\n        promote,\n        dryRun,\n        force,\n        // Test the freshly signed code before pushing; every payload must\n        // pass or the function is rejected. Dry runs never sign, so nothing\n        // is tested either.\n        ...(!skipTests && input.testPayloads !== undefined\n          ? { testPayloads: input.testPayloads }\n          : {}),\n        logger,\n      });\n      results.push({ name: input.name, result });\n\n      const suffix = result.versionNumber ? ` (version ${result.versionNumber})` : '';\n      const changes =\n        result.changedFields.length > 0 ? ` [${result.changedFields.join(', ')}]` : '';\n      switch (result.outcome) {\n        case 'created':\n          if (result.createdDataSilo && result.dataSiloId) {\n            logger.info(\n              colors.green(\n                `Created DSR integration (data silo ${result.dataSiloId}) for \"${input.name}\"`,\n              ),\n            );\n          }\n          logger.info(colors.green(`Created custom function \"${input.name}\"${suffix}`));\n          break;\n        case 'updated':\n          logger.info(\n            colors.green(\n              `Pushed new revision to \"${input.name}\"${suffix}${changes}${\n                result.promoted ? ' and promoted to active' : ' as a draft'\n              }`,\n            ),\n          );\n          break;\n        case 'metadata-updated':\n          logger.info(\n            colors.green(\n              `Updated metadata for \"${input.name}\"${changes} — code unchanged, no new revision`,\n            ),\n          );\n          break;\n        case 'skipped':\n          logger.info(\n            colors.yellow(\n              `Skipped \"${input.name}\" — no changes detected ` +\n                '(env variable values cannot be diffed; use --force if only values changed)',\n            ),\n          );\n          break;\n        case 'would-create':\n          logger.info(\n            colors.cyan(\n              `[dry run] Would create custom function \"${input.name}\"${\n                input.type === CustomFunctionType.Dsr && !input.dataSiloId\n                  ? ' and its DSR integration (data silo)'\n                  : ''\n              }`,\n            ),\n          );\n          break;\n        case 'would-update':\n          logger.info(\n            colors.cyan(`[dry run] Would push a new revision to \"${input.name}\"${changes}`),\n          );\n          break;\n        case 'test-failed': {\n          const failed = (result.testResults ?? []).filter(({ passed }) => !passed);\n          logger.error(\n            colors.red(\n              `Rejected \"${input.name}\" — ${failed.length} of ${\n                result.testResults?.length ?? 0\n              } test run(s) failed`,\n            ),\n          );\n          failed.forEach(({ payloadType, result: execution }) => {\n            const label = payloadType ? `[${payloadType}] ` : '';\n            logger.error(\n              colors.red(\n                `  ${label}${\n                  execution.error\n                    ? execution.error.message\n                    : `failed with exit code ${execution.exitCode}`\n                }`,\n              ),\n            );\n            execution.logs.forEach(({ file: logFile, message }) => {\n              logger.error(colors.red(`    [${logFile}] ${message}`));\n            });\n          });\n          if (result.createdDataSilo) {\n            logger.error(\n              colors.red(\n                `  The DSR integration (data silo) created for \"${input.name}\" was rolled back.`,\n              ),\n            );\n          }\n          break;\n        }\n      }\n\n      // Anything that reached the push path without a test payload was\n      // promoted untested — call it out so payloads get added over time\n      if (\n        !skipTests &&\n        (input.testPayloads === undefined || input.testPayloads.length === 0) &&\n        (result.outcome === 'created' || result.outcome === 'updated')\n      ) {\n        logger.warn(\n          colors.yellow(\n            `Custom function \"${input.name}\" was pushed without a test run — add a ` +\n              'test-payload to its manifest entry to enable test-before-promote.',\n          ),\n        );\n      }\n\n      // DSR functions have two entry points (default export = DATA_POINT,\n      // enricher export = REQUEST_ENRICHER); nudge toward covering both\n      if (\n        !skipTests &&\n        input.type === CustomFunctionType.Dsr &&\n        input.testPayloads !== undefined &&\n        input.testPayloads.length > 0 &&\n        (result.outcome === 'created' || result.outcome === 'updated')\n      ) {\n        const coveredTypes = new Set(\n          input.testPayloads.map(\n            ({ payloadType }) => payloadType ?? CustomFunctionPayloadType.DataPoint,\n          ),\n        );\n        if (coveredTypes.size === 1) {\n          const [covered] = coveredTypes;\n          const uncovered =\n            covered === CustomFunctionPayloadType.DataPoint\n              ? CustomFunctionPayloadType.RequestEnricher\n              : CustomFunctionPayloadType.DataPoint;\n          logger.warn(\n            colors.yellow(\n              `DSR custom function \"${input.name}\" only tests its ${covered} export — if it ` +\n                `also implements the ${uncovered} export, add a test payload with ` +\n                `payload-type: ${uncovered} so both entry points are tested on every push.`,\n            ),\n          );\n        }\n      }\n    } catch (err) {\n      results.push({ name: input.name, error: err as Error });\n      logger.error(\n        colors.red(`Failed to sync custom function \"${input.name}\": ${(err as Error).message}`),\n      );\n    }\n  });\n\n  // Write assigned IDs back into the manifest so future pushes match by ID\n  // instead of by (potentially non-unique) name, and DSR entries keep\n  // pointing at their (possibly auto-created) integration\n  if (updateManifest && !dryRun) {\n    const idsByIndex = configs.map((input, index) => {\n      const result = results[index]?.result;\n      if (!result) {\n        return undefined;\n      }\n      const ids = {\n        ...(!input.id && result.customFunctionId ? { id: result.customFunctionId } : {}),\n        ...(!input.dataSiloId && result.dataSiloId ? { dataSiloId: result.dataSiloId } : {}),\n      };\n      return Object.keys(ids).length > 0 ? ids : undefined;\n    });\n    const updatedCount = writeCustomFunctionIdsToManifest(file, idsByIndex);\n    if (updatedCount > 0) {\n      logger.info(\n        colors.green(\n          `Wrote assigned id(s) back to ${updatedCount} manifest entr(ies) in \"${file}\" — ` +\n            'commit this change so future pushes match by ID.',\n        ),\n      );\n    }\n  }\n\n  // Summarize\n  const count = (outcome: CustomFunctionSyncResult['outcome']): number =>\n    results.filter(({ result }) => result?.outcome === outcome).length;\n  const failures = results.filter(({ error }) => error !== undefined);\n  const rejected = count('test-failed');\n  logger.info(\n    colors.magenta(\n      `Custom function sync complete: ${count('created') + count('would-create')} created, ${\n        count('updated') + count('would-update')\n      } updated, ${count('metadata-updated')} metadata-only, ${count(\n        'skipped',\n      )} skipped, ${rejected} rejected (test failed), ${failures.length} failed${\n        dryRun ? ' (dry run)' : ''\n      }`,\n    ),\n  );\n\n  if (failures.length > 0 || rejected > 0) {\n    this.process.exit(1);\n  }\n}\n"],"mappings":"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"}