@se-studio/contentful-cms 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/bin/cms-edit.js +4 -0
  4. package/dist/bin/cms-edit.js.map +1 -1
  5. package/dist/commands/backup.d.ts +3 -0
  6. package/dist/commands/backup.d.ts.map +1 -0
  7. package/dist/commands/backup.js +143 -0
  8. package/dist/commands/backup.js.map +1 -0
  9. package/dist/commands/diff.d.ts.map +1 -1
  10. package/dist/commands/diff.js +43 -3
  11. package/dist/commands/diff.js.map +1 -1
  12. package/dist/commands/restore.d.ts +4 -0
  13. package/dist/commands/restore.d.ts.map +1 -0
  14. package/dist/commands/restore.js +146 -0
  15. package/dist/commands/restore.js.map +1 -0
  16. package/dist/commands/rtf.d.ts.map +1 -1
  17. package/dist/commands/rtf.js +107 -2
  18. package/dist/commands/rtf.js.map +1 -1
  19. package/dist/commands/skill.js +3 -3
  20. package/dist/commands/skill.js.map +1 -1
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/rtf/fromMarkdown.d.ts +6 -1
  26. package/dist/rtf/fromMarkdown.d.ts.map +1 -1
  27. package/dist/rtf/fromMarkdown.js +7 -0
  28. package/dist/rtf/fromMarkdown.js.map +1 -1
  29. package/dist/rtf/replacePlainText.d.ts +66 -0
  30. package/dist/rtf/replacePlainText.d.ts.map +1 -0
  31. package/dist/rtf/replacePlainText.js +455 -0
  32. package/dist/rtf/replacePlainText.js.map +1 -0
  33. package/dist/session/backup.d.ts +34 -0
  34. package/dist/session/backup.d.ts.map +1 -0
  35. package/dist/session/backup.js +64 -0
  36. package/dist/session/backup.js.map +1 -0
  37. package/package.json +3 -3
  38. package/skills/cms-guidelines/README.md +3 -3
  39. package/skills/core/SKILL.md +14 -0
@@ -0,0 +1,455 @@
1
+ import { BLOCKS } from '@contentful/rich-text-types';
2
+ import { markdownToRtfInlines } from './fromMarkdown.js';
3
+ import { isRtfDocument } from './utils.js';
4
+ export class RichTextReplaceError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'RichTextReplaceError';
8
+ }
9
+ }
10
+ function plainToInlines(plain) {
11
+ const node = { nodeType: 'text', value: plain, marks: [], data: {} };
12
+ return [node];
13
+ }
14
+ function resolveReplacementInlines(options) {
15
+ const { replaceMarkdown, replacePlain } = options;
16
+ if (replacePlain !== undefined && replaceMarkdown !== undefined) {
17
+ throw new RichTextReplaceError('Provide either replacePlain or replaceMarkdown, not both');
18
+ }
19
+ if (replacePlain !== undefined) {
20
+ if (replacePlain.includes('\n') || replacePlain.includes('\r')) {
21
+ throw new RichTextReplaceError('replacePlain must not contain line breaks (inline only)');
22
+ }
23
+ return plainToInlines(replacePlain);
24
+ }
25
+ if (replaceMarkdown === undefined || replaceMarkdown === '') {
26
+ throw new RichTextReplaceError('Provide replacePlain or replaceMarkdown');
27
+ }
28
+ if (replaceMarkdown.includes('\n') || replaceMarkdown.includes('\r')) {
29
+ throw new RichTextReplaceError('replaceMarkdown must not contain line breaks (inline only)');
30
+ }
31
+ return markdownToRtfInlines(replaceMarkdown);
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Quidget span protection (FR-1)
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Returns the [start, end) ranges of all top-level `{*...*}` quidget spans in `str`.
38
+ * Only non-nested spans (no inner braces) are matched. Used to prevent search matches
39
+ * from landing inside an already-inserted quidget token.
40
+ */
41
+ function getQuidgetSpans(str) {
42
+ const spans = [];
43
+ const re = /\{\*[^{}]*\*\}/g;
44
+ for (;;) {
45
+ const m = re.exec(str);
46
+ if (m === null)
47
+ break;
48
+ spans.push([m.index, m.index + m[0].length]);
49
+ }
50
+ return spans;
51
+ }
52
+ /**
53
+ * Returns true if the range [matchStart, matchEnd) is strictly contained inside any protected
54
+ * span — i.e. it starts AFTER the span start. This allows searching for a full quidget token
55
+ * itself (matchStart === spanStart is not protected) while blocking matches for shorter strings
56
+ * that land inside an existing quidget.
57
+ */
58
+ function isInsideAnySpan(matchStart, matchEnd, spans) {
59
+ for (const [s, e] of spans) {
60
+ if (matchStart > s && matchEnd <= e)
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Section filtering helpers
67
+ // ---------------------------------------------------------------------------
68
+ const HEADING_LEVEL_MAP = {
69
+ [BLOCKS.HEADING_1]: 1,
70
+ [BLOCKS.HEADING_2]: 2,
71
+ [BLOCKS.HEADING_3]: 3,
72
+ [BLOCKS.HEADING_4]: 4,
73
+ [BLOCKS.HEADING_5]: 5,
74
+ [BLOCKS.HEADING_6]: 6,
75
+ };
76
+ function isHeadingBlock(block) {
77
+ return block.nodeType in HEADING_LEVEL_MAP;
78
+ }
79
+ function getHeadingLevel(block) {
80
+ return HEADING_LEVEL_MAP[block.nodeType] ?? 0;
81
+ }
82
+ function extractBlockPlainText(block) {
83
+ const parts = [];
84
+ walkTextNodes(block.content, (_p, _i, t) => parts.push(t.value));
85
+ return parts.join('');
86
+ }
87
+ /**
88
+ * Returns a shallow Document view containing only the blocks within the named heading section.
89
+ * The section starts immediately after the matched heading and ends before the next heading of
90
+ * equal or higher level (i.e. lower or equal heading number).
91
+ *
92
+ * When multiple headings share the same text, `nth` (1-indexed, default 1) selects which to use.
93
+ * Throws {@link RichTextReplaceError} if the heading is not found or `nth` exceeds the count.
94
+ */
95
+ function resolveSectionDoc(clone, section, nth = 1) {
96
+ const blocks = clone.content;
97
+ let matchCount = 0;
98
+ for (let i = 0; i < blocks.length; i++) {
99
+ const block = blocks[i];
100
+ if (!block || !isHeadingBlock(block))
101
+ continue;
102
+ if (extractBlockPlainText(block) !== section)
103
+ continue;
104
+ matchCount++;
105
+ if (matchCount < nth)
106
+ continue;
107
+ const level = getHeadingLevel(block);
108
+ let end = blocks.length;
109
+ for (let j = i + 1; j < blocks.length; j++) {
110
+ const b = blocks[j];
111
+ if (b && isHeadingBlock(b) && getHeadingLevel(b) <= level) {
112
+ end = j;
113
+ break;
114
+ }
115
+ }
116
+ // Shallow doc wrapping the same block objects — mutations via applyAllReplacements affect clone
117
+ return {
118
+ nodeType: BLOCKS.DOCUMENT,
119
+ data: {},
120
+ content: blocks.slice(i + 1, end),
121
+ };
122
+ }
123
+ if (matchCount === 0) {
124
+ throw new RichTextReplaceError(`No heading matching section "${section}" found in document`);
125
+ }
126
+ throw new RichTextReplaceError(`Section "${section}" has ${String(matchCount)} occurrence(s) but --section-nth ${String(nth)} was requested`);
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Public API
130
+ // ---------------------------------------------------------------------------
131
+ /**
132
+ * Replaces plain-text occurrences in a Contentful rich-text document.
133
+ *
134
+ * **Matching:** By default, search runs only inside individual `text` node `value` strings.
135
+ * Use `ignoreMarks: true` to match across adjacent sibling text nodes in the same block
136
+ * (e.g. `**Annual fee: **$95` split by bold).
137
+ *
138
+ * **Quidget protection:** Matches that fall inside an existing `{*...*}` span are always
139
+ * skipped — this prevents nesting a new quidget inside an already-inserted one.
140
+ *
141
+ * **Modes:** `all` replaces every non-overlapping occurrence. `exactlyOne` requires exactly
142
+ * one occurrence in the entire document or throws {@link RichTextReplaceError}.
143
+ *
144
+ * @returns A deep-cloned document with replacements applied (input is not mutated).
145
+ */
146
+ export function replaceRtfPlainText(document, options) {
147
+ if (!isRtfDocument(document)) {
148
+ throw new RichTextReplaceError('Value is not a rich text document');
149
+ }
150
+ const { find, mode, ignoreMarks, section, sectionNth } = options;
151
+ if (find === '') {
152
+ throw new RichTextReplaceError('find must be a non-empty string');
153
+ }
154
+ const replacement = resolveReplacementInlines(options);
155
+ const clone = structuredClone(document);
156
+ const targetDoc = section !== undefined ? resolveSectionDoc(clone, section, sectionNth) : clone;
157
+ const total = ignoreMarks
158
+ ? countOccurrencesIgnoreMarks(targetDoc, find, options.tableCol)
159
+ : countOccurrencesInDocument(targetDoc, find, options.tableCol);
160
+ if (mode === 'exactlyOne' && total !== 1) {
161
+ throw new RichTextReplaceError(total === 0
162
+ ? `No occurrence of find string in document`
163
+ : `Expected exactly one occurrence of find string, found ${String(total)}`);
164
+ }
165
+ if (total === 0) {
166
+ return { document: clone, matchCount: 0 };
167
+ }
168
+ if (ignoreMarks) {
169
+ applyAllReplacementsIgnoreMarks(targetDoc, find, replacement, options.tableCol);
170
+ }
171
+ else {
172
+ applyAllReplacements(targetDoc, find, replacement, options.tableCol);
173
+ }
174
+ return { document: clone, matchCount: total };
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Block / inline root collection (FR-4: tableCol filtering)
178
+ // ---------------------------------------------------------------------------
179
+ function collectInlineRoots(doc, tableCol) {
180
+ const roots = [];
181
+ for (const block of doc.content) {
182
+ collectInlineRootsFromBlock(block, roots, tableCol);
183
+ }
184
+ return roots;
185
+ }
186
+ function collectInlineRootsFromBlock(block, roots, tableCol) {
187
+ switch (block.nodeType) {
188
+ case BLOCKS.PARAGRAPH:
189
+ case BLOCKS.HEADING_1:
190
+ case BLOCKS.HEADING_2:
191
+ case BLOCKS.HEADING_3:
192
+ case BLOCKS.HEADING_4:
193
+ case BLOCKS.HEADING_5:
194
+ case BLOCKS.HEADING_6:
195
+ roots.push(block.content);
196
+ break;
197
+ case BLOCKS.QUOTE:
198
+ for (const b of block.content) {
199
+ collectInlineRootsFromBlock(b, roots, tableCol);
200
+ }
201
+ break;
202
+ case BLOCKS.UL_LIST:
203
+ case BLOCKS.OL_LIST:
204
+ for (const item of block.content) {
205
+ collectInlineRootsFromBlock(item, roots, tableCol);
206
+ }
207
+ break;
208
+ case BLOCKS.LIST_ITEM:
209
+ for (const b of block.content) {
210
+ collectInlineRootsFromBlock(b, roots, tableCol);
211
+ }
212
+ break;
213
+ case BLOCKS.TABLE:
214
+ for (const row of block.content) {
215
+ if (row.nodeType !== BLOCKS.TABLE_ROW)
216
+ continue;
217
+ const cells = row.content;
218
+ for (let colIdx = 0; colIdx < cells.length; colIdx++) {
219
+ if (tableCol !== undefined && colIdx + 1 !== tableCol)
220
+ continue;
221
+ const cell = cells[colIdx];
222
+ if (!cell)
223
+ continue;
224
+ for (const inner of cell.content) {
225
+ collectInlineRootsFromBlock(inner, roots, tableCol);
226
+ }
227
+ }
228
+ }
229
+ break;
230
+ default:
231
+ break;
232
+ }
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Text node walking
236
+ // ---------------------------------------------------------------------------
237
+ export function walkTextNodes(inlines, fn) {
238
+ for (let i = 0; i < inlines.length; i++) {
239
+ const n = inlines[i];
240
+ if (!n)
241
+ continue;
242
+ if (n.nodeType === 'text') {
243
+ fn(inlines, i, n);
244
+ }
245
+ else {
246
+ const inl = n;
247
+ if ('content' in inl && Array.isArray(inl.content)) {
248
+ walkTextNodes(inl.content, fn);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ // ---------------------------------------------------------------------------
254
+ // Per-text-node count & replace (default mode, FR-1 quidget protection)
255
+ // ---------------------------------------------------------------------------
256
+ function countOccurrencesInDocument(doc, find, tableCol) {
257
+ let n = 0;
258
+ for (const root of collectInlineRoots(doc, tableCol)) {
259
+ walkTextNodes(root, (_parent, _index, text) => {
260
+ n += countNonOverlappingInString(text.value, find);
261
+ });
262
+ }
263
+ return n;
264
+ }
265
+ function countNonOverlappingInString(haystack, needle) {
266
+ const spans = getQuidgetSpans(haystack);
267
+ let count = 0;
268
+ let pos = 0;
269
+ while (pos <= haystack.length - needle.length) {
270
+ const i = haystack.indexOf(needle, pos);
271
+ if (i === -1)
272
+ break;
273
+ if (isInsideAnySpan(i, i + needle.length, spans)) {
274
+ // Advance past the end of the span that contains this match
275
+ const containing = spans.find(([s, e]) => i > s && i + needle.length <= e);
276
+ pos = containing ? containing[1] : i + needle.length;
277
+ continue;
278
+ }
279
+ count++;
280
+ pos = i + needle.length;
281
+ }
282
+ return count;
283
+ }
284
+ function applyAllReplacements(doc, find, replacement, tableCol) {
285
+ const byParent = new Map();
286
+ for (const root of collectInlineRoots(doc, tableCol)) {
287
+ walkTextNodes(root, (parent, index, text) => {
288
+ if (countNonOverlappingInString(text.value, find) === 0)
289
+ return;
290
+ let list = byParent.get(parent);
291
+ if (!list) {
292
+ list = [];
293
+ byParent.set(parent, list);
294
+ }
295
+ list.push(index);
296
+ });
297
+ }
298
+ for (const [parent, indices] of byParent) {
299
+ const uniqueDesc = [...new Set(indices)].sort((a, b) => b - a);
300
+ for (const idx of uniqueDesc) {
301
+ replaceAllInTextNodeAt(parent, idx, find, replacement);
302
+ }
303
+ }
304
+ }
305
+ function replaceAllInTextNodeAt(parent, index, find, replacement) {
306
+ const node = parent[index];
307
+ const v = node.value;
308
+ const spans = getQuidgetSpans(v);
309
+ const matches = [];
310
+ let pos = 0;
311
+ while (pos <= v.length - find.length) {
312
+ const i = v.indexOf(find, pos);
313
+ if (i === -1)
314
+ break;
315
+ if (isInsideAnySpan(i, i + find.length, spans)) {
316
+ const containing = spans.find(([s, e]) => i > s && i + find.length <= e);
317
+ pos = containing ? containing[1] : i + find.length;
318
+ continue;
319
+ }
320
+ matches.push(i);
321
+ pos = i + find.length;
322
+ }
323
+ if (matches.length === 0)
324
+ return;
325
+ const out = [];
326
+ let cursor = 0;
327
+ for (const m of matches) {
328
+ if (m > cursor) {
329
+ out.push({ ...node, value: v.slice(cursor, m) });
330
+ }
331
+ out.push(...replacement.map(cloneInlineTree));
332
+ cursor = m + find.length;
333
+ }
334
+ if (cursor < v.length) {
335
+ out.push({ ...node, value: v.slice(cursor) });
336
+ }
337
+ parent.splice(index, 1, ...out);
338
+ }
339
+ /** Groups consecutive direct-child text nodes in `inlines` into runs (inline nodes break runs). */
340
+ function collectDirectTextRuns(inlines) {
341
+ const runs = [];
342
+ let current = [];
343
+ let offset = 0;
344
+ const flush = () => {
345
+ if (current.length > 0) {
346
+ runs.push({
347
+ parent: inlines,
348
+ textNodes: current,
349
+ virtualStr: current.map((e) => e.node.value).join(''),
350
+ });
351
+ current = [];
352
+ offset = 0;
353
+ }
354
+ };
355
+ for (let i = 0; i < inlines.length; i++) {
356
+ const n = inlines[i];
357
+ if (!n)
358
+ continue;
359
+ if (n.nodeType === 'text') {
360
+ const start = offset;
361
+ offset += n.value.length;
362
+ current.push({ nodeIndex: i, node: n, startOffset: start, endOffset: offset });
363
+ }
364
+ else {
365
+ flush();
366
+ }
367
+ }
368
+ flush();
369
+ return runs;
370
+ }
371
+ function countOccurrencesIgnoreMarks(doc, find, tableCol) {
372
+ let n = 0;
373
+ for (const root of collectInlineRoots(doc, tableCol)) {
374
+ for (const run of collectDirectTextRuns(root)) {
375
+ n += countNonOverlappingInString(run.virtualStr, find);
376
+ }
377
+ }
378
+ return n;
379
+ }
380
+ function applyAllReplacementsIgnoreMarks(doc, find, replacement, tableCol) {
381
+ for (const root of collectInlineRoots(doc, tableCol)) {
382
+ // Collect runs fresh each pass (splices invalidate indices)
383
+ // Process one run at a time; within a run process matches in reverse order.
384
+ const runs = collectDirectTextRuns(root);
385
+ // Reverse so that splices at later positions don't shift earlier ones.
386
+ for (let ri = runs.length - 1; ri >= 0; ri--) {
387
+ const run = runs[ri];
388
+ if (!run)
389
+ continue;
390
+ const { virtualStr, textNodes, parent } = run;
391
+ const spans = getQuidgetSpans(virtualStr);
392
+ const matches = [];
393
+ let pos = 0;
394
+ while (pos <= virtualStr.length - find.length) {
395
+ const i = virtualStr.indexOf(find, pos);
396
+ if (i === -1)
397
+ break;
398
+ if (isInsideAnySpan(i, i + find.length, spans)) {
399
+ const containing = spans.find(([s, e]) => i > s && i + find.length <= e);
400
+ pos = containing ? containing[1] : i + find.length;
401
+ continue;
402
+ }
403
+ matches.push(i);
404
+ pos = i + find.length;
405
+ }
406
+ if (matches.length === 0)
407
+ continue;
408
+ // Apply in reverse so splices don't corrupt earlier match positions.
409
+ for (let mi = matches.length - 1; mi >= 0; mi--) {
410
+ const matchStart = matches[mi];
411
+ if (matchStart === undefined)
412
+ continue;
413
+ const matchEnd = matchStart + find.length;
414
+ const involved = textNodes.filter((tn) => tn.startOffset < matchEnd && tn.endOffset > matchStart);
415
+ if (involved.length === 0)
416
+ continue;
417
+ const first = involved[0];
418
+ const last = involved[involved.length - 1];
419
+ if (!first || !last)
420
+ continue;
421
+ const out = [];
422
+ const prefixLen = matchStart - first.startOffset;
423
+ if (prefixLen > 0) {
424
+ out.push({ ...first.node, value: first.node.value.slice(0, prefixLen) });
425
+ }
426
+ out.push(...replacement.map(cloneInlineTree));
427
+ const suffixStart = matchEnd - last.startOffset;
428
+ if (suffixStart < last.node.value.length) {
429
+ out.push({ ...last.node, value: last.node.value.slice(suffixStart) });
430
+ }
431
+ parent.splice(first.nodeIndex, last.nodeIndex - first.nodeIndex + 1, ...out);
432
+ }
433
+ }
434
+ }
435
+ }
436
+ // ---------------------------------------------------------------------------
437
+ // Clone helpers
438
+ // ---------------------------------------------------------------------------
439
+ function cloneInlineTree(n) {
440
+ if (n.nodeType === 'text') {
441
+ return {
442
+ ...n,
443
+ marks: n.marks.map((m) => ({ ...m })),
444
+ };
445
+ }
446
+ const inl = n;
447
+ if (Array.isArray(inl.content)) {
448
+ return {
449
+ ...inl,
450
+ content: inl.content.map(cloneInlineTree),
451
+ };
452
+ }
453
+ return { ...inl };
454
+ }
455
+ //# sourceMappingURL=replacePlainText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replacePlainText.js","sourceRoot":"","sources":["../../src/rtf/replacePlainText.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { SerializedEntry } from './state.js';
2
+ export declare const BACKUPS_DIR: string;
3
+ export interface BackupMeta {
4
+ version: 1;
5
+ createdAt: string;
6
+ label: string;
7
+ spaceKey: string;
8
+ spaceId: string;
9
+ environment: string;
10
+ defaultLocale: string;
11
+ rootEntryId: string;
12
+ rootSlug: string | null;
13
+ entryCount: number;
14
+ }
15
+ export interface BackupFile {
16
+ meta: BackupMeta;
17
+ entries: Record<string, SerializedEntry>;
18
+ }
19
+ export declare function generateBackupFilename(spaceKey: string, rootEntryId: string): string;
20
+ /**
21
+ * Resolves a backup name/path to an absolute file path.
22
+ * - Bare filename (no path separators) → look in BACKUPS_DIR
23
+ * - Relative or absolute path → resolve from cwd
24
+ */
25
+ export declare function resolveBackupPath(nameOrPath: string): string;
26
+ export declare function writeBackup(backup: BackupFile, outputPath: string): void;
27
+ export declare function readBackup(resolvedPath: string): BackupFile;
28
+ export interface BackupListEntry {
29
+ filename: string;
30
+ filePath: string;
31
+ meta: BackupMeta;
32
+ }
33
+ export declare function listBackups(): BackupListEntry[];
34
+ //# sourceMappingURL=backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/session/backup.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,WAAW,QAAwD,CAAC;AAEjF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC1C;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAOpF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAGxE;AAED,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,UAAU,CAiB3D;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,wBAAgB,WAAW,IAAI,eAAe,EAAE,CAiB/C"}
@@ -0,0 +1,64 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ export const BACKUPS_DIR = path.join(os.homedir(), '.contentful-cms', 'backups');
5
+ export function generateBackupFilename(spaceKey, rootEntryId) {
6
+ const now = new Date();
7
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
8
+ const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
9
+ `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
10
+ return `${spaceKey}-${rootEntryId}-${timestamp}.json`;
11
+ }
12
+ /**
13
+ * Resolves a backup name/path to an absolute file path.
14
+ * - Bare filename (no path separators) → look in BACKUPS_DIR
15
+ * - Relative or absolute path → resolve from cwd
16
+ */
17
+ export function resolveBackupPath(nameOrPath) {
18
+ if (!nameOrPath.includes(path.sep) && !nameOrPath.includes('/')) {
19
+ return path.join(BACKUPS_DIR, nameOrPath);
20
+ }
21
+ return path.resolve(nameOrPath);
22
+ }
23
+ export function writeBackup(backup, outputPath) {
24
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
25
+ fs.writeFileSync(outputPath, JSON.stringify(backup, null, 2), 'utf-8');
26
+ }
27
+ export function readBackup(resolvedPath) {
28
+ if (!fs.existsSync(resolvedPath)) {
29
+ throw new Error(`Backup not found: ${resolvedPath}\nRun "cms-edit backup list" to see available backups.`);
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
34
+ }
35
+ catch {
36
+ throw new Error(`Failed to parse backup file: ${resolvedPath}`);
37
+ }
38
+ const backup = parsed;
39
+ if (!backup?.meta || backup.meta.version !== 1 || typeof backup.entries !== 'object') {
40
+ throw new Error(`Invalid backup file format: ${resolvedPath}`);
41
+ }
42
+ return backup;
43
+ }
44
+ export function listBackups() {
45
+ if (!fs.existsSync(BACKUPS_DIR))
46
+ return [];
47
+ const files = fs.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith('.json'));
48
+ const results = [];
49
+ for (const filename of files) {
50
+ const filePath = path.join(BACKUPS_DIR, filename);
51
+ try {
52
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
53
+ if (parsed?.meta?.version === 1) {
54
+ results.push({ filename, filePath, meta: parsed.meta });
55
+ }
56
+ }
57
+ catch {
58
+ // skip corrupt files silently — caller may warn
59
+ }
60
+ }
61
+ // Sort newest-first by createdAt (ISO strings sort lexicographically)
62
+ return results.sort((a, b) => b.meta.createdAt.localeCompare(a.meta.createdAt));
63
+ }
64
+ //# sourceMappingURL=backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/session/backup.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/contentful-cms",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "CLI tool for AI agents to read and edit Contentful draft content without publish permissions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "picocolors": "^1.1.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@biomejs/biome": "^2.4.7",
36
+ "@biomejs/biome": "^2.4.8",
37
37
  "@types/node": "^22.19.15",
38
38
  "typescript": "^5.9.3",
39
39
  "vitest": "^4.1.0"
@@ -56,6 +56,6 @@
56
56
  "type-check": "tsc --noEmit",
57
57
  "lint": "biome lint .",
58
58
  "clean": "rm -rf dist .turbo *.tsbuildinfo",
59
- "test": "vitest run --passWithNoTests"
59
+ "test": "vitest run"
60
60
  }
61
61
  }
@@ -18,7 +18,7 @@ Guidelines are short markdown fragments — one per CMS type — that describe h
18
18
  pnpm --filter <appName> generate-showcase
19
19
  ```
20
20
  This writes `src/generated/showcase-examples.json`.
21
- 3. **Curate showcase mocks** — run the [curate-showcase-mocks skill](.cursor/skills/se-marketing-sites/curate-showcase-mocks/SKILL.md) to select the best examples into `showcase-mocks.json`. This makes the showcase (and screenshots) show realistic content.
21
+ 3. **Curate showcase mocks** — run the [curate-showcase-mocks skill](.agents/skills/se-marketing-sites/curate-showcase-mocks/SKILL.md) to select the best examples into `showcase-mocks.json`. This makes the showcase (and screenshots) show realistic content.
22
22
  4. **Generate field list** — parse TypeScript types to produce structured field metadata:
23
23
  ```bash
24
24
  # From the app directory:
@@ -27,7 +27,7 @@ Guidelines are short markdown fragments — one per CMS type — that describe h
27
27
  # Or from repo root:
28
28
  pnpm --filter <appName> exec cms-generate-field-list --app-dir .
29
29
  ```
30
- 5. **Generate guidelines** — run the [Generate CMS guidelines skill](.cursor/skills/contentful-cms/generate-cms-guidelines/SKILL.md).
30
+ 5. **Generate guidelines** — run the [Generate CMS guidelines skill](.agents/skills/contentful-cms/generate-cms-guidelines/SKILL.md).
31
31
 
32
32
  Steps 1–3 only need to be repeated when content or component code changes significantly. Step 4 only needs to be repeated when TypeScript types change. Step 5 can be re-run at any time.
33
33
 
@@ -35,7 +35,7 @@ Steps 1–3 only need to be repeated when content or component code changes sign
35
35
 
36
36
  ## How to run guideline generation
37
37
 
38
- Use the Cursor skill at [`.cursor/skills/contentful-cms/generate-cms-guidelines/SKILL.md`](.cursor/skills/contentful-cms/generate-cms-guidelines/SKILL.md). It supports three modes:
38
+ Use the Cursor skill at [`.agents/skills/contentful-cms/generate-cms-guidelines/SKILL.md`](.agents/skills/contentful-cms/generate-cms-guidelines/SKILL.md). It supports three modes:
39
39
 
40
40
  | Mode | When to use |
41
41
  |---|---|
@@ -121,6 +121,20 @@ cms-edit rtf @c1 body --file path/to/file.md
121
121
  cms-edit rtf @c1 body - < path/to/file.md
122
122
  ```
123
123
 
124
+ **Surgical rich text replace** (compliance / quidget tokens — does not re-import the whole field):
125
+
126
+ - Matching is **per Contentful text node** only. If the author split a phrase with bold (e.g. `**Annual fee: **$95`), the string `Annual fee: $95` will **not** match as one piece; use a shorter `--find` or full `rtf … --file`.
127
+ - Quidget strings contain `*`, which Markdown would treat as italic — use **`--replace-plain`**, not `--replace`.
128
+
129
+ ```bash
130
+ # Replace every "$95" in this field with the quidget (e.g. multiple table cells)
131
+ cms-edit rtf replace @c2 body --find '$95' --replace-plain '{*credit_card_id*:*5048345*,*field*:*annual_fees*,*api*:*cc*}' --mode all
132
+
133
+ # Exactly one occurrence required (fails if 0 or 2+)
134
+ cms-edit rtf replace @c2 body --find '75,000 miles after spending $4,000 in the first 3 months' \
135
+ --replace-plain '{*credit_card_id*:*5048345*,*field*:*bonus_miles*,*api*:*cc*}' --mode exactlyOne
136
+ ```
137
+
124
138
  ### Step 5: Review changes
125
139
 
126
140
  ```bash