feat: add `update` command 61409340
- Adds interactive command to update either the sequoia.json config or
the atproto publication record
Steve · 2026-02-03 20:04 5 file(s) · +688 −1
docs/docs/pages/cli-reference.mdx +15 −0
79 79
  --dry-run, -n            - Preview what would be synced without making changes [optional]
80 80
  --help, -h               - show help [optional]
81 81
```
82 +
83 +
## `update`
84 +
85 +
```bash [Terminal]
86 +
sequoia update
87 +
> Update local config or ATProto publication record
88 +
89 +
FLAGS:
90 +
  --help, -h - show help [optional]
91 +
```
92 +
93 +
Interactive command to modify your existing configuration. Choose between:
94 +
95 +
- **Local configuration**: Edit `sequoia.json` settings including site URL, directory paths, frontmatter mappings, advanced options, and Bluesky settings
96 +
- **ATProto publication**: Update your publication record's name, description, URL, icon, and discover visibility
packages/cli/src/commands/update.ts (added) +569 −0
1 +
import * as fs from "node:fs/promises";
2 +
import { command } from "cmd-ts";
3 +
import {
4 +
	intro,
5 +
	outro,
6 +
	note,
7 +
	text,
8 +
	confirm,
9 +
	select,
10 +
	spinner,
11 +
	log,
12 +
} from "@clack/prompts";
13 +
import { findConfig, loadConfig, generateConfigTemplate } from "../lib/config";
14 +
import { loadCredentials } from "../lib/credentials";
15 +
import { createAgent, getPublication, updatePublication } from "../lib/atproto";
16 +
import { exitOnCancel } from "../lib/prompts";
17 +
import type {
18 +
	PublisherConfig,
19 +
	FrontmatterMapping,
20 +
	BlueskyConfig,
21 +
} from "../lib/types";
22 +
23 +
export const updateCommand = command({
24 +
	name: "update",
25 +
	description: "Update local config or ATProto publication record",
26 +
	args: {},
27 +
	handler: async () => {
28 +
		intro("Sequoia Update");
29 +
30 +
		// Check if config exists
31 +
		const configPath = await findConfig();
32 +
		if (!configPath) {
33 +
			log.error("No configuration found. Run 'sequoia init' first.");
34 +
			process.exit(1);
35 +
		}
36 +
37 +
		const config = await loadConfig(configPath);
38 +
39 +
		// Ask what to update
40 +
		const updateChoice = exitOnCancel(
41 +
			await select({
42 +
				message: "What would you like to update?",
43 +
				options: [
44 +
					{ label: "Local configuration (sequoia.json)", value: "config" },
45 +
					{ label: "ATProto publication record", value: "publication" },
46 +
				],
47 +
			}),
48 +
		);
49 +
50 +
		if (updateChoice === "config") {
51 +
			await updateConfigFlow(config, configPath);
52 +
		} else {
53 +
			await updatePublicationFlow(config);
54 +
		}
55 +
56 +
		outro("Update complete!");
57 +
	},
58 +
});
59 +
60 +
async function updateConfigFlow(
61 +
	config: PublisherConfig,
62 +
	configPath: string,
63 +
): Promise<void> {
64 +
	// Show current config summary
65 +
	const configSummary = [
66 +
		`Site URL: ${config.siteUrl}`,
67 +
		`Content Dir: ${config.contentDir}`,
68 +
		`Path Prefix: ${config.pathPrefix || "/posts"}`,
69 +
		`Publication URI: ${config.publicationUri}`,
70 +
		config.imagesDir ? `Images Dir: ${config.imagesDir}` : null,
71 +
		config.outputDir ? `Output Dir: ${config.outputDir}` : null,
72 +
		config.bluesky?.enabled ? `Bluesky: enabled` : null,
73 +
	]
74 +
		.filter(Boolean)
75 +
		.join("\n");
76 +
77 +
	note(configSummary, "Current Configuration");
78 +
79 +
	let configUpdated = { ...config };
80 +
	let editing = true;
81 +
82 +
	while (editing) {
83 +
		const section = exitOnCancel(
84 +
			await select({
85 +
				message: "Select a section to edit:",
86 +
				options: [
87 +
					{ label: "Site settings (siteUrl, pathPrefix)", value: "site" },
88 +
					{
89 +
						label:
90 +
							"Directory paths (contentDir, imagesDir, publicDir, outputDir)",
91 +
						value: "directories",
92 +
					},
93 +
					{
94 +
						label:
95 +
							"Frontmatter mappings (title, description, publishDate, etc.)",
96 +
						value: "frontmatter",
97 +
					},
98 +
					{
99 +
						label:
100 +
							"Advanced options (pdsUrl, identity, ignore, removeIndexFromSlug, etc.)",
101 +
						value: "advanced",
102 +
					},
103 +
					{
104 +
						label: "Bluesky settings (enabled, maxAgeDays)",
105 +
						value: "bluesky",
106 +
					},
107 +
					{ label: "Done editing", value: "done" },
108 +
				],
109 +
			}),
110 +
		);
111 +
112 +
		if (section === "done") {
113 +
			editing = false;
114 +
			continue;
115 +
		}
116 +
117 +
		switch (section) {
118 +
			case "site":
119 +
				configUpdated = await editSiteSettings(configUpdated);
120 +
				break;
121 +
			case "directories":
122 +
				configUpdated = await editDirectories(configUpdated);
123 +
				break;
124 +
			case "frontmatter":
125 +
				configUpdated = await editFrontmatter(configUpdated);
126 +
				break;
127 +
			case "advanced":
128 +
				configUpdated = await editAdvanced(configUpdated);
129 +
				break;
130 +
			case "bluesky":
131 +
				configUpdated = await editBluesky(configUpdated);
132 +
				break;
133 +
		}
134 +
	}
135 +
136 +
	// Confirm before saving
137 +
	const shouldSave = exitOnCancel(
138 +
		await confirm({
139 +
			message: "Save changes to sequoia.json?",
140 +
			initialValue: true,
141 +
		}),
142 +
	);
143 +
144 +
	if (shouldSave) {
145 +
		const configContent = generateConfigTemplate({
146 +
			siteUrl: configUpdated.siteUrl,
147 +
			contentDir: configUpdated.contentDir,
148 +
			imagesDir: configUpdated.imagesDir,
149 +
			publicDir: configUpdated.publicDir,
150 +
			outputDir: configUpdated.outputDir,
151 +
			pathPrefix: configUpdated.pathPrefix,
152 +
			publicationUri: configUpdated.publicationUri,
153 +
			pdsUrl: configUpdated.pdsUrl,
154 +
			frontmatter: configUpdated.frontmatter,
155 +
			ignore: configUpdated.ignore,
156 +
			removeIndexFromSlug: configUpdated.removeIndexFromSlug,
157 +
			stripDatePrefix: configUpdated.stripDatePrefix,
158 +
			textContentField: configUpdated.textContentField,
159 +
			bluesky: configUpdated.bluesky,
160 +
		});
161 +
162 +
		await fs.writeFile(configPath, configContent);
163 +
		log.success("Configuration saved!");
164 +
	} else {
165 +
		log.info("Changes discarded.");
166 +
	}
167 +
}
168 +
169 +
async function editSiteSettings(
170 +
	config: PublisherConfig,
171 +
): Promise<PublisherConfig> {
172 +
	const siteUrl = exitOnCancel(
173 +
		await text({
174 +
			message: "Site URL:",
175 +
			initialValue: config.siteUrl,
176 +
			validate: (value) => {
177 +
				if (!value) return "Site URL is required";
178 +
				try {
179 +
					new URL(value);
180 +
				} catch {
181 +
					return "Please enter a valid URL";
182 +
				}
183 +
			},
184 +
		}),
185 +
	);
186 +
187 +
	const pathPrefix = exitOnCancel(
188 +
		await text({
189 +
			message: "URL path prefix for posts:",
190 +
			initialValue: config.pathPrefix || "/posts",
191 +
		}),
192 +
	);
193 +
194 +
	return {
195 +
		...config,
196 +
		siteUrl,
197 +
		pathPrefix: pathPrefix || undefined,
198 +
	};
199 +
}
200 +
201 +
async function editDirectories(
202 +
	config: PublisherConfig,
203 +
): Promise<PublisherConfig> {
204 +
	const contentDir = exitOnCancel(
205 +
		await text({
206 +
			message: "Content directory:",
207 +
			initialValue: config.contentDir,
208 +
			validate: (value) => {
209 +
				if (!value) return "Content directory is required";
210 +
			},
211 +
		}),
212 +
	);
213 +
214 +
	const imagesDir = exitOnCancel(
215 +
		await text({
216 +
			message: "Cover images directory (leave empty to skip):",
217 +
			initialValue: config.imagesDir || "",
218 +
		}),
219 +
	);
220 +
221 +
	const publicDir = exitOnCancel(
222 +
		await text({
223 +
			message: "Public/static directory:",
224 +
			initialValue: config.publicDir || "./public",
225 +
		}),
226 +
	);
227 +
228 +
	const outputDir = exitOnCancel(
229 +
		await text({
230 +
			message: "Build output directory:",
231 +
			initialValue: config.outputDir || "./dist",
232 +
		}),
233 +
	);
234 +
235 +
	return {
236 +
		...config,
237 +
		contentDir,
238 +
		imagesDir: imagesDir || undefined,
239 +
		publicDir: publicDir || undefined,
240 +
		outputDir: outputDir || undefined,
241 +
	};
242 +
}
243 +
244 +
async function editFrontmatter(
245 +
	config: PublisherConfig,
246 +
): Promise<PublisherConfig> {
247 +
	const currentFrontmatter = config.frontmatter || {};
248 +
249 +
	log.info("Press Enter to keep current value, or type a new field name.");
250 +
251 +
	const titleField = exitOnCancel(
252 +
		await text({
253 +
			message: "Field name for title:",
254 +
			initialValue: currentFrontmatter.title || "title",
255 +
		}),
256 +
	);
257 +
258 +
	const descField = exitOnCancel(
259 +
		await text({
260 +
			message: "Field name for description:",
261 +
			initialValue: currentFrontmatter.description || "description",
262 +
		}),
263 +
	);
264 +
265 +
	const dateField = exitOnCancel(
266 +
		await text({
267 +
			message: "Field name for publish date:",
268 +
			initialValue: currentFrontmatter.publishDate || "publishDate",
269 +
		}),
270 +
	);
271 +
272 +
	const coverField = exitOnCancel(
273 +
		await text({
274 +
			message: "Field name for cover image:",
275 +
			initialValue: currentFrontmatter.coverImage || "ogImage",
276 +
		}),
277 +
	);
278 +
279 +
	const tagsField = exitOnCancel(
280 +
		await text({
281 +
			message: "Field name for tags:",
282 +
			initialValue: currentFrontmatter.tags || "tags",
283 +
		}),
284 +
	);
285 +
286 +
	const draftField = exitOnCancel(
287 +
		await text({
288 +
			message: "Field name for draft status:",
289 +
			initialValue: currentFrontmatter.draft || "draft",
290 +
		}),
291 +
	);
292 +
293 +
	const slugField = exitOnCancel(
294 +
		await text({
295 +
			message: "Field name for slug (leave empty to use filepath):",
296 +
			initialValue: currentFrontmatter.slugField || "",
297 +
		}),
298 +
	);
299 +
300 +
	// Build frontmatter mapping, only including non-default values
301 +
	const fieldMappings: Array<[keyof FrontmatterMapping, string, string]> = [
302 +
		["title", titleField, "title"],
303 +
		["description", descField, "description"],
304 +
		["publishDate", dateField, "publishDate"],
305 +
		["coverImage", coverField, "ogImage"],
306 +
		["tags", tagsField, "tags"],
307 +
		["draft", draftField, "draft"],
308 +
	];
309 +
310 +
	const builtMapping = fieldMappings.reduce<FrontmatterMapping>(
311 +
		(acc, [key, value, defaultValue]) => {
312 +
			if (value !== defaultValue) {
313 +
				acc[key] = value;
314 +
			}
315 +
			return acc;
316 +
		},
317 +
		{},
318 +
	);
319 +
320 +
	// Handle slugField separately since it has no default
321 +
	if (slugField) {
322 +
		builtMapping.slugField = slugField;
323 +
	}
324 +
325 +
	const frontmatter =
326 +
		Object.keys(builtMapping).length > 0 ? builtMapping : undefined;
327 +
328 +
	return {
329 +
		...config,
330 +
		frontmatter,
331 +
	};
332 +
}
333 +
334 +
async function editAdvanced(config: PublisherConfig): Promise<PublisherConfig> {
335 +
	const pdsUrl = exitOnCancel(
336 +
		await text({
337 +
			message: "PDS URL (leave empty for default bsky.social):",
338 +
			initialValue: config.pdsUrl || "",
339 +
		}),
340 +
	);
341 +
342 +
	const identity = exitOnCancel(
343 +
		await text({
344 +
			message: "Identity/profile to use (leave empty for auto-detect):",
345 +
			initialValue: config.identity || "",
346 +
		}),
347 +
	);
348 +
349 +
	const ignoreInput = exitOnCancel(
350 +
		await text({
351 +
			message: "Ignore patterns (comma-separated, e.g., _index.md,drafts/**):",
352 +
			initialValue: config.ignore?.join(", ") || "",
353 +
		}),
354 +
	);
355 +
356 +
	const removeIndexFromSlug = exitOnCancel(
357 +
		await confirm({
358 +
			message: "Remove /index or /_index suffix from paths?",
359 +
			initialValue: config.removeIndexFromSlug || false,
360 +
		}),
361 +
	);
362 +
363 +
	const stripDatePrefix = exitOnCancel(
364 +
		await confirm({
365 +
			message: "Strip YYYY-MM-DD- prefix from filenames (Jekyll-style)?",
366 +
			initialValue: config.stripDatePrefix || false,
367 +
		}),
368 +
	);
369 +
370 +
	const textContentField = exitOnCancel(
371 +
		await text({
372 +
			message:
373 +
				"Frontmatter field for textContent (leave empty to use markdown body):",
374 +
			initialValue: config.textContentField || "",
375 +
		}),
376 +
	);
377 +
378 +
	// Parse ignore patterns
379 +
	const ignore = ignoreInput
380 +
		? ignoreInput
381 +
				.split(",")
382 +
				.map((p) => p.trim())
383 +
				.filter(Boolean)
384 +
		: undefined;
385 +
386 +
	return {
387 +
		...config,
388 +
		pdsUrl: pdsUrl || undefined,
389 +
		identity: identity || undefined,
390 +
		ignore: ignore && ignore.length > 0 ? ignore : undefined,
391 +
		removeIndexFromSlug: removeIndexFromSlug || undefined,
392 +
		stripDatePrefix: stripDatePrefix || undefined,
393 +
		textContentField: textContentField || undefined,
394 +
	};
395 +
}
396 +
397 +
async function editBluesky(config: PublisherConfig): Promise<PublisherConfig> {
398 +
	const enabled = exitOnCancel(
399 +
		await confirm({
400 +
			message: "Enable automatic Bluesky posting when publishing?",
401 +
			initialValue: config.bluesky?.enabled || false,
402 +
		}),
403 +
	);
404 +
405 +
	if (!enabled) {
406 +
		return {
407 +
			...config,
408 +
			bluesky: undefined,
409 +
		};
410 +
	}
411 +
412 +
	const maxAgeDaysInput = exitOnCancel(
413 +
		await text({
414 +
			message: "Maximum age (in days) for posts to be shared on Bluesky:",
415 +
			initialValue: String(config.bluesky?.maxAgeDays || 7),
416 +
			validate: (value) => {
417 +
				if (!value) return "Please enter a number";
418 +
				const num = Number.parseInt(value, 10);
419 +
				if (Number.isNaN(num) || num < 1) {
420 +
					return "Please enter a positive number";
421 +
				}
422 +
			},
423 +
		}),
424 +
	);
425 +
426 +
	const maxAgeDays = parseInt(maxAgeDaysInput, 10);
427 +
428 +
	const bluesky: BlueskyConfig = {
429 +
		enabled: true,
430 +
		...(maxAgeDays !== 7 && { maxAgeDays }),
431 +
	};
432 +
433 +
	return {
434 +
		...config,
435 +
		bluesky,
436 +
	};
437 +
}
438 +
439 +
async function updatePublicationFlow(config: PublisherConfig): Promise<void> {
440 +
	// Load credentials
441 +
	const credentials = await loadCredentials(config.identity);
442 +
	if (!credentials) {
443 +
		log.error(
444 +
			"No credentials found. Run 'sequoia auth' or 'sequoia login' first.",
445 +
		);
446 +
		process.exit(1);
447 +
	}
448 +
449 +
	const s = spinner();
450 +
	s.start("Connecting to ATProto...");
451 +
452 +
	let agent: Awaited<ReturnType<typeof createAgent>>;
453 +
	try {
454 +
		agent = await createAgent(credentials);
455 +
		s.stop("Connected!");
456 +
	} catch (error) {
457 +
		s.stop("Failed to connect");
458 +
		log.error(`Failed to connect: ${error}`);
459 +
		process.exit(1);
460 +
	}
461 +
462 +
	// Fetch existing publication
463 +
	s.start("Fetching publication...");
464 +
	const publication = await getPublication(agent, config.publicationUri);
465 +
466 +
	if (!publication) {
467 +
		s.stop("Publication not found");
468 +
		log.error(`Could not find publication: ${config.publicationUri}`);
469 +
		process.exit(1);
470 +
	}
471 +
	s.stop("Publication loaded!");
472 +
473 +
	// Show current publication info
474 +
	const pubRecord = publication.value;
475 +
	const pubSummary = [
476 +
		`Name: ${pubRecord.name}`,
477 +
		`URL: ${pubRecord.url}`,
478 +
		pubRecord.description ? `Description: ${pubRecord.description}` : null,
479 +
		pubRecord.icon ? `Icon: (uploaded)` : null,
480 +
		`Show in Discover: ${pubRecord.preferences?.showInDiscover ?? true}`,
481 +
		`Created: ${pubRecord.createdAt}`,
482 +
	]
483 +
		.filter(Boolean)
484 +
		.join("\n");
485 +
486 +
	note(pubSummary, "Current Publication");
487 +
488 +
	// Collect updates with pre-populated values
489 +
	const name = exitOnCancel(
490 +
		await text({
491 +
			message: "Publication name:",
492 +
			initialValue: pubRecord.name,
493 +
			validate: (value) => {
494 +
				if (!value) return "Publication name is required";
495 +
			},
496 +
		}),
497 +
	);
498 +
499 +
	const description = exitOnCancel(
500 +
		await text({
501 +
			message: "Publication description (leave empty to clear):",
502 +
			initialValue: pubRecord.description || "",
503 +
		}),
504 +
	);
505 +
506 +
	const url = exitOnCancel(
507 +
		await text({
508 +
			message: "Publication URL:",
509 +
			initialValue: pubRecord.url,
510 +
			validate: (value) => {
511 +
				if (!value) return "URL is required";
512 +
				try {
513 +
					new URL(value);
514 +
				} catch {
515 +
					return "Please enter a valid URL";
516 +
				}
517 +
			},
518 +
		}),
519 +
	);
520 +
521 +
	const iconPath = exitOnCancel(
522 +
		await text({
523 +
			message: "New icon path (leave empty to keep existing):",
524 +
			initialValue: "",
525 +
		}),
526 +
	);
527 +
528 +
	const showInDiscover = exitOnCancel(
529 +
		await confirm({
530 +
			message: "Show in Discover feed?",
531 +
			initialValue: pubRecord.preferences?.showInDiscover ?? true,
532 +
		}),
533 +
	);
534 +
535 +
	// Confirm before updating
536 +
	const shouldUpdate = exitOnCancel(
537 +
		await confirm({
538 +
			message: "Update publication on ATProto?",
539 +
			initialValue: true,
540 +
		}),
541 +
	);
542 +
543 +
	if (!shouldUpdate) {
544 +
		log.info("Update cancelled.");
545 +
		return;
546 +
	}
547 +
548 +
	// Perform update
549 +
	s.start("Updating publication...");
550 +
	try {
551 +
		await updatePublication(
552 +
			agent,
553 +
			config.publicationUri,
554 +
			{
555 +
				name,
556 +
				description,
557 +
				url,
558 +
				iconPath: iconPath || undefined,
559 +
				showInDiscover,
560 +
			},
561 +
			pubRecord,
562 +
		);
563 +
		s.stop("Publication updated!");
564 +
	} catch (error) {
565 +
		s.stop("Failed to update publication");
566 +
		log.error(`Failed to update: ${error}`);
567 +
		process.exit(1);
568 +
	}
569 +
}
packages/cli/src/index.ts +2 −0
7 7
import { loginCommand } from "./commands/login";
8 8
import { publishCommand } from "./commands/publish";
9 9
import { syncCommand } from "./commands/sync";
10 +
import { updateCommand } from "./commands/update";
10 11
11 12
const app = subcommands({
12 13
	name: "sequoia",
42 43
		login: loginCommand,
43 44
		publish: publishCommand,
44 45
		sync: syncCommand,
46 +
		update: updateCommand,
45 47
	},
46 48
});
47 49
packages/cli/src/lib/atproto.ts +97 −0
8 8
	BlobObject,
9 9
	BlogPost,
10 10
	Credentials,
11 +
	PublicationRecord,
11 12
	PublisherConfig,
12 13
	StrongRef,
13 14
} from "./types";
440 441
	});
441 442
442 443
	return response.data.uri;
444 +
}
445 +
446 +
export interface GetPublicationResult {
447 +
	uri: string;
448 +
	cid: string;
449 +
	value: PublicationRecord;
450 +
}
451 +
452 +
export async function getPublication(
453 +
	agent: Agent,
454 +
	publicationUri: string,
455 +
): Promise<GetPublicationResult | null> {
456 +
	const parsed = parseAtUri(publicationUri);
457 +
	if (!parsed) {
458 +
		return null;
459 +
	}
460 +
461 +
	try {
462 +
		const response = await agent.com.atproto.repo.getRecord({
463 +
			repo: parsed.did,
464 +
			collection: parsed.collection,
465 +
			rkey: parsed.rkey,
466 +
		});
467 +
468 +
		return {
469 +
			uri: publicationUri,
470 +
			cid: response.data.cid!,
471 +
			value: response.data.value as unknown as PublicationRecord,
472 +
		};
473 +
	} catch {
474 +
		return null;
475 +
	}
476 +
}
477 +
478 +
export interface UpdatePublicationOptions {
479 +
	url?: string;
480 +
	name?: string;
481 +
	description?: string;
482 +
	iconPath?: string;
483 +
	showInDiscover?: boolean;
484 +
}
485 +
486 +
export async function updatePublication(
487 +
	agent: Agent,
488 +
	publicationUri: string,
489 +
	options: UpdatePublicationOptions,
490 +
	existingRecord: PublicationRecord,
491 +
): Promise<void> {
492 +
	const parsed = parseAtUri(publicationUri);
493 +
	if (!parsed) {
494 +
		throw new Error(`Invalid publication URI: ${publicationUri}`);
495 +
	}
496 +
497 +
	// Build updated record, preserving createdAt and $type
498 +
	const record: Record<string, unknown> = {
499 +
		$type: existingRecord.$type,
500 +
		url: options.url ?? existingRecord.url,
501 +
		name: options.name ?? existingRecord.name,
502 +
		createdAt: existingRecord.createdAt,
503 +
	};
504 +
505 +
	// Handle description - can be cleared with empty string
506 +
	if (options.description !== undefined) {
507 +
		if (options.description) {
508 +
			record.description = options.description;
509 +
		}
510 +
		// If empty string, don't include description (clears it)
511 +
	} else if (existingRecord.description) {
512 +
		record.description = existingRecord.description;
513 +
	}
514 +
515 +
	// Handle icon - upload new if provided, otherwise keep existing
516 +
	if (options.iconPath) {
517 +
		const icon = await uploadImage(agent, options.iconPath);
518 +
		if (icon) {
519 +
			record.icon = icon;
520 +
		}
521 +
	} else if (existingRecord.icon) {
522 +
		record.icon = existingRecord.icon;
523 +
	}
524 +
525 +
	// Handle preferences
526 +
	if (options.showInDiscover !== undefined) {
527 +
		record.preferences = {
528 +
			showInDiscover: options.showInDiscover,
529 +
		};
530 +
	} else if (existingRecord.preferences) {
531 +
		record.preferences = existingRecord.preferences;
532 +
	}
533 +
534 +
	await agent.com.atproto.repo.putRecord({
535 +
		repo: parsed.did,
536 +
		collection: parsed.collection,
537 +
		rkey: parsed.rkey,
538 +
		record,
539 +
	});
443 540
}
444 541
445 542
// --- Bluesky Post Creation ---
packages/cli/src/lib/markdown.ts +5 −1
186 186
	rawFrontmatter: Record<string, unknown>,
187 187
	options: SlugOptions = {},
188 188
): string {
189 -
	const { slugField, removeIndexFromSlug = false, stripDatePrefix = false } = options;
189 +
	const {
190 +
		slugField,
191 +
		removeIndexFromSlug = false,
192 +
		stripDatePrefix = false,
193 +
	} = options;
190 194
191 195
	let slug: string;
192 196