@sitecoreai-labs/sitecoreai-cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) 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/recipe/schema.js +4 -1
  15. package/dist/campaigns/recipe/schema.d.ts +39 -8
  16. package/dist/campaigns/recipe/schema.js +40 -10
  17. package/dist/commands/agents/sync.js +2 -2
  18. package/dist/commands/brand/seed.js +1 -1
  19. package/dist/commands/brand/sync.js +2 -2
  20. package/dist/commands/brief/sync.js +2 -2
  21. package/dist/commands/campaign/sync.js +2 -2
  22. package/dist/recipe/compile/design-parameters-template.js +5 -3
  23. package/dist/recipe/compile/enumeration.js +6 -11
  24. package/dist/recipe/compile/shared.js +12 -12
  25. package/dist/recipe/compile.js +4 -4
  26. package/dist/recipe/io.d.ts +8 -3
  27. package/dist/recipe/io.js +11 -81
  28. package/dist/recipe/items/read-current.js +31 -24
  29. package/dist/recipe/schema/recipe.d.ts +167 -84
  30. package/dist/recipe/schema/recipe.js +130 -46
  31. package/dist/recipe/schema/source-fields.d.ts +20 -0
  32. package/dist/recipe/schema/source-fields.js +25 -1
  33. package/dist/recipe/validate.d.ts +3 -3
  34. package/dist/recipe/validate.js +20 -10
  35. package/dist/sync/aggregate.js +2 -2
  36. package/dist/sync/io.d.ts +13 -3
  37. package/dist/sync/io.js +43 -17
  38. package/dist/sync/typescript-recipe.d.ts +30 -0
  39. package/dist/sync/typescript-recipe.js +112 -0
  40. package/package.json +1 -1
@@ -4,11 +4,16 @@ import type { Plan } from "./runtime/plan";
4
4
  /**
5
5
  * Load a recipe file. Supports both shapes:
6
6
  *
7
- * - `.recipe.ts` — TypeScript source compiled at runtime via `tsx/cjs/api`.
8
- * Recipes export the recipe object as either the default export or a
9
- * named export (the first one found wins).
7
+ * - `.recipe.ts` (or `.tsx`/`.mts`/`.cts`) — TypeScript source compiled
8
+ * and executed in the recipe sandbox. Recipes export the recipe
9
+ * object as either the default export or a named export (the first
10
+ * one found wins).
10
11
  * - `.recipe.json` — pre-serialized recipe JSON.
11
12
  *
13
+ * The TypeScript path is shared with the schema-aware loader at
14
+ * `@/sync/typescript-recipe` so brand-kit, agent, campaign, and brief
15
+ * recipes can also be authored as `.recipe.ts`.
16
+ *
12
17
  * Validates against `RecipeSchema` (the `Recipe = ComponentTemplateRecipe |
13
18
  * ContentTemplateRecipe` discriminated union) so both kinds parse uniformly.
14
19
  */
package/dist/recipe/io.js CHANGED
@@ -7,9 +7,9 @@ exports.defaultPlanPath = exports.defaultIrPath = exports.writePlan = exports.lo
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const errors_1 = require("../shared/errors");
10
+ const typescript_recipe_1 = require("../sync/typescript-recipe");
10
11
  const recipe_1 = require("./schema/recipe");
11
12
  const operations_1 = require("./ir/operations");
12
- const load_1 = require("./sandbox/load");
13
13
  /**
14
14
  * Recipe + IR + Plan I/O.
15
15
  *
@@ -46,18 +46,22 @@ const writeJson = async (filePath, value) => {
46
46
  /**
47
47
  * Load a recipe file. Supports both shapes:
48
48
  *
49
- * - `.recipe.ts` — TypeScript source compiled at runtime via `tsx/cjs/api`.
50
- * Recipes export the recipe object as either the default export or a
51
- * named export (the first one found wins).
49
+ * - `.recipe.ts` (or `.tsx`/`.mts`/`.cts`) — TypeScript source compiled
50
+ * and executed in the recipe sandbox. Recipes export the recipe
51
+ * object as either the default export or a named export (the first
52
+ * one found wins).
52
53
  * - `.recipe.json` — pre-serialized recipe JSON.
53
54
  *
55
+ * The TypeScript path is shared with the schema-aware loader at
56
+ * `@/sync/typescript-recipe` so brand-kit, agent, campaign, and brief
57
+ * recipes can also be authored as `.recipe.ts`.
58
+ *
54
59
  * Validates against `RecipeSchema` (the `Recipe = ComponentTemplateRecipe |
55
60
  * ContentTemplateRecipe` discriminated union) so both kinds parse uniformly.
56
61
  */
57
62
  const loadRecipe = async (filePath) => {
58
- const ext = node_path_1.default.extname(filePath).toLowerCase();
59
- const raw = ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts"
60
- ? await loadRecipeFromTypeScript(filePath)
63
+ const raw = (0, typescript_recipe_1.isTypeScriptRecipePath)(filePath)
64
+ ? await (0, typescript_recipe_1.loadTypeScriptRecipe)(filePath)
61
65
  : await readJson(filePath);
62
66
  const result = recipe_1.RecipeSchema.safeParse(raw);
63
67
  if (!result.success) {
@@ -69,80 +73,6 @@ const loadRecipe = async (filePath) => {
69
73
  return result.data;
70
74
  };
71
75
  exports.loadRecipe = loadRecipe;
72
- /**
73
- * Lazily install tsx's CommonJS require hook *once* per process. Earlier
74
- * versions of this file called `register()` and the matching `unregister`
75
- * cleanup on every `loadRecipe` call — fine for one-shot CLI runs but
76
- * wasteful in long-running surfaces like `scai cli shell` where N recipe
77
- * loads = N register/unregister cycles. Once installed, the hook stays
78
- * for the rest of the process lifetime; recipe changes mid-shell-session
79
- * therefore require an exit (the per-recipe `require.cache` clear below
80
- * handles re-imports of the *same* file path within the same process).
81
- */
82
- let tsxRegistered = false;
83
- const ensureTsxRegistered = () => {
84
- if (tsxRegistered) {
85
- return;
86
- }
87
- /* eslint-disable @typescript-eslint/no-require-imports */
88
- const tsxApi = require("tsx/cjs/api");
89
- /* eslint-enable @typescript-eslint/no-require-imports */
90
- tsxApi.register();
91
- tsxRegistered = true;
92
- };
93
- let sandboxOptOutWarned = false;
94
- const warnSandboxDisabled = () => {
95
- if (sandboxOptOutWarned) {
96
- return;
97
- }
98
- sandboxOptOutWarned = true;
99
- process.stderr.write("scai: SITECOREAI_RECIPE_SANDBOX=0 — loading .recipe.ts in-process; " +
100
- "a hostile recipe runs with scai's full privileges.\n");
101
- };
102
- /**
103
- * Load a `.recipe.ts` and return its exported recipe object. By default the
104
- * file is loaded in a confined child process (see docs/recipe-sandbox.md) so
105
- * a hostile recipe a weaponized config could point at cannot run with scai's
106
- * privileges. `SITECOREAI_RECIPE_SANDBOX=0` forces the legacy in-process load.
107
- */
108
- const loadRecipeFromTypeScript = async (filePath) => {
109
- if ((0, load_1.isRecipeSandboxEnabled)()) {
110
- return (0, load_1.loadRecipeInSandbox)(filePath);
111
- }
112
- warnSandboxDisabled();
113
- return loadRecipeInProcess(filePath);
114
- };
115
- const loadRecipeInProcess = async (filePath) => {
116
- const absolute = node_path_1.default.resolve(filePath);
117
- try {
118
- ensureTsxRegistered();
119
- if (require.cache[absolute]) {
120
- delete require.cache[absolute];
121
- }
122
- /* eslint-disable @typescript-eslint/no-require-imports */
123
- const mod = require(absolute);
124
- /* eslint-enable @typescript-eslint/no-require-imports */
125
- if (mod.default !== undefined) {
126
- return mod.default;
127
- }
128
- // Fall back to the first non-default exported value.
129
- for (const key of Object.keys(mod)) {
130
- if (key !== "default" && mod[key] !== undefined) {
131
- return mod[key];
132
- }
133
- }
134
- throw (0, errors_1.createScaiError)(`Recipe at ${filePath} has no exports.`, "INPUT_INVALID", {
135
- hint: "Add `export default <recipe>` (or any named export) to the file.",
136
- });
137
- }
138
- catch (error) {
139
- // Don't double-wrap our own CliErrors (e.g. "Recipe at <path> has no exports.").
140
- if (error instanceof errors_1.ScaiError) {
141
- throw error;
142
- }
143
- throw (0, errors_1.createScaiError)(`Failed to load recipe TypeScript file ${filePath}: ${error instanceof Error ? error.message : String(error)}`, "INPUT_INVALID", { hint: "Confirm the file compiles standalone (try `pnpm exec tsx <file>`)." });
144
- }
145
- };
146
76
  const writeIr = async (filePath, ir) => {
147
77
  await writeJson(filePath, ir);
148
78
  };
@@ -237,11 +237,11 @@ const shapeFromSitecoreType = (type) => {
237
237
  * `sitecore.type`), the section it lives under (`sitecore.section`),
238
238
  * `sitecore.sortOrder`, and the storage axis (`sitecore.storage`, recovered
239
239
  * from the field's `Shared` / `Unversioned` flags). The `Source` value is
240
- * preserved verbatim via `sitecore.sourceRaw` — the structured
241
- * `sourceTypes`/`sourceQuery`/`sourceScope` decomposition is intentionally
242
- * NOT reverse-engineered (it would require parsing the URL-encoded Source
243
- * and resolving GUIDs back to handles); `sourceRaw` round-trips to the
244
- * identical wire string.
240
+ * preserved verbatim via `sitecore.source = { kind: "raw", value }` —
241
+ * the structured `filter` decomposition (`types`/`query`/`scope`) is
242
+ * intentionally NOT reverse-engineered (it would require parsing the
243
+ * URL-encoded Source and resolving GUIDs back to recipe handles);
244
+ * `kind: "raw"` round-trips to the identical wire string.
245
245
  *
246
246
  * LOSSY / omitted: `required`, `hint`, `default`, `enumHandle`, and the
247
247
  * abstract `multiple` flag are not recoverable from a field item alone and
@@ -259,8 +259,9 @@ const fieldFromItem = (fieldItem, sectionName) => {
259
259
  augment.type = sitecoreType;
260
260
  const source = fieldValue(fieldItem, sitecore_templates_1.TEMPLATE_FIELD_FIELDS.SOURCE, "Source");
261
261
  if (source !== undefined && source !== "") {
262
- // Verbatim round-trip: sourceRaw re-emits the identical Source string.
263
- augment.sourceRaw = source;
262
+ // Verbatim round-trip: `source: { kind: "raw", value }` re-emits
263
+ // the identical Source string at compile time.
264
+ augment.source = { kind: "raw", value: source };
264
265
  }
265
266
  // Field storage axis — `Shared` / `Unversioned` are shared flags on the
266
267
  // field item. `versioned` is the Sitecore default; omit it rather than
@@ -473,7 +474,7 @@ const componentSectionFromItem = (folderItem) => {
473
474
  * schema requires `values.min(1)`) — such a container is skipped by the
474
475
  * orchestrator with no error.
475
476
  */
476
- const enumerationFromItem = async (containerItem, folder, client) => {
477
+ const enumerationFromItem = async (containerItem, folderSegments, client) => {
477
478
  const valueItems = (await client.getChildren({ itemId: containerItem.itemId }))
478
479
  .filter((child) => child.name !== "__Standard Values")
479
480
  .sort(byTreeOrder);
@@ -507,8 +508,9 @@ const enumerationFromItem = async (containerItem, folder, client) => {
507
508
  }
508
509
  if (description !== undefined && description !== "")
509
510
  recipe.description = description;
510
- if (folder)
511
- recipe.location = { scope: "site", folder };
511
+ if (folderSegments.length > 0) {
512
+ recipe.location = { scope: "site", folder: folderSegments };
513
+ }
512
514
  // Only carry `default` when it names a real value — the compiler rejects
513
515
  // an out-of-range default, and a stale container `Value` is not intent.
514
516
  if (defaultValue !== undefined &&
@@ -666,7 +668,11 @@ const walkEnumerationsTree = async (rootPath, client) => {
666
668
  * a container. `Enumerations Folder` items are recursed; everything else
667
669
  * with ≥1 child is treated as a container.
668
670
  */
669
- const walk = async (parent, folderPath) => {
671
+ // `folderSegments` carries the grouping-folder path as `string[]` —
672
+ // matches the canonical array shape on `EnumerationRecipe.location.folder`
673
+ // so the reverse-projected recipe emits the same wire shape authors hand
674
+ // to scai (no slash-joined fallback).
675
+ const walk = async (parent, folderSegments) => {
670
676
  const children = (await client.getChildren({ itemId: parent.itemId }))
671
677
  .filter((c) => c.name !== "__Standard Values")
672
678
  .sort(byTreeOrder);
@@ -690,17 +696,16 @@ const walkEnumerationsTree = async (rootPath, client) => {
690
696
  }
691
697
  }
692
698
  if (groupsContainers) {
693
- const nextFolder = folderPath ? `${folderPath}/${child.name}` : child.name;
694
- await walk(child, nextFolder);
699
+ await walk(child, [...folderSegments, child.name]);
695
700
  }
696
701
  else {
697
- const recipe = await enumerationFromItem(child, folderPath, client);
702
+ const recipe = await enumerationFromItem(child, folderSegments, client);
698
703
  if (recipe)
699
704
  recipes.push(recipe);
700
705
  }
701
706
  }
702
707
  };
703
- await walk(root, undefined);
708
+ await walk(root, []);
704
709
  return recipes;
705
710
  };
706
711
  /**
@@ -1022,7 +1027,7 @@ const pageFromItem = (item, templateHandle, guidIndex) => {
1022
1027
  * `PlaceholderRecipe` REQUIRES a non-empty `key`, and a key-less
1023
1028
  * Placeholder Settings item is not reverse-projectable.
1024
1029
  */
1025
- const placeholderFromItem = (item, folder, guidIndex) => {
1030
+ const placeholderFromItem = (item, folderSegments, guidIndex) => {
1026
1031
  const key = fieldValue(item, sitecore_templates_1.PLACEHOLDER_FIELDS.PLACEHOLDER_KEY, "Placeholder Key");
1027
1032
  if (key === undefined || key.trim() === "") {
1028
1033
  // No Placeholder Key — schema requires `key.min(1)`. Skip.
@@ -1056,8 +1061,8 @@ const placeholderFromItem = (item, folder, guidIndex) => {
1056
1061
  recipe.description = description;
1057
1062
  if (icon !== undefined && icon !== "")
1058
1063
  recipe.icon = icon;
1059
- if (folder)
1060
- recipe.folder = folder;
1064
+ if (folderSegments.length > 0)
1065
+ recipe.folder = folderSegments;
1061
1066
  return recipe;
1062
1067
  };
1063
1068
  /**
@@ -1171,24 +1176,26 @@ const walkPlaceholderSettingsTree = async (rootPath, client, guidIndex) => {
1171
1176
  const root = rootPath ? await client.getItem({ path: rootPath }) : null;
1172
1177
  if (!root)
1173
1178
  return recipes;
1174
- const visit = async (parent, folderPath) => {
1179
+ // `folderSegments` carries the grouping-folder path as `string[]` so
1180
+ // reverse-projected placeholder recipes emit the canonical array
1181
+ // shape that schemas/recipe.ts's `FolderPath` accepts.
1182
+ const visit = async (parent, folderSegments) => {
1175
1183
  const children = (await client.getChildren({ itemId: parent.itemId }))
1176
1184
  .filter((c) => c.name !== "__Standard Values")
1177
1185
  .sort(byTreeOrder);
1178
1186
  for (const child of children) {
1179
1187
  if (conformsTo(child, sitecore_templates_1.PLACEHOLDER_TEMPLATE_ID)) {
1180
- const recipe = placeholderFromItem(child, folderPath, guidIndex);
1188
+ const recipe = placeholderFromItem(child, folderSegments, guidIndex);
1181
1189
  if (recipe)
1182
1190
  recipes.push(recipe);
1183
1191
  continue;
1184
1192
  }
1185
1193
  // Anything that isn't a Placeholder leaf is a grouping folder —
1186
- // descend, extending the cumulative folder path.
1187
- const nextFolder = folderPath ? `${folderPath}/${child.name}` : child.name;
1188
- await visit(child, nextFolder);
1194
+ // descend, extending the cumulative segment list.
1195
+ await visit(child, [...folderSegments, child.name]);
1189
1196
  }
1190
1197
  };
1191
- await visit(root, undefined);
1198
+ await visit(root, []);
1192
1199
  return recipes;
1193
1200
  };
1194
1201
  /**