@ebowwa/crm 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.
Files changed (187) hide show
  1. package/README.md +174 -0
  2. package/dist/cli/commands/activities.d.ts +11 -0
  3. package/dist/cli/commands/activities.d.ts.map +1 -0
  4. package/dist/cli/commands/activities.js +427 -0
  5. package/dist/cli/commands/activities.js.map +1 -0
  6. package/dist/cli/commands/contacts.d.ts +11 -0
  7. package/dist/cli/commands/contacts.d.ts.map +1 -0
  8. package/dist/cli/commands/contacts.js +458 -0
  9. package/dist/cli/commands/contacts.js.map +1 -0
  10. package/dist/cli/commands/deals.d.ts +11 -0
  11. package/dist/cli/commands/deals.d.ts.map +1 -0
  12. package/dist/cli/commands/deals.js +498 -0
  13. package/dist/cli/commands/deals.js.map +1 -0
  14. package/dist/cli/commands/media.d.ts +11 -0
  15. package/dist/cli/commands/media.d.ts.map +1 -0
  16. package/dist/cli/commands/media.js +417 -0
  17. package/dist/cli/commands/media.js.map +1 -0
  18. package/dist/cli/commands/search.d.ts +11 -0
  19. package/dist/cli/commands/search.d.ts.map +1 -0
  20. package/dist/cli/commands/search.js +346 -0
  21. package/dist/cli/commands/search.js.map +1 -0
  22. package/dist/cli/index.d.ts +13 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +173 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/cli/repl.d.ts +15 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +318 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/utils/config.d.ts +91 -0
  31. package/dist/cli/utils/config.d.ts.map +1 -0
  32. package/dist/cli/utils/config.js +212 -0
  33. package/dist/cli/utils/config.js.map +1 -0
  34. package/dist/cli/utils/output.d.ts +136 -0
  35. package/dist/cli/utils/output.d.ts.map +1 -0
  36. package/dist/cli/utils/output.js +323 -0
  37. package/dist/cli/utils/output.js.map +1 -0
  38. package/dist/cli/utils/prompt.d.ts +81 -0
  39. package/dist/cli/utils/prompt.d.ts.map +1 -0
  40. package/dist/cli/utils/prompt.js +341 -0
  41. package/dist/cli/utils/prompt.js.map +1 -0
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +8 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/core/index.d.ts +6 -0
  47. package/dist/core/index.d.ts.map +1 -0
  48. package/dist/core/index.js +32 -0
  49. package/dist/core/index.js.map +1 -0
  50. package/dist/core/schemas.d.ts +3050 -0
  51. package/dist/core/schemas.d.ts.map +1 -0
  52. package/dist/core/schemas.js +667 -0
  53. package/dist/core/schemas.js.map +1 -0
  54. package/dist/core/types.d.ts +597 -0
  55. package/dist/core/types.d.ts.map +1 -0
  56. package/dist/core/types.js +8 -0
  57. package/dist/core/types.js.map +1 -0
  58. package/dist/index.d.ts +7 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +8 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/mcp/index.d.ts +14 -0
  63. package/dist/mcp/index.d.ts.map +1 -0
  64. package/dist/mcp/index.js +11 -0
  65. package/dist/mcp/index.js.map +1 -0
  66. package/dist/mcp/server.d.ts +13 -0
  67. package/dist/mcp/server.d.ts.map +1 -0
  68. package/dist/mcp/server.js +18 -0
  69. package/dist/mcp/server.js.map +1 -0
  70. package/dist/mcp/storage/client.d.ts +109 -0
  71. package/dist/mcp/storage/client.d.ts.map +1 -0
  72. package/dist/mcp/storage/client.js +355 -0
  73. package/dist/mcp/storage/client.js.map +1 -0
  74. package/dist/mcp/storage/index.d.ts +7 -0
  75. package/dist/mcp/storage/index.d.ts.map +1 -0
  76. package/dist/mcp/storage/index.js +6 -0
  77. package/dist/mcp/storage/index.js.map +1 -0
  78. package/dist/mcp/storage/types.d.ts +44 -0
  79. package/dist/mcp/storage/types.d.ts.map +1 -0
  80. package/dist/mcp/storage/types.js +35 -0
  81. package/dist/mcp/storage/types.js.map +1 -0
  82. package/dist/mcp/tools/definitions.d.ts +16 -0
  83. package/dist/mcp/tools/definitions.d.ts.map +1 -0
  84. package/dist/mcp/tools/definitions.js +914 -0
  85. package/dist/mcp/tools/definitions.js.map +1 -0
  86. package/dist/mcp/tools/handlers.d.ts +50 -0
  87. package/dist/mcp/tools/handlers.d.ts.map +1 -0
  88. package/dist/mcp/tools/handlers.js +760 -0
  89. package/dist/mcp/tools/handlers.js.map +1 -0
  90. package/dist/mcp/tools/index.d.ts +7 -0
  91. package/dist/mcp/tools/index.d.ts.map +1 -0
  92. package/dist/mcp/tools/index.js +6 -0
  93. package/dist/mcp/tools/index.js.map +1 -0
  94. package/dist/mcp/tools/types.d.ts +314 -0
  95. package/dist/mcp/tools/types.d.ts.map +1 -0
  96. package/dist/mcp/tools/types.js +5 -0
  97. package/dist/mcp/tools/types.js.map +1 -0
  98. package/dist/mcp/transports/stdio.d.ts +27 -0
  99. package/dist/mcp/transports/stdio.d.ts.map +1 -0
  100. package/dist/mcp/transports/stdio.js +237 -0
  101. package/dist/mcp/transports/stdio.js.map +1 -0
  102. package/dist/telemetry/index.d.ts +58 -0
  103. package/dist/telemetry/index.d.ts.map +1 -0
  104. package/dist/telemetry/index.js +109 -0
  105. package/dist/telemetry/index.js.map +1 -0
  106. package/dist/telemetry/logger.d.ts +116 -0
  107. package/dist/telemetry/logger.d.ts.map +1 -0
  108. package/dist/telemetry/logger.js +256 -0
  109. package/dist/telemetry/logger.js.map +1 -0
  110. package/dist/telemetry/metrics.d.ts +115 -0
  111. package/dist/telemetry/metrics.d.ts.map +1 -0
  112. package/dist/telemetry/metrics.js +292 -0
  113. package/dist/telemetry/metrics.js.map +1 -0
  114. package/dist/telemetry/tracer.d.ts +227 -0
  115. package/dist/telemetry/tracer.d.ts.map +1 -0
  116. package/dist/telemetry/tracer.js +355 -0
  117. package/dist/telemetry/tracer.js.map +1 -0
  118. package/dist/web/app.d.ts +2 -0
  119. package/dist/web/app.d.ts.map +1 -0
  120. package/dist/web/app.js +115 -0
  121. package/dist/web/app.js.map +1 -0
  122. package/dist/web/components/ContactList.d.ts +3 -0
  123. package/dist/web/components/ContactList.d.ts.map +1 -0
  124. package/dist/web/components/ContactList.js +262 -0
  125. package/dist/web/components/ContactList.js.map +1 -0
  126. package/dist/web/components/Dashboard.d.ts +3 -0
  127. package/dist/web/components/Dashboard.d.ts.map +1 -0
  128. package/dist/web/components/Dashboard.js +158 -0
  129. package/dist/web/components/Dashboard.js.map +1 -0
  130. package/dist/web/components/DealPipeline.d.ts +3 -0
  131. package/dist/web/components/DealPipeline.d.ts.map +1 -0
  132. package/dist/web/components/DealPipeline.js +306 -0
  133. package/dist/web/components/DealPipeline.js.map +1 -0
  134. package/dist/web/index.d.ts +2 -0
  135. package/dist/web/index.d.ts.map +1 -0
  136. package/dist/web/index.js +269 -0
  137. package/dist/web/index.js.map +1 -0
  138. package/dist/web/types.d.ts +75 -0
  139. package/dist/web/types.d.ts.map +1 -0
  140. package/dist/web/types.js +3 -0
  141. package/dist/web/types.js.map +1 -0
  142. package/native/index.d.ts +571 -0
  143. package/native/index.js +687 -0
  144. package/package.json +105 -0
  145. package/src/cli/commands/activities.ts +543 -0
  146. package/src/cli/commands/contacts.ts +563 -0
  147. package/src/cli/commands/deals.ts +637 -0
  148. package/src/cli/commands/media.ts +521 -0
  149. package/src/cli/commands/search.ts +426 -0
  150. package/src/cli/index.ts +203 -0
  151. package/src/cli/repl.ts +379 -0
  152. package/src/cli/utils/config.ts +299 -0
  153. package/src/cli/utils/output.ts +386 -0
  154. package/src/cli/utils/prompt.ts +444 -0
  155. package/src/cli.ts +11 -0
  156. package/src/core/index.ts +184 -0
  157. package/src/core/schemas.ts +770 -0
  158. package/src/core/types.ts +969 -0
  159. package/src/index.ts +8 -0
  160. package/src/mcp/index.ts +17 -0
  161. package/src/mcp/server.ts +26 -0
  162. package/src/mcp/storage/client.ts +408 -0
  163. package/src/mcp/storage/index.ts +7 -0
  164. package/src/mcp/storage/types.ts +72 -0
  165. package/src/mcp/tools/definitions.ts +961 -0
  166. package/src/mcp/tools/handlers.ts +805 -0
  167. package/src/mcp/tools/index.ts +7 -0
  168. package/src/mcp/tools/types.ts +390 -0
  169. package/src/mcp/transports/stdio.ts +225 -0
  170. package/src/telemetry/index.ts +131 -0
  171. package/src/telemetry/logger.ts +318 -0
  172. package/src/telemetry/metrics.ts +393 -0
  173. package/src/telemetry/tracer.ts +487 -0
  174. package/src/web/api/activities.ts +41 -0
  175. package/src/web/api/contacts.ts +114 -0
  176. package/src/web/api/deals.ts +108 -0
  177. package/src/web/api/media.ts +98 -0
  178. package/src/web/app.tsx +143 -0
  179. package/src/web/components/ActivityFeed.tsx +195 -0
  180. package/src/web/components/ContactList.tsx +340 -0
  181. package/src/web/components/Dashboard.tsx +214 -0
  182. package/src/web/components/DealPipeline.tsx +405 -0
  183. package/src/web/components/MediaGallery.tsx +334 -0
  184. package/src/web/index.html +14 -0
  185. package/src/web/index.ts +326 -0
  186. package/src/web/styles/main.css +180 -0
  187. package/src/web/types.ts +311 -0
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Media Commands
3
+ *
4
+ * CLI commands for managing media files in the CRM.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import {
11
+ printSuccess,
12
+ printError,
13
+ printWarning,
14
+ printTable,
15
+ printJSON,
16
+ printHeader,
17
+ printDivider,
18
+ formatRelativeTime,
19
+ formatFileSize,
20
+ truncate,
21
+ output,
22
+ } from '../utils/output.js';
23
+ import {
24
+ prompt,
25
+ promptRequired,
26
+ confirm,
27
+ select,
28
+ Spinner,
29
+ } from '../utils/prompt.js';
30
+ import { CRMStorageClient } from '../../mcp/storage/client.js';
31
+ import type { Media, MediaType, MimeType } from '../../core/types.js';
32
+
33
+ // Media type icons
34
+ const MEDIA_ICONS: Record<MediaType, string> = {
35
+ image: '\u{1F4F7}',
36
+ video: '\u{1F3AC}',
37
+ audio: '\u{1F3B5}',
38
+ document: '\u{1F4C4}',
39
+ spreadsheet: '\u{1F4CA}',
40
+ presentation: '\u{1F4BB}',
41
+ pdf: '\u{1F4C4}',
42
+ archive: '\u{1F4E6}',
43
+ other: '\u{1F4CE}',
44
+ };
45
+
46
+ // MIME type to media type mapping
47
+ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
48
+ 'image/jpeg': 'image',
49
+ 'image/png': 'image',
50
+ 'image/gif': 'image',
51
+ 'image/webp': 'image',
52
+ 'image/svg+xml': 'image',
53
+ 'video/mp4': 'video',
54
+ 'video/webm': 'video',
55
+ 'video/quicktime': 'video',
56
+ 'audio/mpeg': 'audio',
57
+ 'audio/wav': 'audio',
58
+ 'audio/ogg': 'audio',
59
+ 'application/pdf': 'pdf',
60
+ 'application/msword': 'document',
61
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document',
62
+ 'application/vnd.ms-excel': 'spreadsheet',
63
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'spreadsheet',
64
+ 'application/vnd.ms-powerpoint': 'presentation',
65
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'presentation',
66
+ 'application/zip': 'archive',
67
+ 'application/x-rar-compressed': 'archive',
68
+ 'application/x-7z-compressed': 'archive',
69
+ };
70
+
71
+ // Storage client singleton
72
+ let _storage: CRMStorageClient | null = null;
73
+
74
+ /**
75
+ * Get storage client (lazy initialization)
76
+ */
77
+ async function getStorage(): Promise<CRMStorageClient> {
78
+ if (!_storage) {
79
+ _storage = new CRMStorageClient({
80
+ path: process.env.CRM_DB_PATH || './data/crm-cli',
81
+ mapSize: 256 * 1024 * 1024, // 256MB
82
+ maxDbs: 20,
83
+ });
84
+ await _storage.initialize();
85
+ }
86
+ return _storage;
87
+ }
88
+
89
+ /**
90
+ * Get media type from MIME type
91
+ */
92
+ function getMediaType(mimeType: string): MediaType {
93
+ return MIME_TO_MEDIA_TYPE[mimeType] || 'other';
94
+ }
95
+
96
+ /**
97
+ * Get all media
98
+ */
99
+ async function getMedia(): Promise<Media[]> {
100
+ const storage = await getStorage();
101
+ return storage.list('media');
102
+ }
103
+
104
+ /**
105
+ * Get media by ID
106
+ */
107
+ async function getMediaById(id: string): Promise<Media | null> {
108
+ const storage = await getStorage();
109
+ return storage.get('media', id);
110
+ }
111
+
112
+ /**
113
+ * Create media entry
114
+ */
115
+ async function createMedia(data: Partial<Media>): Promise<Media> {
116
+ const storage = await getStorage();
117
+ return storage.insert('media', {
118
+ entityType: data.entityType || 'contact',
119
+ entityId: data.entityId || '',
120
+ type: data.type || 'other',
121
+ filename: data.filename || 'unknown',
122
+ mimeType: data.mimeType || 'application/octet-stream',
123
+ size: data.size || 0,
124
+ url: data.url || '',
125
+ thumbnailUrl: data.thumbnailUrl,
126
+ metadata: data.metadata || {},
127
+ altText: data.altText,
128
+ caption: data.caption,
129
+ isPublic: data.isPublic ?? false,
130
+ downloadCount: data.downloadCount || 0,
131
+ uploadedBy: data.uploadedBy,
132
+ expiresAt: data.expiresAt,
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Delete media
138
+ */
139
+ async function deleteMedia(id: string): Promise<boolean> {
140
+ const storage = await getStorage();
141
+ const existing = await storage.get('media', id);
142
+ if (!existing) return false;
143
+ await storage.delete('media', id);
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * Increment download count
149
+ */
150
+ async function incrementDownloadCount(id: string): Promise<void> {
151
+ const storage = await getStorage();
152
+ const media = storage.get<Media>('media', id);
153
+ if (media) {
154
+ const updates: Partial<Media> = {
155
+ downloadCount: (media.downloadCount || 0) + 1,
156
+ };
157
+ await storage.update('media', id, updates);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Filter media by contact
163
+ */
164
+ async function filterByContact(contactId: string): Promise<Media[]> {
165
+ const media = await getMedia();
166
+ return media.filter(
167
+ (m) => m.entityType === 'contact' && m.entityId === contactId
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Filter media by deal
173
+ */
174
+ async function filterByDeal(dealId: string): Promise<Media[]> {
175
+ const media = await getMedia();
176
+ return media.filter(
177
+ (m) => m.entityType === 'deal' && m.entityId === dealId
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Generate UUID
183
+ */
184
+ function generateId(): string {
185
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
186
+ const r = (Math.random() * 16) | 0;
187
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
188
+ return v.toString(16);
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Register media commands
194
+ */
195
+ export function registerMediaCommands(program: Command): void {
196
+ const media = program.command('media').description('Manage media files');
197
+
198
+ // List media
199
+ media
200
+ .command('list')
201
+ .description('List media files')
202
+ .option('-c, --contact <id>', 'Filter by contact ID')
203
+ .option('-d, --deal <id>', 'Filter by deal ID')
204
+ .option('-t, --type <type>', 'Filter by media type')
205
+ .option('--json', 'Output as JSON')
206
+ .option('--limit <number>', 'Limit results', '20')
207
+ .action(async (options) => {
208
+ let mediaFiles = await getMedia();
209
+
210
+ if (options.contact) {
211
+ mediaFiles = await filterByContact(options.contact);
212
+ }
213
+
214
+ if (options.deal) {
215
+ mediaFiles = await filterByDeal(options.deal);
216
+ }
217
+
218
+ if (options.type) {
219
+ mediaFiles = mediaFiles.filter(
220
+ (m) => m.type.toLowerCase() === options.type.toLowerCase()
221
+ );
222
+ }
223
+
224
+ // Sort by date descending
225
+ mediaFiles.sort(
226
+ (a, b) =>
227
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
228
+ );
229
+
230
+ const limit = parseInt(options.limit);
231
+ mediaFiles = mediaFiles.slice(0, limit);
232
+
233
+ if (options.json) {
234
+ printJSON(mediaFiles);
235
+ return;
236
+ }
237
+
238
+ if (mediaFiles.length === 0) {
239
+ printWarning('No media files found');
240
+ return;
241
+ }
242
+
243
+ printHeader(`Media Files (${mediaFiles.length})`);
244
+
245
+ printTable(mediaFiles, [
246
+ {
247
+ key: 'type',
248
+ header: '',
249
+ width: 2,
250
+ format: (v) => MEDIA_ICONS[v as MediaType] || '\u{1F4CE}',
251
+ },
252
+ { key: 'filename', header: 'Filename', width: 30, format: (v) => truncate(String(v), 30) },
253
+ {
254
+ key: 'size',
255
+ header: 'Size',
256
+ width: 10,
257
+ format: (v) => formatFileSize(Number(v)),
258
+ },
259
+ { key: 'type', header: 'Type', width: 10 },
260
+ {
261
+ key: 'createdAt',
262
+ header: 'Uploaded',
263
+ format: (v) => formatRelativeTime(String(v)),
264
+ },
265
+ ]);
266
+ });
267
+
268
+ // Upload media
269
+ media
270
+ .command('upload <file>')
271
+ .description('Upload a media file')
272
+ .option('-c, --contact <id>', 'Associate with contact')
273
+ .option('-d, --deal <id>', 'Associate with deal')
274
+ .option('--caption <text>', 'Caption for the media')
275
+ .option('--public', 'Make media public')
276
+ .action(async (filePath, options) => {
277
+ // Check file exists
278
+ if (!fs.existsSync(filePath)) {
279
+ printError(`File not found: ${filePath}`);
280
+ process.exit(1);
281
+ }
282
+
283
+ // Get file stats
284
+ const stats = fs.statSync(filePath);
285
+ const filename = path.basename(filePath);
286
+
287
+ // Determine entity
288
+ let entityType: 'contact' | 'deal' = 'contact';
289
+ let entityId: string;
290
+
291
+ if (options.contact) {
292
+ entityType = 'contact';
293
+ entityId = options.contact;
294
+ } else if (options.deal) {
295
+ entityType = 'deal';
296
+ entityId = options.deal;
297
+ } else {
298
+ entityId = await promptRequired('Contact or Deal ID');
299
+ entityType = (await select('Entity Type', ['contact', 'deal'])) as
300
+ | 'contact'
301
+ | 'deal';
302
+ }
303
+
304
+ // Detect MIME type (simplified)
305
+ const ext = path.extname(filePath).toLowerCase();
306
+ const mimeMap: Record<string, string> = {
307
+ '.jpg': 'image/jpeg',
308
+ '.jpeg': 'image/jpeg',
309
+ '.png': 'image/png',
310
+ '.gif': 'image/gif',
311
+ '.webp': 'image/webp',
312
+ '.svg': 'image/svg+xml',
313
+ '.mp4': 'video/mp4',
314
+ '.webm': 'video/webm',
315
+ '.mov': 'video/quicktime',
316
+ '.mp3': 'audio/mpeg',
317
+ '.wav': 'audio/wav',
318
+ '.pdf': 'application/pdf',
319
+ '.doc': 'application/msword',
320
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
321
+ '.xls': 'application/vnd.ms-excel',
322
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
323
+ '.ppt': 'application/vnd.ms-powerpoint',
324
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
325
+ '.zip': 'application/zip',
326
+ '.txt': 'text/plain',
327
+ '.csv': 'text/csv',
328
+ };
329
+
330
+ const mimeType: MimeType = mimeMap[ext] || 'application/octet-stream';
331
+ const mediaType = getMediaType(mimeType);
332
+
333
+ // Caption
334
+ const caption = options.caption || (await prompt('Caption (optional)'));
335
+
336
+ // Preview
337
+ console.log();
338
+ console.log(`${MEDIA_ICONS[mediaType]} ${output.bold(filename)}`);
339
+ console.log(`${output.dim('Size:')} ${formatFileSize(stats.size)}`);
340
+ console.log(`${output.dim('Type:')} ${mediaType}`);
341
+ console.log(`${output.dim('MIME:')} ${mimeType}`);
342
+ console.log();
343
+
344
+ const confirmed = await confirm('Upload this file?');
345
+ if (!confirmed) {
346
+ printWarning('Upload cancelled');
347
+ return;
348
+ }
349
+
350
+ // Simulate upload
351
+ const spinner = new Spinner('Uploading...').start();
352
+ await new Promise((r) => setTimeout(r, 1000 + Math.random() * 1000));
353
+
354
+ const mediaEntry = await createMedia({
355
+ entityType,
356
+ entityId,
357
+ type: mediaType,
358
+ filename,
359
+ mimeType,
360
+ size: stats.size,
361
+ url: `/uploads/${generateId()}/${filename}`,
362
+ caption,
363
+ isPublic: options.public || false,
364
+ });
365
+
366
+ spinner.succeed('Upload complete');
367
+ printSuccess(`Media ID: ${mediaEntry.id}`);
368
+ console.log();
369
+ });
370
+
371
+ // Get media details
372
+ media
373
+ .command('get <id>')
374
+ .description('Get media details')
375
+ .option('--json', 'Output as JSON')
376
+ .action(async (id, options) => {
377
+ const mediaFile = await getMediaById(id);
378
+
379
+ if (!mediaFile) {
380
+ printError(`Media not found: ${id}`);
381
+ process.exit(1);
382
+ }
383
+
384
+ if (options.json) {
385
+ printJSON(mediaFile);
386
+ return;
387
+ }
388
+
389
+ const icon = MEDIA_ICONS[mediaFile.type];
390
+ printHeader(`${icon} ${mediaFile.filename}`);
391
+
392
+ console.log(`${output.dim('ID:')} ${mediaFile.id}`);
393
+ console.log(`${output.dim('Type:')} ${mediaFile.type}`);
394
+ console.log();
395
+
396
+ // File Info
397
+ console.log(output.bold('File Information'));
398
+ printDivider('-');
399
+ console.log(`${output.dim('Filename:')} ${mediaFile.filename}`);
400
+ console.log(`${output.dim('MIME Type:')} ${mediaFile.mimeType}`);
401
+ console.log(`${output.dim('Size:')} ${formatFileSize(mediaFile.size)}`);
402
+ console.log(`${output.dim('URL:')} ${mediaFile.url}`);
403
+
404
+ if (mediaFile.thumbnailUrl) {
405
+ console.log(`${output.dim('Thumbnail:')} ${mediaFile.thumbnailUrl}`);
406
+ }
407
+
408
+ console.log();
409
+
410
+ // Association
411
+ console.log(output.bold('Association'));
412
+ printDivider('-');
413
+ console.log(`${output.dim('Entity Type:')} ${mediaFile.entityType}`);
414
+ console.log(`${output.dim('Entity ID:')} ${mediaFile.entityId}`);
415
+
416
+ console.log();
417
+
418
+ // Additional Info
419
+ console.log(output.bold('Additional Info'));
420
+ printDivider('-');
421
+ console.log(`${output.dim('Public:')} ${mediaFile.isPublic ? 'Yes' : 'No'}`);
422
+ console.log(`${output.dim('Downloads:')} ${mediaFile.downloadCount}`);
423
+
424
+ if (mediaFile.caption) {
425
+ console.log(`${output.dim('Caption:')} ${mediaFile.caption}`);
426
+ }
427
+
428
+ if (mediaFile.altText) {
429
+ console.log(`${output.dim('Alt Text:')} ${mediaFile.altText}`);
430
+ }
431
+
432
+ console.log();
433
+
434
+ // Timestamps
435
+ console.log(output.bold('Timestamps'));
436
+ printDivider('-');
437
+ console.log(
438
+ `${output.dim('Uploaded:')} ${new Date(mediaFile.createdAt).toLocaleString()}`
439
+ );
440
+
441
+ if (mediaFile.expiresAt) {
442
+ console.log(
443
+ `${output.dim('Expires:')} ${new Date(mediaFile.expiresAt).toLocaleString()}`
444
+ );
445
+ }
446
+
447
+ console.log();
448
+ });
449
+
450
+ // Delete media
451
+ media
452
+ .command('delete <id>')
453
+ .description('Delete a media file')
454
+ .option('-f, --force', 'Skip confirmation')
455
+ .action(async (id, options) => {
456
+ const mediaFile = await getMediaById(id);
457
+
458
+ if (!mediaFile) {
459
+ printError(`Media not found: ${id}`);
460
+ process.exit(1);
461
+ }
462
+
463
+ if (!options.force) {
464
+ const icon = MEDIA_ICONS[mediaFile.type];
465
+ console.log(`\n${icon} ${output.bold(mediaFile.filename)}`);
466
+ console.log(`Size: ${formatFileSize(mediaFile.size)}`);
467
+ console.log(`Type: ${mediaFile.type}\n`);
468
+
469
+ const confirmed = await confirm(
470
+ 'Are you sure you want to delete this media file?'
471
+ );
472
+
473
+ if (!confirmed) {
474
+ printWarning('Deletion cancelled');
475
+ return;
476
+ }
477
+ }
478
+
479
+ const spinner = new Spinner('Deleting media...').start();
480
+ await new Promise((r) => setTimeout(r, 300));
481
+
482
+ await deleteMedia(id);
483
+ spinner.succeed('Media deleted successfully');
484
+ });
485
+
486
+ // Download media
487
+ media
488
+ .command('download <id>')
489
+ .description('Download a media file')
490
+ .option('-o, --output <path>', 'Output path')
491
+ .action(async (id, options) => {
492
+ const mediaFile = await getMediaById(id);
493
+
494
+ if (!mediaFile) {
495
+ printError(`Media not found: ${id}`);
496
+ process.exit(1);
497
+ }
498
+
499
+ const outputPath =
500
+ options.output || path.join(process.cwd(), mediaFile.filename);
501
+
502
+ console.log(`\n${MEDIA_ICONS[mediaFile.type]} ${mediaFile.filename}`);
503
+ console.log(`${output.dim('Source:')} ${mediaFile.url}`);
504
+ console.log(`${output.dim('Target:')} ${outputPath}\n`);
505
+
506
+ const spinner = new Spinner('Downloading...').start();
507
+
508
+ // Simulate download
509
+ await new Promise((r) => setTimeout(r, 500 + Math.random() * 1000));
510
+
511
+ // In real implementation, would actually download the file
512
+ // For now, just create a placeholder
513
+ fs.writeFileSync(outputPath, `[Placeholder for ${mediaFile.filename}]`);
514
+
515
+ // Increment download count
516
+ await incrementDownloadCount(id);
517
+
518
+ spinner.succeed('Download complete');
519
+ printSuccess(`Saved to: ${outputPath}`);
520
+ });
521
+ }