@sitecoreai-labs/sitecoreai-cli 0.1.2 → 0.2.1

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 (65) hide show
  1. package/dist/agents/tasks/agent.js +2 -2
  2. package/dist/agents/tasks/resources.js +2 -2
  3. package/dist/agents/tasks/space.js +1 -1
  4. package/dist/brand/api/auth.d.ts +16 -9
  5. package/dist/brand/api/auth.js +29 -19
  6. package/dist/brand/credential.d.ts +71 -1
  7. package/dist/brand/credential.js +119 -2
  8. package/dist/brand/recipe/diff.js +7 -2
  9. package/dist/brand/recipe/kind.js +0 -0
  10. package/dist/brand/recipe/schema.d.ts +113 -7
  11. package/dist/brand/recipe/schema.js +137 -8
  12. package/dist/brand/seed.d.ts +9 -5
  13. package/dist/brand/seed.js +30 -5
  14. package/dist/brief/api/briefs.d.ts +8 -0
  15. package/dist/brief/api/briefs.js +49 -11
  16. package/dist/brief/index.d.ts +1 -1
  17. package/dist/brief/index.js +2 -1
  18. package/dist/brief/recipe/index.d.ts +11 -2
  19. package/dist/brief/recipe/index.js +17 -3
  20. package/dist/brief/recipe/instance-diff.d.ts +4 -0
  21. package/dist/brief/recipe/instance-diff.js +77 -0
  22. package/dist/brief/recipe/instance-kind.d.ts +4 -0
  23. package/dist/brief/recipe/instance-kind.js +190 -0
  24. package/dist/brief/recipe/instance-schema.d.ts +61 -0
  25. package/dist/brief/recipe/instance-schema.js +68 -0
  26. package/dist/brief/recipe/schema.js +4 -1
  27. package/dist/brief/tasks/index.d.ts +35 -0
  28. package/dist/brief/tasks/index.js +62 -1
  29. package/dist/campaigns/recipe/schema.d.ts +39 -8
  30. package/dist/campaigns/recipe/schema.js +40 -10
  31. package/dist/commands/agents/sync.js +2 -2
  32. package/dist/commands/brand/seed.js +1 -1
  33. package/dist/commands/brand/sync.js +2 -2
  34. package/dist/commands/brief/create.d.ts +2 -0
  35. package/dist/commands/brief/create.js +56 -0
  36. package/dist/commands/brief/index.d.ts +3 -1
  37. package/dist/commands/brief/index.js +11 -1
  38. package/dist/commands/brief/sync.d.ts +9 -6
  39. package/dist/commands/brief/sync.js +54 -22
  40. package/dist/commands/brief/update.d.ts +2 -0
  41. package/dist/commands/brief/update.js +84 -0
  42. package/dist/commands/campaign/sync.js +2 -2
  43. package/dist/mcp/descriptions.js +3 -3
  44. package/dist/mcp/tools/brief-recipe.js +67 -23
  45. package/dist/mcp/tools/brief.js +83 -6
  46. package/dist/recipe/compile/design-parameters-template.js +5 -3
  47. package/dist/recipe/compile/enumeration.js +6 -11
  48. package/dist/recipe/compile/shared.js +12 -12
  49. package/dist/recipe/compile.js +4 -4
  50. package/dist/recipe/io.d.ts +8 -3
  51. package/dist/recipe/io.js +11 -81
  52. package/dist/recipe/items/read-current.js +31 -24
  53. package/dist/recipe/schema/recipe.d.ts +167 -84
  54. package/dist/recipe/schema/recipe.js +130 -46
  55. package/dist/recipe/schema/source-fields.d.ts +20 -0
  56. package/dist/recipe/schema/source-fields.js +25 -1
  57. package/dist/recipe/validate.d.ts +3 -3
  58. package/dist/recipe/validate.js +20 -10
  59. package/dist/sync/aggregate-kinds.js +1 -0
  60. package/dist/sync/aggregate.js +2 -2
  61. package/dist/sync/io.d.ts +13 -3
  62. package/dist/sync/io.js +43 -17
  63. package/dist/sync/typescript-recipe.d.ts +30 -0
  64. package/dist/sync/typescript-recipe.js +112 -0
  65. package/package.json +1 -1
@@ -1,8 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RecipeSchema = exports.WorkflowRecipeSchema = exports.WebhookAuthorizationRecipeSchema = exports.EnumerationRecipeSchema = exports.EnumerationValueSchema = exports.SiteRecipeSchema = exports.SiteGroupingSchema = exports.SiteTemplateRecipeSchema = exports.SiteTemplateTaxonomyEntrySchema = exports.SiteTemplateDictionaryEntrySchema = exports.PageDesignRecipeSchema = exports.PartialDesignRecipeSchema = exports.PageRecipeSchema = exports.PageTemplateRecipeSchema = exports.LayoutSchema = exports.ComponentPlacementSchema = exports.ContentItemRecipeSchema = exports.ContentVersionSchema = exports.ContentVariantSchema = exports.ContentTranslationSchema = exports.ContentFieldValueSchema = exports.SectionDefinitionRecipeSchema = exports.DesignParametersTemplateRecipeSchema = exports.ContentTemplateRecipeSchema = exports.RecipeMetaSchema = exports.RecipeMetaTaxSchema = exports.ComponentTemplateRecipeSchema = exports.ComponentSectionRecipeSchema = exports.RecipeDatasourceSchema = exports.RenderingDatasourceLocationSchema = exports.PlaceholderRecipeSchema = exports.PlaceholderDefinitionSchema = exports.RenderingVariantDefinitionSchema = exports.DesignParameterSchema = exports.FieldDefinitionSchema = exports.SitecoreFieldAugmentSchema = void 0;
3
+ exports.RecipeSchema = exports.WorkflowRecipeSchema = exports.WebhookAuthorizationRecipeSchema = exports.EnumerationRecipeSchema = exports.EnumerationValueSchema = exports.SiteRecipeSchema = exports.SiteGroupingSchema = exports.SiteTemplateRecipeSchema = exports.SiteTemplateTaxonomyEntrySchema = exports.SiteTemplateDictionaryEntrySchema = exports.PageDesignRecipeSchema = exports.PartialDesignRecipeSchema = exports.PageRecipeSchema = exports.PageTemplateRecipeSchema = exports.LayoutSchema = exports.ComponentPlacementSchema = exports.ContentItemRecipeSchema = exports.ContentVersionSchema = exports.ContentVariantSchema = exports.ContentTranslationSchema = exports.ContentFieldValueSchema = exports.SectionDefinitionRecipeSchema = exports.DesignParametersTemplateRecipeSchema = exports.ContentTemplateRecipeSchema = exports.RecipeMetaSchema = exports.RecipeMetaTaxSchema = exports.ComponentTemplateRecipeSchema = exports.ComponentSectionRecipeSchema = exports.RecipeDatasourceSchema = exports.RenderingDatasourceLocationSchema = exports.PlaceholderRecipeSchema = exports.PlaceholderDefinitionSchema = exports.RenderingVariantDefinitionSchema = exports.DesignParameterSchema = exports.FieldDefinitionSchema = exports.SitecoreFieldAugmentSchema = exports.SitecoreFieldSourceSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const field_types_1 = require("./field-types");
6
+ /**
7
+ * Multi-segment folder path accepted by `location.folder` /
8
+ * `placeholder.folder`. Two wire shapes:
9
+ *
10
+ * Array form (canonical): `["Theme", "Color"]`
11
+ * Slash-string form (legacy): `"Theme/Color"`
12
+ *
13
+ * Both normalize to `string[]` after parsing. The registry moved its
14
+ * recipe schema to array form because the slash-string was implicit
15
+ * and fragile to author through Agent Studio (no IDE help for the
16
+ * segments inside the string); scai accepts both so old recipes keep
17
+ * working and new ones use the explicit shape. Empty segments
18
+ * (`""` / `"a//b"`) after split + trim are filtered out so callers
19
+ * don't have to remember to clean them.
20
+ *
21
+ * Downstream consumers (compile/enumeration, compile/placeholder,
22
+ * read-current) all see `string[]` and don't need to split anything
23
+ * themselves.
24
+ */
25
+ const FolderPath = zod_1.z
26
+ .union([zod_1.z.string().min(1), zod_1.z.array(zod_1.z.string().min(1)).min(1)])
27
+ .transform((value) => {
28
+ const segments = (Array.isArray(value) ? value : value.split("/")).map((s) => s.trim());
29
+ return segments.filter((s) => s.length > 0);
30
+ })
31
+ .pipe(zod_1.z.array(zod_1.z.string().min(1)).min(1));
6
32
  /**
7
33
  * Recipe author surface — what users hand-author for one Sitecore template.
8
34
  *
@@ -41,48 +67,77 @@ const field_types_1 = require("./field-types");
41
67
  */
42
68
  const HANDLE_PATTERN = /^[a-z][a-z0-9-]*@[0-9]+$/;
43
69
  /**
44
- * Sitecore-side override on a field or param. Defaults apply when omitted.
70
+ * The picker-scope source ("Sitecore's Source field") expressed as a
71
+ * discriminated union over the two real modes:
45
72
  *
46
- * The picker-scope concept (Sitecore's `Source` field) is expressed as
47
- * three composable structured fields rather than a stringly-typed
48
- * mini-language. They combine: e.g. `sourceScope` + `sourceTypes` becomes
49
- * `DataSource=<path>&IncludeTemplatesForSelection={GUID},...` on emit.
73
+ * - `filter` composable structured fields. `types` is a picker
74
+ * filter restricting which recipe-defined templates appear; `query`
75
+ * is a Sitecore Query; `scope` is a fixed content-tree path. They
76
+ * combine, e.g. `scope + types` → `DataSource=<path>&IncludeTemplatesForSelection=...`.
77
+ * - `raw` — verbatim Source string, the escape hatch. Use when the
78
+ * structured surface doesn't fit (e.g. a bare path Treelist source
79
+ * like `/sitecore/content/Tags`).
80
+ *
81
+ * Previously the four fields were peers on `SitecoreFieldAugment` with
82
+ * a `.refine` enforcing the mutex; the union makes the constraint
83
+ * structural so JSON Schema's `oneOf` expresses it natively and Agent
84
+ * Studio can't emit an invalid combination. See
85
+ * `docs/recipe-schema-audit.md` (A1).
86
+ */
87
+ exports.SitecoreFieldSourceSchema = zod_1.z.discriminatedUnion("kind", [
88
+ zod_1.z.object({
89
+ kind: zod_1.z.literal("filter"),
90
+ types: zod_1.z
91
+ .array(zod_1.z.string())
92
+ .min(1)
93
+ .optional()
94
+ .describe("Picker filter: restrict to items conforming to one of these recipe handles. Compiler resolves each handle to its deterministic template GUID and emits `IncludeTemplatesForSelection={GUID},{GUID}`."),
95
+ query: zod_1.z
96
+ .string()
97
+ .optional()
98
+ .describe("Where to look: a Sitecore Query (e.g. `$site/*[@@name='Data']`). Standalone becomes `query:<query>`; combined with `types` becomes `DataSource=query:<query>&IncludeTemplatesForSelection=...`."),
99
+ scope: zod_1.z
100
+ .string()
101
+ .optional()
102
+ .describe("Where to look: a fixed Sitecore content-tree path. Emitted as `DataSource=<path>`, alone or combined with `types`."),
103
+ }),
104
+ zod_1.z.object({
105
+ kind: zod_1.z.literal("raw"),
106
+ value: zod_1.z
107
+ .string()
108
+ .min(1)
109
+ .describe("Verbatim Sitecore Source string. Escape hatch for Source shapes that don't fit the `filter` mode (e.g. a bare path Treelist source like `/sitecore/content/Tags`)."),
110
+ }),
111
+ ]);
112
+ /**
113
+ * Sitecore-side override on a field or param. Defaults apply when
114
+ * omitted.
50
115
  *
51
- * sourceTypes — "picker filter": only items of these recipe handles.
52
- * sourceQuery — "where to look": a Sitecore Query (e.g. `$site/...`).
53
- * sourceScope — "where to look": a fixed content-tree path.
54
- * sourceRaw — escape hatch; verbatim Source string (mutually exclusive
55
- * with the structured fields).
116
+ * `source` carries the picker-scope shape as a discriminated union
117
+ * (`filter` | `raw`) see `SitecoreFieldSourceSchema`. Internal
118
+ * scai code converts to a flat `SourceFields` bag via
119
+ * `augmentSourceToFields()` before passing to `renderSourceFields()`.
120
+ */
121
+ /**
122
+ * Defensive guard: pre-A1 recipes carried `sourceTypes` / `sourceQuery` /
123
+ * `sourceScope` / `sourceRaw` as peer optional fields on the augment.
124
+ * After A1 the picker scope lives inside a discriminated `source`
125
+ * union. Without this guard Zod's default `.strip()` would silently
126
+ * drop the legacy keys and produce a parsed augment with no `source`
127
+ * — losing author intent quietly. Reject explicitly with a migration
128
+ * pointer instead.
56
129
  */
130
+ const LEGACY_SOURCE_KEYS = ["sourceTypes", "sourceQuery", "sourceScope", "sourceRaw"];
57
131
  exports.SitecoreFieldAugmentSchema = zod_1.z
58
132
  .object({
59
133
  /** Override the default shape→Sitecore type mapping. */
60
134
  type: field_types_1.SitecoreFieldTypeSchema.optional(),
61
135
  /**
62
- * Picker filter: restrict to items conforming to one of these recipe
63
- * handles. Compiler resolves each handle to its deterministic template
64
- * GUID and emits `IncludeTemplatesForSelection={GUID},{GUID}`.
136
+ * Picker scope discriminated union of two modes: `filter`
137
+ * (composable `types` / `query` / `scope`) or `raw` (verbatim
138
+ * Source string). See `SitecoreFieldSourceSchema`.
65
139
  */
66
- sourceTypes: zod_1.z.array(zod_1.z.string()).optional(),
67
- /**
68
- * Where to look: a Sitecore Query (e.g. `$site/*[@@name='Data']`).
69
- * Standalone, becomes the entire Source as `query:<query>` (the
70
- * shorthand Sitecore evaluates directly for Droplist-style fields).
71
- * Combined with `sourceTypes`, becomes `DataSource=query:<query>&...`.
72
- */
73
- sourceQuery: zod_1.z.string().optional(),
74
- /**
75
- * Where to look: a fixed Sitecore content-tree path. Emitted as
76
- * `DataSource=<path>`, alone or combined with `sourceTypes`.
77
- */
78
- sourceScope: zod_1.z.string().optional(),
79
- /**
80
- * Escape hatch: verbatim Source string. Mutually exclusive with the
81
- * structured fields above. Use when you need a Source form that
82
- * doesn't fit the structured surface (e.g. a bare path Treelist
83
- * source like `/sitecore/content/Tags`).
84
- */
85
- sourceRaw: zod_1.z.string().optional(),
140
+ source: exports.SitecoreFieldSourceSchema.optional(),
86
141
  /** Author-facing hint surfaced in the CMS. */
87
142
  hint: zod_1.z.string().optional(),
88
143
  /** Required marker (translates to a Sitecore validation rule). */
@@ -126,10 +181,21 @@ exports.SitecoreFieldAugmentSchema = zod_1.z
126
181
  */
127
182
  storage: zod_1.z.enum(["versioned", "unversioned", "shared"]).optional(),
128
183
  })
129
- .refine((v) => v.sourceRaw === undefined ||
130
- (v.sourceTypes === undefined && v.sourceQuery === undefined && v.sourceScope === undefined), {
131
- message: "sourceRaw is mutually exclusive with sourceTypes/sourceQuery/sourceScope",
132
- path: ["sourceRaw"],
184
+ .passthrough()
185
+ .superRefine((augment, ctx) => {
186
+ // Pre-A1 recipes carried sourceTypes/sourceQuery/sourceScope/sourceRaw
187
+ // as peer fields; the new shape is `source: { kind, ... }`. Without
188
+ // `.passthrough()` Zod's default `.strip()` would drop those keys
189
+ // before the refine runs; with passthrough they survive to here and
190
+ // we reject loudly with a migration pointer.
191
+ const legacyPresent = LEGACY_SOURCE_KEYS.filter((key) => augment[key] !== undefined);
192
+ if (legacyPresent.length > 0) {
193
+ ctx.addIssue({
194
+ code: zod_1.z.ZodIssueCode.custom,
195
+ path: [legacyPresent[0]],
196
+ message: `Legacy source field(s) [${legacyPresent.join(", ")}] are no longer accepted on \`sitecore\`. Move them into the new \`source\` discriminated union: \`source: { kind: "filter", types/query/scope }\` or \`source: { kind: "raw", value }\`. See docs/recipe-schema-audit.md (A1).`,
197
+ });
198
+ }
133
199
  });
134
200
  exports.FieldDefinitionSchema = zod_1.z.object({
135
201
  name: zod_1.z.string().min(1),
@@ -209,7 +275,7 @@ exports.PlaceholderDefinitionSchema = zod_1.z.object({
209
275
  * Insert Options. Recipes naming the same folder share it. Omit → the
210
276
  * item lands flat at the root.
211
277
  */
212
- folder: zod_1.z.string().min(1).optional(),
278
+ folder: FolderPath.optional(),
213
279
  /**
214
280
  * SXA dynamic placeholder. When true the host rendering must also set
215
281
  * `dynamicPlaceholders: true` so SXA generates per-instance keys; the
@@ -268,7 +334,7 @@ exports.PlaceholderRecipeSchema = zod_1.z.object({
268
334
  * Settings Folder` template (inheriting its Insert Options). Recipes
269
335
  * naming the same folder share it. Omit → flat at the root.
270
336
  */
271
- folder: zod_1.z.string().min(1).optional(),
337
+ folder: FolderPath.optional(),
272
338
  /** SXA dynamic placeholder — see `PlaceholderDefinitionSchema.dynamic`. */
273
339
  dynamic: zod_1.z.boolean().default(false),
274
340
  /**
@@ -405,7 +471,8 @@ exports.ComponentSectionRecipeSchema = zod_1.z.object({
405
471
  */
406
472
  sortOrder: zod_1.z.number().int().optional(),
407
473
  });
408
- exports.ComponentTemplateRecipeSchema = zod_1.z.object({
474
+ exports.ComponentTemplateRecipeSchema = zod_1.z
475
+ .object({
409
476
  kind: zod_1.z.literal("component-template"),
410
477
  schemaVersion: zod_1.z.literal("1"),
411
478
  /** Stable identifier of the form `<kebab-name>@<major>`, e.g. `cta-button@1`. */
@@ -557,7 +624,14 @@ exports.ComponentTemplateRecipeSchema = zod_1.z.object({
557
624
  * `autoCreate` / `dynamicPlaceholders` — useful for the rare case
558
625
  * where you need to force a specific value.
559
626
  */
560
- otherProperties: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
627
+ otherProperties: zod_1.z
628
+ .record(zod_1.z.string(), zod_1.z.string())
629
+ .optional()
630
+ .describe("Free-form key/value pairs encoded into the rendering's `OtherProperties` URL-encoded shared field. Reserved keys `IsAutoDatasourceRendering` and `IsRenderingsWithDynamicPlaceholders` should normally be set via the typed `datasource.autoCreate` and `dynamicPlaceholders` shortcuts — overriding here silently wins and is intended only for the rare escape-hatch case."),
631
+ })
632
+ .refine((recipe) => !(recipe.parameters !== undefined && recipe.params.length > 0), {
633
+ message: "Set either `parameters` (external template ref) or inline `params`, not both — the compiler ignores `params` when `parameters` is set, which silently drops author intent. Pick one form per recipe.",
634
+ path: ["params"],
561
635
  });
562
636
  /**
563
637
  * A content-only template. Has fields but no rendering — exists as a data
@@ -664,11 +738,21 @@ exports.DesignParametersTemplateRecipeSchema = zod_1.z.object({
664
738
  /** Defaults to "Office/32x32/document.png" if omitted. */
665
739
  icon: zod_1.z.string().optional(),
666
740
  /**
667
- * Section name under which this parameters template lands —
668
- * `Components/<section>/Presentation Parameters/<name>`. Required:
741
+ * Reference to a `ComponentSectionRecipe` whose section folders this
742
+ * parameters template lands under —
743
+ * `Components/<section.name>/Presentation Parameters/<name>`. Required:
669
744
  * presentation parameters are organised per-section by convention.
745
+ *
746
+ * Compile errors INPUT_INVALID if `section.handle` doesn't resolve to
747
+ * a `ComponentSectionRecipe` in the same recipe set. Matches the
748
+ * shape used by `ComponentTemplateRecipe.section` — `{ handle }` ref,
749
+ * not a bare section name string.
670
750
  */
671
- section: zod_1.z.string().min(1),
751
+ section: zod_1.z.object({
752
+ handle: zod_1.z.string().regex(HANDLE_PATTERN, {
753
+ message: "section.handle must match `<kebab-name>@<major>`",
754
+ }),
755
+ }),
672
756
  params: zod_1.z.array(exports.DesignParameterSchema).default([]),
673
757
  });
674
758
  /**
@@ -1486,7 +1570,7 @@ exports.EnumerationRecipeSchema = zod_1.z.object({
1486
1570
  location: zod_1.z
1487
1571
  .object({
1488
1572
  scope: zod_1.z.enum(["site", "siteCollection"]),
1489
- folder: zod_1.z.string().min(1).optional(),
1573
+ folder: FolderPath.optional(),
1490
1574
  })
1491
1575
  .optional(),
1492
1576
  values: zod_1.z.array(exports.EnumerationValueSchema).min(1),
@@ -45,3 +45,23 @@ export declare const renderSourceFields: (fields: SourceFields, resolveHandle: (
45
45
  * `ref-source-fields`.
46
46
  */
47
47
  export declare const sourceFieldsNeedHandleResolution: (fields: SourceFields) => boolean;
48
+ /**
49
+ * Convert the author-surface `SitecoreFieldSource` discriminated union
50
+ * to the flat `SourceFields` bag the renderer + IR consume. Internal
51
+ * adapter — author surface stays union-shaped (clean for JSON Schema
52
+ * and Agent Studio), compiler internals stay flat-shaped (clean for
53
+ * `renderSourceFields` and the `ref-source-fields` IR op).
54
+ *
55
+ * Returns an empty bag when `source` is undefined, so callers can
56
+ * unconditionally invoke this and feed the result to
57
+ * `renderSourceFields` / `sourceFieldsNeedHandleResolution`.
58
+ */
59
+ export declare const augmentSourceToFields: (source: {
60
+ kind: "filter";
61
+ types?: readonly string[];
62
+ query?: string;
63
+ scope?: string;
64
+ } | {
65
+ kind: "raw";
66
+ value: string;
67
+ } | undefined) => SourceFields;
@@ -25,7 +25,7 @@
25
25
  * `/sitecore/content/Tags` Treelist source) use `sourceRaw`.
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.sourceFieldsNeedHandleResolution = exports.renderSourceFields = void 0;
28
+ exports.augmentSourceToFields = exports.sourceFieldsNeedHandleResolution = exports.renderSourceFields = void 0;
29
29
  const formatGuidCurly = (guid) => `{${guid.toUpperCase()}}`;
30
30
  /**
31
31
  * Compose structured source fields into the Sitecore-encoded Source string.
@@ -77,3 +77,27 @@ exports.renderSourceFields = renderSourceFields;
77
77
  */
78
78
  const sourceFieldsNeedHandleResolution = (fields) => Array.isArray(fields.sourceTypes) && fields.sourceTypes.length > 0;
79
79
  exports.sourceFieldsNeedHandleResolution = sourceFieldsNeedHandleResolution;
80
+ /**
81
+ * Convert the author-surface `SitecoreFieldSource` discriminated union
82
+ * to the flat `SourceFields` bag the renderer + IR consume. Internal
83
+ * adapter — author surface stays union-shaped (clean for JSON Schema
84
+ * and Agent Studio), compiler internals stay flat-shaped (clean for
85
+ * `renderSourceFields` and the `ref-source-fields` IR op).
86
+ *
87
+ * Returns an empty bag when `source` is undefined, so callers can
88
+ * unconditionally invoke this and feed the result to
89
+ * `renderSourceFields` / `sourceFieldsNeedHandleResolution`.
90
+ */
91
+ const augmentSourceToFields = (source) => {
92
+ if (!source)
93
+ return {};
94
+ if (source.kind === "raw") {
95
+ return { sourceRaw: source.value };
96
+ }
97
+ return {
98
+ sourceTypes: source.types,
99
+ sourceQuery: source.query,
100
+ sourceScope: source.scope,
101
+ };
102
+ };
103
+ exports.augmentSourceToFields = augmentSourceToFields;
@@ -10,8 +10,8 @@ import type { Recipe } from "./schema/recipe";
10
10
  * Reference inventory checked:
11
11
  *
12
12
  * ComponentTemplateRecipe / ContentTemplateRecipe
13
- * fields[*].sitecore.sourceTypes[*] → any template-bearing recipe
14
- * params[*].sitecore.sourceTypes[*] → any template-bearing recipe
13
+ * fields[*].sitecore.source.types[*] → any template-bearing recipe
14
+ * params[*].sitecore.source.types[*] → any template-bearing recipe
15
15
  * insertOptions[*] → any template-bearing recipe
16
16
  *
17
17
  * ComponentTemplateRecipe
@@ -23,7 +23,7 @@ import type { Recipe } from "./schema/recipe";
23
23
  * fields[*].reference.refs[*] → any recipe
24
24
  *
25
25
  * PageTemplateRecipe
26
- * fields[*].sitecore.sourceTypes[*] → any template-bearing recipe
26
+ * fields[*].sitecore.source.types[*] → any template-bearing recipe
27
27
  * insertOptions[*] → PageTemplateRecipe
28
28
  * layout.placeholders[*][*].componentHandle → ComponentTemplateRecipe
29
29
  * layout.placeholders[*][*].datasourceRef.handle → ContentItemRecipe
@@ -5,6 +5,16 @@ exports.formatValidationErrors = formatValidationErrors;
5
5
  exports.validateRecipeSet = validateRecipeSet;
6
6
  exports.validateRecipeSetOrThrow = validateRecipeSetOrThrow;
7
7
  const errors_1 = require("../shared/errors");
8
+ /**
9
+ * The picker-scope handles to validate on a `SitecoreFieldAugment`.
10
+ * Empty when the augment is absent OR uses the `raw` source mode
11
+ * (verbatim Source string; nothing to resolve).
12
+ */
13
+ const sourceTypesOf = (augment) => {
14
+ if (augment?.source?.kind !== "filter")
15
+ return [];
16
+ return augment.source.types ?? [];
17
+ };
8
18
  const TEMPLATE_KINDS = [
9
19
  "component-template",
10
20
  "content-template",
@@ -184,13 +194,13 @@ function validateRecipeSet(recipes) {
184
194
  switch (recipe.kind) {
185
195
  case "component-template":
186
196
  recipe.fields.forEach((field, idx) => {
187
- field.sitecore?.sourceTypes?.forEach((handle, sIdx) => {
188
- checkRef(recipe.handle, `fields.${idx}.sitecore.sourceTypes.${sIdx}`, handle, TEMPLATE_KINDS);
197
+ sourceTypesOf(field.sitecore).forEach((handle, sIdx) => {
198
+ checkRef(recipe.handle, `fields.${idx}.sitecore.source.types.${sIdx}`, handle, TEMPLATE_KINDS);
189
199
  });
190
200
  });
191
201
  recipe.params.forEach((param, idx) => {
192
- param.sitecore?.sourceTypes?.forEach((handle, sIdx) => {
193
- checkRef(recipe.handle, `params.${idx}.sitecore.sourceTypes.${sIdx}`, handle, TEMPLATE_KINDS);
202
+ sourceTypesOf(param.sitecore).forEach((handle, sIdx) => {
203
+ checkRef(recipe.handle, `params.${idx}.sitecore.source.types.${sIdx}`, handle, TEMPLATE_KINDS);
194
204
  });
195
205
  });
196
206
  recipe.insertOptions?.forEach((handle, idx) => {
@@ -216,8 +226,8 @@ function validateRecipeSet(recipes) {
216
226
  break;
217
227
  case "design-parameters-template":
218
228
  recipe.params.forEach((param, idx) => {
219
- param.sitecore?.sourceTypes?.forEach((handle, sIdx) => {
220
- checkRef(recipe.handle, `params.${idx}.sitecore.sourceTypes.${sIdx}`, handle, TEMPLATE_KINDS);
229
+ sourceTypesOf(param.sitecore).forEach((handle, sIdx) => {
230
+ checkRef(recipe.handle, `params.${idx}.sitecore.source.types.${sIdx}`, handle, TEMPLATE_KINDS);
221
231
  });
222
232
  });
223
233
  break;
@@ -227,8 +237,8 @@ function validateRecipeSet(recipes) {
227
237
  break;
228
238
  case "content-template":
229
239
  recipe.fields.forEach((field, idx) => {
230
- field.sitecore?.sourceTypes?.forEach((handle, sIdx) => {
231
- checkRef(recipe.handle, `fields.${idx}.sitecore.sourceTypes.${sIdx}`, handle, TEMPLATE_KINDS);
240
+ sourceTypesOf(field.sitecore).forEach((handle, sIdx) => {
241
+ checkRef(recipe.handle, `fields.${idx}.sitecore.source.types.${sIdx}`, handle, TEMPLATE_KINDS);
232
242
  });
233
243
  });
234
244
  recipe.insertOptions?.forEach((handle, idx) => {
@@ -280,8 +290,8 @@ function validateRecipeSet(recipes) {
280
290
  break;
281
291
  case "page-template":
282
292
  (recipe.fields ?? []).forEach((field, idx) => {
283
- field.sitecore?.sourceTypes?.forEach((handle, sIdx) => {
284
- checkRef(recipe.handle, `fields.${idx}.sitecore.sourceTypes.${sIdx}`, handle, TEMPLATE_KINDS);
293
+ sourceTypesOf(field.sitecore).forEach((handle, sIdx) => {
294
+ checkRef(recipe.handle, `fields.${idx}.sitecore.source.types.${sIdx}`, handle, TEMPLATE_KINDS);
285
295
  });
286
296
  });
287
297
  recipe.insertOptions?.forEach((handle, idx) => {
@@ -29,4 +29,5 @@ const recipe_2 = require("../brief/recipe");
29
29
  exports.ENUMERABLE_RECIPE_KINDS = [
30
30
  recipe_1.brandKitKind,
31
31
  recipe_2.briefTypeKind,
32
+ recipe_2.briefInstanceKind,
32
33
  ];
@@ -102,7 +102,7 @@ const aggregateStatus = async (kinds, ctx, options = {}) => {
102
102
  }
103
103
  const items = [];
104
104
  for (const file of files) {
105
- const recipe = (0, io_1.loadRecipe)(file, kind.schema);
105
+ const recipe = await (0, io_1.loadRecipe)(file, kind.schema);
106
106
  const id = recipeId(recipe);
107
107
  try {
108
108
  const plan = await (0, engine_1.syncDiff)(kind, recipe, { kind: kind.name, id }, ctx);
@@ -139,7 +139,7 @@ const aggregatePush = async (kinds, ctx, options) => {
139
139
  }
140
140
  const items = [];
141
141
  for (const file of files) {
142
- const recipe = (0, io_1.loadRecipe)(file, kind.schema);
142
+ const recipe = await (0, io_1.loadRecipe)(file, kind.schema);
143
143
  const id = recipeId(recipe);
144
144
  try {
145
145
  const outcome = await (0, engine_1.syncPush)(kind, recipe, { kind: kind.name, id }, ctx, {
package/dist/sync/io.d.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  import type { ZodType } from "zod";
2
2
  /**
3
- * Read, parse, and schema-validate a recipe file. YAML is a superset of
4
- * JSON, so `.yaml`, `.yml`, and `.json` all parse through the same path.
3
+ * Read, parse, and schema-validate a recipe file. The on-disk format is
4
+ * picked from the file extension:
5
+ *
6
+ * - `.ts` / `.tsx` / `.mts` / `.cts` → transpiled + executed in the
7
+ * recipe sandbox; the default export (or first named export) is
8
+ * Zod-parsed.
9
+ * - everything else → read as text and run through the YAML parser
10
+ * (which also accepts JSON).
11
+ *
12
+ * Async because the TypeScript path forks a child process. All current
13
+ * call sites already run inside `async` task runners or commander
14
+ * `command.action(async …)` handlers.
5
15
  */
6
- export declare const loadRecipe: <T>(filePath: string, schema: ZodType<T>) => T;
16
+ export declare const loadRecipe: <T>(filePath: string, schema: ZodType<T>) => Promise<T>;
7
17
  /** Serialize a recipe to a file — JSON when the path ends `.json`, else YAML. */
8
18
  export declare const writeRecipe: (filePath: string, recipe: unknown) => void;
package/dist/sync/io.js CHANGED
@@ -2,21 +2,56 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeRecipe = exports.loadRecipe = void 0;
4
4
  /**
5
- * Recipe file I/O for the `sync` engine — load a recipe from a YAML or
6
- * JSON file and validate it against a kind's schema, or serialize a
7
- * captured recipe back to disk.
5
+ * Recipe file I/O for the `sync` engine — load a recipe from a YAML,
6
+ * JSON, or TypeScript file and validate it against a kind's schema, or
7
+ * serialize a captured recipe back to disk.
8
8
  *
9
- * See docs/recipe-sync-architecture.md.
9
+ * Three on-disk formats are supported, all kinds, one loader:
10
+ *
11
+ * - `.ts` / `.tsx` / `.mts` / `.cts` — TypeScript source. Loaded via
12
+ * the sandboxed transpile path (`@/sync/typescript-recipe`) so a
13
+ * hostile authored recipe cannot run with scai's privileges.
14
+ * Authors get Zod-derived `satisfies` checks at write time.
15
+ * - `.yaml` / `.yml` — YAML.
16
+ * - `.json` — JSON (parsed through the YAML parser; YAML is a superset).
17
+ *
18
+ * See docs/recipe-sync-architecture.md and docs/recipe-sandbox.md.
10
19
  */
11
20
  const node_fs_1 = require("node:fs");
12
21
  const node_path_1 = require("node:path");
13
22
  const yaml_1 = require("yaml");
14
23
  const errors_1 = require("../shared/errors");
24
+ const typescript_recipe_1 = require("./typescript-recipe");
15
25
  /**
16
- * Read, parse, and schema-validate a recipe file. YAML is a superset of
17
- * JSON, so `.yaml`, `.yml`, and `.json` all parse through the same path.
26
+ * Read, parse, and schema-validate a recipe file. The on-disk format is
27
+ * picked from the file extension:
28
+ *
29
+ * - `.ts` / `.tsx` / `.mts` / `.cts` → transpiled + executed in the
30
+ * recipe sandbox; the default export (or first named export) is
31
+ * Zod-parsed.
32
+ * - everything else → read as text and run through the YAML parser
33
+ * (which also accepts JSON).
34
+ *
35
+ * Async because the TypeScript path forks a child process. All current
36
+ * call sites already run inside `async` task runners or commander
37
+ * `command.action(async …)` handlers.
18
38
  */
19
- const loadRecipe = (filePath, schema) => {
39
+ const loadRecipe = async (filePath, schema) => {
40
+ const parsed = (0, typescript_recipe_1.isTypeScriptRecipePath)(filePath)
41
+ ? await (0, typescript_recipe_1.loadTypeScriptRecipe)(filePath)
42
+ : parseYamlOrJsonRecipe(filePath);
43
+ const result = schema.safeParse(parsed);
44
+ if (!result.success) {
45
+ throw (0, errors_1.createScaiError)(`Recipe file "${filePath}" failed schema validation`, "INPUT_INVALID", {
46
+ details: result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`),
47
+ });
48
+ }
49
+ return result.data;
50
+ };
51
+ exports.loadRecipe = loadRecipe;
52
+ /** Read and YAML-parse a recipe file. YAML is a superset of JSON, so
53
+ * `.yaml`, `.yml`, and `.json` all go through the same path. */
54
+ const parseYamlOrJsonRecipe = (filePath) => {
20
55
  let raw;
21
56
  try {
22
57
  raw = (0, node_fs_1.readFileSync)(filePath, "utf8");
@@ -26,24 +61,15 @@ const loadRecipe = (filePath, schema) => {
26
61
  hint: error instanceof Error ? error.message : undefined,
27
62
  });
28
63
  }
29
- let parsed;
30
64
  try {
31
- parsed = (0, yaml_1.parse)(raw);
65
+ return (0, yaml_1.parse)(raw);
32
66
  }
33
67
  catch (error) {
34
68
  throw (0, errors_1.createScaiError)(`Recipe file "${filePath}" is not valid YAML/JSON`, "INPUT_INVALID", {
35
69
  hint: error instanceof Error ? error.message : undefined,
36
70
  });
37
71
  }
38
- const result = schema.safeParse(parsed);
39
- if (!result.success) {
40
- throw (0, errors_1.createScaiError)(`Recipe file "${filePath}" failed schema validation`, "INPUT_INVALID", {
41
- details: result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`),
42
- });
43
- }
44
- return result.data;
45
72
  };
46
- exports.loadRecipe = loadRecipe;
47
73
  /** Serialize a recipe to a file — JSON when the path ends `.json`, else YAML. */
48
74
  const writeRecipe = (filePath, recipe) => {
49
75
  const serialized = (0, node_path_1.extname)(filePath) === ".json" ? `${JSON.stringify(recipe, null, 2)}\n` : (0, yaml_1.stringify)(recipe);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared `.recipe.ts` loader.
3
+ *
4
+ * Authored recipes (CMS, brand-kit, agents, campaigns, briefs, etc.) all
5
+ * load through this path so end users can write `.recipe.ts` files with
6
+ * Zod-derived types and `satisfies` checks. The transpile + sandboxed
7
+ * execute machinery is owned by `@/recipe/sandbox` — this module is the
8
+ * thin wrapper that picks between the confined child and the legacy
9
+ * in-process tsx loader, and is consumed by both:
10
+ *
11
+ * - `src/sync/io.ts:loadRecipe` (schema-aware loader used by brand,
12
+ * agents, campaigns, briefs, …)
13
+ * - `src/recipe/io.ts:loadRecipe` (CMS recipe discriminated-union loader)
14
+ *
15
+ * Output is unvalidated — callers Zod-parse the returned value. See
16
+ * docs/recipe-sandbox.md.
17
+ */
18
+ /** Whether the given file path should be loaded via the TypeScript path. */
19
+ export declare const isTypeScriptRecipePath: (filePath: string) => boolean;
20
+ /**
21
+ * Load a `.recipe.ts` (or `.tsx`/`.mts`/`.cts`) file and return its
22
+ * exported recipe value. By default the file is loaded in a confined
23
+ * child process (see docs/recipe-sandbox.md) so a hostile recipe cannot
24
+ * run with scai's full privileges. `SITECOREAI_RECIPE_SANDBOX=0` forces
25
+ * the legacy in-process loader (useful for debugging — the warning lands
26
+ * on stderr so JSON-mode CLI streams are not corrupted).
27
+ *
28
+ * The result is unvalidated: callers Zod-parse it.
29
+ */
30
+ export declare const loadTypeScriptRecipe: (filePath: string) => Promise<unknown>;