@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.
- package/dist/agents/tasks/agent.js +2 -2
- package/dist/agents/tasks/resources.js +2 -2
- package/dist/agents/tasks/space.js +1 -1
- package/dist/brand/api/auth.d.ts +16 -9
- package/dist/brand/api/auth.js +29 -19
- package/dist/brand/credential.d.ts +71 -1
- package/dist/brand/credential.js +119 -2
- package/dist/brand/recipe/diff.js +7 -2
- package/dist/brand/recipe/kind.js +0 -0
- package/dist/brand/recipe/schema.d.ts +113 -7
- package/dist/brand/recipe/schema.js +137 -8
- package/dist/brand/seed.d.ts +9 -5
- package/dist/brand/seed.js +30 -5
- package/dist/brief/recipe/schema.js +4 -1
- package/dist/campaigns/recipe/schema.d.ts +39 -8
- package/dist/campaigns/recipe/schema.js +40 -10
- package/dist/commands/agents/sync.js +2 -2
- package/dist/commands/brand/seed.js +1 -1
- package/dist/commands/brand/sync.js +2 -2
- package/dist/commands/brief/sync.js +2 -2
- package/dist/commands/campaign/sync.js +2 -2
- package/dist/recipe/compile/design-parameters-template.js +5 -3
- package/dist/recipe/compile/enumeration.js +6 -11
- package/dist/recipe/compile/shared.js +12 -12
- package/dist/recipe/compile.js +4 -4
- package/dist/recipe/io.d.ts +8 -3
- package/dist/recipe/io.js +11 -81
- package/dist/recipe/items/read-current.js +31 -24
- package/dist/recipe/schema/recipe.d.ts +167 -84
- package/dist/recipe/schema/recipe.js +130 -46
- package/dist/recipe/schema/source-fields.d.ts +20 -0
- package/dist/recipe/schema/source-fields.js +25 -1
- package/dist/recipe/validate.d.ts +3 -3
- package/dist/recipe/validate.js +20 -10
- package/dist/sync/aggregate.js +2 -2
- package/dist/sync/io.d.ts +13 -3
- package/dist/sync/io.js +43 -17
- package/dist/sync/typescript-recipe.d.ts +30 -0
- package/dist/sync/typescript-recipe.js +112 -0
- package/package.json +1 -1
package/dist/recipe/io.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
9
|
-
* named export (the first
|
|
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
|
|
50
|
-
*
|
|
51
|
-
* named export (the first
|
|
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
|
|
59
|
-
|
|
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.
|
|
241
|
-
* `
|
|
242
|
-
* NOT reverse-engineered (it would require parsing the
|
|
243
|
-
* and resolving GUIDs back to handles);
|
|
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:
|
|
263
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
694
|
-
await walk(child, nextFolder);
|
|
699
|
+
await walk(child, [...folderSegments, child.name]);
|
|
695
700
|
}
|
|
696
701
|
else {
|
|
697
|
-
const recipe = await enumerationFromItem(child,
|
|
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,
|
|
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,
|
|
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 (
|
|
1060
|
-
recipe.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
|
-
|
|
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,
|
|
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
|
|
1187
|
-
|
|
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,
|
|
1198
|
+
await visit(root, []);
|
|
1192
1199
|
return recipes;
|
|
1193
1200
|
};
|
|
1194
1201
|
/**
|