|
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 |
+ |
} |