@sitecoreai-labs/sitecoreai-cli 0.2.2 → 0.2.3
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/brand/index.d.ts +1 -1
- package/dist/brand/index.js +2 -1
- package/dist/brand/recipe/kind.js +0 -0
- package/dist/brand/recipe/synthesize-doc.d.ts +59 -0
- package/dist/brand/recipe/synthesize-doc.js +140 -0
- package/dist/brand/seed.d.ts +34 -0
- package/dist/brand/seed.js +115 -1
- package/dist/recipe/compile/component-template.js +23 -9
- package/package.json +1 -1
package/dist/brand/index.d.ts
CHANGED
|
@@ -31,4 +31,4 @@ export { listBrandKits, getBrandKit, type BrandKitSummary, type ListBrandKitsRes
|
|
|
31
31
|
export { listBrandKitSections, listBrandKitFields, updateBrandKitField, type BrandKitSectionSummary, type BrandKitFieldSummary, type BrandKitFieldType, type BrandKitFieldValue, type BrandKitArrayEntry, type BrandKitRichArrayEntry, type ListBrandKitSectionsOptions, type ListBrandKitFieldsOptions, type UpdateBrandKitFieldOptions, } from "./kits/sections";
|
|
32
32
|
export { createBrandKit, publishBrandKit, deleteBrandKit, type CreateBrandKitOptions, type PublishBrandKitOptions, type DeleteBrandKitOptions, } from "./kits/create";
|
|
33
33
|
export { listDocuments, getDocument, deleteDocument, type DocumentSummary, type ListDocumentsResponse, type ListDocumentsOptions, type GetDocumentOptions, type DeleteDocumentOptions, } from "./documents/list";
|
|
34
|
-
export { seedBrandKit, type SeedBrandKitOptions, type SeedBrandKitResult, type SeedProgressEvent, type SeedStage, } from "./seed";
|
|
34
|
+
export { seedBrandKit, enrichBrandKitWithDocuments, type SeedBrandKitOptions, type SeedBrandKitResult, type SeedProgressEvent, type SeedStage, type EnrichExistingKitOptions, type EnrichExistingKitResult, } from "./seed";
|
package/dist/brand/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* change in any release without a major bump or a changeset.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.seedBrandKit = exports.deleteDocument = exports.getDocument = exports.listDocuments = exports.deleteBrandKit = exports.publishBrandKit = exports.createBrandKit = exports.updateBrandKitField = exports.listBrandKitFields = exports.listBrandKitSections = exports.getBrandKit = exports.listBrandKits = exports.PIPELINE_BASE_PATH = exports.runEnrichSectionsPipeline = exports.runBrandIngestionPipeline = exports.LOCAL_UPLOAD_UNSUPPORTED_HINT = exports.LOCAL_UPLOAD_UNSUPPORTED_MESSAGE = exports.DOCUMENTS_BASE_PATH = exports.uploadDocument = exports.formatTextSummary = exports.formatTextRow = exports.buildTextReport = exports.buildSarifReport = exports.buildJsonReport = exports.summarizeOutcomes = exports.scoreToSarifLevel = exports.computeExitCode = exports.detectInputFormat = exports.resolveReviewInputs = exports.runBrandReview = exports.runBrandLogin = exports.generateBrandReview = exports.BRAND_REVIEW_BASE_PATH = exports.BRAND_MANAGEMENT_BASE_PATH = exports.BRAND_API_HOST = exports.requestBrandApi = exports.extractScopes = exports.hasBrandScopes = exports.acquireBrandToken = exports.BRAND_REQUIRED_SCOPES = void 0;
|
|
19
|
+
exports.enrichBrandKitWithDocuments = exports.seedBrandKit = exports.deleteDocument = exports.getDocument = exports.listDocuments = exports.deleteBrandKit = exports.publishBrandKit = exports.createBrandKit = exports.updateBrandKitField = exports.listBrandKitFields = exports.listBrandKitSections = exports.getBrandKit = exports.listBrandKits = exports.PIPELINE_BASE_PATH = exports.runEnrichSectionsPipeline = exports.runBrandIngestionPipeline = exports.LOCAL_UPLOAD_UNSUPPORTED_HINT = exports.LOCAL_UPLOAD_UNSUPPORTED_MESSAGE = exports.DOCUMENTS_BASE_PATH = exports.uploadDocument = exports.formatTextSummary = exports.formatTextRow = exports.buildTextReport = exports.buildSarifReport = exports.buildJsonReport = exports.summarizeOutcomes = exports.scoreToSarifLevel = exports.computeExitCode = exports.detectInputFormat = exports.resolveReviewInputs = exports.runBrandReview = exports.runBrandLogin = exports.generateBrandReview = exports.BRAND_REVIEW_BASE_PATH = exports.BRAND_MANAGEMENT_BASE_PATH = exports.BRAND_API_HOST = exports.requestBrandApi = exports.extractScopes = exports.hasBrandScopes = exports.acquireBrandToken = exports.BRAND_REQUIRED_SCOPES = void 0;
|
|
20
20
|
var auth_1 = require("./api/auth");
|
|
21
21
|
Object.defineProperty(exports, "BRAND_REQUIRED_SCOPES", { enumerable: true, get: function () { return auth_1.BRAND_REQUIRED_SCOPES; } });
|
|
22
22
|
Object.defineProperty(exports, "acquireBrandToken", { enumerable: true, get: function () { return auth_1.acquireBrandToken; } });
|
|
@@ -75,3 +75,4 @@ Object.defineProperty(exports, "getDocument", { enumerable: true, get: function
|
|
|
75
75
|
Object.defineProperty(exports, "deleteDocument", { enumerable: true, get: function () { return list_2.deleteDocument; } });
|
|
76
76
|
var seed_1 = require("./seed");
|
|
77
77
|
Object.defineProperty(exports, "seedBrandKit", { enumerable: true, get: function () { return seed_1.seedBrandKit; } });
|
|
78
|
+
Object.defineProperty(exports, "enrichBrandKitWithDocuments", { enumerable: true, get: function () { return seed_1.enrichBrandKitWithDocuments; } });
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesize a minimal stub PDF to drive the Sitecore EnrichSections
|
|
3
|
+
* pipeline when a brand-kit recipe declares section content but ships
|
|
4
|
+
* no source document.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: the Sitecore Brand Management API has no "create
|
|
7
|
+
* section" endpoint — sections only appear as a side effect of
|
|
8
|
+
* `EnrichSectionsPipeline` running over an uploaded document. A
|
|
9
|
+
* recipe that wants to push field values into the canonical 7
|
|
10
|
+
* sections (Global Goals, Brand Context, Tone of Voice, Do's and
|
|
11
|
+
* Don'ts, Visual Guidelines, Grammar Checklists, Glossary and
|
|
12
|
+
* Localization) must therefore feed the pipeline *something*. When
|
|
13
|
+
* the operator has no real brand-guidelines PDF to upload, we
|
|
14
|
+
* synthesize one: a single-page PDF naming each declared section so
|
|
15
|
+
* enrichment produces the matching section structure. The actual
|
|
16
|
+
* field values converge via `updateBrandKitField` PATCH calls
|
|
17
|
+
* immediately after — the stub's content is throwaway scaffolding.
|
|
18
|
+
*
|
|
19
|
+
* The PDF is hand-rolled (no `pdf-lib` dependency). The format we
|
|
20
|
+
* emit is a minimal, valid PDF 1.4 document with:
|
|
21
|
+
* - one Catalog → Pages → Page → Contents chain
|
|
22
|
+
* - one Helvetica font (built-in, no embedded data)
|
|
23
|
+
* - a single text stream with section names line-broken
|
|
24
|
+
*
|
|
25
|
+
* Sitecore's Documents API accepts `data:application/pdf;base64,…`
|
|
26
|
+
* URLs in the v2 form-urlencoded create-request flow (verified in
|
|
27
|
+
* [[project-scai-documents-api-v1-vs-v2]]), with a ~1MB limit from
|
|
28
|
+
* the form-urlencoded body cap. Our synthesized PDF is well under
|
|
29
|
+
* 1KB even for kits with the full canonical section list.
|
|
30
|
+
*/
|
|
31
|
+
export interface SynthesizedStubDocument {
|
|
32
|
+
/** Data URL (`data:application/pdf;base64,…`) ready for `BrandDocument.url`. */
|
|
33
|
+
url: string;
|
|
34
|
+
/** Display title used when uploading. */
|
|
35
|
+
title: string;
|
|
36
|
+
/** Tags applied to the synthesized document. Distinguishes it from
|
|
37
|
+
* operator-supplied docs for downstream filtering. */
|
|
38
|
+
tags: string[];
|
|
39
|
+
/** Byte length of the underlying PDF — useful for tests / logging. */
|
|
40
|
+
byteLength: number;
|
|
41
|
+
}
|
|
42
|
+
export interface SynthesizeStubDocumentOptions {
|
|
43
|
+
/** Brand-kit display name; used in the doc title + PDF body. */
|
|
44
|
+
brandKitName: string;
|
|
45
|
+
/** Section names declared in the recipe — one PDF line per name. */
|
|
46
|
+
sectionNames: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a stub brand-document for a recipe that has section data but
|
|
50
|
+
* no source PDF. The synthesized doc has just enough structure for
|
|
51
|
+
* Sitecore's BrandIngestion + EnrichSections pipelines to produce
|
|
52
|
+
* the canonical section set; the field values declared in the recipe
|
|
53
|
+
* overwrite enrichment's output via `updateBrandKitField`.
|
|
54
|
+
*
|
|
55
|
+
* Throws when `sectionNames` is empty — a stub doc with no section
|
|
56
|
+
* names defeats the purpose, and callers should have gated on
|
|
57
|
+
* "recipe has sections" before reaching here.
|
|
58
|
+
*/
|
|
59
|
+
export declare const synthesizeBrandStubDocument: (options: SynthesizeStubDocumentOptions) => SynthesizedStubDocument;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Synthesize a minimal stub PDF to drive the Sitecore EnrichSections
|
|
4
|
+
* pipeline when a brand-kit recipe declares section content but ships
|
|
5
|
+
* no source document.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: the Sitecore Brand Management API has no "create
|
|
8
|
+
* section" endpoint — sections only appear as a side effect of
|
|
9
|
+
* `EnrichSectionsPipeline` running over an uploaded document. A
|
|
10
|
+
* recipe that wants to push field values into the canonical 7
|
|
11
|
+
* sections (Global Goals, Brand Context, Tone of Voice, Do's and
|
|
12
|
+
* Don'ts, Visual Guidelines, Grammar Checklists, Glossary and
|
|
13
|
+
* Localization) must therefore feed the pipeline *something*. When
|
|
14
|
+
* the operator has no real brand-guidelines PDF to upload, we
|
|
15
|
+
* synthesize one: a single-page PDF naming each declared section so
|
|
16
|
+
* enrichment produces the matching section structure. The actual
|
|
17
|
+
* field values converge via `updateBrandKitField` PATCH calls
|
|
18
|
+
* immediately after — the stub's content is throwaway scaffolding.
|
|
19
|
+
*
|
|
20
|
+
* The PDF is hand-rolled (no `pdf-lib` dependency). The format we
|
|
21
|
+
* emit is a minimal, valid PDF 1.4 document with:
|
|
22
|
+
* - one Catalog → Pages → Page → Contents chain
|
|
23
|
+
* - one Helvetica font (built-in, no embedded data)
|
|
24
|
+
* - a single text stream with section names line-broken
|
|
25
|
+
*
|
|
26
|
+
* Sitecore's Documents API accepts `data:application/pdf;base64,…`
|
|
27
|
+
* URLs in the v2 form-urlencoded create-request flow (verified in
|
|
28
|
+
* [[project-scai-documents-api-v1-vs-v2]]), with a ~1MB limit from
|
|
29
|
+
* the form-urlencoded body cap. Our synthesized PDF is well under
|
|
30
|
+
* 1KB even for kits with the full canonical section list.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.synthesizeBrandStubDocument = void 0;
|
|
34
|
+
/**
|
|
35
|
+
* Reduce arbitrary input text to printable ASCII for safe embedding
|
|
36
|
+
* in a PDF content stream that uses Helvetica + WinAnsiEncoding.
|
|
37
|
+
*
|
|
38
|
+
* Non-ASCII bytes would either (a) get truncated by
|
|
39
|
+
* `Buffer.from(..., "binary")` to garbage codepoints, or (b) render
|
|
40
|
+
* as the wrong glyph through the font's encoding map. Since this PDF
|
|
41
|
+
* is throwaway scaffolding the Sitecore pipeline parses for section
|
|
42
|
+
* names — not something a human reads — a lossy ASCII coercion is
|
|
43
|
+
* the right tradeoff. Operators who care about display content
|
|
44
|
+
* should ship a real document instead.
|
|
45
|
+
*/
|
|
46
|
+
const toPrintableAscii = (input) => input
|
|
47
|
+
.normalize("NFKD")
|
|
48
|
+
.replace(/[‐-―]/g, "-") // dashes
|
|
49
|
+
.replace(/[‘’‚‛]/g, "'") // single quotes
|
|
50
|
+
.replace(/[“”„‟]/g, '"') // double quotes
|
|
51
|
+
.replace(/…/g, "...") // ellipsis
|
|
52
|
+
.replace(/[^\x20-\x7E]/g, "?");
|
|
53
|
+
/**
|
|
54
|
+
* Escape a string for inclusion inside a PDF string literal — the
|
|
55
|
+
* `(...)` form. Per PDF 1.4 §3.2.3, the backslash escape applies to
|
|
56
|
+
* `(`, `)`, and `\`. Newlines are emitted as actual newlines in the
|
|
57
|
+
* stream via separate Td positioning, so they never appear inside
|
|
58
|
+
* a literal here. Caller is responsible for ASCII-sanitizing the
|
|
59
|
+
* input first (see `toPrintableAscii`).
|
|
60
|
+
*/
|
|
61
|
+
const escapePdfLiteral = (input) => input.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
62
|
+
/**
|
|
63
|
+
* Build a single-page PDF whose content stream lists the given lines,
|
|
64
|
+
* one line per row in 12pt Helvetica. Returns the raw bytes.
|
|
65
|
+
*
|
|
66
|
+
* Byte-offset bookkeeping: PDF's `xref` table demands the exact byte
|
|
67
|
+
* offset of each object's first byte from the start of the file. We
|
|
68
|
+
* concatenate object bodies into a running buffer and record each
|
|
69
|
+
* object's position before appending it; the final `startxref` value
|
|
70
|
+
* is the position of the `xref` keyword itself.
|
|
71
|
+
*/
|
|
72
|
+
const buildMinimalPdf = (lines) => {
|
|
73
|
+
// PDF 1.4 binary-tag comment — the four high-bit bytes mark the
|
|
74
|
+
// file as binary so naive ASCII-only tools don't truncate it.
|
|
75
|
+
const header = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n";
|
|
76
|
+
// Content stream — BT/ET wraps the text-rendering block; Td moves
|
|
77
|
+
// the text cursor. 72,720 is roughly 1in from top-left on letter.
|
|
78
|
+
const streamLines = ["BT", "/F1 12 Tf", "72 720 Td"];
|
|
79
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
80
|
+
if (i > 0)
|
|
81
|
+
streamLines.push("0 -16 Td");
|
|
82
|
+
streamLines.push(`(${escapePdfLiteral(lines[i] ?? "")}) Tj`);
|
|
83
|
+
}
|
|
84
|
+
streamLines.push("ET");
|
|
85
|
+
const stream = streamLines.join("\n");
|
|
86
|
+
// 5 indirect objects: Catalog, Pages, Page, Contents (text stream),
|
|
87
|
+
// Font. Order matters — refs (e.g. /Pages 2 0 R) point at the
|
|
88
|
+
// numbered indirect object below.
|
|
89
|
+
const objects = [
|
|
90
|
+
"<< /Type /Catalog /Pages 2 0 R >>",
|
|
91
|
+
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
92
|
+
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] " +
|
|
93
|
+
"/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
|
|
94
|
+
`<< /Length ${Buffer.byteLength(stream, "binary")} >>\nstream\n${stream}\nendstream`,
|
|
95
|
+
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
|
|
96
|
+
];
|
|
97
|
+
let body = header;
|
|
98
|
+
const offsets = [];
|
|
99
|
+
for (let i = 0; i < objects.length; i += 1) {
|
|
100
|
+
offsets.push(Buffer.byteLength(body, "binary"));
|
|
101
|
+
body += `${i + 1} 0 obj\n${objects[i]}\nendobj\n`;
|
|
102
|
+
}
|
|
103
|
+
const xrefStart = Buffer.byteLength(body, "binary");
|
|
104
|
+
let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
|
|
105
|
+
for (const off of offsets) {
|
|
106
|
+
xref += `${String(off).padStart(10, "0")} 00000 n \n`;
|
|
107
|
+
}
|
|
108
|
+
xref +=
|
|
109
|
+
`trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n` + `startxref\n${xrefStart}\n%%EOF\n`;
|
|
110
|
+
return Buffer.from(body + xref, "binary");
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Build a stub brand-document for a recipe that has section data but
|
|
114
|
+
* no source PDF. The synthesized doc has just enough structure for
|
|
115
|
+
* Sitecore's BrandIngestion + EnrichSections pipelines to produce
|
|
116
|
+
* the canonical section set; the field values declared in the recipe
|
|
117
|
+
* overwrite enrichment's output via `updateBrandKitField`.
|
|
118
|
+
*
|
|
119
|
+
* Throws when `sectionNames` is empty — a stub doc with no section
|
|
120
|
+
* names defeats the purpose, and callers should have gated on
|
|
121
|
+
* "recipe has sections" before reaching here.
|
|
122
|
+
*/
|
|
123
|
+
const synthesizeBrandStubDocument = (options) => {
|
|
124
|
+
if (options.sectionNames.length === 0) {
|
|
125
|
+
throw new Error("synthesizeBrandStubDocument requires at least one section name; " +
|
|
126
|
+
"callers should check `Object.keys(recipe.sections).length > 0` first.");
|
|
127
|
+
}
|
|
128
|
+
const headline = toPrintableAscii(`${options.brandKitName} - brand kit scaffold (synthesized by scai).`);
|
|
129
|
+
const sectionLines = options.sectionNames.map((name) => toPrintableAscii(`Section: ${name}`));
|
|
130
|
+
const lines = [headline, "", ...sectionLines];
|
|
131
|
+
const pdf = buildMinimalPdf(lines);
|
|
132
|
+
const base64 = pdf.toString("base64");
|
|
133
|
+
return {
|
|
134
|
+
url: `data:application/pdf;base64,${base64}`,
|
|
135
|
+
title: `${options.brandKitName} brand kit scaffold`,
|
|
136
|
+
tags: ["scai-synthesized", "stub"],
|
|
137
|
+
byteLength: pdf.byteLength,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
exports.synthesizeBrandStubDocument = synthesizeBrandStubDocument;
|
package/dist/brand/seed.d.ts
CHANGED
|
@@ -61,6 +61,40 @@ export interface SeedBrandKitResult {
|
|
|
61
61
|
/** Total wall-clock seconds spent. */
|
|
62
62
|
elapsedSec: number;
|
|
63
63
|
}
|
|
64
|
+
export interface EnrichExistingKitOptions {
|
|
65
|
+
client: BrandApiClientOptions;
|
|
66
|
+
/** Brand kit id (the API uuid, not the display name). */
|
|
67
|
+
brandKitId: string;
|
|
68
|
+
/** Display name — used only for default upload metadata and log lines. */
|
|
69
|
+
name: string;
|
|
70
|
+
/** One or more documents to upload before the ingest+enrich cycle. */
|
|
71
|
+
documents: BrandDocument[];
|
|
72
|
+
pollIntervalSec?: number;
|
|
73
|
+
timeoutSec?: number;
|
|
74
|
+
onProgress?: (event: SeedProgressEvent) => void;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
export interface EnrichExistingKitResult {
|
|
78
|
+
document: UploadedDocument;
|
|
79
|
+
sections: BrandKitSectionSummary[];
|
|
80
|
+
elapsedSec: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run the upload → publish → ingest → enrich → poll pipeline against
|
|
84
|
+
* an EXISTING brand kit. Mirrors steps 2-6 of `seedBrandKit` without
|
|
85
|
+
* creating a new kit — the self-heal path for bare kits that were
|
|
86
|
+
* previously created without documents (and therefore have zero
|
|
87
|
+
* sections).
|
|
88
|
+
*
|
|
89
|
+
* Why this exists: `scai brand sync push` would historically call
|
|
90
|
+
* `createBrandKit` (no documents) when the operator's recipe shipped
|
|
91
|
+
* no source PDF, leaving the kit with zero sections forever. The
|
|
92
|
+
* synthesize-stub-PDF feature fixed initial creation; this function
|
|
93
|
+
* unblocks the kits already stuck in that state — calling
|
|
94
|
+
* `apply()` against the existing kit now produces a stub doc and
|
|
95
|
+
* runs enrichment, so field writes have somewhere to land.
|
|
96
|
+
*/
|
|
97
|
+
export declare const enrichBrandKitWithDocuments: (options: EnrichExistingKitOptions) => Promise<EnrichExistingKitResult>;
|
|
64
98
|
/**
|
|
65
99
|
* The headline composite — drive a brand kit from "doesn't exist" to
|
|
66
100
|
* "has populated sections and is ready for Brand Review" in one call.
|
package/dist/brand/seed.js
CHANGED
|
@@ -1,12 +1,126 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.seedBrandKit = void 0;
|
|
3
|
+
exports.seedBrandKit = exports.enrichBrandKitWithDocuments = void 0;
|
|
4
4
|
const create_1 = require("./kits/create");
|
|
5
5
|
const sections_1 = require("./kits/sections");
|
|
6
6
|
const upload_1 = require("./documents/upload");
|
|
7
7
|
const run_1 = require("./pipeline/run");
|
|
8
8
|
const errors_1 = require("../shared/errors");
|
|
9
9
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
/**
|
|
11
|
+
* Run the upload → publish → ingest → enrich → poll pipeline against
|
|
12
|
+
* an EXISTING brand kit. Mirrors steps 2-6 of `seedBrandKit` without
|
|
13
|
+
* creating a new kit — the self-heal path for bare kits that were
|
|
14
|
+
* previously created without documents (and therefore have zero
|
|
15
|
+
* sections).
|
|
16
|
+
*
|
|
17
|
+
* Why this exists: `scai brand sync push` would historically call
|
|
18
|
+
* `createBrandKit` (no documents) when the operator's recipe shipped
|
|
19
|
+
* no source PDF, leaving the kit with zero sections forever. The
|
|
20
|
+
* synthesize-stub-PDF feature fixed initial creation; this function
|
|
21
|
+
* unblocks the kits already stuck in that state — calling
|
|
22
|
+
* `apply()` against the existing kit now produces a stub doc and
|
|
23
|
+
* runs enrichment, so field writes have somewhere to land.
|
|
24
|
+
*/
|
|
25
|
+
const enrichBrandKitWithDocuments = async (options) => {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
const pollIntervalSec = options.pollIntervalSec ?? 15;
|
|
28
|
+
const timeoutSec = options.timeoutSec ?? 900;
|
|
29
|
+
const emit = (event) => {
|
|
30
|
+
options.onProgress?.({
|
|
31
|
+
...event,
|
|
32
|
+
elapsedSec: Math.round((Date.now() - start) / 1000),
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
// Validate the document list — same registry-file guard seedBrandKit
|
|
36
|
+
// applies; keeps the rejection contract consistent across the two
|
|
37
|
+
// entry points.
|
|
38
|
+
for (const doc of options.documents) {
|
|
39
|
+
if (doc.kind === "registry-file") {
|
|
40
|
+
throw (0, errors_1.createScaiError)(`Brand document "${doc.path}" is a registry-file path; scai cannot upload local bytes.`, "INPUT_INVALID", {
|
|
41
|
+
hint: 'Host the file at an HTTPS URL Sitecore\'s edge can reach (S3, GitHub raw, a CDN) and pass it as a `{ kind: "url", url }` document.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (options.documents.length === 0) {
|
|
46
|
+
throw (0, errors_1.createScaiError)("enrichBrandKitWithDocuments requires at least one document.", "INPUT_INVALID");
|
|
47
|
+
}
|
|
48
|
+
// Upload docs
|
|
49
|
+
const documents = [];
|
|
50
|
+
for (const [index, doc] of options.documents.entries()) {
|
|
51
|
+
if (doc.kind !== "url") {
|
|
52
|
+
throw (0, errors_1.createScaiError)("Unreachable: non-URL document survived the registry-file guard.", "INPUT_INVALID");
|
|
53
|
+
}
|
|
54
|
+
emit({
|
|
55
|
+
stage: "uploadDocument",
|
|
56
|
+
message: `Uploading source document ${index + 1}/${options.documents.length}…`,
|
|
57
|
+
});
|
|
58
|
+
const uploaded = await (0, upload_1.uploadDocument)({
|
|
59
|
+
client: options.client,
|
|
60
|
+
brandKitId: options.brandKitId,
|
|
61
|
+
source: { url: doc.url },
|
|
62
|
+
title: doc.title ?? `${options.name} Brand Guidelines`,
|
|
63
|
+
summary: doc.summary ?? `Source doc for ${options.name} brand kit (seeded via scai)`,
|
|
64
|
+
type: "brand guidelines",
|
|
65
|
+
fileType: "application/pdf",
|
|
66
|
+
});
|
|
67
|
+
documents.push(uploaded);
|
|
68
|
+
emit({ stage: "uploadDocument", message: `Uploaded document ${uploaded.id}` });
|
|
69
|
+
}
|
|
70
|
+
// Publish kit (idempotent — already-published kits accept the PATCH).
|
|
71
|
+
emit({ stage: "publishKit", message: "Publishing kit…" });
|
|
72
|
+
await (0, create_1.publishBrandKit)({ client: options.client, brandKitId: options.brandKitId });
|
|
73
|
+
emit({ stage: "publishKit", message: "Kit published" });
|
|
74
|
+
// Ingest + enrich
|
|
75
|
+
emit({ stage: "runIngestion", message: "Triggering BrandIngestionPipeline…" });
|
|
76
|
+
const ingestionRun = await (0, run_1.runBrandIngestionPipeline)({
|
|
77
|
+
client: options.client,
|
|
78
|
+
brandKitId: options.brandKitId,
|
|
79
|
+
populateSections: true,
|
|
80
|
+
documentIds: documents.map((doc) => doc.id),
|
|
81
|
+
});
|
|
82
|
+
emit({ stage: "runIngestion", message: `Ingestion run started: ${ingestionRun.id}` });
|
|
83
|
+
emit({ stage: "runEnrichment", message: "Triggering EnrichSectionsPipeline…" });
|
|
84
|
+
const enrichmentRun = await (0, run_1.runEnrichSectionsPipeline)({
|
|
85
|
+
client: options.client,
|
|
86
|
+
brandKitId: options.brandKitId,
|
|
87
|
+
});
|
|
88
|
+
emit({ stage: "runEnrichment", message: `Enrichment run started: ${enrichmentRun.id}` });
|
|
89
|
+
// Poll until sections appear
|
|
90
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
91
|
+
let sections = [];
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
if (options.signal?.aborted) {
|
|
94
|
+
throw new Error("enrichBrandKitWithDocuments aborted by signal");
|
|
95
|
+
}
|
|
96
|
+
sections = await (0, sections_1.listBrandKitSections)({
|
|
97
|
+
client: options.client,
|
|
98
|
+
brandKitId: options.brandKitId,
|
|
99
|
+
});
|
|
100
|
+
emit({
|
|
101
|
+
stage: "pollSections",
|
|
102
|
+
message: `Sections: ${sections.length}`,
|
|
103
|
+
sectionCount: sections.length,
|
|
104
|
+
});
|
|
105
|
+
if (sections.length > 0)
|
|
106
|
+
break;
|
|
107
|
+
await sleep(pollIntervalSec * 1000);
|
|
108
|
+
}
|
|
109
|
+
if (sections.length === 0) {
|
|
110
|
+
throw new Error(`Timed out after ${timeoutSec}s waiting for sections to populate on kit ${options.brandKitId}.`);
|
|
111
|
+
}
|
|
112
|
+
emit({
|
|
113
|
+
stage: "done",
|
|
114
|
+
message: `Enriched kit ${options.brandKitId} with ${sections.length} sections`,
|
|
115
|
+
sectionCount: sections.length,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
document: documents[0],
|
|
119
|
+
sections,
|
|
120
|
+
elapsedSec: Math.round((Date.now() - start) / 1000),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
exports.enrichBrandKitWithDocuments = enrichBrandKitWithDocuments;
|
|
10
124
|
/**
|
|
11
125
|
* The headline composite — drive a brand kit from "doesn't exist" to
|
|
12
126
|
* "has populated sections and is ready for Brand Review" in one call.
|
|
@@ -448,19 +448,33 @@ function emitRendering(operations, recipe, context, icon, hasParams, policy, emi
|
|
|
448
448
|
const sectionName = resolveSectionName(recipe, context);
|
|
449
449
|
const renderingParentPath = (0, shared_1.resolveRenderingParent)(context, sectionName);
|
|
450
450
|
const renderingPath = (0, shared_1.joinPath)(renderingParentPath, recipe.name);
|
|
451
|
-
// Datasource template ref
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
451
|
+
// Datasource template ref. Three cases:
|
|
452
|
+
// 1. Explicit `datasource.template` handle → reference the separate
|
|
453
|
+
// ContentTemplateRecipe (compatible-data-source pattern).
|
|
454
|
+
// 2. Inline `fields:` (recipe has ≥ 1 field) → the component template
|
|
455
|
+
// IS the datasource template (legacy inline-fields pattern).
|
|
456
|
+
// 3. Neither — a pure-layout rendering (Container, ColumnSplitter,
|
|
457
|
+
// RowSplitter, …). Emit no Datasource Template field at all so
|
|
458
|
+
// the rendering item's shared field stays empty. Sitecore Pages
|
|
459
|
+
// gates its "create or pick a datasource" prompt on this field
|
|
460
|
+
// being non-empty, so an empty value is what makes a layout-only
|
|
461
|
+
// rendering droppable without an authoring prompt.
|
|
462
|
+
const hasInlineFields = (recipe.fields?.length ?? 0) > 0;
|
|
455
463
|
const datasourceRefKey = recipe.datasource?.template
|
|
456
464
|
? (0, guids_1.templateId)(site, recipe.datasource.template.handle)
|
|
457
|
-
:
|
|
465
|
+
: hasInlineFields
|
|
466
|
+
? (0, guids_1.templateId)(site, recipe.handle)
|
|
467
|
+
: undefined;
|
|
458
468
|
const fields = [
|
|
459
469
|
(0, shared_1.sharedField)(sitecore_templates_1.RENDERING_FIELDS.COMPONENT_NAME, { kind: "string", value: recipe.name }),
|
|
460
|
-
(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
...(datasourceRefKey
|
|
471
|
+
? [
|
|
472
|
+
(0, shared_1.sharedField)(sitecore_templates_1.RENDERING_FIELDS.DATASOURCE_TEMPLATE, {
|
|
473
|
+
kind: "ref-recipe",
|
|
474
|
+
refKey: datasourceRefKey,
|
|
475
|
+
}),
|
|
476
|
+
]
|
|
477
|
+
: []),
|
|
464
478
|
(0, shared_1.sharedField)(sitecore_templates_1.SYSTEM_FIELDS.ICON, { kind: "string", value: icon }),
|
|
465
479
|
(0, shared_1.versionedField)(sitecore_templates_1.SYSTEM_FIELDS.DISPLAY_NAME, { kind: "string", value: recipe.displayName }),
|
|
466
480
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sitecoreai-labs/sitecoreai-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "SitecoreAI developer toolkit — a native TypeScript CLI, SDK, and MCP server for deploy, serialization, recipes, publishing, and content operations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|