@remkoj/optimizely-cms-cli 6.0.0-rc.1 → 6.0.0-rc.4
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/AGENTS.md +68 -0
- package/dist/index.js +365 -84
- package/dist/index.js.map +1 -1
- package/package.json +26 -12
- package/script/postinstall.mjs +45 -0
package/dist/index.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Developer Utitility providing helpers for common tasks to building a
|
|
4
|
+
* frontend application that uses Optimizely CMS/Graph as content repository.
|
|
5
|
+
*
|
|
6
|
+
* License: Apache 2
|
|
7
|
+
* Copyright (c) 2023-2026 - Remko Jantzen
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { globSync, glob, globIterate } from 'glob';
|
|
2
11
|
import { config } from 'dotenv';
|
|
3
12
|
import { expand } from 'dotenv-expand';
|
|
4
13
|
import path from 'node:path';
|
|
5
14
|
import fs from 'node:fs';
|
|
6
|
-
import { readPartialEnvConfig, createClient,
|
|
15
|
+
import { readPartialEnvConfig, createClient, ContentRoots } from '@remkoj/optimizely-cms-api';
|
|
7
16
|
import yargs from 'yargs';
|
|
8
17
|
import chalk from 'chalk';
|
|
9
18
|
import figures from 'figures';
|
|
10
19
|
import Table from 'cli-table3';
|
|
11
20
|
import slugify from '@sindresorhus/slugify';
|
|
12
21
|
import { diff } from 'deep-object-diff';
|
|
13
|
-
import
|
|
22
|
+
import fs$1 from 'node:fs/promises';
|
|
14
23
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
15
24
|
import createDeepMerge from '@fastify/deepmerge';
|
|
16
25
|
import { Ajv } from 'ajv';
|
|
@@ -209,10 +218,12 @@ function getStyleFilePathsSync(displayTemplate, contentBaseType = '_Component',
|
|
|
209
218
|
target = displayTemplate.nodeType;
|
|
210
219
|
break;
|
|
211
220
|
case 'component':
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
{
|
|
222
|
+
const baseType = contentBaseType;
|
|
223
|
+
target = displayTemplate.contentType;
|
|
224
|
+
groupPath = path.join(keyToSlug(baseType, { stripLeadingUnderscore: true }), keyToSlug(displayTemplate.contentType, { stripLeadingGroup: true }));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
216
227
|
default:
|
|
217
228
|
throw new Error(`Unsupported DisplayTemplate target for ${displayTemplate.key}`);
|
|
218
229
|
}
|
|
@@ -230,7 +241,7 @@ function getStyleFilePathsSync(displayTemplate, contentBaseType = '_Component',
|
|
|
230
241
|
};
|
|
231
242
|
}
|
|
232
243
|
async function getStyleFilePaths(definition, opts) {
|
|
233
|
-
let defintionBaseType
|
|
244
|
+
let defintionBaseType;
|
|
234
245
|
if (definition.contentType && !opts.contentBaseType && opts.client) {
|
|
235
246
|
const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
|
|
236
247
|
defintionBaseType = contentType?.baseType;
|
|
@@ -340,7 +351,7 @@ function isFolder(contentType) {
|
|
|
340
351
|
* Test if the provided content type is a system type
|
|
341
352
|
*
|
|
342
353
|
* @param contentType The ContentType to test
|
|
343
|
-
* @returns `true` if the ContentType is a
|
|
354
|
+
* @returns `true` if the ContentType is a system type, `false` otherwise
|
|
344
355
|
* @see {@link IntegrationApi.ContentType}
|
|
345
356
|
*/
|
|
346
357
|
function isSystemType(contentType) {
|
|
@@ -348,12 +359,30 @@ function isSystemType(contentType) {
|
|
|
348
359
|
return false;
|
|
349
360
|
return contentType.source === 'system';
|
|
350
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Type guard that returns `true` when `toTest` is a non-empty array.
|
|
364
|
+
*
|
|
365
|
+
* @param toTest The value to inspect.
|
|
366
|
+
* @returns `true` when `toTest` is an `Array` with at least one element.
|
|
367
|
+
*/
|
|
351
368
|
function isNonEmptyArray(toTest) {
|
|
352
369
|
return Array.isArray(toTest) && toTest.length > 0;
|
|
353
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Type guard that returns `true` when `toTest` is `null`, `undefined`, or an empty array.
|
|
373
|
+
*
|
|
374
|
+
* @param toTest The value to inspect.
|
|
375
|
+
* @returns `true` when `toTest` is `null` or an `Array` with no elements.
|
|
376
|
+
*/
|
|
354
377
|
function isEmptyArray(toTest) {
|
|
355
378
|
return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
|
|
356
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Type guard that returns `true` when `toTest` is neither `null` nor `undefined`.
|
|
382
|
+
*
|
|
383
|
+
* @param toTest The value to inspect.
|
|
384
|
+
* @returns `true` when `toTest` is a defined, non-null value of type `T`.
|
|
385
|
+
*/
|
|
357
386
|
function isDefined(toTest) {
|
|
358
387
|
return toTest !== null && toTest !== undefined;
|
|
359
388
|
}
|
|
@@ -441,25 +470,28 @@ async function* getAllContentTypes(client, debug = false, pageSize = 25) {
|
|
|
441
470
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
|
|
442
471
|
let requestPageSize = pageSize;
|
|
443
472
|
let requestPageIndex = 0;
|
|
444
|
-
let totalItemCount
|
|
445
|
-
let totalPages
|
|
473
|
+
let totalItemCount;
|
|
474
|
+
let totalPages;
|
|
446
475
|
do {
|
|
447
|
-
const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((
|
|
476
|
+
const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch(() => {
|
|
448
477
|
return {
|
|
449
478
|
items: [],
|
|
450
479
|
totalItemCount: 0,
|
|
480
|
+
totalCount: 0,
|
|
451
481
|
pageIndex: requestPageIndex,
|
|
452
482
|
pageSize: requestPageSize
|
|
453
483
|
};
|
|
454
484
|
});
|
|
485
|
+
console.log(resultsPage);
|
|
455
486
|
// Calculate fields for next page
|
|
456
|
-
|
|
487
|
+
//@ts-expect-error There's a difference between the SaaS & PaaS API, hence we're ignoring the next line
|
|
488
|
+
totalItemCount = resultsPage.totalItemCount ?? resultsPage.totalCount ?? 0;
|
|
457
489
|
requestPageSize = resultsPage.pageSize;
|
|
458
490
|
requestPageIndex = resultsPage.pageIndex + 1;
|
|
459
491
|
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
460
492
|
// Debug output
|
|
461
493
|
if (debug)
|
|
462
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
494
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page, ${totalItemCount} items available)\n`));
|
|
463
495
|
// Yield items
|
|
464
496
|
for (const contentType of (resultsPage.items ?? [])) {
|
|
465
497
|
yield contentType;
|
|
@@ -543,10 +575,10 @@ async function getStyles(client, args, pageSize = 25) {
|
|
|
543
575
|
async function* getAllStyles(client, debug = false, pageSize = 5) {
|
|
544
576
|
let requestPageSize = pageSize;
|
|
545
577
|
let requestPageIndex = 0;
|
|
546
|
-
let totalItemCount
|
|
547
|
-
let totalPages
|
|
578
|
+
let totalItemCount;
|
|
579
|
+
let totalPages;
|
|
548
580
|
do {
|
|
549
|
-
const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((
|
|
581
|
+
const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch(() => {
|
|
550
582
|
return {
|
|
551
583
|
items: [],
|
|
552
584
|
totalItemCount: 0,
|
|
@@ -555,13 +587,14 @@ async function* getAllStyles(client, debug = false, pageSize = 5) {
|
|
|
555
587
|
};
|
|
556
588
|
});
|
|
557
589
|
// Calculate fields for next page
|
|
558
|
-
|
|
590
|
+
//@ts-expect-error Difference between PaaS & SaaS
|
|
591
|
+
totalItemCount = resultsPage.totalItemCount ?? resultsPage.totalCount ?? 0;
|
|
559
592
|
requestPageSize = resultsPage.pageSize;
|
|
560
593
|
requestPageIndex = resultsPage.pageIndex + 1;
|
|
561
594
|
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
562
595
|
// Debug output
|
|
563
596
|
if (debug)
|
|
564
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
597
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page, ${totalItemCount} items available)\n`));
|
|
565
598
|
// Yield items
|
|
566
599
|
for (const displayTemplate of (resultsPage.items ?? [])) {
|
|
567
600
|
yield displayTemplate;
|
|
@@ -624,7 +657,7 @@ async function toTypeFilesList(displayTemplates, client, basePath) {
|
|
|
624
657
|
return new TypeFilesList();
|
|
625
658
|
}
|
|
626
659
|
|
|
627
|
-
function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource) {
|
|
660
|
+
function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource, atomicKeys = []) {
|
|
628
661
|
if (value === undefined || value === null)
|
|
629
662
|
return null;
|
|
630
663
|
if (Array.isArray(value))
|
|
@@ -632,8 +665,15 @@ function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource)
|
|
|
632
665
|
if (typeof value !== 'object')
|
|
633
666
|
return value;
|
|
634
667
|
const output = Object.entries(value).reduce((acc, [key, entry]) => {
|
|
635
|
-
if (!omitKeys.includes(key))
|
|
636
|
-
|
|
668
|
+
if (!omitKeys.includes(key)) {
|
|
669
|
+
if (atomicKeys.includes(key) && requiredSource !== undefined) {
|
|
670
|
+
// Include the full value from newValue verbatim — do not recurse into the diff
|
|
671
|
+
acc[key] = requiredSource[key];
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
acc[key] = normalizeMergePatch(entry, omitKeys);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
637
677
|
return acc;
|
|
638
678
|
}, {});
|
|
639
679
|
const withRequiredKeys = requiredKeys.reduce((acc, key) => {
|
|
@@ -698,13 +738,18 @@ function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource)
|
|
|
698
738
|
* regardless of whether they changed.
|
|
699
739
|
* @param options.requiredFields - Keys that should always appear in the generated patch,
|
|
700
740
|
* using values from `newValue`.
|
|
741
|
+
* @param options.atomicFields - Keys whose values are always included verbatim when they
|
|
742
|
+
* change, rather than being recursively patched. Use for
|
|
743
|
+
* record- or dictionary-typed properties (e.g.
|
|
744
|
+
* `{ [key: string]: SomeObject }`) and arrays of objects.
|
|
701
745
|
* @returns A {@link MergePatch} object ready to be serialised as an `application/merge-patch+json` body.
|
|
702
746
|
*/
|
|
703
747
|
function generatePatch(currentValue, newValue, options = {}) {
|
|
704
748
|
const requiredKeys = options.requiredFields ?? [];
|
|
705
749
|
const omitKeys = (options.readonlyFields ?? []).filter(key => !requiredKeys.includes(key));
|
|
750
|
+
const atomicKeys = (options.atomicFields ?? []);
|
|
706
751
|
const patch = diff(currentValue, newValue);
|
|
707
|
-
return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue);
|
|
752
|
+
return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue, atomicKeys);
|
|
708
753
|
}
|
|
709
754
|
/**
|
|
710
755
|
* Extracts all field paths from a {@link MergePatch} as dot-separated strings.
|
|
@@ -776,18 +821,17 @@ const StylesPushCommand = {
|
|
|
776
821
|
// Create / Replace the current template
|
|
777
822
|
const newTemplate = await (currentTemplate ? (async () => {
|
|
778
823
|
const patch = generatePatch(currentTemplate, styleDefinition, {
|
|
779
|
-
readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy']
|
|
824
|
+
readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy'],
|
|
825
|
+
atomicFields: ['settings']
|
|
780
826
|
});
|
|
781
827
|
if (!path || Object.entries(patch).length === 0)
|
|
782
828
|
return currentTemplate;
|
|
783
|
-
// @ts-expect-error There's a mis-match between the logic in the CMS and the contents of the
|
|
784
|
-
// OpenAPI Spec file.
|
|
785
829
|
return client.displayTemplatesPatch({ path: { key: styleKey }, body: patch });
|
|
786
830
|
})() :
|
|
787
831
|
client.displayTemplatesCreate({ body: styleDefinition }));
|
|
788
|
-
const missedFields = generatePatch(newTemplate, currentTemplate, {
|
|
832
|
+
const missedFields = newTemplate ? generatePatch(newTemplate, currentTemplate, {
|
|
789
833
|
readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy']
|
|
790
|
-
});
|
|
834
|
+
}) : undefined;
|
|
791
835
|
if (missedFields && Object.keys(missedFields).length > 0)
|
|
792
836
|
throw new Error(`The Display template ${styleKey} failed to update properties: ${getPatchFields(missedFields).join('; ')}`);
|
|
793
837
|
return newTemplate;
|
|
@@ -804,16 +848,21 @@ const StylesPushCommand = {
|
|
|
804
848
|
});
|
|
805
849
|
results.forEach(result => {
|
|
806
850
|
if (result.status === 'fulfilled') {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
851
|
+
if (isDefined(result.value)) {
|
|
852
|
+
const tpl = result.value;
|
|
853
|
+
styles.push([
|
|
854
|
+
tpl.displayName,
|
|
855
|
+
tpl.key,
|
|
856
|
+
tpl.isDefault ? figures.tick : figures.cross,
|
|
857
|
+
tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
process.stderr.write(`DisplayTemplate pushed without errors, but no result was returned.\n`);
|
|
862
|
+
}
|
|
814
863
|
}
|
|
815
864
|
else {
|
|
816
|
-
process.stderr.write(`Error processing DisplayTemplate: ${result.reason}\n`);
|
|
865
|
+
process.stderr.write(`Error processing DisplayTemplate: ${result.reason ?? "n"}\n`);
|
|
817
866
|
}
|
|
818
867
|
});
|
|
819
868
|
process.stdout.write(styles.toString() + "\n");
|
|
@@ -826,7 +875,7 @@ function tryReadJsonFile(filePath, debug = false) {
|
|
|
826
875
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
|
|
827
876
|
return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
|
|
828
877
|
}
|
|
829
|
-
catch
|
|
878
|
+
catch {
|
|
830
879
|
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
|
|
831
880
|
}
|
|
832
881
|
return undefined;
|
|
@@ -837,10 +886,6 @@ const StylesListCommand = {
|
|
|
837
886
|
describe: "List Visual Builder style definitions from the CMS",
|
|
838
887
|
handler: async (args) => {
|
|
839
888
|
const client = createCmsClient(args);
|
|
840
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
|
|
841
|
-
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
889
|
const { all: results } = await getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [], all: false });
|
|
845
890
|
const styles = new Table({
|
|
846
891
|
head: [
|
|
@@ -893,7 +938,7 @@ const StylesPullCommand = {
|
|
|
893
938
|
const displayTemplateGroup = styleFiles.get(groupIdentifier);
|
|
894
939
|
for (const { file: filePath, data: displayTemplate } of (displayTemplateGroup?.templates || [])) {
|
|
895
940
|
// Write JSON to disk
|
|
896
|
-
|
|
941
|
+
const updatedJson = await createDisplayTemplateFile(displayTemplate, filePath, force, cfg.debug);
|
|
897
942
|
if (updatedJson)
|
|
898
943
|
updatedTemplates.push(displayTemplate.key);
|
|
899
944
|
}
|
|
@@ -940,7 +985,7 @@ async function createDisplayTemplateFile(displayTemplate, filePath, force = fals
|
|
|
940
985
|
async function createDisplayTemplateHelper(typeFile, typeFileId, force = false, debug = false) {
|
|
941
986
|
const prefix = '//not-modified - Remove this line when making change to prevent it from being updated by the CLI tools';
|
|
942
987
|
const { filePath: typeFilePath, templates } = typeFile;
|
|
943
|
-
const shouldWrite = await
|
|
988
|
+
const shouldWrite = await fs$1.readFile(typeFilePath, { encoding: "utf-8" }).then(data => data.startsWith('//not-modified')).catch((e) => {
|
|
944
989
|
if (e?.code === 'ENOENT')
|
|
945
990
|
return true;
|
|
946
991
|
if (debug)
|
|
@@ -1004,7 +1049,7 @@ export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : prop
|
|
|
1004
1049
|
}`);
|
|
1005
1050
|
});
|
|
1006
1051
|
}
|
|
1007
|
-
void await
|
|
1052
|
+
void await fs$1.writeFile(typeFilePath, prefix + "\n" + imports.join("\n") + "\n\n" + typeContents.join("\n"));
|
|
1008
1053
|
return true;
|
|
1009
1054
|
}
|
|
1010
1055
|
|
|
@@ -1132,12 +1177,12 @@ const StylesDeleteCommand = {
|
|
|
1132
1177
|
// Remove style file
|
|
1133
1178
|
if (args.withStyleFile) {
|
|
1134
1179
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing *.opti-style.json file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
|
|
1135
|
-
await
|
|
1180
|
+
await fs$1.rm(styleFilePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1136
1181
|
if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
|
|
1137
1182
|
return;
|
|
1138
1183
|
throw e;
|
|
1139
1184
|
});
|
|
1140
|
-
await
|
|
1185
|
+
await fs$1.rmdir(itemPath, { recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1141
1186
|
if (e?.code === 'ENOTEMPTY') {
|
|
1142
1187
|
process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${itemPath} manually if needed\n`));
|
|
1143
1188
|
return;
|
|
@@ -1153,12 +1198,12 @@ const StylesDeleteCommand = {
|
|
|
1153
1198
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing/updating displayTemplates.ts file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
|
|
1154
1199
|
const remainingTemplates = styleHelpers[targetType].templates.filter(x => !keysToDelete.includes(x.key));
|
|
1155
1200
|
if (remainingTemplates.length === 0) {
|
|
1156
|
-
await
|
|
1201
|
+
await fs$1.rm(styleHelpers[targetType].filePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1157
1202
|
if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
|
|
1158
1203
|
return;
|
|
1159
1204
|
throw e;
|
|
1160
1205
|
});
|
|
1161
|
-
await
|
|
1206
|
+
await fs$1.rmdir(typesPath, { recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1162
1207
|
if (e?.code === 'ENOTEMPTY') {
|
|
1163
1208
|
process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${typesPath} manually if needed\n`));
|
|
1164
1209
|
return;
|
|
@@ -1716,10 +1761,11 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
|
1716
1761
|
|
|
1717
1762
|
export default ${varName}`,
|
|
1718
1763
|
// Template for all experience component types
|
|
1719
|
-
experience: (contentType, varName, displayTemplate) => `import
|
|
1764
|
+
experience: (contentType, varName, displayTemplate) => `import 'server-only'
|
|
1765
|
+
import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1720
1766
|
import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
|
|
1721
1767
|
import { getFragmentData } from '@/gql/fragment-masking'
|
|
1722
|
-
import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1768
|
+
import { OptimizelyComposition, isNode, CmsEditable, type ServerContext } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1723
1769
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1724
1770
|
import { getSdk } from "@/gql/client"
|
|
1725
1771
|
|
|
@@ -1735,7 +1781,7 @@ import { getSdk } from "@/gql/client"
|
|
|
1735
1781
|
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1736
1782
|
*/
|
|
1737
1783
|
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
|
|
1738
|
-
if (ctx) ctx
|
|
1784
|
+
if (ctx) (ctx as ServerContext).setEditableContentIsExperience(true)
|
|
1739
1785
|
const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
|
|
1740
1786
|
const componentName = '${contentType.displayName}'
|
|
1741
1787
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
@@ -1915,8 +1961,8 @@ const NextJsFactoryCommand = {
|
|
|
1915
1961
|
// Build factory / component structure
|
|
1916
1962
|
const componentFactoryDefintions = new Map();
|
|
1917
1963
|
components.forEach(component => {
|
|
1918
|
-
const componentDir = path.dirname(path.join(...component));
|
|
1919
|
-
path.basename(path.join(...component));
|
|
1964
|
+
const componentDir = path.dirname(path.posix.join(...component));
|
|
1965
|
+
// const componentFile = path.basename(path.posix.join(...component));
|
|
1920
1966
|
// Determine component target
|
|
1921
1967
|
const componentKey = getComponentKey(component, basePath);
|
|
1922
1968
|
let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
|
|
@@ -2050,12 +2096,12 @@ function shouldWriteFactory(factoryFile, force = false, debug = false) {
|
|
|
2050
2096
|
function processName(input) {
|
|
2051
2097
|
if (input == ROOT_FACTORY_KEY)
|
|
2052
2098
|
return "Cms";
|
|
2053
|
-
const nameSegements = input.split(/[
|
|
2099
|
+
const nameSegements = input.split(/[-_]/g);
|
|
2054
2100
|
return nameSegements.map(ucFirst).join('');
|
|
2055
2101
|
}
|
|
2056
2102
|
function generateFactory(factoryInfo, factoryKey) {
|
|
2057
2103
|
// Get the factory name
|
|
2058
|
-
const factoryName = factoryKey.split(path.sep).map(processName).join("") + "Factory";
|
|
2104
|
+
const factoryName = factoryKey.split(path.posix.sep).map(processName).join("") + "Factory";
|
|
2059
2105
|
// Get the components and sub-factories, sorted by key to minimize changes between runs
|
|
2060
2106
|
const components = [...factoryInfo.entries].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
|
|
2061
2107
|
const subFactories = [...factoryInfo.subfactories].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
|
|
@@ -2371,12 +2417,12 @@ const SchemaDownloadCommand = {
|
|
|
2371
2417
|
const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
|
|
2372
2418
|
const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
|
|
2373
2419
|
process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
|
|
2374
|
-
const schemaDirInfo = await
|
|
2420
|
+
const schemaDirInfo = await fs$1.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
|
|
2375
2421
|
return undefined;
|
|
2376
2422
|
else
|
|
2377
2423
|
throw e; });
|
|
2378
2424
|
if (!schemaDirInfo) {
|
|
2379
|
-
await
|
|
2425
|
+
await fs$1.mkdir(fullSchemaDir, { recursive: true });
|
|
2380
2426
|
}
|
|
2381
2427
|
else if (!schemaDirInfo.isDirectory()) {
|
|
2382
2428
|
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
|
|
@@ -2389,7 +2435,7 @@ const SchemaDownloadCommand = {
|
|
|
2389
2435
|
const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
|
|
2390
2436
|
const relativePath = path.relative(projectPath, schemaFile);
|
|
2391
2437
|
try {
|
|
2392
|
-
await
|
|
2438
|
+
await fs$1.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
|
|
2393
2439
|
encoding: "utf-8",
|
|
2394
2440
|
flag: force ? 'w' : 'wx'
|
|
2395
2441
|
});
|
|
@@ -2639,7 +2685,7 @@ const MigrateCommand = {
|
|
|
2639
2685
|
const operations = [];
|
|
2640
2686
|
for (const file of files) {
|
|
2641
2687
|
const fullPath = path.join(componentsPath, file);
|
|
2642
|
-
const pathInfo = await
|
|
2688
|
+
const pathInfo = await fs$1.stat(fullPath);
|
|
2643
2689
|
if (pathInfo.isDirectory()) { // We only need to rename directories
|
|
2644
2690
|
const dirName = path.basename(fullPath);
|
|
2645
2691
|
const dirParent = path.dirname(fullPath);
|
|
@@ -2662,13 +2708,260 @@ const MigrateCommand = {
|
|
|
2662
2708
|
}
|
|
2663
2709
|
}
|
|
2664
2710
|
for (const operation of operations) {
|
|
2665
|
-
void await
|
|
2711
|
+
void await fs$1.rename(operation.from, operation.to);
|
|
2666
2712
|
}
|
|
2667
2713
|
NextJsFactoryCommand.handler(args);
|
|
2668
2714
|
}
|
|
2669
2715
|
};
|
|
2670
2716
|
|
|
2671
|
-
|
|
2717
|
+
const SECTION_START = '<!-- @remkoj/optimizely-packages:start -->';
|
|
2718
|
+
const SECTION_END = '<!-- @remkoj/optimizely-packages:end -->';
|
|
2719
|
+
const ProjectAiCommand = {
|
|
2720
|
+
command: 'project:ai',
|
|
2721
|
+
describe: 'Create or update AI assistant configuration files (AGENTS.md, CLAUDE.md, GitHub Copilot, Cursor) so installed @remkoj package documentation is available to the model',
|
|
2722
|
+
builder: (yargs) => {
|
|
2723
|
+
yargs.option('force', {
|
|
2724
|
+
alias: 'f',
|
|
2725
|
+
description: 'Overwrite existing files entirely instead of merging into existing content',
|
|
2726
|
+
boolean: true,
|
|
2727
|
+
type: 'boolean',
|
|
2728
|
+
demandOption: false,
|
|
2729
|
+
default: false,
|
|
2730
|
+
});
|
|
2731
|
+
yargs.option('tools', {
|
|
2732
|
+
alias: 't',
|
|
2733
|
+
description: 'AI tools to configure',
|
|
2734
|
+
array: true,
|
|
2735
|
+
type: 'string',
|
|
2736
|
+
choices: ['agents', 'claude', 'copilot', 'cursor'],
|
|
2737
|
+
demandOption: false,
|
|
2738
|
+
default: ['agents', 'claude', 'copilot', 'cursor'],
|
|
2739
|
+
});
|
|
2740
|
+
return yargs;
|
|
2741
|
+
},
|
|
2742
|
+
handler: async (args) => {
|
|
2743
|
+
const projectPath = path.isAbsolute(args.path)
|
|
2744
|
+
? args.path
|
|
2745
|
+
: path.normalize(path.join(process.cwd(), args.path));
|
|
2746
|
+
const force = args.force ?? false;
|
|
2747
|
+
const tools = args.tools ?? ['agents', 'claude', 'copilot', 'cursor'];
|
|
2748
|
+
// Discover installed @remkoj packages that include AGENTS.md
|
|
2749
|
+
process.stdout.write(`\n${figures.arrowRight} Scanning for @remkoj packages with AGENTS.md\n`);
|
|
2750
|
+
const packages = await findRemkojPackages(projectPath);
|
|
2751
|
+
if (packages.length === 0) {
|
|
2752
|
+
process.stderr.write(chalk.yellowBright(`\n${figures.warning} No @remkoj packages with AGENTS.md found in node_modules — run \`npm install\` first.\n`));
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
for (const pkg of packages) {
|
|
2756
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${chalk.bold(pkg.name)}\n`);
|
|
2757
|
+
}
|
|
2758
|
+
if (tools.includes('agents')) {
|
|
2759
|
+
process.stdout.write(`\n${figures.arrowRight} Updating AGENTS.md\n`);
|
|
2760
|
+
await updateWithSectionMarkers(path.join(projectPath, 'AGENTS.md'), buildAgentsMdSection(packages, projectPath), buildAgentsMdPreamble(), force, projectPath);
|
|
2761
|
+
}
|
|
2762
|
+
if (tools.includes('claude')) {
|
|
2763
|
+
process.stdout.write(`\n${figures.arrowRight} Writing CLAUDE.md\n`);
|
|
2764
|
+
await writeNewFileOnly(path.join(projectPath, 'CLAUDE.md'), `# CLAUDE.md\n\nSee [AGENTS.md](./AGENTS.md) for guidance on working with the Optimizely DXP packages in this project.\n`, force, projectPath);
|
|
2765
|
+
}
|
|
2766
|
+
if (tools.includes('copilot')) {
|
|
2767
|
+
process.stdout.write(`\n${figures.arrowRight} Updating GitHub Copilot instruction files\n`);
|
|
2768
|
+
await updateCopilotInstructions(projectPath, packages);
|
|
2769
|
+
}
|
|
2770
|
+
if (tools.includes('cursor')) {
|
|
2771
|
+
process.stdout.write(`\n${figures.arrowRight} Writing Cursor rules\n`);
|
|
2772
|
+
const rulesDir = path.join(projectPath, '.cursor', 'rules');
|
|
2773
|
+
await ensureDir(rulesDir);
|
|
2774
|
+
await writeCursorRules(path.join(rulesDir, 'optimizely-packages.mdc'), packages, projectPath);
|
|
2775
|
+
}
|
|
2776
|
+
process.stdout.write(`\n${chalk.greenBright(chalk.bold(figures.tick))} AI assistant configuration updated successfully\n`);
|
|
2777
|
+
},
|
|
2778
|
+
};
|
|
2779
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
2780
|
+
async function findRemkojPackages(projectPath) {
|
|
2781
|
+
const scopeDir = path.join(projectPath, 'node_modules', '@remkoj');
|
|
2782
|
+
const packages = [];
|
|
2783
|
+
try {
|
|
2784
|
+
const dirs = await fs$1.readdir(scopeDir);
|
|
2785
|
+
for (const dir of dirs) {
|
|
2786
|
+
const agentsPath = path.join(scopeDir, dir, 'AGENTS.md');
|
|
2787
|
+
try {
|
|
2788
|
+
await fs$1.access(agentsPath);
|
|
2789
|
+
packages.push({ name: `@remkoj/${dir}`, agentsPath });
|
|
2790
|
+
}
|
|
2791
|
+
catch {
|
|
2792
|
+
// Package has no AGENTS.md — skip silently
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
catch {
|
|
2797
|
+
// @remkoj scope directory not found in node_modules
|
|
2798
|
+
}
|
|
2799
|
+
return packages;
|
|
2800
|
+
}
|
|
2801
|
+
function wrapSection(content) {
|
|
2802
|
+
return `${SECTION_START}\n${content}\n${SECTION_END}`;
|
|
2803
|
+
}
|
|
2804
|
+
function buildReferenceList(packages, projectPath) {
|
|
2805
|
+
return packages
|
|
2806
|
+
.map((pkg) => {
|
|
2807
|
+
const rel = path.relative(projectPath, pkg.agentsPath).replace(/\\/g, '/');
|
|
2808
|
+
return `- [\`${pkg.name}\`](${rel})`;
|
|
2809
|
+
})
|
|
2810
|
+
.join('\n');
|
|
2811
|
+
}
|
|
2812
|
+
function buildAgentsMdPreamble() {
|
|
2813
|
+
return `# AI Agent Documentation — Optimizely DXP Packages
|
|
2814
|
+
|
|
2815
|
+
The section below is auto-generated by \`opti-cms project:ai\`. Re-run after installing or upgrading \`@remkoj\` packages.
|
|
2816
|
+
|
|
2817
|
+
`;
|
|
2818
|
+
}
|
|
2819
|
+
function buildAgentsMdSection(packages, projectPath) {
|
|
2820
|
+
return `The following \`@remkoj\` packages are installed. Read their \`AGENTS.md\` for documentation:\n\n${buildReferenceList(packages, projectPath)}`;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Creates or updates the auto-generated section delimited by SECTION_START/SECTION_END markers.
|
|
2824
|
+
*
|
|
2825
|
+
* - File absent: creates it with preamble + section.
|
|
2826
|
+
* - File present with markers: replaces the section in-place.
|
|
2827
|
+
* - File present without markers: appends the section (preserves existing content).
|
|
2828
|
+
* - force=true: always replaces the entire file with preamble + section.
|
|
2829
|
+
*/
|
|
2830
|
+
async function updateWithSectionMarkers(filePath, sectionContent, preamble, force, projectPath) {
|
|
2831
|
+
const rel = path.relative(projectPath, filePath);
|
|
2832
|
+
const section = wrapSection(sectionContent);
|
|
2833
|
+
let existing;
|
|
2834
|
+
try {
|
|
2835
|
+
existing = await fs$1.readFile(filePath, 'utf-8');
|
|
2836
|
+
}
|
|
2837
|
+
catch {
|
|
2838
|
+
// File does not exist — will be created
|
|
2839
|
+
}
|
|
2840
|
+
if (!existing || force) {
|
|
2841
|
+
await fs$1.writeFile(filePath, preamble + section + '\n', 'utf-8');
|
|
2842
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${existing ? 'Replaced' : 'Created'} ${rel}\n`);
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (existing.includes(SECTION_START) && existing.includes(SECTION_END)) {
|
|
2846
|
+
const before = existing.slice(0, existing.indexOf(SECTION_START));
|
|
2847
|
+
const after = existing.slice(existing.indexOf(SECTION_END) + SECTION_END.length);
|
|
2848
|
+
await fs$1.writeFile(filePath, before + section + after, 'utf-8');
|
|
2849
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Updated section in ${rel}\n`);
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
await fs$1.writeFile(filePath, existing.trimEnd() + '\n\n' + section + '\n', 'utf-8');
|
|
2853
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Appended section to ${rel}\n`);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Writes a file only when it does not already exist.
|
|
2858
|
+
* When force=true the file is always (re)written.
|
|
2859
|
+
*/
|
|
2860
|
+
async function writeNewFileOnly(filePath, content, force, projectPath) {
|
|
2861
|
+
const rel = path.relative(projectPath, filePath);
|
|
2862
|
+
let exists = false;
|
|
2863
|
+
try {
|
|
2864
|
+
await fs$1.access(filePath);
|
|
2865
|
+
exists = true;
|
|
2866
|
+
}
|
|
2867
|
+
catch {
|
|
2868
|
+
// File does not exist
|
|
2869
|
+
}
|
|
2870
|
+
if (exists && !force) {
|
|
2871
|
+
process.stdout.write(` ${chalk.yellowBright(figures.info)} ${rel} already exists — use --force to overwrite\n`);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2875
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${exists ? 'Replaced' : 'Created'} ${rel}\n`);
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Writes one `.vscode/instructions/remkoj-<name>.instructions.md` file per package,
|
|
2879
|
+
* each containing a `#file:` reference to the package's AGENTS.md in node_modules.
|
|
2880
|
+
* VS Code Copilot resolves `#file:` references at query time, so content stays
|
|
2881
|
+
* current after a package upgrade without re-running this command.
|
|
2882
|
+
*
|
|
2883
|
+
* Also removes the deprecated `github.copilot.chat.codeGeneration.instructions`
|
|
2884
|
+
* key from `.vscode/settings.json` if it was written by a previous run.
|
|
2885
|
+
*/
|
|
2886
|
+
async function updateCopilotInstructions(projectPath, packages) {
|
|
2887
|
+
const instructionsDir = path.join(projectPath, '.vscode', 'instructions');
|
|
2888
|
+
await ensureDir(instructionsDir);
|
|
2889
|
+
// Remove stale remkoj instruction files from previous runs
|
|
2890
|
+
const FILE_PREFIX = 'remkoj-';
|
|
2891
|
+
try {
|
|
2892
|
+
const existing = await fs$1.readdir(instructionsDir);
|
|
2893
|
+
for (const f of existing) {
|
|
2894
|
+
if (f.startsWith(FILE_PREFIX) && f.endsWith('.instructions.md')) {
|
|
2895
|
+
await fs$1.unlink(path.join(instructionsDir, f));
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
catch {
|
|
2900
|
+
// Directory unreadable — ignore
|
|
2901
|
+
}
|
|
2902
|
+
// Write one instruction file per package using a #file: reference
|
|
2903
|
+
for (const pkg of packages) {
|
|
2904
|
+
const safeName = pkg.name.replace('@remkoj/', '');
|
|
2905
|
+
const filePath = path.join(instructionsDir, `${FILE_PREFIX}${safeName}.instructions.md`);
|
|
2906
|
+
const ref = path.relative(projectPath, pkg.agentsPath).replace(/\\/g, '/');
|
|
2907
|
+
const content = `---\napplyTo: "**"\n---\n\n#file:${ref}\n`;
|
|
2908
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2909
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Wrote ${path.relative(projectPath, filePath)}\n`);
|
|
2910
|
+
}
|
|
2911
|
+
// Clean up the deprecated settings key written by previous versions of this command
|
|
2912
|
+
await removeDeprecatedCopilotSetting(projectPath);
|
|
2913
|
+
}
|
|
2914
|
+
async function removeDeprecatedCopilotSetting(projectPath) {
|
|
2915
|
+
const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
|
|
2916
|
+
let settings;
|
|
2917
|
+
try {
|
|
2918
|
+
settings = JSON.parse(await fs$1.readFile(settingsFile, 'utf-8'));
|
|
2919
|
+
}
|
|
2920
|
+
catch {
|
|
2921
|
+
return; // File absent or invalid — nothing to clean up
|
|
2922
|
+
}
|
|
2923
|
+
const key = 'github.copilot.chat.codeGeneration.instructions';
|
|
2924
|
+
if (!(key in settings))
|
|
2925
|
+
return;
|
|
2926
|
+
const remaining = settings[key].filter((e) => !e.file?.startsWith('node_modules/@remkoj/'));
|
|
2927
|
+
if (remaining.length === 0) {
|
|
2928
|
+
delete settings[key];
|
|
2929
|
+
}
|
|
2930
|
+
else {
|
|
2931
|
+
settings[key] = remaining;
|
|
2932
|
+
}
|
|
2933
|
+
await fs$1.writeFile(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
2934
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Removed deprecated setting from ${path.relative(projectPath, settingsFile)}\n`);
|
|
2935
|
+
}
|
|
2936
|
+
/** Writes the Cursor rules file with a reference list; always regenerated since it is fully derived from node_modules. */
|
|
2937
|
+
async function writeCursorRules(filePath, packages, projectPath) {
|
|
2938
|
+
const rel = path.relative(projectPath, filePath);
|
|
2939
|
+
let exists = false;
|
|
2940
|
+
try {
|
|
2941
|
+
await fs$1.access(filePath);
|
|
2942
|
+
exists = true;
|
|
2943
|
+
}
|
|
2944
|
+
catch {
|
|
2945
|
+
// File does not exist
|
|
2946
|
+
}
|
|
2947
|
+
const list = buildReferenceList(packages, projectPath);
|
|
2948
|
+
const content = [
|
|
2949
|
+
'---',
|
|
2950
|
+
'description: Documentation for @remkoj Optimizely DXP packages installed in this project',
|
|
2951
|
+
'alwaysApply: true',
|
|
2952
|
+
'---',
|
|
2953
|
+
'',
|
|
2954
|
+
wrapSection(`The following \`@remkoj\` packages are installed. Read their \`AGENTS.md\` for documentation:\n\n${list}`),
|
|
2955
|
+
'',
|
|
2956
|
+
].join('\n');
|
|
2957
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2958
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${exists ? 'Updated' : 'Created'} ${rel}\n`);
|
|
2959
|
+
}
|
|
2960
|
+
async function ensureDir(dirPath) {
|
|
2961
|
+
await fs$1.mkdir(dirPath, { recursive: true });
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
var version = "6.0.0-rc.4";
|
|
2672
2965
|
var name = "opti-cms";
|
|
2673
2966
|
var APP = {
|
|
2674
2967
|
version: version,
|
|
@@ -2685,14 +2978,6 @@ const CmsVersionCommand = {
|
|
|
2685
2978
|
const client = createCmsClient(args);
|
|
2686
2979
|
if (client.debug)
|
|
2687
2980
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading version information Optimizely CMS\n`));
|
|
2688
|
-
const versionInfo = await client.getInstanceInfo().catch((error) => {
|
|
2689
|
-
switch (error.message) {
|
|
2690
|
-
case "fetch failed":
|
|
2691
|
-
throw new Error("Unable to connect to Optimizely CMS, please verify that the CMS URL is correct");
|
|
2692
|
-
default:
|
|
2693
|
-
throw error;
|
|
2694
|
-
}
|
|
2695
|
-
});
|
|
2696
2981
|
const info = new Table({
|
|
2697
2982
|
head: [
|
|
2698
2983
|
chalk.yellow(chalk.bold("Component")),
|
|
@@ -2701,14 +2986,10 @@ const CmsVersionCommand = {
|
|
|
2701
2986
|
colWidths: [30, 60],
|
|
2702
2987
|
colAligns: ["left", "left"]
|
|
2703
2988
|
});
|
|
2704
|
-
info.push(["Base URL",
|
|
2989
|
+
info.push(["Base URL", client.cmsUrl.href]);
|
|
2705
2990
|
info.push(["Client API", client.apiVersion]);
|
|
2706
|
-
info.push(["Service API", versionInfo.apiVersion]);
|
|
2707
|
-
info.push(["CMS Build", versionInfo.cmsVersion]);
|
|
2708
|
-
info.push(["Service Build", versionInfo.serviceVersion]);
|
|
2709
2991
|
info.push(["SDK", APP.version]);
|
|
2710
2992
|
process.stdout.write(info.toString() + "\n");
|
|
2711
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
|
|
2712
2993
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
2713
2994
|
}
|
|
2714
2995
|
};
|
|
@@ -2765,7 +3046,7 @@ const CmsResetCommand = {
|
|
|
2765
3046
|
});
|
|
2766
3047
|
if (!didRemoveCustomProperties) {
|
|
2767
3048
|
const ctUrl = new URL('/ui/EPiServer.Cms.UI.Admin/default#/ContentTypes', client.cmsUrl);
|
|
2768
|
-
process.stdout.write(`${chalk.redBright(figures.cross)} Custom properties on the
|
|
3049
|
+
process.stdout.write(`${chalk.redBright(figures.cross)} Custom properties on the "Blank Section" and "Blank Experience" Content Types must be removed manually, please remove and restart command.\n`);
|
|
2769
3050
|
process.stdout.write(` ${figures.arrowRight} Content Type manager: ${ctUrl}\n`);
|
|
2770
3051
|
process.exit(1);
|
|
2771
3052
|
}
|
|
@@ -2799,7 +3080,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2799
3080
|
const children = await getContentItemChildren(client, key);
|
|
2800
3081
|
let hasError = false;
|
|
2801
3082
|
let removedCount = 0;
|
|
2802
|
-
for (
|
|
3083
|
+
for (const child of children.items)
|
|
2803
3084
|
if (child.key) {
|
|
2804
3085
|
removedCount = removedCount + await deleteContentItem(client, child.key).catch(e => {
|
|
2805
3086
|
console.error(e);
|
|
@@ -2807,7 +3088,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2807
3088
|
return 0;
|
|
2808
3089
|
});
|
|
2809
3090
|
}
|
|
2810
|
-
for (
|
|
3091
|
+
for (const child of children.assets)
|
|
2811
3092
|
if (child.key) {
|
|
2812
3093
|
removedCount = removedCount + await deleteContentItem(client, child.key).catch(e => {
|
|
2813
3094
|
console.error(e);
|
|
@@ -2826,7 +3107,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2826
3107
|
process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
|
|
2827
3108
|
return removedCount;
|
|
2828
3109
|
}
|
|
2829
|
-
const deleteResult = await client.contentDelete({ path: { key }, headers: { "cms-permanent-delete": true } }).then(r => r.key).catch((e) => {
|
|
3110
|
+
const deleteResult = await client.contentDelete({ path: { key }, headers: { "cms-permanent-delete": true } }).then(r => r ? r.key : key).catch((e) => {
|
|
2830
3111
|
if (e.status == 404)
|
|
2831
3112
|
return key;
|
|
2832
3113
|
throw e;
|
|
@@ -2850,7 +3131,6 @@ async function resetSystemTypes(client) {
|
|
|
2850
3131
|
const newType = await client.contentTypesPatch({
|
|
2851
3132
|
path: { key: systemType },
|
|
2852
3133
|
body: {
|
|
2853
|
-
key: systemType,
|
|
2854
3134
|
properties: {}
|
|
2855
3135
|
}
|
|
2856
3136
|
}).catch((e) => e.status == 404 ? undefined : null);
|
|
@@ -2862,7 +3142,7 @@ async function resetSystemTypes(client) {
|
|
|
2862
3142
|
async function removeContentTypes(client) {
|
|
2863
3143
|
const contentTypes = await getAllTypes(client);
|
|
2864
3144
|
let removedCount = 0;
|
|
2865
|
-
for (
|
|
3145
|
+
for (const contentType of contentTypes.filter(item => item.source == '' && !reservedTypes.includes(item.key))) {
|
|
2866
3146
|
if (client.debug)
|
|
2867
3147
|
process.stdout.write(` ${chalk.blueBright(figures.arrowRight)} Removing content type ${contentType.displayName} (${contentType.key})\n`);
|
|
2868
3148
|
const result = await client.contentTypesDelete({ path: { key: contentType.key } }).catch((e) => {
|
|
@@ -2903,7 +3183,7 @@ async function getAllTemplates(client, batchSize = 100) {
|
|
|
2903
3183
|
}
|
|
2904
3184
|
throw e;
|
|
2905
3185
|
});
|
|
2906
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3186
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2907
3187
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2908
3188
|
const actualItems = items.items ?? [];
|
|
2909
3189
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -2933,7 +3213,7 @@ async function getAllTypes(client, batchSize = 100) {
|
|
|
2933
3213
|
}
|
|
2934
3214
|
throw e;
|
|
2935
3215
|
});
|
|
2936
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3216
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2937
3217
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2938
3218
|
const actualItems = items.items ?? [];
|
|
2939
3219
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -2963,7 +3243,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
2963
3243
|
}
|
|
2964
3244
|
throw e;
|
|
2965
3245
|
});
|
|
2966
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3246
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2967
3247
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2968
3248
|
const actualItems = items.items ?? [];
|
|
2969
3249
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -2993,7 +3273,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
2993
3273
|
}
|
|
2994
3274
|
throw e;
|
|
2995
3275
|
});
|
|
2996
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3276
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2997
3277
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2998
3278
|
const actualItems = items.items ?? [];
|
|
2999
3279
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -3034,7 +3314,8 @@ const commands = [
|
|
|
3034
3314
|
StylesDeleteCommand,
|
|
3035
3315
|
TypesPullCommand,
|
|
3036
3316
|
TypesPushCommand,
|
|
3037
|
-
MigrateCommand
|
|
3317
|
+
MigrateCommand,
|
|
3318
|
+
ProjectAiCommand,
|
|
3038
3319
|
];
|
|
3039
3320
|
|
|
3040
3321
|
async function main() {
|