@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.
- package/bin/index.js +63 -0
- package/bin/mcp.js +63 -0
- package/dist/index.js +63 -0
- package/microservices/microservice-ads/package.json +27 -0
- package/microservices/microservice-ads/src/cli/index.ts +605 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +797 -0
- package/microservices/microservice-ads/src/db/database.ts +93 -0
- package/microservices/microservice-ads/src/db/migrations.ts +60 -0
- package/microservices/microservice-ads/src/index.ts +39 -0
- package/microservices/microservice-ads/src/mcp/index.ts +480 -0
- package/microservices/microservice-contracts/package.json +27 -0
- package/microservices/microservice-contracts/src/cli/index.ts +770 -0
- package/microservices/microservice-contracts/src/db/contracts.ts +925 -0
- package/microservices/microservice-contracts/src/db/database.ts +93 -0
- package/microservices/microservice-contracts/src/db/migrations.ts +141 -0
- package/microservices/microservice-contracts/src/index.ts +43 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +617 -0
- package/microservices/microservice-domains/package.json +27 -0
- package/microservices/microservice-domains/src/cli/index.ts +691 -0
- package/microservices/microservice-domains/src/db/database.ts +93 -0
- package/microservices/microservice-domains/src/db/domains.ts +1164 -0
- package/microservices/microservice-domains/src/db/migrations.ts +60 -0
- package/microservices/microservice-domains/src/index.ts +65 -0
- package/microservices/microservice-domains/src/mcp/index.ts +536 -0
- package/microservices/microservice-hiring/package.json +27 -0
- package/microservices/microservice-hiring/src/cli/index.ts +741 -0
- package/microservices/microservice-hiring/src/db/database.ts +93 -0
- package/microservices/microservice-hiring/src/db/hiring.ts +1085 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +89 -0
- package/microservices/microservice-hiring/src/index.ts +80 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +709 -0
- package/microservices/microservice-payments/package.json +27 -0
- package/microservices/microservice-payments/src/cli/index.ts +609 -0
- package/microservices/microservice-payments/src/db/database.ts +93 -0
- package/microservices/microservice-payments/src/db/migrations.ts +81 -0
- package/microservices/microservice-payments/src/db/payments.ts +1204 -0
- package/microservices/microservice-payments/src/index.ts +51 -0
- package/microservices/microservice-payments/src/mcp/index.ts +683 -0
- package/microservices/microservice-payroll/package.json +27 -0
- package/microservices/microservice-payroll/src/cli/index.ts +643 -0
- package/microservices/microservice-payroll/src/db/database.ts +93 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +95 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +1377 -0
- package/microservices/microservice-payroll/src/index.ts +48 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +666 -0
- package/microservices/microservice-shipping/package.json +27 -0
- package/microservices/microservice-shipping/src/cli/index.ts +606 -0
- package/microservices/microservice-shipping/src/db/database.ts +93 -0
- package/microservices/microservice-shipping/src/db/migrations.ts +69 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +1093 -0
- package/microservices/microservice-shipping/src/index.ts +53 -0
- package/microservices/microservice-shipping/src/mcp/index.ts +533 -0
- package/microservices/microservice-social/package.json +27 -0
- package/microservices/microservice-social/src/cli/index.ts +689 -0
- package/microservices/microservice-social/src/db/database.ts +93 -0
- package/microservices/microservice-social/src/db/migrations.ts +88 -0
- package/microservices/microservice-social/src/db/social.ts +1046 -0
- package/microservices/microservice-social/src/index.ts +46 -0
- package/microservices/microservice-social/src/mcp/index.ts +655 -0
- package/microservices/microservice-subscriptions/package.json +27 -0
- package/microservices/microservice-subscriptions/src/cli/index.ts +715 -0
- package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +125 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +1256 -0
- package/microservices/microservice-subscriptions/src/index.ts +41 -0
- package/microservices/microservice-subscriptions/src/mcp/index.ts +631 -0
- 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);
|