typefully 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.js +1411 -0
- package/package.json +40 -0
- package/skills/skill.md +458 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/aliases.ts
|
|
8
|
+
import fs3 from "fs";
|
|
9
|
+
|
|
10
|
+
// src/utils/config.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import * as clack from "@clack/prompts";
|
|
15
|
+
import pc from "picocolors";
|
|
16
|
+
|
|
17
|
+
// src/types.ts
|
|
18
|
+
import { z } from "zod/v4";
|
|
19
|
+
var ConfigSchema = z.object({
|
|
20
|
+
apiKey: z.string().optional(),
|
|
21
|
+
defaultSocialSetId: z.union([z.string(), z.number()]).optional()
|
|
22
|
+
});
|
|
23
|
+
var PLATFORMS = ["x", "linkedin", "threads", "bluesky", "mastodon"];
|
|
24
|
+
|
|
25
|
+
// src/utils/config.ts
|
|
26
|
+
var GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".config", "typefully");
|
|
27
|
+
var GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, "config.json");
|
|
28
|
+
var LOCAL_CONFIG_DIR = ".typefully";
|
|
29
|
+
var LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, "config.json");
|
|
30
|
+
var API_BASE = process.env.TYPEFULLY_API_BASE || "https://api.typefully.com/v2";
|
|
31
|
+
var API_KEY_URL = "https://typefully.com/?settings=api";
|
|
32
|
+
function readConfigFile(configPath) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(configPath)) return null;
|
|
35
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
36
|
+
const parsed = JSON.parse(content);
|
|
37
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
38
|
+
if (!result.success) return null;
|
|
39
|
+
return result.data;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getApiKey() {
|
|
45
|
+
if (process.env.TYPEFULLY_API_KEY) {
|
|
46
|
+
return { source: "environment variable", key: process.env.TYPEFULLY_API_KEY };
|
|
47
|
+
}
|
|
48
|
+
const localPath = path.join(process.cwd(), LOCAL_CONFIG_FILE);
|
|
49
|
+
const localConfig = readConfigFile(localPath);
|
|
50
|
+
if (localConfig?.apiKey) {
|
|
51
|
+
return { source: localPath, key: localConfig.apiKey };
|
|
52
|
+
}
|
|
53
|
+
const globalConfig = readConfigFile(GLOBAL_CONFIG_FILE);
|
|
54
|
+
if (globalConfig?.apiKey) {
|
|
55
|
+
return { source: GLOBAL_CONFIG_FILE, key: globalConfig.apiKey };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async function requireApiKey() {
|
|
60
|
+
const info = getApiKey();
|
|
61
|
+
if (info) return info;
|
|
62
|
+
if (!process.stderr.isTTY) {
|
|
63
|
+
console.error(
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
{ error: "API key not found", hint: "Run: typefully setup", api_key_url: API_KEY_URL },
|
|
66
|
+
null,
|
|
67
|
+
2
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
clack.intro(pc.bgCyan(pc.black(" Typefully ")));
|
|
73
|
+
console.error("");
|
|
74
|
+
console.error(pc.dim(" No API key found. Let's get you set up."));
|
|
75
|
+
console.error(` ${pc.blue("\u2192")} Get your free API key at: ${pc.cyan(API_KEY_URL)}`);
|
|
76
|
+
console.error("");
|
|
77
|
+
const keyInput = await clack.text({
|
|
78
|
+
message: "Paste your Typefully API key",
|
|
79
|
+
validate: (val = "") => {
|
|
80
|
+
if (!val.trim()) return "API key is required";
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (clack.isCancel(keyInput)) {
|
|
84
|
+
clack.outro(pc.dim("Setup cancelled."));
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
const apiKey = keyInput;
|
|
88
|
+
const locationChoice = await clack.select({
|
|
89
|
+
message: "Where should the API key be stored?",
|
|
90
|
+
options: [
|
|
91
|
+
{
|
|
92
|
+
value: "global",
|
|
93
|
+
label: `Global ${pc.dim("(~/.config/typefully/)")}`,
|
|
94
|
+
hint: "available to all projects"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: "local",
|
|
98
|
+
label: `Local ${pc.dim("(./.typefully/)")}`,
|
|
99
|
+
hint: "only this project"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
if (clack.isCancel(locationChoice)) {
|
|
104
|
+
clack.outro(pc.dim("Setup cancelled."));
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
const isLocal = locationChoice === "local";
|
|
108
|
+
const configPath = isLocal ? getLocalConfigFile() : getGlobalConfigFile();
|
|
109
|
+
const existingConfig = readConfigFile(configPath) ?? {};
|
|
110
|
+
writeConfig(configPath, { ...existingConfig, apiKey });
|
|
111
|
+
clack.outro(pc.green(`API key saved. Run ${pc.bold("typefully setup")} for full configuration.`));
|
|
112
|
+
console.error("");
|
|
113
|
+
return { source: configPath, key: apiKey };
|
|
114
|
+
}
|
|
115
|
+
function getDefaultSocialSetId() {
|
|
116
|
+
const localPath = path.join(process.cwd(), LOCAL_CONFIG_FILE);
|
|
117
|
+
const localConfig = readConfigFile(localPath);
|
|
118
|
+
if (localConfig?.defaultSocialSetId != null) {
|
|
119
|
+
return { source: localPath, id: localConfig.defaultSocialSetId };
|
|
120
|
+
}
|
|
121
|
+
const globalConfig = readConfigFile(GLOBAL_CONFIG_FILE);
|
|
122
|
+
if (globalConfig?.defaultSocialSetId != null) {
|
|
123
|
+
return { source: GLOBAL_CONFIG_FILE, id: globalConfig.defaultSocialSetId };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function requireSocialSetId(providedId) {
|
|
128
|
+
if (providedId) return providedId;
|
|
129
|
+
const defaultResult = getDefaultSocialSetId();
|
|
130
|
+
if (defaultResult) return defaultResult.id;
|
|
131
|
+
console.error(
|
|
132
|
+
JSON.stringify(
|
|
133
|
+
{
|
|
134
|
+
error: "social_set_id is required",
|
|
135
|
+
hint: "Run: typefully config set-default to set a default, or provide it as an argument"
|
|
136
|
+
},
|
|
137
|
+
null,
|
|
138
|
+
2
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
function writeConfig(configPath, config) {
|
|
144
|
+
const dir = path.dirname(configPath);
|
|
145
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
147
|
+
`, {
|
|
148
|
+
mode: 384
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function getGlobalConfigFile() {
|
|
152
|
+
return GLOBAL_CONFIG_FILE;
|
|
153
|
+
}
|
|
154
|
+
function getLocalConfigFile() {
|
|
155
|
+
return path.join(process.cwd(), LOCAL_CONFIG_FILE);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/utils/output.ts
|
|
159
|
+
import ora from "ora";
|
|
160
|
+
var _jsonMode = false;
|
|
161
|
+
function setJsonMode(mode) {
|
|
162
|
+
_jsonMode = mode;
|
|
163
|
+
}
|
|
164
|
+
function isJsonMode() {
|
|
165
|
+
return _jsonMode || !process.stdout.isTTY;
|
|
166
|
+
}
|
|
167
|
+
function output(data) {
|
|
168
|
+
console.log(JSON.stringify(data, null, 2));
|
|
169
|
+
}
|
|
170
|
+
function display(data, humanRenderer) {
|
|
171
|
+
if (!humanRenderer || isJsonMode()) {
|
|
172
|
+
output(data);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
humanRenderer();
|
|
176
|
+
}
|
|
177
|
+
var NOOP_SPINNER = {
|
|
178
|
+
text: "",
|
|
179
|
+
start() {
|
|
180
|
+
},
|
|
181
|
+
stop() {
|
|
182
|
+
},
|
|
183
|
+
succeed() {
|
|
184
|
+
},
|
|
185
|
+
fail() {
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
function spin(text4) {
|
|
189
|
+
if (isJsonMode()) return NOOP_SPINNER;
|
|
190
|
+
return ora({ text: text4, color: "cyan" });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/utils/api.ts
|
|
194
|
+
async function apiRequest(method, endpoint, body, opts = {}) {
|
|
195
|
+
const { exitOnError = true } = opts;
|
|
196
|
+
const { key } = await requireApiKey();
|
|
197
|
+
const url = `${opts.baseUrl || API_BASE}${endpoint}`;
|
|
198
|
+
const headers = {
|
|
199
|
+
Authorization: `Bearer ${key}`,
|
|
200
|
+
...opts.headers
|
|
201
|
+
};
|
|
202
|
+
const fetchOpts = { method, headers };
|
|
203
|
+
if (opts.rawBody) {
|
|
204
|
+
fetchOpts.body = opts.rawBody;
|
|
205
|
+
} else if (body) {
|
|
206
|
+
headers["Content-Type"] = "application/json";
|
|
207
|
+
fetchOpts.body = JSON.stringify(body);
|
|
208
|
+
}
|
|
209
|
+
const res = await fetch(url, fetchOpts);
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
const text5 = await res.text().catch(() => "");
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(text5);
|
|
215
|
+
} catch {
|
|
216
|
+
parsed = text5;
|
|
217
|
+
}
|
|
218
|
+
if (exitOnError) {
|
|
219
|
+
console.error(JSON.stringify({ error: `HTTP ${res.status}`, response: parsed }, null, 2));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
223
|
+
err.response = parsed;
|
|
224
|
+
err.status = res.status;
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
const text4 = await res.text();
|
|
228
|
+
if (!text4) return {};
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(text4);
|
|
231
|
+
} catch {
|
|
232
|
+
return text4;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function sleep(ms) {
|
|
236
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/utils/helpers.ts
|
|
240
|
+
import path2 from "path";
|
|
241
|
+
import pc2 from "picocolors";
|
|
242
|
+
function splitThreadText(text4) {
|
|
243
|
+
return text4.split(/\r?\n[ \t]*---[ \t]*\r?\n/).filter((t) => t.trim());
|
|
244
|
+
}
|
|
245
|
+
function sanitizeFilename(filename) {
|
|
246
|
+
const ext = path2.extname(filename).toLowerCase();
|
|
247
|
+
const basename = path2.basename(filename, path2.extname(filename));
|
|
248
|
+
const sanitized = basename.replace(/[^a-zA-Z0-9_.()-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
249
|
+
return `${sanitized || "upload"}${ext}`;
|
|
250
|
+
}
|
|
251
|
+
function parseCsvArg(value, flagName) {
|
|
252
|
+
if (value === true) {
|
|
253
|
+
exitWithError(`${flagName} requires a value`);
|
|
254
|
+
}
|
|
255
|
+
if (value == null) return null;
|
|
256
|
+
if (typeof value !== "string") {
|
|
257
|
+
exitWithError(`${flagName} must be a string`);
|
|
258
|
+
}
|
|
259
|
+
if (value.trim() === "") return [];
|
|
260
|
+
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
function exitWithError(message, details) {
|
|
263
|
+
output({ error: message, ...details });
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
function resolveDraftTarget(firstArg, secondArg, commandName, useDefault, socialSetIdFlag) {
|
|
267
|
+
if (socialSetIdFlag) {
|
|
268
|
+
if (!firstArg) exitWithError("draft_id is required");
|
|
269
|
+
return { socialSetId: socialSetIdFlag, draftId: firstArg };
|
|
270
|
+
}
|
|
271
|
+
if (firstArg && secondArg) {
|
|
272
|
+
return { socialSetId: firstArg, draftId: secondArg };
|
|
273
|
+
}
|
|
274
|
+
if (!firstArg && !secondArg) {
|
|
275
|
+
exitWithError("draft_id is required");
|
|
276
|
+
}
|
|
277
|
+
const singleArg = firstArg || secondArg;
|
|
278
|
+
const defaultResult = getDefaultSocialSetId();
|
|
279
|
+
if (!defaultResult) {
|
|
280
|
+
exitWithError("draft_id is required", {
|
|
281
|
+
hint: "Provide both social_set_id and draft_id, or set a default: typefully config set-default"
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (!useDefault) {
|
|
285
|
+
exitWithError(`Ambiguous arguments for ${commandName}`, {
|
|
286
|
+
hint: `Add --use-default to confirm using default social set (${defaultResult.id}), or provide both arguments.`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return { socialSetId: defaultResult.id, draftId: singleArg };
|
|
290
|
+
}
|
|
291
|
+
function formatSocialSetsForDisplay(socialSets) {
|
|
292
|
+
const personal = socialSets.filter((s) => !s.team);
|
|
293
|
+
const team = socialSets.filter((s) => s.team);
|
|
294
|
+
const teamObj = team;
|
|
295
|
+
const sorted = [
|
|
296
|
+
...personal,
|
|
297
|
+
...teamObj.slice().sort((a, b) => {
|
|
298
|
+
const aTeam = a.team;
|
|
299
|
+
const bTeam = b.team;
|
|
300
|
+
return (aTeam?.name || "").localeCompare(bTeam?.name || "");
|
|
301
|
+
})
|
|
302
|
+
];
|
|
303
|
+
return sorted.map((set, index) => {
|
|
304
|
+
const num = pc2.yellow(`${index + 1}.`.padStart(3));
|
|
305
|
+
const name = pc2.bold(String(set.name || "Unnamed"));
|
|
306
|
+
const username = set.username ? pc2.dim(` @${set.username}`) : "";
|
|
307
|
+
const teamLabel = set.team ? pc2.dim(` [${set.team?.name ?? ""}]`) : "";
|
|
308
|
+
const displayLine = ` ${num} ${name}${username}${teamLabel}`;
|
|
309
|
+
return { set, displayLine, index: index + 1 };
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/commands/drafts.ts
|
|
314
|
+
import fs2 from "fs";
|
|
315
|
+
import pc3 from "picocolors";
|
|
316
|
+
function enabledPlatforms(draft) {
|
|
317
|
+
if (!draft.platforms) return "";
|
|
318
|
+
return Object.entries(draft.platforms).filter(([, v]) => v.enabled).map(([k]) => k).join(" \xB7 ");
|
|
319
|
+
}
|
|
320
|
+
function firstPostText(draft) {
|
|
321
|
+
if (!draft.platforms) return "";
|
|
322
|
+
for (const config of Object.values(
|
|
323
|
+
draft.platforms
|
|
324
|
+
)) {
|
|
325
|
+
if (config.enabled && config.posts?.[0]?.text) {
|
|
326
|
+
const t = config.posts[0].text;
|
|
327
|
+
return t.length > 80 ? `${t.slice(0, 80)}\u2026` : t;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return "";
|
|
331
|
+
}
|
|
332
|
+
function statusBadge(status) {
|
|
333
|
+
switch (status) {
|
|
334
|
+
case "draft":
|
|
335
|
+
return pc3.dim("draft ");
|
|
336
|
+
case "scheduled":
|
|
337
|
+
return pc3.cyan("scheduled");
|
|
338
|
+
case "published":
|
|
339
|
+
return pc3.green("published");
|
|
340
|
+
case "error":
|
|
341
|
+
return pc3.red("error ");
|
|
342
|
+
default:
|
|
343
|
+
return pc3.dim(status.padEnd(9));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function renderDraftsList(data) {
|
|
347
|
+
const results = data.results ?? [];
|
|
348
|
+
if (results.length === 0) {
|
|
349
|
+
console.log(pc3.yellow("\n No drafts found.\n"));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const total = data.total;
|
|
353
|
+
const count = total ?? results.length;
|
|
354
|
+
console.log("");
|
|
355
|
+
console.log(pc3.dim(` ${count} draft${count !== 1 ? "s" : ""}`));
|
|
356
|
+
console.log("");
|
|
357
|
+
for (const draft of results) {
|
|
358
|
+
const id = pc3.dim(String(draft.id ?? "").slice(0, 8));
|
|
359
|
+
const badge = statusBadge(String(draft.status ?? "draft"));
|
|
360
|
+
const platforms = enabledPlatforms(draft);
|
|
361
|
+
const preview = firstPostText(draft);
|
|
362
|
+
console.log(` ${id} ${badge} ${pc3.dim(platforms)}`);
|
|
363
|
+
if (preview) console.log(` ${pc3.dim(preview)}`);
|
|
364
|
+
console.log("");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function renderDraft(data, verb) {
|
|
368
|
+
const id = String(data.id ?? "");
|
|
369
|
+
const status = String(data.status ?? "draft");
|
|
370
|
+
const platforms = enabledPlatforms(data);
|
|
371
|
+
const prefix = verb ? `${pc3.green("\u2713")} ${verb} ` : "";
|
|
372
|
+
console.log("");
|
|
373
|
+
console.log(` ${prefix}${pc3.bold(id)} \xB7 ${status} \xB7 ${pc3.dim(platforms)}`);
|
|
374
|
+
if (data.share_url) console.log(pc3.dim(` Share: ${data.share_url}`));
|
|
375
|
+
console.log("");
|
|
376
|
+
}
|
|
377
|
+
async function getFirstConnectedPlatform(socialSetId) {
|
|
378
|
+
const socialSet = await apiRequest("GET", `/social-sets/${socialSetId}`);
|
|
379
|
+
const platforms = socialSet.platforms ?? {};
|
|
380
|
+
for (const platform of PLATFORMS) {
|
|
381
|
+
if (platforms[platform]) return platform;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
async function getAllConnectedPlatforms(socialSetId) {
|
|
386
|
+
const socialSet = await apiRequest("GET", `/social-sets/${socialSetId}`);
|
|
387
|
+
const platforms = socialSet.platforms ?? {};
|
|
388
|
+
const connected = [];
|
|
389
|
+
for (const platform of PLATFORMS) {
|
|
390
|
+
if (platforms[platform]) connected.push(platform);
|
|
391
|
+
}
|
|
392
|
+
return connected;
|
|
393
|
+
}
|
|
394
|
+
function resolveSocialSetId(socialSetId) {
|
|
395
|
+
return requireSocialSetId(socialSetId ?? null);
|
|
396
|
+
}
|
|
397
|
+
function registerDraftsCommand(program2) {
|
|
398
|
+
const cmd = program2.command("drafts").description("Manage drafts");
|
|
399
|
+
cmd.command("list").description("List drafts").argument("[social_set_id]", "Social set ID (uses default if omitted)").option("--social-set-id <id>", "Social set ID via flag (overrides positional)").option("--status <status>", "Filter by status: draft, scheduled, published, error").option("--tag <tag>", "Filter by tag slug").option("--sort <order>", "Sort order").option("--limit <n>", "Max results (default: 10)").action(async (socialSetId, opts) => {
|
|
400
|
+
const id = resolveSocialSetId(opts.socialSetId ?? socialSetId);
|
|
401
|
+
const params = new URLSearchParams();
|
|
402
|
+
params.set("limit", opts.limit ?? "10");
|
|
403
|
+
if (opts.status) params.set("status", opts.status);
|
|
404
|
+
if (opts.tag) params.set("tag", opts.tag);
|
|
405
|
+
if (opts.sort) params.set("order_by", opts.sort);
|
|
406
|
+
const spinner = spin("Fetching drafts\u2026");
|
|
407
|
+
spinner.start();
|
|
408
|
+
const data = await apiRequest("GET", `/social-sets/${id}/drafts?${params}`);
|
|
409
|
+
spinner.stop();
|
|
410
|
+
display(data, () => renderDraftsList(data));
|
|
411
|
+
});
|
|
412
|
+
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(
|
|
413
|
+
async (firstArg, secondArg, opts) => {
|
|
414
|
+
const { socialSetId, draftId } = resolveDraftTarget(
|
|
415
|
+
firstArg,
|
|
416
|
+
secondArg,
|
|
417
|
+
"drafts get",
|
|
418
|
+
!!opts.useDefault,
|
|
419
|
+
opts.socialSetId
|
|
420
|
+
);
|
|
421
|
+
const spinner = spin("Fetching draft\u2026");
|
|
422
|
+
spinner.start();
|
|
423
|
+
const data = await apiRequest("GET", `/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
424
|
+
spinner.stop();
|
|
425
|
+
display(data, () => renderDraft(data));
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
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
|
+
const id = resolveSocialSetId(opts.socialSetId ?? socialSetId);
|
|
430
|
+
let text4 = opts.text;
|
|
431
|
+
if (opts.file) {
|
|
432
|
+
const filePath = opts.file;
|
|
433
|
+
if (!fs2.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
434
|
+
text4 = fs2.readFileSync(filePath, "utf-8");
|
|
435
|
+
}
|
|
436
|
+
if (!text4) exitWithError("--text or --file is required");
|
|
437
|
+
if (opts.all && opts.platform) {
|
|
438
|
+
exitWithError("Cannot use both --all and --platform flags");
|
|
439
|
+
}
|
|
440
|
+
let platformList;
|
|
441
|
+
if (opts.all) {
|
|
442
|
+
const allPlatforms = await getAllConnectedPlatforms(id);
|
|
443
|
+
if (allPlatforms.length === 0) exitWithError("No connected platforms found");
|
|
444
|
+
platformList = [...allPlatforms];
|
|
445
|
+
} else if (opts.platform) {
|
|
446
|
+
platformList = opts.platform.split(",").map((p) => p.trim());
|
|
447
|
+
} else {
|
|
448
|
+
const defaultPlatform = await getFirstConnectedPlatform(id);
|
|
449
|
+
if (!defaultPlatform) exitWithError("No connected platforms found. Specify --platform");
|
|
450
|
+
platformList = [defaultPlatform];
|
|
451
|
+
}
|
|
452
|
+
const posts = splitThreadText(text4);
|
|
453
|
+
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
454
|
+
const postsArray = posts.map((postText, index) => {
|
|
455
|
+
const post = { text: postText };
|
|
456
|
+
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
457
|
+
return post;
|
|
458
|
+
});
|
|
459
|
+
const platformsObj = {};
|
|
460
|
+
for (const platform of platformList) {
|
|
461
|
+
const platformConfig = { enabled: true, posts: postsArray };
|
|
462
|
+
if (platform === "x" && (opts.replyTo || opts.community)) {
|
|
463
|
+
const settings = {};
|
|
464
|
+
if (opts.replyTo) settings.reply_to_url = opts.replyTo;
|
|
465
|
+
if (opts.community) settings.community_id = opts.community;
|
|
466
|
+
platformConfig.settings = settings;
|
|
467
|
+
}
|
|
468
|
+
platformsObj[platform] = platformConfig;
|
|
469
|
+
}
|
|
470
|
+
const body = { platforms: platformsObj };
|
|
471
|
+
if (opts.title) body.draft_title = opts.title;
|
|
472
|
+
if (opts.schedule) body.publish_at = opts.schedule;
|
|
473
|
+
if (opts.tags !== void 0) {
|
|
474
|
+
body.tags = parseCsvArg(opts.tags, "--tags");
|
|
475
|
+
}
|
|
476
|
+
if (opts.share) body.share = true;
|
|
477
|
+
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
478
|
+
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
479
|
+
const spinner = spin("Creating draft\u2026");
|
|
480
|
+
spinner.start();
|
|
481
|
+
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
482
|
+
spinner.stop();
|
|
483
|
+
display(data, () => renderDraft(data, "Draft created"));
|
|
484
|
+
});
|
|
485
|
+
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(
|
|
486
|
+
async (firstArg, secondArg, opts) => {
|
|
487
|
+
const { socialSetId, draftId } = resolveDraftTarget(
|
|
488
|
+
firstArg,
|
|
489
|
+
secondArg,
|
|
490
|
+
"drafts update",
|
|
491
|
+
!!opts.useDefault,
|
|
492
|
+
opts.socialSetId
|
|
493
|
+
);
|
|
494
|
+
let text4 = opts.text;
|
|
495
|
+
if (opts.file) {
|
|
496
|
+
const filePath = opts.file;
|
|
497
|
+
if (!fs2.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
498
|
+
text4 = fs2.readFileSync(filePath, "utf-8");
|
|
499
|
+
}
|
|
500
|
+
const body = {};
|
|
501
|
+
if (text4) {
|
|
502
|
+
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
503
|
+
const existing = await apiRequest(
|
|
504
|
+
"GET",
|
|
505
|
+
`/social-sets/${socialSetId}/drafts/${draftId}`
|
|
506
|
+
);
|
|
507
|
+
let platformList;
|
|
508
|
+
if (opts.platform) {
|
|
509
|
+
platformList = opts.platform.split(",").map((p) => p.trim());
|
|
510
|
+
} else {
|
|
511
|
+
const existingPlatforms = existing.platforms;
|
|
512
|
+
platformList = Object.entries(existingPlatforms ?? {}).filter(([, config]) => config.enabled).map(([platform]) => platform);
|
|
513
|
+
if (platformList.length === 0) {
|
|
514
|
+
const defaultPlatform = await getFirstConnectedPlatform(socialSetId);
|
|
515
|
+
if (!defaultPlatform) exitWithError("No connected platforms found");
|
|
516
|
+
platformList = [defaultPlatform];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
let postsArray;
|
|
520
|
+
if (opts.append) {
|
|
521
|
+
let existingPosts = [];
|
|
522
|
+
const existingPlatforms = existing.platforms;
|
|
523
|
+
for (const config of Object.values(existingPlatforms ?? {})) {
|
|
524
|
+
if (config.enabled && config.posts) {
|
|
525
|
+
existingPosts = config.posts;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const newPost = { text: text4 };
|
|
530
|
+
if (mediaIds.length > 0) newPost.media_ids = mediaIds;
|
|
531
|
+
postsArray = [...existingPosts, newPost];
|
|
532
|
+
} else {
|
|
533
|
+
const posts = splitThreadText(text4);
|
|
534
|
+
postsArray = posts.map((postText, index) => {
|
|
535
|
+
const post = { text: postText };
|
|
536
|
+
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
537
|
+
return post;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const platformsObj = {};
|
|
541
|
+
for (const p of platformList) {
|
|
542
|
+
platformsObj[p] = { enabled: true, posts: postsArray };
|
|
543
|
+
}
|
|
544
|
+
body.platforms = platformsObj;
|
|
545
|
+
}
|
|
546
|
+
if (opts.title) body.draft_title = opts.title;
|
|
547
|
+
if (opts.schedule) body.publish_at = opts.schedule;
|
|
548
|
+
if (opts.share) body.share = true;
|
|
549
|
+
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
550
|
+
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
551
|
+
if (opts.tags !== void 0) {
|
|
552
|
+
body.tags = parseCsvArg(opts.tags, "--tags");
|
|
553
|
+
}
|
|
554
|
+
if (Object.keys(body).length === 0) {
|
|
555
|
+
exitWithError(
|
|
556
|
+
"At least one option is required (--text, --file, --title, --schedule, --share, --scratchpad/--notes, or --tags)"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const spinner = spin("Updating draft\u2026");
|
|
560
|
+
spinner.start();
|
|
561
|
+
const data = await apiRequest(
|
|
562
|
+
"PATCH",
|
|
563
|
+
`/social-sets/${socialSetId}/drafts/${draftId}`,
|
|
564
|
+
body
|
|
565
|
+
);
|
|
566
|
+
spinner.stop();
|
|
567
|
+
display(data, () => renderDraft(data, "Draft updated"));
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
cmd.command("delete").description("Delete a 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(
|
|
571
|
+
async (firstArg, secondArg, opts) => {
|
|
572
|
+
const { socialSetId, draftId } = resolveDraftTarget(
|
|
573
|
+
firstArg,
|
|
574
|
+
secondArg,
|
|
575
|
+
"drafts delete",
|
|
576
|
+
!!opts.useDefault,
|
|
577
|
+
opts.socialSetId
|
|
578
|
+
);
|
|
579
|
+
const spinner = spin("Deleting draft\u2026");
|
|
580
|
+
spinner.start();
|
|
581
|
+
await apiRequest("DELETE", `/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
582
|
+
spinner.succeed("Draft deleted");
|
|
583
|
+
display({ success: true, message: "Draft deleted" }, () => {
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
cmd.command("schedule").description("Schedule a 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("--time <time>", '"next-free-slot" or ISO datetime (required)').option("--use-default", "Confirm using default social set").action(
|
|
588
|
+
async (firstArg, secondArg, opts) => {
|
|
589
|
+
const { socialSetId, draftId } = resolveDraftTarget(
|
|
590
|
+
firstArg,
|
|
591
|
+
secondArg,
|
|
592
|
+
"drafts schedule",
|
|
593
|
+
!!opts.useDefault,
|
|
594
|
+
opts.socialSetId
|
|
595
|
+
);
|
|
596
|
+
if (!opts.time) exitWithError('--time is required (use "next-free-slot" or ISO datetime)');
|
|
597
|
+
const spinner = spin("Scheduling draft\u2026");
|
|
598
|
+
spinner.start();
|
|
599
|
+
const data = await apiRequest("PATCH", `/social-sets/${socialSetId}/drafts/${draftId}`, {
|
|
600
|
+
publish_at: opts.time
|
|
601
|
+
});
|
|
602
|
+
spinner.stop();
|
|
603
|
+
display(data, () => renderDraft(data, "Draft scheduled"));
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
cmd.command("publish").description("Publish a draft immediately").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(
|
|
607
|
+
async (firstArg, secondArg, opts) => {
|
|
608
|
+
const { socialSetId, draftId } = resolveDraftTarget(
|
|
609
|
+
firstArg,
|
|
610
|
+
secondArg,
|
|
611
|
+
"drafts publish",
|
|
612
|
+
!!opts.useDefault,
|
|
613
|
+
opts.socialSetId
|
|
614
|
+
);
|
|
615
|
+
const spinner = spin("Publishing draft\u2026");
|
|
616
|
+
spinner.start();
|
|
617
|
+
const data = await apiRequest("PATCH", `/social-sets/${socialSetId}/drafts/${draftId}`, {
|
|
618
|
+
publish_at: "now"
|
|
619
|
+
});
|
|
620
|
+
spinner.stop();
|
|
621
|
+
display(data, () => renderDraft(data, "Draft published"));
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/commands/aliases.ts
|
|
627
|
+
function registerAliasCommands(program2) {
|
|
628
|
+
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
|
+
const id = requireSocialSetId(opts.socialSetId ?? null);
|
|
630
|
+
let text4;
|
|
631
|
+
if (opts.file) {
|
|
632
|
+
const filePath = opts.file;
|
|
633
|
+
if (!fs3.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
634
|
+
text4 = fs3.readFileSync(filePath, "utf-8");
|
|
635
|
+
} else {
|
|
636
|
+
text4 = opts.text ?? positionalText;
|
|
637
|
+
}
|
|
638
|
+
if (!text4)
|
|
639
|
+
exitWithError("Draft text is required (provide as argument, or use --text/--file)");
|
|
640
|
+
if (opts.all && opts.platform) exitWithError("Cannot use both --all and --platform flags");
|
|
641
|
+
let platformList;
|
|
642
|
+
if (opts.all) {
|
|
643
|
+
const allPlatforms = await getAllConnectedPlatforms(id);
|
|
644
|
+
if (allPlatforms.length === 0) exitWithError("No connected platforms found");
|
|
645
|
+
platformList = [...allPlatforms];
|
|
646
|
+
} else if (opts.platform) {
|
|
647
|
+
platformList = opts.platform.split(",").map((p) => p.trim());
|
|
648
|
+
} else {
|
|
649
|
+
const defaultPlatform = await getFirstConnectedPlatform(id);
|
|
650
|
+
if (!defaultPlatform) exitWithError("No connected platforms found. Specify --platform");
|
|
651
|
+
platformList = [defaultPlatform];
|
|
652
|
+
}
|
|
653
|
+
const posts = splitThreadText(text4);
|
|
654
|
+
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
655
|
+
const postsArray = posts.map((postText, index) => {
|
|
656
|
+
const post = { text: postText };
|
|
657
|
+
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
658
|
+
return post;
|
|
659
|
+
});
|
|
660
|
+
const platformsObj = {};
|
|
661
|
+
for (const platform of platformList) {
|
|
662
|
+
const platformConfig = { enabled: true, posts: postsArray };
|
|
663
|
+
if (platform === "x" && (opts.replyTo || opts.community)) {
|
|
664
|
+
const settings = {};
|
|
665
|
+
if (opts.replyTo) settings.reply_to_url = opts.replyTo;
|
|
666
|
+
if (opts.community) settings.community_id = opts.community;
|
|
667
|
+
platformConfig.settings = settings;
|
|
668
|
+
}
|
|
669
|
+
platformsObj[platform] = platformConfig;
|
|
670
|
+
}
|
|
671
|
+
const body = { platforms: platformsObj };
|
|
672
|
+
if (opts.title) body.draft_title = opts.title;
|
|
673
|
+
if (opts.schedule) body.publish_at = opts.schedule;
|
|
674
|
+
if (opts.tags !== void 0) body.tags = parseCsvArg(opts.tags, "--tags");
|
|
675
|
+
if (opts.share) body.share = true;
|
|
676
|
+
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
677
|
+
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
678
|
+
const spinner = spin("Creating draft\u2026");
|
|
679
|
+
spinner.start();
|
|
680
|
+
const data = await apiRequest("POST", `/social-sets/${id}/drafts`, body);
|
|
681
|
+
spinner.stop();
|
|
682
|
+
display(data, () => renderDraft(data, "Draft created"));
|
|
683
|
+
});
|
|
684
|
+
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
|
+
async (draftId, positionalText, opts) => {
|
|
686
|
+
const socialSetId = requireSocialSetId(opts.socialSetId ?? null);
|
|
687
|
+
let text4;
|
|
688
|
+
if (opts.file) {
|
|
689
|
+
const filePath = opts.file;
|
|
690
|
+
if (!fs3.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
691
|
+
text4 = fs3.readFileSync(filePath, "utf-8");
|
|
692
|
+
} else {
|
|
693
|
+
text4 = opts.text ?? positionalText;
|
|
694
|
+
}
|
|
695
|
+
const body = {};
|
|
696
|
+
if (text4) {
|
|
697
|
+
const mediaIds = opts.media ? opts.media.split(",").map((m) => m.trim()) : [];
|
|
698
|
+
const existing = await apiRequest(
|
|
699
|
+
"GET",
|
|
700
|
+
`/social-sets/${socialSetId}/drafts/${draftId}`
|
|
701
|
+
);
|
|
702
|
+
let platformList;
|
|
703
|
+
if (opts.platform) {
|
|
704
|
+
platformList = opts.platform.split(",").map((p) => p.trim());
|
|
705
|
+
} else {
|
|
706
|
+
const existingPlatforms = existing.platforms;
|
|
707
|
+
platformList = Object.entries(existingPlatforms ?? {}).filter(([, config]) => config.enabled).map(([platform]) => platform);
|
|
708
|
+
if (platformList.length === 0) {
|
|
709
|
+
const defaultPlatform = await getFirstConnectedPlatform(socialSetId);
|
|
710
|
+
if (!defaultPlatform) exitWithError("No connected platforms found");
|
|
711
|
+
platformList = [defaultPlatform];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
let postsArray;
|
|
715
|
+
if (opts.append) {
|
|
716
|
+
let existingPosts = [];
|
|
717
|
+
const existingPlatforms = existing.platforms;
|
|
718
|
+
for (const config of Object.values(existingPlatforms ?? {})) {
|
|
719
|
+
if (config.enabled && config.posts) {
|
|
720
|
+
existingPosts = config.posts;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const newPost = { text: text4 };
|
|
725
|
+
if (mediaIds.length > 0) newPost.media_ids = mediaIds;
|
|
726
|
+
postsArray = [...existingPosts, newPost];
|
|
727
|
+
} else {
|
|
728
|
+
const posts = splitThreadText(text4);
|
|
729
|
+
postsArray = posts.map((postText, index) => {
|
|
730
|
+
const post = { text: postText };
|
|
731
|
+
if (index === 0 && mediaIds.length > 0) post.media_ids = mediaIds;
|
|
732
|
+
return post;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const platformsObj = {};
|
|
736
|
+
for (const p of platformList) {
|
|
737
|
+
platformsObj[p] = { enabled: true, posts: postsArray };
|
|
738
|
+
}
|
|
739
|
+
body.platforms = platformsObj;
|
|
740
|
+
}
|
|
741
|
+
if (opts.title) body.draft_title = opts.title;
|
|
742
|
+
if (opts.schedule) body.publish_at = opts.schedule;
|
|
743
|
+
if (opts.share) body.share = true;
|
|
744
|
+
const scratchpad = opts.notes ?? opts.scratchpad;
|
|
745
|
+
if (scratchpad) body.scratchpad_text = scratchpad;
|
|
746
|
+
if (opts.tags !== void 0) body.tags = parseCsvArg(opts.tags, "--tags");
|
|
747
|
+
if (Object.keys(body).length === 0) {
|
|
748
|
+
exitWithError(
|
|
749
|
+
"At least one option is required (--text, --file, --title, --schedule, --share, --scratchpad/--notes, or --tags)"
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const spinner = spin("Updating draft\u2026");
|
|
753
|
+
spinner.start();
|
|
754
|
+
const data = await apiRequest(
|
|
755
|
+
"PATCH",
|
|
756
|
+
`/social-sets/${socialSetId}/drafts/${draftId}`,
|
|
757
|
+
body
|
|
758
|
+
);
|
|
759
|
+
spinner.stop();
|
|
760
|
+
display(data, () => renderDraft(data, "Draft updated"));
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/commands/config.ts
|
|
766
|
+
import * as clack2 from "@clack/prompts";
|
|
767
|
+
import pc4 from "picocolors";
|
|
768
|
+
function renderConfigShow(data) {
|
|
769
|
+
if (!data.configured) {
|
|
770
|
+
console.log("");
|
|
771
|
+
console.log(` ${pc4.yellow("\u26A0")} Not configured`);
|
|
772
|
+
console.log(pc4.dim(` Run: typefully setup \xB7 Get key at: ${API_KEY_URL}`));
|
|
773
|
+
console.log("");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const keyPreview = String(data.api_key_preview ?? "");
|
|
777
|
+
const source = String(data.active_source ?? "");
|
|
778
|
+
const defaultSet = data.default_social_set;
|
|
779
|
+
console.log("");
|
|
780
|
+
console.log(` ${pc4.green("\u2713")} Configured`);
|
|
781
|
+
console.log(pc4.dim(` API Key: ${keyPreview} \xB7 from ${source}`));
|
|
782
|
+
if (defaultSet) {
|
|
783
|
+
console.log(pc4.dim(` Default social set: ${defaultSet.id} \xB7 from ${defaultSet.source}`));
|
|
784
|
+
} else {
|
|
785
|
+
console.log(pc4.dim(" Default social set: not set \xB7 Run: typefully config set-default"));
|
|
786
|
+
}
|
|
787
|
+
console.log("");
|
|
788
|
+
}
|
|
789
|
+
function registerConfigCommand(program2) {
|
|
790
|
+
const cmd = program2.command("config").description("Manage CLI configuration");
|
|
791
|
+
cmd.command("show").description("Show current config, API key source, and default social set").action(async () => {
|
|
792
|
+
const result = getApiKey();
|
|
793
|
+
if (!result) {
|
|
794
|
+
const data2 = { configured: false, hint: "Run: typefully setup", api_key_url: API_KEY_URL };
|
|
795
|
+
display(data2, () => renderConfigShow(data2));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const localConfigPath = getLocalConfigFile();
|
|
799
|
+
const globalConfigPath = getGlobalConfigFile();
|
|
800
|
+
const localConfig = readConfigFile(localConfigPath);
|
|
801
|
+
const globalConfig = readConfigFile(globalConfigPath);
|
|
802
|
+
const defaultSocialSet = getDefaultSocialSetId();
|
|
803
|
+
const data = {
|
|
804
|
+
configured: true,
|
|
805
|
+
active_source: result.source,
|
|
806
|
+
api_key_preview: `${result.key.slice(0, 8)}...`,
|
|
807
|
+
default_social_set: defaultSocialSet ? { id: defaultSocialSet.id, source: defaultSocialSet.source } : null,
|
|
808
|
+
config_files: {
|
|
809
|
+
local: localConfig ? {
|
|
810
|
+
path: localConfigPath,
|
|
811
|
+
has_key: !!localConfig.apiKey,
|
|
812
|
+
has_default_social_set: !!localConfig.defaultSocialSetId
|
|
813
|
+
} : null,
|
|
814
|
+
global: globalConfig ? {
|
|
815
|
+
path: globalConfigPath,
|
|
816
|
+
has_key: !!globalConfig.apiKey,
|
|
817
|
+
has_default_social_set: !!globalConfig.defaultSocialSetId
|
|
818
|
+
} : null
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
display(data, () => renderConfigShow(data));
|
|
822
|
+
});
|
|
823
|
+
cmd.command("set-default").description("Set default social set").argument("[social_set_id]", "Social set ID (interactive if omitted)").option("--location <location>", "Storage location: global or local").option("--scope <scope>", "Alias for --location: global or local").action(async (socialSetIdArg, opts) => {
|
|
824
|
+
await requireApiKey();
|
|
825
|
+
let socialSetId = socialSetIdArg;
|
|
826
|
+
let location = opts.scope ?? opts.location;
|
|
827
|
+
if (!socialSetId) {
|
|
828
|
+
const spinner = spin("Fetching social sets\u2026");
|
|
829
|
+
spinner.start();
|
|
830
|
+
const socialSets = await apiRequest("GET", "/social-sets?limit=50");
|
|
831
|
+
spinner.stop();
|
|
832
|
+
const results = socialSets.results;
|
|
833
|
+
if (!results || results.length === 0) {
|
|
834
|
+
exitWithError("No social sets found. Create one at typefully.com first.");
|
|
835
|
+
}
|
|
836
|
+
const formatted = formatSocialSetsForDisplay(results);
|
|
837
|
+
if (formatted.length === 1) {
|
|
838
|
+
socialSetId = formatted[0]?.set.id;
|
|
839
|
+
console.error(
|
|
840
|
+
pc4.green(`\u2713 Auto-selecting: ${pc4.bold(String(formatted[0]?.set.name || "Unnamed"))}`)
|
|
841
|
+
);
|
|
842
|
+
} else {
|
|
843
|
+
console.error(pc4.bold("Available social sets:"));
|
|
844
|
+
console.error("");
|
|
845
|
+
for (const f of formatted) console.error(f.displayLine);
|
|
846
|
+
console.error("");
|
|
847
|
+
const choice = await clack2.text({
|
|
848
|
+
message: "Enter number",
|
|
849
|
+
validate: (val = "") => {
|
|
850
|
+
const num = Number.parseInt(val, 10);
|
|
851
|
+
if (Number.isNaN(num) || num < 1 || num > formatted.length)
|
|
852
|
+
return "Invalid selection";
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
if (clack2.isCancel(choice)) process.exit(0);
|
|
856
|
+
const choiceNum = Number.parseInt(choice, 10);
|
|
857
|
+
socialSetId = formatted[choiceNum - 1]?.set.id;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
await apiRequest("GET", `/social-sets/${socialSetId}`, null, { exitOnError: false });
|
|
862
|
+
} catch {
|
|
863
|
+
exitWithError(`Social set ${socialSetId} not found or not accessible`);
|
|
864
|
+
}
|
|
865
|
+
if (!location) {
|
|
866
|
+
const locationChoice = await clack2.select({
|
|
867
|
+
message: "Where should the default be stored?",
|
|
868
|
+
options: [
|
|
869
|
+
{
|
|
870
|
+
value: "global",
|
|
871
|
+
label: `Global ${pc4.dim("(~/.config/typefully/)")}`,
|
|
872
|
+
hint: "available to all projects"
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
value: "local",
|
|
876
|
+
label: `Local ${pc4.dim("(./.typefully/)")}`,
|
|
877
|
+
hint: "only this project"
|
|
878
|
+
}
|
|
879
|
+
]
|
|
880
|
+
});
|
|
881
|
+
if (clack2.isCancel(locationChoice)) process.exit(0);
|
|
882
|
+
location = locationChoice;
|
|
883
|
+
}
|
|
884
|
+
const isLocal = location === "local";
|
|
885
|
+
const configPath = isLocal ? getLocalConfigFile() : getGlobalConfigFile();
|
|
886
|
+
const existingConfig = readConfigFile(configPath) ?? {};
|
|
887
|
+
writeConfig(configPath, { ...existingConfig, defaultSocialSetId: socialSetId });
|
|
888
|
+
const result = {
|
|
889
|
+
success: true,
|
|
890
|
+
message: "Default social set configured",
|
|
891
|
+
default_social_set_id: socialSetId,
|
|
892
|
+
config_path: configPath,
|
|
893
|
+
scope: isLocal ? "local" : "global"
|
|
894
|
+
};
|
|
895
|
+
display(result, () => {
|
|
896
|
+
console.log("");
|
|
897
|
+
console.log(` ${pc4.green("\u2713")} Default social set saved: ${pc4.bold(String(socialSetId))}`);
|
|
898
|
+
console.log(pc4.dim(` Config: ${configPath}`));
|
|
899
|
+
console.log("");
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/commands/me.ts
|
|
905
|
+
import pc5 from "picocolors";
|
|
906
|
+
var KNOWN_ORDER = ["id", "email", "username", "plan", "timezone", "locale", "avatar_url"];
|
|
907
|
+
function renderMe(data) {
|
|
908
|
+
console.log("");
|
|
909
|
+
console.log(` ${pc5.bold(String(data.name ?? "Unknown"))}`);
|
|
910
|
+
console.log("");
|
|
911
|
+
const rendered = /* @__PURE__ */ new Set(["name"]);
|
|
912
|
+
for (const key of KNOWN_ORDER) {
|
|
913
|
+
if (!(key in data) || data[key] == null || data[key] === "") continue;
|
|
914
|
+
const val = data[key];
|
|
915
|
+
const label = key === "avatar_url" ? "Avatar" : key.replace(/_/g, " ");
|
|
916
|
+
const display_val = key === "username" ? `@${val}` : String(val);
|
|
917
|
+
console.log(` ${pc5.dim(`${label}:`).padEnd(22)} ${display_val}`);
|
|
918
|
+
rendered.add(key);
|
|
919
|
+
}
|
|
920
|
+
for (const [key, val] of Object.entries(data)) {
|
|
921
|
+
if (rendered.has(key) || val == null || val === "") continue;
|
|
922
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
923
|
+
const nested = val;
|
|
924
|
+
console.log(` ${pc5.dim(`${key.replace(/_/g, " ")}:`)}`);
|
|
925
|
+
for (const [nk, nv] of Object.entries(nested)) {
|
|
926
|
+
if (nv == null || nv === "") continue;
|
|
927
|
+
console.log(` ${pc5.dim(`${nk.replace(/_/g, " ")}:`).padEnd(22)} ${nv}`);
|
|
928
|
+
}
|
|
929
|
+
} else if (Array.isArray(val)) {
|
|
930
|
+
if (val.length > 0) {
|
|
931
|
+
console.log(` ${pc5.dim(`${key.replace(/_/g, " ")}:`)} ${val.join(", ")}`);
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
const label = key.replace(/_/g, " ");
|
|
935
|
+
console.log(` ${pc5.dim(`${label}:`).padEnd(22)} ${val}`);
|
|
936
|
+
}
|
|
937
|
+
rendered.add(key);
|
|
938
|
+
}
|
|
939
|
+
console.log("");
|
|
940
|
+
}
|
|
941
|
+
function registerMeCommand(program2) {
|
|
942
|
+
program2.command("me").description("Get authenticated user info").action(async () => {
|
|
943
|
+
const spinner = spin("Fetching user info\u2026");
|
|
944
|
+
spinner.start();
|
|
945
|
+
const data = await apiRequest("GET", "/me");
|
|
946
|
+
spinner.stop();
|
|
947
|
+
display(data, () => renderMe(data));
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/commands/media.ts
|
|
952
|
+
import fs4 from "fs";
|
|
953
|
+
import path3 from "path";
|
|
954
|
+
import pc6 from "picocolors";
|
|
955
|
+
function registerMediaCommand(program2) {
|
|
956
|
+
const cmd = program2.command("media").description("Manage media uploads");
|
|
957
|
+
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(
|
|
958
|
+
async (filePath, socialSetId, opts) => {
|
|
959
|
+
const flagId = opts.socialSetId;
|
|
960
|
+
const id = requireSocialSetId(flagId ?? socialSetId ?? null);
|
|
961
|
+
if (!fs4.existsSync(filePath)) exitWithError(`File not found: ${filePath}`);
|
|
962
|
+
const rawFilename = path3.basename(filePath);
|
|
963
|
+
const filename = sanitizeFilename(rawFilename);
|
|
964
|
+
const timeout = Number.parseInt(String(opts.timeout ?? "60"), 10) * 1e3;
|
|
965
|
+
const pollIntervalMs = (() => {
|
|
966
|
+
const raw = process.env.TYPEFULLY_MEDIA_POLL_INTERVAL_MS;
|
|
967
|
+
if (!raw) return 2e3;
|
|
968
|
+
const n = Number.parseInt(raw, 10);
|
|
969
|
+
return Number.isFinite(n) && n >= 0 ? n : 2e3;
|
|
970
|
+
})();
|
|
971
|
+
const spinner = spin(`Uploading ${pc6.bold(rawFilename)}\u2026`);
|
|
972
|
+
spinner.start();
|
|
973
|
+
const presignedResponse = await apiRequest("POST", `/social-sets/${id}/media/upload`, {
|
|
974
|
+
file_name: filename
|
|
975
|
+
});
|
|
976
|
+
const uploadUrl = presignedResponse.upload_url;
|
|
977
|
+
const mediaId = presignedResponse.media_id;
|
|
978
|
+
if (!uploadUrl)
|
|
979
|
+
exitWithError("Failed to get presigned URL", { response: presignedResponse });
|
|
980
|
+
const fileBuffer = fs4.readFileSync(filePath);
|
|
981
|
+
const uploadResponse = await fetch(uploadUrl, { method: "PUT", body: fileBuffer });
|
|
982
|
+
if (!uploadResponse.ok) {
|
|
983
|
+
spinner.fail("Upload failed");
|
|
984
|
+
exitWithError("Failed to upload file to S3", {
|
|
985
|
+
http_code: uploadResponse.status,
|
|
986
|
+
status_text: uploadResponse.statusText
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
if (opts.wait === false) {
|
|
990
|
+
spinner.succeed("Uploaded");
|
|
991
|
+
display(
|
|
992
|
+
{ media_id: mediaId, message: "Upload complete. Use media status to check processing." },
|
|
993
|
+
() => {
|
|
994
|
+
console.log("");
|
|
995
|
+
console.log(` ${pc6.green("\u2713")} Uploaded \xB7 ID: ${pc6.bold(mediaId)}`);
|
|
996
|
+
console.log(pc6.dim(" Run: typefully media status <id> to check processing"));
|
|
997
|
+
console.log("");
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
spinner.text = "Processing\u2026";
|
|
1003
|
+
const startTime = Date.now();
|
|
1004
|
+
while (Date.now() - startTime < timeout) {
|
|
1005
|
+
const statusResponse = await apiRequest(
|
|
1006
|
+
"GET",
|
|
1007
|
+
`/social-sets/${id}/media/${mediaId}`
|
|
1008
|
+
);
|
|
1009
|
+
if (statusResponse.status === "ready") {
|
|
1010
|
+
spinner.succeed("Media ready");
|
|
1011
|
+
display({ media_id: mediaId, status: "ready", message: "Media uploaded and ready" }, () => {
|
|
1012
|
+
console.log("");
|
|
1013
|
+
console.log(` ${pc6.green("\u2713")} Media ready \xB7 ID: ${pc6.bold(mediaId)}`);
|
|
1014
|
+
console.log("");
|
|
1015
|
+
});
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (statusResponse.status === "error" || statusResponse.status === "failed") {
|
|
1019
|
+
spinner.fail("Processing failed");
|
|
1020
|
+
exitWithError("Media processing failed", { status: statusResponse });
|
|
1021
|
+
}
|
|
1022
|
+
await sleep(pollIntervalMs);
|
|
1023
|
+
}
|
|
1024
|
+
spinner.stop();
|
|
1025
|
+
const timeoutResult = {
|
|
1026
|
+
media_id: mediaId,
|
|
1027
|
+
status: "processing",
|
|
1028
|
+
message: "Upload complete but still processing. Use media status to check.",
|
|
1029
|
+
hint: "Increase timeout with --timeout <seconds>"
|
|
1030
|
+
};
|
|
1031
|
+
display(timeoutResult, () => {
|
|
1032
|
+
console.log("");
|
|
1033
|
+
console.log(` ${pc6.yellow("\u26A0")} Still processing \xB7 ID: ${pc6.bold(mediaId)}`);
|
|
1034
|
+
console.log(pc6.dim(" Run: typefully media status <id> to check"));
|
|
1035
|
+
console.log("");
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
cmd.command("status").description("Check media upload status").argument("<media_id>", "Media ID").argument("[social_set_id]", "Social set ID (uses default if omitted)").option("--social-set-id <id>", "Social set ID via flag").action(
|
|
1040
|
+
async (mediaId, socialSetId, opts) => {
|
|
1041
|
+
const flagId = opts.socialSetId;
|
|
1042
|
+
const id = requireSocialSetId(flagId ?? socialSetId ?? null);
|
|
1043
|
+
const spinner = spin("Checking media status\u2026");
|
|
1044
|
+
spinner.start();
|
|
1045
|
+
const data = await apiRequest("GET", `/social-sets/${id}/media/${mediaId}`);
|
|
1046
|
+
spinner.stop();
|
|
1047
|
+
display(data, () => {
|
|
1048
|
+
const d = data;
|
|
1049
|
+
const status = String(d.status ?? "unknown");
|
|
1050
|
+
const icon = status === "ready" ? pc6.green("\u2713") : status === "error" || status === "failed" ? pc6.red("\u2717") : pc6.yellow("\u23F3");
|
|
1051
|
+
console.log("");
|
|
1052
|
+
console.log(` ${icon} ${pc6.bold(mediaId)} \xB7 ${status}`);
|
|
1053
|
+
console.log("");
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/commands/setup.ts
|
|
1060
|
+
import fs5 from "fs";
|
|
1061
|
+
import path4 from "path";
|
|
1062
|
+
import * as clack3 from "@clack/prompts";
|
|
1063
|
+
import pc7 from "picocolors";
|
|
1064
|
+
function registerSetupCommand(program2) {
|
|
1065
|
+
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
|
+
let apiKey = opts.key;
|
|
1067
|
+
let location = opts.scope ?? opts.location;
|
|
1068
|
+
const defaultSocialSetArg = opts.defaultSocialSet;
|
|
1069
|
+
const noDefault = opts.default === false;
|
|
1070
|
+
const isNonInteractive = !!apiKey;
|
|
1071
|
+
if (!apiKey) {
|
|
1072
|
+
clack3.intro(pc7.bold("Typefully CLI Setup"));
|
|
1073
|
+
console.error(pc7.dim("Sign up free at typefully.com if you don't have an account."));
|
|
1074
|
+
console.error(`${pc7.blue("\u2192")} Get your API key at: ${pc7.cyan(API_KEY_URL)}`);
|
|
1075
|
+
const keyInput = await clack3.text({
|
|
1076
|
+
message: "Enter your Typefully API key",
|
|
1077
|
+
validate: (val = "") => {
|
|
1078
|
+
if (!val.trim()) return "API key is required";
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
if (clack3.isCancel(keyInput)) process.exit(0);
|
|
1082
|
+
apiKey = keyInput;
|
|
1083
|
+
}
|
|
1084
|
+
if (!apiKey) exitWithError("API key is required");
|
|
1085
|
+
if (!location) {
|
|
1086
|
+
if (isNonInteractive) {
|
|
1087
|
+
location = "global";
|
|
1088
|
+
} else {
|
|
1089
|
+
const locationChoice = await clack3.select({
|
|
1090
|
+
message: "Where should the API key be stored?",
|
|
1091
|
+
options: [
|
|
1092
|
+
{
|
|
1093
|
+
value: "global",
|
|
1094
|
+
label: `Global ${pc7.dim("(~/.config/typefully/)")} \u2014 Available to all projects`
|
|
1095
|
+
},
|
|
1096
|
+
{ value: "local", label: `Local ${pc7.dim("(./.typefully/)")} \u2014 Only this project` }
|
|
1097
|
+
]
|
|
1098
|
+
});
|
|
1099
|
+
if (clack3.isCancel(locationChoice)) process.exit(0);
|
|
1100
|
+
location = locationChoice;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const isLocal = location === "local" || location === "2";
|
|
1104
|
+
const configPath = isLocal ? getLocalConfigFile() : getGlobalConfigFile();
|
|
1105
|
+
const existingConfig = readConfigFile(configPath) ?? {};
|
|
1106
|
+
writeConfig(configPath, { ...existingConfig, apiKey });
|
|
1107
|
+
if (isLocal) {
|
|
1108
|
+
const gitignorePath = path4.join(process.cwd(), ".gitignore");
|
|
1109
|
+
if (fs5.existsSync(gitignorePath)) {
|
|
1110
|
+
const gitignore = fs5.readFileSync(gitignorePath, "utf-8");
|
|
1111
|
+
if (!gitignore.includes(".typefully/") && !gitignore.includes(".typefully\n")) {
|
|
1112
|
+
if (isNonInteractive) {
|
|
1113
|
+
fs5.appendFileSync(
|
|
1114
|
+
gitignorePath,
|
|
1115
|
+
"\n# Typefully config (contains API key)\n.typefully/\n"
|
|
1116
|
+
);
|
|
1117
|
+
console.error(pc7.green("\u2713 Added .typefully/ to .gitignore"));
|
|
1118
|
+
} else {
|
|
1119
|
+
const addToGitignore = await clack3.confirm({
|
|
1120
|
+
message: "Add .typefully/ to .gitignore?",
|
|
1121
|
+
initialValue: true
|
|
1122
|
+
});
|
|
1123
|
+
if (!clack3.isCancel(addToGitignore) && addToGitignore) {
|
|
1124
|
+
fs5.appendFileSync(
|
|
1125
|
+
gitignorePath,
|
|
1126
|
+
"\n# Typefully config (contains API key)\n.typefully/\n"
|
|
1127
|
+
);
|
|
1128
|
+
console.error(pc7.green("\u2713 Added .typefully/ to .gitignore"));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} else if (isNonInteractive) {
|
|
1133
|
+
fs5.writeFileSync(gitignorePath, "# Typefully config (contains API key)\n.typefully/\n");
|
|
1134
|
+
console.error(pc7.green("\u2713 Created .gitignore with .typefully/ entry"));
|
|
1135
|
+
} else {
|
|
1136
|
+
console.error(
|
|
1137
|
+
pc7.yellow("\u26A0 No .gitignore found. Your API key could be accidentally committed.")
|
|
1138
|
+
);
|
|
1139
|
+
const createGitignore = await clack3.confirm({
|
|
1140
|
+
message: "Create .gitignore with .typefully/ entry?",
|
|
1141
|
+
initialValue: true
|
|
1142
|
+
});
|
|
1143
|
+
if (!clack3.isCancel(createGitignore) && createGitignore) {
|
|
1144
|
+
fs5.writeFileSync(gitignorePath, "# Typefully config (contains API key)\n.typefully/\n");
|
|
1145
|
+
console.error(pc7.green("\u2713 Created .gitignore with .typefully/ entry"));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
console.error(pc7.green(`\u2713 API key saved to ${pc7.dim(configPath)}`));
|
|
1150
|
+
let defaultSocialSetId = null;
|
|
1151
|
+
if (defaultSocialSetArg) {
|
|
1152
|
+
const origKey = process.env.TYPEFULLY_API_KEY;
|
|
1153
|
+
process.env.TYPEFULLY_API_KEY = apiKey;
|
|
1154
|
+
try {
|
|
1155
|
+
await apiRequest("GET", `/social-sets/${defaultSocialSetArg}`, null, {
|
|
1156
|
+
exitOnError: false
|
|
1157
|
+
});
|
|
1158
|
+
} catch {
|
|
1159
|
+
if (origKey) process.env.TYPEFULLY_API_KEY = origKey;
|
|
1160
|
+
else delete process.env.TYPEFULLY_API_KEY;
|
|
1161
|
+
exitWithError(`Social set ${defaultSocialSetArg} not found or not accessible`);
|
|
1162
|
+
}
|
|
1163
|
+
if (origKey) process.env.TYPEFULLY_API_KEY = origKey;
|
|
1164
|
+
else delete process.env.TYPEFULLY_API_KEY;
|
|
1165
|
+
defaultSocialSetId = defaultSocialSetArg;
|
|
1166
|
+
const updatedConfig = readConfigFile(configPath) ?? {};
|
|
1167
|
+
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1168
|
+
console.error(pc7.green(`\u2713 Default social set saved: ${defaultSocialSetId}`));
|
|
1169
|
+
} else if (noDefault) {
|
|
1170
|
+
console.error(pc7.dim("Skipping default social set configuration."));
|
|
1171
|
+
} else {
|
|
1172
|
+
let socialSets = null;
|
|
1173
|
+
const origKey = process.env.TYPEFULLY_API_KEY;
|
|
1174
|
+
process.env.TYPEFULLY_API_KEY = apiKey;
|
|
1175
|
+
try {
|
|
1176
|
+
socialSets = await apiRequest("GET", "/social-sets?limit=50", null, {
|
|
1177
|
+
exitOnError: false
|
|
1178
|
+
});
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
console.error(pc7.yellow(`\u26A0 Could not fetch social sets: ${err.message}`));
|
|
1181
|
+
}
|
|
1182
|
+
if (origKey) process.env.TYPEFULLY_API_KEY = origKey;
|
|
1183
|
+
else delete process.env.TYPEFULLY_API_KEY;
|
|
1184
|
+
if (socialSets) {
|
|
1185
|
+
const results = socialSets.results;
|
|
1186
|
+
if (!results || results.length === 0) {
|
|
1187
|
+
console.error(pc7.yellow("\u26A0 No social sets found."));
|
|
1188
|
+
console.error(pc7.dim("Connect a social account at typefully.com"));
|
|
1189
|
+
} else if (results.length === 1) {
|
|
1190
|
+
const firstSet = results[0];
|
|
1191
|
+
defaultSocialSetId = firstSet.id;
|
|
1192
|
+
const updatedConfig = readConfigFile(configPath) ?? {};
|
|
1193
|
+
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1194
|
+
const name = String(firstSet.name || "Unnamed");
|
|
1195
|
+
const username = firstSet.username ? ` @${firstSet.username}` : "";
|
|
1196
|
+
console.error(pc7.green(`\u2713 Default social set: ${pc7.bold(name)}${pc7.dim(username)}`));
|
|
1197
|
+
} else if (isNonInteractive) {
|
|
1198
|
+
console.error(
|
|
1199
|
+
pc7.blue(
|
|
1200
|
+
`\u2192 Found ${results.length} social sets. Use --default-social-set <id> to set one as default.`
|
|
1201
|
+
)
|
|
1202
|
+
);
|
|
1203
|
+
} else {
|
|
1204
|
+
const formatted = formatSocialSetsForDisplay(results);
|
|
1205
|
+
console.error("");
|
|
1206
|
+
console.error(pc7.bold("Choose a default social set"));
|
|
1207
|
+
console.error(
|
|
1208
|
+
pc7.dim("This will be used when you don't specify one. You can always override it.")
|
|
1209
|
+
);
|
|
1210
|
+
console.error("");
|
|
1211
|
+
for (const f of formatted) console.error(f.displayLine);
|
|
1212
|
+
console.error("");
|
|
1213
|
+
const choice = await clack3.text({
|
|
1214
|
+
message: "Enter number (or press Enter to skip)"
|
|
1215
|
+
});
|
|
1216
|
+
if (!clack3.isCancel(choice) && choice) {
|
|
1217
|
+
const choiceNum = Number.parseInt(choice, 10);
|
|
1218
|
+
if (!Number.isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= formatted.length) {
|
|
1219
|
+
defaultSocialSetId = formatted[choiceNum - 1]?.set.id;
|
|
1220
|
+
const updatedConfig = readConfigFile(configPath) ?? {};
|
|
1221
|
+
writeConfig(configPath, { ...updatedConfig, defaultSocialSetId });
|
|
1222
|
+
console.error(pc7.green("\u2713 Default social set saved"));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
output({
|
|
1229
|
+
success: true,
|
|
1230
|
+
message: "Setup complete",
|
|
1231
|
+
config_path: configPath,
|
|
1232
|
+
scope: isLocal ? "local" : "global",
|
|
1233
|
+
default_social_set_id: defaultSocialSetId
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/commands/social-sets.ts
|
|
1239
|
+
import pc8 from "picocolors";
|
|
1240
|
+
var PLATFORM_ORDER = ["x", "linkedin", "threads", "bluesky", "mastodon"];
|
|
1241
|
+
function renderPlatforms(platforms, indent = " ") {
|
|
1242
|
+
for (const p of PLATFORM_ORDER) {
|
|
1243
|
+
const cfg = platforms[p];
|
|
1244
|
+
if (!cfg) continue;
|
|
1245
|
+
const isConnected = cfg.connected !== false;
|
|
1246
|
+
const dot = isConnected ? pc8.green("\u25CF") : pc8.dim("\u25CB");
|
|
1247
|
+
const handle = cfg.username ? pc8.dim(` @${cfg.username}`) : "";
|
|
1248
|
+
const status = isConnected ? "" : pc8.dim(" (not connected)");
|
|
1249
|
+
console.log(`${indent}${dot} ${p.padEnd(9)}${handle}${status}`);
|
|
1250
|
+
}
|
|
1251
|
+
for (const [p, cfg] of Object.entries(platforms)) {
|
|
1252
|
+
if (PLATFORM_ORDER.includes(p)) continue;
|
|
1253
|
+
const isConnected = cfg.connected !== false;
|
|
1254
|
+
const dot = isConnected ? pc8.green("\u25CF") : pc8.dim("\u25CB");
|
|
1255
|
+
const handle = cfg.username ? pc8.dim(` @${cfg.username}`) : "";
|
|
1256
|
+
console.log(`${indent}${dot} ${p.padEnd(9)}${handle}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function renderSocialSetCard(set, index) {
|
|
1260
|
+
const team = set.team;
|
|
1261
|
+
const platforms = set.platforms;
|
|
1262
|
+
const prefix = index != null ? `${pc8.dim(`${String(index + 1)}.`)} ` : " ";
|
|
1263
|
+
const name = pc8.bold(String(set.name ?? "Unnamed"));
|
|
1264
|
+
const username = set.username ? pc8.dim(` @${set.username}`) : "";
|
|
1265
|
+
const teamLabel = team ? pc8.dim(` [${team.name}]`) : "";
|
|
1266
|
+
console.log(`${prefix}${name}${username}${teamLabel}`);
|
|
1267
|
+
console.log(
|
|
1268
|
+
pc8.dim(` ID: ${set.id} \xB7 ${team ? `Team: ${team.name}${team.id ? ` (ID: ${team.id})` : ""}` : "Personal"}`)
|
|
1269
|
+
);
|
|
1270
|
+
if (platforms && Object.keys(platforms).length > 0) {
|
|
1271
|
+
console.log("");
|
|
1272
|
+
renderPlatforms(platforms, " ");
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function renderSocialSetsList(data) {
|
|
1276
|
+
const results = data.results ?? [];
|
|
1277
|
+
const total = data.total ?? results.length;
|
|
1278
|
+
if (results.length === 0) {
|
|
1279
|
+
console.log(pc8.yellow("\n No social sets found."));
|
|
1280
|
+
console.log(pc8.dim(" Connect a social account at typefully.com\n"));
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
console.log("");
|
|
1284
|
+
console.log(pc8.dim(` ${total} social set${total !== 1 ? "s" : ""}`));
|
|
1285
|
+
for (let i = 0; i < results.length; i++) {
|
|
1286
|
+
console.log("");
|
|
1287
|
+
renderSocialSetCard(results[i], i);
|
|
1288
|
+
}
|
|
1289
|
+
console.log("");
|
|
1290
|
+
}
|
|
1291
|
+
function renderSocialSet(data) {
|
|
1292
|
+
console.log("");
|
|
1293
|
+
renderSocialSetCard(data);
|
|
1294
|
+
console.log("");
|
|
1295
|
+
}
|
|
1296
|
+
function registerSocialSetsCommand(program2) {
|
|
1297
|
+
const cmd = program2.command("social-sets").description("Manage social sets");
|
|
1298
|
+
cmd.command("list").description("List all social sets").action(async () => {
|
|
1299
|
+
const spinner = spin("Fetching social sets\u2026");
|
|
1300
|
+
spinner.start();
|
|
1301
|
+
const data = await apiRequest("GET", "/social-sets?limit=50");
|
|
1302
|
+
spinner.stop();
|
|
1303
|
+
display(data, () => renderSocialSetsList(data));
|
|
1304
|
+
});
|
|
1305
|
+
cmd.command("get").description("Get social set details").argument("[social_set_id]", "Social set ID (uses default if omitted)").action(async (socialSetId) => {
|
|
1306
|
+
const id = requireSocialSetId(socialSetId ?? null);
|
|
1307
|
+
const spinner = spin("Fetching social set\u2026");
|
|
1308
|
+
spinner.start();
|
|
1309
|
+
const data = await apiRequest("GET", `/social-sets/${id}`);
|
|
1310
|
+
spinner.stop();
|
|
1311
|
+
display(data, () => renderSocialSet(data));
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/commands/tags.ts
|
|
1316
|
+
import pc9 from "picocolors";
|
|
1317
|
+
function renderTagsList(data) {
|
|
1318
|
+
const results = data.results ?? [];
|
|
1319
|
+
if (results.length === 0) {
|
|
1320
|
+
console.log(pc9.yellow("\n No tags found.\n"));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
console.log("");
|
|
1324
|
+
console.log(pc9.dim(` ${results.length} tag${results.length !== 1 ? "s" : ""}`));
|
|
1325
|
+
console.log("");
|
|
1326
|
+
for (let i = 0; i < results.length; i++) {
|
|
1327
|
+
const tag = results[i];
|
|
1328
|
+
const num = pc9.dim(`${String(i + 1)}.`.padStart(3));
|
|
1329
|
+
const name = pc9.bold(String(tag.name ?? ""));
|
|
1330
|
+
const slug = tag.slug ? pc9.dim(` (${tag.slug})`) : "";
|
|
1331
|
+
console.log(` ${num} ${name}${slug}`);
|
|
1332
|
+
}
|
|
1333
|
+
console.log("");
|
|
1334
|
+
}
|
|
1335
|
+
function registerTagsCommand(program2) {
|
|
1336
|
+
const cmd = program2.command("tags").description("Manage tags");
|
|
1337
|
+
cmd.command("list").description("List all tags").argument("[social_set_id]", "Social set ID (uses default if omitted)").action(async (socialSetId) => {
|
|
1338
|
+
const id = requireSocialSetId(socialSetId ?? null);
|
|
1339
|
+
const spinner = spin("Fetching tags\u2026");
|
|
1340
|
+
spinner.start();
|
|
1341
|
+
const data = await apiRequest("GET", `/social-sets/${id}/tags?limit=50`);
|
|
1342
|
+
spinner.stop();
|
|
1343
|
+
display(data, () => renderTagsList(data));
|
|
1344
|
+
});
|
|
1345
|
+
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
|
+
const id = requireSocialSetId(socialSetId ?? null);
|
|
1347
|
+
if (!opts.name) exitWithError("--name is required");
|
|
1348
|
+
const spinner = spin("Creating tag\u2026");
|
|
1349
|
+
spinner.start();
|
|
1350
|
+
const data = await apiRequest("POST", `/social-sets/${id}/tags`, { name: opts.name });
|
|
1351
|
+
spinner.stop();
|
|
1352
|
+
display(data, () => {
|
|
1353
|
+
const tag = data;
|
|
1354
|
+
console.log("");
|
|
1355
|
+
console.log(` ${pc9.green("\u2713")} Tag created: ${pc9.bold(String(tag.name ?? opts.name))}`);
|
|
1356
|
+
if (tag.slug) console.log(pc9.dim(` Slug: ${tag.slug}`));
|
|
1357
|
+
console.log("");
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/utils/banner.ts
|
|
1363
|
+
import pc10 from "picocolors";
|
|
1364
|
+
var BANNER_WIDE = `
|
|
1365
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
1366
|
+
\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
|
|
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
|
|
1375
|
+
`;
|
|
1376
|
+
function showBanner() {
|
|
1377
|
+
const cols = process.stdout.columns || 80;
|
|
1378
|
+
const banner = cols >= 80 ? BANNER_WIDE : BANNER_COMPACT;
|
|
1379
|
+
console.error(pc10.dim(banner));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/cli.ts
|
|
1383
|
+
var require2 = createRequire(import.meta.url);
|
|
1384
|
+
var pkg = require2("../package.json");
|
|
1385
|
+
function createCli() {
|
|
1386
|
+
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").hook("preAction", (_thisCommand) => {
|
|
1388
|
+
const jsonMode = !!program2.opts().json;
|
|
1389
|
+
setJsonMode(jsonMode);
|
|
1390
|
+
const isVersion = process.argv.includes("-v") || process.argv.includes("--version");
|
|
1391
|
+
if (!isVersion && !jsonMode) {
|
|
1392
|
+
showBanner();
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
registerSetupCommand(program2);
|
|
1396
|
+
registerMeCommand(program2);
|
|
1397
|
+
registerSocialSetsCommand(program2);
|
|
1398
|
+
registerDraftsCommand(program2);
|
|
1399
|
+
registerAliasCommands(program2);
|
|
1400
|
+
registerTagsCommand(program2);
|
|
1401
|
+
registerMediaCommand(program2);
|
|
1402
|
+
registerConfigCommand(program2);
|
|
1403
|
+
return program2;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/index.ts
|
|
1407
|
+
var program = createCli();
|
|
1408
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1409
|
+
console.error(err);
|
|
1410
|
+
process.exit(1);
|
|
1411
|
+
});
|