@cyber-dash-tech/revela 0.18.3 → 0.18.5

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,203 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"
2
+ import { join } from "path"
3
+ import { randomBytes } from "crypto"
4
+ import { workspaceMetaPath } from "../workspace-meta"
5
+
6
+ export type ReviewCommentStatus = "open" | "queued" | "applying" | "applied" | "failed"
7
+
8
+ export interface ReviewCommentRecord {
9
+ id: string
10
+ deckFile: string
11
+ slideIndex: number
12
+ deckVersion: string
13
+ comment: string
14
+ elements: any[]
15
+ asset?: any
16
+ drop?: any
17
+ status: ReviewCommentStatus
18
+ createdAt: string
19
+ updatedAt: string
20
+ lastApplyRequestId?: string
21
+ lastApplyError?: string
22
+ lastApplyRaw?: string
23
+ }
24
+
25
+ export interface ReviewCommentCreateInput {
26
+ deckFile: string
27
+ deckVersion: string
28
+ comment: string
29
+ elements: any[]
30
+ asset?: any
31
+ drop?: any
32
+ }
33
+
34
+ export function createReviewComment(workspaceRoot: string, input: ReviewCommentCreateInput): ReviewCommentRecord {
35
+ const comment = input.comment.trim()
36
+ if (!comment) throw new Error("Comment is required")
37
+ const elements = Array.isArray(input.elements) ? input.elements : []
38
+ const slideIndex = deriveSlideIndex(elements)
39
+ const now = new Date().toISOString()
40
+ const record: ReviewCommentRecord = {
41
+ id: randomBytes(10).toString("base64url"),
42
+ deckFile: normalizeDeckFile(input.deckFile),
43
+ slideIndex,
44
+ deckVersion: input.deckVersion,
45
+ comment,
46
+ elements,
47
+ ...(input.asset ? { asset: input.asset } : {}),
48
+ ...(input.drop ? { drop: input.drop } : {}),
49
+ status: "open",
50
+ createdAt: now,
51
+ updatedAt: now,
52
+ }
53
+ writeReviewComment(workspaceRoot, record)
54
+ return record
55
+ }
56
+
57
+ export function listReviewComments(workspaceRoot: string, deckFile: string): ReviewCommentRecord[] {
58
+ const file = normalizeDeckFile(deckFile)
59
+ const dir = reviewCommentsDir(workspaceRoot)
60
+ if (!existsSync(dir)) return []
61
+ const registry = readRegistry(workspaceRoot)
62
+ const records = registry.commentIds
63
+ .map((id) => readReviewComment(workspaceRoot, id))
64
+ .filter((record): record is ReviewCommentRecord => Boolean(record))
65
+ return records
66
+ .filter((record) => record.deckFile === file)
67
+ .sort((a, b) => a.slideIndex - b.slideIndex || a.createdAt.localeCompare(b.createdAt))
68
+ }
69
+
70
+ export function readReviewComment(workspaceRoot: string, id: string): ReviewCommentRecord | undefined {
71
+ const safeId = normalizeId(id)
72
+ if (!safeId) return undefined
73
+ const path = reviewCommentPath(workspaceRoot, safeId)
74
+ if (!existsSync(path)) return undefined
75
+ return JSON.parse(readFileSync(path, "utf-8")) as ReviewCommentRecord
76
+ }
77
+
78
+ export function markReviewCommentApplying(workspaceRoot: string, id: string, requestId: string): ReviewCommentRecord | undefined {
79
+ return updateReviewComment(workspaceRoot, id, (record) => ({
80
+ ...record,
81
+ status: "applying",
82
+ lastApplyRequestId: requestId,
83
+ lastApplyError: undefined,
84
+ lastApplyRaw: undefined,
85
+ }))
86
+ }
87
+
88
+ export function markReviewCommentQueued(workspaceRoot: string, id: string): ReviewCommentRecord | undefined {
89
+ return updateReviewComment(workspaceRoot, id, (record) => ({
90
+ ...record,
91
+ status: "queued",
92
+ lastApplyError: undefined,
93
+ lastApplyRaw: undefined,
94
+ }))
95
+ }
96
+
97
+ export function markReviewCommentApplied(workspaceRoot: string, id: string): ReviewCommentRecord | undefined {
98
+ return updateReviewComment(workspaceRoot, id, (record) => ({
99
+ ...record,
100
+ status: "applied",
101
+ lastApplyError: undefined,
102
+ lastApplyRaw: undefined,
103
+ }))
104
+ }
105
+
106
+ export function markReviewCommentFailed(workspaceRoot: string, id: string, error: string, raw?: string): ReviewCommentRecord | undefined {
107
+ return updateReviewComment(workspaceRoot, id, (record) => ({
108
+ ...record,
109
+ status: "failed",
110
+ lastApplyError: error,
111
+ ...(raw ? { lastApplyRaw: boundedTail(raw) } : {}),
112
+ }))
113
+ }
114
+
115
+ export function markReviewCommentStopped(workspaceRoot: string, id: string): ReviewCommentRecord | undefined {
116
+ return markReviewCommentFailed(workspaceRoot, id, "Stopped by user.", "Stopped by user.")
117
+ }
118
+
119
+ export function deleteReviewComment(workspaceRoot: string, id: string): boolean {
120
+ const safeId = normalizeId(id)
121
+ if (!safeId) return false
122
+ const path = reviewCommentPath(workspaceRoot, safeId)
123
+ if (!existsSync(path)) return false
124
+ unlinkSync(path)
125
+ const registry = readRegistry(workspaceRoot)
126
+ writeRegistry(workspaceRoot, {
127
+ version: 1,
128
+ commentIds: registry.commentIds.filter((item) => item !== safeId),
129
+ })
130
+ return true
131
+ }
132
+
133
+ function updateReviewComment(
134
+ workspaceRoot: string,
135
+ id: string,
136
+ update: (record: ReviewCommentRecord) => ReviewCommentRecord,
137
+ ): ReviewCommentRecord | undefined {
138
+ const record = readReviewComment(workspaceRoot, id)
139
+ if (!record) return undefined
140
+ const next = { ...update(record), updatedAt: new Date().toISOString() }
141
+ writeReviewComment(workspaceRoot, next)
142
+ return next
143
+ }
144
+
145
+ function writeReviewComment(workspaceRoot: string, record: ReviewCommentRecord): void {
146
+ const registry = readRegistry(workspaceRoot)
147
+ const dir = reviewCommentsDir(workspaceRoot)
148
+ mkdirSync(dir, { recursive: true })
149
+ writeFileSync(reviewCommentPath(workspaceRoot, record.id), `${JSON.stringify(record, null, 2)}\n`, "utf-8")
150
+ if (!registry.commentIds.includes(record.id)) {
151
+ registry.commentIds.push(record.id)
152
+ writeRegistry(workspaceRoot, registry)
153
+ }
154
+ }
155
+
156
+ function readRegistry(workspaceRoot: string): { version: 1; commentIds: string[] } {
157
+ const path = join(reviewCommentsDir(workspaceRoot), "registry.json")
158
+ if (!existsSync(path)) return { version: 1, commentIds: [] }
159
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as { version?: number; commentIds?: unknown[] }
160
+ return {
161
+ version: 1,
162
+ commentIds: Array.isArray(parsed.commentIds) ? parsed.commentIds.filter((item): item is string => typeof item === "string") : [],
163
+ }
164
+ }
165
+
166
+ function writeRegistry(workspaceRoot: string, registry: { version: 1; commentIds: string[] }): void {
167
+ const dir = reviewCommentsDir(workspaceRoot)
168
+ mkdirSync(dir, { recursive: true })
169
+ writeFileSync(join(dir, "registry.json"), `${JSON.stringify(registry, null, 2)}\n`, "utf-8")
170
+ }
171
+
172
+ function reviewCommentsDir(workspaceRoot: string): string {
173
+ return workspaceMetaPath(workspaceRoot, "review-comments")
174
+ }
175
+
176
+ function reviewCommentPath(workspaceRoot: string, id: string): string {
177
+ return join(reviewCommentsDir(workspaceRoot), `${id}.json`)
178
+ }
179
+
180
+ function normalizeDeckFile(file: string): string {
181
+ return file.replace(/\\/g, "/").replace(/^\.\//, "")
182
+ }
183
+
184
+ function normalizeId(id: string): string {
185
+ const trimmed = id.trim()
186
+ return /^[A-Za-z0-9_-]+$/.test(trimmed) ? trimmed : ""
187
+ }
188
+
189
+ function deriveSlideIndex(elements: any[]): number {
190
+ const indexes = new Set<number>()
191
+ for (const element of elements) {
192
+ const value = element?.slideIndex
193
+ if (Number.isInteger(value) && value > 0) indexes.add(value)
194
+ }
195
+ if (indexes.size === 0) throw new Error("Comment must reference one slide. Ctrl/Cmd-click an element on the target slide first.")
196
+ if (indexes.size > 1) throw new Error("Comment references multiple slides. Split this into per-slide comments before applying fixes.")
197
+ return [...indexes][0]
198
+ }
199
+
200
+ function boundedTail(text: string, limit = 4096): string {
201
+ if (text.length <= limit) return text
202
+ return text.slice(text.length - limit)
203
+ }