@bdsqqq/lnr-cli 1.5.0 โ†’ 2.0.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.
Files changed (47) hide show
  1. package/package.json +2 -3
  2. package/src/bench-lnr-overhead.ts +160 -0
  3. package/src/e2e-mutations.test.ts +378 -0
  4. package/src/e2e-readonly.test.ts +103 -0
  5. package/src/generated/doc.ts +270 -0
  6. package/src/generated/issue.ts +807 -0
  7. package/src/generated/label.ts +273 -0
  8. package/src/generated/project.ts +596 -0
  9. package/src/generated/template.ts +157 -0
  10. package/src/hand-crafted/issue.ts +27 -0
  11. package/src/lib/adapters/doc.ts +14 -0
  12. package/src/lib/adapters/index.ts +4 -0
  13. package/src/lib/adapters/issue.ts +32 -0
  14. package/src/lib/adapters/label.ts +20 -0
  15. package/src/lib/adapters/project.ts +23 -0
  16. package/src/lib/arktype-config.ts +18 -0
  17. package/src/lib/command-introspection.ts +97 -0
  18. package/src/lib/dispatch-effects.test.ts +297 -0
  19. package/src/lib/error.ts +37 -1
  20. package/src/lib/operation-spec.test.ts +317 -0
  21. package/src/lib/operation-spec.ts +11 -0
  22. package/src/lib/operation-specs.ts +21 -0
  23. package/src/lib/output.test.ts +3 -1
  24. package/src/lib/output.ts +1 -296
  25. package/src/lib/renderers/comments.ts +300 -0
  26. package/src/lib/renderers/detail.ts +61 -0
  27. package/src/lib/renderers/index.ts +2 -0
  28. package/src/router/agent-sessions.ts +253 -0
  29. package/src/router/auth.ts +6 -5
  30. package/src/router/config.ts +7 -6
  31. package/src/router/contract.test.ts +364 -0
  32. package/src/router/cycles.ts +372 -95
  33. package/src/router/git-automation-states.ts +355 -0
  34. package/src/router/git-automation-target-branches.ts +309 -0
  35. package/src/router/index.ts +26 -8
  36. package/src/router/initiatives.ts +260 -0
  37. package/src/router/me.ts +8 -7
  38. package/src/router/notifications.ts +176 -0
  39. package/src/router/roadmaps.ts +172 -0
  40. package/src/router/search.ts +7 -6
  41. package/src/router/teams.ts +82 -24
  42. package/src/router/users.ts +126 -0
  43. package/src/router/views.ts +399 -0
  44. package/src/router/docs.ts +0 -153
  45. package/src/router/issues.ts +0 -600
  46. package/src/router/labels.ts +0 -192
  47. package/src/router/projects.ts +0 -220
package/src/lib/output.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { getConfigValue, type Comment } from "@bdsqqq/lnr-core";
2
+ import { getConfigValue } from "@bdsqqq/lnr-core";
3
3
 
4
4
  export type OutputFormat = "table" | "json" | "quiet";
5
5
 
@@ -117,299 +117,4 @@ export function formatRelativeTime(date: Date): string {
117
117
  return `${diffYears}y ago`;
118
118
  }
119
119
 
120
- export function buildChildMap(comments: Comment[]): Map<string | null, Comment[]> {
121
- const childMap = new Map<string | null, Comment[]>();
122
120
 
123
- for (const c of comments) {
124
- const parentKey = c.parentId ?? null;
125
- const existing = childMap.get(parentKey) ?? [];
126
- existing.push(c);
127
- childMap.set(parentKey, existing);
128
- }
129
-
130
- for (const children of Array.from(childMap.values())) {
131
- children.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
132
- }
133
-
134
- return childMap;
135
- }
136
-
137
- const EMOJI_MAP: Record<string, string> = {
138
- // gestures
139
- "+1": "๐Ÿ‘", thumbsup: "๐Ÿ‘", "-1": "๐Ÿ‘Ž", thumbsdown: "๐Ÿ‘Ž",
140
- wave: "๐Ÿ‘‹", raised_back_of_hand: "๐Ÿคš", raised_hand: "โœ‹", hand: "โœ‹",
141
- vulcan_salute: "๐Ÿ––", ok_hand: "๐Ÿ‘Œ", pinched_fingers: "๐ŸคŒ", pinching_hand: "๐Ÿค",
142
- v: "โœŒ๏ธ", crossed_fingers: "๐Ÿคž", love_you_gesture: "๐ŸคŸ", metal: "๐Ÿค˜",
143
- call_me_hand: "๐Ÿค™", point_left: "๐Ÿ‘ˆ", point_right: "๐Ÿ‘‰", point_up_2: "๐Ÿ‘†",
144
- point_down: "๐Ÿ‘‡", point_up: "โ˜๏ธ", fist: "โœŠ", fist_raised: "โœŠ",
145
- facepunch: "๐Ÿ‘Š", punch: "๐Ÿ‘Š", fist_oncoming: "๐Ÿ‘Š", fist_left: "๐Ÿค›", fist_right: "๐Ÿคœ",
146
- clap: "๐Ÿ‘", raised_hands: "๐Ÿ™Œ", open_hands: "๐Ÿ‘", palms_up_together: "๐Ÿคฒ",
147
- handshake: "๐Ÿค", pray: "๐Ÿ™", writing_hand: "โœ๏ธ", nail_care: "๐Ÿ’…",
148
- selfie: "๐Ÿคณ", muscle: "๐Ÿ’ช", mechanical_arm: "๐Ÿฆพ",
149
- eyes: "๐Ÿ‘€", eye: "๐Ÿ‘๏ธ", tongue: "๐Ÿ‘…", lips: "๐Ÿ‘„",
150
-
151
- // smileys
152
- grinning: "๐Ÿ˜€", smile: "๐Ÿ˜Š", grin: "๐Ÿ˜", joy: "๐Ÿ˜‚", rofl: "๐Ÿคฃ",
153
- smiley: "๐Ÿ˜ƒ", sweat_smile: "๐Ÿ˜…", laughing: "๐Ÿ˜†", laugh: "๐Ÿ˜„",
154
- wink: "๐Ÿ˜‰", blush: "๐Ÿ˜Š", yum: "๐Ÿ˜‹", sunglasses: "๐Ÿ˜Ž", heart_eyes: "๐Ÿ˜",
155
- kissing_heart: "๐Ÿ’‹", kissing: "๐Ÿ˜—", relaxed: "โ˜บ๏ธ",
156
- stuck_out_tongue: "๐Ÿ˜›", stuck_out_tongue_winking_eye: "๐Ÿ˜œ", stuck_out_tongue_closed_eyes: "๐Ÿ˜",
157
- disappointed: "๐Ÿ˜ž", worried: "๐Ÿ˜Ÿ", angry: "๐Ÿ˜ ", rage: "๐Ÿ˜ก", pout: "๐Ÿ˜ก",
158
- cry: "๐Ÿ˜ข", persevere: "๐Ÿ˜ฃ", triumph: "๐Ÿ˜ค", disappointed_relieved: "๐Ÿ˜ฅ",
159
- frowning: "๐Ÿ˜ฆ", anguished: "๐Ÿ˜ง", fearful: "๐Ÿ˜จ", weary: "๐Ÿ˜ฉ",
160
- sleepy: "๐Ÿ˜ช", tired_face: "๐Ÿ˜ซ", grimacing: "๐Ÿ˜ฌ", sob: "๐Ÿ˜ญ",
161
- open_mouth: "๐Ÿ˜ฎ", hushed: "๐Ÿ˜ฏ", cold_sweat: "๐Ÿ˜ฐ", scream: "๐Ÿ˜ฑ",
162
- astonished: "๐Ÿ˜ฒ", flushed: "๐Ÿ˜ณ", sleeping: "๐Ÿ˜ด", dizzy_face: "๐Ÿ˜ต",
163
- no_mouth: "๐Ÿ˜ถ", mask: "๐Ÿ˜ท", neutral_face: "๐Ÿ˜", expressionless: "๐Ÿ˜‘",
164
- unamused: "๐Ÿ˜’", sweat: "๐Ÿ˜“", pensive: "๐Ÿ˜”", confused: "๐Ÿ˜•", confounded: "๐Ÿ˜–",
165
- upside_down_face: "๐Ÿ™ƒ", money_mouth_face: "๐Ÿค‘", thinking_face: "๐Ÿค”", thinking: "๐Ÿค”",
166
- zipper_mouth_face: "๐Ÿค", nerd_face: "๐Ÿค“", hugs: "๐Ÿค—", rolling_eyes: "๐Ÿ™„",
167
- smirk: "๐Ÿ˜", drooling_face: "๐Ÿคค", lying_face: "๐Ÿคฅ",
168
- face_with_raised_eyebrow: "๐Ÿคจ", shushing_face: "๐Ÿคซ", face_with_hand_over_mouth: "๐Ÿคญ",
169
- face_vomiting: "๐Ÿคฎ", exploding_head: "๐Ÿคฏ", cowboy_hat_face: "๐Ÿค ",
170
- partying_face: "๐Ÿฅณ", disguised_face: "๐Ÿฅธ", pleading_face: "๐Ÿฅบ",
171
- skull: "๐Ÿ’€", skull_and_crossbones: "โ˜ ๏ธ", ghost: "๐Ÿ‘ป", alien: "๐Ÿ‘ฝ",
172
- robot: "๐Ÿค–", poop: "๐Ÿ’ฉ", hankey: "๐Ÿ’ฉ", clown_face: "๐Ÿคก",
173
-
174
- // hearts
175
- heart: "โค๏ธ", red_heart: "โค๏ธ", orange_heart: "๐Ÿงก", yellow_heart: "๐Ÿ’›",
176
- green_heart: "๐Ÿ’š", blue_heart: "๐Ÿ’™", purple_heart: "๐Ÿ’œ", black_heart: "๐Ÿ–ค",
177
- white_heart: "๐Ÿค", brown_heart: "๐ŸคŽ", broken_heart: "๐Ÿ’”", heart_exclamation: "โฃ๏ธ",
178
- two_hearts: "๐Ÿ’•", revolving_hearts: "๐Ÿ’ž", heartbeat: "๐Ÿ’“", heartpulse: "๐Ÿ’—",
179
- sparkling_heart: "๐Ÿ’–", cupid: "๐Ÿ’˜", gift_heart: "๐Ÿ’", heart_decoration: "๐Ÿ’Ÿ",
180
-
181
- // celebration
182
- tada: "๐ŸŽ‰", confetti_ball: "๐ŸŽŠ", balloon: "๐ŸŽˆ", birthday: "๐ŸŽ‚", gift: "๐ŸŽ",
183
- trophy: "๐Ÿ†", medal_military: "๐ŸŽ–๏ธ", medal_sports: "๐Ÿ…",
184
- first_place_medal: "๐Ÿฅ‡", second_place_medal: "๐Ÿฅˆ", third_place_medal: "๐Ÿฅ‰",
185
-
186
- // symbols
187
- fire: "๐Ÿ”ฅ", sparkles: "โœจ", star: "โญ", star2: "๐ŸŒŸ", dizzy: "๐Ÿ’ซ",
188
- boom: "๐Ÿ’ฅ", collision: "๐Ÿ’ฅ", zap: "โšก", lightning: "โšก",
189
- snowflake: "โ„๏ธ", cloud: "โ˜๏ธ", sunny: "โ˜€๏ธ", rainbow: "๐ŸŒˆ",
190
- rocket: "๐Ÿš€", airplane: "โœˆ๏ธ", 100: "๐Ÿ’ฏ",
191
- check: "โœ…", white_check_mark: "โœ…", ballot_box_with_check: "โ˜‘๏ธ", heavy_check_mark: "โœ”๏ธ",
192
- x: "โŒ", cross_mark: "โŒ", negative_squared_cross_mark: "โŽ",
193
- warning: "โš ๏ธ", exclamation: "โ—", question: "โ“",
194
- grey_exclamation: "โ•", grey_question: "โ”", bangbang: "โ€ผ๏ธ", interrobang: "โ‰๏ธ",
195
- bulb: "๐Ÿ’ก", memo: "๐Ÿ“", pencil: "โœ๏ธ", pencil2: "โœ๏ธ", pen: "๐Ÿ–Š๏ธ",
196
- lock: "๐Ÿ”’", unlock: "๐Ÿ”“", key: "๐Ÿ”‘", bell: "๐Ÿ””", no_bell: "๐Ÿ”•",
197
- bookmark: "๐Ÿ”–", link: "๐Ÿ”—", paperclip: "๐Ÿ“Ž", pushpin: "๐Ÿ“Œ", scissors: "โœ‚๏ธ",
198
- file_folder: "๐Ÿ“", open_file_folder: "๐Ÿ“‚", page_facing_up: "๐Ÿ“„", page_with_curl: "๐Ÿ“ƒ",
199
- calendar: "๐Ÿ“…", date: "๐Ÿ“…", clipboard: "๐Ÿ“‹",
200
- chart_with_upwards_trend: "๐Ÿ“ˆ", chart_with_downwards_trend: "๐Ÿ“‰", bar_chart: "๐Ÿ“Š",
201
- email: "๐Ÿ“ง", envelope: "โœ‰๏ธ", inbox_tray: "๐Ÿ“ฅ", outbox_tray: "๐Ÿ“ค",
202
- package: "๐Ÿ“ฆ", mailbox: "๐Ÿ“ซ", speech_balloon: "๐Ÿ’ฌ", thought_balloon: "๐Ÿ’ญ",
203
- mag: "๐Ÿ”", mag_right: "๐Ÿ”Ž", gear: "โš™๏ธ", wrench: "๐Ÿ”ง", hammer: "๐Ÿ”จ",
204
- hammer_and_wrench: "๐Ÿ› ๏ธ", tools: "๐Ÿ› ๏ธ", nut_and_bolt: "๐Ÿ”ฉ", shield: "๐Ÿ›ก๏ธ",
205
- hourglass: "โŒ›", hourglass_flowing_sand: "โณ", watch: "โŒš", alarm_clock: "โฐ",
206
- stopwatch: "โฑ๏ธ", timer_clock: "โฒ๏ธ",
207
-
208
- // animals
209
- see_no_evil: "๐Ÿ™ˆ", hear_no_evil: "๐Ÿ™‰", speak_no_evil: "๐Ÿ™Š",
210
- monkey: "๐Ÿ’", monkey_face: "๐Ÿต", dog: "๐Ÿ•", dog2: "๐Ÿ•", cat: "๐Ÿˆ", cat2: "๐Ÿˆ",
211
- tiger: "๐Ÿ…", tiger2: "๐Ÿ…", lion: "๐Ÿฆ", horse: "๐Ÿด", unicorn: "๐Ÿฆ„",
212
- cow: "๐Ÿ„", cow2: "๐Ÿ„", pig: "๐Ÿท", pig2: "๐Ÿท", chicken: "๐Ÿ”", penguin: "๐Ÿง",
213
- bird: "๐Ÿฆ", eagle: "๐Ÿฆ…", duck: "๐Ÿฆ†", owl: "๐Ÿฆ‰", bat: "๐Ÿฆ‡", wolf: "๐Ÿบ",
214
- fox_face: "๐ŸฆŠ", bear: "๐Ÿป", panda_face: "๐Ÿผ", koala: "๐Ÿจ",
215
- rabbit: "๐Ÿฐ", rabbit2: "๐Ÿ‡", mouse: "๐Ÿญ", mouse2: "๐Ÿ", rat: "๐Ÿ€", hamster: "๐Ÿน",
216
- frog: "๐Ÿธ", snake: "๐Ÿ", turtle: "๐Ÿข", lizard: "๐ŸฆŽ", dragon: "๐Ÿ‰", dragon_face: "๐Ÿฒ",
217
- whale: "๐Ÿ‹", whale2: "๐Ÿ‹", dolphin: "๐Ÿฌ", fish: "๐ŸŸ", tropical_fish: "๐Ÿ ",
218
- blowfish: "๐Ÿก", shark: "๐Ÿฆˆ", octopus: "๐Ÿ™", crab: "๐Ÿฆ€", lobster: "๐Ÿฆž",
219
- shrimp: "๐Ÿฆ", squid: "๐Ÿฆ‘", snail: "๐ŸŒ", butterfly: "๐Ÿฆ‹", bug: "๐Ÿ›",
220
- ant: "๐Ÿœ", bee: "๐Ÿ", honeybee: "๐Ÿ", spider: "๐Ÿ•ท๏ธ", spider_web: "๐Ÿ•ธ๏ธ",
221
-
222
- // food & drink
223
- apple: "๐ŸŽ", green_apple: "๐Ÿ", pear: "๐Ÿ", tangerine: "๐ŸŠ", orange: "๐ŸŠ",
224
- lemon: "๐Ÿ‹", banana: "๐ŸŒ", watermelon: "๐Ÿ‰", grapes: "๐Ÿ‡", strawberry: "๐Ÿ“",
225
- peach: "๐Ÿ‘", cherries: "๐Ÿ’", pizza: "๐Ÿ•", hamburger: "๐Ÿ”", fries: "๐ŸŸ",
226
- hotdog: "๐ŸŒญ", sandwich: "๐Ÿฅช", taco: "๐ŸŒฎ", burrito: "๐ŸŒฏ", egg: "๐Ÿฅš", cooking: "๐Ÿณ",
227
- cake: "๐Ÿฐ", cookie: "๐Ÿช", chocolate_bar: "๐Ÿซ", candy: "๐Ÿฌ", lollipop: "๐Ÿญ",
228
- ice_cream: "๐Ÿจ", icecream: "๐Ÿฆ", doughnut: "๐Ÿฉ",
229
- coffee: "โ˜•", tea: "๐Ÿต", beer: "๐Ÿบ", beers: "๐Ÿป", wine_glass: "๐Ÿท",
230
- cocktail: "๐Ÿธ", tropical_drink: "๐Ÿน", champagne: "๐Ÿพ",
231
-
232
- // objects
233
- computer: "๐Ÿ’ป", keyboard: "โŒจ๏ธ", desktop_computer: "๐Ÿ–ฅ๏ธ", printer: "๐Ÿ–จ๏ธ",
234
- mouse_three_button: "๐Ÿ–ฑ๏ธ", trackball: "๐Ÿ–ฒ๏ธ", joystick: "๐Ÿ•น๏ธ", video_game: "๐ŸŽฎ",
235
- phone: "๐Ÿ“ฑ", iphone: "๐Ÿ“ฑ", telephone: "โ˜Ž๏ธ", telephone_receiver: "๐Ÿ“ž",
236
- battery: "๐Ÿ”‹", electric_plug: "๐Ÿ”Œ", camera: "๐Ÿ“ท", camera_flash: "๐Ÿ“ธ",
237
- video_camera: "๐Ÿ“น", movie_camera: "๐ŸŽฅ", film_projector: "๐Ÿ“ฝ๏ธ", tv: "๐Ÿ“บ",
238
- radio: "๐Ÿ“ป", microphone: "๐ŸŽค", headphones: "๐ŸŽง", musical_note: "๐ŸŽต", notes: "๐ŸŽถ",
239
- art: "๐ŸŽจ", performing_arts: "๐ŸŽญ", tickets: "๐ŸŽŸ๏ธ", clapper: "๐ŸŽฌ",
240
- books: "๐Ÿ“š", book: "๐Ÿ“–", notebook: "๐Ÿ““", newspaper: "๐Ÿ“ฐ", scroll: "๐Ÿ“œ",
241
- moneybag: "๐Ÿ’ฐ", dollar: "๐Ÿ’ต", credit_card: "๐Ÿ’ณ", gem: "๐Ÿ’Ž", ring: "๐Ÿ’",
242
- crown: "๐Ÿ‘‘", tophat: "๐ŸŽฉ", necktie: "๐Ÿ‘”", shirt: "๐Ÿ‘•", jeans: "๐Ÿ‘–",
243
- dress: "๐Ÿ‘—", lipstick: "๐Ÿ’„", kiss: "๐Ÿ’‹", footprints: "๐Ÿ‘ฃ",
244
-
245
- // arrows
246
- arrow_up: "โฌ†๏ธ", arrow_down: "โฌ‡๏ธ", arrow_left: "โฌ…๏ธ", arrow_right: "โžก๏ธ",
247
- arrow_upper_left: "โ†–๏ธ", arrow_upper_right: "โ†—๏ธ", arrow_lower_left: "โ†™๏ธ", arrow_lower_right: "โ†˜๏ธ",
248
- left_right_arrow: "โ†”๏ธ", arrow_up_down: "โ†•๏ธ",
249
- arrows_counterclockwise: "๐Ÿ”„", arrows_clockwise: "๐Ÿ”ƒ",
250
- rewind: "โช", fast_forward: "โฉ", play_or_pause_button: "โฏ๏ธ",
251
- pause_button: "โธ๏ธ", stop_button: "โน๏ธ", record_button: "โบ๏ธ",
252
-
253
- // numbers
254
- zero: "0๏ธโƒฃ", one: "1๏ธโƒฃ", two: "2๏ธโƒฃ", three: "3๏ธโƒฃ", four: "4๏ธโƒฃ",
255
- five: "5๏ธโƒฃ", six: "6๏ธโƒฃ", seven: "7๏ธโƒฃ", eight: "8๏ธโƒฃ", nine: "9๏ธโƒฃ", keycap_ten: "๐Ÿ”Ÿ",
256
- };
257
-
258
- export function shortcodeToEmoji(shortcode: string): string {
259
- return EMOJI_MAP[shortcode] ?? `:${shortcode}:`;
260
- }
261
-
262
- export function formatReactions(reactions: { emoji: string; count: number }[]): string {
263
- if (reactions.length === 0) return "";
264
- return reactions.map((r) => {
265
- const emoji = shortcodeToEmoji(r.emoji);
266
- return `${emoji}${r.count > 1 ? r.count : ""}`;
267
- }).join(" ");
268
- }
269
-
270
- export function wrapText(text: string, width: number, indent: string): string[] {
271
- const lines: string[] = [];
272
- const paragraphs = text.split(/\n/);
273
-
274
- for (const paragraph of paragraphs) {
275
- if (paragraph.trim() === "") {
276
- lines.push("");
277
- continue;
278
- }
279
-
280
- const words = paragraph.split(/\s+/);
281
- let currentLine = "";
282
-
283
- for (const word of words) {
284
- if (currentLine.length + word.length + 1 <= width) {
285
- currentLine += (currentLine ? " " : "") + word;
286
- } else {
287
- if (currentLine) lines.push(indent + currentLine);
288
- currentLine = word;
289
- }
290
- }
291
- if (currentLine) lines.push(indent + currentLine);
292
- }
293
-
294
- return lines;
295
- }
296
-
297
- function getActorName(comment: Comment): string {
298
- return comment.externalUser ?? comment.user ?? comment.botActor ?? "unknown";
299
- }
300
-
301
- function getSourceLabel(comment: Comment): string {
302
- const sync = comment.syncedWith[0];
303
- if (!sync) return "";
304
- const serviceName = sync.service.charAt(0).toUpperCase() + sync.service.slice(1).toLowerCase();
305
- return ` via ${serviceName}`;
306
- }
307
-
308
- function getSyncChannelName(comment: Comment): string | undefined {
309
- const sync = comment.syncedWith[0];
310
- if (!sync) return undefined;
311
-
312
- if (sync.meta.type === "slack") {
313
- return sync.meta.channelName;
314
- }
315
- if (sync.meta.type === "github" && sync.meta.repo) {
316
- return sync.meta.owner ? `${sync.meta.owner}/${sync.meta.repo}` : sync.meta.repo;
317
- }
318
- if (sync.meta.type === "jira" && sync.meta.issueKey) {
319
- return sync.meta.issueKey;
320
- }
321
- return undefined;
322
- }
323
-
324
- function formatCommentHeader(
325
- comment: Comment,
326
- isThreadRoot: boolean,
327
- replyCount?: number,
328
- threadUrl?: string
329
- ): string {
330
- const sync = comment.syncedWith[0];
331
- const time = formatRelativeTime(comment.createdAt);
332
-
333
- if (isThreadRoot && sync) {
334
- const channelName = getSyncChannelName(comment);
335
- const channelPart = channelName ? ` in #${chalk.white(channelName)}` : "";
336
- const serviceName = sync.service.charAt(0).toUpperCase() + sync.service.slice(1).toLowerCase();
337
- let header = `${chalk.white(serviceName)} thread connected${channelPart} ${chalk.dim(time)}`;
338
-
339
- if (replyCount && replyCount > 3 && threadUrl) {
340
- header += `\nโ”” ${chalk.dim(`${replyCount - 3} previous replies,`)} [view all](${threadUrl})`;
341
- }
342
- return header;
343
- }
344
-
345
- const actor = chalk.white(`@${getActorName(comment)}`);
346
- const source = chalk.dim(getSourceLabel(comment));
347
- return `${actor} ${chalk.dim(time)}${source}`;
348
- }
349
-
350
- function outputSingleComment(comment: Comment, indent: string): void {
351
- const bodyLines = wrapText(comment.body.trim(), 60, indent + "โ”” ");
352
- for (const line of bodyLines) {
353
- console.log(line);
354
- }
355
-
356
- const reactions = formatReactions(comment.reactions);
357
- if (reactions) {
358
- console.log(`${indent}โ”” ${chalk.dim(`[${reactions}]`)}`);
359
- }
360
- }
361
-
362
- function renderCommentRecursive(
363
- comment: Comment,
364
- childMap: Map<string | null, Comment[]>,
365
- depth: number,
366
- maxRepliesPerLevel: number
367
- ): void {
368
- const indent = " ".repeat(depth);
369
- const header = formatCommentHeader(comment, false);
370
- console.log(`${indent}โ”” ${header}`);
371
- outputSingleComment(comment, indent + " ");
372
-
373
- const children = childMap.get(comment.id) ?? [];
374
- const recentChildren = children.slice(-maxRepliesPerLevel);
375
-
376
- for (const child of recentChildren) {
377
- renderCommentRecursive(child, childMap, depth + 1, maxRepliesPerLevel);
378
- }
379
- }
380
-
381
- export function outputCommentThreads(comments: Comment[], maxThreads = 3): void {
382
- if (comments.length === 0) {
383
- console.log(chalk.dim("no comments"));
384
- return;
385
- }
386
-
387
- const childMap = buildChildMap(comments);
388
- const rootComments = childMap.get(null) ?? [];
389
- const recentRoots = rootComments.slice(-maxThreads);
390
-
391
- for (let i = 0; i < recentRoots.length; i++) {
392
- const root = recentRoots[i];
393
- if (!root) continue;
394
-
395
- const children = childMap.get(root.id) ?? [];
396
- const totalReplies = children.length;
397
- const threadUrl = root.url;
398
- const hasSync = root.syncedWith.length > 0;
399
-
400
- console.log(formatCommentHeader(root, true, totalReplies, threadUrl));
401
-
402
- if (!hasSync) {
403
- outputSingleComment(root, "");
404
- }
405
-
406
- const recentChildren = children.slice(-3);
407
- for (const child of recentChildren) {
408
- renderCommentRecursive(child, childMap, 0, 3);
409
- }
410
-
411
- if (i < recentRoots.length - 1) {
412
- console.log();
413
- }
414
- }
415
- }
@@ -0,0 +1,300 @@
1
+ import chalk from "chalk";
2
+ import type { Comment } from "@bdsqqq/lnr-core";
3
+ import { formatRelativeTime } from "../output";
4
+
5
+ export function buildChildMap(comments: Comment[]): Map<string | null, Comment[]> {
6
+ const childMap = new Map<string | null, Comment[]>();
7
+
8
+ for (const c of comments) {
9
+ const parentKey = c.parentId ?? null;
10
+ const existing = childMap.get(parentKey) ?? [];
11
+ existing.push(c);
12
+ childMap.set(parentKey, existing);
13
+ }
14
+
15
+ for (const children of Array.from(childMap.values())) {
16
+ children.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
17
+ }
18
+
19
+ return childMap;
20
+ }
21
+
22
+ const EMOJI_MAP: Record<string, string> = {
23
+ // gestures
24
+ "+1": "๐Ÿ‘", thumbsup: "๐Ÿ‘", "-1": "๐Ÿ‘Ž", thumbsdown: "๐Ÿ‘Ž",
25
+ wave: "๐Ÿ‘‹", raised_back_of_hand: "๐Ÿคš", raised_hand: "โœ‹", hand: "โœ‹",
26
+ vulcan_salute: "๐Ÿ––", ok_hand: "๐Ÿ‘Œ", pinched_fingers: "๐ŸคŒ", pinching_hand: "๐Ÿค",
27
+ v: "โœŒ๏ธ", crossed_fingers: "๐Ÿคž", love_you_gesture: "๐ŸคŸ", metal: "๐Ÿค˜",
28
+ call_me_hand: "๐Ÿค™", point_left: "๐Ÿ‘ˆ", point_right: "๐Ÿ‘‰", point_up_2: "๐Ÿ‘†",
29
+ point_down: "๐Ÿ‘‡", point_up: "โ˜๏ธ", fist: "โœŠ", fist_raised: "โœŠ",
30
+ facepunch: "๐Ÿ‘Š", punch: "๐Ÿ‘Š", fist_oncoming: "๐Ÿ‘Š", fist_left: "๐Ÿค›", fist_right: "๐Ÿคœ",
31
+ clap: "๐Ÿ‘", raised_hands: "๐Ÿ™Œ", open_hands: "๐Ÿ‘", palms_up_together: "๐Ÿคฒ",
32
+ handshake: "๐Ÿค", pray: "๐Ÿ™", writing_hand: "โœ๏ธ", nail_care: "๐Ÿ’…",
33
+ selfie: "๐Ÿคณ", muscle: "๐Ÿ’ช", mechanical_arm: "๐Ÿฆพ",
34
+ eyes: "๐Ÿ‘€", eye: "๐Ÿ‘๏ธ", tongue: "๐Ÿ‘…", lips: "๐Ÿ‘„",
35
+
36
+ // smileys
37
+ grinning: "๐Ÿ˜€", smile: "๐Ÿ˜Š", grin: "๐Ÿ˜", joy: "๐Ÿ˜‚", rofl: "๐Ÿคฃ",
38
+ smiley: "๐Ÿ˜ƒ", sweat_smile: "๐Ÿ˜…", laughing: "๐Ÿ˜†", laugh: "๐Ÿ˜„",
39
+ wink: "๐Ÿ˜‰", blush: "๐Ÿ˜Š", yum: "๐Ÿ˜‹", sunglasses: "๐Ÿ˜Ž", heart_eyes: "๐Ÿ˜",
40
+ kissing_heart: "๐Ÿ’‹", kissing: "๐Ÿ˜—", relaxed: "โ˜บ๏ธ",
41
+ stuck_out_tongue: "๐Ÿ˜›", stuck_out_tongue_winking_eye: "๐Ÿ˜œ", stuck_out_tongue_closed_eyes: "๐Ÿ˜",
42
+ disappointed: "๐Ÿ˜ž", worried: "๐Ÿ˜Ÿ", angry: "๐Ÿ˜ ", rage: "๐Ÿ˜ก", pout: "๐Ÿ˜ก",
43
+ cry: "๐Ÿ˜ข", persevere: "๐Ÿ˜ฃ", triumph: "๐Ÿ˜ค", disappointed_relieved: "๐Ÿ˜ฅ",
44
+ frowning: "๐Ÿ˜ฆ", anguished: "๐Ÿ˜ง", fearful: "๐Ÿ˜จ", weary: "๐Ÿ˜ฉ",
45
+ sleepy: "๐Ÿ˜ช", tired_face: "๐Ÿ˜ซ", grimacing: "๐Ÿ˜ฌ", sob: "๐Ÿ˜ญ",
46
+ open_mouth: "๐Ÿ˜ฎ", hushed: "๐Ÿ˜ฏ", cold_sweat: "๐Ÿ˜ฐ", scream: "๐Ÿ˜ฑ",
47
+ astonished: "๐Ÿ˜ฒ", flushed: "๐Ÿ˜ณ", sleeping: "๐Ÿ˜ด", dizzy_face: "๐Ÿ˜ต",
48
+ no_mouth: "๐Ÿ˜ถ", mask: "๐Ÿ˜ท", neutral_face: "๐Ÿ˜", expressionless: "๐Ÿ˜‘",
49
+ unamused: "๐Ÿ˜’", sweat: "๐Ÿ˜“", pensive: "๐Ÿ˜”", confused: "๐Ÿ˜•", confounded: "๐Ÿ˜–",
50
+ upside_down_face: "๐Ÿ™ƒ", money_mouth_face: "๐Ÿค‘", thinking_face: "๐Ÿค”", thinking: "๐Ÿค”",
51
+ zipper_mouth_face: "๐Ÿค", nerd_face: "๐Ÿค“", hugs: "๐Ÿค—", rolling_eyes: "๐Ÿ™„",
52
+ smirk: "๐Ÿ˜", drooling_face: "๐Ÿคค", lying_face: "๐Ÿคฅ",
53
+ face_with_raised_eyebrow: "๐Ÿคจ", shushing_face: "๐Ÿคซ", face_with_hand_over_mouth: "๐Ÿคญ",
54
+ face_vomiting: "๐Ÿคฎ", exploding_head: "๐Ÿคฏ", cowboy_hat_face: "๐Ÿค ",
55
+ partying_face: "๐Ÿฅณ", disguised_face: "๐Ÿฅธ", pleading_face: "๐Ÿฅบ",
56
+ skull: "๐Ÿ’€", skull_and_crossbones: "โ˜ ๏ธ", ghost: "๐Ÿ‘ป", alien: "๐Ÿ‘ฝ",
57
+ robot: "๐Ÿค–", poop: "๐Ÿ’ฉ", hankey: "๐Ÿ’ฉ", clown_face: "๐Ÿคก",
58
+
59
+ // hearts
60
+ heart: "โค๏ธ", red_heart: "โค๏ธ", orange_heart: "๐Ÿงก", yellow_heart: "๐Ÿ’›",
61
+ green_heart: "๐Ÿ’š", blue_heart: "๐Ÿ’™", purple_heart: "๐Ÿ’œ", black_heart: "๐Ÿ–ค",
62
+ white_heart: "๐Ÿค", brown_heart: "๐ŸคŽ", broken_heart: "๐Ÿ’”", heart_exclamation: "โฃ๏ธ",
63
+ two_hearts: "๐Ÿ’•", revolving_hearts: "๐Ÿ’ž", heartbeat: "๐Ÿ’“", heartpulse: "๐Ÿ’—",
64
+ sparkling_heart: "๐Ÿ’–", cupid: "๐Ÿ’˜", gift_heart: "๐Ÿ’", heart_decoration: "๐Ÿ’Ÿ",
65
+
66
+ // celebration
67
+ tada: "๐ŸŽ‰", confetti_ball: "๐ŸŽŠ", balloon: "๐ŸŽˆ", birthday: "๐ŸŽ‚", gift: "๐ŸŽ",
68
+ trophy: "๐Ÿ†", medal_military: "๐ŸŽ–๏ธ", medal_sports: "๐Ÿ…",
69
+ first_place_medal: "๐Ÿฅ‡", second_place_medal: "๐Ÿฅˆ", third_place_medal: "๐Ÿฅ‰",
70
+
71
+ // symbols
72
+ fire: "๐Ÿ”ฅ", sparkles: "โœจ", star: "โญ", star2: "๐ŸŒŸ", dizzy: "๐Ÿ’ซ",
73
+ boom: "๐Ÿ’ฅ", collision: "๐Ÿ’ฅ", zap: "โšก", lightning: "โšก",
74
+ snowflake: "โ„๏ธ", cloud: "โ˜๏ธ", sunny: "โ˜€๏ธ", rainbow: "๐ŸŒˆ",
75
+ rocket: "๐Ÿš€", airplane: "โœˆ๏ธ", 100: "๐Ÿ’ฏ",
76
+ check: "โœ…", white_check_mark: "โœ…", ballot_box_with_check: "โ˜‘๏ธ", heavy_check_mark: "โœ”๏ธ",
77
+ x: "โŒ", cross_mark: "โŒ", negative_squared_cross_mark: "โŽ",
78
+ warning: "โš ๏ธ", exclamation: "โ—", question: "โ“",
79
+ grey_exclamation: "โ•", grey_question: "โ”", bangbang: "โ€ผ๏ธ", interrobang: "โ‰๏ธ",
80
+ bulb: "๐Ÿ’ก", memo: "๐Ÿ“", pencil: "โœ๏ธ", pencil2: "โœ๏ธ", pen: "๐Ÿ–Š๏ธ",
81
+ lock: "๐Ÿ”’", unlock: "๐Ÿ”“", key: "๐Ÿ”‘", bell: "๐Ÿ””", no_bell: "๐Ÿ”•",
82
+ bookmark: "๐Ÿ”–", link: "๐Ÿ”—", paperclip: "๐Ÿ“Ž", pushpin: "๐Ÿ“Œ", scissors: "โœ‚๏ธ",
83
+ file_folder: "๐Ÿ“", open_file_folder: "๐Ÿ“‚", page_facing_up: "๐Ÿ“„", page_with_curl: "๐Ÿ“ƒ",
84
+ calendar: "๐Ÿ“…", date: "๐Ÿ“…", clipboard: "๐Ÿ“‹",
85
+ chart_with_upwards_trend: "๐Ÿ“ˆ", chart_with_downwards_trend: "๐Ÿ“‰", bar_chart: "๐Ÿ“Š",
86
+ email: "๐Ÿ“ง", envelope: "โœ‰๏ธ", inbox_tray: "๐Ÿ“ฅ", outbox_tray: "๐Ÿ“ค",
87
+ package: "๐Ÿ“ฆ", mailbox: "๐Ÿ“ซ", speech_balloon: "๐Ÿ’ฌ", thought_balloon: "๐Ÿ’ญ",
88
+ mag: "๐Ÿ”", mag_right: "๐Ÿ”Ž", gear: "โš™๏ธ", wrench: "๐Ÿ”ง", hammer: "๐Ÿ”จ",
89
+ hammer_and_wrench: "๐Ÿ› ๏ธ", tools: "๐Ÿ› ๏ธ", nut_and_bolt: "๐Ÿ”ฉ", shield: "๐Ÿ›ก๏ธ",
90
+ hourglass: "โŒ›", hourglass_flowing_sand: "โณ", watch: "โŒš", alarm_clock: "โฐ",
91
+ stopwatch: "โฑ๏ธ", timer_clock: "โฒ๏ธ",
92
+
93
+ // animals
94
+ see_no_evil: "๐Ÿ™ˆ", hear_no_evil: "๐Ÿ™‰", speak_no_evil: "๐Ÿ™Š",
95
+ monkey: "๐Ÿ’", monkey_face: "๐Ÿต", dog: "๐Ÿ•", dog2: "๐Ÿ•", cat: "๐Ÿˆ", cat2: "๐Ÿˆ",
96
+ tiger: "๐Ÿ…", tiger2: "๐Ÿ…", lion: "๐Ÿฆ", horse: "๐Ÿด", unicorn: "๐Ÿฆ„",
97
+ cow: "๐Ÿ„", cow2: "๐Ÿ„", pig: "๐Ÿท", pig2: "๐Ÿท", chicken: "๐Ÿ”", penguin: "๐Ÿง",
98
+ bird: "๐Ÿฆ", eagle: "๐Ÿฆ…", duck: "๐Ÿฆ†", owl: "๐Ÿฆ‰", bat: "๐Ÿฆ‡", wolf: "๐Ÿบ",
99
+ fox_face: "๐ŸฆŠ", bear: "๐Ÿป", panda_face: "๐Ÿผ", koala: "๐Ÿจ",
100
+ rabbit: "๐Ÿฐ", rabbit2: "๐Ÿ‡", mouse: "๐Ÿญ", mouse2: "๐Ÿ", rat: "๐Ÿ€", hamster: "๐Ÿน",
101
+ frog: "๐Ÿธ", snake: "๐Ÿ", turtle: "๐Ÿข", lizard: "๐ŸฆŽ", dragon: "๐Ÿ‰", dragon_face: "๐Ÿฒ",
102
+ whale: "๐Ÿ‹", whale2: "๐Ÿ‹", dolphin: "๐Ÿฌ", fish: "๐ŸŸ", tropical_fish: "๐Ÿ ",
103
+ blowfish: "๐Ÿก", shark: "๐Ÿฆˆ", octopus: "๐Ÿ™", crab: "๐Ÿฆ€", lobster: "๐Ÿฆž",
104
+ shrimp: "๐Ÿฆ", squid: "๐Ÿฆ‘", snail: "๐ŸŒ", butterfly: "๐Ÿฆ‹", bug: "๐Ÿ›",
105
+ ant: "๐Ÿœ", bee: "๐Ÿ", honeybee: "๐Ÿ", spider: "๐Ÿ•ท๏ธ", spider_web: "๐Ÿ•ธ๏ธ",
106
+
107
+ // food & drink
108
+ apple: "๐ŸŽ", green_apple: "๐Ÿ", pear: "๐Ÿ", tangerine: "๐ŸŠ", orange: "๐ŸŠ",
109
+ lemon: "๐Ÿ‹", banana: "๐ŸŒ", watermelon: "๐Ÿ‰", grapes: "๐Ÿ‡", strawberry: "๐Ÿ“",
110
+ peach: "๐Ÿ‘", cherries: "๐Ÿ’", pizza: "๐Ÿ•", hamburger: "๐Ÿ”", fries: "๐ŸŸ",
111
+ hotdog: "๐ŸŒญ", sandwich: "๐Ÿฅช", taco: "๐ŸŒฎ", burrito: "๐ŸŒฏ", egg: "๐Ÿฅš", cooking: "๐Ÿณ",
112
+ cake: "๐Ÿฐ", cookie: "๐Ÿช", chocolate_bar: "๐Ÿซ", candy: "๐Ÿฌ", lollipop: "๐Ÿญ",
113
+ ice_cream: "๐Ÿจ", icecream: "๐Ÿฆ", doughnut: "๐Ÿฉ",
114
+ coffee: "โ˜•", tea: "๐Ÿต", beer: "๐Ÿบ", beers: "๐Ÿป", wine_glass: "๐Ÿท",
115
+ cocktail: "๐Ÿธ", tropical_drink: "๐Ÿน", champagne: "๐Ÿพ",
116
+
117
+ // objects
118
+ computer: "๐Ÿ’ป", keyboard: "โŒจ๏ธ", desktop_computer: "๐Ÿ–ฅ๏ธ", printer: "๐Ÿ–จ๏ธ",
119
+ mouse_three_button: "๐Ÿ–ฑ๏ธ", trackball: "๐Ÿ–ฒ๏ธ", joystick: "๐Ÿ•น๏ธ", video_game: "๐ŸŽฎ",
120
+ phone: "๐Ÿ“ฑ", iphone: "๐Ÿ“ฑ", telephone: "โ˜Ž๏ธ", telephone_receiver: "๐Ÿ“ž",
121
+ battery: "๐Ÿ”‹", electric_plug: "๐Ÿ”Œ", camera: "๐Ÿ“ท", camera_flash: "๐Ÿ“ธ",
122
+ video_camera: "๐Ÿ“น", movie_camera: "๐ŸŽฅ", film_projector: "๐Ÿ“ฝ๏ธ", tv: "๐Ÿ“บ",
123
+ radio: "๐Ÿ“ป", microphone: "๐ŸŽค", headphones: "๐ŸŽง", musical_note: "๐ŸŽต", notes: "๐ŸŽถ",
124
+ art: "๐ŸŽจ", performing_arts: "๐ŸŽญ", tickets: "๐ŸŽŸ๏ธ", clapper: "๐ŸŽฌ",
125
+ books: "๐Ÿ“š", book: "๐Ÿ“–", notebook: "๐Ÿ““", newspaper: "๐Ÿ“ฐ", scroll: "๐Ÿ“œ",
126
+ moneybag: "๐Ÿ’ฐ", dollar: "๐Ÿ’ต", credit_card: "๐Ÿ’ณ", gem: "๐Ÿ’Ž", ring: "๐Ÿ’",
127
+ crown: "๐Ÿ‘‘", tophat: "๐ŸŽฉ", necktie: "๐Ÿ‘”", shirt: "๐Ÿ‘•", jeans: "๐Ÿ‘–",
128
+ dress: "๐Ÿ‘—", lipstick: "๐Ÿ’„", kiss: "๐Ÿ’‹", footprints: "๐Ÿ‘ฃ",
129
+
130
+ // arrows
131
+ arrow_up: "โฌ†๏ธ", arrow_down: "โฌ‡๏ธ", arrow_left: "โฌ…๏ธ", arrow_right: "โžก๏ธ",
132
+ arrow_upper_left: "โ†–๏ธ", arrow_upper_right: "โ†—๏ธ", arrow_lower_left: "โ†™๏ธ", arrow_lower_right: "โ†˜๏ธ",
133
+ left_right_arrow: "โ†”๏ธ", arrow_up_down: "โ†•๏ธ",
134
+ arrows_counterclockwise: "๐Ÿ”„", arrows_clockwise: "๐Ÿ”ƒ",
135
+ rewind: "โช", fast_forward: "โฉ", play_or_pause_button: "โฏ๏ธ",
136
+ pause_button: "โธ๏ธ", stop_button: "โน๏ธ", record_button: "โบ๏ธ",
137
+
138
+ // numbers
139
+ zero: "0๏ธโƒฃ", one: "1๏ธโƒฃ", two: "2๏ธโƒฃ", three: "3๏ธโƒฃ", four: "4๏ธโƒฃ",
140
+ five: "5๏ธโƒฃ", six: "6๏ธโƒฃ", seven: "7๏ธโƒฃ", eight: "8๏ธโƒฃ", nine: "9๏ธโƒฃ", keycap_ten: "๐Ÿ”Ÿ",
141
+ };
142
+
143
+ export function shortcodeToEmoji(shortcode: string): string {
144
+ return EMOJI_MAP[shortcode] ?? `:${shortcode}:`;
145
+ }
146
+
147
+ export function formatReactions(reactions: { emoji: string; count: number }[]): string {
148
+ if (reactions.length === 0) return "";
149
+ return reactions.map((r) => {
150
+ const emoji = shortcodeToEmoji(r.emoji);
151
+ return `${emoji}${r.count > 1 ? r.count : ""}`;
152
+ }).join(" ");
153
+ }
154
+
155
+ export function wrapText(text: string, width: number, indent: string): string[] {
156
+ const lines: string[] = [];
157
+ const paragraphs = text.split(/\n/);
158
+
159
+ for (const paragraph of paragraphs) {
160
+ if (paragraph.trim() === "") {
161
+ lines.push("");
162
+ continue;
163
+ }
164
+
165
+ const words = paragraph.split(/\s+/);
166
+ let currentLine = "";
167
+
168
+ for (const word of words) {
169
+ if (currentLine.length + word.length + 1 <= width) {
170
+ currentLine += (currentLine ? " " : "") + word;
171
+ } else {
172
+ if (currentLine) lines.push(indent + currentLine);
173
+ currentLine = word;
174
+ }
175
+ }
176
+ if (currentLine) lines.push(indent + currentLine);
177
+ }
178
+
179
+ return lines;
180
+ }
181
+
182
+ function getActorName(comment: Comment): string {
183
+ return comment.externalUser ?? comment.user ?? comment.botActor ?? "unknown";
184
+ }
185
+
186
+ function getSourceLabel(comment: Comment): string {
187
+ const sync = comment.syncedWith[0];
188
+ if (!sync) return "";
189
+ const serviceName = sync.service.charAt(0).toUpperCase() + sync.service.slice(1).toLowerCase();
190
+ return ` via ${serviceName}`;
191
+ }
192
+
193
+ function getSyncChannelName(comment: Comment): string | undefined {
194
+ const sync = comment.syncedWith[0];
195
+ if (!sync) return undefined;
196
+
197
+ if (sync.meta.type === "slack") {
198
+ return sync.meta.channelName;
199
+ }
200
+ if (sync.meta.type === "github" && sync.meta.repo) {
201
+ return sync.meta.owner ? `${sync.meta.owner}/${sync.meta.repo}` : sync.meta.repo;
202
+ }
203
+ if (sync.meta.type === "jira" && sync.meta.issueKey) {
204
+ return sync.meta.issueKey;
205
+ }
206
+ return undefined;
207
+ }
208
+
209
+ function formatCommentHeader(
210
+ comment: Comment,
211
+ isThreadRoot: boolean,
212
+ replyCount?: number,
213
+ threadUrl?: string
214
+ ): string {
215
+ const sync = comment.syncedWith[0];
216
+ const time = formatRelativeTime(comment.createdAt);
217
+
218
+ if (isThreadRoot && sync) {
219
+ const channelName = getSyncChannelName(comment);
220
+ const channelPart = channelName ? ` in #${chalk.white(channelName)}` : "";
221
+ const serviceName = sync.service.charAt(0).toUpperCase() + sync.service.slice(1).toLowerCase();
222
+ let header = `${chalk.white(serviceName)} thread connected${channelPart} ${chalk.dim(time)}`;
223
+
224
+ if (replyCount && replyCount > 3 && threadUrl) {
225
+ header += `\nโ”” ${chalk.dim(`${replyCount - 3} previous replies,`)} [view all](${threadUrl})`;
226
+ }
227
+ return header;
228
+ }
229
+
230
+ const actor = chalk.white(`@${getActorName(comment)}`);
231
+ const source = chalk.dim(getSourceLabel(comment));
232
+ return `${actor} ${chalk.dim(time)}${source}`;
233
+ }
234
+
235
+ function outputSingleComment(comment: Comment, indent: string): void {
236
+ const bodyLines = wrapText(comment.body.trim(), 60, indent + "โ”” ");
237
+ for (const line of bodyLines) {
238
+ console.log(line);
239
+ }
240
+
241
+ const reactions = formatReactions(comment.reactions);
242
+ if (reactions) {
243
+ console.log(`${indent}โ”” ${chalk.dim(`[${reactions}]`)}`);
244
+ }
245
+ }
246
+
247
+ function renderCommentRecursive(
248
+ comment: Comment,
249
+ childMap: Map<string | null, Comment[]>,
250
+ depth: number,
251
+ maxRepliesPerLevel: number
252
+ ): void {
253
+ const indent = " ".repeat(depth);
254
+ const header = formatCommentHeader(comment, false);
255
+ console.log(`${indent}โ”” ${header}`);
256
+ outputSingleComment(comment, indent + " ");
257
+
258
+ const children = childMap.get(comment.id) ?? [];
259
+ const recentChildren = children.slice(-maxRepliesPerLevel);
260
+
261
+ for (const child of recentChildren) {
262
+ renderCommentRecursive(child, childMap, depth + 1, maxRepliesPerLevel);
263
+ }
264
+ }
265
+
266
+ export function outputCommentThreads(comments: Comment[], maxThreads = 3): void {
267
+ if (comments.length === 0) {
268
+ console.log(chalk.dim("no comments"));
269
+ return;
270
+ }
271
+
272
+ const childMap = buildChildMap(comments);
273
+ const rootComments = childMap.get(null) ?? [];
274
+ const recentRoots = rootComments.slice(-maxThreads);
275
+
276
+ for (let i = 0; i < recentRoots.length; i++) {
277
+ const root = recentRoots[i];
278
+ if (!root) continue;
279
+
280
+ const children = childMap.get(root.id) ?? [];
281
+ const totalReplies = children.length;
282
+ const threadUrl = root.url;
283
+ const hasSync = root.syncedWith.length > 0;
284
+
285
+ console.log(formatCommentHeader(root, true, totalReplies, threadUrl));
286
+
287
+ if (!hasSync) {
288
+ outputSingleComment(root, "");
289
+ }
290
+
291
+ const recentChildren = children.slice(-3);
292
+ for (const child of recentChildren) {
293
+ renderCommentRecursive(child, childMap, 0, 3);
294
+ }
295
+
296
+ if (i < recentRoots.length - 1) {
297
+ console.log();
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,61 @@
1
+ import chalk from "chalk";
2
+
3
+ export interface HeaderSection {
4
+ type: "header";
5
+ title: string;
6
+ subtitle?: string;
7
+ }
8
+
9
+ export interface FieldsSection {
10
+ type: "fields";
11
+ fields: { label: string; value: string }[];
12
+ }
13
+
14
+ export interface TextSection {
15
+ type: "text";
16
+ body: string;
17
+ }
18
+
19
+ export interface DividerSection {
20
+ type: "divider";
21
+ }
22
+
23
+ export type DetailSection =
24
+ | HeaderSection
25
+ | FieldsSection
26
+ | TextSection
27
+ | DividerSection;
28
+
29
+ export function outputDetail(sections: DetailSection[]): void {
30
+ for (const section of sections) {
31
+ switch (section.type) {
32
+ case "header": {
33
+ console.log(section.title);
34
+ if (section.subtitle) {
35
+ console.log(section.subtitle);
36
+ }
37
+ break;
38
+ }
39
+ case "fields": {
40
+ const maxLabelWidth = Math.max(
41
+ ...section.fields.map((f) => f.label.length)
42
+ );
43
+ console.log();
44
+ for (const field of section.fields) {
45
+ const paddedLabel = field.label.padEnd(maxLabelWidth);
46
+ console.log(`${paddedLabel} ${field.value}`);
47
+ }
48
+ break;
49
+ }
50
+ case "text": {
51
+ console.log();
52
+ console.log(section.body);
53
+ break;
54
+ }
55
+ case "divider": {
56
+ console.log(chalk.dim("โ”€".repeat(40)));
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,2 @@
1
+ export { outputDetail, type DetailSection, type HeaderSection, type FieldsSection, type TextSection, type DividerSection } from "./detail";
2
+ export { outputCommentThreads, buildChildMap, shortcodeToEmoji, formatReactions, wrapText } from "./comments";