posterboy 0.1.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/index.js +3645 -0
  4. package/package.json +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,3645 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+ import { createRequire } from "node:module";
4
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
+
6
+ // src/index.ts
7
+ import { parseArgs as parseArgs20 } from "util";
8
+
9
+ // src/constants.ts
10
+ var VERSION = "0.1.5";
11
+ var CONFIG_DIR = `${process.env.HOME}/.posterboy`;
12
+ var CONFIG_FILE = `${CONFIG_DIR}/config.json`;
13
+
14
+ // src/commands/auth/login.ts
15
+ import { parseArgs } from "node:util";
16
+ import { stdin, stdout } from "node:process";
17
+ import { createInterface } from "node:readline";
18
+
19
+ // src/constants.ts
20
+ var CONFIG_DIR2 = `${process.env.HOME}/.posterboy`;
21
+ var CONFIG_FILE2 = `${CONFIG_DIR2}/config.json`;
22
+ var API_BASE_URL = "https://api.upload-post.com/api";
23
+ var ALL_PLATFORMS = [
24
+ "tiktok",
25
+ "instagram",
26
+ "youtube",
27
+ "linkedin",
28
+ "facebook",
29
+ "x",
30
+ "threads",
31
+ "pinterest",
32
+ "reddit",
33
+ "bluesky"
34
+ ];
35
+ var EXIT_CODES = {
36
+ SUCCESS: 0,
37
+ USER_ERROR: 1,
38
+ API_ERROR: 2,
39
+ NETWORK_ERROR: 3
40
+ };
41
+
42
+ // src/lib/errors.ts
43
+ class PosterBoyError extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = this.constructor.name;
47
+ Error.captureStackTrace(this, this.constructor);
48
+ }
49
+ json() {
50
+ return {
51
+ success: false,
52
+ error: this.message,
53
+ code: this.name.replace(/Error$/, "").toUpperCase()
54
+ };
55
+ }
56
+ }
57
+
58
+ class UserError extends PosterBoyError {
59
+ exitCode = EXIT_CODES.USER_ERROR;
60
+ }
61
+
62
+ class ApiError extends PosterBoyError {
63
+ exitCode = EXIT_CODES.API_ERROR;
64
+ statusCode;
65
+ apiMessage;
66
+ constructor(message, statusCode, apiMessage) {
67
+ super(message);
68
+ this.statusCode = statusCode;
69
+ this.apiMessage = apiMessage || message;
70
+ }
71
+ json() {
72
+ return {
73
+ success: false,
74
+ error: this.message,
75
+ code: "API_ERROR",
76
+ statusCode: this.statusCode,
77
+ apiMessage: this.apiMessage
78
+ };
79
+ }
80
+ }
81
+
82
+ class NetworkError extends PosterBoyError {
83
+ exitCode = EXIT_CODES.NETWORK_ERROR;
84
+ }
85
+
86
+ // src/lib/config.ts
87
+ import { mkdirSync, readFileSync, existsSync } from "node:fs";
88
+ import { writeFile, chmod } from "node:fs/promises";
89
+ import { dirname } from "node:path";
90
+ function readConfig() {
91
+ const configPath = resolveConfigPath();
92
+ if (!existsSync(configPath)) {
93
+ return null;
94
+ }
95
+ try {
96
+ const content = readFileSync(configPath, "utf-8");
97
+ const config = JSON.parse(content);
98
+ validateConfig(config);
99
+ return config;
100
+ } catch (error) {
101
+ if (error instanceof UserError) {
102
+ throw error;
103
+ }
104
+ throw new UserError(`Failed to read config file at ${configPath}.
105
+ ` + `The file may be corrupted. Please run 'posterboy auth login' to recreate it.`);
106
+ }
107
+ }
108
+ async function writeConfig(config) {
109
+ const configPath = resolveConfigPath();
110
+ const configDir = dirname(configPath);
111
+ if (!existsSync(configDir)) {
112
+ mkdirSync(configDir, { recursive: true, mode: 448 });
113
+ }
114
+ validateConfig(config);
115
+ const content = JSON.stringify(config, null, 2);
116
+ await writeFile(configPath, content, { mode: 384 });
117
+ await chmod(configDir, 448);
118
+ }
119
+ function validateConfig(config) {
120
+ if (typeof config !== "object" || config === null) {
121
+ throw new UserError("Config must be an object");
122
+ }
123
+ if (typeof config.version !== "number") {
124
+ throw new UserError("Config must have a version number");
125
+ }
126
+ if (config.version !== 1) {
127
+ throw new UserError(`Unsupported config version: ${config.version}. Expected version 1.`);
128
+ }
129
+ if (config.api_key !== undefined && typeof config.api_key !== "string") {
130
+ throw new UserError("Config api_key must be a string");
131
+ }
132
+ if (config.default_profile !== undefined && typeof config.default_profile !== "string") {
133
+ throw new UserError("Config default_profile must be a string");
134
+ }
135
+ if (config.default_platforms !== undefined) {
136
+ if (!Array.isArray(config.default_platforms)) {
137
+ throw new UserError("Config default_platforms must be an array");
138
+ }
139
+ }
140
+ if (config.default_timezone !== undefined && typeof config.default_timezone !== "string") {
141
+ throw new UserError("Config default_timezone must be a string");
142
+ }
143
+ }
144
+ function resolveConfigPath(overridePath) {
145
+ return overridePath || process.env.POSTERBOY_CONFIG || CONFIG_FILE2;
146
+ }
147
+ function resolveValue(flagValue, envVar, configValue, defaultValue) {
148
+ if (flagValue !== undefined) {
149
+ return flagValue;
150
+ }
151
+ const envValue = process.env[envVar];
152
+ if (envValue !== undefined) {
153
+ return envValue;
154
+ }
155
+ if (configValue !== undefined) {
156
+ return configValue;
157
+ }
158
+ return defaultValue;
159
+ }
160
+ function getApiKey(flagValue, configValue) {
161
+ const apiKey = resolveValue(flagValue, "POSTERBOY_API_KEY", configValue, "");
162
+ if (!apiKey) {
163
+ throw new UserError(`API key not found.
164
+ ` + "Please run 'posterboy auth login' or set POSTERBOY_API_KEY environment variable.");
165
+ }
166
+ return apiKey;
167
+ }
168
+ function getDefaultProfile(flagValue, config) {
169
+ return resolveValue(flagValue, "POSTERBOY_PROFILE", config?.default_profile, undefined);
170
+ }
171
+
172
+ // src/lib/api.ts
173
+ class ApiClient {
174
+ apiKey;
175
+ baseUrl;
176
+ verbose;
177
+ constructor(apiKey, options) {
178
+ this.apiKey = apiKey;
179
+ this.baseUrl = options?.baseUrl ?? API_BASE_URL;
180
+ this.verbose = options?.verbose ?? false;
181
+ }
182
+ async request(method, path, body) {
183
+ const url = `${this.baseUrl}${path}`;
184
+ const controller = new AbortController;
185
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
186
+ if (this.verbose) {
187
+ const headers = {
188
+ Authorization: `Bearer ${this.apiKey.substring(0, 7)}...`,
189
+ "Content-Type": "application/json",
190
+ Accept: "application/json"
191
+ };
192
+ console.error(`[verbose] → ${method} ${url}`);
193
+ console.error(`[verbose] Headers: ${JSON.stringify(headers)}`);
194
+ }
195
+ const startTime = Date.now();
196
+ try {
197
+ const response = await fetch(url, {
198
+ method,
199
+ headers: {
200
+ Authorization: `Bearer ${this.apiKey}`,
201
+ "Content-Type": "application/json",
202
+ Accept: "application/json"
203
+ },
204
+ body: body ? JSON.stringify(body) : undefined,
205
+ signal: controller.signal
206
+ });
207
+ clearTimeout(timeoutId);
208
+ const elapsed = Date.now() - startTime;
209
+ if (this.verbose) {
210
+ console.error(`[verbose] ← ${response.status} ${response.statusText} (${elapsed}ms)`);
211
+ }
212
+ if (!response.ok) {
213
+ const errorBody = await response.text();
214
+ let errorMessage = `API error: ${response.status}`;
215
+ let apiMessage = errorMessage;
216
+ try {
217
+ const parsed = JSON.parse(errorBody);
218
+ errorMessage = parsed.message || parsed.error || errorMessage;
219
+ apiMessage = parsed.message || parsed.error || apiMessage;
220
+ } catch {}
221
+ throw new ApiError(errorMessage, response.status, apiMessage);
222
+ }
223
+ try {
224
+ return await response.json();
225
+ } catch {
226
+ throw new ApiError("Invalid JSON response from API", response.status, "Response body was not valid JSON");
227
+ }
228
+ } catch (error) {
229
+ clearTimeout(timeoutId);
230
+ if (this.verbose && error instanceof Error) {
231
+ console.error(`[verbose] ✗ ${error.message}`);
232
+ }
233
+ if (error instanceof ApiError)
234
+ throw error;
235
+ if (error instanceof Error && error.name === "AbortError") {
236
+ throw new NetworkError("Request timeout after 30 seconds");
237
+ }
238
+ throw new NetworkError(error instanceof Error ? error.message : "Network request failed");
239
+ }
240
+ }
241
+ async uploadRequest(method, path, formData) {
242
+ const url = `${this.baseUrl}${path}`;
243
+ const controller = new AbortController;
244
+ const timeoutId = setTimeout(() => controller.abort(), 120000);
245
+ if (this.verbose) {
246
+ const headers = {
247
+ Authorization: `Bearer ${this.apiKey.substring(0, 7)}...`,
248
+ Accept: "application/json"
249
+ };
250
+ console.error(`[verbose] → ${method} ${url} (upload)`);
251
+ console.error(`[verbose] Headers: ${JSON.stringify(headers)}`);
252
+ }
253
+ const startTime = Date.now();
254
+ try {
255
+ const response = await fetch(url, {
256
+ method,
257
+ headers: {
258
+ Authorization: `Bearer ${this.apiKey}`,
259
+ Accept: "application/json"
260
+ },
261
+ body: formData,
262
+ signal: controller.signal
263
+ });
264
+ clearTimeout(timeoutId);
265
+ const elapsed = Date.now() - startTime;
266
+ if (this.verbose) {
267
+ console.error(`[verbose] ← ${response.status} ${response.statusText} (${elapsed}ms)`);
268
+ }
269
+ if (!response.ok) {
270
+ const errorBody = await response.text();
271
+ let errorMessage = `API error: ${response.status}`;
272
+ let apiMessage = errorMessage;
273
+ try {
274
+ const parsed = JSON.parse(errorBody);
275
+ errorMessage = parsed.message || parsed.error || errorMessage;
276
+ apiMessage = parsed.message || parsed.error || apiMessage;
277
+ } catch {}
278
+ throw new ApiError(errorMessage, response.status, apiMessage);
279
+ }
280
+ try {
281
+ return await response.json();
282
+ } catch {
283
+ throw new ApiError("Invalid JSON response from API", response.status, "Response body was not valid JSON");
284
+ }
285
+ } catch (error) {
286
+ clearTimeout(timeoutId);
287
+ if (this.verbose && error instanceof Error) {
288
+ console.error(`[verbose] ✗ ${error.message}`);
289
+ }
290
+ if (error instanceof ApiError)
291
+ throw error;
292
+ if (error instanceof Error && error.name === "AbortError") {
293
+ throw new NetworkError("Request timeout after 120 seconds");
294
+ }
295
+ throw new NetworkError(error instanceof Error ? error.message : "Network request failed");
296
+ }
297
+ }
298
+ async me() {
299
+ return this.request("GET", "/uploadposts/me");
300
+ }
301
+ async listUsers() {
302
+ return this.request("GET", "/uploadposts/users");
303
+ }
304
+ async createUser(username) {
305
+ return this.request("POST", "/uploadposts/users", { username });
306
+ }
307
+ async deleteUser(username) {
308
+ return this.request("DELETE", "/uploadposts/users", { username });
309
+ }
310
+ async generateJwt(username, options) {
311
+ const body = { username };
312
+ if (options?.platforms)
313
+ body.platforms = options.platforms;
314
+ if (options?.redirect)
315
+ body.redirect = options.redirect;
316
+ return this.request("POST", "/uploadposts/users/generate-jwt", body);
317
+ }
318
+ async getUserProfile(username) {
319
+ return this.request("GET", `/uploadposts/users/${username}`);
320
+ }
321
+ async facebookPages(username) {
322
+ return this.request("GET", `/uploadposts/facebook/pages?username=${username}`);
323
+ }
324
+ async linkedinPages(username) {
325
+ return this.request("GET", `/uploadposts/linkedin/pages?username=${username}`);
326
+ }
327
+ async pinterestBoards(username) {
328
+ return this.request("GET", `/uploadposts/pinterest/boards?username=${username}`);
329
+ }
330
+ async postText(params) {
331
+ const body = {
332
+ profile: params.profile,
333
+ platforms: params.platforms,
334
+ text: params.text
335
+ };
336
+ if (params.schedule)
337
+ body.schedule = params.schedule;
338
+ if (params.timezone)
339
+ body.timezone = params.timezone;
340
+ if (params.queue)
341
+ body.queue = params.queue;
342
+ if (params.async)
343
+ body.async = params.async;
344
+ if (params.first_comment)
345
+ body.first_comment = params.first_comment;
346
+ if (params.x_title)
347
+ body.x_title = params.x_title;
348
+ if (params.x_reply_to)
349
+ body.x_reply_to = params.x_reply_to;
350
+ if (params.x_reply_settings)
351
+ body.x_reply_settings = params.x_reply_settings;
352
+ if (params.x_quote_tweet)
353
+ body.x_quote_tweet = params.x_quote_tweet;
354
+ if (params.x_long_text_as_post !== undefined)
355
+ body.x_long_text_as_post = params.x_long_text_as_post;
356
+ if (params.x_poll_options)
357
+ body.x_poll_options = params.x_poll_options;
358
+ if (params.x_poll_duration)
359
+ body.x_poll_duration = params.x_poll_duration;
360
+ if (params.linkedin_title)
361
+ body.linkedin_title = params.linkedin_title;
362
+ if (params.linkedin_page)
363
+ body.linkedin_page = params.linkedin_page;
364
+ if (params.linkedin_visibility)
365
+ body.linkedin_visibility = params.linkedin_visibility;
366
+ if (params.facebook_title)
367
+ body.facebook_title = params.facebook_title;
368
+ if (params.facebook_page)
369
+ body.facebook_page = params.facebook_page;
370
+ if (params.facebook_link)
371
+ body.facebook_link = params.facebook_link;
372
+ if (params.threads_title)
373
+ body.threads_title = params.threads_title;
374
+ if (params.threads_long_text_as_post !== undefined)
375
+ body.threads_long_text_as_post = params.threads_long_text_as_post;
376
+ if (params.reddit_subreddit)
377
+ body.reddit_subreddit = params.reddit_subreddit;
378
+ if (params.reddit_flair)
379
+ body.reddit_flair = params.reddit_flair;
380
+ if (params.bluesky_title)
381
+ body.bluesky_title = params.bluesky_title;
382
+ if (params.bluesky_reply_to)
383
+ body.bluesky_reply_to = params.bluesky_reply_to;
384
+ return this.request("POST", "/upload_text", body);
385
+ }
386
+ async postPhotos(params) {
387
+ const formData = new FormData;
388
+ if (params.files) {
389
+ for (let i = 0;i < params.files.length; i++) {
390
+ const file = Bun.file(params.files[i]);
391
+ formData.append(`media_file_${i + 1}`, file);
392
+ }
393
+ } else if (params.urls) {
394
+ for (let i = 0;i < params.urls.length; i++) {
395
+ formData.append(`media_url_${i + 1}`, params.urls[i]);
396
+ }
397
+ }
398
+ formData.append("profile", params.profile);
399
+ formData.append("platforms", JSON.stringify(params.platforms));
400
+ formData.append("title", params.title);
401
+ if (params.description)
402
+ formData.append("description", params.description);
403
+ if (params.schedule)
404
+ formData.append("schedule", params.schedule);
405
+ if (params.timezone)
406
+ formData.append("timezone", params.timezone);
407
+ if (params.queue)
408
+ formData.append("queue", String(params.queue));
409
+ if (params.async)
410
+ formData.append("async", String(params.async));
411
+ if (params.first_comment)
412
+ formData.append("first_comment", params.first_comment);
413
+ if (params.instagram_title)
414
+ formData.append("instagram_title", params.instagram_title);
415
+ if (params.instagram_media_type)
416
+ formData.append("instagram_media_type", params.instagram_media_type);
417
+ if (params.instagram_collaborators)
418
+ formData.append("instagram_collaborators", params.instagram_collaborators);
419
+ if (params.instagram_location)
420
+ formData.append("instagram_location", params.instagram_location);
421
+ if (params.instagram_user_tags)
422
+ formData.append("instagram_user_tags", params.instagram_user_tags);
423
+ if (params.facebook_page)
424
+ formData.append("facebook_page", params.facebook_page);
425
+ if (params.facebook_media_type)
426
+ formData.append("facebook_media_type", params.facebook_media_type);
427
+ if (params.tiktok_title)
428
+ formData.append("tiktok_title", params.tiktok_title);
429
+ if (params.tiktok_privacy)
430
+ formData.append("tiktok_privacy", params.tiktok_privacy);
431
+ if (params.tiktok_disable_comments !== undefined)
432
+ formData.append("tiktok_disable_comments", String(params.tiktok_disable_comments));
433
+ if (params.tiktok_auto_music !== undefined)
434
+ formData.append("tiktok_auto_music", String(params.tiktok_auto_music));
435
+ if (params.tiktok_cover_index !== undefined)
436
+ formData.append("tiktok_cover_index", String(params.tiktok_cover_index));
437
+ if (params.x_title)
438
+ formData.append("x_title", params.x_title);
439
+ if (params.x_thread_image_layout)
440
+ formData.append("x_thread_image_layout", params.x_thread_image_layout);
441
+ if (params.linkedin_title)
442
+ formData.append("linkedin_title", params.linkedin_title);
443
+ if (params.linkedin_page)
444
+ formData.append("linkedin_page", params.linkedin_page);
445
+ if (params.linkedin_visibility)
446
+ formData.append("linkedin_visibility", params.linkedin_visibility);
447
+ if (params.threads_title)
448
+ formData.append("threads_title", params.threads_title);
449
+ if (params.pinterest_board)
450
+ formData.append("pinterest_board", params.pinterest_board);
451
+ if (params.pinterest_link)
452
+ formData.append("pinterest_link", params.pinterest_link);
453
+ if (params.pinterest_alt_text)
454
+ formData.append("pinterest_alt_text", params.pinterest_alt_text);
455
+ if (params.reddit_subreddit)
456
+ formData.append("reddit_subreddit", params.reddit_subreddit);
457
+ if (params.reddit_flair)
458
+ formData.append("reddit_flair", params.reddit_flair);
459
+ if (params.bluesky_title)
460
+ formData.append("bluesky_title", params.bluesky_title);
461
+ return this.uploadRequest("POST", "/upload_photos", formData);
462
+ }
463
+ async postVideo(params) {
464
+ const formData = new FormData;
465
+ if (params.file) {
466
+ formData.append("media_file", Bun.file(params.file));
467
+ } else if (params.url) {
468
+ formData.append("media_url", params.url);
469
+ }
470
+ formData.append("profile", params.profile);
471
+ formData.append("platforms", JSON.stringify(params.platforms));
472
+ formData.append("title", params.title);
473
+ if (params.description)
474
+ formData.append("description", params.description);
475
+ if (params.schedule)
476
+ formData.append("schedule", params.schedule);
477
+ if (params.timezone)
478
+ formData.append("timezone", params.timezone);
479
+ if (params.queue)
480
+ formData.append("queue", String(params.queue));
481
+ if (params.async)
482
+ formData.append("async", String(params.async));
483
+ if (params.first_comment)
484
+ formData.append("first_comment", params.first_comment);
485
+ if (params.tiktok_title)
486
+ formData.append("tiktok_title", params.tiktok_title);
487
+ if (params.tiktok_privacy)
488
+ formData.append("tiktok_privacy", params.tiktok_privacy);
489
+ if (params.tiktok_disable_duet !== undefined)
490
+ formData.append("tiktok_disable_duet", String(params.tiktok_disable_duet));
491
+ if (params.tiktok_disable_comment !== undefined)
492
+ formData.append("tiktok_disable_comment", String(params.tiktok_disable_comment));
493
+ if (params.tiktok_disable_stitch !== undefined)
494
+ formData.append("tiktok_disable_stitch", String(params.tiktok_disable_stitch));
495
+ if (params.tiktok_post_mode)
496
+ formData.append("tiktok_post_mode", params.tiktok_post_mode);
497
+ if (params.tiktok_cover_timestamp !== undefined)
498
+ formData.append("tiktok_cover_timestamp", String(params.tiktok_cover_timestamp));
499
+ if (params.tiktok_brand_content !== undefined)
500
+ formData.append("tiktok_brand_content", String(params.tiktok_brand_content));
501
+ if (params.tiktok_brand_organic !== undefined)
502
+ formData.append("tiktok_brand_organic", String(params.tiktok_brand_organic));
503
+ if (params.tiktok_aigc !== undefined)
504
+ formData.append("tiktok_aigc", String(params.tiktok_aigc));
505
+ if (params.instagram_title)
506
+ formData.append("instagram_title", params.instagram_title);
507
+ if (params.instagram_media_type)
508
+ formData.append("instagram_media_type", params.instagram_media_type);
509
+ if (params.instagram_collaborators)
510
+ formData.append("instagram_collaborators", params.instagram_collaborators);
511
+ if (params.instagram_cover_url)
512
+ formData.append("instagram_cover_url", params.instagram_cover_url);
513
+ if (params.instagram_share_to_feed !== undefined)
514
+ formData.append("instagram_share_to_feed", String(params.instagram_share_to_feed));
515
+ if (params.instagram_audio_name)
516
+ formData.append("instagram_audio_name", params.instagram_audio_name);
517
+ if (params.instagram_thumb_offset !== undefined)
518
+ formData.append("instagram_thumb_offset", String(params.instagram_thumb_offset));
519
+ if (params.youtube_title)
520
+ formData.append("youtube_title", params.youtube_title);
521
+ if (params.youtube_description)
522
+ formData.append("youtube_description", params.youtube_description);
523
+ if (params.youtube_tags)
524
+ formData.append("youtube_tags", params.youtube_tags);
525
+ if (params.youtube_category)
526
+ formData.append("youtube_category", params.youtube_category);
527
+ if (params.youtube_privacy)
528
+ formData.append("youtube_privacy", params.youtube_privacy);
529
+ if (params.youtube_embeddable !== undefined)
530
+ formData.append("youtube_embeddable", String(params.youtube_embeddable));
531
+ if (params.youtube_license)
532
+ formData.append("youtube_license", params.youtube_license);
533
+ if (params.youtube_kids !== undefined)
534
+ formData.append("youtube_kids", String(params.youtube_kids));
535
+ if (params.youtube_synthetic_media !== undefined)
536
+ formData.append("youtube_synthetic_media", String(params.youtube_synthetic_media));
537
+ if (params.youtube_language)
538
+ formData.append("youtube_language", params.youtube_language);
539
+ if (params.youtube_thumbnail)
540
+ formData.append("youtube_thumbnail", params.youtube_thumbnail);
541
+ if (params.youtube_recording_date)
542
+ formData.append("youtube_recording_date", params.youtube_recording_date);
543
+ if (params.linkedin_title)
544
+ formData.append("linkedin_title", params.linkedin_title);
545
+ if (params.linkedin_description)
546
+ formData.append("linkedin_description", params.linkedin_description);
547
+ if (params.linkedin_page)
548
+ formData.append("linkedin_page", params.linkedin_page);
549
+ if (params.linkedin_visibility)
550
+ formData.append("linkedin_visibility", params.linkedin_visibility);
551
+ if (params.facebook_title)
552
+ formData.append("facebook_title", params.facebook_title);
553
+ if (params.facebook_description)
554
+ formData.append("facebook_description", params.facebook_description);
555
+ if (params.facebook_page)
556
+ formData.append("facebook_page", params.facebook_page);
557
+ if (params.facebook_media_type)
558
+ formData.append("facebook_media_type", params.facebook_media_type);
559
+ if (params.facebook_thumbnail_url)
560
+ formData.append("facebook_thumbnail_url", params.facebook_thumbnail_url);
561
+ if (params.x_title)
562
+ formData.append("x_title", params.x_title);
563
+ if (params.x_reply_settings)
564
+ formData.append("x_reply_settings", params.x_reply_settings);
565
+ if (params.threads_title)
566
+ formData.append("threads_title", params.threads_title);
567
+ if (params.pinterest_title)
568
+ formData.append("pinterest_title", params.pinterest_title);
569
+ if (params.pinterest_description)
570
+ formData.append("pinterest_description", params.pinterest_description);
571
+ if (params.pinterest_board)
572
+ formData.append("pinterest_board", params.pinterest_board);
573
+ if (params.pinterest_link)
574
+ formData.append("pinterest_link", params.pinterest_link);
575
+ if (params.pinterest_alt_text)
576
+ formData.append("pinterest_alt_text", params.pinterest_alt_text);
577
+ if (params.reddit_title)
578
+ formData.append("reddit_title", params.reddit_title);
579
+ if (params.reddit_subreddit)
580
+ formData.append("reddit_subreddit", params.reddit_subreddit);
581
+ if (params.reddit_flair)
582
+ formData.append("reddit_flair", params.reddit_flair);
583
+ if (params.bluesky_title)
584
+ formData.append("bluesky_title", params.bluesky_title);
585
+ return this.uploadRequest("POST", "/upload_videos", formData);
586
+ }
587
+ async postDocument(params) {
588
+ const formData = new FormData;
589
+ if (params.file) {
590
+ const file = Bun.file(params.file);
591
+ formData.append("media_file", file);
592
+ } else if (params.url) {
593
+ formData.append("media_url", params.url);
594
+ }
595
+ formData.append("profile", params.profile);
596
+ formData.append("title", params.title);
597
+ if (params.description)
598
+ formData.append("description", params.description);
599
+ if (params.linkedin_page)
600
+ formData.append("linkedin_page", params.linkedin_page);
601
+ if (params.linkedin_visibility)
602
+ formData.append("linkedin_visibility", params.linkedin_visibility);
603
+ if (params.schedule)
604
+ formData.append("schedule", params.schedule);
605
+ if (params.timezone)
606
+ formData.append("timezone", params.timezone);
607
+ if (params.queue)
608
+ formData.append("queue", String(params.queue));
609
+ if (params.async)
610
+ formData.append("async", String(params.async));
611
+ return this.uploadRequest("POST", "/upload_document", formData);
612
+ }
613
+ async getStatus(id, type = "request_id") {
614
+ const queryParam = type === "job_id" ? `job_id=${id}` : `request_id=${id}`;
615
+ return this.request("GET", `/uploadposts/status?${queryParam}`);
616
+ }
617
+ async listScheduledPosts(profile) {
618
+ const queryParam = profile ? `?profile=${encodeURIComponent(profile)}` : "";
619
+ return this.request("GET", `/uploadposts/schedule${queryParam}`);
620
+ }
621
+ async cancelScheduledPost(jobId) {
622
+ return this.request("DELETE", `/uploadposts/schedule/${jobId}`);
623
+ }
624
+ async modifyScheduledPost(jobId, updates) {
625
+ return this.request("PATCH", `/uploadposts/schedule/${jobId}`, updates);
626
+ }
627
+ async getQueueSettings(profile) {
628
+ return this.request("GET", `/uploadposts/queue/settings?profile=${encodeURIComponent(profile)}`);
629
+ }
630
+ async updateQueueSettings(profile, updates) {
631
+ const body = { profile, ...updates };
632
+ return this.request("POST", "/uploadposts/queue/settings", body);
633
+ }
634
+ async previewQueue(profile, count) {
635
+ const queryParams = new URLSearchParams({ profile });
636
+ if (count !== undefined) {
637
+ queryParams.set("count", count.toString());
638
+ }
639
+ return this.request("GET", `/uploadposts/queue/preview?${queryParams}`);
640
+ }
641
+ async nextSlot(profile) {
642
+ return this.request("GET", `/uploadposts/queue/next-slot?profile=${encodeURIComponent(profile)}`);
643
+ }
644
+ async getHistory(profile, page, limit) {
645
+ const queryParams = new URLSearchParams;
646
+ if (profile)
647
+ queryParams.set("profile", profile);
648
+ if (page !== undefined)
649
+ queryParams.set("page", page.toString());
650
+ if (limit !== undefined)
651
+ queryParams.set("limit", limit.toString());
652
+ const queryString = queryParams.toString();
653
+ const path = queryString ? `/uploadposts/history?${queryString}` : "/uploadposts/history";
654
+ return this.request("GET", path);
655
+ }
656
+ async getAnalytics(profile, platforms, facebookPage, linkedinPage) {
657
+ const queryParams = new URLSearchParams;
658
+ if (platforms && platforms.length > 0)
659
+ queryParams.set("platforms", platforms.join(","));
660
+ if (facebookPage)
661
+ queryParams.set("facebook_page", facebookPage);
662
+ if (linkedinPage)
663
+ queryParams.set("linkedin_page", linkedinPage);
664
+ const queryString = queryParams.toString();
665
+ const path = queryString ? `/analytics/${encodeURIComponent(profile)}?${queryString}` : `/analytics/${encodeURIComponent(profile)}`;
666
+ return this.request("GET", path);
667
+ }
668
+ }
669
+
670
+ // src/lib/output.ts
671
+ var ANSI = {
672
+ BOLD: "\x1B[1m",
673
+ CYAN: "\x1B[36m",
674
+ YELLOW: "\x1B[33m",
675
+ GREEN: "\x1B[32m",
676
+ RED: "\x1B[31m",
677
+ GRAY: "\x1B[90m",
678
+ RESET: "\x1B[0m"
679
+ };
680
+
681
+ class OutputFormatter {
682
+ _mode;
683
+ _colorEnabled;
684
+ constructor(forceMode, colorEnabled = true) {
685
+ if (forceMode) {
686
+ this._mode = forceMode;
687
+ } else if (!process.stdout.isTTY) {
688
+ this._mode = "json";
689
+ } else {
690
+ this._mode = "pretty";
691
+ }
692
+ this._colorEnabled = colorEnabled && !process.env.NO_COLOR;
693
+ }
694
+ mode() {
695
+ return this._mode;
696
+ }
697
+ json(data) {
698
+ console.log(JSON.stringify(data, null, 2));
699
+ }
700
+ pretty(lines) {
701
+ console.log(lines.join(`
702
+ `));
703
+ }
704
+ error(message, details) {
705
+ const prefix = this.color("Error:", "RED");
706
+ console.error(`${prefix} ${message}`);
707
+ if (details) {
708
+ console.error(details);
709
+ }
710
+ }
711
+ usage(count, limit) {
712
+ const remaining = limit - count;
713
+ const line = `Usage: ${count} / ${limit} (${remaining} remaining)`;
714
+ if (this._mode === "json") {
715
+ return;
716
+ }
717
+ console.log(`
718
+ ` + line);
719
+ }
720
+ table(headers, rows) {
721
+ if (this._mode === "json") {
722
+ return;
723
+ }
724
+ const widths = headers.map((h, i) => {
725
+ const maxRowWidth = Math.max(...rows.map((r) => (r[i] || "").length));
726
+ return Math.max(h.length, maxRowWidth);
727
+ });
728
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
729
+ console.log(this.color(headerLine, "CYAN"));
730
+ rows.forEach((row) => {
731
+ const rowLine = row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
732
+ console.log(rowLine);
733
+ });
734
+ }
735
+ color(text, colorName) {
736
+ if (!this._colorEnabled) {
737
+ return text;
738
+ }
739
+ return `${ANSI[colorName]}${text}${ANSI.RESET}`;
740
+ }
741
+ header(text) {
742
+ return this.color(text, "CYAN");
743
+ }
744
+ label(text) {
745
+ return this.color(text, "BOLD");
746
+ }
747
+ success(text) {
748
+ return this.color(text, "GREEN");
749
+ }
750
+ warning(text) {
751
+ return this.color(text, "YELLOW");
752
+ }
753
+ muted(text) {
754
+ return this.color(text, "GRAY");
755
+ }
756
+ }
757
+ function createOutputFormatter(jsonFlag, prettyFlag, colorEnabled = true) {
758
+ let mode;
759
+ if (jsonFlag) {
760
+ mode = "json";
761
+ } else if (prettyFlag) {
762
+ mode = "pretty";
763
+ }
764
+ return new OutputFormatter(mode, colorEnabled);
765
+ }
766
+
767
+ // src/commands/auth/login.ts
768
+ async function authLogin(args, globalFlags) {
769
+ const { values } = parseArgs({
770
+ args,
771
+ options: {
772
+ key: { type: "string" }
773
+ },
774
+ strict: false
775
+ });
776
+ let apiKey = values.key;
777
+ if (!apiKey) {
778
+ if (stdin.isTTY) {
779
+ apiKey = await promptForKey();
780
+ } else {
781
+ throw new UserError(`API key required. Use --key flag or run interactively.
782
+ Usage: posterboy auth login --key <your-api-key>`);
783
+ }
784
+ }
785
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
786
+ const accountInfo = await client.me();
787
+ const existingConfig = readConfig();
788
+ const config = existingConfig || { version: 1 };
789
+ config.api_key = apiKey;
790
+ await writeConfig(config);
791
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
792
+ const configPath = resolveConfigPath(globalFlags.config);
793
+ if (formatter.mode() === "json") {
794
+ formatter.json({
795
+ success: true,
796
+ email: accountInfo.email,
797
+ plan: accountInfo.plan,
798
+ message: `API key saved to ${configPath}`
799
+ });
800
+ } else {
801
+ formatter.pretty([
802
+ "API key validated and saved.",
803
+ ` ${formatter.label("Account:")} ${accountInfo.email}`,
804
+ ` ${formatter.label("Plan:")} ${accountInfo.plan}`,
805
+ ` ${formatter.label("Config:")} ${configPath}`
806
+ ]);
807
+ }
808
+ }
809
+ async function promptForKey() {
810
+ const rl = createInterface({
811
+ input: stdin,
812
+ output: stdout
813
+ });
814
+ return new Promise((resolve) => {
815
+ rl.question("Enter your Upload-Post API key: ", (answer) => {
816
+ rl.close();
817
+ resolve(answer.trim());
818
+ });
819
+ });
820
+ }
821
+
822
+ // src/commands/auth/status.ts
823
+ async function authStatus(args, globalFlags) {
824
+ const config = readConfig();
825
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
826
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
827
+ const accountInfo = await client.me();
828
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
829
+ if (formatter.mode() === "json") {
830
+ formatter.json({
831
+ email: accountInfo.email,
832
+ plan: accountInfo.plan,
833
+ usage: accountInfo.usage
834
+ });
835
+ } else {
836
+ const usage = accountInfo.usage;
837
+ formatter.pretty([
838
+ formatter.header("Account Status"),
839
+ ` ${formatter.label("Email:")} ${accountInfo.email}`,
840
+ ` ${formatter.label("Plan:")} ${accountInfo.plan}`,
841
+ ` ${formatter.label("Usage:")} ${usage.count} / ${usage.limit} uploads this month (${usage.remaining} remaining)`
842
+ ]);
843
+ }
844
+ }
845
+
846
+ // src/commands/profiles/list.ts
847
+ async function profilesList(args, globalFlags) {
848
+ const config = readConfig();
849
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
850
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
851
+ const result = await client.listUsers();
852
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
853
+ if (formatter.mode() === "json") {
854
+ formatter.json({
855
+ profiles: result.users,
856
+ count: result.users.length,
857
+ limit: 2
858
+ });
859
+ } else {
860
+ if (result.users.length === 0) {
861
+ formatter.pretty([
862
+ formatter.header("Profiles"),
863
+ " No profiles found. Create one with: posterboy profiles create --username <name>"
864
+ ]);
865
+ } else {
866
+ const lines = [formatter.header("Profiles")];
867
+ for (const profile of result.users) {
868
+ const platformsDisplay = profile.connected_platforms.length > 0 ? profile.connected_platforms.join(", ") : formatter.muted("none");
869
+ const createdDate = new Date(profile.created_at).toLocaleDateString();
870
+ lines.push(` ${formatter.label(profile.username)}`, ` Platforms: ${platformsDisplay}`, ` Created: ${createdDate}`, "");
871
+ }
872
+ lines.push(formatter.muted(`${result.users.length} / 2 profiles`));
873
+ formatter.pretty(lines);
874
+ }
875
+ }
876
+ }
877
+
878
+ // src/commands/profiles/create.ts
879
+ import { parseArgs as parseArgs2 } from "node:util";
880
+ async function profilesCreate(args, globalFlags) {
881
+ const { values } = parseArgs2({
882
+ args,
883
+ options: {
884
+ username: { type: "string" }
885
+ },
886
+ strict: false
887
+ });
888
+ const username = values.username;
889
+ if (!username) {
890
+ throw new UserError(`Username is required.
891
+ Usage: posterboy profiles create --username <name>`);
892
+ }
893
+ const config = readConfig();
894
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
895
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
896
+ const result = await client.createUser(username);
897
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
898
+ if (formatter.mode() === "json") {
899
+ formatter.json({
900
+ success: result.success,
901
+ profile: result.profile
902
+ });
903
+ } else {
904
+ formatter.pretty([
905
+ formatter.success(`Profile '${username}' created successfully.`),
906
+ "",
907
+ "Next step: Connect social accounts with:",
908
+ ` posterboy profiles connect --username ${username}`
909
+ ]);
910
+ }
911
+ }
912
+
913
+ // src/commands/profiles/delete.ts
914
+ import { parseArgs as parseArgs3 } from "node:util";
915
+ import { stdin as stdin2, stdout as stdout2 } from "node:process";
916
+ import { createInterface as createInterface2 } from "node:readline";
917
+ async function profilesDelete(args, globalFlags) {
918
+ const { values } = parseArgs3({
919
+ args,
920
+ options: {
921
+ username: { type: "string" },
922
+ confirm: { type: "boolean" }
923
+ },
924
+ strict: false
925
+ });
926
+ const username = values.username;
927
+ const confirm = values.confirm;
928
+ if (!username) {
929
+ throw new UserError(`Username is required.
930
+ Usage: posterboy profiles delete --username <name>`);
931
+ }
932
+ const config = readConfig();
933
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
934
+ if (!confirm) {
935
+ if (globalFlags.json) {
936
+ throw new UserError(`Deletion requires confirmation.
937
+ Add --confirm flag to delete without prompting.`);
938
+ }
939
+ if (stdin2.isTTY) {
940
+ const shouldDelete = await promptForConfirmation(username);
941
+ if (!shouldDelete) {
942
+ console.log("Deletion cancelled.");
943
+ process.exit(0);
944
+ }
945
+ } else {
946
+ throw new UserError(`Deletion requires confirmation.
947
+ Add --confirm flag when running non-interactively.`);
948
+ }
949
+ }
950
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
951
+ const result = await client.deleteUser(username);
952
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
953
+ if (formatter.mode() === "json") {
954
+ formatter.json({
955
+ success: result.success
956
+ });
957
+ } else {
958
+ formatter.pretty([
959
+ formatter.success(`Profile '${username}' deleted successfully.`)
960
+ ]);
961
+ }
962
+ }
963
+ async function promptForConfirmation(username) {
964
+ const rl = createInterface2({
965
+ input: stdin2,
966
+ output: stdout2
967
+ });
968
+ return new Promise((resolve) => {
969
+ rl.question(`Delete profile '${username}'? This removes all connected accounts. [y/N] `, (answer) => {
970
+ rl.close();
971
+ const normalized = answer.trim().toLowerCase();
972
+ resolve(normalized === "y" || normalized === "yes");
973
+ });
974
+ });
975
+ }
976
+
977
+ // src/commands/profiles/connect.ts
978
+ import { parseArgs as parseArgs4 } from "node:util";
979
+
980
+ // src/lib/platforms.ts
981
+ var PLATFORM_CAPABILITIES = {
982
+ tiktok: { text: false, photo: true, video: true, document: false },
983
+ instagram: { text: false, photo: true, video: true, document: false },
984
+ youtube: { text: false, photo: false, video: true, document: false },
985
+ linkedin: { text: true, photo: true, video: true, document: true },
986
+ facebook: { text: true, photo: true, video: true, document: false },
987
+ x: { text: true, photo: true, video: true, document: false },
988
+ threads: { text: true, photo: true, video: true, document: false },
989
+ pinterest: { text: false, photo: true, video: true, document: false },
990
+ reddit: { text: true, photo: true, video: true, document: false },
991
+ bluesky: { text: true, photo: true, video: true, document: false }
992
+ };
993
+ function validatePlatforms(platforms) {
994
+ if (platforms.length === 0) {
995
+ throw new UserError(`At least one platform is required.
996
+ ` + `Valid platforms: ${ALL_PLATFORMS.join(", ")}`);
997
+ }
998
+ const invalid = platforms.filter((p) => !ALL_PLATFORMS.includes(p));
999
+ if (invalid.length > 0) {
1000
+ throw new UserError(`Invalid platform names: ${invalid.join(", ")}.
1001
+ ` + `Valid platforms: ${ALL_PLATFORMS.join(", ")}`);
1002
+ }
1003
+ return platforms;
1004
+ }
1005
+ function getPlatformsForContentType(type) {
1006
+ return ALL_PLATFORMS.filter((platform) => PLATFORM_CAPABILITIES[platform][type]);
1007
+ }
1008
+ function validateContentTypeForPlatforms(contentType, platforms) {
1009
+ const unsupported = platforms.filter((p) => !PLATFORM_CAPABILITIES[p][contentType]);
1010
+ if (unsupported.length > 0) {
1011
+ const supported = getPlatformsForContentType(contentType);
1012
+ throw new UserError(`${unsupported.join(", ")} does not support ${contentType} posts.
1013
+ ` + `Supported platforms for ${contentType}: ${supported.join(", ")}`);
1014
+ }
1015
+ }
1016
+ function validatePlatformRequirements(platforms, params) {
1017
+ for (const platform of platforms) {
1018
+ switch (platform) {
1019
+ case "facebook":
1020
+ if (!params.facebook_page) {
1021
+ throw new UserError(`Facebook requires --facebook-page flag.
1022
+ ` + `Tip: Set a default in ~/.posterboy/config.json:
1023
+ ` + ` "platform_defaults": { "facebook": { "page_id": "your-page-id" } }`);
1024
+ }
1025
+ break;
1026
+ case "pinterest":
1027
+ if (!params.pinterest_board) {
1028
+ throw new UserError(`Pinterest requires --pinterest-board flag.
1029
+ ` + `Tip: Set a default in ~/.posterboy/config.json:
1030
+ ` + ` "platform_defaults": { "pinterest": { "board_id": "your-board-id" } }`);
1031
+ }
1032
+ break;
1033
+ case "reddit":
1034
+ if (!params.reddit_subreddit) {
1035
+ throw new UserError(`Reddit requires --reddit-subreddit flag.
1036
+ ` + `Tip: Set a default in ~/.posterboy/config.json:
1037
+ ` + ` "platform_defaults": { "reddit": { "subreddit": "yoursubreddit" } }`);
1038
+ }
1039
+ break;
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ // src/commands/profiles/connect.ts
1045
+ async function profilesConnect(args, globalFlags) {
1046
+ const { values } = parseArgs4({
1047
+ args,
1048
+ options: {
1049
+ username: { type: "string" },
1050
+ platforms: { type: "string" },
1051
+ redirect: { type: "string" }
1052
+ },
1053
+ strict: false
1054
+ });
1055
+ const username = values.username;
1056
+ const platformsStr = values.platforms;
1057
+ const redirect = values.redirect;
1058
+ if (!username) {
1059
+ throw new UserError(`Username is required.
1060
+ Usage: posterboy profiles connect --username <name> [--platforms platform1,platform2] [--redirect <url>]`);
1061
+ }
1062
+ let platforms;
1063
+ if (platformsStr) {
1064
+ platforms = validatePlatforms(platformsStr.split(",").map((p) => p.trim()));
1065
+ }
1066
+ const config = readConfig();
1067
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
1068
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
1069
+ const result = await client.generateJwt(username, { platforms, redirect });
1070
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1071
+ if (formatter.mode() === "json") {
1072
+ formatter.json({
1073
+ success: result.success,
1074
+ access_url: result.access_url,
1075
+ expires_in: result.expires_in
1076
+ });
1077
+ } else {
1078
+ formatter.pretty([
1079
+ formatter.header("Connect Social Accounts"),
1080
+ "",
1081
+ "Visit this URL to connect accounts:",
1082
+ ` ${formatter.success(result.access_url)}`,
1083
+ "",
1084
+ formatter.muted(`Link expires in ${result.expires_in}`)
1085
+ ]);
1086
+ }
1087
+ }
1088
+
1089
+ // src/commands/platforms.ts
1090
+ import { parseArgs as parseArgs5 } from "node:util";
1091
+ async function platforms(args, globalFlags) {
1092
+ const { values } = parseArgs5({
1093
+ args,
1094
+ options: {
1095
+ profile: { type: "string" }
1096
+ },
1097
+ strict: false
1098
+ });
1099
+ const config = readConfig();
1100
+ const profile = values.profile || getDefaultProfile(globalFlags.profile, config);
1101
+ if (!profile) {
1102
+ throw new UserError(`Profile is required.
1103
+ ` + `Use --profile flag or set default_profile in config.
1104
+ ` + "Usage: posterboy platforms --profile <username>");
1105
+ }
1106
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
1107
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
1108
+ const result = await client.getUserProfile(profile);
1109
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1110
+ if (formatter.mode() === "json") {
1111
+ const platformStatuses = ALL_PLATFORMS.map((platform) => ({
1112
+ platform,
1113
+ connected: result.user.connected_platforms.includes(platform)
1114
+ }));
1115
+ formatter.json({
1116
+ profile: result.user.username,
1117
+ platforms: platformStatuses
1118
+ });
1119
+ } else {
1120
+ const lines = [
1121
+ formatter.header(`Platforms for ${result.user.username}`),
1122
+ ""
1123
+ ];
1124
+ for (const platform of ALL_PLATFORMS) {
1125
+ const connected = result.user.connected_platforms.includes(platform);
1126
+ const indicator = connected ? formatter.success("✓") : formatter.muted("-");
1127
+ const platformName = platform.padEnd(10);
1128
+ lines.push(` ${indicator} ${platformName}`);
1129
+ }
1130
+ lines.push("");
1131
+ lines.push(formatter.muted(`${result.user.connected_platforms.length} of ${ALL_PLATFORMS.length} platforms connected`));
1132
+ formatter.pretty(lines);
1133
+ }
1134
+ }
1135
+ async function platformsPages(subcommand, args, globalFlags) {
1136
+ const { values } = parseArgs5({
1137
+ args,
1138
+ options: {
1139
+ profile: { type: "string" }
1140
+ },
1141
+ strict: false
1142
+ });
1143
+ const config = readConfig();
1144
+ const profile = values.profile || getDefaultProfile(globalFlags.profile, config);
1145
+ if (!profile) {
1146
+ throw new UserError(`Profile is required.
1147
+ ` + `Use --profile flag or set default_profile in config.
1148
+ ` + `Usage: posterboy platforms pages ${subcommand} --profile <username>`);
1149
+ }
1150
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
1151
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
1152
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1153
+ switch (subcommand) {
1154
+ case "facebook":
1155
+ {
1156
+ const result = await client.facebookPages(profile);
1157
+ if (formatter.mode() === "json") {
1158
+ formatter.json({
1159
+ profile,
1160
+ platform: "facebook",
1161
+ pages: result.pages
1162
+ });
1163
+ } else {
1164
+ const lines = [
1165
+ formatter.header(`Facebook Pages for ${profile}`),
1166
+ ""
1167
+ ];
1168
+ if (result.pages.length === 0) {
1169
+ lines.push(" No Facebook pages found.");
1170
+ } else {
1171
+ for (const page of result.pages) {
1172
+ lines.push(` ${formatter.label(page.name)}`, ` ID: ${page.id}`, ` Category: ${page.category}`, "");
1173
+ }
1174
+ }
1175
+ formatter.pretty(lines);
1176
+ }
1177
+ }
1178
+ break;
1179
+ case "linkedin":
1180
+ {
1181
+ const result = await client.linkedinPages(profile);
1182
+ if (formatter.mode() === "json") {
1183
+ formatter.json({
1184
+ profile,
1185
+ platform: "linkedin",
1186
+ pages: result.pages
1187
+ });
1188
+ } else {
1189
+ const lines = [
1190
+ formatter.header(`LinkedIn Pages for ${profile}`),
1191
+ ""
1192
+ ];
1193
+ if (result.pages.length === 0) {
1194
+ lines.push(" No LinkedIn pages found.");
1195
+ } else {
1196
+ for (const page of result.pages) {
1197
+ lines.push(` ${formatter.label(page.name)}`, ` ID: ${page.id}`, "");
1198
+ }
1199
+ }
1200
+ formatter.pretty(lines);
1201
+ }
1202
+ }
1203
+ break;
1204
+ case "pinterest":
1205
+ {
1206
+ const result = await client.pinterestBoards(profile);
1207
+ if (formatter.mode() === "json") {
1208
+ formatter.json({
1209
+ profile,
1210
+ platform: "pinterest",
1211
+ boards: result.boards
1212
+ });
1213
+ } else {
1214
+ const lines = [
1215
+ formatter.header(`Pinterest Boards for ${profile}`),
1216
+ ""
1217
+ ];
1218
+ if (result.boards.length === 0) {
1219
+ lines.push(" No Pinterest boards found.");
1220
+ } else {
1221
+ for (const board of result.boards) {
1222
+ lines.push(` ${formatter.label(board.name)}`, ` ID: ${board.id}`, "");
1223
+ }
1224
+ }
1225
+ formatter.pretty(lines);
1226
+ }
1227
+ }
1228
+ break;
1229
+ default:
1230
+ throw new UserError(`Unknown platform: ${subcommand}
1231
+ ` + "Available platforms: facebook, linkedin, pinterest");
1232
+ }
1233
+ }
1234
+
1235
+ // src/commands/post/text.ts
1236
+ import { parseArgs as parseArgs6 } from "node:util";
1237
+
1238
+ // src/lib/validation.ts
1239
+ function validatePlatforms2(platforms2) {
1240
+ try {
1241
+ return validatePlatforms(platforms2);
1242
+ } catch (_error) {
1243
+ if (_error instanceof Error) {
1244
+ throw new UserError(_error.message);
1245
+ }
1246
+ throw _error;
1247
+ }
1248
+ }
1249
+ function validateISODate(date) {
1250
+ try {
1251
+ const parsed = new Date(date);
1252
+ if (isNaN(parsed.getTime())) {
1253
+ throw new UserError(`Invalid ISO-8601 date: ${date}`);
1254
+ }
1255
+ if (parsed.getTime() <= Date.now()) {
1256
+ throw new UserError(`Schedule date must be in the future: ${date}`);
1257
+ }
1258
+ const maxDate = new Date;
1259
+ maxDate.setDate(maxDate.getDate() + 365);
1260
+ if (parsed.getTime() > maxDate.getTime()) {
1261
+ throw new UserError(`Schedule date must be within 365 days from now: ${date}`);
1262
+ }
1263
+ return true;
1264
+ } catch (_error) {
1265
+ if (_error instanceof UserError) {
1266
+ throw _error;
1267
+ }
1268
+ throw new UserError(`Invalid date format: ${date}`);
1269
+ }
1270
+ }
1271
+ function validateTimezone(tz) {
1272
+ try {
1273
+ Intl.DateTimeFormat(undefined, { timeZone: tz });
1274
+ return true;
1275
+ } catch {
1276
+ throw new UserError(`Invalid timezone: ${tz}.
1277
+ ` + `Must be a valid IANA timezone name (e.g., "America/New_York").`);
1278
+ }
1279
+ }
1280
+ function validateMutuallyExclusive(options, errorMessage) {
1281
+ const provided = Object.entries(options).filter(([_, val]) => val);
1282
+ if (provided.length === 0) {
1283
+ throw new UserError(errorMessage || `Exactly one of the following must be provided: ${Object.keys(options).join(", ")}`);
1284
+ }
1285
+ if (provided.length > 1) {
1286
+ throw new UserError(errorMessage || `Only one of the following can be provided: ${provided.map(([k]) => k).join(", ")}`);
1287
+ }
1288
+ }
1289
+
1290
+ // src/commands/post/text.ts
1291
+ async function postText(args, globalFlags) {
1292
+ const { values } = parseArgs6({
1293
+ args,
1294
+ options: {
1295
+ body: { type: "string" },
1296
+ file: { type: "string" },
1297
+ stdin: { type: "boolean", default: false },
1298
+ platforms: { type: "string" },
1299
+ profile: { type: "string" },
1300
+ schedule: { type: "string" },
1301
+ timezone: { type: "string" },
1302
+ queue: { type: "boolean", default: false },
1303
+ async: { type: "boolean", default: false },
1304
+ "first-comment": { type: "string" },
1305
+ "dry-run": { type: "boolean", default: false },
1306
+ "x-title": { type: "string" },
1307
+ "x-reply-to": { type: "string" },
1308
+ "x-reply-settings": { type: "string" },
1309
+ "x-quote-tweet": { type: "string" },
1310
+ "x-long-text-as-post": { type: "boolean" },
1311
+ "x-poll-options": { type: "string", multiple: true },
1312
+ "x-poll-duration": { type: "string" },
1313
+ "linkedin-title": { type: "string" },
1314
+ "linkedin-page": { type: "string" },
1315
+ "linkedin-visibility": { type: "string" },
1316
+ "facebook-title": { type: "string" },
1317
+ "facebook-page": { type: "string" },
1318
+ "facebook-link": { type: "string" },
1319
+ "threads-title": { type: "string" },
1320
+ "threads-long-text-as-post": { type: "boolean" },
1321
+ "reddit-subreddit": { type: "string" },
1322
+ "reddit-flair": { type: "string" },
1323
+ "bluesky-title": { type: "string" },
1324
+ "bluesky-reply-to": { type: "string" }
1325
+ },
1326
+ strict: false
1327
+ });
1328
+ let text;
1329
+ const hasBody = !!values.body;
1330
+ const hasFile = !!values.file;
1331
+ const hasStdin = values.stdin;
1332
+ const inputModes = [hasBody, hasFile, hasStdin].filter(Boolean).length;
1333
+ if (inputModes === 0) {
1334
+ throw new UserError(`Text content required. Provide exactly one of:
1335
+ ` + ` --body <text> Text content inline
1336
+ ` + ` --file <path> Read text from file
1337
+ ` + " --stdin Read text from stdin");
1338
+ }
1339
+ if (inputModes > 1) {
1340
+ throw new UserError(`Only one text input method allowed.
1341
+ ` + "Choose one of: --body, --file, or --stdin");
1342
+ }
1343
+ if (hasBody) {
1344
+ text = values.body;
1345
+ } else if (hasFile) {
1346
+ const filePath = values.file;
1347
+ try {
1348
+ text = await Bun.file(filePath).text();
1349
+ } catch (error) {
1350
+ throw new UserError(`Failed to read file: ${filePath}
1351
+ ` + `Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1352
+ }
1353
+ } else {
1354
+ try {
1355
+ text = await Bun.stdin.text();
1356
+ } catch (error) {
1357
+ throw new UserError(`Failed to read from stdin
1358
+ ` + `Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1359
+ }
1360
+ }
1361
+ if (!text || text.trim().length === 0) {
1362
+ throw new UserError("Text content cannot be empty");
1363
+ }
1364
+ const config = readConfig();
1365
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
1366
+ if (!profile) {
1367
+ throw new UserError(`Profile required. Provide one of:
1368
+ ` + ` --profile <name> (command flag)
1369
+ ` + ` --profile <name> (global flag) (before 'post')
1370
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
1371
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
1372
+ }
1373
+ let platforms2;
1374
+ if (values.platforms) {
1375
+ const platformList = values.platforms.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
1376
+ platforms2 = validatePlatforms2(platformList);
1377
+ } else if (config?.default_platforms && config.default_platforms.length > 0) {
1378
+ platforms2 = config.default_platforms;
1379
+ } else {
1380
+ throw new UserError(`Platforms required. Provide one of:
1381
+ ` + ` --platforms <list> (comma-separated: x,linkedin,threads)
1382
+ ` + ` POSTERBOY_PLATFORMS=<list> (environment variable)
1383
+ ` + ' "default_platforms": [...] (in ~/.posterboy/config.json)');
1384
+ }
1385
+ validateContentTypeForPlatforms("text", platforms2);
1386
+ const params = {
1387
+ profile,
1388
+ platforms: platforms2,
1389
+ text,
1390
+ facebook_page: values["facebook-page"] || config?.platform_defaults?.facebook?.page_id,
1391
+ reddit_subreddit: values["reddit-subreddit"] || config?.platform_defaults?.reddit?.subreddit
1392
+ };
1393
+ validatePlatformRequirements(platforms2, params);
1394
+ if (values.schedule && values.queue) {
1395
+ throw new UserError(`--schedule and --queue are mutually exclusive.
1396
+ ` + "Use --schedule for a specific date/time, or --queue to use the next available slot.");
1397
+ }
1398
+ if (values.schedule) {
1399
+ validateISODate(values.schedule);
1400
+ }
1401
+ if (values.timezone) {
1402
+ validateTimezone(values.timezone);
1403
+ }
1404
+ const postParams = {
1405
+ profile,
1406
+ platforms: platforms2,
1407
+ text
1408
+ };
1409
+ if (values.schedule)
1410
+ postParams.schedule = values.schedule;
1411
+ if (values.timezone)
1412
+ postParams.timezone = values.timezone;
1413
+ if (values.queue)
1414
+ postParams.queue = values.queue;
1415
+ if (values.async)
1416
+ postParams.async = values.async;
1417
+ if (values["first-comment"])
1418
+ postParams.first_comment = values["first-comment"];
1419
+ if (values["x-title"])
1420
+ postParams.x_title = values["x-title"];
1421
+ if (values["x-reply-to"])
1422
+ postParams.x_reply_to = values["x-reply-to"];
1423
+ if (values["x-reply-settings"])
1424
+ postParams.x_reply_settings = values["x-reply-settings"];
1425
+ if (values["x-quote-tweet"])
1426
+ postParams.x_quote_tweet = values["x-quote-tweet"];
1427
+ if (values["x-long-text-as-post"] !== undefined)
1428
+ postParams.x_long_text_as_post = values["x-long-text-as-post"];
1429
+ if (values["x-poll-options"]) {
1430
+ const options = values["x-poll-options"].flatMap((opt) => opt.split(",")).map((o) => o.trim()).filter((o) => o.length > 0);
1431
+ if (options.length < 2 || options.length > 4) {
1432
+ throw new UserError(`X polls require 2-4 options. You provided ${options.length}.`);
1433
+ }
1434
+ postParams.x_poll_options = options;
1435
+ }
1436
+ if (values["x-poll-duration"]) {
1437
+ const duration = parseInt(values["x-poll-duration"], 10);
1438
+ if (isNaN(duration) || duration < 5 || duration > 10080) {
1439
+ throw new UserError("Invalid poll duration. Must be a number between 5 and 10080 minutes (5 minutes to 7 days).");
1440
+ }
1441
+ postParams.x_poll_duration = duration;
1442
+ }
1443
+ if (values["linkedin-title"])
1444
+ postParams.linkedin_title = values["linkedin-title"];
1445
+ if (values["linkedin-page"])
1446
+ postParams.linkedin_page = values["linkedin-page"];
1447
+ if (values["linkedin-visibility"])
1448
+ postParams.linkedin_visibility = values["linkedin-visibility"];
1449
+ if (values["facebook-title"])
1450
+ postParams.facebook_title = values["facebook-title"];
1451
+ if (values["facebook-page"])
1452
+ postParams.facebook_page = values["facebook-page"];
1453
+ if (values["facebook-link"])
1454
+ postParams.facebook_link = values["facebook-link"];
1455
+ if (values["threads-title"])
1456
+ postParams.threads_title = values["threads-title"];
1457
+ if (values["threads-long-text-as-post"] !== undefined)
1458
+ postParams.threads_long_text_as_post = values["threads-long-text-as-post"];
1459
+ if (values["reddit-subreddit"])
1460
+ postParams.reddit_subreddit = values["reddit-subreddit"];
1461
+ if (values["reddit-flair"])
1462
+ postParams.reddit_flair = values["reddit-flair"];
1463
+ if (values["bluesky-title"])
1464
+ postParams.bluesky_title = values["bluesky-title"];
1465
+ if (values["bluesky-reply-to"])
1466
+ postParams.bluesky_reply_to = values["bluesky-reply-to"];
1467
+ if (values["dry-run"]) {
1468
+ const formatter2 = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1469
+ if (formatter2.mode() === "json") {
1470
+ formatter2.json({
1471
+ dry_run: true,
1472
+ payload: postParams
1473
+ });
1474
+ } else {
1475
+ formatter2.pretty([
1476
+ formatter2.header("Dry Run - Request Payload:"),
1477
+ "",
1478
+ JSON.stringify(postParams, null, 2)
1479
+ ]);
1480
+ }
1481
+ return;
1482
+ }
1483
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
1484
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
1485
+ const result = await client.postText(postParams);
1486
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1487
+ if (formatter.mode() === "json") {
1488
+ formatter.json(result);
1489
+ } else {
1490
+ if (result.scheduled) {
1491
+ formatter.pretty([
1492
+ formatter.success("Post scheduled successfully!"),
1493
+ ` ${formatter.label("Job ID:")} ${result.job_id}`,
1494
+ ` ${formatter.label("Scheduled for:")} ${result.scheduled_date}`
1495
+ ]);
1496
+ } else if (result.results) {
1497
+ const successCount = Object.values(result.results).filter((r) => r.success).length;
1498
+ const lines = [
1499
+ formatter.success(`Posted to ${successCount} platform${successCount === 1 ? "" : "s"}:`),
1500
+ ""
1501
+ ];
1502
+ for (const [platform, platformResult] of Object.entries(result.results)) {
1503
+ if (platformResult.success && platformResult.url) {
1504
+ lines.push(` ${platform.padEnd(12)} ${platformResult.url}`);
1505
+ } else if (!platformResult.success && platformResult.error) {
1506
+ lines.push(` ${platform.padEnd(12)} ${formatter.color("FAILED", "RED")} - ${platformResult.error}`);
1507
+ }
1508
+ }
1509
+ if (result.usage) {
1510
+ lines.push("");
1511
+ lines.push(`Usage: ${result.usage.count} / ${result.usage.limit} (${result.usage.remaining} remaining)`);
1512
+ }
1513
+ formatter.pretty(lines);
1514
+ } else {
1515
+ formatter.pretty([
1516
+ formatter.success("Post queued successfully!"),
1517
+ ` ${formatter.label("Request ID:")} ${result.request_id || "N/A"}`
1518
+ ]);
1519
+ }
1520
+ }
1521
+ }
1522
+
1523
+ // src/commands/post/photo.ts
1524
+ import { parseArgs as parseArgs7 } from "node:util";
1525
+ async function validatePhotoFiles(files) {
1526
+ const validExts = [".jpg", ".jpeg", ".png", ".gif"];
1527
+ const maxSize = 8 * 1024 * 1024;
1528
+ for (const filePath of files) {
1529
+ const file = Bun.file(filePath);
1530
+ if (!await file.exists()) {
1531
+ throw new UserError(`File not found: ${filePath}`);
1532
+ }
1533
+ if (file.size > maxSize) {
1534
+ throw new UserError(`File exceeds 8MB limit: ${filePath} (${(file.size / (1024 * 1024)).toFixed(1)}MB)`);
1535
+ }
1536
+ const ext = filePath.toLowerCase().match(/\.([^./\\]+)$/)?.[1];
1537
+ if (!ext || !validExts.includes(`.${ext}`)) {
1538
+ throw new UserError(`Unsupported format: ${filePath}. Supported: JPEG, PNG, GIF`);
1539
+ }
1540
+ }
1541
+ }
1542
+ async function postPhoto(args, globalFlags) {
1543
+ const { values } = parseArgs7({
1544
+ args,
1545
+ options: {
1546
+ files: { type: "string" },
1547
+ urls: { type: "string" },
1548
+ title: { type: "string" },
1549
+ description: { type: "string" },
1550
+ platforms: { type: "string" },
1551
+ profile: { type: "string" },
1552
+ schedule: { type: "string" },
1553
+ timezone: { type: "string" },
1554
+ queue: { type: "boolean", default: false },
1555
+ async: { type: "boolean", default: false },
1556
+ "first-comment": { type: "string" },
1557
+ "dry-run": { type: "boolean", default: false },
1558
+ "instagram-title": { type: "string" },
1559
+ "instagram-media-type": { type: "string" },
1560
+ "instagram-collaborators": { type: "string" },
1561
+ "instagram-location": { type: "string" },
1562
+ "instagram-user-tags": { type: "string" },
1563
+ "facebook-page": { type: "string" },
1564
+ "facebook-media-type": { type: "string" },
1565
+ "tiktok-title": { type: "string" },
1566
+ "tiktok-privacy": { type: "string" },
1567
+ "tiktok-disable-comments": { type: "boolean" },
1568
+ "tiktok-auto-music": { type: "boolean" },
1569
+ "tiktok-cover-index": { type: "string" },
1570
+ "x-title": { type: "string" },
1571
+ "x-thread-image-layout": { type: "string" },
1572
+ "linkedin-title": { type: "string" },
1573
+ "linkedin-page": { type: "string" },
1574
+ "linkedin-visibility": { type: "string" },
1575
+ "threads-title": { type: "string" },
1576
+ "pinterest-board": { type: "string" },
1577
+ "pinterest-link": { type: "string" },
1578
+ "pinterest-alt-text": { type: "string" },
1579
+ "reddit-subreddit": { type: "string" },
1580
+ "reddit-flair": { type: "string" },
1581
+ "bluesky-title": { type: "string" }
1582
+ },
1583
+ strict: false
1584
+ });
1585
+ validateMutuallyExclusive({
1586
+ "--files": !!values.files,
1587
+ "--urls": !!values.urls
1588
+ }, "Must provide either --files or --urls (not both)");
1589
+ if (!values.title) {
1590
+ throw new UserError("--title is required for photo posts");
1591
+ }
1592
+ let files;
1593
+ let urls;
1594
+ if (values.files) {
1595
+ files = values.files.split(",").map((f) => f.trim()).filter((f) => f.length > 0);
1596
+ await validatePhotoFiles(files);
1597
+ } else if (values.urls) {
1598
+ urls = values.urls.split(",").map((u) => u.trim()).filter((u) => u.length > 0);
1599
+ }
1600
+ const config = readConfig();
1601
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
1602
+ if (!profile) {
1603
+ throw new UserError(`Profile required. Provide one of:
1604
+ ` + ` --profile <name> (command flag)
1605
+ ` + ` --profile <name> (global flag) (before 'post')
1606
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
1607
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
1608
+ }
1609
+ let platforms2;
1610
+ if (values.platforms) {
1611
+ const platformList = values.platforms.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
1612
+ platforms2 = validatePlatforms2(platformList);
1613
+ } else if (config?.default_platforms && config.default_platforms.length > 0) {
1614
+ platforms2 = config.default_platforms;
1615
+ } else {
1616
+ throw new UserError(`Platforms required. Provide one of:
1617
+ ` + ` --platforms <list> (comma-separated: instagram,tiktok,facebook)
1618
+ ` + ` POSTERBOY_PLATFORMS=<list> (environment variable)
1619
+ ` + ' "default_platforms": [...] (in ~/.posterboy/config.json)');
1620
+ }
1621
+ validateContentTypeForPlatforms("photo", platforms2);
1622
+ const params = {
1623
+ profile,
1624
+ platforms: platforms2,
1625
+ facebook_page: values["facebook-page"] || config?.platform_defaults?.facebook?.page_id,
1626
+ pinterest_board: values["pinterest-board"] || config?.platform_defaults?.pinterest?.board_id,
1627
+ reddit_subreddit: values["reddit-subreddit"] || config?.platform_defaults?.reddit?.subreddit
1628
+ };
1629
+ validatePlatformRequirements(platforms2, params);
1630
+ if (values.schedule && values.queue) {
1631
+ throw new UserError(`--schedule and --queue are mutually exclusive.
1632
+ ` + "Use --schedule for a specific date/time, or --queue to use the next available slot.");
1633
+ }
1634
+ if (values.schedule) {
1635
+ validateISODate(values.schedule);
1636
+ }
1637
+ if (values.timezone) {
1638
+ validateTimezone(values.timezone);
1639
+ }
1640
+ const postParams = {
1641
+ profile,
1642
+ platforms: platforms2,
1643
+ files,
1644
+ urls,
1645
+ title: values.title
1646
+ };
1647
+ if (values.description)
1648
+ postParams.description = values.description;
1649
+ if (values.schedule)
1650
+ postParams.schedule = values.schedule;
1651
+ if (values.timezone)
1652
+ postParams.timezone = values.timezone;
1653
+ if (values.queue)
1654
+ postParams.queue = values.queue;
1655
+ if (values.async)
1656
+ postParams.async = values.async;
1657
+ if (values["first-comment"])
1658
+ postParams.first_comment = values["first-comment"];
1659
+ if (values["instagram-title"])
1660
+ postParams.instagram_title = values["instagram-title"];
1661
+ if (values["instagram-media-type"])
1662
+ postParams.instagram_media_type = values["instagram-media-type"];
1663
+ if (values["instagram-collaborators"])
1664
+ postParams.instagram_collaborators = values["instagram-collaborators"];
1665
+ if (values["instagram-location"])
1666
+ postParams.instagram_location = values["instagram-location"];
1667
+ if (values["instagram-user-tags"])
1668
+ postParams.instagram_user_tags = values["instagram-user-tags"];
1669
+ if (values["facebook-page"])
1670
+ postParams.facebook_page = values["facebook-page"];
1671
+ if (values["facebook-media-type"])
1672
+ postParams.facebook_media_type = values["facebook-media-type"];
1673
+ if (values["tiktok-title"])
1674
+ postParams.tiktok_title = values["tiktok-title"];
1675
+ if (values["tiktok-privacy"])
1676
+ postParams.tiktok_privacy = values["tiktok-privacy"];
1677
+ if (values["tiktok-disable-comments"] !== undefined)
1678
+ postParams.tiktok_disable_comments = values["tiktok-disable-comments"];
1679
+ if (values["tiktok-auto-music"] !== undefined)
1680
+ postParams.tiktok_auto_music = values["tiktok-auto-music"];
1681
+ if (values["tiktok-cover-index"]) {
1682
+ const coverIndex = parseInt(values["tiktok-cover-index"], 10);
1683
+ if (!isNaN(coverIndex)) {
1684
+ postParams.tiktok_cover_index = coverIndex;
1685
+ }
1686
+ }
1687
+ if (values["x-title"])
1688
+ postParams.x_title = values["x-title"];
1689
+ if (values["x-thread-image-layout"])
1690
+ postParams.x_thread_image_layout = values["x-thread-image-layout"];
1691
+ if (values["linkedin-title"])
1692
+ postParams.linkedin_title = values["linkedin-title"];
1693
+ if (values["linkedin-page"])
1694
+ postParams.linkedin_page = values["linkedin-page"];
1695
+ if (values["linkedin-visibility"])
1696
+ postParams.linkedin_visibility = values["linkedin-visibility"];
1697
+ if (values["threads-title"])
1698
+ postParams.threads_title = values["threads-title"];
1699
+ if (values["pinterest-board"])
1700
+ postParams.pinterest_board = values["pinterest-board"];
1701
+ if (values["pinterest-link"])
1702
+ postParams.pinterest_link = values["pinterest-link"];
1703
+ if (values["pinterest-alt-text"])
1704
+ postParams.pinterest_alt_text = values["pinterest-alt-text"];
1705
+ if (values["reddit-subreddit"])
1706
+ postParams.reddit_subreddit = values["reddit-subreddit"];
1707
+ if (values["reddit-flair"])
1708
+ postParams.reddit_flair = values["reddit-flair"];
1709
+ if (values["bluesky-title"])
1710
+ postParams.bluesky_title = values["bluesky-title"];
1711
+ if (values["dry-run"]) {
1712
+ const formatter2 = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1713
+ if (formatter2.mode() === "json") {
1714
+ formatter2.json({
1715
+ dry_run: true,
1716
+ payload: postParams
1717
+ });
1718
+ } else {
1719
+ formatter2.pretty([
1720
+ formatter2.header("Dry Run - Request Payload:"),
1721
+ "",
1722
+ JSON.stringify(postParams, null, 2)
1723
+ ]);
1724
+ }
1725
+ return;
1726
+ }
1727
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
1728
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
1729
+ const result = await client.postPhotos(postParams);
1730
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
1731
+ if (formatter.mode() === "json") {
1732
+ formatter.json(result);
1733
+ } else {
1734
+ if (result.scheduled) {
1735
+ formatter.pretty([
1736
+ formatter.success("Photo post scheduled successfully!"),
1737
+ ` ${formatter.label("Job ID:")} ${result.job_id}`,
1738
+ ` ${formatter.label("Scheduled for:")} ${result.scheduled_date}`
1739
+ ]);
1740
+ } else if (result.results) {
1741
+ const successCount = Object.values(result.results).filter((r) => r.success).length;
1742
+ const lines = [
1743
+ formatter.success(`Posted photos to ${successCount} platform${successCount === 1 ? "" : "s"}:`),
1744
+ ""
1745
+ ];
1746
+ for (const [platform, platformResult] of Object.entries(result.results)) {
1747
+ if (platformResult.success && platformResult.url) {
1748
+ lines.push(` ${platform.padEnd(12)} ${platformResult.url}`);
1749
+ } else if (!platformResult.success && platformResult.error) {
1750
+ lines.push(` ${platform.padEnd(12)} ${formatter.color("FAILED", "RED")} - ${platformResult.error}`);
1751
+ }
1752
+ }
1753
+ if (result.usage) {
1754
+ lines.push("");
1755
+ lines.push(`Usage: ${result.usage.count} / ${result.usage.limit} (${result.usage.remaining} remaining)`);
1756
+ }
1757
+ formatter.pretty(lines);
1758
+ } else {
1759
+ formatter.pretty([
1760
+ formatter.success("Photo post queued successfully!"),
1761
+ ` ${formatter.label("Request ID:")} ${result.request_id || "N/A"}`
1762
+ ]);
1763
+ }
1764
+ }
1765
+ }
1766
+
1767
+ // src/commands/post/video.ts
1768
+ import { parseArgs as parseArgs8 } from "node:util";
1769
+ async function validateVideoFile(filePath) {
1770
+ const validExts = [".mp4", ".mov", ".webm", ".avi"];
1771
+ const file = Bun.file(filePath);
1772
+ if (!await file.exists()) {
1773
+ throw new UserError(`File not found: ${filePath}`);
1774
+ }
1775
+ const ext = filePath.toLowerCase().match(/\.([^./\\]+)$/)?.[1];
1776
+ if (!ext || !validExts.includes(`.${ext}`)) {
1777
+ throw new UserError(`Unsupported video format: ${filePath}. Supported: MP4, MOV, WebM, AVI`);
1778
+ }
1779
+ return { size: file.size };
1780
+ }
1781
+ async function postVideo(args, globalFlags) {
1782
+ const { values } = parseArgs8({
1783
+ args,
1784
+ options: {
1785
+ file: { type: "string" },
1786
+ url: { type: "string" },
1787
+ title: { type: "string" },
1788
+ description: { type: "string" },
1789
+ platforms: { type: "string" },
1790
+ profile: { type: "string" },
1791
+ schedule: { type: "string" },
1792
+ timezone: { type: "string" },
1793
+ queue: { type: "boolean", default: false },
1794
+ async: { type: "boolean" },
1795
+ "first-comment": { type: "string" },
1796
+ "dry-run": { type: "boolean", default: false },
1797
+ "tiktok-title": { type: "string" },
1798
+ "tiktok-privacy": { type: "string" },
1799
+ "tiktok-disable-duet": { type: "boolean" },
1800
+ "tiktok-disable-comment": { type: "boolean" },
1801
+ "tiktok-disable-stitch": { type: "boolean" },
1802
+ "tiktok-post-mode": { type: "string" },
1803
+ "tiktok-cover-timestamp": { type: "string" },
1804
+ "tiktok-brand-content": { type: "boolean" },
1805
+ "tiktok-brand-organic": { type: "boolean" },
1806
+ "tiktok-aigc": { type: "boolean" },
1807
+ "instagram-title": { type: "string" },
1808
+ "instagram-media-type": { type: "string" },
1809
+ "instagram-collaborators": { type: "string" },
1810
+ "instagram-cover-url": { type: "string" },
1811
+ "instagram-share-to-feed": { type: "boolean" },
1812
+ "instagram-audio-name": { type: "string" },
1813
+ "instagram-thumb-offset": { type: "string" },
1814
+ "youtube-title": { type: "string" },
1815
+ "youtube-description": { type: "string" },
1816
+ "youtube-tags": { type: "string" },
1817
+ "youtube-category": { type: "string" },
1818
+ "youtube-privacy": { type: "string" },
1819
+ "youtube-embeddable": { type: "boolean" },
1820
+ "youtube-license": { type: "string" },
1821
+ "youtube-kids": { type: "boolean" },
1822
+ "youtube-synthetic-media": { type: "boolean" },
1823
+ "youtube-language": { type: "string" },
1824
+ "youtube-thumbnail": { type: "string" },
1825
+ "youtube-recording-date": { type: "string" },
1826
+ "linkedin-title": { type: "string" },
1827
+ "linkedin-description": { type: "string" },
1828
+ "linkedin-page": { type: "string" },
1829
+ "linkedin-visibility": { type: "string" },
1830
+ "facebook-title": { type: "string" },
1831
+ "facebook-description": { type: "string" },
1832
+ "facebook-page": { type: "string" },
1833
+ "facebook-media-type": { type: "string" },
1834
+ "facebook-thumbnail-url": { type: "string" },
1835
+ "x-title": { type: "string" },
1836
+ "x-reply-settings": { type: "string" },
1837
+ "threads-title": { type: "string" },
1838
+ "pinterest-title": { type: "string" },
1839
+ "pinterest-description": { type: "string" },
1840
+ "pinterest-board": { type: "string" },
1841
+ "pinterest-link": { type: "string" },
1842
+ "pinterest-alt-text": { type: "string" },
1843
+ "reddit-title": { type: "string" },
1844
+ "reddit-subreddit": { type: "string" },
1845
+ "reddit-flair": { type: "string" },
1846
+ "bluesky-title": { type: "string" }
1847
+ },
1848
+ strict: false
1849
+ });
1850
+ validateMutuallyExclusive({
1851
+ "--file": !!values.file,
1852
+ "--url": !!values.url
1853
+ }, `Video source required. Provide exactly one of:
1854
+ ` + ` --file <path> Local video file
1855
+ ` + " --url <url> Public video URL");
1856
+ if (!values.title) {
1857
+ throw new UserError("Title required. Use --title <text>");
1858
+ }
1859
+ const config = readConfig();
1860
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
1861
+ if (!profile) {
1862
+ throw new UserError(`Profile required. Provide one of:
1863
+ ` + ` --profile <name> (command flag)
1864
+ ` + ` --profile <name> (global flag) (before 'post')
1865
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
1866
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
1867
+ }
1868
+ let platforms2;
1869
+ if (values.platforms) {
1870
+ const platformList = values.platforms.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
1871
+ platforms2 = validatePlatforms2(platformList);
1872
+ } else if (config?.default_platforms && config.default_platforms.length > 0) {
1873
+ platforms2 = config.default_platforms;
1874
+ } else {
1875
+ throw new UserError(`Platforms required. Provide one of:
1876
+ ` + ` --platforms <list> (comma-separated: tiktok,youtube,instagram)
1877
+ ` + ` POSTERBOY_PLATFORMS=<list> (environment variable)
1878
+ ` + ' "default_platforms": [...] (in ~/.posterboy/config.json)');
1879
+ }
1880
+ validateContentTypeForPlatforms("video", platforms2);
1881
+ let fileSize = 0;
1882
+ if (values.file) {
1883
+ const validation = await validateVideoFile(values.file);
1884
+ fileSize = validation.size;
1885
+ }
1886
+ let shouldUseAsync = values.async;
1887
+ if (fileSize > 50 * 1024 * 1024 && shouldUseAsync === undefined) {
1888
+ shouldUseAsync = true;
1889
+ }
1890
+ const params = {
1891
+ profile,
1892
+ platforms: platforms2,
1893
+ facebook_page: values["facebook-page"] || config?.platform_defaults?.facebook?.page_id,
1894
+ pinterest_board: values["pinterest-board"] || config?.platform_defaults?.pinterest?.board_id,
1895
+ reddit_subreddit: values["reddit-subreddit"] || config?.platform_defaults?.reddit?.subreddit
1896
+ };
1897
+ validatePlatformRequirements(platforms2, params);
1898
+ if (values.schedule && values.queue) {
1899
+ throw new UserError(`--schedule and --queue are mutually exclusive.
1900
+ ` + "Use --schedule for a specific date/time, or --queue to use the next available slot.");
1901
+ }
1902
+ if (values.schedule) {
1903
+ validateISODate(values.schedule);
1904
+ }
1905
+ if (values.timezone) {
1906
+ validateTimezone(values.timezone);
1907
+ }
1908
+ const postParams = {
1909
+ profile,
1910
+ platforms: platforms2,
1911
+ title: values.title
1912
+ };
1913
+ if (values.file)
1914
+ postParams.file = values.file;
1915
+ if (values.url)
1916
+ postParams.url = values.url;
1917
+ if (values.description)
1918
+ postParams.description = values.description;
1919
+ if (values.schedule)
1920
+ postParams.schedule = values.schedule;
1921
+ if (values.timezone)
1922
+ postParams.timezone = values.timezone;
1923
+ if (values.queue)
1924
+ postParams.queue = values.queue;
1925
+ if (shouldUseAsync !== undefined)
1926
+ postParams.async = shouldUseAsync;
1927
+ if (values["first-comment"])
1928
+ postParams.first_comment = values["first-comment"];
1929
+ if (values["tiktok-title"])
1930
+ postParams.tiktok_title = values["tiktok-title"];
1931
+ if (values["tiktok-privacy"])
1932
+ postParams.tiktok_privacy = values["tiktok-privacy"] || config?.platform_defaults?.tiktok?.privacy;
1933
+ if (values["tiktok-disable-duet"] !== undefined)
1934
+ postParams.tiktok_disable_duet = values["tiktok-disable-duet"];
1935
+ if (values["tiktok-disable-comment"] !== undefined)
1936
+ postParams.tiktok_disable_comment = values["tiktok-disable-comment"];
1937
+ if (values["tiktok-disable-stitch"] !== undefined)
1938
+ postParams.tiktok_disable_stitch = values["tiktok-disable-stitch"];
1939
+ if (values["tiktok-post-mode"])
1940
+ postParams.tiktok_post_mode = values["tiktok-post-mode"];
1941
+ if (values["tiktok-cover-timestamp"]) {
1942
+ const timestamp = parseFloat(values["tiktok-cover-timestamp"]);
1943
+ if (!isNaN(timestamp)) {
1944
+ postParams.tiktok_cover_timestamp = timestamp;
1945
+ }
1946
+ }
1947
+ if (values["tiktok-brand-content"] !== undefined)
1948
+ postParams.tiktok_brand_content = values["tiktok-brand-content"];
1949
+ if (values["tiktok-brand-organic"] !== undefined)
1950
+ postParams.tiktok_brand_organic = values["tiktok-brand-organic"];
1951
+ if (values["tiktok-aigc"] !== undefined)
1952
+ postParams.tiktok_aigc = values["tiktok-aigc"];
1953
+ if (values["instagram-title"])
1954
+ postParams.instagram_title = values["instagram-title"];
1955
+ if (values["instagram-media-type"])
1956
+ postParams.instagram_media_type = values["instagram-media-type"];
1957
+ if (values["instagram-collaborators"])
1958
+ postParams.instagram_collaborators = values["instagram-collaborators"];
1959
+ if (values["instagram-cover-url"])
1960
+ postParams.instagram_cover_url = values["instagram-cover-url"];
1961
+ if (values["instagram-share-to-feed"] !== undefined)
1962
+ postParams.instagram_share_to_feed = values["instagram-share-to-feed"];
1963
+ if (values["instagram-audio-name"])
1964
+ postParams.instagram_audio_name = values["instagram-audio-name"];
1965
+ if (values["instagram-thumb-offset"]) {
1966
+ const offset = parseFloat(values["instagram-thumb-offset"]);
1967
+ if (!isNaN(offset)) {
1968
+ postParams.instagram_thumb_offset = offset;
1969
+ }
1970
+ }
1971
+ if (values["youtube-title"])
1972
+ postParams.youtube_title = values["youtube-title"];
1973
+ if (values["youtube-description"])
1974
+ postParams.youtube_description = values["youtube-description"];
1975
+ if (values["youtube-tags"])
1976
+ postParams.youtube_tags = values["youtube-tags"];
1977
+ if (values["youtube-category"])
1978
+ postParams.youtube_category = values["youtube-category"] || config?.platform_defaults?.youtube?.category;
1979
+ if (values["youtube-privacy"])
1980
+ postParams.youtube_privacy = values["youtube-privacy"] || config?.platform_defaults?.youtube?.privacy;
1981
+ if (values["youtube-embeddable"] !== undefined)
1982
+ postParams.youtube_embeddable = values["youtube-embeddable"];
1983
+ if (values["youtube-license"])
1984
+ postParams.youtube_license = values["youtube-license"];
1985
+ if (values["youtube-kids"] !== undefined)
1986
+ postParams.youtube_kids = values["youtube-kids"];
1987
+ if (values["youtube-synthetic-media"] !== undefined)
1988
+ postParams.youtube_synthetic_media = values["youtube-synthetic-media"];
1989
+ if (values["youtube-language"])
1990
+ postParams.youtube_language = values["youtube-language"];
1991
+ if (values["youtube-thumbnail"])
1992
+ postParams.youtube_thumbnail = values["youtube-thumbnail"];
1993
+ if (values["youtube-recording-date"])
1994
+ postParams.youtube_recording_date = values["youtube-recording-date"];
1995
+ if (values["linkedin-title"])
1996
+ postParams.linkedin_title = values["linkedin-title"];
1997
+ if (values["linkedin-description"])
1998
+ postParams.linkedin_description = values["linkedin-description"];
1999
+ if (values["linkedin-page"])
2000
+ postParams.linkedin_page = values["linkedin-page"];
2001
+ if (values["linkedin-visibility"])
2002
+ postParams.linkedin_visibility = values["linkedin-visibility"];
2003
+ if (values["facebook-title"])
2004
+ postParams.facebook_title = values["facebook-title"];
2005
+ if (values["facebook-description"])
2006
+ postParams.facebook_description = values["facebook-description"];
2007
+ if (values["facebook-page"])
2008
+ postParams.facebook_page = values["facebook-page"];
2009
+ if (values["facebook-media-type"])
2010
+ postParams.facebook_media_type = values["facebook-media-type"];
2011
+ if (values["facebook-thumbnail-url"])
2012
+ postParams.facebook_thumbnail_url = values["facebook-thumbnail-url"];
2013
+ if (values["x-title"])
2014
+ postParams.x_title = values["x-title"];
2015
+ if (values["x-reply-settings"])
2016
+ postParams.x_reply_settings = values["x-reply-settings"];
2017
+ if (values["threads-title"])
2018
+ postParams.threads_title = values["threads-title"];
2019
+ if (values["pinterest-title"])
2020
+ postParams.pinterest_title = values["pinterest-title"];
2021
+ if (values["pinterest-description"])
2022
+ postParams.pinterest_description = values["pinterest-description"];
2023
+ if (values["pinterest-board"])
2024
+ postParams.pinterest_board = values["pinterest-board"];
2025
+ if (values["pinterest-link"])
2026
+ postParams.pinterest_link = values["pinterest-link"];
2027
+ if (values["pinterest-alt-text"])
2028
+ postParams.pinterest_alt_text = values["pinterest-alt-text"];
2029
+ if (values["reddit-title"])
2030
+ postParams.reddit_title = values["reddit-title"];
2031
+ if (values["reddit-subreddit"])
2032
+ postParams.reddit_subreddit = values["reddit-subreddit"];
2033
+ if (values["reddit-flair"])
2034
+ postParams.reddit_flair = values["reddit-flair"];
2035
+ if (values["bluesky-title"])
2036
+ postParams.bluesky_title = values["bluesky-title"];
2037
+ if (values["dry-run"]) {
2038
+ const formatter2 = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2039
+ if (formatter2.mode() === "json") {
2040
+ formatter2.json({
2041
+ dry_run: true,
2042
+ payload: postParams
2043
+ });
2044
+ } else {
2045
+ formatter2.pretty([
2046
+ formatter2.header("Dry Run - Request Payload:"),
2047
+ "",
2048
+ JSON.stringify(postParams, null, 2)
2049
+ ]);
2050
+ }
2051
+ return;
2052
+ }
2053
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2054
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2055
+ const result = await client.postVideo(postParams);
2056
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2057
+ if (formatter.mode() === "json") {
2058
+ formatter.json(result);
2059
+ } else {
2060
+ if (result.scheduled) {
2061
+ formatter.pretty([
2062
+ formatter.success("Video scheduled successfully!"),
2063
+ ` ${formatter.label("Job ID:")} ${result.job_id}`,
2064
+ ` ${formatter.label("Scheduled for:")} ${result.scheduled_date}`
2065
+ ]);
2066
+ } else if (result.results) {
2067
+ const successCount = Object.values(result.results).filter((r) => r.success).length;
2068
+ const lines = [
2069
+ formatter.success(`Posted to ${successCount} platform${successCount === 1 ? "" : "s"}:`),
2070
+ ""
2071
+ ];
2072
+ for (const [platform, platformResult] of Object.entries(result.results)) {
2073
+ if (platformResult.success && platformResult.url) {
2074
+ lines.push(` ${platform.padEnd(12)} ${platformResult.url}`);
2075
+ } else if (!platformResult.success && platformResult.error) {
2076
+ lines.push(` ${platform.padEnd(12)} ${formatter.color("FAILED", "RED")} - ${platformResult.error}`);
2077
+ }
2078
+ }
2079
+ if (result.usage) {
2080
+ lines.push("");
2081
+ lines.push(`Usage: ${result.usage.count} / ${result.usage.limit} (${result.usage.remaining} remaining)`);
2082
+ }
2083
+ formatter.pretty(lines);
2084
+ } else {
2085
+ formatter.pretty([
2086
+ formatter.success("Video upload queued successfully!"),
2087
+ ` ${formatter.label("Request ID:")} ${result.request_id || "N/A"}`,
2088
+ "",
2089
+ "Use 'posterboy status <request_id>' to check upload status."
2090
+ ]);
2091
+ }
2092
+ }
2093
+ }
2094
+
2095
+ // src/commands/post/document.ts
2096
+ import { parseArgs as parseArgs9 } from "node:util";
2097
+ async function validateDocumentFile(filePath) {
2098
+ const validExts = [".pdf", ".ppt", ".pptx", ".doc", ".docx"];
2099
+ const maxSize = 100 * 1024 * 1024;
2100
+ const file = Bun.file(filePath);
2101
+ if (!await file.exists()) {
2102
+ throw new UserError(`File not found: ${filePath}`);
2103
+ }
2104
+ if (file.size > maxSize) {
2105
+ throw new UserError(`File exceeds 100MB limit: ${filePath} (${(file.size / (1024 * 1024)).toFixed(1)}MB)`);
2106
+ }
2107
+ const ext = filePath.toLowerCase().match(/\.([^./\\]+)$/)?.[1];
2108
+ if (!ext || !validExts.includes(`.${ext}`)) {
2109
+ throw new UserError(`Unsupported format: ${filePath}. Supported: PDF, PPT, PPTX, DOC, DOCX`);
2110
+ }
2111
+ }
2112
+ async function postDocument(args, globalFlags) {
2113
+ const { values } = parseArgs9({
2114
+ args,
2115
+ options: {
2116
+ file: { type: "string" },
2117
+ url: { type: "string" },
2118
+ title: { type: "string" },
2119
+ description: { type: "string" },
2120
+ platforms: { type: "string" },
2121
+ profile: { type: "string" },
2122
+ "linkedin-page": { type: "string" },
2123
+ "linkedin-visibility": { type: "string" },
2124
+ schedule: { type: "string" },
2125
+ timezone: { type: "string" },
2126
+ queue: { type: "boolean", default: false },
2127
+ async: { type: "boolean", default: false },
2128
+ "dry-run": { type: "boolean", default: false }
2129
+ },
2130
+ strict: false
2131
+ });
2132
+ validateMutuallyExclusive({
2133
+ "--file": !!values.file,
2134
+ "--url": !!values.url
2135
+ }, "Must provide either --file or --url (not both)");
2136
+ if (!values.title) {
2137
+ throw new UserError("--title is required for document posts");
2138
+ }
2139
+ if (values.file) {
2140
+ await validateDocumentFile(values.file);
2141
+ }
2142
+ const config = readConfig();
2143
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2144
+ if (!profile) {
2145
+ throw new UserError(`Profile required. Provide one of:
2146
+ ` + ` --profile <name> (command flag)
2147
+ ` + ` --profile <name> (global flag) (before 'post')
2148
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
2149
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
2150
+ }
2151
+ if (values.platforms) {
2152
+ const platformList = values.platforms.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
2153
+ const nonLinkedin = platformList.filter((p) => p !== "linkedin");
2154
+ if (nonLinkedin.length > 0) {
2155
+ throw new UserError(`Document posts are only supported on LinkedIn. Unsupported: ${nonLinkedin.join(", ")}`);
2156
+ }
2157
+ }
2158
+ if (values.schedule && values.queue) {
2159
+ throw new UserError(`--schedule and --queue are mutually exclusive.
2160
+ ` + "Use --schedule for a specific date/time, or --queue to use the next available slot.");
2161
+ }
2162
+ if (values.schedule) {
2163
+ validateISODate(values.schedule);
2164
+ }
2165
+ if (values.timezone) {
2166
+ validateTimezone(values.timezone);
2167
+ }
2168
+ const postParams = {
2169
+ profile,
2170
+ file: values.file,
2171
+ url: values.url,
2172
+ title: values.title
2173
+ };
2174
+ if (values.description)
2175
+ postParams.description = values.description;
2176
+ if (values["linkedin-page"] || config?.platform_defaults?.linkedin?.page_id) {
2177
+ postParams.linkedin_page = values["linkedin-page"] || config?.platform_defaults?.linkedin?.page_id;
2178
+ }
2179
+ if (values["linkedin-visibility"] || config?.platform_defaults?.linkedin?.visibility) {
2180
+ postParams.linkedin_visibility = values["linkedin-visibility"] || config?.platform_defaults?.linkedin?.visibility;
2181
+ }
2182
+ if (values.schedule)
2183
+ postParams.schedule = values.schedule;
2184
+ if (values.timezone)
2185
+ postParams.timezone = values.timezone;
2186
+ if (values.queue)
2187
+ postParams.queue = values.queue;
2188
+ if (values.async)
2189
+ postParams.async = values.async;
2190
+ if (values["dry-run"]) {
2191
+ const formatter2 = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2192
+ if (formatter2.mode() === "json") {
2193
+ formatter2.json({
2194
+ dry_run: true,
2195
+ payload: postParams
2196
+ });
2197
+ } else {
2198
+ formatter2.pretty([
2199
+ formatter2.header("Dry Run - Request Payload:"),
2200
+ "",
2201
+ JSON.stringify(postParams, null, 2)
2202
+ ]);
2203
+ }
2204
+ return;
2205
+ }
2206
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2207
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2208
+ const result = await client.postDocument(postParams);
2209
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2210
+ if (formatter.mode() === "json") {
2211
+ formatter.json(result);
2212
+ } else {
2213
+ if (result.scheduled) {
2214
+ formatter.pretty([
2215
+ formatter.success("Document post scheduled successfully on LinkedIn!"),
2216
+ ` ${formatter.label("Job ID:")} ${result.job_id}`,
2217
+ ` ${formatter.label("Scheduled for:")} ${result.scheduled_date}`
2218
+ ]);
2219
+ } else if (result.results) {
2220
+ const lines = [
2221
+ formatter.success("Document posted to LinkedIn successfully!"),
2222
+ ""
2223
+ ];
2224
+ for (const [platform, platformResult] of Object.entries(result.results)) {
2225
+ if (platformResult.success && platformResult.url) {
2226
+ lines.push(` ${platform.padEnd(12)} ${platformResult.url}`);
2227
+ } else if (!platformResult.success && platformResult.error) {
2228
+ lines.push(` ${platform.padEnd(12)} ${formatter.color("FAILED", "RED")} - ${platformResult.error}`);
2229
+ }
2230
+ }
2231
+ if (result.usage) {
2232
+ lines.push("");
2233
+ lines.push(`Usage: ${result.usage.count} / ${result.usage.limit} (${result.usage.remaining} remaining)`);
2234
+ }
2235
+ formatter.pretty(lines);
2236
+ } else {
2237
+ formatter.pretty([
2238
+ formatter.success("Document post queued successfully!"),
2239
+ ` ${formatter.label("Request ID:")} ${result.request_id || "N/A"}`
2240
+ ]);
2241
+ }
2242
+ }
2243
+ }
2244
+
2245
+ // src/commands/status.ts
2246
+ import { parseArgs as parseArgs10 } from "node:util";
2247
+ async function statusCheck(args, globalFlags) {
2248
+ const { values, positionals } = parseArgs10({
2249
+ args,
2250
+ options: {
2251
+ "request-id": { type: "string" },
2252
+ "job-id": { type: "string" },
2253
+ poll: { type: "boolean", default: false },
2254
+ interval: { type: "string" }
2255
+ },
2256
+ strict: false,
2257
+ allowPositionals: true
2258
+ });
2259
+ const hasPositional = positionals.length > 0;
2260
+ const hasRequestId = !!values["request-id"];
2261
+ const hasJobId = !!values["job-id"];
2262
+ const idSourceCount = [hasPositional, hasRequestId, hasJobId].filter(Boolean).length;
2263
+ if (idSourceCount > 1) {
2264
+ throw new UserError(`Only one ID source allowed. Provide one of:
2265
+ ` + ` posterboy status <id>
2266
+ ` + ` posterboy status --request-id <id>
2267
+ ` + " posterboy status --job-id <id>");
2268
+ }
2269
+ let id;
2270
+ let idType = "job_id";
2271
+ if (values["request-id"]) {
2272
+ id = values["request-id"];
2273
+ idType = "request_id";
2274
+ } else if (values["job-id"]) {
2275
+ id = values["job-id"];
2276
+ idType = "job_id";
2277
+ } else if (positionals.length > 0) {
2278
+ id = positionals[0];
2279
+ if (id.startsWith("req_")) {
2280
+ idType = "request_id";
2281
+ }
2282
+ }
2283
+ if (!id) {
2284
+ throw new UserError(`ID required. Provide one of:
2285
+ ` + ` posterboy status <id> (job_id or request_id)
2286
+ ` + ` posterboy status --request-id <id>
2287
+ ` + " posterboy status --job-id <id>");
2288
+ }
2289
+ const pollInterval = values.interval ? parseInt(values.interval, 10) * 1000 : 5000;
2290
+ if (isNaN(pollInterval) || pollInterval < 1000) {
2291
+ throw new UserError("Poll interval must be at least 1 second");
2292
+ }
2293
+ const config = readConfig();
2294
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2295
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2296
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2297
+ if (values.poll) {
2298
+ const isTTY = process.stdout.isTTY;
2299
+ let attempts = 0;
2300
+ const maxAttempts = 120;
2301
+ let interrupted = false;
2302
+ const sigintHandler = () => {
2303
+ interrupted = true;
2304
+ if (isTTY) {
2305
+ console.log(`
2306
+
2307
+ Polling interrupted by user.`);
2308
+ }
2309
+ process.exit(0);
2310
+ };
2311
+ process.on("SIGINT", sigintHandler);
2312
+ while (attempts < maxAttempts && !interrupted) {
2313
+ const result = await client.getStatus(id, idType);
2314
+ if (result.status === "completed" || result.status === "failed") {
2315
+ process.off("SIGINT", sigintHandler);
2316
+ if (formatter.mode() === "json") {
2317
+ formatter.json(result);
2318
+ } else {
2319
+ displayPrettyStatus(result, formatter);
2320
+ }
2321
+ return;
2322
+ }
2323
+ if (isTTY) {
2324
+ const dots = ".".repeat(attempts % 3 + 1);
2325
+ process.stdout.write(`\rPolling status${dots} `);
2326
+ }
2327
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
2328
+ attempts++;
2329
+ }
2330
+ process.off("SIGINT", sigintHandler);
2331
+ if (attempts >= maxAttempts) {
2332
+ throw new UserError("Polling timeout reached. Status is still pending. Try again later.");
2333
+ }
2334
+ } else {
2335
+ const result = await client.getStatus(id, idType);
2336
+ if (formatter.mode() === "json") {
2337
+ formatter.json(result);
2338
+ } else {
2339
+ displayPrettyStatus(result, formatter);
2340
+ }
2341
+ }
2342
+ }
2343
+ function displayPrettyStatus(result, formatter) {
2344
+ const lines = [];
2345
+ if (result.status === "completed") {
2346
+ lines.push(formatter.success("Status: Completed"));
2347
+ } else if (result.status === "failed") {
2348
+ lines.push(formatter.color("Status: Failed", "RED"));
2349
+ } else {
2350
+ lines.push(`Status: ${result.status}`);
2351
+ }
2352
+ if (result.error) {
2353
+ lines.push("");
2354
+ lines.push(formatter.color(`Error: ${result.error}`, "RED"));
2355
+ }
2356
+ if (result.results) {
2357
+ lines.push("");
2358
+ lines.push(formatter.header("Platform Results:"));
2359
+ for (const [platform, platformResult] of Object.entries(result.results)) {
2360
+ const pr = platformResult;
2361
+ if (pr.success && pr.url) {
2362
+ lines.push(` ${platform.padEnd(12)} ${pr.url}`);
2363
+ } else if (!pr.success && pr.error) {
2364
+ lines.push(` ${platform.padEnd(12)} ${formatter.color("FAILED", "RED")} - ${pr.error}`);
2365
+ }
2366
+ }
2367
+ }
2368
+ formatter.pretty(lines);
2369
+ }
2370
+
2371
+ // src/commands/schedule/list.ts
2372
+ import { parseArgs as parseArgs11 } from "node:util";
2373
+ async function scheduleList(args, globalFlags) {
2374
+ const { values } = parseArgs11({
2375
+ args,
2376
+ options: {
2377
+ profile: { type: "string" }
2378
+ },
2379
+ strict: false
2380
+ });
2381
+ const config = readConfig();
2382
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2383
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2384
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2385
+ const result = await client.listScheduledPosts(profile);
2386
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2387
+ if (formatter.mode() === "json") {
2388
+ formatter.json(result);
2389
+ } else {
2390
+ if (result.count === 0) {
2391
+ formatter.pretty([
2392
+ formatter.muted("No scheduled posts found.")
2393
+ ]);
2394
+ } else {
2395
+ const lines = [
2396
+ formatter.header(`Scheduled Posts (${result.count})`),
2397
+ ""
2398
+ ];
2399
+ for (const post of result.scheduled_posts) {
2400
+ const platformsList = post.platforms.join(", ");
2401
+ const title = post.title || "(no title)";
2402
+ lines.push(` ${formatter.label("Job ID:")} ${post.job_id}`);
2403
+ lines.push(` ${formatter.label("Date:")} ${post.scheduled_date}`);
2404
+ lines.push(` ${formatter.label("Platforms:")} ${platformsList}`);
2405
+ lines.push(` ${formatter.label("Type:")} ${post.content_type}`);
2406
+ lines.push(` ${formatter.label("Title:")} ${title}`);
2407
+ lines.push(` ${formatter.label("Profile:")} ${post.profile}`);
2408
+ lines.push("");
2409
+ }
2410
+ formatter.pretty(lines);
2411
+ }
2412
+ }
2413
+ }
2414
+
2415
+ // src/commands/schedule/cancel.ts
2416
+ import { parseArgs as parseArgs12 } from "node:util";
2417
+ async function scheduleCancel(args, globalFlags) {
2418
+ const { values } = parseArgs12({
2419
+ args,
2420
+ options: {
2421
+ "job-id": { type: "string" },
2422
+ confirm: { type: "boolean", default: false }
2423
+ },
2424
+ strict: false
2425
+ });
2426
+ if (!values["job-id"]) {
2427
+ throw new UserError(`Job ID required.
2428
+ ` + "Usage: posterboy schedule cancel --job-id <id> [--confirm]");
2429
+ }
2430
+ const jobId = values["job-id"];
2431
+ const confirmed = values.confirm;
2432
+ if (!confirmed && process.stdout.isTTY) {
2433
+ const readline = await import("node:readline");
2434
+ const rl = readline.createInterface({
2435
+ input: process.stdin,
2436
+ output: process.stdout
2437
+ });
2438
+ const answer = await new Promise((resolve) => {
2439
+ rl.question(`Cancel scheduled post ${jobId}? (y/n): `, resolve);
2440
+ });
2441
+ rl.close();
2442
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
2443
+ console.log("Cancelled.");
2444
+ return;
2445
+ }
2446
+ } else if (!confirmed && !process.stdout.isTTY) {
2447
+ throw new UserError(`Confirmation required for non-interactive mode.
2448
+ Use --confirm flag to proceed without prompt.`);
2449
+ }
2450
+ const config = readConfig();
2451
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2452
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2453
+ const result = await client.cancelScheduledPost(jobId);
2454
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2455
+ if (formatter.mode() === "json") {
2456
+ formatter.json({ success: result.success, job_id: jobId });
2457
+ } else {
2458
+ formatter.pretty([
2459
+ formatter.success(`Scheduled post ${jobId} cancelled.`)
2460
+ ]);
2461
+ }
2462
+ }
2463
+
2464
+ // src/commands/schedule/modify.ts
2465
+ import { parseArgs as parseArgs13 } from "node:util";
2466
+ async function scheduleModify(args, globalFlags) {
2467
+ const { values } = parseArgs13({
2468
+ args,
2469
+ options: {
2470
+ "job-id": { type: "string" },
2471
+ schedule: { type: "string" },
2472
+ title: { type: "string" },
2473
+ timezone: { type: "string" }
2474
+ },
2475
+ strict: false
2476
+ });
2477
+ if (!values["job-id"]) {
2478
+ throw new UserError(`Job ID required.
2479
+ ` + "Usage: posterboy schedule modify --job-id <id> [--schedule <datetime>] [--title <text>] [--timezone <tz>]");
2480
+ }
2481
+ const jobId = values["job-id"];
2482
+ const updates = {};
2483
+ if (values.schedule) {
2484
+ const scheduleDate = values.schedule;
2485
+ validateISODate(scheduleDate);
2486
+ updates.schedule = scheduleDate;
2487
+ }
2488
+ if (values.title) {
2489
+ updates.title = values.title;
2490
+ }
2491
+ if (values.timezone) {
2492
+ const tz = values.timezone;
2493
+ validateTimezone(tz);
2494
+ updates.timezone = tz;
2495
+ }
2496
+ if (Object.keys(updates).length === 0) {
2497
+ throw new UserError(`At least one update required.
2498
+ ` + "Provide one of: --schedule, --title, or --timezone");
2499
+ }
2500
+ const config = readConfig();
2501
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2502
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2503
+ const result = await client.modifyScheduledPost(jobId, updates);
2504
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2505
+ if (formatter.mode() === "json") {
2506
+ formatter.json({ success: result.success, job_id: jobId });
2507
+ } else {
2508
+ formatter.pretty([
2509
+ formatter.success(`Scheduled post ${jobId} updated.`)
2510
+ ]);
2511
+ }
2512
+ }
2513
+
2514
+ // src/commands/queue/settings.ts
2515
+ import { parseArgs as parseArgs14 } from "node:util";
2516
+ function validateTimeSlot(slot) {
2517
+ const timePattern = /^([0-1][0-9]|2[0-3]):([0-5][0-9])$/;
2518
+ if (!timePattern.test(slot)) {
2519
+ throw new UserError(`Invalid time slot format: ${slot}
2520
+ ` + `Expected format: HH:MM (24-hour time, e.g., 09:00, 14:30, 23:45)`);
2521
+ }
2522
+ return true;
2523
+ }
2524
+ function parseTimeSlots(slotsString) {
2525
+ const slots = slotsString.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2526
+ if (slots.length === 0) {
2527
+ throw new UserError("At least one time slot is required");
2528
+ }
2529
+ if (slots.length > 24) {
2530
+ throw new UserError(`Maximum 24 time slots allowed. You provided ${slots.length}.`);
2531
+ }
2532
+ slots.forEach((slot) => validateTimeSlot(slot));
2533
+ return slots;
2534
+ }
2535
+ function parseDaysOfWeek(daysString) {
2536
+ const dayMap = {
2537
+ "0": "sun",
2538
+ "1": "mon",
2539
+ "2": "tue",
2540
+ "3": "wed",
2541
+ "4": "thu",
2542
+ "5": "fri",
2543
+ "6": "sat",
2544
+ sun: "sun",
2545
+ mon: "mon",
2546
+ tue: "tue",
2547
+ wed: "wed",
2548
+ thu: "thu",
2549
+ fri: "fri",
2550
+ sat: "sat",
2551
+ sunday: "sun",
2552
+ monday: "mon",
2553
+ tuesday: "tue",
2554
+ wednesday: "wed",
2555
+ thursday: "thu",
2556
+ friday: "fri",
2557
+ saturday: "sat"
2558
+ };
2559
+ const days = daysString.split(",").map((d) => d.trim().toLowerCase()).filter((d) => d.length > 0);
2560
+ if (days.length === 0) {
2561
+ throw new UserError("At least one day of the week is required");
2562
+ }
2563
+ const normalized = days.map((day) => {
2564
+ const normalizedDay = dayMap[day];
2565
+ if (!normalizedDay) {
2566
+ throw new UserError(`Invalid day: ${day}
2567
+ ` + `Valid days: mon, tue, wed, thu, fri, sat, sun (or 0-6, or full names)`);
2568
+ }
2569
+ return normalizedDay;
2570
+ });
2571
+ return [...new Set(normalized)];
2572
+ }
2573
+ async function queueSettings(args, globalFlags) {
2574
+ const { values } = parseArgs14({
2575
+ args,
2576
+ options: {
2577
+ profile: { type: "string" },
2578
+ "set-timezone": { type: "string" },
2579
+ "set-slots": { type: "string" },
2580
+ "set-days": { type: "string" }
2581
+ },
2582
+ strict: false
2583
+ });
2584
+ const config = readConfig();
2585
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2586
+ if (!profile) {
2587
+ throw new UserError(`Profile required. Provide one of:
2588
+ ` + ` --profile <name> (command flag)
2589
+ ` + ` --profile <name> (global flag) (before 'queue')
2590
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
2591
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
2592
+ }
2593
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2594
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2595
+ const isUpdate = !!(values["set-timezone"] || values["set-slots"] || values["set-days"]);
2596
+ if (isUpdate) {
2597
+ const updates = {};
2598
+ if (values["set-timezone"]) {
2599
+ const timezone = values["set-timezone"];
2600
+ validateTimezone(timezone);
2601
+ updates.timezone = timezone;
2602
+ }
2603
+ if (values["set-slots"]) {
2604
+ updates.slots = parseTimeSlots(values["set-slots"]);
2605
+ }
2606
+ if (values["set-days"]) {
2607
+ updates.days_of_week = parseDaysOfWeek(values["set-days"]);
2608
+ }
2609
+ await client.updateQueueSettings(profile, updates);
2610
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2611
+ if (formatter.mode() === "json") {
2612
+ formatter.json({ success: true });
2613
+ } else {
2614
+ formatter.pretty([formatter.success("Queue settings updated.")]);
2615
+ }
2616
+ } else {
2617
+ const settings = await client.getQueueSettings(profile);
2618
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2619
+ if (formatter.mode() === "json") {
2620
+ formatter.json(settings);
2621
+ } else {
2622
+ const lines = [
2623
+ formatter.header("Queue Settings:"),
2624
+ "",
2625
+ ` ${formatter.label("Profile:")} ${settings.profile}`,
2626
+ ` ${formatter.label("Timezone:")} ${settings.timezone}`,
2627
+ ` ${formatter.label("Time Slots:")} ${settings.slots.join(", ")}`,
2628
+ ` ${formatter.label("Days Active:")} ${settings.days_of_week.join(", ")}`
2629
+ ];
2630
+ formatter.pretty(lines);
2631
+ }
2632
+ }
2633
+ }
2634
+
2635
+ // src/commands/queue/preview.ts
2636
+ import { parseArgs as parseArgs15 } from "node:util";
2637
+ async function queuePreview(args, globalFlags) {
2638
+ const { values } = parseArgs15({
2639
+ args,
2640
+ options: {
2641
+ profile: { type: "string" },
2642
+ count: { type: "string" }
2643
+ },
2644
+ strict: false
2645
+ });
2646
+ const config = readConfig();
2647
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2648
+ if (!profile) {
2649
+ throw new UserError(`Profile required. Provide one of:
2650
+ ` + ` --profile <name> (command flag)
2651
+ ` + ` --profile <name> (global flag) (before 'queue')
2652
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
2653
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
2654
+ }
2655
+ let count = 10;
2656
+ if (values.count) {
2657
+ const parsed = parseInt(values.count, 10);
2658
+ if (isNaN(parsed) || parsed < 1 || parsed > 50) {
2659
+ throw new UserError(`Invalid count: ${values.count}
2660
+ ` + `Count must be a number between 1 and 50`);
2661
+ }
2662
+ count = parsed;
2663
+ }
2664
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2665
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2666
+ const result = await client.previewQueue(profile, count);
2667
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2668
+ if (formatter.mode() === "json") {
2669
+ formatter.json(result);
2670
+ } else {
2671
+ const lines = [
2672
+ formatter.header(`Next ${result.slots.length} Queue Slots:`),
2673
+ ""
2674
+ ];
2675
+ if (result.slots.length === 0) {
2676
+ lines.push(" No upcoming slots available.");
2677
+ } else {
2678
+ const headers = ["Datetime", "Status"];
2679
+ const rows = result.slots.map((slot) => [
2680
+ slot.datetime,
2681
+ slot.available ? formatter.success("Available") : formatter.muted("Occupied")
2682
+ ]);
2683
+ const col1Width = Math.max(headers[0].length, ...rows.map((r) => r[0].length));
2684
+ lines.push(` ${formatter.color(headers[0].padEnd(col1Width), "CYAN")} ${formatter.color(headers[1], "CYAN")}`);
2685
+ rows.forEach((row) => {
2686
+ lines.push(` ${row[0].padEnd(col1Width)} ${row[1]}`);
2687
+ });
2688
+ }
2689
+ formatter.pretty(lines);
2690
+ }
2691
+ }
2692
+
2693
+ // src/commands/queue/next.ts
2694
+ import { parseArgs as parseArgs16 } from "node:util";
2695
+ async function queueNext(args, globalFlags) {
2696
+ const { values } = parseArgs16({
2697
+ args,
2698
+ options: {
2699
+ profile: { type: "string" }
2700
+ },
2701
+ strict: false
2702
+ });
2703
+ const config = readConfig();
2704
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2705
+ if (!profile) {
2706
+ throw new UserError(`Profile required. Provide one of:
2707
+ ` + ` --profile <name> (command flag)
2708
+ ` + ` --profile <name> (global flag) (before 'queue')
2709
+ ` + ` POSTERBOY_PROFILE=<name> (environment variable)
2710
+ ` + ' "default_profile": "<name>" (in ~/.posterboy/config.json)');
2711
+ }
2712
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2713
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2714
+ const result = await client.nextSlot(profile);
2715
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2716
+ if (formatter.mode() === "json") {
2717
+ formatter.json(result);
2718
+ } else {
2719
+ formatter.pretty([
2720
+ formatter.success(`Next available slot: ${result.next_slot}`)
2721
+ ]);
2722
+ }
2723
+ }
2724
+
2725
+ // src/commands/history.ts
2726
+ import { parseArgs as parseArgs17 } from "node:util";
2727
+ async function history(args, globalFlags) {
2728
+ const { values } = parseArgs17({
2729
+ args,
2730
+ options: {
2731
+ profile: { type: "string" },
2732
+ page: { type: "string" },
2733
+ limit: { type: "string" }
2734
+ },
2735
+ strict: false
2736
+ });
2737
+ const page = values.page ? parseInt(values.page, 10) : 1;
2738
+ if (isNaN(page) || page < 1) {
2739
+ throw new UserError("Page must be a positive integer (>= 1)");
2740
+ }
2741
+ const limit = values.limit ? parseInt(values.limit, 10) : 10;
2742
+ if (isNaN(limit) || limit < 1 || limit > 100) {
2743
+ throw new UserError("Limit must be between 1 and 100");
2744
+ }
2745
+ const config = readConfig();
2746
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2747
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2748
+ const profile = values.profile || globalFlags.profile || getDefaultProfile(undefined, config);
2749
+ const result = await client.getHistory(profile, page, limit);
2750
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2751
+ if (formatter.mode() === "json") {
2752
+ formatter.json(result);
2753
+ } else {
2754
+ if (result.history.length === 0) {
2755
+ formatter.pretty([
2756
+ formatter.muted("No upload history found.")
2757
+ ]);
2758
+ } else {
2759
+ const lines = [
2760
+ formatter.header(`Upload History (page ${result.page} of ${result.total_pages})`),
2761
+ ""
2762
+ ];
2763
+ for (const entry of result.history) {
2764
+ const date = entry.date.substring(0, 16).replace("T", " ");
2765
+ const platformsList = entry.platforms.join(", ");
2766
+ const title = entry.title || "(no title)";
2767
+ const contentType = entry.content_type.padEnd(8);
2768
+ lines.push(` ${date} ${contentType} ${platformsList.padEnd(20)} ${title}`);
2769
+ }
2770
+ formatter.pretty(lines);
2771
+ }
2772
+ }
2773
+ }
2774
+
2775
+ // src/commands/analytics.ts
2776
+ import { parseArgs as parseArgs18 } from "node:util";
2777
+ async function analytics(args, globalFlags) {
2778
+ const { values, positionals } = parseArgs18({
2779
+ args,
2780
+ options: {
2781
+ platforms: { type: "string" },
2782
+ "facebook-page": { type: "string" },
2783
+ "linkedin-page": { type: "string" }
2784
+ },
2785
+ strict: false,
2786
+ allowPositionals: true
2787
+ });
2788
+ const profile = positionals[0];
2789
+ if (!profile) {
2790
+ throw new UserError(`Profile is required.
2791
+ ` + "Usage: posterboy analytics <profile> [--platforms <list>]");
2792
+ }
2793
+ const platforms2 = values.platforms ? values.platforms.split(",").map((p) => p.trim()) : undefined;
2794
+ const facebookPage = values["facebook-page"];
2795
+ const linkedinPage = values["linkedin-page"];
2796
+ if (platforms2?.includes("facebook") && !facebookPage) {
2797
+ throw new UserError(`Facebook analytics require --facebook-page flag.
2798
+ ` + "Usage: posterboy analytics <profile> --platforms facebook --facebook-page <page_id>");
2799
+ }
2800
+ const config = readConfig();
2801
+ const apiKey = getApiKey(globalFlags.apiKey, config?.api_key);
2802
+ const client = new ApiClient(apiKey, { verbose: globalFlags.verbose });
2803
+ const result = await client.getAnalytics(profile, platforms2, facebookPage, linkedinPage);
2804
+ const formatter = createOutputFormatter(globalFlags.json, globalFlags.pretty, true);
2805
+ if (formatter.mode() === "json") {
2806
+ formatter.json(result);
2807
+ } else {
2808
+ const lines = [
2809
+ formatter.header(`Analytics for "${result.profile}"`),
2810
+ ""
2811
+ ];
2812
+ if (Object.keys(result.analytics).length === 0) {
2813
+ lines.push(" No analytics data available.");
2814
+ } else {
2815
+ for (const [platform, metrics] of Object.entries(result.analytics)) {
2816
+ lines.push(` ${formatter.label(platform)}`);
2817
+ for (const [key, value] of Object.entries(metrics)) {
2818
+ const formattedKey = key.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2819
+ let formattedValue;
2820
+ if (typeof value === "number") {
2821
+ formattedValue = value.toLocaleString();
2822
+ } else {
2823
+ formattedValue = String(value);
2824
+ }
2825
+ lines.push(` ${formattedKey.padEnd(15)} ${formattedValue}`);
2826
+ }
2827
+ lines.push("");
2828
+ }
2829
+ }
2830
+ formatter.pretty(lines);
2831
+ }
2832
+ }
2833
+
2834
+ // src/commands/completions.ts
2835
+ import { parseArgs as parseArgs19 } from "node:util";
2836
+ var BASH_COMPLETION = `# posterboy bash completion
2837
+ _posterboy() {
2838
+ local cur prev commands
2839
+ cur="\${COMP_WORDS[COMP_CWORD]}"
2840
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
2841
+ commands="auth profiles post schedule status history queue platforms analytics completions"
2842
+
2843
+ case "\${prev}" in
2844
+ posterboy)
2845
+ COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
2846
+ ;;
2847
+ auth)
2848
+ COMPREPLY=($(compgen -W "login status" -- "\${cur}"))
2849
+ ;;
2850
+ profiles)
2851
+ COMPREPLY=($(compgen -W "list create delete connect" -- "\${cur}"))
2852
+ ;;
2853
+ post)
2854
+ COMPREPLY=($(compgen -W "text photo video document" -- "\${cur}"))
2855
+ ;;
2856
+ schedule)
2857
+ COMPREPLY=($(compgen -W "list cancel modify" -- "\${cur}"))
2858
+ ;;
2859
+ queue)
2860
+ COMPREPLY=($(compgen -W "settings preview next" -- "\${cur}"))
2861
+ ;;
2862
+ platforms)
2863
+ COMPREPLY=($(compgen -W "pages" -- "\${cur}"))
2864
+ ;;
2865
+ pages)
2866
+ COMPREPLY=($(compgen -W "facebook linkedin pinterest" -- "\${cur}"))
2867
+ ;;
2868
+ completions)
2869
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "\${cur}"))
2870
+ ;;
2871
+ *)
2872
+ COMPREPLY=($(compgen -W "--json --pretty --verbose --profile --config --api-key --help --version" -- "\${cur}"))
2873
+ ;;
2874
+ esac
2875
+ }
2876
+
2877
+ complete -F _posterboy posterboy
2878
+ `;
2879
+ var ZSH_COMPLETION = `#compdef posterboy
2880
+ # posterboy zsh completion
2881
+
2882
+ _posterboy() {
2883
+ local curcontext="$curcontext" state line
2884
+ typeset -A opt_args
2885
+
2886
+ _arguments -C \\
2887
+ '1: :->command' \\
2888
+ '*:: :->args' \\
2889
+ '--json[Force JSON output]' \\
2890
+ '--pretty[Force pretty output]' \\
2891
+ '--verbose[Show request/response details]' \\
2892
+ '--config[Override config file path]:file:_files' \\
2893
+ '--api-key[Override API key]:key:' \\
2894
+ '--profile[Override default profile]:profile:' \\
2895
+ '--help[Print help]' \\
2896
+ '--version[Print version]'
2897
+
2898
+ case $state in
2899
+ command)
2900
+ local -a commands
2901
+ commands=(
2902
+ 'auth:Authentication and account management'
2903
+ 'profiles:Profile management'
2904
+ 'post:Content posting'
2905
+ 'schedule:Scheduled post management'
2906
+ 'status:Check upload status'
2907
+ 'history:View upload history'
2908
+ 'queue:Queue management'
2909
+ 'platforms:List connected platforms'
2910
+ 'analytics:View profile analytics'
2911
+ 'completions:Generate shell completions'
2912
+ )
2913
+ _describe 'command' commands
2914
+ ;;
2915
+ args)
2916
+ case $line[1] in
2917
+ auth)
2918
+ _arguments '1: :(login status)'
2919
+ ;;
2920
+ profiles)
2921
+ _arguments '1: :(list create delete connect)'
2922
+ ;;
2923
+ post)
2924
+ _arguments '1: :(text photo video document)'
2925
+ ;;
2926
+ schedule)
2927
+ _arguments '1: :(list cancel modify)'
2928
+ ;;
2929
+ queue)
2930
+ _arguments '1: :(settings preview next)'
2931
+ ;;
2932
+ platforms)
2933
+ _arguments '1: :(pages)' '2: :(facebook linkedin pinterest)'
2934
+ ;;
2935
+ completions)
2936
+ _arguments '1: :(bash zsh fish)'
2937
+ ;;
2938
+ esac
2939
+ ;;
2940
+ esac
2941
+ }
2942
+
2943
+ _posterboy
2944
+ `;
2945
+ var FISH_COMPLETION = `# posterboy fish completion
2946
+
2947
+ # Global options
2948
+ complete -c posterboy -l json -d "Force JSON output"
2949
+ complete -c posterboy -l pretty -d "Force pretty output"
2950
+ complete -c posterboy -l verbose -d "Show request/response details"
2951
+ complete -c posterboy -l config -d "Override config file path" -r
2952
+ complete -c posterboy -l api-key -d "Override API key" -r
2953
+ complete -c posterboy -l profile -d "Override default profile" -r
2954
+ complete -c posterboy -l help -d "Print help"
2955
+ complete -c posterboy -l version -d "Print version"
2956
+
2957
+ # Main commands
2958
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "auth" -d "Authentication and account management"
2959
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "profiles" -d "Profile management"
2960
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "post" -d "Content posting"
2961
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "schedule" -d "Scheduled post management"
2962
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "status" -d "Check upload status"
2963
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "history" -d "View upload history"
2964
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "queue" -d "Queue management"
2965
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "platforms" -d "List connected platforms"
2966
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "analytics" -d "View profile analytics"
2967
+ complete -c posterboy -f -n "__fish_use_subcommand" -a "completions" -d "Generate shell completions"
2968
+
2969
+ # auth subcommands
2970
+ complete -c posterboy -f -n "__fish_seen_subcommand_from auth" -a "login" -d "Store API key in config"
2971
+ complete -c posterboy -f -n "__fish_seen_subcommand_from auth" -a "status" -d "Show account info"
2972
+
2973
+ # profiles subcommands
2974
+ complete -c posterboy -f -n "__fish_seen_subcommand_from profiles" -a "list" -d "List all connected profiles"
2975
+ complete -c posterboy -f -n "__fish_seen_subcommand_from profiles" -a "create" -d "Create a new profile"
2976
+ complete -c posterboy -f -n "__fish_seen_subcommand_from profiles" -a "delete" -d "Delete a profile"
2977
+ complete -c posterboy -f -n "__fish_seen_subcommand_from profiles" -a "connect" -d "Generate JWT URL"
2978
+
2979
+ # post subcommands
2980
+ complete -c posterboy -f -n "__fish_seen_subcommand_from post" -a "text" -d "Post text content"
2981
+ complete -c posterboy -f -n "__fish_seen_subcommand_from post" -a "photo" -d "Post photo/carousel"
2982
+ complete -c posterboy -f -n "__fish_seen_subcommand_from post" -a "video" -d "Post video content"
2983
+ complete -c posterboy -f -n "__fish_seen_subcommand_from post" -a "document" -d "Post document (LinkedIn)"
2984
+
2985
+ # schedule subcommands
2986
+ complete -c posterboy -f -n "__fish_seen_subcommand_from schedule" -a "list" -d "List scheduled posts"
2987
+ complete -c posterboy -f -n "__fish_seen_subcommand_from schedule" -a "cancel" -d "Cancel a scheduled post"
2988
+ complete -c posterboy -f -n "__fish_seen_subcommand_from schedule" -a "modify" -d "Modify a scheduled post"
2989
+
2990
+ # queue subcommands
2991
+ complete -c posterboy -f -n "__fish_seen_subcommand_from queue" -a "settings" -d "View or update queue config"
2992
+ complete -c posterboy -f -n "__fish_seen_subcommand_from queue" -a "preview" -d "Preview upcoming queue slots"
2993
+ complete -c posterboy -f -n "__fish_seen_subcommand_from queue" -a "next" -d "Get next available slot"
2994
+
2995
+ # platforms subcommands
2996
+ complete -c posterboy -f -n "__fish_seen_subcommand_from platforms" -a "pages" -d "List platform-specific pages/boards"
2997
+
2998
+ # platforms pages subcommands
2999
+ complete -c posterboy -f -n "__fish_seen_subcommand_from pages" -a "facebook" -d "List Facebook pages"
3000
+ complete -c posterboy -f -n "__fish_seen_subcommand_from pages" -a "linkedin" -d "List LinkedIn pages"
3001
+ complete -c posterboy -f -n "__fish_seen_subcommand_from pages" -a "pinterest" -d "List Pinterest boards"
3002
+
3003
+ # completions subcommands
3004
+ complete -c posterboy -f -n "__fish_seen_subcommand_from completions" -a "bash" -d "Generate bash completion"
3005
+ complete -c posterboy -f -n "__fish_seen_subcommand_from completions" -a "zsh" -d "Generate zsh completion"
3006
+ complete -c posterboy -f -n "__fish_seen_subcommand_from completions" -a "fish" -d "Generate fish completion"
3007
+ `;
3008
+ async function completions(args, _globalFlags) {
3009
+ const { positionals } = parseArgs19({
3010
+ args,
3011
+ options: {},
3012
+ strict: false,
3013
+ allowPositionals: true
3014
+ });
3015
+ const [shell] = positionals;
3016
+ if (!shell) {
3017
+ throw new UserError(`Shell type required. Specify one of: bash, zsh, fish
3018
+
3019
+ ` + `Examples:
3020
+ ` + ` posterboy completions bash > /etc/bash_completion.d/posterboy
3021
+ ` + ` posterboy completions zsh > ~/.zsh/completions/_posterboy
3022
+ ` + " posterboy completions fish > ~/.config/fish/completions/posterboy.fish");
3023
+ }
3024
+ switch (shell.toLowerCase()) {
3025
+ case "bash":
3026
+ console.log(BASH_COMPLETION);
3027
+ break;
3028
+ case "zsh":
3029
+ console.log(ZSH_COMPLETION);
3030
+ break;
3031
+ case "fish":
3032
+ console.log(FISH_COMPLETION);
3033
+ break;
3034
+ default:
3035
+ throw new UserError(`Unknown shell: ${shell}
3036
+ ` + "Supported shells: bash, zsh, fish");
3037
+ }
3038
+ }
3039
+
3040
+ // src/lib/output.ts
3041
+ var ANSI2 = {
3042
+ BOLD: "\x1B[1m",
3043
+ CYAN: "\x1B[36m",
3044
+ YELLOW: "\x1B[33m",
3045
+ GREEN: "\x1B[32m",
3046
+ RED: "\x1B[31m",
3047
+ GRAY: "\x1B[90m",
3048
+ RESET: "\x1B[0m"
3049
+ };
3050
+
3051
+ class OutputFormatter2 {
3052
+ _mode;
3053
+ _colorEnabled;
3054
+ constructor(forceMode, colorEnabled = true) {
3055
+ if (forceMode) {
3056
+ this._mode = forceMode;
3057
+ } else if (!process.stdout.isTTY) {
3058
+ this._mode = "json";
3059
+ } else {
3060
+ this._mode = "pretty";
3061
+ }
3062
+ this._colorEnabled = colorEnabled && !process.env.NO_COLOR;
3063
+ }
3064
+ mode() {
3065
+ return this._mode;
3066
+ }
3067
+ json(data) {
3068
+ console.log(JSON.stringify(data, null, 2));
3069
+ }
3070
+ pretty(lines) {
3071
+ console.log(lines.join(`
3072
+ `));
3073
+ }
3074
+ error(message, details) {
3075
+ const prefix = this.color("Error:", "RED");
3076
+ console.error(`${prefix} ${message}`);
3077
+ if (details) {
3078
+ console.error(details);
3079
+ }
3080
+ }
3081
+ usage(count, limit) {
3082
+ const remaining = limit - count;
3083
+ const line = `Usage: ${count} / ${limit} (${remaining} remaining)`;
3084
+ if (this._mode === "json") {
3085
+ return;
3086
+ }
3087
+ console.log(`
3088
+ ` + line);
3089
+ }
3090
+ table(headers, rows) {
3091
+ if (this._mode === "json") {
3092
+ return;
3093
+ }
3094
+ const widths = headers.map((h, i) => {
3095
+ const maxRowWidth = Math.max(...rows.map((r) => (r[i] || "").length));
3096
+ return Math.max(h.length, maxRowWidth);
3097
+ });
3098
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
3099
+ console.log(this.color(headerLine, "CYAN"));
3100
+ rows.forEach((row) => {
3101
+ const rowLine = row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
3102
+ console.log(rowLine);
3103
+ });
3104
+ }
3105
+ color(text, colorName) {
3106
+ if (!this._colorEnabled) {
3107
+ return text;
3108
+ }
3109
+ return `${ANSI2[colorName]}${text}${ANSI2.RESET}`;
3110
+ }
3111
+ header(text) {
3112
+ return this.color(text, "CYAN");
3113
+ }
3114
+ label(text) {
3115
+ return this.color(text, "BOLD");
3116
+ }
3117
+ success(text) {
3118
+ return this.color(text, "GREEN");
3119
+ }
3120
+ warning(text) {
3121
+ return this.color(text, "YELLOW");
3122
+ }
3123
+ muted(text) {
3124
+ return this.color(text, "GRAY");
3125
+ }
3126
+ }
3127
+ function createOutputFormatter2(jsonFlag, prettyFlag, colorEnabled = true) {
3128
+ let mode;
3129
+ if (jsonFlag) {
3130
+ mode = "json";
3131
+ } else if (prettyFlag) {
3132
+ mode = "pretty";
3133
+ }
3134
+ return new OutputFormatter2(mode, colorEnabled);
3135
+ }
3136
+
3137
+ // src/lib/errors.ts
3138
+ class PosterBoyError2 extends Error {
3139
+ constructor(message) {
3140
+ super(message);
3141
+ this.name = this.constructor.name;
3142
+ Error.captureStackTrace(this, this.constructor);
3143
+ }
3144
+ json() {
3145
+ return {
3146
+ success: false,
3147
+ error: this.message,
3148
+ code: this.name.replace(/Error$/, "").toUpperCase()
3149
+ };
3150
+ }
3151
+ }
3152
+
3153
+ class UserError2 extends PosterBoyError2 {
3154
+ exitCode = EXIT_CODES.USER_ERROR;
3155
+ }
3156
+
3157
+ class ApiError2 extends PosterBoyError2 {
3158
+ exitCode = EXIT_CODES.API_ERROR;
3159
+ statusCode;
3160
+ apiMessage;
3161
+ constructor(message, statusCode, apiMessage) {
3162
+ super(message);
3163
+ this.statusCode = statusCode;
3164
+ this.apiMessage = apiMessage || message;
3165
+ }
3166
+ json() {
3167
+ return {
3168
+ success: false,
3169
+ error: this.message,
3170
+ code: "API_ERROR",
3171
+ statusCode: this.statusCode,
3172
+ apiMessage: this.apiMessage
3173
+ };
3174
+ }
3175
+ }
3176
+
3177
+ class NetworkError2 extends PosterBoyError2 {
3178
+ exitCode = EXIT_CODES.NETWORK_ERROR;
3179
+ }
3180
+ function suggestFix(error) {
3181
+ const msg = error.message.toLowerCase();
3182
+ if (msg.includes("api key") || msg.includes("unauthorized") || msg.includes("authentication")) {
3183
+ return "Run 'posterboy auth login --key <your-key>' to set up authentication";
3184
+ }
3185
+ if (msg.includes("profile required") || msg.includes("profile not found")) {
3186
+ return "Set a default profile: posterboy profiles create --username <name>";
3187
+ }
3188
+ if (msg.includes("platform") && (msg.includes("not connected") || msg.includes("invalid"))) {
3189
+ return "See available platforms: posterboy platforms --profile <name>";
3190
+ }
3191
+ if (msg.includes("file not found") || msg.includes("no such file")) {
3192
+ return "Check that the file path is correct and the file exists";
3193
+ }
3194
+ return null;
3195
+ }
3196
+
3197
+ // src/lib/suggestions.ts
3198
+ function levenshteinDistance(a, b) {
3199
+ const matrix = [];
3200
+ for (let i = 0;i <= b.length; i++) {
3201
+ matrix[i] = [i];
3202
+ }
3203
+ for (let j = 0;j <= a.length; j++) {
3204
+ matrix[0][j] = j;
3205
+ }
3206
+ for (let i = 1;i <= b.length; i++) {
3207
+ for (let j = 1;j <= a.length; j++) {
3208
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
3209
+ matrix[i][j] = matrix[i - 1][j - 1];
3210
+ } else {
3211
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
3212
+ }
3213
+ }
3214
+ }
3215
+ return matrix[b.length][a.length];
3216
+ }
3217
+ function suggestCommand(input) {
3218
+ const commands = [
3219
+ "auth",
3220
+ "profiles",
3221
+ "post",
3222
+ "schedule",
3223
+ "status",
3224
+ "history",
3225
+ "queue",
3226
+ "platforms",
3227
+ "analytics",
3228
+ "completions"
3229
+ ];
3230
+ let bestMatch = null;
3231
+ let bestDistance = Infinity;
3232
+ for (const cmd of commands) {
3233
+ const distance = levenshteinDistance(input, cmd);
3234
+ if (distance < bestDistance && distance <= 3) {
3235
+ bestDistance = distance;
3236
+ bestMatch = cmd;
3237
+ }
3238
+ }
3239
+ return bestMatch;
3240
+ }
3241
+
3242
+ // src/index.ts
3243
+ var HELP_TEXT = `posterboy - Social media posting CLI
3244
+
3245
+ USAGE:
3246
+ posterboy [global-options] <command> <subcommand> [flags]
3247
+
3248
+ GLOBAL OPTIONS:
3249
+ --json Force JSON output
3250
+ --pretty Force pretty output
3251
+ --config <path> Override config file path
3252
+ --api-key <key> Override API key
3253
+ --profile <name> Override default profile
3254
+ --verbose Show request/response details
3255
+ --version Print version and exit
3256
+ --help Print help and exit
3257
+
3258
+ COMMANDS:
3259
+ auth Authentication and account management
3260
+ login Store API key in config
3261
+ status Show account info, plan, and usage
3262
+
3263
+ profiles Profile management
3264
+ list List all connected profiles
3265
+ create Create a new profile
3266
+ delete Delete a profile
3267
+ connect Generate JWT URL to connect social accounts
3268
+
3269
+ post Content posting
3270
+ text Post text content
3271
+ photo Post photo(s) / carousel
3272
+ video Post video content
3273
+ document Post document (LinkedIn only)
3274
+
3275
+ schedule Scheduled post management
3276
+ list List all scheduled posts
3277
+ cancel Cancel a scheduled post
3278
+ modify Modify a scheduled post
3279
+
3280
+ status Check upload status
3281
+ <id> Check status by job_id or request_id
3282
+
3283
+ history View upload history
3284
+
3285
+ queue Queue management
3286
+ settings View or update queue configuration
3287
+ preview Preview upcoming queue slots
3288
+ next Get next available queue slot
3289
+
3290
+ platforms List connected platforms for a profile
3291
+ pages List platform-specific pages/boards
3292
+ facebook List Facebook pages
3293
+ linkedin List LinkedIn pages
3294
+ pinterest List Pinterest boards
3295
+
3296
+ analytics View profile analytics
3297
+
3298
+ completions Generate shell completions
3299
+ bash Generate bash completion script
3300
+ zsh Generate zsh completion script
3301
+ fish Generate fish completion script
3302
+
3303
+ EXAMPLES:
3304
+ posterboy auth login --key up_xxxx
3305
+ posterboy auth status
3306
+ posterboy post text --body "Hello!" --platforms x,linkedin
3307
+ posterboy post photo --files photo.jpg --title "My photo" --platforms instagram
3308
+ posterboy history
3309
+ posterboy completions bash > /etc/bash_completion.d/posterboy
3310
+ `;
3311
+ async function main() {
3312
+ process.stdout.on("error", (err) => {
3313
+ const nodeErr = err;
3314
+ if (nodeErr.code === "EPIPE") {
3315
+ process.exit(0);
3316
+ }
3317
+ });
3318
+ const args = Bun.argv.slice(2);
3319
+ if (args.includes("--version")) {
3320
+ console.log(VERSION);
3321
+ process.exit(0);
3322
+ }
3323
+ if (args.includes("--help") || args.length === 0) {
3324
+ console.log(HELP_TEXT);
3325
+ process.exit(0);
3326
+ }
3327
+ try {
3328
+ const { values, positionals } = parseArgs20({
3329
+ args,
3330
+ options: {
3331
+ json: { type: "boolean", default: false },
3332
+ pretty: { type: "boolean", default: false },
3333
+ config: { type: "string" },
3334
+ "api-key": { type: "string" },
3335
+ profile: { type: "string" },
3336
+ verbose: { type: "boolean", default: false },
3337
+ version: { type: "boolean" },
3338
+ help: { type: "boolean" }
3339
+ },
3340
+ strict: false,
3341
+ allowPositionals: true
3342
+ });
3343
+ const globalFlags = {
3344
+ json: values.json,
3345
+ pretty: values.pretty,
3346
+ config: values.config,
3347
+ apiKey: values["api-key"],
3348
+ profile: values.profile,
3349
+ verbose: values.verbose
3350
+ };
3351
+ const [command, subcommand, ...remainingArgs] = positionals;
3352
+ switch (command) {
3353
+ case "auth":
3354
+ await handleAuthCommand(subcommand, remainingArgs, globalFlags);
3355
+ break;
3356
+ case "profiles":
3357
+ await handleProfilesCommand(subcommand, remainingArgs, globalFlags);
3358
+ break;
3359
+ case "platforms":
3360
+ await handlePlatformsCommand(subcommand, remainingArgs, globalFlags);
3361
+ break;
3362
+ case "post":
3363
+ await handlePostCommand(subcommand, remainingArgs, globalFlags);
3364
+ break;
3365
+ case "status":
3366
+ await handleStatusCommand(subcommand, remainingArgs, globalFlags);
3367
+ break;
3368
+ case "schedule":
3369
+ await handleScheduleCommand(subcommand, remainingArgs, globalFlags);
3370
+ break;
3371
+ case "queue":
3372
+ await handleQueueCommand(subcommand, remainingArgs, globalFlags);
3373
+ break;
3374
+ case "history":
3375
+ await history(remainingArgs, globalFlags);
3376
+ break;
3377
+ case "analytics":
3378
+ await handleAnalyticsCommand(subcommand, remainingArgs, globalFlags);
3379
+ break;
3380
+ case "completions":
3381
+ await completions(remainingArgs, globalFlags);
3382
+ break;
3383
+ default: {
3384
+ const suggestion = suggestCommand(command);
3385
+ console.error(`Unknown command: ${command}`);
3386
+ if (suggestion) {
3387
+ console.error(`Did you mean '${suggestion}'?`);
3388
+ }
3389
+ console.error("Run 'posterboy --help' for usage information");
3390
+ process.exit(1);
3391
+ }
3392
+ }
3393
+ } catch (error) {
3394
+ await handleError(error);
3395
+ }
3396
+ }
3397
+ async function handleAuthCommand(subcommand, args, globalFlags) {
3398
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
3399
+ console.log(`posterboy auth - Authentication and account management
3400
+
3401
+ SUBCOMMANDS:
3402
+ login Store API key in config
3403
+ status Show account info, plan, and usage
3404
+
3405
+ FLAGS:
3406
+ --key API key (for login)
3407
+ --json Force JSON output
3408
+ --verbose Show request/response details
3409
+ `);
3410
+ return;
3411
+ }
3412
+ switch (subcommand) {
3413
+ case "login":
3414
+ await authLogin(args, globalFlags);
3415
+ break;
3416
+ case "status":
3417
+ await authStatus(args, globalFlags);
3418
+ break;
3419
+ default:
3420
+ console.error(`Unknown auth subcommand: ${subcommand}`);
3421
+ console.error("Available: login, status");
3422
+ process.exit(1);
3423
+ break;
3424
+ }
3425
+ }
3426
+ async function handleProfilesCommand(subcommand, args, globalFlags) {
3427
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
3428
+ console.log(`posterboy profiles - Profile management
3429
+
3430
+ SUBCOMMANDS:
3431
+ list List all connected profiles
3432
+ create Create a new profile
3433
+ delete Delete a profile
3434
+ connect Generate JWT URL to connect social accounts
3435
+
3436
+ FLAGS:
3437
+ --username Profile username (for create/delete/connect)
3438
+ --platforms Target platforms for connection (for connect)
3439
+ --redirect Redirect URL after auth (for connect)
3440
+ --json Force JSON output
3441
+ --verbose Show request/response details
3442
+ `);
3443
+ return;
3444
+ }
3445
+ switch (subcommand) {
3446
+ case "list":
3447
+ await profilesList(args, globalFlags);
3448
+ break;
3449
+ case "create":
3450
+ await profilesCreate(args, globalFlags);
3451
+ break;
3452
+ case "delete":
3453
+ await profilesDelete(args, globalFlags);
3454
+ break;
3455
+ case "connect":
3456
+ await profilesConnect(args, globalFlags);
3457
+ break;
3458
+ default:
3459
+ console.error(`Unknown profiles subcommand: ${subcommand}`);
3460
+ console.error("Available: list, create, delete, connect");
3461
+ process.exit(1);
3462
+ break;
3463
+ }
3464
+ }
3465
+ async function handlePlatformsCommand(subcommand, args, globalFlags) {
3466
+ if (!subcommand) {
3467
+ await platforms(args, globalFlags);
3468
+ return;
3469
+ }
3470
+ if (subcommand === "help" || subcommand === "--help") {
3471
+ console.log(`posterboy platforms - List connected platforms
3472
+
3473
+ USAGE:
3474
+ posterboy platforms [--profile <name>]
3475
+ posterboy platforms pages <platform> [--profile <name>]
3476
+
3477
+ SUBCOMMANDS:
3478
+ pages List platform-specific pages/boards
3479
+ facebook List Facebook pages
3480
+ linkedin List LinkedIn pages
3481
+ pinterest List Pinterest boards
3482
+
3483
+ FLAGS:
3484
+ --profile Profile to check platforms for
3485
+ --json Force JSON output
3486
+ --verbose Show request/response details
3487
+ `);
3488
+ return;
3489
+ }
3490
+ if (subcommand === "pages") {
3491
+ const [platformSubcmd, ...remainingArgs] = args;
3492
+ if (!platformSubcmd) {
3493
+ console.error("Platform required for pages command");
3494
+ console.error("Available: facebook, linkedin, pinterest");
3495
+ process.exit(1);
3496
+ }
3497
+ await platformsPages(platformSubcmd, remainingArgs, globalFlags);
3498
+ return;
3499
+ }
3500
+ console.error(`Unknown platforms subcommand: ${subcommand}`);
3501
+ console.error("Available: pages");
3502
+ process.exit(1);
3503
+ }
3504
+ async function handlePostCommand(subcommand, args, globalFlags) {
3505
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
3506
+ console.log(`posterboy post - Content posting
3507
+
3508
+ SUBCOMMANDS:
3509
+ text Post text content
3510
+ photo Post photo(s) / carousel
3511
+ video Post video content
3512
+ document Post document (LinkedIn only)
3513
+
3514
+ FLAGS:
3515
+ --profile Profile to post from
3516
+ --platforms Target platforms (comma-separated)
3517
+ --schedule ISO-8601 datetime for scheduling
3518
+ --queue Add to next queue slot
3519
+ --dry-run Preview without posting
3520
+ --json Force JSON output
3521
+ --verbose Show request/response details
3522
+ `);
3523
+ return;
3524
+ }
3525
+ switch (subcommand) {
3526
+ case "text":
3527
+ await postText(args, globalFlags);
3528
+ break;
3529
+ case "photo":
3530
+ await postPhoto(args, globalFlags);
3531
+ break;
3532
+ case "video":
3533
+ await postVideo(args, globalFlags);
3534
+ break;
3535
+ case "document":
3536
+ await postDocument(args, globalFlags);
3537
+ break;
3538
+ default:
3539
+ console.error(`Unknown post subcommand: ${subcommand}`);
3540
+ console.error("Available: text, photo, video, document");
3541
+ process.exit(1);
3542
+ break;
3543
+ }
3544
+ }
3545
+ async function handleStatusCommand(subcommand, args, globalFlags) {
3546
+ const allArgs = subcommand ? [subcommand, ...args] : args;
3547
+ await statusCheck(allArgs, globalFlags);
3548
+ }
3549
+ async function handleScheduleCommand(subcommand, args, globalFlags) {
3550
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
3551
+ console.log(`posterboy schedule - Scheduled post management
3552
+
3553
+ SUBCOMMANDS:
3554
+ list List all scheduled posts
3555
+ cancel Cancel a scheduled post
3556
+ modify Modify a scheduled post
3557
+
3558
+ FLAGS:
3559
+ --profile Filter by profile (for list)
3560
+ --id Job ID (for cancel/modify)
3561
+ --schedule New schedule time (for modify)
3562
+ --title New title (for modify)
3563
+ --timezone New timezone (for modify)
3564
+ --json Force JSON output
3565
+ --verbose Show request/response details
3566
+ `);
3567
+ return;
3568
+ }
3569
+ switch (subcommand) {
3570
+ case "list":
3571
+ await scheduleList(args, globalFlags);
3572
+ break;
3573
+ case "cancel":
3574
+ await scheduleCancel(args, globalFlags);
3575
+ break;
3576
+ case "modify":
3577
+ await scheduleModify(args, globalFlags);
3578
+ break;
3579
+ default:
3580
+ console.error(`Unknown schedule subcommand: ${subcommand}`);
3581
+ console.error("Available: list, cancel, modify");
3582
+ process.exit(1);
3583
+ break;
3584
+ }
3585
+ }
3586
+ async function handleQueueCommand(subcommand, args, globalFlags) {
3587
+ if (!subcommand || subcommand === "help" || subcommand === "--help") {
3588
+ console.log(`posterboy queue - Queue management
3589
+
3590
+ SUBCOMMANDS:
3591
+ settings View or update queue configuration
3592
+ preview Preview upcoming queue slots
3593
+ next Get next available queue slot
3594
+
3595
+ FLAGS:
3596
+ --profile Profile to manage queue for
3597
+ --count Number of slots to preview (for preview)
3598
+ --enabled Enable/disable queue (for settings)
3599
+ --times Queue times (comma-separated, for settings)
3600
+ --timezone Queue timezone (for settings)
3601
+ --json Force JSON output
3602
+ --verbose Show request/response details
3603
+ `);
3604
+ return;
3605
+ }
3606
+ switch (subcommand) {
3607
+ case "settings":
3608
+ await queueSettings(args, globalFlags);
3609
+ break;
3610
+ case "preview":
3611
+ await queuePreview(args, globalFlags);
3612
+ break;
3613
+ case "next":
3614
+ await queueNext(args, globalFlags);
3615
+ break;
3616
+ default:
3617
+ console.error(`Unknown queue subcommand: ${subcommand}`);
3618
+ console.error("Available: settings, preview, next");
3619
+ process.exit(1);
3620
+ break;
3621
+ }
3622
+ }
3623
+ async function handleAnalyticsCommand(subcommand, args, globalFlags) {
3624
+ const allArgs = subcommand ? [subcommand, ...args] : args;
3625
+ await analytics(allArgs, globalFlags);
3626
+ }
3627
+ async function handleError(error) {
3628
+ const formatter = createOutputFormatter2(false, false, true);
3629
+ if (error instanceof PosterBoyError2) {
3630
+ formatter.error(error.message);
3631
+ const fix = suggestFix(error);
3632
+ if (fix) {
3633
+ console.error(`
3634
+ Tip: ${fix}`);
3635
+ }
3636
+ process.exit(error.exitCode);
3637
+ } else if (error instanceof Error) {
3638
+ formatter.error(error.message);
3639
+ process.exit(1);
3640
+ } else {
3641
+ formatter.error("An unexpected error occurred");
3642
+ process.exit(1);
3643
+ }
3644
+ }
3645
+ main();