@myvillage/cli 1.1.2 → 1.2.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,352 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ── Time Formatting ─────────────────────────────────────
4
+
5
+ export function relativeTime(dateString) {
6
+ const ms = Date.now() - new Date(dateString).getTime();
7
+ const seconds = Math.floor(ms / 1000);
8
+ const minutes = Math.floor(seconds / 60);
9
+ const hours = Math.floor(minutes / 60);
10
+ const days = Math.floor(hours / 24);
11
+ const weeks = Math.floor(days / 7);
12
+ const months = Math.floor(days / 30);
13
+
14
+ if (seconds < 60) return 'just now';
15
+ if (minutes < 60) return `${minutes}m ago`;
16
+ if (hours < 24) return `${hours}h ago`;
17
+ if (days < 7) return `${days}d ago`;
18
+ if (weeks < 5) return `${weeks}w ago`;
19
+ return `${months}mo ago`;
20
+ }
21
+
22
+ export function formatDate(dateString) {
23
+ return new Date(dateString).toLocaleDateString('en-US', {
24
+ year: 'numeric',
25
+ month: 'short',
26
+ day: 'numeric',
27
+ hour: 'numeric',
28
+ minute: '2-digit',
29
+ });
30
+ }
31
+
32
+ // ── Text Helpers ────────────────────────────────────────
33
+
34
+ export function truncate(text, maxLen = 160) {
35
+ if (!text) return '';
36
+ const clean = text.replace(/\n+/g, ' ').trim();
37
+ if (clean.length <= maxLen) return clean;
38
+ return clean.slice(0, maxLen - 3) + '...';
39
+ }
40
+
41
+ function padRight(str, len) {
42
+ if (str.length >= len) return str;
43
+ return str + ' '.repeat(len - str.length);
44
+ }
45
+
46
+ // ── Post Type Badge ─────────────────────────────────────
47
+
48
+ const POST_TYPE_COLORS = {
49
+ DISCUSSION: chalk.white,
50
+ QUESTION: chalk.cyan,
51
+ PROJECT_SHOWCASE: chalk.green,
52
+ TUTORIAL: chalk.yellow,
53
+ BOUNTY: chalk.magenta,
54
+ ANNOUNCEMENT: chalk.red,
55
+ GAME_SHOWCASE: chalk.green,
56
+ };
57
+
58
+ export function postTypeBadge(type) {
59
+ const colorFn = POST_TYPE_COLORS[type] || chalk.white;
60
+ return colorFn(`[${type}]`);
61
+ }
62
+
63
+ // ── Votes ───────────────────────────────────────────────
64
+
65
+ export function formatVotes(upvotes = 0, downvotes = 0) {
66
+ const up = chalk.green(`▲ ${upvotes}`);
67
+ const down = downvotes > 0 ? chalk.red(` ▼ ${downvotes}`) : '';
68
+ return up + down;
69
+ }
70
+
71
+ // ── Author ──────────────────────────────────────────────
72
+
73
+ export function formatAuthor(post) {
74
+ if (post.authorType === 'AGENT' && post.agentProfile) {
75
+ return chalk.blue(`@${post.agentProfile.handle}`) + chalk.dim(' (agent)');
76
+ }
77
+ if (post.villager) {
78
+ const name = [post.villager.firstName, post.villager.lastName].filter(Boolean).join(' ');
79
+ return chalk.white(`@${name || post.villager.id}`);
80
+ }
81
+ return chalk.dim('unknown');
82
+ }
83
+
84
+ // ── Post Card (feed/list item) ──────────────────────────
85
+
86
+ export function formatPostCard(post) {
87
+ const lines = [];
88
+ const badge = postTypeBadge(post.postType);
89
+ const title = post.title || truncate(post.body, 60);
90
+
91
+ lines.push(` ${chalk.dim('┌')} ${badge} ${chalk.bold(title)}`);
92
+
93
+ const community = post.community
94
+ ? chalk.dim(`r/${post.community.slug}`)
95
+ : '';
96
+ const author = formatAuthor(post);
97
+ const time = chalk.dim(relativeTime(post.createdAt));
98
+ lines.push(` ${chalk.dim('│')} ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time}`);
99
+
100
+ if (post.body) {
101
+ const preview = truncate(post.body, 140);
102
+ lines.push(` ${chalk.dim('│')} ${preview}`);
103
+ }
104
+
105
+ const votes = formatVotes(post.upvoteCount, post.downvoteCount);
106
+ const comments = chalk.dim(`${post.commentCount ?? post._count?.comments ?? 0} comments`);
107
+ lines.push(` ${chalk.dim('│')} ${votes} ${chalk.dim('·')} ${comments}`);
108
+ lines.push(` ${chalk.dim('└')}`);
109
+
110
+ return lines.join('\n');
111
+ }
112
+
113
+ export function formatPostList(posts) {
114
+ if (!posts || posts.length === 0) {
115
+ console.log(chalk.dim('\n No posts found.\n'));
116
+ return;
117
+ }
118
+ console.log('');
119
+ for (const post of posts) {
120
+ console.log(formatPostCard(post));
121
+ console.log('');
122
+ }
123
+ }
124
+
125
+ // ── Post Detail (full view) ─────────────────────────────
126
+
127
+ export function formatPostDetail(post) {
128
+ const lines = [];
129
+
130
+ lines.push('');
131
+ lines.push(` ${postTypeBadge(post.postType)} ${chalk.bold(post.title || '(untitled)')}`);
132
+
133
+ const community = post.community
134
+ ? chalk.dim(`r/${post.community.slug}`)
135
+ : '';
136
+ const author = formatAuthor(post);
137
+ const time = chalk.dim(formatDate(post.createdAt));
138
+ lines.push(` ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time}`);
139
+ lines.push('');
140
+
141
+ if (post.body) {
142
+ const bodyLines = post.body.split('\n');
143
+ for (const line of bodyLines) {
144
+ lines.push(` ${line}`);
145
+ }
146
+ lines.push('');
147
+ }
148
+
149
+ if (post.tags?.length) {
150
+ lines.push(` ${chalk.dim('Tags:')} ${post.tags.map(t => chalk.cyan(t)).join(', ')}`);
151
+ }
152
+
153
+ const votes = formatVotes(post.upvoteCount, post.downvoteCount);
154
+ const commentCount = post.commentCount ?? post._count?.comments ?? 0;
155
+ lines.push(` ${votes} ${chalk.dim('·')} ${chalk.dim(`${commentCount} comments`)}`);
156
+ lines.push(` ${chalk.dim('ID:')} ${chalk.dim(post.id)}`);
157
+
158
+ console.log(lines.join('\n'));
159
+ }
160
+
161
+ // ── Comments (threaded) ─────────────────────────────────
162
+
163
+ export function formatComment(comment, depth = 0) {
164
+ const indent = ' ' + ' '.repeat(depth);
165
+ const lines = [];
166
+
167
+ const author = comment.authorType === 'AGENT' && comment.agentProfile
168
+ ? chalk.blue(`@${comment.agentProfile.handle}`) + chalk.dim(' (agent)')
169
+ : comment.villager
170
+ ? chalk.white(`@${[comment.villager.firstName, comment.villager.lastName].filter(Boolean).join(' ') || comment.villager.id}`)
171
+ : chalk.dim('unknown');
172
+
173
+ const time = chalk.dim(relativeTime(comment.createdAt));
174
+ const votes = chalk.green(`▲ ${comment.upvoteCount || 0}`);
175
+ const helpful = comment.isHelpful ? chalk.green(' ✓ helpful') : '';
176
+
177
+ lines.push(`${indent}${author} ${chalk.dim('·')} ${time}${' '.repeat(Math.max(0, 40 - depth * 4))}${votes}${helpful}`);
178
+
179
+ if (comment.body) {
180
+ const bodyLines = comment.body.split('\n');
181
+ for (const line of bodyLines) {
182
+ lines.push(`${indent}${line}`);
183
+ }
184
+ }
185
+
186
+ console.log(lines.join('\n'));
187
+
188
+ if (comment.replies?.length) {
189
+ for (const reply of comment.replies) {
190
+ console.log('');
191
+ formatComment(reply, depth + 1);
192
+ }
193
+ }
194
+ }
195
+
196
+ export function formatCommentThread(comments) {
197
+ if (!comments || comments.length === 0) {
198
+ console.log(chalk.dim('\n No comments yet.\n'));
199
+ return;
200
+ }
201
+
202
+ console.log(`\n ${chalk.dim('── Comments ──────────────────────────────────────────────')}\n`);
203
+ for (const comment of comments) {
204
+ formatComment(comment, 0);
205
+ console.log('');
206
+ }
207
+ }
208
+
209
+ // ── Community Formatting ────────────────────────────────
210
+
211
+ export function formatCommunityRow(community) {
212
+ const slug = chalk.cyan(padRight(community.slug, 28));
213
+ const name = padRight(community.name, 30);
214
+ const members = chalk.dim(`${community.memberCount ?? 0} members`);
215
+ const desc = community.description ? chalk.dim(truncate(community.description, 60)) : '';
216
+
217
+ console.log(` ${slug} ${name} ${members}`);
218
+ if (desc) {
219
+ console.log(` ${chalk.dim(' ')}${desc}`);
220
+ }
221
+ }
222
+
223
+ export function formatCommunityList(communities) {
224
+ if (!communities || communities.length === 0) {
225
+ console.log(chalk.dim('\n No communities found.\n'));
226
+ return;
227
+ }
228
+ console.log('');
229
+ for (const community of communities) {
230
+ formatCommunityRow(community);
231
+ console.log('');
232
+ }
233
+ }
234
+
235
+ export function formatCommunityDetail(community) {
236
+ const lines = [];
237
+
238
+ lines.push('');
239
+ lines.push(` ${chalk.bold(community.name)}`);
240
+ if (community.description) {
241
+ lines.push(` ${community.description}`);
242
+ }
243
+ lines.push(` ${chalk.dim('─'.repeat(50))}`);
244
+ lines.push('');
245
+ lines.push(` ${chalk.dim('Slug:')} ${chalk.cyan(community.slug)}`);
246
+ lines.push(` ${chalk.dim('Members:')} ${community.memberCount ?? 0}`);
247
+ lines.push(` ${chalk.dim('Posts:')} ${community.postCount ?? 0}`);
248
+ lines.push(` ${chalk.dim('Created:')} ${formatDate(community.createdAt)}`);
249
+
250
+ if (community.tags?.length) {
251
+ lines.push(` ${chalk.dim('Tags:')} ${community.tags.map(t => chalk.cyan(t)).join(', ')}`);
252
+ }
253
+
254
+ if (community.rules) {
255
+ lines.push('');
256
+ lines.push(` ${chalk.dim('Rules:')}`);
257
+ for (const line of community.rules.split('\n')) {
258
+ lines.push(` ${line}`);
259
+ }
260
+ }
261
+
262
+ lines.push('');
263
+ console.log(lines.join('\n'));
264
+ }
265
+
266
+ // ── Profile Formatting ──────────────────────────────────
267
+
268
+ export function formatProfile(profile) {
269
+ const lines = [];
270
+
271
+ lines.push('');
272
+ const handle = profile.handle || profile.displayName || [profile.firstName, profile.lastName].filter(Boolean).join(' ');
273
+ lines.push(` ${chalk.bold(`@${handle}`)}`);
274
+
275
+ if (profile.bio) {
276
+ lines.push(` ${profile.bio}`);
277
+ }
278
+ lines.push(` ${chalk.dim('─'.repeat(50))}`);
279
+ lines.push('');
280
+
281
+ if (profile.totalPosts !== undefined) {
282
+ lines.push(` ${chalk.dim('Posts:')} ${profile.totalPosts}`);
283
+ }
284
+ if (profile.totalComments !== undefined) {
285
+ lines.push(` ${chalk.dim('Comments:')} ${profile.totalComments}`);
286
+ }
287
+ if (profile.karmaScore !== undefined) {
288
+ lines.push(` ${chalk.dim('Karma:')} ${profile.karmaScore}`);
289
+ }
290
+ if (profile.mvtBalance !== undefined) {
291
+ lines.push(` ${chalk.dim('MVT Balance:')} ${profile.mvtBalance} tokens`);
292
+ }
293
+
294
+ if (profile.interests?.length) {
295
+ lines.push(` ${chalk.dim('Interests:')} ${profile.interests.map(t => chalk.cyan(t)).join(', ')}`);
296
+ }
297
+
298
+ lines.push('');
299
+ console.log(lines.join('\n'));
300
+ }
301
+
302
+ // ── Search Results ──────────────────────────────────────
303
+
304
+ export function formatSearchResults(results) {
305
+ if (!results) {
306
+ console.log(chalk.dim('\n No results found.\n'));
307
+ return;
308
+ }
309
+
310
+ console.log('');
311
+
312
+ if (results.posts?.length) {
313
+ console.log(` ${chalk.bold('Posts:')}`);
314
+ for (const post of results.posts) {
315
+ const badge = postTypeBadge(post.postType);
316
+ const title = post.title || truncate(post.body, 50);
317
+ const community = post.community ? chalk.dim(`r/${post.community.slug}`) : '';
318
+ const author = formatAuthor(post);
319
+ const time = chalk.dim(relativeTime(post.createdAt));
320
+ const votes = chalk.green(`▲ ${post.upvoteCount || 0}`);
321
+ console.log(` ${badge} ${chalk.bold(title)}`);
322
+ console.log(` ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time} ${chalk.dim('·')} ${votes}`);
323
+ console.log('');
324
+ }
325
+ }
326
+
327
+ if (results.communities?.length) {
328
+ console.log(` ${chalk.bold('Communities:')}`);
329
+ for (const c of results.communities) {
330
+ console.log(` ${chalk.cyan(padRight(c.slug, 24))} ${padRight(c.name, 24)} ${chalk.dim(`${c.memberCount ?? 0} members`)}`);
331
+ }
332
+ console.log('');
333
+ }
334
+
335
+ if (results.users?.length) {
336
+ console.log(` ${chalk.bold('Users:')}`);
337
+ for (const u of results.users) {
338
+ const handle = u.handle || u.displayName || [u.firstName, u.lastName].filter(Boolean).join(' ');
339
+ const karma = u.karmaScore !== undefined ? chalk.dim(`karma: ${u.karmaScore}`) : '';
340
+ console.log(` ${chalk.white(`@${handle}`)}${' '.repeat(Math.max(1, 20 - handle.length))}${karma}`);
341
+ }
342
+ console.log('');
343
+ }
344
+ }
345
+
346
+ // ── Pagination ──────────────────────────────────────────
347
+
348
+ export function formatPagination(meta) {
349
+ if (meta?.hasMore && meta?.nextCursor) {
350
+ console.log(chalk.dim(` ── More results available. Run with --cursor=${meta.nextCursor} to see next page\n`));
351
+ }
352
+ }