import { Logger } from "../../shared/logger.js"; import type { EnvironmentConfiguration, RootConfiguration } from "../../config/types.js"; import type { AuthoringApiClient } from "../api/client.js"; /** * Shared option shapes for the three `scai provision recipe` tasks. * * All three honor the standard verbosity options (`--quiet`, `--json`, * `--log-file`). Plan and push additionally need an environment to talk * to a tenant; compile is pure-logic and doesn't. */ export interface RecipeCommonOptions { config?: string; verbose?: boolean; trace?: boolean; quiet?: boolean; json?: boolean; logFile?: string; nonInteractive?: boolean; } export interface RecipeCompileOptions extends RecipeCommonOptions { /** Single recipe file path. Defaults to the config `recipes` glob. */ input?: string; output?: string; /** * Directory to collect the WHOLE compiled set into, one * `.ir.json` per IR (per-recipe + cross-recipe aggregates), * flat. Unlike `--output` (single file) this is the artifact a batch * driver hands to `push --from-compiled `. Mutually exclusive with * `--output`. */ outputDir?: string; /** * Locale scope (`--languages`) baked into the compiled IR — the same * COMPILE-time input `recipe push` resolves. Localized content (dictionary * translations, `__Standard Values` locale-map defaults) is EMITTED per * this scope, so a precompiled artifact MUST be compiled with the scope * the eventual push would use. Requires an environment (`-n`) to resolve * the installed-language intersection; without one the scope is a no-op * and every authored locale is emitted (the offline default). */ languages?: readonly string[]; /** Override `templatesRoot` from the env profile. */ templatesRoot?: string; /** Override `renderingsRoot` from the env profile. */ renderingsRoot?: string; /** Override `componentsRoot` from the env profile (per-site folder layout). */ componentsRoot?: string; /** Override `contentModelsRoot` from the env profile (per-site folder layout). */ contentModelsRoot?: string; /** Override `partialDesignsRoot` from the env profile (Phase 4). */ partialDesignsRoot?: string; /** Override `pageDesignsRoot` from the env profile (Phase 4). */ pageDesignsRoot?: string; /** Override `contentItemsRoot` from the env profile (Phase 4). */ contentItemsRoot?: string; /** Override `mediaLibraryRoot` from the env profile (recipe-materialised media). */ mediaLibraryRoot?: string; /** * Path to a JSON image-defaults map (role → external URL). Image * values / SV defaults declaring a matching `role` materialise the * mapped URL instead of the recipe-authored one. Falls back to the * `SITECOREAI_IMAGE_DEFAULTS` env var (also a path). */ imageDefaults?: string; /** Override `headlessVariantsRoot` from the env profile. */ headlessVariantsRoot?: string; /** Override `availableRenderingsRoot` from the env profile. */ availableRenderingsRoot?: string; /** Override `enumerationsRoot` from the env profile. */ enumerationsRoot?: string; /** * Active env profile to source `templatesRoot` / `renderingsRoot` * defaults from when the flags are not passed. Required for compile * since compile is otherwise environment-agnostic. */ environmentName?: string; } export interface RecipeTenantOptions extends RecipeCommonOptions { environmentName?: string; /** * Optional baseline storage backend. Defaults to a per-(env, recipe) * file under `/.scai/baseline/`. Pass a custom * `BaselineStorage` instance (orchestrator-hosted, in-memory, etc.) * to share baselines across operators / CI without changing the * push / pull entry points. * * See `src/recipe/runtime/baseline.ts` for the `BaselineStorage` * interface contract. Caller is responsible for choosing storage * that's appropriate for the run (a remote storage in a CI flow, * file storage for local dev). */ baselineStorage?: import("../runtime/baseline").BaselineStorage; } export interface RecipePlanOptions extends RecipeTenantOptions { /** Single IR file path. Defaults to the config `recipes` glob (compiled in-memory). */ input?: string; output?: string; /** * Operator override for prune-rollback snapshot languages. Mirrors * the `--snapshot-languages` plumbing on `recipe push`; when unset, * the planner auto-discovers via `getTenantLanguages`. See * `RecipePushOptions.snapshotLanguages`. */ snapshotLanguages?: readonly string[]; } export interface RecipePushOptions extends RecipeTenantOptions { /** Single recipe file path. Defaults to the config `recipes` glob. */ input?: string; /** * Directory of pre-compiled `.ir.json` files (as emitted by * `scai provision recipe compile`). When set, the push loads the IR set * from this directory and **skips compilation entirely** — no `.recipe.ts` * loading, no `compileRecipeSet`, no tenant-read compile inputs * (media-root alignment, language resolution). Everything downstream — * the cross-recipe ref pre-seed, `--handles` / `--aggregates-only` * scoping, and apply — runs unchanged against the loaded IRs. * * This is the "compile once, apply many" seam: a batch driver compiles * the whole set ONCE (`recipe compile --output-dir `) and then runs * each chunk as `push --from-compiled --handles `, paying the * full-set compile a single time instead of once per chunk. Mutually * exclusive with `--input` (the CLI enforces it); the config `recipes` * glob is ignored when this is set. */ fromCompiled?: string; /** Override `templatesRoot` from the env profile. */ templatesRoot?: string; /** Override `renderingsRoot` from the env profile. */ renderingsRoot?: string; /** Override `componentsRoot` from the env profile (per-site folder layout). */ componentsRoot?: string; /** Override `contentModelsRoot` from the env profile (per-site folder layout). */ contentModelsRoot?: string; /** Override `partialDesignsRoot` from the env profile (Phase 4). */ partialDesignsRoot?: string; /** Override `pageDesignsRoot` from the env profile (Phase 4). */ pageDesignsRoot?: string; /** Override `contentItemsRoot` from the env profile (Phase 4). */ contentItemsRoot?: string; /** Override `mediaLibraryRoot` from the env profile (recipe-materialised media). */ mediaLibraryRoot?: string; /** * Path to a JSON image-defaults map (role → external URL). Image * values / SV defaults declaring a matching `role` materialise the * mapped URL instead of the recipe-authored one. Falls back to the * `SITECOREAI_IMAGE_DEFAULTS` env var (also a path). */ imageDefaults?: string; /** Override `headlessVariantsRoot` from the env profile. */ headlessVariantsRoot?: string; /** Override `availableRenderingsRoot` from the env profile. */ availableRenderingsRoot?: string; /** Override `enumerationsRoot` from the env profile. */ enumerationsRoot?: string; whatIf?: boolean; allowWrite?: boolean; /** * Operator consent to delete items via `PruneChildren` ops with * `mode: "delete"`. Independent of `allowWrite`: writes are always * gated by --apply + the policy tier; PRUNES are additionally gated * by this flag. Without it, a recipe IR containing a delete-mode * PruneChildren throws `POLICY_DENIED` rather than silently * degrading. Set via `--allow-prune` after reviewing the prune list * from a `--what-if` run. */ allowPrune?: boolean; /** * Operator override for which languages prune-rollback snapshots * capture. When unset, the planner auto-discovers via the Authoring * API's tenant-level `languages { nodes { name } }` connection * (`getTenantLanguages` on the client). The XM Cloud schema does * NOT expose `Item.languages` — item-level discovery isn't possible, * so the planner enumerates tenant languages and probes each per * item via `getItemVersions`. Falls back to `["en"]` if the tenant * `languages` query errors. Set explicitly (comma-separated via * `--snapshot-languages`) to bound snapshot cost on tenants where * auto-discovery returns languages you don't want captured. The * first entry doubles as the inverse `createItem`'s language. */ snapshotLanguages?: readonly string[]; /** * When true, skip recipes whose compiled IR digest + env-profile roots * digest both match the persisted `.scai/recipe-cache.json` entry from * the previous successful push. Speedups re-pushes of an unchanged * recipe set on warm tenants. Off by default — out-of-band CMS edits * to recipe-auto-redetected until either the recipe source changes or * the cache is invalidated. */ skipUnchangedRecipes?: boolean; /** * Optional handle filter. When set, only recipes whose `recipeHandle` * appears in this list are pushed; the rest are dropped after * compile. Matches the `handles` field convention used by the * orchestrator's brief/campaign sync plans so a `recipe-sync` worker * spawning scai can pass through a consistent narrow-by-handle flag. * * Filtering happens AFTER compile because cross-recipe references * need every recipe in the set to resolve. Unknown handles in the * list are ignored (logged at info); when every handle is unknown * the push degrades to a no-op rather than failing. */ handles?: readonly string[]; /** * Apply ONLY the synthetic cross-recipe aggregate IRs (the `__name__` * handles `compileRecipeSet` synthesizes: Available Renderings lists, * shared Data Folder insert options, placeholder settings, subtree * ownership prunes, …). The batch-driver complement to `handles`: * handle-scoped pushes DROP the aggregates (their synthetic handles are * never in a driver's chunk), so a driver runs its chunks with * `--handles` and then one `--aggregates-only` pass to land the * shared-item writes exactly once, after every referenced per-recipe * item exists. Mutually exclusive with `handles` (the CLI enforces it). */ aggregatesOnly?: boolean; /** * Optional locale scope (`--languages`). When set, the push only * REGISTERS scoped locales on the environment and only EMITS localized * content (dictionary translations, `__Standard Values` locale-map * defaults) for locales matching the scope — an entry matches its exact * code and, for bare base languages, every regional variant (`fr` * covers `fr-FR`). The primary locale always installs regardless, so * `--languages en` is the fast "content now, locales later" install * shape; a later push without the flag adds the remaining locales * (version adds + translations are idempotent). Unset = full * declared-locale behaviour. Note: `ContentItemRecipe` story versions * are not scoped by this flag — they carry explicit per-language * authoring and rely on apply-time unregistered-language skips. */ languages?: readonly string[]; /** * Provision the `--languages` scope onto the environment before the * push resolves its localize targets (`--provision-languages`). * * By default a push treats the environment's registered languages as * authoritative: a scoped locale the environment lacks is DROPPED, not * created — provisioning is the operator's step. Install pipelines * that derive the scope from a trusted source (the brand's language * list) opt in here so the environment converges on that list instead * of silently narrowing it. Idempotent (present codes are skipped, * "already added" tolerated) and additive only; newly-added languages * get their fallback chain wired (regional → base → en). Requires * `--languages` — with no scope there is nothing to provision. */ provisionLanguages?: boolean; /** * Three-way merge conflict policy. Governs how the planner resolves * drift entries whose tenant value differs from the last-applied * baseline: * - `"error"` (default) — apply blocks; per-conflict action gets * status `"conflict"` and the recipe aborts before any writes. * - `"recipe-wins"` — recipe value clobbers the tenant edit (matches * the legacy two-way diff behaviour scai had before baselines * existed). * - `"cms-wins"` — tenant value is preserved, recipe-side change is * dropped for this push (the recipe re-writes next time if the * baseline still disagrees). * * Only matters when a baseline is loaded. Without one (`--no-baseline` * or first push to an env), every drift surfaces as `"update"` and * this flag has no effect. */ conflictPolicy?: "error" | "recipe-wins" | "cms-wins"; /** * Opt out of three-way merge baseline loading + post-apply writing. * Use cases: * - First-push test runs against a clean tenant where the operator * doesn't want a baseline file polluting the workspace. * - CI runs where the baseline isn't checked in and operator wants * legacy recipe-wins behaviour. * - Debugging: comparing pre-baseline vs with-baseline behaviour. * * Default `false` — baselines load and write automatically. The * baseline file lives at * `/.scai/baseline//.baseline.json` * and is per-recipe per-environment. */ noBaseline?: boolean; /** * Plan-mode parallelism across recipes. Plan reads are pure (no * mutations, no shared mutable refs across recipes), so plan-mode * IRs can run concurrently. Defaults to 4. Apply-mode always runs * sequentially — within a push, mutations land in topological order. */ planConcurrency?: number; /** * Optional progress callback. Receives per-recipe execution events * as they happen (op-start / op-result / apply-start / apply-success * / apply-error / site-job-poll / rollback events). Used by external * orchestrators (e.g. `scai mcp serve`) to forward live progress to * a client. The CLI logger does not need this — it observes the same * events via its own internal collator. */ emit?: (event: { recipe: string; event: import("../runtime/execute").ExecutionEvent; }) => void; /** * Cooperative cancellation. When the signal fires, the executor * stops between operations, rolls back applied mutations, and the * per-recipe `ExecutionResult.aborted` is set to true. */ signal?: AbortSignal; } export declare const toLogger: (options: RecipeCommonOptions) => Logger; export interface ResolvedTenant { envName: string; environment: EnvironmentConfiguration; root: RootConfiguration; client: AuthoringApiClient; } export declare const resolveTenant: (options: RecipeTenantOptions, clientOptions?: { pathItemIdCache?: Map; }) => ResolvedTenant; /** * Whether a recipe set needs `templatesRoot` / `renderingsRoot` resolved. * * True when at least one recipe creates template / rendering items — * every kind except `ROOTLESS_RECIPE_KINDS`. An empty set (e.g. a push * fed only pre-compiled `.ir.json` inputs, which carry their roots baked * in) needs neither. New recipe kinds default to *needing* roots; add a * kind to `ROOTLESS_RECIPE_KINDS` only once its compiler is confirmed to * ignore both roots. */ export declare const recipeSetNeedsRoots: (recipes: readonly { kind: string; }[]) => boolean; /** * Backfill an env profile's recipe-root fields from `site` + `siteCollection` * when both are set and a given root is absent. Explicit `*Root` config wins * over derivation; CLI flags (applied by the resolvers below) win over both — * preserving the `flag > explicit config > derived` precedence. * * Returns the profile unchanged when it lacks `site`/`siteCollection`, so * existing explicit-roots configs are untouched. Auto-resolving the collection * from the environment (Sites API) lands in a later milestone; for now both * values must be present for derivation to apply. See * `plans/recipe-roots-derivation.md`. */ export declare const withDerivedRecipeRoots: (environment: EnvironmentConfiguration | undefined) => EnvironmentConfiguration | undefined; /** * When an env profile sets `site` but not `siteCollection`, resolve the * collection (parent tenant) by discovering the environment's sites and * matching by name — so authors can configure just `site` and let scai fill in * the collection that recipeRoots derivation needs. No-op when `site` is unset * or `siteCollection` is already configured, so it never hits the network for * explicit-roots / collection-set configs. * * `discover` is injected (callers wire scai's `discoverSites`) to keep this * unit testable without a tenant. Throws a clear `INPUT_INVALID` when discovery * fails or no site matches, pointing at the explicit `siteCollection` escape * hatch. See `plans/recipe-roots-derivation.md`. */ export declare const ensureSiteCollection: (environment: EnvironmentConfiguration | undefined, envName: string, discover: (environment: EnvironmentConfiguration) => Promise>) => Promise; /** * Resolve the recipe parent paths that the compiler will use for top-level * template + rendering items. * * Lookup order: * 1. `--templates-root` / `--renderings-root` CLI flags * 2. `envProfiles[].templatesRoot` / `.renderingsRoot` from * sitecoreai.cli.json (env-overrides via * `SITECOREAI_ENV__TEMPLATES_ROOT` / `_RENDERINGS_ROOT` apply * at config-load time before this helper runs) * 3. When `required`, throws `INPUT_INVALID` with a hint pointing at * the envProfile shape. When not required (a workflow- / * webhook-authorization-only set, or an IR-only push), missing * roots resolve to `""` — the compilers in play never read them. * * Pass `required` from `recipeSetNeedsRoots(recipes)` once the set's * recipe kinds are known. * * Tenant-specific because each site has its own * `/sitecore/templates/Project//Components` location. Putting roots * in config keeps the orchestrator's `recipe push` invocation * config-driven (no plan-schema fields, no extra arg plumbing). */ export declare const resolveRecipeRoots: (options: { templatesRoot?: string; renderingsRoot?: string; }, environment: EnvironmentConfiguration | undefined, envName: string, required?: boolean) => { templatesRoot: string; renderingsRoot: string; }; /** * Derive the GUID-seed site for an env profile. * * Recipe item GUIDs are `uuidv5(`${seed}::${handle}`)` where `seed` is * `CompileContext.site ?? "default"` (see `compile/shared.ts#siteOf`). * Returning `undefined` here leaves `context.site` unset, so the seed stays * `"default"` — byte-identical to legacy behavior for every profile that does * not opt in. * * When a profile sets `siteScopedGuids: true`, the seed becomes its `site`, * so the same recipe handle resolves to a *distinct* item per site (required * to install one recipe onto multiple sites in one Sitecore instance without * colliding on Sitecore's globally-unique item IDs). * * MUST be called from every compile path (push, pull, compile, sync) so the * write path and the read/diff paths agree on item GUIDs — a mismatch would * silently break the three-way merge. * * Throws `INPUT_INVALID` when scoping is enabled but no `site` is configured. */ export declare const resolveSeedSite: (environment: Pick | undefined) => string | undefined; /** * Derive the content-tree path segment for `{site}` substitution in * `PageRecipe.itemPath` — `/`, the SXA Headless * location every install targets (`/sitecore/content//`). * * Distinct from `resolveSeedSite` above on purpose: the GUID seed is an * identity concern (opt-in, defaults to the `default` sentinel so GUIDs * stay stable), while this is the physical tenant tree location. The old * behaviour substituted the GUID seed into `{site}` paths, so pages * landed in a phantom `/sitecore/content/default/` tree unless * `siteScopedGuids` happened to be on — and even then missed the * collection segment. * * Returns `undefined` when either value is missing; `compilePageRecipe` * throws a targeted INPUT_INVALID only when a `{site}` itemPath actually * needs it, so site-less sets (workflow-only, component-only) compile * unchanged. */ export declare const resolveSitePathSegment: (environment: Pick | undefined) => string | undefined; export interface RecipeInputResolution { files: string[]; source: "input-flag" | "config-glob"; } /** * Resolve the recipe files a task should operate on. Precedence: * 1. `--input ` if provided (single file). * 2. `recipes` globs from sitecoreai.cli.json (zero, one, or many files). * * Returns absolute paths. Throws `INPUT_INVALID` when neither path resolves * to any files — telling the user how to fix it. */ export declare const resolveRecipeInputs: (options: { input?: string; config?: string; }, root: RootConfiguration) => Promise; /** * Resolve the pre-compiled `.ir.json` files under a `--from-compiled` * directory. Recurses so a nested `.scai/` layout (compile's default) and * a flat `--output-dir` artifact both resolve. Returns absolute paths as an * `input-flag`-shaped resolution — `partitionInputFiles` routes every match * down the load-not-compile branch. Throws `INPUT_INVALID` when the * directory holds no IR, telling the operator how to produce one. */ export declare const resolveCompiledIrInputs: (dir: string) => Promise; export { ensureAllowWrite } from "../../policy/allow-write.js"; /** * Load the image-defaults map (role → external URL) for a compile/push. * Resolution: `--image-defaults ` flag → `SITECOREAI_IMAGE_DEFAULTS` * env var (also a path) → undefined (recipe defaults apply unchanged). * * The file must be a flat JSON object of string → string; values must be * fully-qualified http(s) URLs (the compiler re-validates per role at the * substitution site, but failing fast here surfaces a broken map before * any tenant work starts). */ export declare const loadImageDefaults: (imageDefaultsPath: string | undefined) => Promise | undefined>;