typefully 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -17
- package/dist/index.js +474 -206
- package/package.json +17 -2
- package/skills/skill.md +63 -77
- package/skills/spec.md +898 -0
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { Command } from "commander";
|
|
|
6
6
|
|
|
7
7
|
// src/commands/aliases.ts
|
|
8
8
|
import fs3 from "fs";
|
|
9
|
+
import * as p from "@clack/prompts";
|
|
10
|
+
import pc4 from "picocolors";
|
|
9
11
|
|
|
10
12
|
// src/utils/config.ts
|
|
11
13
|
import fs from "fs";
|
|
@@ -18,7 +20,8 @@ import pc from "picocolors";
|
|
|
18
20
|
import { z } from "zod/v4";
|
|
19
21
|
var ConfigSchema = z.object({
|
|
20
22
|
apiKey: z.string().optional(),
|
|
21
|
-
defaultSocialSetId: z.union([z.string(), z.number()]).optional()
|
|
23
|
+
defaultSocialSetId: z.union([z.string(), z.number()]).optional(),
|
|
24
|
+
defaultPlatforms: z.array(z.string()).optional()
|
|
22
25
|
});
|
|
23
26
|
var PLATFORMS = ["x", "linkedin", "threads", "bluesky", "mastodon"];
|
|
24
27
|
|
|
@@ -112,6 +115,14 @@ async function requireApiKey() {
|
|
|
112
115
|
console.error("");
|
|
113
116
|
return { source: configPath, key: apiKey };
|
|
114
117
|
}
|
|
118
|
+
function getDefaultPlatforms() {
|
|
119
|
+
const localPath = path.join(process.cwd(), LOCAL_CONFIG_FILE);
|
|
120
|
+
const localConfig = readConfigFile(localPath);
|
|
121
|
+
if (localConfig?.defaultPlatforms?.length) return localConfig.defaultPlatforms;
|
|
122
|
+
const globalConfig = readConfigFile(GLOBAL_CONFIG_FILE);
|
|
123
|
+
if (globalConfig?.defaultPlatforms?.length) return globalConfig.defaultPlatforms;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
115
126
|
function getDefaultSocialSetId() {
|
|
116
127
|
const localPath = path.join(process.cwd(), LOCAL_CONFIG_FILE);
|
|
117
128
|
const localConfig = readConfigFile(localPath);
|
|
@@ -185,9 +196,9 @@ var NOOP_SPINNER = {
|
|
|
185
196
|
fail() {
|
|
186
197
|
}
|
|
187
198
|
};
|
|
188
|
-
function spin(
|
|
199
|
+
function spin(text5) {
|
|
189
200
|
if (isJsonMode()) return NOOP_SPINNER;
|
|
190
|
-
return ora({ text:
|
|
201
|
+
return ora({ text: text5, color: "cyan" });
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
// src/utils/api.ts
|
|
@@ -208,12 +219,12 @@ async function apiRequest(method, endpoint, body, opts = {}) {
|
|
|
208
219
|
}
|
|
209
220
|
const res = await fetch(url, fetchOpts);
|
|
210
221
|
if (!res.ok) {
|
|
211
|
-
const
|
|
222
|
+
const text6 = await res.text().catch(() => "");
|
|
212
223
|
let parsed;
|
|
213
224
|
try {
|
|
214
|
-
parsed = JSON.parse(
|
|
225
|
+
parsed = JSON.parse(text6);
|
|
215
226
|
} catch {
|
|
216
|
-
parsed =
|
|
227
|
+
parsed = text6;
|
|
217
228
|
}
|
|
218
229
|
if (exitOnError) {
|
|
219
230
|
console.error(JSON.stringify({ error: `HTTP ${res.status}`, response: parsed }, null, 2));
|
|
@@ -224,12 +235,12 @@ async function apiRequest(method, endpoint, body, opts = {}) {
|
|
|
224
235
|
err.status = res.status;
|
|
225
236
|
throw err;
|
|
226
237
|
}
|
|
227
|
-
const
|
|
228
|
-
if (!
|
|
238
|
+
const text5 = await res.text();
|
|
239
|
+
if (!text5) return {};
|
|
229
240
|
try {
|
|
230
|
-
return JSON.parse(
|
|
241
|
+
return JSON.parse(text5);
|
|
231
242
|
} catch {
|
|
232
|
-
return
|
|
243
|
+
return text5;
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
function sleep(ms) {
|
|
@@ -239,8 +250,8 @@ function sleep(ms) {
|
|
|
239
250
|
// src/utils/helpers.ts
|
|
240
251
|
import path2 from "path";
|
|
241
252
|
import pc2 from "picocolors";
|
|
242
|
-
function splitThreadText(
|
|
243
|
-
return
|
|
253
|
+
function splitThreadText(text5) {
|
|
254
|
+
return text5.split(/\r?\n[ \t]*---[ \t]*\r?\n/).filter((t) => t.trim());
|
|
244
255
|
}
|
|
245
256
|
function sanitizeFilename(filename) {
|
|
246
257
|
const ext = path2.extname(filename).toLowerCase();
|
|
@@ -313,18 +324,26 @@ function formatSocialSetsForDisplay(socialSets) {
|
|
|
313
324
|
// src/commands/drafts.ts
|
|
314
325
|
import fs2 from "fs";
|
|
315
326
|
import pc3 from "picocolors";
|
|
327
|
+
import terminalLink from "terminal-link";
|
|
316
328
|
function enabledPlatforms(draft) {
|
|
317
329
|
if (!draft.platforms) return "";
|
|
318
330
|
return Object.entries(draft.platforms).filter(([, v]) => v.enabled).map(([k]) => k).join(" \xB7 ");
|
|
319
331
|
}
|
|
320
|
-
function firstPostText(draft) {
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
332
|
+
function firstPostText(draft, maxLen = 80) {
|
|
333
|
+
if (draft.text) {
|
|
334
|
+
const t = String(draft.text);
|
|
335
|
+
return t.length > maxLen ? `${t.slice(0, maxLen)}\u2026` : t;
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(draft.posts)) {
|
|
338
|
+
const t = draft.posts[0]?.text;
|
|
339
|
+
if (t) return t.length > maxLen ? `${t.slice(0, maxLen)}\u2026` : t;
|
|
340
|
+
}
|
|
341
|
+
if (draft.platforms) {
|
|
342
|
+
for (const config of Object.values(
|
|
343
|
+
draft.platforms
|
|
344
|
+
)) {
|
|
345
|
+
const t = config.posts?.[0]?.text;
|
|
346
|
+
if (t) return t.length > maxLen ? `${t.slice(0, maxLen)}\u2026` : t;
|
|
328
347
|
}
|
|
329
348
|
}
|
|
330
349
|
return "";
|
|
@@ -368,10 +387,18 @@ function renderDraft(data, verb) {
|
|
|
368
387
|
const id = String(data.id ?? "");
|
|
369
388
|
const status = String(data.status ?? "draft");
|
|
370
389
|
const platforms = enabledPlatforms(data);
|
|
390
|
+
const preview = firstPostText(data);
|
|
371
391
|
const prefix = verb ? `${pc3.green("\u2713")} ${verb} ` : "";
|
|
392
|
+
const draftUrl = String(data.share_url ?? `https://typefully.com/?d=${id}`);
|
|
393
|
+
const linkedId = terminalLink(pc3.bold(id), draftUrl);
|
|
372
394
|
console.log("");
|
|
373
|
-
console.log(` ${prefix}${
|
|
374
|
-
if (
|
|
395
|
+
console.log(` ${prefix}${linkedId} \xB7 ${statusBadge(status)} \xB7 ${pc3.cyan(platforms)}`);
|
|
396
|
+
if (preview) console.log(` ${pc3.dim(preview)}`);
|
|
397
|
+
if (data.scheduled_at) {
|
|
398
|
+
const date = new Date(String(data.scheduled_at)).toLocaleString();
|
|
399
|
+
console.log(` ${pc3.dim("Scheduled:")} ${pc3.yellow(date)}`);
|
|
400
|
+
}
|
|
401
|
+
if (data.share_url) console.log(` ${pc3.dim("Share:")} ${pc3.cyan(String(data.share_url))}`);
|
|
375
402
|
console.log("");
|
|
376
403
|
}
|
|
377
404
|
async function getFirstConnectedPlatform(socialSetId) {
|
|
@@ -403,10 +430,10 @@ function registerDraftsCommand(program2) {
|
|
|
403
430
|
if (opts.status) params.set("status", opts.status);
|
|
404
431
|
if (opts.tag) params.set("tag", opts.tag);
|
|
405
432
|
if (opts.sort) params.set("order_by", opts.sort);
|
|
406
|
-
const
|
|
407
|
-
|
|
433
|
+
const spinner3 = spin("Fetching drafts\u2026");
|
|
434
|
+
spinner3.start();
|
|
408
435
|
const data = await apiRequest("GET", `/social-sets/${id}/drafts?${params}`);
|
|
409
|
-
|
|
436
|
+
spinner3.stop();
|
|
410
437
|
display(data, () => renderDraftsList(data));
|
|
411
438
|
});
|
|
412
439
|
cmd.command("get").description("Get a specific draft").argument("[first_arg]", "social_set_id or draft_id").argument("[second_arg]", "draft_id (when first arg is social_set_id)").option("--social-set-id <id>", "Social set ID via flag (first arg becomes draft_id)").option("--use-default", "Confirm using default social set").action(
|
|
@@ -418,22 +445,22 @@ function registerDraftsCommand(program2) {
|
|
|
418
445
|
!!opts.useDefault,
|
|
419
446
|
opts.socialSetId
|
|
420
447
|
);
|
|
421
|
-
const
|
|
422
|
-
|
|
448
|
+
const spinner3 = spin("Fetching draft\u2026");
|
|
449
|
+
spinner3.start();
|
|
423
450
|
const data = await apiRequest("GET", `/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
424
|
-
|
|
451
|
+
spinner3.stop();
|
|
425
452
|
display(data, () => renderDraft(data));
|
|
426
453
|
}
|
|
427
454
|
);
|
|
428
455
|
cmd.command("create").description("Create a new draft").argument("[social_set_id]", "Social set ID (uses default if omitted)").option("--social-set-id <id>", "Social set ID via flag (overrides positional)").option("--text <text>", "Post content (use --- on its own line for threads)").option("-f, --file <path>", "Read content from file").option("--platform <platforms>", "Comma-separated platforms").option("--all", "Post to all connected platforms").option("--media <media_ids>", "Comma-separated media IDs").option("--title <title>", "Draft title (internal only)").option("--schedule <time>", '"now", "next-free-slot", or ISO datetime').option("--tags <tags>", "Comma-separated tag slugs").option("--reply-to <url>", "URL of X post to reply to").option("--community <id>", "X community ID").option("--share", "Generate a public share URL").option("--scratchpad <text>", "Internal notes/scratchpad").option("--notes <text>", "Internal notes/scratchpad (alias for --scratchpad)").action(async (socialSetId, opts) => {
|
|
429
456
|
const id = resolveSocialSetId(opts.socialSetId ?? socialSetId);
|
|
430
|
-
let
|
|
457
|
+
let text5 = opts.text;
|
|
431
458
|
if (opts.file) {
|
|
432
459
|
const filePath = opts.file;
|
|
433
460
|
if (!fs2.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
434
|
-
|
|
461
|
+
text5 = fs2.readFileSync(filePath, "utf-8");
|
|
435
462
|
}
|
|
436
|
-
if (!
|
|
463
|
+
if (!text5) exitWithError("--text or --file is required");
|
|
437
464
|
if (opts.all && opts.platform) {
|
|
438
465
|
exitWithError("Cannot use both --all and --platform flags");
|
|
439
466
|
}
|
|
@@ -443,13 +470,20 @@ function registerDraftsCommand(program2) {
|
|
|
443
470
|
if (allPlatforms.length === 0) exitWithError("No connected platforms found");
|
|
444
471
|
platformList = [...allPlatforms];
|
|
445
472
|
} else if (opts.platform) {
|
|
446
|
-
platformList = opts.platform.split(",").map((
|
|
473
|
+
platformList = opts.platform.split(",").map((p3) => p3.trim());
|
|
447
474
|
} else {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
|
|
475
|
+
const saved = getDefaultPlatforms();
|
|
476
|
+
if (saved?.length) {
|
|
477
|
+
const connected = await getAllConnectedPlatforms(id);
|
|
478
|
+
platformList = saved.filter((p3) => connected.includes(p3));
|
|
479
|
+
if (platformList.length === 0) platformList = [...connected];
|
|
480
|
+
} else {
|
|
481
|
+
const defaultPlatform = await getFirstConnectedPlatform(id);
|
|
482
|
+
if (!defaultPlatform) exitWithError("No connected platforms found. Specify --platform");
|
|
483
|
+
platformList = [defaultPlatform];
|
|
484
|
+
}
|
|
451
485
|
}
|
|
452
|
-
const posts = splitThreadText(
|
|
486
|
+
const posts = splitThreadText(text5);
|
|
453
487
|
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
454
488
|
const postsArray = posts.map((postText, index) => {
|
|
455
489
|
const post = { text: postText };
|
|
@@ -476,10 +510,10 @@ function registerDraftsCommand(program2) {
|
|
|
476
510
|
if (opts.share) body.share = true;
|
|
477
511
|
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
478
512
|
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
479
|
-
const
|
|
480
|
-
|
|
513
|
+
const spinner3 = spin("Creating draft\u2026");
|
|
514
|
+
spinner3.start();
|
|
481
515
|
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
482
|
-
|
|
516
|
+
spinner3.stop();
|
|
483
517
|
display(data, () => renderDraft(data, "Draft created"));
|
|
484
518
|
});
|
|
485
519
|
cmd.command("update").description("Update an existing draft").argument("[first_arg]", "social_set_id or draft_id").argument("[second_arg]", "draft_id (when first arg is social_set_id)").option("--social-set-id <id>", "Social set ID via flag (first arg becomes draft_id)").option("--text <text>", "New post content").option("-f, --file <path>", "Read content from file").option("--platform <platforms>", "Comma-separated platforms").option("--media <media_ids>", "Comma-separated media IDs").option("-a, --append", "Append to existing thread").option("--title <title>", "New draft title").option("--schedule <time>", '"now", "next-free-slot", or ISO datetime').option("--tags <tags>", "Comma-separated tag slugs").option("--share", "Generate a public share URL").option("--scratchpad <text>", "Internal notes/scratchpad").option("--notes <text>", "Internal notes/scratchpad (alias for --scratchpad)").option("--use-default", "Confirm using default social set").action(
|
|
@@ -491,14 +525,14 @@ function registerDraftsCommand(program2) {
|
|
|
491
525
|
!!opts.useDefault,
|
|
492
526
|
opts.socialSetId
|
|
493
527
|
);
|
|
494
|
-
let
|
|
528
|
+
let text5 = opts.text;
|
|
495
529
|
if (opts.file) {
|
|
496
530
|
const filePath = opts.file;
|
|
497
531
|
if (!fs2.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
498
|
-
|
|
532
|
+
text5 = fs2.readFileSync(filePath, "utf-8");
|
|
499
533
|
}
|
|
500
534
|
const body = {};
|
|
501
|
-
if (
|
|
535
|
+
if (text5) {
|
|
502
536
|
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
503
537
|
const existing = await apiRequest(
|
|
504
538
|
"GET",
|
|
@@ -506,7 +540,7 @@ function registerDraftsCommand(program2) {
|
|
|
506
540
|
);
|
|
507
541
|
let platformList;
|
|
508
542
|
if (opts.platform) {
|
|
509
|
-
platformList = opts.platform.split(",").map((
|
|
543
|
+
platformList = opts.platform.split(",").map((p3) => p3.trim());
|
|
510
544
|
} else {
|
|
511
545
|
const existingPlatforms = existing.platforms;
|
|
512
546
|
platformList = Object.entries(existingPlatforms ?? {}).filter(([, config]) => config.enabled).map(([platform]) => platform);
|
|
@@ -526,11 +560,11 @@ function registerDraftsCommand(program2) {
|
|
|
526
560
|
break;
|
|
527
561
|
}
|
|
528
562
|
}
|
|
529
|
-
const newPost = { text:
|
|
563
|
+
const newPost = { text: text5 };
|
|
530
564
|
if (mediaIds.length > 0) newPost.media_ids = mediaIds;
|
|
531
565
|
postsArray = [...existingPosts, newPost];
|
|
532
566
|
} else {
|
|
533
|
-
const posts = splitThreadText(
|
|
567
|
+
const posts = splitThreadText(text5);
|
|
534
568
|
postsArray = posts.map((postText, index) => {
|
|
535
569
|
const post = { text: postText };
|
|
536
570
|
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
@@ -538,8 +572,8 @@ function registerDraftsCommand(program2) {
|
|
|
538
572
|
});
|
|
539
573
|
}
|
|
540
574
|
const platformsObj = {};
|
|
541
|
-
for (const
|
|
542
|
-
platformsObj[
|
|
575
|
+
for (const p3 of platformList) {
|
|
576
|
+
platformsObj[p3] = { enabled: true, posts: postsArray };
|
|
543
577
|
}
|
|
544
578
|
body.platforms = platformsObj;
|
|
545
579
|
}
|
|
@@ -556,14 +590,14 @@ function registerDraftsCommand(program2) {
|
|
|
556
590
|
"At least one option is required (--text, --file, --title, --schedule, --share, --scratchpad/--notes, or --tags)"
|
|
557
591
|
);
|
|
558
592
|
}
|
|
559
|
-
const
|
|
560
|
-
|
|
593
|
+
const spinner3 = spin("Updating draft\u2026");
|
|
594
|
+
spinner3.start();
|
|
561
595
|
const data = await apiRequest(
|
|
562
596
|
"PATCH",
|
|
563
597
|
`/social-sets/${socialSetId}/drafts/${draftId}`,
|
|
564
598
|
body
|
|
565
599
|
);
|
|
566
|
-
|
|
600
|
+
spinner3.stop();
|
|
567
601
|
display(data, () => renderDraft(data, "Draft updated"));
|
|
568
602
|
}
|
|
569
603
|
);
|
|
@@ -576,10 +610,10 @@ function registerDraftsCommand(program2) {
|
|
|
576
610
|
!!opts.useDefault,
|
|
577
611
|
opts.socialSetId
|
|
578
612
|
);
|
|
579
|
-
const
|
|
580
|
-
|
|
613
|
+
const spinner3 = spin("Deleting draft\u2026");
|
|
614
|
+
spinner3.start();
|
|
581
615
|
await apiRequest("DELETE", `/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
582
|
-
|
|
616
|
+
spinner3.succeed("Draft deleted");
|
|
583
617
|
display({ success: true, message: "Draft deleted" }, () => {
|
|
584
618
|
});
|
|
585
619
|
}
|
|
@@ -594,12 +628,12 @@ function registerDraftsCommand(program2) {
|
|
|
594
628
|
opts.socialSetId
|
|
595
629
|
);
|
|
596
630
|
if (!opts.time) exitWithError('--time is required (use "next-free-slot" or ISO datetime)');
|
|
597
|
-
const
|
|
598
|
-
|
|
631
|
+
const spinner3 = spin("Scheduling draft\u2026");
|
|
632
|
+
spinner3.start();
|
|
599
633
|
const data = await apiRequest("PATCH", `/social-sets/${socialSetId}/drafts/${draftId}`, {
|
|
600
634
|
publish_at: opts.time
|
|
601
635
|
});
|
|
602
|
-
|
|
636
|
+
spinner3.stop();
|
|
603
637
|
display(data, () => renderDraft(data, "Draft scheduled"));
|
|
604
638
|
}
|
|
605
639
|
);
|
|
@@ -612,12 +646,12 @@ function registerDraftsCommand(program2) {
|
|
|
612
646
|
!!opts.useDefault,
|
|
613
647
|
opts.socialSetId
|
|
614
648
|
);
|
|
615
|
-
const
|
|
616
|
-
|
|
649
|
+
const spinner3 = spin("Publishing draft\u2026");
|
|
650
|
+
spinner3.start();
|
|
617
651
|
const data = await apiRequest("PATCH", `/social-sets/${socialSetId}/drafts/${draftId}`, {
|
|
618
652
|
publish_at: "now"
|
|
619
653
|
});
|
|
620
|
-
|
|
654
|
+
spinner3.stop();
|
|
621
655
|
display(data, () => renderDraft(data, "Draft published"));
|
|
622
656
|
}
|
|
623
657
|
);
|
|
@@ -627,15 +661,15 @@ function registerDraftsCommand(program2) {
|
|
|
627
661
|
function registerAliasCommands(program2) {
|
|
628
662
|
program2.command("create-draft").description('Create a draft \u2014 alias for "drafts create" with positional text').argument("[text]", "Draft text (or use --text/--file)").option("--social-set-id <id>", "Social set ID (uses default if omitted)").option("--text <text>", "Post content (overrides positional text)").option("-f, --file <path>", "Read content from file").option("--platform <platforms>", "Comma-separated platforms").option("--all", "Post to all connected platforms").option("--media <media_ids>", "Comma-separated media IDs").option("--title <title>", "Draft title (internal only)").option("--schedule <time>", '"now", "next-free-slot", or ISO datetime').option("--tags <tags>", "Comma-separated tag slugs").option("--reply-to <url>", "URL of X post to reply to").option("--community <id>", "X community ID").option("--share", "Generate a public share URL").option("--scratchpad <text>", "Internal notes/scratchpad").option("--notes <text>", "Internal notes/scratchpad (alias for --scratchpad)").action(async (positionalText, opts) => {
|
|
629
663
|
const id = requireSocialSetId(opts.socialSetId ?? null);
|
|
630
|
-
let
|
|
664
|
+
let text5;
|
|
631
665
|
if (opts.file) {
|
|
632
666
|
const filePath = opts.file;
|
|
633
667
|
if (!fs3.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
634
|
-
|
|
668
|
+
text5 = fs3.readFileSync(filePath, "utf-8");
|
|
635
669
|
} else {
|
|
636
|
-
|
|
670
|
+
text5 = opts.text ?? positionalText;
|
|
637
671
|
}
|
|
638
|
-
if (!
|
|
672
|
+
if (!text5)
|
|
639
673
|
exitWithError("Draft text is required (provide as argument, or use --text/--file)");
|
|
640
674
|
if (opts.all && opts.platform) exitWithError("Cannot use both --all and --platform flags");
|
|
641
675
|
let platformList;
|
|
@@ -644,13 +678,20 @@ function registerAliasCommands(program2) {
|
|
|
644
678
|
if (allPlatforms.length === 0) exitWithError("No connected platforms found");
|
|
645
679
|
platformList = [...allPlatforms];
|
|
646
680
|
} else if (opts.platform) {
|
|
647
|
-
platformList = opts.platform.split(",").map((
|
|
681
|
+
platformList = opts.platform.split(",").map((p3) => p3.trim());
|
|
648
682
|
} else {
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
651
|
-
|
|
683
|
+
const saved = getDefaultPlatforms();
|
|
684
|
+
if (saved?.length) {
|
|
685
|
+
const connected = await getAllConnectedPlatforms(id);
|
|
686
|
+
platformList = saved.filter((p3) => connected.includes(p3));
|
|
687
|
+
if (platformList.length === 0) platformList = [...connected];
|
|
688
|
+
} else {
|
|
689
|
+
const defaultPlatform = await getFirstConnectedPlatform(id);
|
|
690
|
+
if (!defaultPlatform) exitWithError("No connected platforms found. Specify --platform");
|
|
691
|
+
platformList = [defaultPlatform];
|
|
692
|
+
}
|
|
652
693
|
}
|
|
653
|
-
const posts = splitThreadText(
|
|
694
|
+
const posts = splitThreadText(text5);
|
|
654
695
|
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
655
696
|
const postsArray = posts.map((postText, index) => {
|
|
656
697
|
const post = { text: postText };
|
|
@@ -675,25 +716,25 @@ function registerAliasCommands(program2) {
|
|
|
675
716
|
if (opts.share) body.share = true;
|
|
676
717
|
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
677
718
|
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
678
|
-
const
|
|
679
|
-
|
|
719
|
+
const spinner3 = spin("Creating draft\u2026");
|
|
720
|
+
spinner3.start();
|
|
680
721
|
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
681
|
-
|
|
722
|
+
spinner3.stop();
|
|
682
723
|
display(data, () => renderDraft(data, "Draft created"));
|
|
683
724
|
});
|
|
684
725
|
program2.command("update-draft").description('Update a draft \u2014 alias for "drafts update" with positional draft_id').argument("<draft_id>", "Draft ID").argument("[text]", "New draft text (or use --text/--file)").option("--social-set-id <id>", "Social set ID (uses default if omitted)").option("--text <text>", "New post content (overrides positional text)").option("-f, --file <path>", "Read content from file").option("--platform <platforms>", "Comma-separated platforms").option("--media <media_ids>", "Comma-separated media IDs").option("-a, --append", "Append to existing thread").option("--title <title>", "New draft title").option("--schedule <time>", '"now", "next-free-slot", or ISO datetime').option("--tags <tags>", "Comma-separated tag slugs").option("--share", "Generate a public share URL").option("--scratchpad <text>", "Internal notes/scratchpad").option("--notes <text>", "Internal notes/scratchpad (alias for --scratchpad)").action(
|
|
685
726
|
async (draftId, positionalText, opts) => {
|
|
686
727
|
const socialSetId = requireSocialSetId(opts.socialSetId ?? null);
|
|
687
|
-
let
|
|
728
|
+
let text5;
|
|
688
729
|
if (opts.file) {
|
|
689
730
|
const filePath = opts.file;
|
|
690
731
|
if (!fs3.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
691
|
-
|
|
732
|
+
text5 = fs3.readFileSync(filePath, "utf-8");
|
|
692
733
|
} else {
|
|
693
|
-
|
|
734
|
+
text5 = opts.text ?? positionalText;
|
|
694
735
|
}
|
|
695
736
|
const body = {};
|
|
696
|
-
if (
|
|
737
|
+
if (text5) {
|
|
697
738
|
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
698
739
|
const existing = await apiRequest(
|
|
699
740
|
"GET",
|
|
@@ -701,7 +742,7 @@ function registerAliasCommands(program2) {
|
|
|
701
742
|
);
|
|
702
743
|
let platformList;
|
|
703
744
|
if (opts.platform) {
|
|
704
|
-
platformList = opts.platform.split(",").map((
|
|
745
|
+
platformList = opts.platform.split(",").map((p3) => p3.trim());
|
|
705
746
|
} else {
|
|
706
747
|
const existingPlatforms = existing.platforms;
|
|
707
748
|
platformList = Object.entries(existingPlatforms ?? {}).filter(([, config]) => config.enabled).map(([platform]) => platform);
|
|
@@ -721,11 +762,11 @@ function registerAliasCommands(program2) {
|
|
|
721
762
|
break;
|
|
722
763
|
}
|
|
723
764
|
}
|
|
724
|
-
const newPost = { text:
|
|
765
|
+
const newPost = { text: text5 };
|
|
725
766
|
if (mediaIds.length > 0) newPost.media_ids = mediaIds;
|
|
726
767
|
postsArray = [...existingPosts, newPost];
|
|
727
768
|
} else {
|
|
728
|
-
const posts = splitThreadText(
|
|
769
|
+
const posts = splitThreadText(text5);
|
|
729
770
|
postsArray = posts.map((postText, index) => {
|
|
730
771
|
const post = { text: postText };
|
|
731
772
|
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
@@ -733,8 +774,8 @@ function registerAliasCommands(program2) {
|
|
|
733
774
|
});
|
|
734
775
|
}
|
|
735
776
|
const platformsObj = {};
|
|
736
|
-
for (const
|
|
737
|
-
platformsObj[
|
|
777
|
+
for (const p3 of platformList) {
|
|
778
|
+
platformsObj[p3] = { enabled: true, posts: postsArray };
|
|
738
779
|
}
|
|
739
780
|
body.platforms = platformsObj;
|
|
740
781
|
}
|
|
@@ -749,27 +790,97 @@ function registerAliasCommands(program2) {
|
|
|
749
790
|
"At least one option is required (--text, --file, --title, --schedule, --share, --scratchpad/--notes, or --tags)"
|
|
750
791
|
);
|
|
751
792
|
}
|
|
752
|
-
const
|
|
753
|
-
|
|
793
|
+
const spinner3 = spin("Updating draft\u2026");
|
|
794
|
+
spinner3.start();
|
|
754
795
|
const data = await apiRequest(
|
|
755
796
|
"PATCH",
|
|
756
797
|
`/social-sets/${socialSetId}/drafts/${draftId}`,
|
|
757
798
|
body
|
|
758
799
|
);
|
|
759
|
-
|
|
800
|
+
spinner3.stop();
|
|
760
801
|
display(data, () => renderDraft(data, "Draft updated"));
|
|
761
802
|
}
|
|
762
803
|
);
|
|
804
|
+
program2.command("rm").description("Delete a draft \u2014 provide draft_id or pick interactively").argument("[draft_id]", "Draft ID to delete (omit for interactive picker)").option("--social-set-id <id>", "Social set ID (uses default if omitted)").option("--status <status>", "Filter drafts by status in picker (default: draft)").option("--limit <n>", "Max drafts to show in picker (default: 20)").action(async (draftId, opts) => {
|
|
805
|
+
const socialSetId = requireSocialSetId(opts.socialSetId ?? null);
|
|
806
|
+
if (draftId) {
|
|
807
|
+
const spinner3 = spin("Deleting draft\u2026");
|
|
808
|
+
spinner3.start();
|
|
809
|
+
await apiRequest("DELETE", `/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
810
|
+
spinner3.succeed("Draft deleted");
|
|
811
|
+
display({ success: true, message: "Draft deleted" }, () => {
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const fetchSpinner = p.spinner();
|
|
816
|
+
fetchSpinner.start("Loading drafts\u2026");
|
|
817
|
+
const params = new URLSearchParams();
|
|
818
|
+
params.set("limit", String(opts.limit ?? "20"));
|
|
819
|
+
params.set("status", String(opts.status ?? "draft"));
|
|
820
|
+
const data = await apiRequest(
|
|
821
|
+
"GET",
|
|
822
|
+
`/social-sets/${socialSetId}/drafts?${params}`
|
|
823
|
+
);
|
|
824
|
+
const results = data.results ?? [];
|
|
825
|
+
if (results.length === 0) {
|
|
826
|
+
fetchSpinner.stop("No drafts found.");
|
|
827
|
+
p.outro("Nothing to delete.");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const fullDrafts = await Promise.all(
|
|
831
|
+
results.map(
|
|
832
|
+
(draft) => apiRequest("GET", `/social-sets/${socialSetId}/drafts/${draft.id}`).catch(() => draft)
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
fetchSpinner.stop(`${fullDrafts.length} draft${fullDrafts.length !== 1 ? "s" : ""} loaded`);
|
|
836
|
+
const options = fullDrafts.map((draft) => {
|
|
837
|
+
const id = String(draft.id ?? "");
|
|
838
|
+
const shortId = id.slice(0, 8);
|
|
839
|
+
const platforms = draft.platforms ? Object.entries(draft.platforms).filter(([, v]) => v?.enabled !== false).map(([k]) => k).join(" \xB7 ") : "";
|
|
840
|
+
const preview = firstPostText(draft, 60);
|
|
841
|
+
return {
|
|
842
|
+
value: id,
|
|
843
|
+
label: preview || pc4.dim("(no text)"),
|
|
844
|
+
hint: platforms ? `${shortId} \xB7 ${platforms}` : shortId
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
const selected = await p.multiselect({
|
|
848
|
+
message: `Pick drafts to delete ${pc4.dim(`(${results.length} loaded)`)}`,
|
|
849
|
+
options,
|
|
850
|
+
required: true
|
|
851
|
+
});
|
|
852
|
+
if (p.isCancel(selected)) {
|
|
853
|
+
p.cancel("Cancelled.");
|
|
854
|
+
process.exit(0);
|
|
855
|
+
}
|
|
856
|
+
const ids = selected;
|
|
857
|
+
const confirm3 = await p.confirm({
|
|
858
|
+
message: `Delete ${ids.length} draft${ids.length !== 1 ? "s" : ""}?`,
|
|
859
|
+
initialValue: false
|
|
860
|
+
});
|
|
861
|
+
if (p.isCancel(confirm3) || !confirm3) {
|
|
862
|
+
p.cancel("Cancelled.");
|
|
863
|
+
process.exit(0);
|
|
864
|
+
}
|
|
865
|
+
const deleteSpinner = p.spinner();
|
|
866
|
+
deleteSpinner.start(`Deleting ${ids.length} draft${ids.length !== 1 ? "s" : ""}\u2026`);
|
|
867
|
+
for (const id of ids) {
|
|
868
|
+
await apiRequest("DELETE", `/social-sets/${socialSetId}/drafts/${id}`);
|
|
869
|
+
}
|
|
870
|
+
deleteSpinner.stop(
|
|
871
|
+
`${pc4.green("\u2713")} Deleted ${ids.length} draft${ids.length !== 1 ? "s" : ""}`
|
|
872
|
+
);
|
|
873
|
+
});
|
|
763
874
|
}
|
|
764
875
|
|
|
765
876
|
// src/commands/config.ts
|
|
766
877
|
import * as clack2 from "@clack/prompts";
|
|
767
|
-
import
|
|
878
|
+
import pc5 from "picocolors";
|
|
768
879
|
function renderConfigShow(data) {
|
|
769
880
|
if (!data.configured) {
|
|
770
881
|
console.log("");
|
|
771
|
-
console.log(` ${
|
|
772
|
-
console.log(
|
|
882
|
+
console.log(` ${pc5.yellow("\u26A0")} Not configured`);
|
|
883
|
+
console.log(pc5.dim(` Run: typefully setup \xB7 Get key at: ${API_KEY_URL}`));
|
|
773
884
|
console.log("");
|
|
774
885
|
return;
|
|
775
886
|
}
|
|
@@ -777,12 +888,18 @@ function renderConfigShow(data) {
|
|
|
777
888
|
const source = String(data.active_source ?? "");
|
|
778
889
|
const defaultSet = data.default_social_set;
|
|
779
890
|
console.log("");
|
|
780
|
-
console.log(` ${
|
|
781
|
-
console.log(
|
|
891
|
+
console.log(` ${pc5.green("\u2713")} Configured`);
|
|
892
|
+
console.log(pc5.dim(` API Key: ${keyPreview} \xB7 from ${source}`));
|
|
782
893
|
if (defaultSet) {
|
|
783
|
-
console.log(
|
|
894
|
+
console.log(pc5.dim(` Default social set: ${defaultSet.id} \xB7 from ${defaultSet.source}`));
|
|
784
895
|
} else {
|
|
785
|
-
console.log(
|
|
896
|
+
console.log(pc5.dim(" Default social set: not set \xB7 Run: typefully config set-default"));
|
|
897
|
+
}
|
|
898
|
+
const defaultPlatforms = getDefaultPlatforms();
|
|
899
|
+
if (defaultPlatforms) {
|
|
900
|
+
console.log(pc5.dim(` Default platforms: ${defaultPlatforms.join(", ")}`));
|
|
901
|
+
} else {
|
|
902
|
+
console.log(pc5.dim(" Default platforms: not set \xB7 Run: typefully config set-platforms"));
|
|
786
903
|
}
|
|
787
904
|
console.log("");
|
|
788
905
|
}
|
|
@@ -825,10 +942,10 @@ function registerConfigCommand(program2) {
|
|
|
825
942
|
let socialSetId = socialSetIdArg;
|
|
826
943
|
let location = opts.scope ?? opts.location;
|
|
827
944
|
if (!socialSetId) {
|
|
828
|
-
const
|
|
829
|
-
|
|
945
|
+
const spinner3 = spin("Fetching social sets\u2026");
|
|
946
|
+
spinner3.start();
|
|
830
947
|
const socialSets = await apiRequest("GET", "/social-sets?limit=50");
|
|
831
|
-
|
|
948
|
+
spinner3.stop();
|
|
832
949
|
const results = socialSets.results;
|
|
833
950
|
if (!results || results.length === 0) {
|
|
834
951
|
exitWithError("No social sets found. Create one at typefully.com first.");
|
|
@@ -837,10 +954,10 @@ function registerConfigCommand(program2) {
|
|
|
837
954
|
if (formatted.length === 1) {
|
|
838
955
|
socialSetId = formatted[0]?.set.id;
|
|
839
956
|
console.error(
|
|
840
|
-
|
|
957
|
+
pc5.green(`\u2713 Auto-selecting: ${pc5.bold(String(formatted[0]?.set.name || "Unnamed"))}`)
|
|
841
958
|
);
|
|
842
959
|
} else {
|
|
843
|
-
console.error(
|
|
960
|
+
console.error(pc5.bold("Available social sets:"));
|
|
844
961
|
console.error("");
|
|
845
962
|
for (const f of formatted) console.error(f.displayLine);
|
|
846
963
|
console.error("");
|
|
@@ -868,12 +985,12 @@ function registerConfigCommand(program2) {
|
|
|
868
985
|
options: [
|
|
869
986
|
{
|
|
870
987
|
value: "global",
|
|
871
|
-
label: `Global ${
|
|
988
|
+
label: `Global ${pc5.dim("(~/.config/typefully/)")}`,
|
|
872
989
|
hint: "available to all projects"
|
|
873
990
|
},
|
|
874
991
|
{
|
|
875
992
|
value: "local",
|
|
876
|
-
label: `Local ${
|
|
993
|
+
label: `Local ${pc5.dim("(./.typefully/)")}`,
|
|
877
994
|
hint: "only this project"
|
|
878
995
|
}
|
|
879
996
|
]
|
|
@@ -894,19 +1011,178 @@ function registerConfigCommand(program2) {
|
|
|
894
1011
|
};
|
|
895
1012
|
display(result, () => {
|
|
896
1013
|
console.log("");
|
|
897
|
-
console.log(` ${
|
|
898
|
-
console.log(
|
|
1014
|
+
console.log(` ${pc5.green("\u2713")} Default social set saved: ${pc5.bold(String(socialSetId))}`);
|
|
1015
|
+
console.log(pc5.dim(` Config: ${configPath}`));
|
|
899
1016
|
console.log("");
|
|
900
1017
|
});
|
|
901
1018
|
});
|
|
1019
|
+
cmd.command("set-platforms").description("Set default platforms for new drafts").option("--platforms <platforms>", "Comma-separated platforms (skips interactive)").option("--location <location>", "Storage location: global or local").option("--scope <scope>", "Alias for --location").action(async (opts) => {
|
|
1020
|
+
let platformList;
|
|
1021
|
+
if (opts.platforms) {
|
|
1022
|
+
platformList = opts.platforms.split(",").map((p3) => p3.trim());
|
|
1023
|
+
} else {
|
|
1024
|
+
const selected = await clack2.multiselect({
|
|
1025
|
+
message: "Default platforms for new drafts",
|
|
1026
|
+
initialValues: ["x"],
|
|
1027
|
+
options: PLATFORMS.map((plat) => ({ value: plat, label: plat }))
|
|
1028
|
+
});
|
|
1029
|
+
if (clack2.isCancel(selected)) {
|
|
1030
|
+
clack2.cancel("Cancelled.");
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
}
|
|
1033
|
+
platformList = selected;
|
|
1034
|
+
}
|
|
1035
|
+
let location = opts.scope ?? opts.location;
|
|
1036
|
+
if (!location) {
|
|
1037
|
+
const choice = await clack2.select({
|
|
1038
|
+
message: "Where should this be stored?",
|
|
1039
|
+
options: [
|
|
1040
|
+
{
|
|
1041
|
+
value: "global",
|
|
1042
|
+
label: `Global ${pc5.dim("(~/.config/typefully/)")}`,
|
|
1043
|
+
hint: "all projects"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
value: "local",
|
|
1047
|
+
label: `Local ${pc5.dim("(./.typefully/)")}`,
|
|
1048
|
+
hint: "this project only"
|
|
1049
|
+
}
|
|
1050
|
+
]
|
|
1051
|
+
});
|
|
1052
|
+
if (clack2.isCancel(choice)) {
|
|
1053
|
+
clack2.cancel("Cancelled.");
|
|
1054
|
+
process.exit(0);
|
|
1055
|
+
}
|
|
1056
|
+
location = choice;
|
|
1057
|
+
}
|
|
1058
|
+
const isLocal = location === "local";
|
|
1059
|
+
const configPath = isLocal ? getLocalConfigFile() : getGlobalConfigFile();
|
|
1060
|
+
const existingConfig = readConfigFile(configPath) ?? {};
|
|
1061
|
+
writeConfig(configPath, { ...existingConfig, defaultPlatforms: platformList });
|
|
1062
|
+
display(
|
|
1063
|
+
{ success: true, default_platforms: platformList, config_path: configPath },
|
|
1064
|
+
() => {
|
|
1065
|
+
console.log("");
|
|
1066
|
+
console.log(
|
|
1067
|
+
` ${pc5.green("\u2713")} Default platforms saved: ${pc5.bold(platformList.join(", "))}`
|
|
1068
|
+
);
|
|
1069
|
+
console.log(pc5.dim(` Config: ${configPath}`));
|
|
1070
|
+
console.log("");
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/commands/interactive.ts
|
|
1077
|
+
import * as p2 from "@clack/prompts";
|
|
1078
|
+
async function createDraftDirect(text5) {
|
|
1079
|
+
const id = requireSocialSetId(null);
|
|
1080
|
+
let platformList;
|
|
1081
|
+
const saved = getDefaultPlatforms();
|
|
1082
|
+
if (saved?.length) {
|
|
1083
|
+
const connected = await getAllConnectedPlatforms(id);
|
|
1084
|
+
platformList = saved.filter((p3) => connected.includes(p3));
|
|
1085
|
+
if (platformList.length === 0) platformList = [...connected];
|
|
1086
|
+
} else {
|
|
1087
|
+
const platform = await getFirstConnectedPlatform(id);
|
|
1088
|
+
if (!platform) exitWithError("No connected platforms found. Run: typefully setup");
|
|
1089
|
+
platformList = [platform];
|
|
1090
|
+
}
|
|
1091
|
+
const posts = splitThreadText(text5);
|
|
1092
|
+
const postsArray = posts.map((postText) => ({ text: postText }));
|
|
1093
|
+
const platformsObj = {};
|
|
1094
|
+
for (const p3 of platformList) platformsObj[p3] = { enabled: true, posts: postsArray };
|
|
1095
|
+
const body = { platforms: platformsObj };
|
|
1096
|
+
const spinner3 = spin("Creating draft\u2026");
|
|
1097
|
+
spinner3.start();
|
|
1098
|
+
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
1099
|
+
spinner3.stop();
|
|
1100
|
+
display(data, () => renderDraft(data, "Draft created"));
|
|
1101
|
+
}
|
|
1102
|
+
async function createDraftInteractive() {
|
|
1103
|
+
const id = requireSocialSetId(null);
|
|
1104
|
+
const textResult = await p2.text({
|
|
1105
|
+
message: "What do you want to post?",
|
|
1106
|
+
placeholder: "Your post content\u2026 (use --- on a new line to split into a thread)",
|
|
1107
|
+
validate: (v) => !v.trim() ? "Post content is required." : void 0
|
|
1108
|
+
});
|
|
1109
|
+
if (p2.isCancel(textResult)) {
|
|
1110
|
+
p2.cancel("Cancelled.");
|
|
1111
|
+
process.exit(0);
|
|
1112
|
+
}
|
|
1113
|
+
const text5 = textResult;
|
|
1114
|
+
let availablePlatforms = [];
|
|
1115
|
+
try {
|
|
1116
|
+
availablePlatforms = await getAllConnectedPlatforms(id);
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
const savedPlatforms = getDefaultPlatforms();
|
|
1120
|
+
const defaultInitial = savedPlatforms?.length ? availablePlatforms.filter((p3) => savedPlatforms.includes(p3)) : [availablePlatforms[0]];
|
|
1121
|
+
let platformList;
|
|
1122
|
+
if (availablePlatforms.length > 1) {
|
|
1123
|
+
const selected = await p2.multiselect({
|
|
1124
|
+
message: "Post to",
|
|
1125
|
+
initialValues: defaultInitial.length ? defaultInitial : [availablePlatforms[0]],
|
|
1126
|
+
options: availablePlatforms.map((platform) => ({
|
|
1127
|
+
value: platform,
|
|
1128
|
+
label: platform
|
|
1129
|
+
}))
|
|
1130
|
+
});
|
|
1131
|
+
if (p2.isCancel(selected)) {
|
|
1132
|
+
p2.cancel("Cancelled.");
|
|
1133
|
+
process.exit(0);
|
|
1134
|
+
}
|
|
1135
|
+
platformList = selected;
|
|
1136
|
+
} else if (availablePlatforms.length === 1) {
|
|
1137
|
+
platformList = [availablePlatforms[0]];
|
|
1138
|
+
} else {
|
|
1139
|
+
const def = await getFirstConnectedPlatform(id);
|
|
1140
|
+
if (!def) {
|
|
1141
|
+
p2.cancel("No connected platforms found. Run: typefully setup");
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
platformList = [def];
|
|
1145
|
+
}
|
|
1146
|
+
const schedule = await p2.select({
|
|
1147
|
+
message: "When?",
|
|
1148
|
+
options: [
|
|
1149
|
+
{ value: "draft", label: "Save as draft" },
|
|
1150
|
+
{ value: "next-free-slot", label: "Schedule: next free slot" },
|
|
1151
|
+
{ value: "now", label: "Publish now" }
|
|
1152
|
+
]
|
|
1153
|
+
});
|
|
1154
|
+
if (p2.isCancel(schedule)) {
|
|
1155
|
+
p2.cancel("Cancelled.");
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
const posts = splitThreadText(text5);
|
|
1159
|
+
const postsArray = posts.map((postText) => ({ text: postText }));
|
|
1160
|
+
const platformsObj = {};
|
|
1161
|
+
for (const platform of platformList) {
|
|
1162
|
+
platformsObj[platform] = { enabled: true, posts: postsArray };
|
|
1163
|
+
}
|
|
1164
|
+
const body = { platforms: platformsObj };
|
|
1165
|
+
if (schedule !== "draft") body.publish_at = schedule;
|
|
1166
|
+
const s = p2.spinner();
|
|
1167
|
+
s.start("Creating draft\u2026");
|
|
1168
|
+
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
1169
|
+
s.stop("");
|
|
1170
|
+
display(data, () => renderDraft(data, "Draft created"));
|
|
1171
|
+
}
|
|
1172
|
+
async function runDraft(text5) {
|
|
1173
|
+
if (text5) {
|
|
1174
|
+
await createDraftDirect(text5);
|
|
1175
|
+
} else {
|
|
1176
|
+
await createDraftInteractive();
|
|
1177
|
+
}
|
|
902
1178
|
}
|
|
903
1179
|
|
|
904
1180
|
// src/commands/me.ts
|
|
905
|
-
import
|
|
1181
|
+
import pc6 from "picocolors";
|
|
906
1182
|
var KNOWN_ORDER = ["id", "email", "username", "plan", "timezone", "locale", "avatar_url"];
|
|
907
1183
|
function renderMe(data) {
|
|
908
1184
|
console.log("");
|
|
909
|
-
console.log(` ${
|
|
1185
|
+
console.log(` ${pc6.bold(String(data.name ?? "Unknown"))}`);
|
|
910
1186
|
console.log("");
|
|
911
1187
|
const rendered = /* @__PURE__ */ new Set(["name"]);
|
|
912
1188
|
for (const key of KNOWN_ORDER) {
|
|
@@ -914,25 +1190,25 @@ function renderMe(data) {
|
|
|
914
1190
|
const val = data[key];
|
|
915
1191
|
const label = key === "avatar_url" ? "Avatar" : key.replace(/_/g, " ");
|
|
916
1192
|
const display_val = key === "username" ? `@${val}` : String(val);
|
|
917
|
-
console.log(` ${
|
|
1193
|
+
console.log(` ${pc6.dim(`${label}:`).padEnd(22)} ${display_val}`);
|
|
918
1194
|
rendered.add(key);
|
|
919
1195
|
}
|
|
920
1196
|
for (const [key, val] of Object.entries(data)) {
|
|
921
1197
|
if (rendered.has(key) || val == null || val === "") continue;
|
|
922
1198
|
if (typeof val === "object" && !Array.isArray(val)) {
|
|
923
1199
|
const nested = val;
|
|
924
|
-
console.log(` ${
|
|
1200
|
+
console.log(` ${pc6.dim(`${key.replace(/_/g, " ")}:`)}`);
|
|
925
1201
|
for (const [nk, nv] of Object.entries(nested)) {
|
|
926
1202
|
if (nv == null || nv === "") continue;
|
|
927
|
-
console.log(` ${
|
|
1203
|
+
console.log(` ${pc6.dim(`${nk.replace(/_/g, " ")}:`).padEnd(22)} ${nv}`);
|
|
928
1204
|
}
|
|
929
1205
|
} else if (Array.isArray(val)) {
|
|
930
1206
|
if (val.length > 0) {
|
|
931
|
-
console.log(` ${
|
|
1207
|
+
console.log(` ${pc6.dim(`${key.replace(/_/g, " ")}:`)} ${val.join(", ")}`);
|
|
932
1208
|
}
|
|
933
1209
|
} else {
|
|
934
1210
|
const label = key.replace(/_/g, " ");
|
|
935
|
-
console.log(` ${
|
|
1211
|
+
console.log(` ${pc6.dim(`${label}:`).padEnd(22)} ${val}`);
|
|
936
1212
|
}
|
|
937
1213
|
rendered.add(key);
|
|
938
1214
|
}
|
|
@@ -940,10 +1216,10 @@ function renderMe(data) {
|
|
|
940
1216
|
}
|
|
941
1217
|
function registerMeCommand(program2) {
|
|
942
1218
|
program2.command("me").description("Get authenticated user info").action(async () => {
|
|
943
|
-
const
|
|
944
|
-
|
|
1219
|
+
const spinner3 = spin("Fetching user info\u2026");
|
|
1220
|
+
spinner3.start();
|
|
945
1221
|
const data = await apiRequest("GET", "/me");
|
|
946
|
-
|
|
1222
|
+
spinner3.stop();
|
|
947
1223
|
display(data, () => renderMe(data));
|
|
948
1224
|
});
|
|
949
1225
|
}
|
|
@@ -951,7 +1227,7 @@ function registerMeCommand(program2) {
|
|
|
951
1227
|
// src/commands/media.ts
|
|
952
1228
|
import fs4 from "fs";
|
|
953
1229
|
import path3 from "path";
|
|
954
|
-
import
|
|
1230
|
+
import pc7 from "picocolors";
|
|
955
1231
|
function registerMediaCommand(program2) {
|
|
956
1232
|
const cmd = program2.command("media").description("Manage media uploads");
|
|
957
1233
|
cmd.command("upload").description("Upload a media file").argument("<file_path>", "Path to file").argument("[social_set_id]", "Social set ID (uses default if omitted)").option("--social-set-id <id>", "Social set ID via flag").option("--no-wait", "Return immediately after upload").option("--timeout <seconds>", "Max wait for processing (default: 60)").action(
|
|
@@ -968,8 +1244,8 @@ function registerMediaCommand(program2) {
|
|
|
968
1244
|
const n = Number.parseInt(raw, 10);
|
|
969
1245
|
return Number.isFinite(n) && n >= 0 ? n : 2e3;
|
|
970
1246
|
})();
|
|
971
|
-
const
|
|
972
|
-
|
|
1247
|
+
const spinner3 = spin(`Uploading ${pc7.bold(rawFilename)}\u2026`);
|
|
1248
|
+
spinner3.start();
|
|
973
1249
|
const presignedResponse = await apiRequest("POST", `/social-sets/${id}/media/upload`, {
|
|
974
1250
|
file_name: filename
|
|
975
1251
|
});
|
|
@@ -980,26 +1256,26 @@ function registerMediaCommand(program2) {
|
|
|
980
1256
|
const fileBuffer = fs4.readFileSync(filePath);
|
|
981
1257
|
const uploadResponse = await fetch(uploadUrl, { method: "PUT", body: fileBuffer });
|
|
982
1258
|
if (!uploadResponse.ok) {
|
|
983
|
-
|
|
1259
|
+
spinner3.fail("Upload failed");
|
|
984
1260
|
exitWithError("Failed to upload file to S3", {
|
|
985
1261
|
http_code: uploadResponse.status,
|
|
986
1262
|
status_text: uploadResponse.statusText
|
|
987
1263
|
});
|
|
988
1264
|
}
|
|
989
1265
|
if (opts.wait === false) {
|
|
990
|
-
|
|
1266
|
+
spinner3.succeed("Uploaded");
|
|
991
1267
|
display(
|
|
992
1268
|
{ media_id: mediaId, message: "Upload complete. Use media status to check processing." },
|
|
993
1269
|
() => {
|
|
994
1270
|
console.log("");
|
|
995
|
-
console.log(` ${
|
|
996
|
-
console.log(
|
|
1271
|
+
console.log(` ${pc7.green("\u2713")} Uploaded \xB7 ID: ${pc7.bold(mediaId)}`);
|
|
1272
|
+
console.log(pc7.dim(" Run: typefully media status <id> to check processing"));
|
|
997
1273
|
console.log("");
|
|
998
1274
|
}
|
|
999
1275
|
);
|
|
1000
1276
|
return;
|
|
1001
1277
|
}
|
|
1002
|
-
|
|
1278
|
+
spinner3.text = "Processing\u2026";
|
|
1003
1279
|
const startTime = Date.now();
|
|
1004
1280
|
while (Date.now() - startTime < timeout) {
|
|
1005
1281
|
const statusResponse = await apiRequest(
|
|
@@ -1007,21 +1283,21 @@ function registerMediaCommand(program2) {
|
|
|
1007
1283
|
`/social-sets/${id}/media/${mediaId}`
|
|
1008
1284
|
);
|
|
1009
1285
|
if (statusResponse.status === "ready") {
|
|
1010
|
-
|
|
1286
|
+
spinner3.succeed("Media ready");
|
|
1011
1287
|
display({ media_id: mediaId, status: "ready", message: "Media uploaded and ready" }, () => {
|
|
1012
1288
|
console.log("");
|
|
1013
|
-
console.log(` ${
|
|
1289
|
+
console.log(` ${pc7.green("\u2713")} Media ready \xB7 ID: ${pc7.bold(mediaId)}`);
|
|
1014
1290
|
console.log("");
|
|
1015
1291
|
});
|
|
1016
1292
|
return;
|
|
1017
1293
|
}
|
|
1018
1294
|
if (statusResponse.status === "error" || statusResponse.status === "failed") {
|
|
1019
|
-
|
|
1295
|
+
spinner3.fail("Processing failed");
|
|
1020
1296
|
exitWithError("Media processing failed", { status: statusResponse });
|
|
1021
1297
|
}
|
|
1022
1298
|
await sleep(pollIntervalMs);
|
|
1023
1299
|
}
|
|
1024
|
-
|
|
1300
|
+
spinner3.stop();
|
|
1025
1301
|
const timeoutResult = {
|
|
1026
1302
|
media_id: mediaId,
|
|
1027
1303
|
status: "processing",
|
|
@@ -1030,8 +1306,8 @@ function registerMediaCommand(program2) {
|
|
|
1030
1306
|
};
|
|
1031
1307
|
display(timeoutResult, () => {
|
|
1032
1308
|
console.log("");
|
|
1033
|
-
console.log(` ${
|
|
1034
|
-
console.log(
|
|
1309
|
+
console.log(` ${pc7.yellow("\u26A0")} Still processing \xB7 ID: ${pc7.bold(mediaId)}`);
|
|
1310
|
+
console.log(pc7.dim(" Run: typefully media status <id> to check"));
|
|
1035
1311
|
console.log("");
|
|
1036
1312
|
});
|
|
1037
1313
|
}
|
|
@@ -1040,16 +1316,16 @@ function registerMediaCommand(program2) {
|
|
|
1040
1316
|
async (mediaId, socialSetId, opts) => {
|
|
1041
1317
|
const flagId = opts.socialSetId;
|
|
1042
1318
|
const id = requireSocialSetId(flagId ?? socialSetId ?? null);
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1319
|
+
const spinner3 = spin("Checking media status\u2026");
|
|
1320
|
+
spinner3.start();
|
|
1045
1321
|
const data = await apiRequest("GET", `/social-sets/${id}/media/${mediaId}`);
|
|
1046
|
-
|
|
1322
|
+
spinner3.stop();
|
|
1047
1323
|
display(data, () => {
|
|
1048
1324
|
const d = data;
|
|
1049
1325
|
const status = String(d.status ?? "unknown");
|
|
1050
|
-
const icon = status === "ready" ?
|
|
1326
|
+
const icon = status === "ready" ? pc7.green("\u2713") : status === "error" || status === "failed" ? pc7.red("\u2717") : pc7.yellow("\u23F3");
|
|
1051
1327
|
console.log("");
|
|
1052
|
-
console.log(` ${icon} ${
|
|
1328
|
+
console.log(` ${icon} ${pc7.bold(mediaId)} \xB7 ${status}`);
|
|
1053
1329
|
console.log("");
|
|
1054
1330
|
});
|
|
1055
1331
|
}
|
|
@@ -1060,7 +1336,7 @@ function registerMediaCommand(program2) {
|
|
|
1060
1336
|
import fs5 from "fs";
|
|
1061
1337
|
import path4 from "path";
|
|
1062
1338
|
import * as clack3 from "@clack/prompts";
|
|
1063
|
-
import
|
|
1339
|
+
import pc8 from "picocolors";
|
|
1064
1340
|
function registerSetupCommand(program2) {
|
|
1065
1341
|
program2.command("setup").description("Interactive setup \u2014 saves API key and optional default social set").option("--key <api_key>", "Provide key non-interactively").option("--location <location>", "Config location: global or local").option("--scope <scope>", "Alias for --location: global or local").option("--default-social-set <id>", "Set default social set non-interactively").option("--no-default", "Skip setting default social set").action(async (opts) => {
|
|
1066
1342
|
let apiKey = opts.key;
|
|
@@ -1069,9 +1345,9 @@ function registerSetupCommand(program2) {
|
|
|
1069
1345
|
const noDefault = opts.default === false;
|
|
1070
1346
|
const isNonInteractive = !!apiKey;
|
|
1071
1347
|
if (!apiKey) {
|
|
1072
|
-
clack3.intro(
|
|
1073
|
-
console.error(
|
|
1074
|
-
console.error(`${
|
|
1348
|
+
clack3.intro(pc8.bold("Typefully CLI Setup"));
|
|
1349
|
+
console.error(pc8.dim("Sign up free at typefully.com if you don't have an account."));
|
|
1350
|
+
console.error(`${pc8.blue("\u2192")} Get your API key at: ${pc8.cyan(API_KEY_URL)}`);
|
|
1075
1351
|
const keyInput = await clack3.text({
|
|
1076
1352
|
message: "Enter your Typefully API key",
|
|
1077
1353
|
validate: (val = "") => {
|
|
@@ -1091,9 +1367,9 @@ function registerSetupCommand(program2) {
|
|
|
1091
1367
|
options: [
|
|
1092
1368
|
{
|
|
1093
1369
|
value: "global",
|
|
1094
|
-
label: `Global ${
|
|
1370
|
+
label: `Global ${pc8.dim("(~/.config/typefully/)")} \u2014 Available to all projects`
|
|
1095
1371
|
},
|
|
1096
|
-
{ value: "local", label: `Local ${
|
|
1372
|
+
{ value: "local", label: `Local ${pc8.dim("(./.typefully/)")} \u2014 Only this project` }
|
|
1097
1373
|
]
|
|
1098
1374
|
});
|
|
1099
1375
|
if (clack3.isCancel(locationChoice)) process.exit(0);
|
|
@@ -1114,7 +1390,7 @@ function registerSetupCommand(program2) {
|
|
|
1114
1390
|
gitignorePath,
|
|
1115
1391
|
"\n# Typefully config (contains API key)\n.typefully/\n"
|
|
1116
1392
|
);
|
|
1117
|
-
console.error(
|
|
1393
|
+
console.error(pc8.green("\u2713 Added .typefully/ to .gitignore"));
|
|
1118
1394
|
} else {
|
|
1119
1395
|
const addToGitignore = await clack3.confirm({
|
|
1120
1396
|
message: "Add .typefully/ to .gitignore?",
|
|
@@ -1125,16 +1401,16 @@ function registerSetupCommand(program2) {
|
|
|
1125
1401
|
gitignorePath,
|
|
1126
1402
|
"\n# Typefully config (contains API key)\n.typefully/\n"
|
|
1127
1403
|
);
|
|
1128
|
-
console.error(
|
|
1404
|
+
console.error(pc8.green("\u2713 Added .typefully/ to .gitignore"));
|
|
1129
1405
|
}
|
|
1130
1406
|
}
|
|
1131
1407
|
}
|
|
1132
1408
|
} else if (isNonInteractive) {
|
|
1133
1409
|
fs5.writeFileSync(gitignorePath, "# Typefully config (contains API key)\n.typefully/\n");
|
|
1134
|
-
console.error(
|
|
1410
|
+
console.error(pc8.green("\u2713 Created .gitignore with .typefully/ entry"));
|
|
1135
1411
|
} else {
|
|
1136
1412
|
console.error(
|
|
1137
|
-
|
|
1413
|
+
pc8.yellow("\u26A0 No .gitignore found. Your API key could be accidentally committed.")
|
|
1138
1414
|
);
|
|
1139
1415
|
const createGitignore = await clack3.confirm({
|
|
1140
1416
|
message: "Create .gitignore with .typefully/ entry?",
|
|
@@ -1142,11 +1418,11 @@ function registerSetupCommand(program2) {
|
|
|
1142
1418
|
});
|
|
1143
1419
|
if (!clack3.isCancel(createGitignore) && createGitignore) {
|
|
1144
1420
|
fs5.writeFileSync(gitignorePath, "# Typefully config (contains API key)\n.typefully/\n");
|
|
1145
|
-
console.error(
|
|
1421
|
+
console.error(pc8.green("\u2713 Created .gitignore with .typefully/ entry"));
|
|
1146
1422
|
}
|
|
1147
1423
|
}
|
|
1148
1424
|
}
|
|
1149
|
-
console.error(
|
|
1425
|
+
console.error(pc8.green(`\u2713 API key saved to ${pc8.dim(configPath)}`));
|
|
1150
1426
|
let defaultSocialSetId = null;
|
|
1151
1427
|
if (defaultSocialSetArg) {
|
|
1152
1428
|
const origKey = process.env.TYPEFULLY_API_KEY;
|
|
@@ -1165,9 +1441,9 @@ function registerSetupCommand(program2) {
|
|
|
1165
1441
|
defaultSocialSetId = defaultSocialSetArg;
|
|
1166
1442
|
const updatedConfig = readConfigFile(configPath) ?? {};
|
|
1167
1443
|
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1168
|
-
console.error(
|
|
1444
|
+
console.error(pc8.green(`\u2713 Default social set saved: ${defaultSocialSetId}`));
|
|
1169
1445
|
} else if (noDefault) {
|
|
1170
|
-
console.error(
|
|
1446
|
+
console.error(pc8.dim("Skipping default social set configuration."));
|
|
1171
1447
|
} else {
|
|
1172
1448
|
let socialSets = null;
|
|
1173
1449
|
const origKey = process.env.TYPEFULLY_API_KEY;
|
|
@@ -1177,15 +1453,15 @@ function registerSetupCommand(program2) {
|
|
|
1177
1453
|
exitOnError: false
|
|
1178
1454
|
});
|
|
1179
1455
|
} catch (err) {
|
|
1180
|
-
console.error(
|
|
1456
|
+
console.error(pc8.yellow(`\u26A0 Could not fetch social sets: ${err.message}`));
|
|
1181
1457
|
}
|
|
1182
1458
|
if (origKey) process.env.TYPEFULLY_API_KEY = origKey;
|
|
1183
1459
|
else delete process.env.TYPEFULLY_API_KEY;
|
|
1184
1460
|
if (socialSets) {
|
|
1185
1461
|
const results = socialSets.results;
|
|
1186
1462
|
if (!results || results.length === 0) {
|
|
1187
|
-
console.error(
|
|
1188
|
-
console.error(
|
|
1463
|
+
console.error(pc8.yellow("\u26A0 No social sets found."));
|
|
1464
|
+
console.error(pc8.dim("Connect a social account at typefully.com"));
|
|
1189
1465
|
} else if (results.length === 1) {
|
|
1190
1466
|
const firstSet = results[0];
|
|
1191
1467
|
defaultSocialSetId = firstSet.id;
|
|
@@ -1193,19 +1469,19 @@ function registerSetupCommand(program2) {
|
|
|
1193
1469
|
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1194
1470
|
const name = String(firstSet.name || "Unnamed");
|
|
1195
1471
|
const username = firstSet.username ? ` @${firstSet.username}` : "";
|
|
1196
|
-
console.error(
|
|
1472
|
+
console.error(pc8.green(`\u2713 Default social set: ${pc8.bold(name)}${pc8.dim(username)}`));
|
|
1197
1473
|
} else if (isNonInteractive) {
|
|
1198
1474
|
console.error(
|
|
1199
|
-
|
|
1475
|
+
pc8.blue(
|
|
1200
1476
|
`\u2192 Found ${results.length} social sets. Use --default-social-set <id> to set one as default.`
|
|
1201
1477
|
)
|
|
1202
1478
|
);
|
|
1203
1479
|
} else {
|
|
1204
1480
|
const formatted = formatSocialSetsForDisplay(results);
|
|
1205
1481
|
console.error("");
|
|
1206
|
-
console.error(
|
|
1482
|
+
console.error(pc8.bold("Choose a default social set"));
|
|
1207
1483
|
console.error(
|
|
1208
|
-
|
|
1484
|
+
pc8.dim("This will be used when you don't specify one. You can always override it.")
|
|
1209
1485
|
);
|
|
1210
1486
|
console.error("");
|
|
1211
1487
|
for (const f of formatted) console.error(f.displayLine);
|
|
@@ -1219,7 +1495,7 @@ function registerSetupCommand(program2) {
|
|
|
1219
1495
|
defaultSocialSetId = formatted[choiceNum - 1]?.set.id;
|
|
1220
1496
|
const updatedConfig = readConfigFile(configPath) ?? {};
|
|
1221
1497
|
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1222
|
-
console.error(
|
|
1498
|
+
console.error(pc8.green("\u2713 Default social set saved"));
|
|
1223
1499
|
}
|
|
1224
1500
|
}
|
|
1225
1501
|
}
|
|
@@ -1236,36 +1512,36 @@ function registerSetupCommand(program2) {
|
|
|
1236
1512
|
}
|
|
1237
1513
|
|
|
1238
1514
|
// src/commands/social-sets.ts
|
|
1239
|
-
import
|
|
1515
|
+
import pc9 from "picocolors";
|
|
1240
1516
|
var PLATFORM_ORDER = ["x", "linkedin", "threads", "bluesky", "mastodon"];
|
|
1241
1517
|
function renderPlatforms(platforms, indent = " ") {
|
|
1242
|
-
for (const
|
|
1243
|
-
const cfg = platforms[
|
|
1518
|
+
for (const p3 of PLATFORM_ORDER) {
|
|
1519
|
+
const cfg = platforms[p3];
|
|
1244
1520
|
if (!cfg) continue;
|
|
1245
1521
|
const isConnected = cfg.connected !== false;
|
|
1246
|
-
const dot = isConnected ?
|
|
1247
|
-
const handle = cfg.username ?
|
|
1248
|
-
const status = isConnected ? "" :
|
|
1249
|
-
console.log(`${indent}${dot} ${
|
|
1522
|
+
const dot = isConnected ? pc9.green("\u25CF") : pc9.dim("\u25CB");
|
|
1523
|
+
const handle = cfg.username ? pc9.dim(` @${cfg.username}`) : "";
|
|
1524
|
+
const status = isConnected ? "" : pc9.dim(" (not connected)");
|
|
1525
|
+
console.log(`${indent}${dot} ${p3.padEnd(9)}${handle}${status}`);
|
|
1250
1526
|
}
|
|
1251
|
-
for (const [
|
|
1252
|
-
if (PLATFORM_ORDER.includes(
|
|
1527
|
+
for (const [p3, cfg] of Object.entries(platforms)) {
|
|
1528
|
+
if (PLATFORM_ORDER.includes(p3)) continue;
|
|
1253
1529
|
const isConnected = cfg.connected !== false;
|
|
1254
|
-
const dot = isConnected ?
|
|
1255
|
-
const handle = cfg.username ?
|
|
1256
|
-
console.log(`${indent}${dot} ${
|
|
1530
|
+
const dot = isConnected ? pc9.green("\u25CF") : pc9.dim("\u25CB");
|
|
1531
|
+
const handle = cfg.username ? pc9.dim(` @${cfg.username}`) : "";
|
|
1532
|
+
console.log(`${indent}${dot} ${p3.padEnd(9)}${handle}`);
|
|
1257
1533
|
}
|
|
1258
1534
|
}
|
|
1259
1535
|
function renderSocialSetCard(set, index) {
|
|
1260
1536
|
const team = set.team;
|
|
1261
1537
|
const platforms = set.platforms;
|
|
1262
|
-
const prefix = index != null ? `${
|
|
1263
|
-
const name =
|
|
1264
|
-
const username = set.username ?
|
|
1265
|
-
const teamLabel = team ?
|
|
1538
|
+
const prefix = index != null ? `${pc9.dim(`${String(index + 1)}.`)} ` : " ";
|
|
1539
|
+
const name = pc9.bold(String(set.name ?? "Unnamed"));
|
|
1540
|
+
const username = set.username ? pc9.dim(` @${set.username}`) : "";
|
|
1541
|
+
const teamLabel = team ? pc9.dim(` [${team.name}]`) : "";
|
|
1266
1542
|
console.log(`${prefix}${name}${username}${teamLabel}`);
|
|
1267
1543
|
console.log(
|
|
1268
|
-
|
|
1544
|
+
pc9.dim(` ID: ${set.id} \xB7 ${team ? `Team: ${team.name}${team.id ? ` (ID: ${team.id})` : ""}` : "Personal"}`)
|
|
1269
1545
|
);
|
|
1270
1546
|
if (platforms && Object.keys(platforms).length > 0) {
|
|
1271
1547
|
console.log("");
|
|
@@ -1276,12 +1552,12 @@ function renderSocialSetsList(data) {
|
|
|
1276
1552
|
const results = data.results ?? [];
|
|
1277
1553
|
const total = data.total ?? results.length;
|
|
1278
1554
|
if (results.length === 0) {
|
|
1279
|
-
console.log(
|
|
1280
|
-
console.log(
|
|
1555
|
+
console.log(pc9.yellow("\n No social sets found."));
|
|
1556
|
+
console.log(pc9.dim(" Connect a social account at typefully.com\n"));
|
|
1281
1557
|
return;
|
|
1282
1558
|
}
|
|
1283
1559
|
console.log("");
|
|
1284
|
-
console.log(
|
|
1560
|
+
console.log(pc9.dim(` ${total} social set${total !== 1 ? "s" : ""}`));
|
|
1285
1561
|
for (let i = 0; i < results.length; i++) {
|
|
1286
1562
|
console.log("");
|
|
1287
1563
|
renderSocialSetCard(results[i], i);
|
|
@@ -1296,38 +1572,38 @@ function renderSocialSet(data) {
|
|
|
1296
1572
|
function registerSocialSetsCommand(program2) {
|
|
1297
1573
|
const cmd = program2.command("social-sets").description("Manage social sets");
|
|
1298
1574
|
cmd.command("list").description("List all social sets").action(async () => {
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1575
|
+
const spinner3 = spin("Fetching social sets\u2026");
|
|
1576
|
+
spinner3.start();
|
|
1301
1577
|
const data = await apiRequest("GET", "/social-sets?limit=50");
|
|
1302
|
-
|
|
1578
|
+
spinner3.stop();
|
|
1303
1579
|
display(data, () => renderSocialSetsList(data));
|
|
1304
1580
|
});
|
|
1305
1581
|
cmd.command("get").description("Get social set details").argument("[social_set_id]", "Social set ID (uses default if omitted)").action(async (socialSetId) => {
|
|
1306
1582
|
const id = requireSocialSetId(socialSetId ?? null);
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1583
|
+
const spinner3 = spin("Fetching social set\u2026");
|
|
1584
|
+
spinner3.start();
|
|
1309
1585
|
const data = await apiRequest("GET", `/social-sets/${id}`);
|
|
1310
|
-
|
|
1586
|
+
spinner3.stop();
|
|
1311
1587
|
display(data, () => renderSocialSet(data));
|
|
1312
1588
|
});
|
|
1313
1589
|
}
|
|
1314
1590
|
|
|
1315
1591
|
// src/commands/tags.ts
|
|
1316
|
-
import
|
|
1592
|
+
import pc10 from "picocolors";
|
|
1317
1593
|
function renderTagsList(data) {
|
|
1318
1594
|
const results = data.results ?? [];
|
|
1319
1595
|
if (results.length === 0) {
|
|
1320
|
-
console.log(
|
|
1596
|
+
console.log(pc10.yellow("\n No tags found.\n"));
|
|
1321
1597
|
return;
|
|
1322
1598
|
}
|
|
1323
1599
|
console.log("");
|
|
1324
|
-
console.log(
|
|
1600
|
+
console.log(pc10.dim(` ${results.length} tag${results.length !== 1 ? "s" : ""}`));
|
|
1325
1601
|
console.log("");
|
|
1326
1602
|
for (let i = 0; i < results.length; i++) {
|
|
1327
1603
|
const tag = results[i];
|
|
1328
|
-
const num =
|
|
1329
|
-
const name =
|
|
1330
|
-
const slug = tag.slug ?
|
|
1604
|
+
const num = pc10.dim(`${String(i + 1)}.`.padStart(3));
|
|
1605
|
+
const name = pc10.bold(String(tag.name ?? ""));
|
|
1606
|
+
const slug = tag.slug ? pc10.dim(` (${tag.slug})`) : "";
|
|
1331
1607
|
console.log(` ${num} ${name}${slug}`);
|
|
1332
1608
|
}
|
|
1333
1609
|
console.log("");
|
|
@@ -1336,47 +1612,37 @@ function registerTagsCommand(program2) {
|
|
|
1336
1612
|
const cmd = program2.command("tags").description("Manage tags");
|
|
1337
1613
|
cmd.command("list").description("List all tags").argument("[social_set_id]", "Social set ID (uses default if omitted)").action(async (socialSetId) => {
|
|
1338
1614
|
const id = requireSocialSetId(socialSetId ?? null);
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1615
|
+
const spinner3 = spin("Fetching tags\u2026");
|
|
1616
|
+
spinner3.start();
|
|
1341
1617
|
const data = await apiRequest("GET", `/social-sets/${id}/tags?limit=50`);
|
|
1342
|
-
|
|
1618
|
+
spinner3.stop();
|
|
1343
1619
|
display(data, () => renderTagsList(data));
|
|
1344
1620
|
});
|
|
1345
1621
|
cmd.command("create").description("Create a new tag").argument("[social_set_id]", "Social set ID (uses default if omitted)").option("--name <name>", "Tag name (required)").action(async (socialSetId, opts) => {
|
|
1346
1622
|
const id = requireSocialSetId(socialSetId ?? null);
|
|
1347
1623
|
if (!opts.name) exitWithError("--name is required");
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1624
|
+
const spinner3 = spin("Creating tag\u2026");
|
|
1625
|
+
spinner3.start();
|
|
1350
1626
|
const data = await apiRequest("POST", `/social-sets/${id}/tags`, { name: opts.name });
|
|
1351
|
-
|
|
1627
|
+
spinner3.stop();
|
|
1352
1628
|
display(data, () => {
|
|
1353
1629
|
const tag = data;
|
|
1354
1630
|
console.log("");
|
|
1355
|
-
console.log(` ${
|
|
1356
|
-
if (tag.slug) console.log(
|
|
1631
|
+
console.log(` ${pc10.green("\u2713")} Tag created: ${pc10.bold(String(tag.name ?? opts.name))}`);
|
|
1632
|
+
if (tag.slug) console.log(pc10.dim(` Slug: ${tag.slug}`));
|
|
1357
1633
|
console.log("");
|
|
1358
1634
|
});
|
|
1359
1635
|
});
|
|
1360
1636
|
}
|
|
1361
1637
|
|
|
1362
1638
|
// src/utils/banner.ts
|
|
1363
|
-
import
|
|
1364
|
-
var
|
|
1365
|
-
\
|
|
1366
|
-
\
|
|
1367
|
-
\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D
|
|
1368
|
-
\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
|
|
1369
|
-
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551
|
|
1370
|
-
\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D
|
|
1371
|
-
`;
|
|
1372
|
-
var BANNER_COMPACT = `
|
|
1373
|
-
\u2580\u2588\u2580 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588 \u2588 \u2588 \u2588 \u2588\u2584\u2588
|
|
1374
|
-
\u2588 \u2588 \u2588\u2580\u2580 \u2588\u2588\u2584 \u2588\u2580 \u2588\u2584\u2588 \u2588\u2584\u2584 \u2588\u2584\u2584 \u2588
|
|
1639
|
+
import pc11 from "picocolors";
|
|
1640
|
+
var BANNER = `
|
|
1641
|
+
\u281B\u28FF\u281B \u28FF\u28E4\u28FF \u28FF\u281B\u28FF \u28FF\u281B\u281B \u28FF\u281B\u281B \u28FF \u28FF \u28FF \u28FF \u28FF\u28E4\u28FF
|
|
1642
|
+
\u28FF \u28FF \u28FF\u281B\u281B \u28FF\u28FF\u28E4 \u28FF\u281B \u28FF\u28E4\u28FF \u28FF\u28E4\u28E4 \u28FF\u28E4\u28E4 \u28FF
|
|
1375
1643
|
`;
|
|
1376
1644
|
function showBanner() {
|
|
1377
|
-
|
|
1378
|
-
const banner = cols >= 80 ? BANNER_WIDE : BANNER_COMPACT;
|
|
1379
|
-
console.error(pc10.dim(banner));
|
|
1645
|
+
console.error(pc11.dim(BANNER));
|
|
1380
1646
|
}
|
|
1381
1647
|
|
|
1382
1648
|
// src/cli.ts
|
|
@@ -1384,7 +1650,9 @@ var require2 = createRequire(import.meta.url);
|
|
|
1384
1650
|
var pkg = require2("../package.json");
|
|
1385
1651
|
function createCli() {
|
|
1386
1652
|
const program2 = new Command();
|
|
1387
|
-
program2.name("typefully").description("Manage social media posts via the Typefully API").version(pkg.version, "-v, --version").option("-j, --json", "Output raw JSON instead of human-readable text").
|
|
1653
|
+
program2.name("typefully").description("Manage social media posts via the Typefully API").version(pkg.version, "-v, --version").option("-j, --json", "Output raw JSON instead of human-readable text").argument("[text]", "Post text \u2014 creates a draft directly, or omit for interactive mode").action(async (text5) => {
|
|
1654
|
+
await runDraft(text5);
|
|
1655
|
+
}).hook("preAction", (_thisCommand) => {
|
|
1388
1656
|
const jsonMode = !!program2.opts().json;
|
|
1389
1657
|
setJsonMode(jsonMode);
|
|
1390
1658
|
const isVersion = process.argv.includes("-v") || process.argv.includes("--version");
|