@hasna/microservices 0.0.3 → 0.0.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.
Files changed (68) hide show
  1. package/bin/index.js +63 -0
  2. package/bin/mcp.js +63 -0
  3. package/dist/index.js +63 -0
  4. package/microservices/microservice-ads/package.json +27 -0
  5. package/microservices/microservice-ads/src/cli/index.ts +605 -0
  6. package/microservices/microservice-ads/src/db/campaigns.ts +797 -0
  7. package/microservices/microservice-ads/src/db/database.ts +93 -0
  8. package/microservices/microservice-ads/src/db/migrations.ts +60 -0
  9. package/microservices/microservice-ads/src/index.ts +39 -0
  10. package/microservices/microservice-ads/src/mcp/index.ts +480 -0
  11. package/microservices/microservice-contracts/package.json +27 -0
  12. package/microservices/microservice-contracts/src/cli/index.ts +770 -0
  13. package/microservices/microservice-contracts/src/db/contracts.ts +925 -0
  14. package/microservices/microservice-contracts/src/db/database.ts +93 -0
  15. package/microservices/microservice-contracts/src/db/migrations.ts +141 -0
  16. package/microservices/microservice-contracts/src/index.ts +43 -0
  17. package/microservices/microservice-contracts/src/mcp/index.ts +617 -0
  18. package/microservices/microservice-domains/package.json +27 -0
  19. package/microservices/microservice-domains/src/cli/index.ts +691 -0
  20. package/microservices/microservice-domains/src/db/database.ts +93 -0
  21. package/microservices/microservice-domains/src/db/domains.ts +1164 -0
  22. package/microservices/microservice-domains/src/db/migrations.ts +60 -0
  23. package/microservices/microservice-domains/src/index.ts +65 -0
  24. package/microservices/microservice-domains/src/mcp/index.ts +536 -0
  25. package/microservices/microservice-hiring/package.json +27 -0
  26. package/microservices/microservice-hiring/src/cli/index.ts +741 -0
  27. package/microservices/microservice-hiring/src/db/database.ts +93 -0
  28. package/microservices/microservice-hiring/src/db/hiring.ts +1085 -0
  29. package/microservices/microservice-hiring/src/db/migrations.ts +89 -0
  30. package/microservices/microservice-hiring/src/index.ts +80 -0
  31. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  32. package/microservices/microservice-hiring/src/mcp/index.ts +709 -0
  33. package/microservices/microservice-payments/package.json +27 -0
  34. package/microservices/microservice-payments/src/cli/index.ts +609 -0
  35. package/microservices/microservice-payments/src/db/database.ts +93 -0
  36. package/microservices/microservice-payments/src/db/migrations.ts +81 -0
  37. package/microservices/microservice-payments/src/db/payments.ts +1204 -0
  38. package/microservices/microservice-payments/src/index.ts +51 -0
  39. package/microservices/microservice-payments/src/mcp/index.ts +683 -0
  40. package/microservices/microservice-payroll/package.json +27 -0
  41. package/microservices/microservice-payroll/src/cli/index.ts +643 -0
  42. package/microservices/microservice-payroll/src/db/database.ts +93 -0
  43. package/microservices/microservice-payroll/src/db/migrations.ts +95 -0
  44. package/microservices/microservice-payroll/src/db/payroll.ts +1377 -0
  45. package/microservices/microservice-payroll/src/index.ts +48 -0
  46. package/microservices/microservice-payroll/src/mcp/index.ts +666 -0
  47. package/microservices/microservice-shipping/package.json +27 -0
  48. package/microservices/microservice-shipping/src/cli/index.ts +606 -0
  49. package/microservices/microservice-shipping/src/db/database.ts +93 -0
  50. package/microservices/microservice-shipping/src/db/migrations.ts +69 -0
  51. package/microservices/microservice-shipping/src/db/shipping.ts +1093 -0
  52. package/microservices/microservice-shipping/src/index.ts +53 -0
  53. package/microservices/microservice-shipping/src/mcp/index.ts +533 -0
  54. package/microservices/microservice-social/package.json +27 -0
  55. package/microservices/microservice-social/src/cli/index.ts +689 -0
  56. package/microservices/microservice-social/src/db/database.ts +93 -0
  57. package/microservices/microservice-social/src/db/migrations.ts +88 -0
  58. package/microservices/microservice-social/src/db/social.ts +1046 -0
  59. package/microservices/microservice-social/src/index.ts +46 -0
  60. package/microservices/microservice-social/src/mcp/index.ts +655 -0
  61. package/microservices/microservice-subscriptions/package.json +27 -0
  62. package/microservices/microservice-subscriptions/src/cli/index.ts +715 -0
  63. package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
  64. package/microservices/microservice-subscriptions/src/db/migrations.ts +125 -0
  65. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +1256 -0
  66. package/microservices/microservice-subscriptions/src/index.ts +41 -0
  67. package/microservices/microservice-subscriptions/src/mcp/index.ts +631 -0
  68. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander";
4
+ import { readFileSync } from "node:fs";
5
+ import {
6
+ createAccount,
7
+ getAccount,
8
+ listAccounts,
9
+ updateAccount,
10
+ deleteAccount,
11
+ createPost,
12
+ getPost,
13
+ listPosts,
14
+ updatePost,
15
+ deletePost,
16
+ schedulePost,
17
+ publishPost,
18
+ createTemplate,
19
+ listTemplates,
20
+ getTemplate,
21
+ deleteTemplate,
22
+ useTemplate,
23
+ getEngagementStats,
24
+ getStatsByPlatform,
25
+ getCalendar,
26
+ getOverallStats,
27
+ batchSchedule,
28
+ crossPost,
29
+ getBestTimeToPost,
30
+ reschedulePost,
31
+ submitPostForReview,
32
+ approvePost,
33
+ createRecurringPost,
34
+ getHashtagStats,
35
+ checkPlatformLimit,
36
+ PLATFORM_LIMITS,
37
+ type Platform,
38
+ type PostStatus,
39
+ type Recurrence,
40
+ } from "../db/social.js";
41
+
42
+ const program = new Command();
43
+
44
+ program
45
+ .name("microservice-social")
46
+ .description("Social media management microservice")
47
+ .version("0.0.1");
48
+
49
+ // --- Posts ---
50
+
51
+ const postCmd = program
52
+ .command("post")
53
+ .description("Post management");
54
+
55
+ postCmd
56
+ .command("create")
57
+ .description("Create a new post")
58
+ .requiredOption("--account <id>", "Account ID")
59
+ .requiredOption("--content <text>", "Post content")
60
+ .option("--media <urls>", "Comma-separated media URLs")
61
+ .option("--status <status>", "Post status (draft/scheduled/published/failed)", "draft")
62
+ .option("--scheduled-at <datetime>", "Schedule date/time")
63
+ .option("--tags <tags>", "Comma-separated tags")
64
+ .option("--recurring <recurrence>", "Recurrence (daily/weekly/biweekly/monthly)")
65
+ .option("--json", "Output as JSON", false)
66
+ .action((opts) => {
67
+ // Check platform character limit
68
+ const warning = checkPlatformLimit(opts.content, opts.account);
69
+ if (warning) {
70
+ console.warn(`Warning: Content (${warning.content_length} chars) exceeds ${warning.platform} limit (${warning.limit} chars) by ${warning.over_by} chars`);
71
+ }
72
+
73
+ const post = createPost({
74
+ account_id: opts.account,
75
+ content: opts.content,
76
+ media_urls: opts.media ? opts.media.split(",").map((u: string) => u.trim()) : undefined,
77
+ status: opts.status as PostStatus,
78
+ scheduled_at: opts.scheduledAt,
79
+ tags: opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined,
80
+ recurrence: opts.recurring as Recurrence | undefined,
81
+ });
82
+
83
+ if (opts.json) {
84
+ console.log(JSON.stringify(post, null, 2));
85
+ } else {
86
+ console.log(`Created post: ${post.id} [${post.status}]`);
87
+ console.log(` Content: ${post.content.substring(0, 80)}${post.content.length > 80 ? "..." : ""}`);
88
+ }
89
+ });
90
+
91
+ postCmd
92
+ .command("list")
93
+ .description("List posts")
94
+ .option("--account <id>", "Filter by account ID")
95
+ .option("--status <status>", "Filter by status")
96
+ .option("--tag <tag>", "Filter by tag")
97
+ .option("--search <query>", "Search post content")
98
+ .option("--limit <n>", "Limit results")
99
+ .option("--json", "Output as JSON", false)
100
+ .action((opts) => {
101
+ const posts = listPosts({
102
+ account_id: opts.account,
103
+ status: opts.status as PostStatus | undefined,
104
+ tag: opts.tag,
105
+ search: opts.search,
106
+ limit: opts.limit ? parseInt(opts.limit) : undefined,
107
+ });
108
+
109
+ if (opts.json) {
110
+ console.log(JSON.stringify(posts, null, 2));
111
+ } else {
112
+ if (posts.length === 0) {
113
+ console.log("No posts found.");
114
+ return;
115
+ }
116
+ for (const p of posts) {
117
+ const preview = p.content.substring(0, 60) + (p.content.length > 60 ? "..." : "");
118
+ const tags = p.tags.length ? ` [${p.tags.join(", ")}]` : "";
119
+ console.log(` [${p.status}] ${preview}${tags}`);
120
+ }
121
+ console.log(`\n${posts.length} post(s)`);
122
+ }
123
+ });
124
+
125
+ postCmd
126
+ .command("get")
127
+ .description("Get a post by ID")
128
+ .argument("<id>", "Post ID")
129
+ .option("--json", "Output as JSON", false)
130
+ .action((id, opts) => {
131
+ const post = getPost(id);
132
+ if (!post) {
133
+ console.error(`Post '${id}' not found.`);
134
+ process.exit(1);
135
+ }
136
+
137
+ if (opts.json) {
138
+ console.log(JSON.stringify(post, null, 2));
139
+ } else {
140
+ console.log(`Post ${post.id} [${post.status}]`);
141
+ console.log(` Content: ${post.content}`);
142
+ console.log(` Account: ${post.account_id}`);
143
+ if (post.scheduled_at) console.log(` Scheduled: ${post.scheduled_at}`);
144
+ if (post.published_at) console.log(` Published: ${post.published_at}`);
145
+ if (post.tags.length) console.log(` Tags: ${post.tags.join(", ")}`);
146
+ if (Object.keys(post.engagement).length) {
147
+ console.log(` Engagement: ${JSON.stringify(post.engagement)}`);
148
+ }
149
+ }
150
+ });
151
+
152
+ postCmd
153
+ .command("schedule")
154
+ .description("Schedule a post")
155
+ .argument("<id>", "Post ID")
156
+ .requiredOption("--at <datetime>", "Schedule date/time")
157
+ .option("--recurring <recurrence>", "Recurrence (daily/weekly/biweekly/monthly)")
158
+ .option("--json", "Output as JSON", false)
159
+ .action((id, opts) => {
160
+ if (opts.recurring) {
161
+ const post = getPost(id);
162
+ if (!post) {
163
+ console.error(`Post '${id}' not found.`);
164
+ process.exit(1);
165
+ }
166
+ updatePost(id, { recurrence: opts.recurring as Recurrence });
167
+ }
168
+
169
+ const post = schedulePost(id, opts.at);
170
+ if (!post) {
171
+ console.error(`Post '${id}' not found.`);
172
+ process.exit(1);
173
+ }
174
+
175
+ if (opts.json) {
176
+ console.log(JSON.stringify(post, null, 2));
177
+ } else {
178
+ console.log(`Scheduled post ${post.id} for ${post.scheduled_at}${post.recurrence ? ` (recurring: ${post.recurrence})` : ""}`);
179
+ }
180
+ });
181
+
182
+ postCmd
183
+ .command("publish")
184
+ .description("Mark a post as published")
185
+ .argument("<id>", "Post ID")
186
+ .option("--platform-id <id>", "Platform post ID")
187
+ .option("--json", "Output as JSON", false)
188
+ .action((id, opts) => {
189
+ const post = publishPost(id, opts.platformId);
190
+ if (!post) {
191
+ console.error(`Post '${id}' not found.`);
192
+ process.exit(1);
193
+ }
194
+
195
+ if (opts.json) {
196
+ console.log(JSON.stringify(post, null, 2));
197
+ } else {
198
+ console.log(`Published post ${post.id} at ${post.published_at}`);
199
+ }
200
+ });
201
+
202
+ postCmd
203
+ .command("schedule-batch")
204
+ .description("Schedule multiple posts from a JSON file")
205
+ .requiredOption("--file <path>", "Path to JSON file with post array")
206
+ .option("--json", "Output as JSON", false)
207
+ .action((opts) => {
208
+ let postsData;
209
+ try {
210
+ postsData = JSON.parse(readFileSync(opts.file, "utf-8"));
211
+ } catch (err) {
212
+ console.error(`Failed to read file: ${err instanceof Error ? err.message : String(err)}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ if (!Array.isArray(postsData)) {
217
+ console.error("File must contain a JSON array of posts.");
218
+ process.exit(1);
219
+ }
220
+
221
+ const result = batchSchedule(postsData);
222
+
223
+ if (opts.json) {
224
+ console.log(JSON.stringify(result, null, 2));
225
+ } else {
226
+ console.log(`Scheduled ${result.scheduled.length} post(s)`);
227
+ if (result.errors.length > 0) {
228
+ console.log(`Errors: ${result.errors.length}`);
229
+ for (const err of result.errors) {
230
+ console.log(` [${err.index}] ${err.error}`);
231
+ }
232
+ }
233
+ if (result.warnings.length > 0) {
234
+ console.log("Warnings:");
235
+ for (const w of result.warnings) {
236
+ console.log(` ${w.platform}: content (${w.content_length}) exceeds limit (${w.limit}) by ${w.over_by} chars`);
237
+ }
238
+ }
239
+ }
240
+ });
241
+
242
+ postCmd
243
+ .command("crosspost")
244
+ .description("Create identical post on multiple platforms")
245
+ .requiredOption("--content <text>", "Post content")
246
+ .requiredOption("--platforms <list>", "Comma-separated platforms (x,linkedin,bluesky,...)")
247
+ .option("--tags <tags>", "Comma-separated tags")
248
+ .option("--scheduled-at <datetime>", "Schedule date/time")
249
+ .option("--json", "Output as JSON", false)
250
+ .action((opts) => {
251
+ const platforms = opts.platforms.split(",").map((p: string) => p.trim()) as Platform[];
252
+
253
+ try {
254
+ const result = crossPost(opts.content, platforms, {
255
+ tags: opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined,
256
+ scheduled_at: opts.scheduledAt,
257
+ });
258
+
259
+ if (opts.json) {
260
+ console.log(JSON.stringify(result, null, 2));
261
+ } else {
262
+ console.log(`Cross-posted to ${result.posts.length} platform(s):`);
263
+ for (const post of result.posts) {
264
+ const account = getAccount(post.account_id);
265
+ console.log(` ${account?.platform || "?"} → ${post.id} [${post.status}]`);
266
+ }
267
+ if (result.warnings.length > 0) {
268
+ console.log("Warnings:");
269
+ for (const w of result.warnings) {
270
+ console.log(` ${w.platform}: content (${w.content_length}) exceeds limit (${w.limit}) by ${w.over_by} chars`);
271
+ }
272
+ }
273
+ }
274
+ } catch (err) {
275
+ console.error(err instanceof Error ? err.message : String(err));
276
+ process.exit(1);
277
+ }
278
+ });
279
+
280
+ postCmd
281
+ .command("reschedule")
282
+ .description("Reschedule a post to a new date/time")
283
+ .argument("<id>", "Post ID")
284
+ .requiredOption("--to <datetime>", "New schedule date/time")
285
+ .option("--json", "Output as JSON", false)
286
+ .action((id, opts) => {
287
+ try {
288
+ const post = reschedulePost(id, opts.to);
289
+ if (!post) {
290
+ console.error(`Post '${id}' not found.`);
291
+ process.exit(1);
292
+ }
293
+
294
+ if (opts.json) {
295
+ console.log(JSON.stringify(post, null, 2));
296
+ } else {
297
+ console.log(`Rescheduled post ${post.id} to ${post.scheduled_at}`);
298
+ }
299
+ } catch (err) {
300
+ console.error(err instanceof Error ? err.message : String(err));
301
+ process.exit(1);
302
+ }
303
+ });
304
+
305
+ postCmd
306
+ .command("submit")
307
+ .description("Submit a draft post for review")
308
+ .argument("<id>", "Post ID")
309
+ .option("--json", "Output as JSON", false)
310
+ .action((id, opts) => {
311
+ try {
312
+ const post = submitPostForReview(id);
313
+ if (!post) {
314
+ console.error(`Post '${id}' not found.`);
315
+ process.exit(1);
316
+ }
317
+
318
+ if (opts.json) {
319
+ console.log(JSON.stringify(post, null, 2));
320
+ } else {
321
+ console.log(`Post ${post.id} submitted for review [${post.status}]`);
322
+ }
323
+ } catch (err) {
324
+ console.error(err instanceof Error ? err.message : String(err));
325
+ process.exit(1);
326
+ }
327
+ });
328
+
329
+ postCmd
330
+ .command("approve")
331
+ .description("Approve a post pending review")
332
+ .argument("<id>", "Post ID")
333
+ .option("--at <datetime>", "Schedule date/time for approved post")
334
+ .option("--json", "Output as JSON", false)
335
+ .action((id, opts) => {
336
+ try {
337
+ const post = approvePost(id, opts.at);
338
+ if (!post) {
339
+ console.error(`Post '${id}' not found.`);
340
+ process.exit(1);
341
+ }
342
+
343
+ if (opts.json) {
344
+ console.log(JSON.stringify(post, null, 2));
345
+ } else {
346
+ console.log(`Post ${post.id} approved and scheduled for ${post.scheduled_at}`);
347
+ }
348
+ } catch (err) {
349
+ console.error(err instanceof Error ? err.message : String(err));
350
+ process.exit(1);
351
+ }
352
+ });
353
+
354
+ postCmd
355
+ .command("delete")
356
+ .description("Delete a post")
357
+ .argument("<id>", "Post ID")
358
+ .action((id) => {
359
+ const deleted = deletePost(id);
360
+ if (deleted) {
361
+ console.log(`Deleted post ${id}`);
362
+ } else {
363
+ console.error(`Post '${id}' not found.`);
364
+ process.exit(1);
365
+ }
366
+ });
367
+
368
+ // --- Accounts ---
369
+
370
+ const accountCmd = program
371
+ .command("account")
372
+ .description("Account management");
373
+
374
+ accountCmd
375
+ .command("add")
376
+ .description("Add a social media account")
377
+ .requiredOption("--platform <platform>", "Platform (x/linkedin/instagram/threads/bluesky)")
378
+ .requiredOption("--handle <handle>", "Account handle")
379
+ .option("--display-name <name>", "Display name")
380
+ .option("--connected", "Mark as connected", false)
381
+ .option("--token-env <var>", "Environment variable for access token")
382
+ .option("--json", "Output as JSON", false)
383
+ .action((opts) => {
384
+ const account = createAccount({
385
+ platform: opts.platform as Platform,
386
+ handle: opts.handle,
387
+ display_name: opts.displayName,
388
+ connected: opts.connected,
389
+ access_token_env: opts.tokenEnv,
390
+ });
391
+
392
+ if (opts.json) {
393
+ console.log(JSON.stringify(account, null, 2));
394
+ } else {
395
+ console.log(`Added account: @${account.handle} on ${account.platform} (${account.id})`);
396
+ }
397
+ });
398
+
399
+ accountCmd
400
+ .command("list")
401
+ .description("List accounts")
402
+ .option("--platform <platform>", "Filter by platform")
403
+ .option("--connected", "Show only connected accounts")
404
+ .option("--json", "Output as JSON", false)
405
+ .action((opts) => {
406
+ const accounts = listAccounts({
407
+ platform: opts.platform as Platform | undefined,
408
+ connected: opts.connected ? true : undefined,
409
+ });
410
+
411
+ if (opts.json) {
412
+ console.log(JSON.stringify(accounts, null, 2));
413
+ } else {
414
+ if (accounts.length === 0) {
415
+ console.log("No accounts found.");
416
+ return;
417
+ }
418
+ for (const a of accounts) {
419
+ const connected = a.connected ? " (connected)" : "";
420
+ const name = a.display_name ? ` - ${a.display_name}` : "";
421
+ console.log(` [${a.platform}] @${a.handle}${name}${connected}`);
422
+ }
423
+ console.log(`\n${accounts.length} account(s)`);
424
+ }
425
+ });
426
+
427
+ accountCmd
428
+ .command("remove")
429
+ .description("Remove an account")
430
+ .argument("<id>", "Account ID")
431
+ .action((id) => {
432
+ const deleted = deleteAccount(id);
433
+ if (deleted) {
434
+ console.log(`Removed account ${id}`);
435
+ } else {
436
+ console.error(`Account '${id}' not found.`);
437
+ process.exit(1);
438
+ }
439
+ });
440
+
441
+ // --- Calendar ---
442
+
443
+ program
444
+ .command("calendar")
445
+ .description("View scheduled posts by date")
446
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
447
+ .option("--end <date>", "End date (YYYY-MM-DD)")
448
+ .option("--json", "Output as JSON", false)
449
+ .action((opts) => {
450
+ const calendar = getCalendar(opts.start, opts.end);
451
+
452
+ if (opts.json) {
453
+ console.log(JSON.stringify(calendar, null, 2));
454
+ } else {
455
+ const dates = Object.keys(calendar).sort();
456
+ if (dates.length === 0) {
457
+ console.log("No scheduled posts.");
458
+ return;
459
+ }
460
+ for (const date of dates) {
461
+ console.log(`\n${date}:`);
462
+ for (const post of calendar[date]) {
463
+ const preview = post.content.substring(0, 50) + (post.content.length > 50 ? "..." : "");
464
+ console.log(` ${post.scheduled_at} — ${preview}`);
465
+ }
466
+ }
467
+ }
468
+ });
469
+
470
+ // --- Analytics ---
471
+
472
+ const analyticsCmd = program
473
+ .command("analytics")
474
+ .description("Engagement analytics");
475
+
476
+ analyticsCmd
477
+ .command("engagement")
478
+ .description("View engagement analytics")
479
+ .option("--account <id>", "Filter by account ID")
480
+ .option("--by-platform", "Group by platform")
481
+ .option("--json", "Output as JSON", false)
482
+ .action((opts) => {
483
+ if (opts.byPlatform) {
484
+ const stats = getStatsByPlatform();
485
+
486
+ if (opts.json) {
487
+ console.log(JSON.stringify(stats, null, 2));
488
+ } else {
489
+ if (stats.length === 0) {
490
+ console.log("No platform data.");
491
+ return;
492
+ }
493
+ for (const s of stats) {
494
+ console.log(`\n${s.platform} (${s.account_count} account(s), ${s.post_count} post(s)):`);
495
+ console.log(` Published: ${s.engagement.total_posts}`);
496
+ console.log(` Likes: ${s.engagement.total_likes} (avg ${s.engagement.avg_likes})`);
497
+ console.log(` Shares: ${s.engagement.total_shares} (avg ${s.engagement.avg_shares})`);
498
+ console.log(` Comments: ${s.engagement.total_comments} (avg ${s.engagement.avg_comments})`);
499
+ console.log(` Impressions: ${s.engagement.total_impressions} (avg ${s.engagement.avg_impressions})`);
500
+ }
501
+ }
502
+ } else {
503
+ const stats = getEngagementStats(opts.account);
504
+
505
+ if (opts.json) {
506
+ console.log(JSON.stringify(stats, null, 2));
507
+ } else {
508
+ console.log("Engagement Analytics:");
509
+ console.log(` Published posts: ${stats.total_posts}`);
510
+ console.log(` Total likes: ${stats.total_likes} (avg ${stats.avg_likes})`);
511
+ console.log(` Total shares: ${stats.total_shares} (avg ${stats.avg_shares})`);
512
+ console.log(` Total comments: ${stats.total_comments} (avg ${stats.avg_comments})`);
513
+ console.log(` Total impressions: ${stats.total_impressions} (avg ${stats.avg_impressions})`);
514
+ console.log(` Total clicks: ${stats.total_clicks} (avg ${stats.avg_clicks})`);
515
+ }
516
+ }
517
+ });
518
+
519
+ analyticsCmd
520
+ .command("best-time")
521
+ .description("Find best time to post based on historical engagement")
522
+ .requiredOption("--account <id>", "Account ID")
523
+ .option("--json", "Output as JSON", false)
524
+ .action((opts) => {
525
+ const result = getBestTimeToPost(opts.account);
526
+
527
+ if (opts.json) {
528
+ console.log(JSON.stringify(result, null, 2));
529
+ } else {
530
+ if (result.total_analyzed === 0) {
531
+ console.log("No published posts to analyze.");
532
+ return;
533
+ }
534
+ console.log(`Analyzed ${result.total_analyzed} published post(s)\n`);
535
+
536
+ if (result.best_hours.length > 0) {
537
+ console.log("Best hours to post:");
538
+ for (const slot of result.best_hours.slice(0, 5)) {
539
+ console.log(` ${slot.day_name} ${slot.hour}:00 — avg engagement: ${slot.avg_engagement} (${slot.post_count} posts)`);
540
+ }
541
+ }
542
+
543
+ if (result.best_days.length > 0) {
544
+ console.log("\nBest days to post:");
545
+ for (const day of result.best_days) {
546
+ console.log(` ${day.day_name} — avg engagement: ${day.avg_engagement} (${day.post_count} posts)`);
547
+ }
548
+ }
549
+ }
550
+ });
551
+
552
+ analyticsCmd
553
+ .command("hashtags")
554
+ .description("Hashtag performance analytics")
555
+ .requiredOption("--account <id>", "Account ID")
556
+ .option("--json", "Output as JSON", false)
557
+ .action((opts) => {
558
+ const stats = getHashtagStats(opts.account);
559
+
560
+ if (opts.json) {
561
+ console.log(JSON.stringify(stats, null, 2));
562
+ } else {
563
+ if (stats.length === 0) {
564
+ console.log("No hashtags found in published posts.");
565
+ return;
566
+ }
567
+ console.log("Hashtag Analytics:");
568
+ for (const h of stats) {
569
+ console.log(` #${h.hashtag} — ${h.post_count} post(s), avg engagement: ${h.avg_engagement}`);
570
+ console.log(` likes: ${h.total_likes}, shares: ${h.total_shares}, comments: ${h.total_comments}`);
571
+ }
572
+ }
573
+ });
574
+
575
+ // --- Templates ---
576
+
577
+ const templateCmd = program
578
+ .command("template")
579
+ .description("Post template management");
580
+
581
+ templateCmd
582
+ .command("create")
583
+ .description("Create a post template")
584
+ .requiredOption("--name <name>", "Template name")
585
+ .requiredOption("--content <content>", "Template content (use {{var}} for variables)")
586
+ .option("--variables <vars>", "Comma-separated variable names")
587
+ .option("--json", "Output as JSON", false)
588
+ .action((opts) => {
589
+ const template = createTemplate({
590
+ name: opts.name,
591
+ content: opts.content,
592
+ variables: opts.variables ? opts.variables.split(",").map((v: string) => v.trim()) : undefined,
593
+ });
594
+
595
+ if (opts.json) {
596
+ console.log(JSON.stringify(template, null, 2));
597
+ } else {
598
+ console.log(`Created template: ${template.name} (${template.id})`);
599
+ }
600
+ });
601
+
602
+ templateCmd
603
+ .command("list")
604
+ .description("List templates")
605
+ .option("--json", "Output as JSON", false)
606
+ .action((opts) => {
607
+ const templates = listTemplates();
608
+
609
+ if (opts.json) {
610
+ console.log(JSON.stringify(templates, null, 2));
611
+ } else {
612
+ if (templates.length === 0) {
613
+ console.log("No templates found.");
614
+ return;
615
+ }
616
+ for (const t of templates) {
617
+ const vars = t.variables.length ? ` (vars: ${t.variables.join(", ")})` : "";
618
+ console.log(` ${t.name}${vars}`);
619
+ }
620
+ console.log(`\n${templates.length} template(s)`);
621
+ }
622
+ });
623
+
624
+ templateCmd
625
+ .command("use")
626
+ .description("Create a post from a template")
627
+ .argument("<template-id>", "Template ID")
628
+ .requiredOption("--account <id>", "Account ID")
629
+ .option("--values <json>", "JSON object of variable values")
630
+ .option("--tags <tags>", "Comma-separated tags")
631
+ .option("--json", "Output as JSON", false)
632
+ .action((templateId, opts) => {
633
+ const values = opts.values ? JSON.parse(opts.values) : {};
634
+ const tags = opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined;
635
+
636
+ const post = useTemplate(templateId, opts.account, values, tags);
637
+
638
+ if (opts.json) {
639
+ console.log(JSON.stringify(post, null, 2));
640
+ } else {
641
+ console.log(`Created post from template: ${post.id}`);
642
+ console.log(` Content: ${post.content.substring(0, 80)}${post.content.length > 80 ? "..." : ""}`);
643
+ }
644
+ });
645
+
646
+ templateCmd
647
+ .command("delete")
648
+ .description("Delete a template")
649
+ .argument("<id>", "Template ID")
650
+ .action((id) => {
651
+ const deleted = deleteTemplate(id);
652
+ if (deleted) {
653
+ console.log(`Deleted template ${id}`);
654
+ } else {
655
+ console.error(`Template '${id}' not found.`);
656
+ process.exit(1);
657
+ }
658
+ });
659
+
660
+ // --- Stats ---
661
+
662
+ program
663
+ .command("stats")
664
+ .description("Overall statistics")
665
+ .option("--json", "Output as JSON", false)
666
+ .action((opts) => {
667
+ const stats = getOverallStats();
668
+
669
+ if (opts.json) {
670
+ console.log(JSON.stringify(stats, null, 2));
671
+ } else {
672
+ console.log("Social Media Stats:");
673
+ console.log(` Accounts: ${stats.total_accounts}`);
674
+ console.log(` Posts: ${stats.total_posts}`);
675
+ if (Object.keys(stats.posts_by_status).length) {
676
+ for (const [status, count] of Object.entries(stats.posts_by_status)) {
677
+ console.log(` ${status}: ${count}`);
678
+ }
679
+ }
680
+ console.log(` Templates: ${stats.total_templates}`);
681
+ console.log(` Published engagement:`);
682
+ console.log(` Likes: ${stats.engagement.total_likes}`);
683
+ console.log(` Shares: ${stats.engagement.total_shares}`);
684
+ console.log(` Comments: ${stats.engagement.total_comments}`);
685
+ console.log(` Impressions: ${stats.engagement.total_impressions}`);
686
+ }
687
+ });
688
+
689
+ program.parse(process.argv);