@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.
- 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/api/briefs.d.ts +8 -0
- package/dist/brief/api/briefs.js +49 -11
- package/dist/brief/index.d.ts +1 -1
- package/dist/brief/index.js +2 -1
- package/dist/brief/recipe/index.d.ts +11 -2
- package/dist/brief/recipe/index.js +17 -3
- package/dist/brief/recipe/instance-diff.d.ts +4 -0
- package/dist/brief/recipe/instance-diff.js +77 -0
- package/dist/brief/recipe/instance-kind.d.ts +4 -0
- package/dist/brief/recipe/instance-kind.js +190 -0
- package/dist/brief/recipe/instance-schema.d.ts +61 -0
- package/dist/brief/recipe/instance-schema.js +68 -0
- package/dist/brief/recipe/schema.js +4 -1
- package/dist/brief/tasks/index.d.ts +35 -0
- package/dist/brief/tasks/index.js +62 -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/create.d.ts +2 -0
- package/dist/commands/brief/create.js +56 -0
- package/dist/commands/brief/index.d.ts +3 -1
- package/dist/commands/brief/index.js +11 -1
- package/dist/commands/brief/sync.d.ts +9 -6
- package/dist/commands/brief/sync.js +54 -22
- package/dist/commands/brief/update.d.ts +2 -0
- package/dist/commands/brief/update.js +84 -0
- package/dist/commands/campaign/sync.js +2 -2
- package/dist/mcp/descriptions.js +3 -3
- package/dist/mcp/tools/brief-recipe.js +67 -23
- package/dist/mcp/tools/brief.js +83 -6
- 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-kinds.js +1 -0
- 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/mcp/tools/brief.js
CHANGED
|
@@ -186,21 +186,21 @@ const registerBriefTools = (registry) => {
|
|
|
186
186
|
.describe("Which Brief resource the verb targets. 'brief-type' supports create/update/delete; 'brief' supports set-status/delete; 'comment' supports create."),
|
|
187
187
|
verb: zod_1.z
|
|
188
188
|
.enum(["create", "update", "delete", "set-status"])
|
|
189
|
-
.describe("Mutation verb. brief-type: create (POST), update (PUT-replace), delete (irreversible). brief: set-status, delete. comment: create."),
|
|
189
|
+
.describe("Mutation verb. brief-type: create (POST), update (PUT-replace), delete (irreversible). brief: create (POST), update (partial PUT), set-status, delete. comment: create."),
|
|
190
190
|
briefTypeId: zod_1.z
|
|
191
191
|
.string()
|
|
192
192
|
.uuid()
|
|
193
193
|
.optional()
|
|
194
|
-
.describe("Brief type UUID. Required for brief-type verb='update' and verb='delete'."),
|
|
194
|
+
.describe("Brief type UUID. Required for brief-type verb='update' and verb='delete', and for brief verb='create' (the type to build the brief against)."),
|
|
195
195
|
briefId: zod_1.z
|
|
196
196
|
.string()
|
|
197
197
|
.uuid()
|
|
198
198
|
.optional()
|
|
199
|
-
.describe("Brief UUID. Required for resource='brief'
|
|
199
|
+
.describe("Brief UUID. Required for resource='brief' verbs 'update', 'set-status', and 'delete'."),
|
|
200
200
|
status: zod_1.z
|
|
201
201
|
.enum(["Draft", "InReview", "Approved", "Canceled", "Archived"])
|
|
202
202
|
.optional()
|
|
203
|
-
.describe("Target brief status for verb='set-status'. Wire form — 'InReview' is the 'In Review' UI label. A brief must leave 'Draft' before it can be linked to a campaign."),
|
|
203
|
+
.describe("Target brief status. Required for verb='set-status'; optional on brief 'create'/'update' (defaults to server 'Draft' on create). Wire form — 'InReview' is the 'In Review' UI label. A brief must leave 'Draft' before it can be linked to a campaign."),
|
|
204
204
|
commentText: zod_1.z
|
|
205
205
|
.string()
|
|
206
206
|
.optional()
|
|
@@ -221,7 +221,19 @@ const registerBriefTools = (registry) => {
|
|
|
221
221
|
.describe("Field definitions (RichText | DateTime | Timeline | Budget). Use brief_inspect verb='type' on an existing type for a worked example."),
|
|
222
222
|
})
|
|
223
223
|
.optional()
|
|
224
|
-
.describe("Full BriefType body for verb='create' or verb='update'. Required on writes; ignored on delete."),
|
|
224
|
+
.describe("Full BriefType body for resource='brief-type' verb='create' or verb='update'. Required on brief-type writes; ignored on delete."),
|
|
225
|
+
brief: zod_1.z
|
|
226
|
+
.object({
|
|
227
|
+
name: zod_1.z.string().min(1).describe("Brief display name."),
|
|
228
|
+
locale: zod_1.z.string().optional().describe("BCP-47-ish locale, e.g. 'en-us'."),
|
|
229
|
+
fields: zod_1.z
|
|
230
|
+
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
231
|
+
.optional()
|
|
232
|
+
.describe("Field values keyed by BriefField.name; the per-field shape follows the brief type's field definitions."),
|
|
233
|
+
isTemplate: zod_1.z.boolean().optional().describe("Whether the brief is a template."),
|
|
234
|
+
})
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("Brief body. Required for resource='brief' verb='create' (with `briefTypeId`); optional for verb='update' (any subset of name/locale/fields/isTemplate plus the top-level `status`)."),
|
|
225
237
|
...common_1.environmentBindingShape,
|
|
226
238
|
...common_1.allowWriteShape,
|
|
227
239
|
...common_1.whatIfShape,
|
|
@@ -230,6 +242,71 @@ const registerBriefTools = (registry) => {
|
|
|
230
242
|
const taskOpts = baseTaskOptions(context.configPath, input.environmentName ?? context.envName);
|
|
231
243
|
const whatIf = input.whatIf;
|
|
232
244
|
if (input.resource === "brief") {
|
|
245
|
+
if (input.verb === "create") {
|
|
246
|
+
if (!input.brief) {
|
|
247
|
+
throw (0, errors_1.createScaiError)("verb='create' on resource='brief' requires `brief`.", "INPUT_INVALID");
|
|
248
|
+
}
|
|
249
|
+
if (!input.briefTypeId) {
|
|
250
|
+
throw (0, errors_1.createScaiError)("verb='create' on resource='brief' requires `briefTypeId`.", "INPUT_INVALID");
|
|
251
|
+
}
|
|
252
|
+
const createInput = {
|
|
253
|
+
name: input.brief.name,
|
|
254
|
+
briefTypeId: input.briefTypeId,
|
|
255
|
+
...(input.brief.locale !== undefined && { locale: input.brief.locale }),
|
|
256
|
+
...(input.brief.fields !== undefined && { fields: input.brief.fields }),
|
|
257
|
+
...(input.brief.isTemplate !== undefined && { isTemplate: input.brief.isTemplate }),
|
|
258
|
+
};
|
|
259
|
+
const result = await (0, tasks_1.runBriefCreate)({
|
|
260
|
+
...taskOpts,
|
|
261
|
+
input: createInput,
|
|
262
|
+
whatIf,
|
|
263
|
+
});
|
|
264
|
+
const isPlan = "plan" in result;
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: isPlan
|
|
270
|
+
? `Plan: create brief '${input.brief.name}'.`
|
|
271
|
+
: `Created brief '${input.brief.name}'.`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
structuredContent: { resource: input.resource, verb: input.verb, result },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (input.verb === "update") {
|
|
278
|
+
if (!input.briefId) {
|
|
279
|
+
throw (0, errors_1.createScaiError)("verb='update' on resource='brief' requires `briefId`.", "INPUT_INVALID");
|
|
280
|
+
}
|
|
281
|
+
const patch = {
|
|
282
|
+
...(input.brief?.name !== undefined && { name: input.brief.name }),
|
|
283
|
+
...(input.brief?.locale !== undefined && { locale: input.brief.locale }),
|
|
284
|
+
...(input.brief?.fields !== undefined && { fields: input.brief.fields }),
|
|
285
|
+
...(input.brief?.isTemplate !== undefined && { isTemplate: input.brief.isTemplate }),
|
|
286
|
+
...(input.status !== undefined && { status: input.status }),
|
|
287
|
+
};
|
|
288
|
+
if (Object.keys(patch).length === 0) {
|
|
289
|
+
throw (0, errors_1.createScaiError)("verb='update' on resource='brief' requires at least one field in `brief` or a top-level `status`.", "INPUT_INVALID");
|
|
290
|
+
}
|
|
291
|
+
const result = await (0, tasks_1.runBriefUpdate)({
|
|
292
|
+
...taskOpts,
|
|
293
|
+
briefId: input.briefId,
|
|
294
|
+
patch,
|
|
295
|
+
whatIf,
|
|
296
|
+
});
|
|
297
|
+
const isPlan = "plan" in result;
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: isPlan
|
|
303
|
+
? `Plan: update brief ${input.briefId} (${Object.keys(patch).join(", ")}).`
|
|
304
|
+
: `Updated brief ${input.briefId}.`,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
structuredContent: { resource: input.resource, verb: input.verb, result },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
233
310
|
if (input.verb === "set-status") {
|
|
234
311
|
if (!input.briefId) {
|
|
235
312
|
throw (0, errors_1.createScaiError)("verb='set-status' requires `briefId`.", "INPUT_INVALID");
|
|
@@ -277,7 +354,7 @@ const registerBriefTools = (registry) => {
|
|
|
277
354
|
structuredContent: { resource: input.resource, verb: input.verb, result },
|
|
278
355
|
};
|
|
279
356
|
}
|
|
280
|
-
throw (0, errors_1.createScaiError)("resource='brief' supports
|
|
357
|
+
throw (0, errors_1.createScaiError)("resource='brief' supports verbs 'create', 'update', 'set-status', and 'delete'.", "INPUT_INVALID");
|
|
281
358
|
}
|
|
282
359
|
if (input.resource === "comment") {
|
|
283
360
|
if (input.verb !== "create") {
|
|
@@ -6,6 +6,7 @@ const operations_1 = require("../ir/operations");
|
|
|
6
6
|
const policy_1 = require("../runtime/policy");
|
|
7
7
|
const sitecore_templates_1 = require("../ir/sitecore-templates");
|
|
8
8
|
const recipe_1 = require("../schema/recipe");
|
|
9
|
+
const component_section_1 = require("./component-section");
|
|
9
10
|
const shared_1 = require("./shared");
|
|
10
11
|
/**
|
|
11
12
|
* Compile a standalone `DesignParametersTemplateRecipe` to an Operation IR.
|
|
@@ -22,9 +23,10 @@ function compileDesignParametersTemplateRecipe(input, context, emittedFolders =
|
|
|
22
23
|
const policy = (0, policy_1.defaultPolicyForRecipe)(recipe.kind);
|
|
23
24
|
const icon = recipe.icon ?? sitecore_templates_1.DEFAULT_ICON;
|
|
24
25
|
const site = (0, shared_1.siteOf)(context);
|
|
25
|
-
(0,
|
|
26
|
-
|
|
27
|
-
const
|
|
26
|
+
const sectionName = (0, component_section_1.resolveSectionRecipe)(recipe.handle, recipe.section.handle, context.sectionsByHandle).name;
|
|
27
|
+
(0, shared_1.ensureSectionFolder)(operations, context, sectionName, emittedFolders);
|
|
28
|
+
const bucketRefKey = (0, shared_1.ensurePresentationDesignParametersBucket)(operations, context, sectionName, emittedFolders);
|
|
29
|
+
const parentPath = (0, shared_1.resolvePresentationDesignParametersBucketPath)(context, sectionName);
|
|
28
30
|
// The standalone parameters template lands at the same identity
|
|
29
31
|
// (designParametersTemplateId) as inline-hoisted ones — keeps re-pushes
|
|
30
32
|
// idempotent if a recipe migrates from inline to standalone.
|
|
@@ -118,17 +118,12 @@ function compileEnumerationRecipe(input, context, emittedFolders = new Set()) {
|
|
|
118
118
|
kind: "ref-path",
|
|
119
119
|
value: context.enumerationsRoot,
|
|
120
120
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (folderSegments.length === 0) {
|
|
128
|
-
throw (0, errors_1.createScaiError)(`Recipe '${recipe.handle}' declares location.folder that is empty after trimming.`, "INPUT_INVALID", {
|
|
129
|
-
hint: "Use a non-empty folder string like 'Theme' or 'Components/Card'.",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
121
|
+
// `recipe.location.folder` is normalized to `string[]` by the schema
|
|
122
|
+
// (`FolderPath` in schema/recipe.ts) — both `"Theme/Color"` and
|
|
123
|
+
// `["Theme", "Color"]` input shapes land here as `["Theme", "Color"]`,
|
|
124
|
+
// already trimmed and non-empty.
|
|
125
|
+
const folderSegments = recipe.location?.folder;
|
|
126
|
+
if (folderSegments && folderSegments.length > 0) {
|
|
132
127
|
const cumulativeSegments = [];
|
|
133
128
|
for (const segment of folderSegments) {
|
|
134
129
|
cumulativeSegments.push(segment);
|
|
@@ -50,9 +50,12 @@ const resolveEnumFolderPath = (context, enumHandle, consumerHandle) => {
|
|
|
50
50
|
hint: "Add an `EnumerationRecipe` (kind: 'enumeration') with the matching handle to the recipe set, or change the field's `sitecore.enumHandle` to point at an existing one.",
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// `location.folder` is a `string[]` of grouping segments after the
|
|
54
|
+
// schema's `FolderPath` normalisation — join with `/` here to build
|
|
55
|
+
// the cumulative path under enumerationsRoot.
|
|
56
|
+
const folderSegments = enumRecipe.location?.folder;
|
|
57
|
+
return folderSegments && folderSegments.length > 0
|
|
58
|
+
? (0, exports.joinPath)((0, exports.joinPath)(context.enumerationsRoot, folderSegments.join("/")), enumRecipe.name)
|
|
56
59
|
: (0, exports.joinPath)(context.enumerationsRoot, enumRecipe.name);
|
|
57
60
|
};
|
|
58
61
|
exports.resolveEnumFolderPath = resolveEnumFolderPath;
|
|
@@ -799,19 +802,16 @@ function encodeStandardValueDefault(raw, type) {
|
|
|
799
802
|
function resolveFieldSource(field, type, site, recipeHandle, context) {
|
|
800
803
|
const sc = field.sitecore;
|
|
801
804
|
if (sc) {
|
|
802
|
-
const fields =
|
|
803
|
-
sourceTypes: sc.sourceTypes,
|
|
804
|
-
sourceQuery: sc.sourceQuery,
|
|
805
|
-
sourceScope: sc.sourceScope,
|
|
806
|
-
sourceRaw: sc.sourceRaw,
|
|
807
|
-
};
|
|
805
|
+
const fields = (0, source_fields_1.augmentSourceToFields)(sc.source);
|
|
808
806
|
if ((0, source_fields_1.sourceFieldsNeedHandleResolution)(fields)) {
|
|
807
|
+
// `types` is non-empty here because `sourceFieldsNeedHandleResolution`
|
|
808
|
+
// returned true; the cast is to satisfy the IR's `.min(1)` constraint.
|
|
809
809
|
return {
|
|
810
810
|
kind: "ref-source-fields",
|
|
811
811
|
site,
|
|
812
|
-
sourceTypes:
|
|
813
|
-
sourceQuery:
|
|
814
|
-
sourceScope:
|
|
812
|
+
sourceTypes: fields.sourceTypes,
|
|
813
|
+
sourceQuery: fields.sourceQuery,
|
|
814
|
+
sourceScope: fields.sourceScope,
|
|
815
815
|
};
|
|
816
816
|
}
|
|
817
817
|
const rendered = (0, source_fields_1.renderSourceFields)(fields, () => {
|
package/dist/recipe/compile.js
CHANGED
|
@@ -720,10 +720,10 @@ const buildPlaceholderSettingsAggregate = (recipes, context, site) => {
|
|
|
720
720
|
// parent ref + path the leaf placeholder item lands under.
|
|
721
721
|
const emittedFolders = new Set();
|
|
722
722
|
const resolveFolderParent = (folder) => {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
723
|
+
// `folder` is normalised to `string[]` at the schema boundary
|
|
724
|
+
// (`FolderPath` in schema/recipe.ts) — both array and legacy
|
|
725
|
+
// slash-string inputs land here as a clean segment list.
|
|
726
|
+
const segments = folder ?? [];
|
|
727
727
|
let parentPath = root;
|
|
728
728
|
let parentRef = { kind: "ref-path", value: root };
|
|
729
729
|
let cumulative = "";
|
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
|
/**
|