paseo-kanban 0.1.0

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.
@@ -0,0 +1,696 @@
1
+ import {
2
+ type KanbanBoard,
3
+ type KanbanTask,
4
+ type KanbanLane,
5
+ type KanbanSubtask,
6
+ type ProjectFilter,
7
+ addTask,
8
+ updateTask,
9
+ deleteTask,
10
+ addLane,
11
+ updateLane,
12
+ deleteLane,
13
+ reorderTask,
14
+ } from "../shared/kanban.ts";
15
+
16
+ export type SessionResult =
17
+ | { success: true }
18
+ | { success: false; error: string };
19
+
20
+ export type SaveFn = (board: KanbanBoard, revision: string) => Promise<boolean>;
21
+
22
+ export type SessionLanguage = "zh" | "en";
23
+
24
+ export const sessionMessages = {
25
+ zh: {
26
+ subtaskEmpty: "新增子步骤内容不能为空",
27
+ savingInProgress: "正在保存中,请勿重复提交",
28
+ taskTitleEmpty: "任务标题不能为空",
29
+ subtaskTitleEmpty: (idx: number) => `第 ${idx} 个子步骤标题不能为空,请修改或删除该步骤`,
30
+ taskNotFound: (id: string) => `任务 "${id}" 不存在或已被删除`,
31
+ taskSaveConflict: "保存失败:数据可能已被其他会话修改或保存未生效。草稿已保留,请核实后再试。",
32
+ savingActionInProgress: "正在保存中,请勿重复操作",
33
+ taskDeleteOnlyInEdit: "仅在编辑模式下可删除任务",
34
+ taskDeleteFailed: "删除失败:数据可能已被其他会话修改或删除未生效。",
35
+ laneTitleEmpty: "泳道名称不能为空",
36
+ laneNotFound: (id: string) => `泳道 "${id}" 不存在或已被删除`,
37
+ laneSaveConflict: "保存失败:数据可能已被其他会话修改或保存未生效。草稿已保留。",
38
+ laneDeleteOnlyInEdit: "仅在编辑模式下可删除泳道",
39
+ laneDeleteFailed: "删除失败:数据可能已被其他会话修改或删除未生效。",
40
+ reorderConflict: "保存失败或版本冲突,未修改数据。请刷新最新数据后重试。",
41
+ },
42
+ en: {
43
+ subtaskEmpty: "Subtask content cannot be empty",
44
+ savingInProgress: "Saving in progress, please do not submit repeatedly",
45
+ taskTitleEmpty: "Task title cannot be empty",
46
+ subtaskTitleEmpty: (idx: number) => `Subtask #${idx} title cannot be empty. Please modify or remove it`,
47
+ taskNotFound: (id: string) => `Task "${id}" does not exist or has been deleted`,
48
+ taskSaveConflict: "Save failed: Data may have been modified by another session or save was not applied. Draft preserved, please verify and try again.",
49
+ savingActionInProgress: "Operation in progress, please do not submit repeatedly",
50
+ taskDeleteOnlyInEdit: "Tasks can only be deleted in edit mode",
51
+ taskDeleteFailed: "Delete failed: Data may have been modified by another session or delete was not applied.",
52
+ laneTitleEmpty: "Lane name cannot be empty",
53
+ laneNotFound: (id: string) => `Lane "${id}" does not exist or has been deleted`,
54
+ laneSaveConflict: "Save failed: Data may have been modified by another session or save was not applied. Draft preserved.",
55
+ laneDeleteOnlyInEdit: "Lanes can only be deleted in edit mode",
56
+ laneDeleteFailed: "Delete failed: Data may have been modified by another session or delete was not applied.",
57
+ reorderConflict: "Save failed or version conflict, data was not modified. Please refresh and try again.",
58
+ },
59
+ } as const;
60
+
61
+ export interface TaskDraft {
62
+ id?: string;
63
+ title: string;
64
+ description?: string;
65
+ laneId: string;
66
+ projectId?: string | null;
67
+ subtasks?: KanbanSubtask[];
68
+ }
69
+
70
+ export interface TaskSessionInitialValues {
71
+ id?: string;
72
+ title: string;
73
+ description: string;
74
+ laneId: string;
75
+ projectId: string | null;
76
+ subtasks: KanbanSubtask[];
77
+ }
78
+
79
+ export interface TaskSessionSnapshot {
80
+ readonly id?: string;
81
+ readonly title: string;
82
+ readonly description: string;
83
+ readonly laneId: string;
84
+ readonly projectId: string | null;
85
+ readonly subtasks: readonly KanbanSubtask[];
86
+ readonly lanes: readonly KanbanLane[];
87
+ readonly isSaving: boolean;
88
+ readonly error: string | null;
89
+ }
90
+
91
+ export interface TaskEditSession {
92
+ readonly mode: "create" | "edit";
93
+ readonly baseBoard: KanbanBoard;
94
+ readonly baseRevision: string;
95
+ readonly initialValues: TaskSessionInitialValues;
96
+ getSnapshot(): TaskSessionSnapshot;
97
+ subscribe(listener: () => void): () => void;
98
+ setTitle(title: string): void;
99
+ setDescription(description: string): void;
100
+ setLaneId(laneId: string): void;
101
+ setProjectId(projectId: string | null): void;
102
+ clearError(): void;
103
+ addSubtask(title: string): boolean;
104
+ updateSubtask(id: string, patch: { title?: string; completed?: boolean }): void;
105
+ removeSubtask(id: string): void;
106
+ save(draft?: TaskDraft): Promise<SessionResult>;
107
+ saveDraft(draft?: TaskDraft): Promise<SessionResult>;
108
+ deleteTask(): Promise<SessionResult>;
109
+ }
110
+
111
+ export interface LaneDraft {
112
+ id?: string;
113
+ title: string;
114
+ }
115
+
116
+ export interface LaneSessionInitialValues {
117
+ id?: string;
118
+ title: string;
119
+ }
120
+
121
+ export interface LaneSessionSnapshot {
122
+ readonly id?: string;
123
+ readonly title: string;
124
+ readonly lanesCount: number;
125
+ readonly tasksInLaneCount: number;
126
+ readonly canDelete: boolean;
127
+ readonly isSaving: boolean;
128
+ readonly error: string | null;
129
+ }
130
+
131
+ export interface LaneEditSession {
132
+ readonly mode: "create" | "edit";
133
+ readonly baseBoard: KanbanBoard;
134
+ readonly baseRevision: string;
135
+ readonly initialValues: LaneSessionInitialValues;
136
+ readonly lanesCount: number;
137
+ readonly tasksInLaneCount: number;
138
+ readonly canDelete: boolean;
139
+ getSnapshot(): LaneSessionSnapshot;
140
+ subscribe(listener: () => void): () => void;
141
+ setTitle(title: string): void;
142
+ clearError(): void;
143
+ save(draft?: LaneDraft): Promise<SessionResult>;
144
+ saveDraft(draft?: LaneDraft): Promise<SessionResult>;
145
+ deleteLane(): Promise<SessionResult>;
146
+ }
147
+
148
+ export interface OpenTaskSessionOptions {
149
+ mode: "create" | "edit";
150
+ baseBoard: KanbanBoard;
151
+ baseRevision: string;
152
+ save: SaveFn;
153
+ taskId?: string;
154
+ defaultLaneId?: string;
155
+ defaultProjectId?: string | null;
156
+ idGenerator?: () => string;
157
+ language?: SessionLanguage;
158
+ }
159
+
160
+ export function openTaskSession(options: OpenTaskSessionOptions): TaskEditSession {
161
+ const {
162
+ mode,
163
+ baseBoard,
164
+ baseRevision,
165
+ save: saveFn,
166
+ taskId,
167
+ defaultLaneId,
168
+ defaultProjectId,
169
+ idGenerator = () =>
170
+ `sub_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
171
+ language = "zh",
172
+ } = options;
173
+
174
+ const msgs = sessionMessages[language] ?? sessionMessages.zh;
175
+
176
+ const task =
177
+ mode === "edit" && taskId
178
+ ? baseBoard.tasks.find((t) => t.id === taskId) ?? null
179
+ : null;
180
+ const initialLaneId =
181
+ mode === "edit" && task
182
+ ? task.laneId
183
+ : (defaultLaneId ?? baseBoard.lanes[0]?.id ?? "");
184
+ const initialProjectId =
185
+ mode === "edit" && task ? task.projectId : (defaultProjectId ?? null);
186
+
187
+ const initialValues: TaskSessionInitialValues = {
188
+ id: task?.id,
189
+ title: task?.title ?? "",
190
+ description: task?.description ?? "",
191
+ laneId: initialLaneId,
192
+ projectId: initialProjectId,
193
+ subtasks: task ? task.subtasks.map((s) => ({ ...s })) : [],
194
+ };
195
+
196
+ let title = initialValues.title;
197
+ let description = initialValues.description;
198
+ let laneId = initialValues.laneId;
199
+ let projectId = initialValues.projectId;
200
+ let subtasks: KanbanSubtask[] = initialValues.subtasks.map((s) => ({ ...s }));
201
+ let isSaving = false;
202
+ let error: string | null = null;
203
+
204
+ const listeners = new Set<() => void>();
205
+ let cachedSnapshot: TaskSessionSnapshot | null = null;
206
+
207
+ function buildSnapshot(): TaskSessionSnapshot {
208
+ if (!cachedSnapshot) {
209
+ cachedSnapshot = {
210
+ id: initialValues.id,
211
+ title,
212
+ description,
213
+ laneId,
214
+ projectId,
215
+ subtasks: [...subtasks],
216
+ lanes: baseBoard.lanes,
217
+ isSaving,
218
+ error,
219
+ };
220
+ }
221
+ return cachedSnapshot;
222
+ }
223
+
224
+ function notify() {
225
+ cachedSnapshot = null;
226
+ buildSnapshot();
227
+ for (const listener of listeners) {
228
+ listener();
229
+ }
230
+ }
231
+
232
+ function setTitle(newTitle: string) {
233
+ if (isSaving || newTitle === title) return;
234
+ title = newTitle;
235
+ if (error) error = null;
236
+ notify();
237
+ }
238
+
239
+ function setDescription(newDesc: string) {
240
+ if (isSaving || newDesc === description) return;
241
+ description = newDesc;
242
+ notify();
243
+ }
244
+
245
+ function setLaneId(newLaneId: string) {
246
+ if (isSaving || newLaneId === laneId) return;
247
+ laneId = newLaneId;
248
+ notify();
249
+ }
250
+
251
+ function setProjectId(newProjectId: string | null) {
252
+ if (isSaving || newProjectId === projectId) return;
253
+ projectId = newProjectId;
254
+ notify();
255
+ }
256
+
257
+ function clearError() {
258
+ if (error) {
259
+ error = null;
260
+ notify();
261
+ }
262
+ }
263
+
264
+ function addSubtask(subtaskTitle: string): boolean {
265
+ if (isSaving) return false;
266
+ const trimmed = subtaskTitle.trim();
267
+ if (!trimmed) {
268
+ error = msgs.subtaskEmpty;
269
+ notify();
270
+ return false;
271
+ }
272
+ let newId = idGenerator();
273
+ while (subtasks.some((s) => s.id === newId)) {
274
+ newId = `${newId}_${Math.random().toString(36).slice(2, 5)}`;
275
+ }
276
+ subtasks = [...subtasks, { id: newId, title: trimmed, completed: false }];
277
+ if (error) error = null;
278
+ notify();
279
+ return true;
280
+ }
281
+
282
+ function updateSubtask(
283
+ id: string,
284
+ patch: { title?: string; completed?: boolean }
285
+ ) {
286
+ if (isSaving) return;
287
+ let changed = false;
288
+ subtasks = subtasks.map((s) => {
289
+ if (s.id === id) {
290
+ const nextTitle = patch.title !== undefined ? patch.title : s.title;
291
+ const nextCompleted =
292
+ patch.completed !== undefined ? patch.completed : s.completed;
293
+ if (nextTitle !== s.title || nextCompleted !== s.completed) {
294
+ changed = true;
295
+ return { ...s, title: nextTitle, completed: nextCompleted };
296
+ }
297
+ }
298
+ return s;
299
+ });
300
+ if (changed) {
301
+ if (error) error = null;
302
+ notify();
303
+ }
304
+ }
305
+
306
+ function removeSubtask(id: string) {
307
+ if (isSaving) return;
308
+ const next = subtasks.filter((s) => s.id !== id);
309
+ if (next.length !== subtasks.length) {
310
+ subtasks = next;
311
+ if (error) error = null;
312
+ notify();
313
+ }
314
+ }
315
+
316
+ async function save(draft?: TaskDraft): Promise<SessionResult> {
317
+ if (isSaving) {
318
+ return { success: false, error: msgs.savingInProgress };
319
+ }
320
+
321
+ if (draft) {
322
+ if (draft.title !== undefined) title = draft.title;
323
+ if (draft.description !== undefined) description = draft.description ?? "";
324
+ if (draft.laneId !== undefined) laneId = draft.laneId;
325
+ if (draft.projectId !== undefined) projectId = draft.projectId ?? null;
326
+ if (draft.subtasks !== undefined)
327
+ subtasks = draft.subtasks.map((s) => ({ ...s }));
328
+ }
329
+
330
+ const trimmedTitle = title.trim();
331
+ if (!trimmedTitle) {
332
+ error = msgs.taskTitleEmpty;
333
+ notify();
334
+ return { success: false, error };
335
+ }
336
+
337
+ for (let i = 0; i < subtasks.length; i++) {
338
+ if (!subtasks[i].title.trim()) {
339
+ error = msgs.subtaskTitleEmpty(i + 1);
340
+ notify();
341
+ return { success: false, error };
342
+ }
343
+ }
344
+
345
+ isSaving = true;
346
+ error = null;
347
+ notify();
348
+
349
+ try {
350
+ let nextBoard: KanbanBoard;
351
+ const normalizedSubtasks = subtasks.map((s) => ({
352
+ id: s.id,
353
+ title: s.title.trim(),
354
+ completed: Boolean(s.completed),
355
+ }));
356
+
357
+ if (mode === "create") {
358
+ const generatedId =
359
+ draft?.id?.trim() ||
360
+ `task_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
361
+ nextBoard = addTask(baseBoard, {
362
+ id: generatedId,
363
+ title: trimmedTitle,
364
+ description: description.trim(),
365
+ laneId,
366
+ projectId,
367
+ subtasks: normalizedSubtasks,
368
+ });
369
+ } else {
370
+ if (!taskId || !baseBoard.tasks.some((t) => t.id === taskId)) {
371
+ isSaving = false;
372
+ error = msgs.taskNotFound(taskId ?? "");
373
+ notify();
374
+ return { success: false, error };
375
+ }
376
+ nextBoard = updateTask(baseBoard, taskId, {
377
+ title: trimmedTitle,
378
+ description: description.trim(),
379
+ laneId,
380
+ projectId,
381
+ subtasks: normalizedSubtasks,
382
+ });
383
+ }
384
+
385
+ const saved = await saveFn(nextBoard, baseRevision);
386
+ isSaving = false;
387
+ if (!saved) {
388
+ error = msgs.taskSaveConflict;
389
+ notify();
390
+ return { success: false, error };
391
+ }
392
+ notify();
393
+ return { success: true };
394
+ } catch (err) {
395
+ isSaving = false;
396
+ error = err instanceof Error ? err.message : String(err);
397
+ notify();
398
+ return { success: false, error };
399
+ }
400
+ }
401
+
402
+ async function deleteTaskAction(): Promise<SessionResult> {
403
+ if (isSaving) {
404
+ return { success: false, error: msgs.savingActionInProgress };
405
+ }
406
+ if (mode !== "edit" || !taskId) {
407
+ return { success: false, error: msgs.taskDeleteOnlyInEdit };
408
+ }
409
+ if (!baseBoard.tasks.some((t) => t.id === taskId)) {
410
+ return {
411
+ success: false,
412
+ error: msgs.taskNotFound(taskId),
413
+ };
414
+ }
415
+
416
+ isSaving = true;
417
+ error = null;
418
+ notify();
419
+
420
+ try {
421
+ const nextBoard = deleteTask(baseBoard, taskId);
422
+ const saved = await saveFn(nextBoard, baseRevision);
423
+ isSaving = false;
424
+ if (!saved) {
425
+ error = msgs.taskDeleteFailed;
426
+ notify();
427
+ return { success: false, error };
428
+ }
429
+ notify();
430
+ return { success: true };
431
+ } catch (err) {
432
+ isSaving = false;
433
+ error = err instanceof Error ? err.message : String(err);
434
+ notify();
435
+ return { success: false, error };
436
+ }
437
+ }
438
+
439
+ return {
440
+ mode,
441
+ baseBoard,
442
+ baseRevision,
443
+ initialValues,
444
+ getSnapshot: buildSnapshot,
445
+ subscribe(listener: () => void) {
446
+ listeners.add(listener);
447
+ return () => {
448
+ listeners.delete(listener);
449
+ };
450
+ },
451
+ setTitle,
452
+ setDescription,
453
+ setLaneId,
454
+ setProjectId,
455
+ clearError,
456
+ addSubtask,
457
+ updateSubtask,
458
+ removeSubtask,
459
+ save,
460
+ saveDraft: save,
461
+ deleteTask: deleteTaskAction,
462
+ };
463
+ }
464
+
465
+ export interface OpenLaneSessionOptions {
466
+ mode: "create" | "edit";
467
+ baseBoard: KanbanBoard;
468
+ baseRevision: string;
469
+ save: SaveFn;
470
+ laneId?: string;
471
+ language?: SessionLanguage;
472
+ }
473
+
474
+ export function openLaneSession(options: OpenLaneSessionOptions): LaneEditSession {
475
+ const { mode, baseBoard, baseRevision, save: saveFn, laneId, language = "zh" } = options;
476
+ const msgs = sessionMessages[language] ?? sessionMessages.zh;
477
+
478
+ const lane =
479
+ mode === "edit" && laneId
480
+ ? baseBoard.lanes.find((l) => l.id === laneId) ?? null
481
+ : null;
482
+ const lanesCount = baseBoard.lanes.length;
483
+ const tasksInLaneCount = lane
484
+ ? baseBoard.tasks.filter((t) => t.laneId === lane.id).length
485
+ : 0;
486
+ const canDelete = Boolean(lane && lanesCount > 1 && tasksInLaneCount === 0);
487
+
488
+ const initialValues: LaneSessionInitialValues = {
489
+ id: lane?.id,
490
+ title: lane?.title ?? "",
491
+ };
492
+
493
+ let title = initialValues.title;
494
+ let isSaving = false;
495
+ let error: string | null = null;
496
+
497
+ const listeners = new Set<() => void>();
498
+ let cachedSnapshot: LaneSessionSnapshot | null = null;
499
+
500
+ function buildSnapshot(): LaneSessionSnapshot {
501
+ if (!cachedSnapshot) {
502
+ cachedSnapshot = {
503
+ id: initialValues.id,
504
+ title,
505
+ lanesCount,
506
+ tasksInLaneCount,
507
+ canDelete,
508
+ isSaving,
509
+ error,
510
+ };
511
+ }
512
+ return cachedSnapshot;
513
+ }
514
+
515
+ function notify() {
516
+ cachedSnapshot = null;
517
+ buildSnapshot();
518
+ for (const listener of listeners) {
519
+ listener();
520
+ }
521
+ }
522
+
523
+ function setTitle(newTitle: string) {
524
+ if (isSaving || newTitle === title) return;
525
+ title = newTitle;
526
+ if (error) error = null;
527
+ notify();
528
+ }
529
+
530
+ function clearError() {
531
+ if (error) {
532
+ error = null;
533
+ notify();
534
+ }
535
+ }
536
+
537
+ async function save(draft?: LaneDraft): Promise<SessionResult> {
538
+ if (isSaving) {
539
+ return { success: false, error: msgs.savingInProgress };
540
+ }
541
+
542
+ if (draft && draft.title !== undefined) {
543
+ title = draft.title;
544
+ }
545
+
546
+ const trimmedTitle = title.trim();
547
+ if (!trimmedTitle) {
548
+ error = msgs.laneTitleEmpty;
549
+ notify();
550
+ return { success: false, error };
551
+ }
552
+
553
+ isSaving = true;
554
+ error = null;
555
+ notify();
556
+
557
+ try {
558
+ let nextBoard: KanbanBoard;
559
+ if (mode === "create") {
560
+ const generatedId =
561
+ draft?.id?.trim() ||
562
+ `lane_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
563
+ nextBoard = addLane(baseBoard, {
564
+ id: generatedId,
565
+ title: trimmedTitle,
566
+ });
567
+ } else {
568
+ if (!laneId || !baseBoard.lanes.some((l) => l.id === laneId)) {
569
+ isSaving = false;
570
+ error = msgs.laneNotFound(laneId ?? "");
571
+ notify();
572
+ return { success: false, error };
573
+ }
574
+ nextBoard = updateLane(baseBoard, laneId, {
575
+ title: trimmedTitle,
576
+ });
577
+ }
578
+
579
+ const saved = await saveFn(nextBoard, baseRevision);
580
+ isSaving = false;
581
+ if (!saved) {
582
+ error = msgs.laneSaveConflict;
583
+ notify();
584
+ return { success: false, error };
585
+ }
586
+ notify();
587
+ return { success: true };
588
+ } catch (err) {
589
+ isSaving = false;
590
+ error = err instanceof Error ? err.message : String(err);
591
+ notify();
592
+ return { success: false, error };
593
+ }
594
+ }
595
+
596
+ async function deleteLaneAction(): Promise<SessionResult> {
597
+ if (isSaving) {
598
+ return { success: false, error: msgs.savingActionInProgress };
599
+ }
600
+ if (mode !== "edit" || !laneId) {
601
+ return { success: false, error: msgs.laneDeleteOnlyInEdit };
602
+ }
603
+ if (!baseBoard.lanes.some((l) => l.id === laneId)) {
604
+ return {
605
+ success: false,
606
+ error: msgs.laneNotFound(laneId),
607
+ };
608
+ }
609
+
610
+ isSaving = true;
611
+ error = null;
612
+ notify();
613
+
614
+ try {
615
+ const nextBoard = deleteLane(baseBoard, laneId);
616
+ const saved = await saveFn(nextBoard, baseRevision);
617
+ isSaving = false;
618
+ if (!saved) {
619
+ error = msgs.laneDeleteFailed;
620
+ notify();
621
+ return { success: false, error };
622
+ }
623
+ notify();
624
+ return { success: true };
625
+ } catch (err) {
626
+ isSaving = false;
627
+ error = err instanceof Error ? err.message : String(err);
628
+ notify();
629
+ return { success: false, error };
630
+ }
631
+ }
632
+
633
+ return {
634
+ mode,
635
+ baseBoard,
636
+ baseRevision,
637
+ initialValues,
638
+ lanesCount,
639
+ tasksInLaneCount,
640
+ canDelete,
641
+ getSnapshot: buildSnapshot,
642
+ subscribe(listener: () => void) {
643
+ listeners.add(listener);
644
+ return () => {
645
+ listeners.delete(listener);
646
+ };
647
+ },
648
+ setTitle,
649
+ clearError,
650
+ save,
651
+ saveDraft: save,
652
+ deleteLane: deleteLaneAction,
653
+ };
654
+ }
655
+
656
+ export interface ReorderTaskOperationOptions {
657
+ baseBoard: KanbanBoard;
658
+ baseRevision: string;
659
+ taskId: string;
660
+ targetLaneId: string;
661
+ targetIndex?: number;
662
+ filter?: ProjectFilter | string;
663
+ save: SaveFn;
664
+ language?: SessionLanguage;
665
+ }
666
+
667
+ export type ReorderOperationResult =
668
+ | { success: true; unchanged?: boolean }
669
+ | { success: false; error: string };
670
+
671
+ export async function executeTaskReorder(
672
+ options: ReorderTaskOperationOptions
673
+ ): Promise<ReorderOperationResult> {
674
+ const { baseBoard, baseRevision, taskId, targetLaneId, targetIndex, filter, save, language = "zh" } = options;
675
+ const msgs = sessionMessages[language] ?? sessionMessages.zh;
676
+ try {
677
+ const nextBoard = reorderTask(baseBoard, taskId, targetLaneId, targetIndex, filter);
678
+ if (nextBoard === baseBoard) {
679
+ return { success: true, unchanged: true };
680
+ }
681
+ const saved = await save(nextBoard, baseRevision);
682
+ if (!saved) {
683
+ return {
684
+ success: false,
685
+ error: msgs.reorderConflict,
686
+ };
687
+ }
688
+ return { success: true };
689
+ } catch (err) {
690
+ return {
691
+ success: false,
692
+ error: err instanceof Error ? err.message : String(err),
693
+ };
694
+ }
695
+ }
696
+