@so-me/cli 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.
@@ -0,0 +1,2727 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import axios from "axios";
10
+
11
+ // src/lib/config.ts
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import os from "os";
15
+ var DEFAULT_CONFIG = {
16
+ apiUrl: "http://localhost:8000",
17
+ outputFormat: "json"
18
+ };
19
+ function getConfigDir() {
20
+ return process.env.SOME_CONFIG_DIR || path.join(os.homedir(), ".so-me");
21
+ }
22
+ function getConfigPath() {
23
+ return path.join(getConfigDir(), "config.json");
24
+ }
25
+ function getConfig() {
26
+ const configPath = getConfigPath();
27
+ if (!fs.existsSync(configPath)) {
28
+ return { ...DEFAULT_CONFIG };
29
+ }
30
+ const raw = fs.readFileSync(configPath, "utf-8");
31
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
32
+ }
33
+ function setConfig(partial) {
34
+ const configDir = getConfigDir();
35
+ if (!fs.existsSync(configDir)) {
36
+ fs.mkdirSync(configDir, { recursive: true, mode: 448 });
37
+ }
38
+ const current = getConfig();
39
+ const updated = { ...current, ...partial };
40
+ fs.writeFileSync(getConfigPath(), JSON.stringify(updated, null, 2), {
41
+ mode: 384
42
+ });
43
+ }
44
+ function clearConfig() {
45
+ const configPath = getConfigPath();
46
+ if (fs.existsSync(configPath)) {
47
+ fs.unlinkSync(configPath);
48
+ }
49
+ }
50
+ function getApiKey(flagValue) {
51
+ if (flagValue) return flagValue;
52
+ if (process.env.SOME_API_KEY) return process.env.SOME_API_KEY;
53
+ return getConfig().apiKey;
54
+ }
55
+ function getApiUrl(flagValue) {
56
+ if (flagValue) return flagValue;
57
+ if (process.env.SOME_API_URL) return process.env.SOME_API_URL;
58
+ return getConfig().apiUrl || DEFAULT_CONFIG.apiUrl;
59
+ }
60
+
61
+ // src/commands/auth.ts
62
+ async function deviceFlow(apiUrl) {
63
+ const spinner = ora("Starting device authorization...").start();
64
+ try {
65
+ const { data } = await axios.post(`${apiUrl}/auth/device/code`);
66
+ spinner.stop();
67
+ const { device_code, user_code, verification_uri, expires_in, interval } = data;
68
+ console.log();
69
+ console.log(chalk.bold(" To authenticate, open this URL in your browser:"));
70
+ console.log();
71
+ console.log(` ${chalk.cyan(verification_uri)}`);
72
+ console.log();
73
+ console.log(chalk.bold(" And enter this code:"));
74
+ console.log();
75
+ console.log(` ${chalk.yellow.bold(user_code)}`);
76
+ console.log();
77
+ console.log(chalk.dim(` Code expires in ${Math.floor(expires_in / 60)} minutes.`));
78
+ console.log();
79
+ try {
80
+ const open = await import("open");
81
+ await open.default(verification_uri);
82
+ console.log(chalk.dim(" Browser opened automatically."));
83
+ } catch {
84
+ }
85
+ const pollSpinner = ora("Waiting for authorization...").start();
86
+ const pollInterval = (interval || 5) * 1e3;
87
+ const deadline = Date.now() + expires_in * 1e3;
88
+ while (Date.now() < deadline) {
89
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
90
+ try {
91
+ const { data: tokenData } = await axios.post(`${apiUrl}/auth/device/token`, {
92
+ device_code
93
+ });
94
+ if (tokenData.status === "authorized") {
95
+ pollSpinner.succeed("Device authorized!");
96
+ console.log();
97
+ console.log(chalk.green(` Logged in as ${tokenData.email}`));
98
+ if (tokenData.api_key) {
99
+ setConfig({ apiKey: tokenData.api_key });
100
+ console.log(chalk.dim(" API key generated and saved."));
101
+ } else {
102
+ console.log(chalk.yellow(" Warning: Could not generate API key."));
103
+ console.log(chalk.dim(" Your plan may not support API access (Scale plan required)."));
104
+ console.log(chalk.dim(' Use "so-me auth:login --api-key <key>" with a manually created key.'));
105
+ }
106
+ console.log(chalk.dim(' Run "so-me auth:status" to verify.'));
107
+ return;
108
+ }
109
+ } catch (error) {
110
+ const status = error?.response?.status;
111
+ if (status === 400) {
112
+ const msg = error?.response?.data?.message;
113
+ if (msg?.includes("expired") || msg?.includes("invalid")) {
114
+ pollSpinner.fail("Device code expired");
115
+ console.log(chalk.dim(' Run "so-me auth:login" to try again.'));
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ pollSpinner.fail("Authorization timed out");
123
+ console.log(chalk.dim(' Run "so-me auth:login" to try again.'));
124
+ process.exitCode = 1;
125
+ } catch (error) {
126
+ spinner.fail("Failed to start device authorization");
127
+ const msg = error?.response?.data?.message || error?.message || "Unknown error";
128
+ console.error(chalk.red(` ${msg}`));
129
+ process.exitCode = 1;
130
+ }
131
+ }
132
+ function registerAuthCommands(program2) {
133
+ program2.command("auth:login").description("Authenticate with the API").option("--api-key <key>", "API key to store").action(async function(opts) {
134
+ const apiKey = opts.apiKey ?? this.parent?.opts()?.apiKey;
135
+ if (apiKey) {
136
+ opts = { ...opts, apiKey };
137
+ }
138
+ if (opts.apiKey) {
139
+ setConfig({ apiKey: opts.apiKey });
140
+ console.log(chalk.green("API key saved successfully."));
141
+ console.log(chalk.dim('Run "so-me auth:status" to verify.'));
142
+ return;
143
+ }
144
+ const globalOpts = program2.opts();
145
+ const apiUrl = getApiUrl(globalOpts.apiUrl);
146
+ await deviceFlow(apiUrl);
147
+ });
148
+ program2.command("auth:status").description("Show current authentication status").action(async () => {
149
+ const apiKey = getApiKey();
150
+ const apiUrl = getApiUrl();
151
+ if (!apiKey) {
152
+ console.log(chalk.yellow("Not authenticated."));
153
+ console.log(chalk.dim('Run "so-me auth:login --api-key <key>" to authenticate.'));
154
+ return;
155
+ }
156
+ const maskedKey = apiKey.slice(0, 12) + "..." + apiKey.slice(-4);
157
+ console.log(chalk.green("Authenticated"));
158
+ console.log(` API Key: ${maskedKey}`);
159
+ console.log(` API URL: ${apiUrl}`);
160
+ const config = getConfig();
161
+ if (config.defaultWorkspace) {
162
+ console.log(` Workspace: ${config.defaultWorkspace}`);
163
+ }
164
+ });
165
+ program2.command("auth:logout").description("Remove stored credentials").action(async () => {
166
+ clearConfig();
167
+ console.log(chalk.green("Logged out. Credentials removed."));
168
+ });
169
+ }
170
+
171
+ // src/commands/posts.ts
172
+ import chalk2 from "chalk";
173
+ import ora2 from "ora";
174
+ import { createInterface } from "readline";
175
+
176
+ // src/lib/api.ts
177
+ import axios2 from "axios";
178
+ var ApiError = class extends Error {
179
+ constructor(statusCode, status, detail) {
180
+ super(`Error (${statusCode}): ${detail}`);
181
+ this.statusCode = statusCode;
182
+ this.status = status;
183
+ this.detail = detail;
184
+ this.name = "ApiError";
185
+ }
186
+ statusCode;
187
+ status;
188
+ detail;
189
+ };
190
+ function createApiClient(opts) {
191
+ const baseURL = getApiUrl(opts?.apiUrl);
192
+ const client = axios2.create({
193
+ baseURL,
194
+ timeout: 3e4,
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ Accept: "application/json"
198
+ }
199
+ });
200
+ client.interceptors.request.use((config) => {
201
+ const key = getApiKey(opts?.apiKey);
202
+ if (!key) {
203
+ throw new ApiError(
204
+ 401,
205
+ "Unauthorized",
206
+ "No API key configured. Set SOME_API_KEY or run 'so-me auth:login'."
207
+ );
208
+ }
209
+ config.headers["X-API-Key"] = key;
210
+ return config;
211
+ });
212
+ client.interceptors.response.use(
213
+ (response) => response,
214
+ (error) => {
215
+ if (error instanceof ApiError) throw error;
216
+ if (!error.response) {
217
+ throw new ApiError(
218
+ 0,
219
+ "NetworkError",
220
+ `Could not connect to API at ${baseURL}. Check your internet connection.`
221
+ );
222
+ }
223
+ const { status, data } = error.response;
224
+ const detail = data?.message || data?.error || error.message;
225
+ if (status === 401) {
226
+ throw new ApiError(401, "Unauthorized", "Invalid API key. Run 'so-me auth:login' or set SOME_API_KEY.");
227
+ }
228
+ if (status === 403) {
229
+ throw new ApiError(403, "Forbidden", "Insufficient permissions. Your plan may not include this feature.");
230
+ }
231
+ if (status === 429) {
232
+ throw new ApiError(429, "TooManyRequests", "Rate limit exceeded. Try again later.");
233
+ }
234
+ throw new ApiError(status, "Error", detail);
235
+ }
236
+ );
237
+ return client;
238
+ }
239
+
240
+ // src/lib/output.ts
241
+ import Table from "cli-table3";
242
+ function formatOutput(data, format) {
243
+ if (format === "json") {
244
+ return JSON.stringify(data, null, 2);
245
+ }
246
+ return formatTable(data);
247
+ }
248
+ function formatTable(data) {
249
+ if (Array.isArray(data)) {
250
+ if (data.length === 0) {
251
+ return "No data";
252
+ }
253
+ const keys = Object.keys(data[0]);
254
+ const table = new Table({ head: keys });
255
+ for (const row of data) {
256
+ table.push(keys.map((k) => truncate(String(row[k] ?? ""), 50)));
257
+ }
258
+ return table.toString();
259
+ }
260
+ if (data && typeof data === "object") {
261
+ const table = new Table();
262
+ for (const [key, value] of Object.entries(data)) {
263
+ table.push({ [key]: truncate(String(value ?? ""), 80) });
264
+ }
265
+ return table.toString();
266
+ }
267
+ return String(data);
268
+ }
269
+ function truncate(str, max) {
270
+ if (str.length <= max) return str;
271
+ return str.slice(0, max - 3) + "...";
272
+ }
273
+ function printOutput(data, format) {
274
+ console.log(formatOutput(data, format));
275
+ }
276
+ function printError(error) {
277
+ if (error instanceof Error) {
278
+ console.error(error.message);
279
+ } else {
280
+ console.error(String(error));
281
+ }
282
+ }
283
+
284
+ // src/commands/posts.ts
285
+ function getFormat(program2) {
286
+ const opts = program2.opts();
287
+ if (opts.table) return "table";
288
+ return getConfig().outputFormat || "json";
289
+ }
290
+ function getGlobalOpts(program2) {
291
+ return program2.opts();
292
+ }
293
+ function askQuestion(question) {
294
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
295
+ return new Promise((resolve) => {
296
+ rl.question(question, (answer) => {
297
+ rl.close();
298
+ resolve(answer.trim());
299
+ });
300
+ });
301
+ }
302
+ async function resolveAccountForPlatform(client, platform) {
303
+ const { data: accounts } = await client.get("/v1/accounts");
304
+ const matches = accounts.filter(
305
+ (a) => a.platform === platform.toUpperCase()
306
+ );
307
+ if (matches.length === 0) {
308
+ console.error(chalk2.red(`No ${platform} account found. Connect one first.`));
309
+ return null;
310
+ }
311
+ if (matches.length === 1) return matches[0];
312
+ console.log(chalk2.bold(` Found ${matches.length} ${platform} accounts:`));
313
+ console.log();
314
+ matches.forEach((a, i) => {
315
+ const name = a.accountName || a.userName || a.accountId;
316
+ console.log(` ${chalk2.cyan(String(i + 1))}. ${name}`);
317
+ });
318
+ console.log();
319
+ const answer = await askQuestion(` Select account (1-${matches.length}): `);
320
+ const index = parseInt(answer, 10) - 1;
321
+ if (isNaN(index) || index < 0 || index >= matches.length) {
322
+ console.log(chalk2.yellow(" Invalid selection. Cancelled."));
323
+ return null;
324
+ }
325
+ return matches[index];
326
+ }
327
+ function registerPostsCommands(program2) {
328
+ program2.command("posts:list").description("List posts").option("--start-date <date>", "Filter by start date (ISO 8601)").option("--end-date <date>", "Filter by end date (ISO 8601)").option("--status <status>", "Filter by status").option("--platform <platform>", "Filter by platform (e.g. TWITTER, LINKEDIN)").option("--page <number>", "Page number", "1").option("--limit <number>", "Items per page", "20").action(async (opts) => {
329
+ const globalOpts = getGlobalOpts(program2);
330
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
331
+ const spinner = ora2("Fetching posts...").start();
332
+ try {
333
+ const params = {
334
+ page: parseInt(opts.page),
335
+ limit: parseInt(opts.limit)
336
+ };
337
+ if (opts.startDate) params.startDate = opts.startDate;
338
+ if (opts.endDate) params.endDate = opts.endDate;
339
+ if (opts.status) params.status = opts.status;
340
+ if (opts.platform) params.socialMedia = opts.platform;
341
+ const { data } = await client.get("/v1/posts", { params });
342
+ spinner.stop();
343
+ printOutput(data, getFormat(program2));
344
+ } catch (error) {
345
+ spinner.fail("Failed to fetch posts");
346
+ printError(error);
347
+ if (globalOpts.verbose && error instanceof Error) {
348
+ console.error(error.stack);
349
+ }
350
+ process.exitCode = 1;
351
+ }
352
+ });
353
+ program2.command("posts:get").description("Get a single post").argument("<id>", "Post ID").action(async (id) => {
354
+ const globalOpts = getGlobalOpts(program2);
355
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
356
+ const spinner = ora2("Fetching post...").start();
357
+ try {
358
+ const { data } = await client.get(`/v1/posts/${id}`);
359
+ spinner.stop();
360
+ printOutput(data, getFormat(program2));
361
+ } catch (error) {
362
+ spinner.fail("Failed to fetch post");
363
+ printError(error);
364
+ process.exitCode = 1;
365
+ }
366
+ });
367
+ program2.command("posts:create").description("Create a post").requiredOption("-c, --content <text>", "Post content").option("-s, --date <date>", "Scheduled date (ISO 8601)").option("-t, --type <type>", "Post type: schedule or draft", "schedule").option("-m, --media <ids>", "Comma-separated file IDs").option("-a, --accounts <ids>", "Comma-separated account IDs").option("--post-type <type>", "Content type: TEXT, IMAGE, VIDEO, etc.", "TEXT").option("--platform <platform>", "Social platform: TWITTER, LINKEDIN, etc.", "TWITTER").option("--settings <json>", "Platform-specific settings as JSON").option("-j, --json-file <path>", "Path to JSON file for complex post").addHelpText("after", `
368
+ Examples:
369
+ $ so-me posts:create -c "Hello world!" --platform TWITTER
370
+ $ so-me posts:create -c "Scheduled post" -s 2026-04-10T14:00:00Z --platform LINKEDIN
371
+ $ so-me posts:create -c "With media" -m file-id-1,file-id-2 --post-type IMAGE
372
+ $ so-me posts:create -j ./campaign.json Create from JSON file`).action(async (opts) => {
373
+ const globalOpts = getGlobalOpts(program2);
374
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
375
+ let spinner = null;
376
+ try {
377
+ let body;
378
+ if (opts.jsonFile) {
379
+ const fs3 = await import("fs");
380
+ body = JSON.parse(fs3.readFileSync(opts.jsonFile, "utf-8"));
381
+ } else {
382
+ body = {
383
+ text: opts.content,
384
+ postType: opts.postType,
385
+ socialMedia: opts.platform
386
+ };
387
+ if (opts.date) body.scheduledAt = opts.date;
388
+ if (opts.media) {
389
+ body.files = opts.media.split(",").map((id) => ({ id: id.trim() }));
390
+ }
391
+ if (opts.settings) body.settings = JSON.parse(opts.settings);
392
+ if (opts.accounts) {
393
+ body.accountId = opts.accounts;
394
+ } else if (opts.platform) {
395
+ const account = await resolveAccountForPlatform(client, opts.platform);
396
+ if (!account) {
397
+ process.exitCode = 1;
398
+ return;
399
+ }
400
+ body.accountId = account.accountId;
401
+ }
402
+ }
403
+ spinner = ora2("Creating post...").start();
404
+ const { data } = await client.post("/v1/posts", body);
405
+ spinner.succeed("Post created");
406
+ printOutput(data, getFormat(program2));
407
+ } catch (error) {
408
+ if (spinner) spinner.fail("Failed to create post");
409
+ else console.error(chalk2.red("Failed to create post"));
410
+ printError(error);
411
+ process.exitCode = 1;
412
+ }
413
+ });
414
+ program2.command("posts:update").description("Update a post").argument("<id>", "Post ID").option("-c, --content <text>", "Updated content").option("-s, --date <date>", "Updated scheduled date (ISO 8601)").option("-m, --media <ids>", "Updated comma-separated file IDs").action(async (id, opts) => {
415
+ const globalOpts = getGlobalOpts(program2);
416
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
417
+ const spinner = ora2("Updating post...").start();
418
+ try {
419
+ const body = {};
420
+ if (opts.content) body.text = opts.content;
421
+ if (opts.date) body.scheduledAt = opts.date;
422
+ if (opts.media) body.fileIds = opts.media.split(",").map((id2) => id2.trim());
423
+ const { data } = await client.patch(`/v1/posts/${id}`, body);
424
+ spinner.succeed("Post updated");
425
+ printOutput(data, getFormat(program2));
426
+ } catch (error) {
427
+ spinner.fail("Failed to update post");
428
+ printError(error);
429
+ process.exitCode = 1;
430
+ }
431
+ });
432
+ program2.command("posts:delete").description("Delete one or more posts").argument("<ids...>", "Post ID(s)").option("-y, --yes", "Skip confirmation prompt").action(async (ids, opts) => {
433
+ const globalOpts = getGlobalOpts(program2);
434
+ if (!opts.yes) {
435
+ const readline = await import("readline");
436
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
437
+ const label = ids.length === 1 ? `post ${ids[0]}` : `${ids.length} posts`;
438
+ const answer = await new Promise((resolve) => {
439
+ rl.question(`Are you sure you want to delete ${label}? (y/N) `, resolve);
440
+ });
441
+ rl.close();
442
+ if (answer.toLowerCase() !== "y") {
443
+ console.log("Cancelled.");
444
+ return;
445
+ }
446
+ }
447
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
448
+ const spinner = ora2("Deleting post(s)...").start();
449
+ try {
450
+ if (ids.length === 1) {
451
+ await client.delete(`/v1/posts/${ids[0]}`);
452
+ spinner.succeed(`Post ${ids[0]} deleted`);
453
+ } else {
454
+ const { data } = await client.post("/v1/posts/bulk-delete", { ids });
455
+ spinner.succeed(`Deleted ${data.deleted}/${data.total} posts`);
456
+ printOutput(data, getFormat(program2));
457
+ }
458
+ } catch (error) {
459
+ spinner.fail("Failed to delete post(s)");
460
+ printError(error);
461
+ process.exitCode = 1;
462
+ }
463
+ });
464
+ program2.command("posts:schedule").description("Schedule a draft post").argument("<id>", "Post ID").option("-s, --date <date>", "Schedule date (ISO 8601)").action(async (id, opts) => {
465
+ const globalOpts = getGlobalOpts(program2);
466
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
467
+ const spinner = ora2("Scheduling post...").start();
468
+ try {
469
+ const body = {};
470
+ if (opts.date) body.scheduledAt = opts.date;
471
+ const { data } = await client.post(`/v1/posts/${id}/schedule`, body);
472
+ spinner.succeed(`Post ${id} scheduled`);
473
+ printOutput(data, getFormat(program2));
474
+ } catch (error) {
475
+ spinner.fail("Failed to schedule post");
476
+ printError(error);
477
+ process.exitCode = 1;
478
+ }
479
+ });
480
+ program2.command("posts:unschedule").description("Remove a post from the schedule").argument("<id>", "Post ID").action(async (id) => {
481
+ const globalOpts = getGlobalOpts(program2);
482
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
483
+ const spinner = ora2("Unscheduling post...").start();
484
+ try {
485
+ const { data } = await client.post(`/v1/posts/${id}/unschedule`);
486
+ spinner.succeed(`Post ${id} unscheduled`);
487
+ printOutput(data, getFormat(program2));
488
+ } catch (error) {
489
+ spinner.fail("Failed to unschedule post");
490
+ printError(error);
491
+ process.exitCode = 1;
492
+ }
493
+ });
494
+ program2.command("posts:retry").description("Retry a failed post").argument("<id>", "Post ID").action(async (id) => {
495
+ const globalOpts = getGlobalOpts(program2);
496
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
497
+ const spinner = ora2("Retrying post...").start();
498
+ try {
499
+ const { data } = await client.post(`/v1/posts/${id}/retry`);
500
+ spinner.succeed(`Post ${id} queued for retry`);
501
+ printOutput(data, getFormat(program2));
502
+ } catch (error) {
503
+ spinner.fail("Failed to retry post");
504
+ printError(error);
505
+ process.exitCode = 1;
506
+ }
507
+ });
508
+ program2.command("posts:resubmit").description("Resubmit a rejected post for approval").argument("<id>", "Post ID").action(async (id) => {
509
+ const globalOpts = getGlobalOpts(program2);
510
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
511
+ const spinner = ora2("Resubmitting post...").start();
512
+ try {
513
+ const { data } = await client.post(`/v1/posts/${id}/resubmit`);
514
+ spinner.succeed(`Post ${id} resubmitted for approval`);
515
+ printOutput(data, getFormat(program2));
516
+ } catch (error) {
517
+ spinner.fail("Failed to resubmit post");
518
+ printError(error);
519
+ process.exitCode = 1;
520
+ }
521
+ });
522
+ }
523
+
524
+ // src/commands/accounts.ts
525
+ import ora3 from "ora";
526
+ import axios3 from "axios";
527
+ import chalk3 from "chalk";
528
+ import { createInterface as createInterface2 } from "readline";
529
+ function getFormat2(program2) {
530
+ const opts = program2.opts();
531
+ if (opts.table) return "table";
532
+ return getConfig().outputFormat || "json";
533
+ }
534
+ function getGlobalOpts2(program2) {
535
+ return program2.opts();
536
+ }
537
+ var PLATFORMS = [
538
+ "twitter",
539
+ "linkedin",
540
+ "linkedin_page",
541
+ "instagram",
542
+ "facebook",
543
+ "tiktok",
544
+ "youtube",
545
+ "threads"
546
+ ];
547
+ function validatePlatform(value) {
548
+ const normalized = value.toLowerCase();
549
+ if (!PLATFORMS.includes(normalized)) {
550
+ console.error(
551
+ chalk3.red(`Invalid platform: ${value}`)
552
+ );
553
+ console.error(
554
+ chalk3.dim(`Valid platforms: ${PLATFORMS.join(", ")}`)
555
+ );
556
+ process.exit(1);
557
+ }
558
+ return normalized;
559
+ }
560
+ function askQuestion2(question) {
561
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
562
+ return new Promise((resolve) => {
563
+ rl.question(question, (answer) => {
564
+ rl.close();
565
+ resolve(answer.trim());
566
+ });
567
+ });
568
+ }
569
+ function registerAccountsCommands(program2) {
570
+ program2.command("accounts:list").description("List connected social accounts").action(async () => {
571
+ const globalOpts = getGlobalOpts2(program2);
572
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
573
+ const spinner = ora3("Fetching accounts...").start();
574
+ try {
575
+ const { data } = await client.get("/v1/accounts");
576
+ spinner.stop();
577
+ printOutput(data, getFormat2(program2));
578
+ } catch (error) {
579
+ spinner.fail("Failed to fetch accounts");
580
+ printError(error);
581
+ process.exitCode = 1;
582
+ }
583
+ });
584
+ program2.command("accounts:get").description("Get account details by ID or platform name").argument("<id-or-platform>", "Account ID or platform name (twitter, linkedin, etc.)").action(async (idOrPlatform) => {
585
+ const globalOpts = getGlobalOpts2(program2);
586
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
587
+ const spinner = ora3("Fetching account...").start();
588
+ try {
589
+ const isPlatform = PLATFORMS.includes(idOrPlatform.toLowerCase());
590
+ if (isPlatform) {
591
+ const { data: accounts } = await client.get("/v1/accounts");
592
+ const filtered = accounts.filter(
593
+ (a) => a.platform.toLowerCase() === idOrPlatform.toLowerCase()
594
+ );
595
+ if (filtered.length === 0) {
596
+ spinner.info(`No ${idOrPlatform.toLowerCase()} accounts found.`);
597
+ return;
598
+ }
599
+ spinner.stop();
600
+ printOutput(filtered.length === 1 ? filtered[0] : filtered, getFormat2(program2));
601
+ } else {
602
+ const { data } = await client.get(`/v1/accounts/${idOrPlatform}`);
603
+ spinner.stop();
604
+ printOutput(data, getFormat2(program2));
605
+ }
606
+ } catch (error) {
607
+ spinner.fail("Failed to fetch account");
608
+ printError(error);
609
+ process.exitCode = 1;
610
+ }
611
+ });
612
+ program2.command("accounts:connect").description("Connect a social account via browser OAuth").requiredOption("--platform <platform>", "Social platform to connect").action(async (opts) => {
613
+ const globalOpts = getGlobalOpts2(program2);
614
+ const platform = validatePlatform(opts.platform);
615
+ const apiKey = getApiKey(globalOpts.apiKey);
616
+ const apiUrl = getApiUrl(globalOpts.apiUrl);
617
+ if (!apiKey) {
618
+ console.error(chalk3.red('Not authenticated. Run "so-me auth:login" first.'));
619
+ process.exitCode = 1;
620
+ return;
621
+ }
622
+ const spinner = ora3("Starting device authorization...").start();
623
+ try {
624
+ const { data } = await axios3.post(
625
+ `${apiUrl}/auth/device/code`,
626
+ { action: "connect_social", platform },
627
+ { headers: { "X-API-Key": apiKey } }
628
+ );
629
+ spinner.stop();
630
+ const { device_code, user_code, verification_uri, expires_in, interval } = data;
631
+ console.log();
632
+ console.log(chalk3.bold(" To connect your account, open this URL in your browser:"));
633
+ console.log();
634
+ console.log(` ${chalk3.cyan(verification_uri)}`);
635
+ console.log();
636
+ console.log(chalk3.bold(" And enter this code:"));
637
+ console.log();
638
+ console.log(` ${chalk3.yellow.bold(user_code)}`);
639
+ console.log();
640
+ console.log(chalk3.dim(` Code expires in ${Math.floor(expires_in / 60)} minutes.`));
641
+ console.log();
642
+ try {
643
+ const open = await import("open");
644
+ await open.default(verification_uri);
645
+ console.log(chalk3.dim(" Browser opened automatically."));
646
+ } catch {
647
+ }
648
+ const pollSpinner = ora3("Waiting for account connection...").start();
649
+ const pollInterval = (interval || 5) * 1e3;
650
+ const deadline = Date.now() + expires_in * 1e3;
651
+ while (Date.now() < deadline) {
652
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
653
+ try {
654
+ const { data: tokenData } = await axios3.post(`${apiUrl}/auth/device/token`, {
655
+ device_code
656
+ });
657
+ if (tokenData.status === "authorized") {
658
+ pollSpinner.succeed("Account connected!");
659
+ console.log();
660
+ const name = tokenData.account_name || tokenData.user_name || platform;
661
+ const username = tokenData.user_name ? ` (${tokenData.user_name})` : "";
662
+ console.log(
663
+ chalk3.green(` Connected ${platform} account: ${name}${username}`)
664
+ );
665
+ console.log(chalk3.dim(' Run "so-me accounts:list" to see all accounts.'));
666
+ return;
667
+ }
668
+ } catch (error) {
669
+ const status = error?.response?.status;
670
+ if (status === 400) {
671
+ const msg = error?.response?.data?.message;
672
+ if (msg?.includes("expired") || msg?.includes("invalid")) {
673
+ pollSpinner.fail("Device code expired");
674
+ console.log(chalk3.dim(' Run "so-me accounts:connect" to try again.'));
675
+ process.exitCode = 1;
676
+ return;
677
+ }
678
+ }
679
+ }
680
+ }
681
+ pollSpinner.fail("Authorization timed out");
682
+ console.log(chalk3.dim(' Run "so-me accounts:connect" to try again.'));
683
+ process.exitCode = 1;
684
+ } catch (error) {
685
+ spinner.fail("Failed to start device authorization");
686
+ const msg = error?.response?.data?.message || error?.message || "Unknown error";
687
+ console.error(chalk3.red(` ${msg}`));
688
+ process.exitCode = 1;
689
+ }
690
+ });
691
+ program2.command("accounts:remove").description("Remove a connected social account").argument("<platform>", "Social platform (twitter, linkedin, linkedin_page, instagram, facebook, tiktok, youtube, threads)").action(async (platformArg) => {
692
+ const globalOpts = getGlobalOpts2(program2);
693
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
694
+ const platform = validatePlatform(platformArg);
695
+ const spinner = ora3("Fetching accounts...").start();
696
+ try {
697
+ const { data: accounts } = await client.get("/v1/accounts");
698
+ const filtered = accounts.filter(
699
+ (a) => a.platform.toLowerCase() === platform
700
+ );
701
+ if (filtered.length === 0) {
702
+ spinner.info(`No ${platform} accounts found.`);
703
+ return;
704
+ }
705
+ spinner.stop();
706
+ let account;
707
+ if (filtered.length === 1) {
708
+ account = filtered[0];
709
+ } else {
710
+ console.log();
711
+ console.log(chalk3.bold(` Found ${filtered.length} ${platform} accounts:`));
712
+ console.log();
713
+ filtered.forEach((a, i) => {
714
+ const name2 = a.accountName || a.userName || a.accountId;
715
+ const user = a.userName ? ` (@${a.userName})` : "";
716
+ console.log(` ${chalk3.cyan(String(i + 1))}. ${name2}${user}`);
717
+ });
718
+ console.log();
719
+ const answer = await askQuestion2(` Select account to remove (1-${filtered.length}): `);
720
+ const index = parseInt(answer, 10) - 1;
721
+ if (isNaN(index) || index < 0 || index >= filtered.length) {
722
+ console.log(chalk3.yellow(" Invalid selection. Cancelled."));
723
+ return;
724
+ }
725
+ account = filtered[index];
726
+ }
727
+ const name = account.accountName || account.userName || account.accountId;
728
+ const confirm = await askQuestion2(
729
+ ` Remove ${name} (${account.platform})? (y/N): `
730
+ );
731
+ if (confirm.toLowerCase() !== "y") {
732
+ console.log(chalk3.dim(" Cancelled."));
733
+ return;
734
+ }
735
+ const deleteSpinner = ora3("Removing account...").start();
736
+ await client.delete(`/v1/accounts/${account.id}`);
737
+ deleteSpinner.succeed("Account removed.");
738
+ console.log(chalk3.green(` Removed ${name} (${account.platform}).`));
739
+ } catch (error) {
740
+ spinner.fail("Failed to remove account");
741
+ printError(error);
742
+ process.exitCode = 1;
743
+ }
744
+ });
745
+ }
746
+
747
+ // src/commands/media.ts
748
+ import ora4 from "ora";
749
+ import fs2 from "fs";
750
+ import path2 from "path";
751
+ import axios4 from "axios";
752
+ import chalk4 from "chalk";
753
+ import { createInterface as createInterface3 } from "readline";
754
+ function getFormat3(program2) {
755
+ const opts = program2.opts();
756
+ if (opts.table) return "table";
757
+ return getConfig().outputFormat || "json";
758
+ }
759
+ function getGlobalOpts3(program2) {
760
+ return program2.opts();
761
+ }
762
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
763
+ function isUUID(value) {
764
+ return UUID_RE.test(value);
765
+ }
766
+ function askQuestion3(question) {
767
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
768
+ return new Promise((resolve) => {
769
+ rl.question(question, (answer) => {
770
+ rl.close();
771
+ resolve(answer.trim());
772
+ });
773
+ });
774
+ }
775
+ async function pickOne(items, labelFn, prompt) {
776
+ console.log();
777
+ items.forEach((item, i) => {
778
+ console.log(` ${chalk4.cyan(String(i + 1))}. ${labelFn(item)}`);
779
+ });
780
+ console.log();
781
+ const answer = await askQuestion3(prompt);
782
+ const index = parseInt(answer, 10) - 1;
783
+ if (isNaN(index) || index < 0 || index >= items.length) {
784
+ console.log(chalk4.yellow(" Invalid selection. Cancelled."));
785
+ return null;
786
+ }
787
+ return items[index];
788
+ }
789
+ var UUID_PREFIX_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-/i;
790
+ async function resolveFile(client, idOrName) {
791
+ if (isUUID(idOrName)) {
792
+ const { data } = await client.get(`/v1/media/files/${idOrName}`);
793
+ return data;
794
+ }
795
+ const uuidPrefix = idOrName.match(UUID_PREFIX_RE);
796
+ if (uuidPrefix) {
797
+ try {
798
+ const { data } = await client.get(`/v1/media/files/${uuidPrefix[1]}`);
799
+ return data;
800
+ } catch {
801
+ }
802
+ }
803
+ const { data: matches } = await client.get("/v1/media/files/search", {
804
+ params: { name: idOrName }
805
+ });
806
+ const exact = matches.filter(
807
+ (f) => f.originalName?.toLowerCase() === idOrName.toLowerCase() || f.filename?.toLowerCase() === idOrName.toLowerCase()
808
+ );
809
+ const results = exact.length > 0 ? exact : matches;
810
+ if (results.length === 0) {
811
+ console.error(chalk4.red(`No file found matching "${idOrName}"`));
812
+ return null;
813
+ }
814
+ if (results.length === 1) return results[0];
815
+ console.log(chalk4.bold(` Found ${results.length} files matching "${idOrName}":`));
816
+ return pickOne(results, (f) => `${f.originalName} (${f.id})`, ` Select file (1-${results.length}): `);
817
+ }
818
+ async function resolveFolder(client, idOrName) {
819
+ const { data: folders } = await client.get("/v1/media/folders");
820
+ if (isUUID(idOrName)) {
821
+ const match = folders.find((f) => f.id === idOrName);
822
+ if (!match) {
823
+ console.error(chalk4.red(`No folder found matching "${idOrName}"`));
824
+ return null;
825
+ }
826
+ return match;
827
+ }
828
+ const matches = folders.filter(
829
+ (f) => f.name?.toLowerCase() === idOrName.toLowerCase()
830
+ );
831
+ if (matches.length === 0) {
832
+ console.error(chalk4.red(`No folder found matching "${idOrName}"`));
833
+ return null;
834
+ }
835
+ if (matches.length === 1) return matches[0];
836
+ console.log(chalk4.bold(` Found ${matches.length} folders matching "${idOrName}":`));
837
+ return pickOne(matches, (f) => `${f.name} (${f.id})`, ` Select folder (1-${matches.length}): `);
838
+ }
839
+ async function resolveTarget(client, idOrName) {
840
+ const { data: folders } = await client.get("/v1/media/folders");
841
+ let folderMatches;
842
+ let fileMatches;
843
+ if (isUUID(idOrName)) {
844
+ folderMatches = folders.filter((f) => f.id === idOrName);
845
+ try {
846
+ const { data } = await client.get(`/v1/media/files/${idOrName}`);
847
+ fileMatches = data ? [data] : [];
848
+ } catch {
849
+ fileMatches = [];
850
+ }
851
+ } else {
852
+ folderMatches = folders.filter((f) => f.name?.toLowerCase() === idOrName.toLowerCase());
853
+ const { data: searchResults } = await client.get("/v1/media/files/search", {
854
+ params: { name: idOrName }
855
+ });
856
+ const exact = searchResults.filter(
857
+ (f) => f.originalName?.toLowerCase() === idOrName.toLowerCase() || f.filename?.toLowerCase() === idOrName.toLowerCase()
858
+ );
859
+ fileMatches = exact.length > 0 ? exact : searchResults;
860
+ }
861
+ const all = [
862
+ ...folderMatches.map((f) => ({ type: "folder", item: f })),
863
+ ...fileMatches.map((f) => ({ type: "file", item: f }))
864
+ ];
865
+ if (all.length === 0) {
866
+ console.error(chalk4.red(`No file or folder found matching "${idOrName}"`));
867
+ return null;
868
+ }
869
+ if (all.length === 1) return all[0];
870
+ console.log(chalk4.bold(` Found ${all.length} items matching "${idOrName}":`));
871
+ const picked = await pickOne(
872
+ all,
873
+ (entry) => {
874
+ const label = entry.type === "folder" ? entry.item.name : entry.item.originalName;
875
+ return `[${entry.type}] ${label} (${entry.item.id})`;
876
+ },
877
+ ` Select item (1-${all.length}): `
878
+ );
879
+ return picked;
880
+ }
881
+ function registerMediaCommands(program2) {
882
+ program2.command("media:upload").description("Upload a file (presign + upload to S3)").argument("[file]", "Path to file (or drag & drop)").option("--folder <id-or-name>", "Upload into a specific folder").addHelpText("after", `
883
+ Examples:
884
+ $ so-me media:upload ./photo.jpg
885
+ $ so-me media:upload ./photo.jpg --folder Assets
886
+ $ so-me media:upload # prompts for path (drag & drop a file)`).action(async (filePath, opts) => {
887
+ if (!filePath) {
888
+ filePath = await askQuestion3(" Drop a file here (or enter path): ");
889
+ }
890
+ filePath = filePath.replace(/^['"]|['"]$/g, "").trim();
891
+ if (!filePath) {
892
+ console.error(chalk4.red("No file path provided."));
893
+ process.exitCode = 1;
894
+ return;
895
+ }
896
+ const globalOpts = getGlobalOpts3(program2);
897
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
898
+ const spinner = ora4("Uploading file...").start();
899
+ try {
900
+ const resolvedPath = path2.resolve(filePath);
901
+ const stat = fs2.statSync(resolvedPath);
902
+ const filename = path2.basename(resolvedPath);
903
+ const ext = path2.extname(resolvedPath).toLowerCase();
904
+ const mimeTypes = {
905
+ ".png": "image/png",
906
+ ".jpg": "image/jpeg",
907
+ ".jpeg": "image/jpeg",
908
+ ".gif": "image/gif",
909
+ ".webp": "image/webp",
910
+ ".svg": "image/svg+xml",
911
+ ".mp4": "video/mp4",
912
+ ".mov": "video/quicktime",
913
+ ".avi": "video/x-msvideo",
914
+ ".webm": "video/webm",
915
+ ".mp3": "audio/mpeg",
916
+ ".wav": "audio/wav",
917
+ ".pdf": "application/pdf"
918
+ };
919
+ const mimetype = mimeTypes[ext] || "application/octet-stream";
920
+ let folderId;
921
+ if (opts.folder) {
922
+ spinner.text = "Resolving folder...";
923
+ if (isUUID(opts.folder)) {
924
+ folderId = opts.folder;
925
+ } else {
926
+ spinner.stop();
927
+ const folder = await resolveFolder(client, opts.folder);
928
+ if (!folder) {
929
+ process.exitCode = 1;
930
+ return;
931
+ }
932
+ folderId = folder.id;
933
+ spinner.start();
934
+ }
935
+ }
936
+ spinner.text = "Getting presigned URL...";
937
+ const { data: presignData } = await client.post("/v1/media/presign-upload", {
938
+ files: [{ filename, mimetype, size: stat.size }],
939
+ ...folderId && { folderId }
940
+ });
941
+ const presigned = presignData[0];
942
+ spinner.text = "Uploading to storage...";
943
+ const fileBuffer = fs2.readFileSync(resolvedPath);
944
+ await axios4.put(presigned.uploadUrl, fileBuffer, {
945
+ headers: { "Content-Type": mimetype },
946
+ maxBodyLength: Infinity
947
+ });
948
+ spinner.succeed("File uploaded");
949
+ printOutput({
950
+ fileId: presigned.fileId,
951
+ url: presigned.fileSrc,
952
+ filename,
953
+ size: stat.size
954
+ }, getFormat3(program2));
955
+ } catch (error) {
956
+ spinner.fail("Failed to upload file");
957
+ printError(error);
958
+ process.exitCode = 1;
959
+ }
960
+ });
961
+ program2.command("media:list").description("List media files and folders").option("--folder <id-or-name>", "Folder ID or name to list contents of").option("--file <id-or-name>", "File ID or name to get details of").action(async (opts) => {
962
+ const globalOpts = getGlobalOpts3(program2);
963
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
964
+ const spinner = ora4("Fetching media...").start();
965
+ try {
966
+ if (opts.file) {
967
+ spinner.text = "Fetching file...";
968
+ const file = await resolveFile(client, opts.file);
969
+ spinner.stop();
970
+ if (!file) {
971
+ process.exitCode = 1;
972
+ return;
973
+ }
974
+ printOutput(file, getFormat3(program2));
975
+ return;
976
+ }
977
+ let folderId;
978
+ if (opts.folder) {
979
+ spinner.text = "Resolving folder...";
980
+ if (isUUID(opts.folder)) {
981
+ folderId = opts.folder;
982
+ } else {
983
+ spinner.stop();
984
+ const folder = await resolveFolder(client, opts.folder);
985
+ if (!folder) {
986
+ process.exitCode = 1;
987
+ return;
988
+ }
989
+ folderId = folder.id;
990
+ spinner.start("Fetching media...");
991
+ }
992
+ }
993
+ const params = {};
994
+ if (folderId) params.folderId = folderId;
995
+ const { data } = await client.get("/v1/media", { params });
996
+ spinner.stop();
997
+ printOutput(data, getFormat3(program2));
998
+ } catch (error) {
999
+ spinner.fail("Failed to fetch media");
1000
+ printError(error);
1001
+ process.exitCode = 1;
1002
+ }
1003
+ });
1004
+ program2.command("media:delete").description("Delete one or more media files, or a folder").argument("<items...>", "File/folder ID(s) or name(s)").option("--type <type>", "Type: file (default) or folder", "file").option("-y, --yes", "Skip confirmation").action(async (items, opts) => {
1005
+ const globalOpts = getGlobalOpts3(program2);
1006
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1007
+ const spinner = ora4("Resolving...").start();
1008
+ try {
1009
+ if (opts.type === "folder") {
1010
+ if (items.length > 1) {
1011
+ spinner.fail("Only one folder can be deleted at a time.");
1012
+ process.exitCode = 1;
1013
+ return;
1014
+ }
1015
+ spinner.stop();
1016
+ const folder = await resolveFolder(client, items[0]);
1017
+ if (!folder) {
1018
+ process.exitCode = 1;
1019
+ return;
1020
+ }
1021
+ if (!opts.yes) {
1022
+ const answer = await askQuestion3(
1023
+ ` Delete folder "${folder.name}" and all its contents? (y/N) `
1024
+ );
1025
+ if (answer.toLowerCase() !== "y") {
1026
+ console.log(chalk4.dim(" Cancelled."));
1027
+ return;
1028
+ }
1029
+ }
1030
+ const deleteSpinner2 = ora4("Deleting folder...").start();
1031
+ await client.delete(`/v1/media/folders/${folder.id}`);
1032
+ deleteSpinner2.succeed(`Folder "${folder.name}" deleted.`);
1033
+ return;
1034
+ }
1035
+ spinner.stop();
1036
+ const resolved = [];
1037
+ for (const item of items) {
1038
+ const file = await resolveFile(client, item);
1039
+ if (!file) {
1040
+ process.exitCode = 1;
1041
+ return;
1042
+ }
1043
+ resolved.push(file);
1044
+ }
1045
+ if (!opts.yes) {
1046
+ if (resolved.length === 1) {
1047
+ const answer = await askQuestion3(
1048
+ ` Delete file "${resolved[0].originalName}"? (y/N) `
1049
+ );
1050
+ if (answer.toLowerCase() !== "y") {
1051
+ console.log(chalk4.dim(" Cancelled."));
1052
+ return;
1053
+ }
1054
+ } else {
1055
+ console.log();
1056
+ console.log(chalk4.bold(` Files to delete:`));
1057
+ console.log();
1058
+ resolved.forEach((f, i) => {
1059
+ console.log(` ${chalk4.cyan(String(i + 1))}. ${f.originalName} (${f.id})`);
1060
+ });
1061
+ console.log();
1062
+ const answer = await askQuestion3(
1063
+ ` Delete ${resolved.length} files? (y/N) `
1064
+ );
1065
+ if (answer.toLowerCase() !== "y") {
1066
+ console.log(chalk4.dim(" Cancelled."));
1067
+ return;
1068
+ }
1069
+ }
1070
+ }
1071
+ const deleteSpinner = ora4("Deleting...").start();
1072
+ if (resolved.length === 1) {
1073
+ await client.delete(`/v1/media/${resolved[0].id}`);
1074
+ deleteSpinner.succeed(`File "${resolved[0].originalName}" deleted.`);
1075
+ } else {
1076
+ await client.post("/v1/media/files/bulk-delete", {
1077
+ fileIds: resolved.map((f) => f.id)
1078
+ });
1079
+ deleteSpinner.succeed(`${resolved.length} files deleted.`);
1080
+ }
1081
+ } catch (error) {
1082
+ spinner.fail("Failed to delete");
1083
+ printError(error);
1084
+ process.exitCode = 1;
1085
+ }
1086
+ });
1087
+ program2.command("media:move").description("Move a file or folder to a different location").argument("<id-or-name>", "File or folder ID or name").requiredOption("--to <folder-id-or-name>", 'Destination folder ID, name, or "root"').action(async (idOrName, opts) => {
1088
+ const globalOpts = getGlobalOpts3(program2);
1089
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1090
+ const spinner = ora4("Resolving...").start();
1091
+ try {
1092
+ spinner.stop();
1093
+ const source = await resolveTarget(client, idOrName);
1094
+ if (!source) {
1095
+ process.exitCode = 1;
1096
+ return;
1097
+ }
1098
+ let destinationId;
1099
+ let destinationName = "root";
1100
+ if (opts.to.toLowerCase() !== "root") {
1101
+ const destFolder = await resolveFolder(client, opts.to);
1102
+ if (!destFolder) {
1103
+ process.exitCode = 1;
1104
+ return;
1105
+ }
1106
+ destinationId = destFolder.id;
1107
+ destinationName = destFolder.name;
1108
+ }
1109
+ const sourceName = source.type === "folder" ? source.item.name : source.item.originalName;
1110
+ const moveSpinner = ora4(`Moving "${sourceName}" to "${destinationName}"...`).start();
1111
+ if (source.type === "file") {
1112
+ await client.patch(`/v1/media/files/${source.item.id}/move`, {
1113
+ folderId: destinationId || null
1114
+ });
1115
+ } else {
1116
+ await client.patch(`/v1/media/folders/${source.item.id}/move`, {
1117
+ parentId: destinationId || null
1118
+ });
1119
+ }
1120
+ moveSpinner.succeed(`Moved "${sourceName}" to "${destinationName}".`);
1121
+ } catch (error) {
1122
+ spinner.fail("Failed to move");
1123
+ printError(error);
1124
+ process.exitCode = 1;
1125
+ }
1126
+ });
1127
+ program2.command("media:rename").description("Rename a file or folder").argument("<id-or-name>", "File or folder ID or name").requiredOption("--name <new-name>", "New name").option("--type <type>", "Type: file (default) or folder", "file").action(async (idOrName, opts) => {
1128
+ const globalOpts = getGlobalOpts3(program2);
1129
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1130
+ const spinner = ora4("Resolving...").start();
1131
+ try {
1132
+ if (opts.type === "folder") {
1133
+ spinner.stop();
1134
+ const folder = await resolveFolder(client, idOrName);
1135
+ if (!folder) {
1136
+ process.exitCode = 1;
1137
+ return;
1138
+ }
1139
+ const renameSpinner = ora4("Renaming folder...").start();
1140
+ await client.patch(`/v1/media/folders/${folder.id}/rename`, { name: opts.name });
1141
+ renameSpinner.succeed(`Renamed "${folder.name}" to "${opts.name}".`);
1142
+ } else {
1143
+ spinner.stop();
1144
+ const file = await resolveFile(client, idOrName);
1145
+ if (!file) {
1146
+ process.exitCode = 1;
1147
+ return;
1148
+ }
1149
+ const renameSpinner = ora4("Renaming file...").start();
1150
+ await client.patch(`/v1/media/files/${file.id}/rename`, { name: opts.name });
1151
+ renameSpinner.succeed(`Renamed "${file.originalName}" to "${opts.name}".`);
1152
+ }
1153
+ } catch (error) {
1154
+ spinner.fail("Failed to rename");
1155
+ printError(error);
1156
+ process.exitCode = 1;
1157
+ }
1158
+ });
1159
+ program2.command("media:folders").description("List media folders").action(async () => {
1160
+ const globalOpts = getGlobalOpts3(program2);
1161
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1162
+ const spinner = ora4("Fetching folders...").start();
1163
+ try {
1164
+ const { data } = await client.get("/v1/media/folders");
1165
+ spinner.stop();
1166
+ printOutput(data, getFormat3(program2));
1167
+ } catch (error) {
1168
+ spinner.fail("Failed to fetch folders");
1169
+ printError(error);
1170
+ process.exitCode = 1;
1171
+ }
1172
+ });
1173
+ program2.command("media:create-folder").description("Create a media folder").requiredOption("--name <name>", "Folder name").action(async (opts) => {
1174
+ const globalOpts = getGlobalOpts3(program2);
1175
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1176
+ const spinner = ora4("Creating folder...").start();
1177
+ try {
1178
+ const { data } = await client.post("/v1/media/folders", { name: opts.name });
1179
+ spinner.succeed("Folder created");
1180
+ printOutput(data, getFormat3(program2));
1181
+ } catch (error) {
1182
+ spinner.fail("Failed to create folder");
1183
+ printError(error);
1184
+ process.exitCode = 1;
1185
+ }
1186
+ });
1187
+ }
1188
+
1189
+ // src/commands/analytics.ts
1190
+ import chalk5 from "chalk";
1191
+ import ora5 from "ora";
1192
+ function getFormat4(program2) {
1193
+ const opts = program2.opts();
1194
+ if (opts.table) return "table";
1195
+ return getConfig().outputFormat || "json";
1196
+ }
1197
+ function getGlobalOpts4(program2) {
1198
+ return program2.opts();
1199
+ }
1200
+ var PLATFORM_SECTIONS = {
1201
+ facebook: { sections: ["page", "posts", "videos"], description: "Facebook" },
1202
+ instagram: { sections: ["account", "media", "stories"], description: "Instagram" },
1203
+ linkedin: { sections: ["page", "posts"], description: "LinkedIn" },
1204
+ linkedin_page: { sections: ["page", "posts"], description: "LinkedIn Page" },
1205
+ youtube: { sections: ["channel", "videos"], description: "YouTube" },
1206
+ x: { sections: ["account", "content"], description: "Twitter / X" },
1207
+ twitter: { sections: ["account", "content"], description: "Twitter / X" },
1208
+ whatsapp: { sections: ["account"], description: "WhatsApp" }
1209
+ };
1210
+ function resolveCliPlatform(input) {
1211
+ const lower = input.toLowerCase();
1212
+ if (lower === "twitter") return "x";
1213
+ if (lower === "linkedin_page") return "linkedin";
1214
+ return lower;
1215
+ }
1216
+ function registerAnalyticsCommands(program2) {
1217
+ program2.command("analytics:platform").description("Get analytics for a platform section").argument("<account-id-or-platform>", "Account ID or platform name (facebook, instagram, linkedin, youtube, x, whatsapp)").argument("[section]", "Section (e.g. page, posts, videos, account, media, stories, channel, content)").option("-d, --days <number>", "Lookback period in days (7, 30, 90)", "7").addHelpText("after", `
1218
+ Sections per platform:
1219
+ facebook page, posts, videos
1220
+ instagram account, media, stories
1221
+ linkedin page, posts
1222
+ youtube channel, videos
1223
+ x / twitter account, content
1224
+ whatsapp account
1225
+
1226
+ Examples:
1227
+ $ so-me analytics:platform facebook page --days 30
1228
+ $ so-me analytics:platform instagram media --days 7
1229
+ $ so-me analytics:platform <account-id> posts --days 90`).action(async (identifier, section, opts) => {
1230
+ const globalOpts = getGlobalOpts4(program2);
1231
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1232
+ const platformInfo = PLATFORM_SECTIONS[identifier.toLowerCase()];
1233
+ if (platformInfo && !section) {
1234
+ console.log(chalk5.bold(` Available sections for ${platformInfo.description}:`));
1235
+ console.log();
1236
+ platformInfo.sections.forEach((s) => {
1237
+ console.log(` ${chalk5.cyan(s)}`);
1238
+ });
1239
+ console.log();
1240
+ console.log(chalk5.dim(` Usage: so-me analytics:platform ${identifier} <section> --days <number>`));
1241
+ return;
1242
+ }
1243
+ if (!section) {
1244
+ const spinner2 = ora5("Fetching analytics...").start();
1245
+ try {
1246
+ const { data } = await client.get(`/v1/analytics/platform/${identifier}`, {
1247
+ params: { days: opts.days }
1248
+ });
1249
+ spinner2.stop();
1250
+ printOutput(data, getFormat4(program2));
1251
+ } catch (error) {
1252
+ spinner2.fail("Failed to fetch analytics");
1253
+ printError(error);
1254
+ process.exitCode = 1;
1255
+ }
1256
+ return;
1257
+ }
1258
+ if (platformInfo && !platformInfo.sections.includes(section)) {
1259
+ console.error(chalk5.red(`Invalid section "${section}" for ${platformInfo.description}`));
1260
+ console.error(chalk5.dim(`Valid sections: ${platformInfo.sections.join(", ")}`));
1261
+ process.exitCode = 1;
1262
+ return;
1263
+ }
1264
+ const apiPlatform = resolveCliPlatform(identifier);
1265
+ const spinner = ora5(`Fetching ${identifier} ${section} analytics...`).start();
1266
+ try {
1267
+ const { data } = await client.get(
1268
+ `/v1/analytics/${apiPlatform}/${identifier}/${section}`,
1269
+ { params: { days: opts.days } }
1270
+ );
1271
+ spinner.stop();
1272
+ printOutput(data, getFormat4(program2));
1273
+ } catch (error) {
1274
+ spinner.fail("Failed to fetch analytics");
1275
+ printError(error);
1276
+ process.exitCode = 1;
1277
+ }
1278
+ });
1279
+ }
1280
+
1281
+ // src/commands/inbox.ts
1282
+ import chalk6 from "chalk";
1283
+ import ora6 from "ora";
1284
+ import { createInterface as createInterface4 } from "readline";
1285
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1286
+ async function resolveAccount(client, identifier) {
1287
+ const { data: accounts } = await client.get("/v1/accounts");
1288
+ if (UUID_RE2.test(identifier)) {
1289
+ const match = accounts.find((a) => a.id === identifier);
1290
+ if (!match) {
1291
+ console.error(chalk6.red(`No account found matching "${identifier}"`));
1292
+ return null;
1293
+ }
1294
+ return match;
1295
+ }
1296
+ const platform = identifier.toUpperCase();
1297
+ const matches = accounts.filter((a) => a.platform === platform);
1298
+ if (matches.length === 0) {
1299
+ console.error(chalk6.red(`No ${identifier} account found`));
1300
+ return null;
1301
+ }
1302
+ if (matches.length === 1) return matches[0];
1303
+ console.log(chalk6.bold(` Found ${matches.length} ${identifier} accounts:`));
1304
+ console.log();
1305
+ matches.forEach((a, i) => {
1306
+ const name = a.accountName || a.userName || a.accountId;
1307
+ console.log(` ${chalk6.cyan(String(i + 1))}. ${name}`);
1308
+ });
1309
+ console.log();
1310
+ const answer = await askQuestion4(` Select account (1-${matches.length}): `);
1311
+ const index = parseInt(answer, 10) - 1;
1312
+ if (isNaN(index) || index < 0 || index >= matches.length) {
1313
+ console.log(chalk6.yellow(" Invalid selection. Cancelled."));
1314
+ return null;
1315
+ }
1316
+ return matches[index];
1317
+ }
1318
+ function askQuestion4(question) {
1319
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
1320
+ return new Promise((resolve) => {
1321
+ rl.question(question, (answer) => {
1322
+ rl.close();
1323
+ resolve(answer.trim());
1324
+ });
1325
+ });
1326
+ }
1327
+ function getFormat5(program2) {
1328
+ const opts = program2.opts();
1329
+ if (opts.table) return "table";
1330
+ return getConfig().outputFormat || "json";
1331
+ }
1332
+ function getGlobalOpts5(program2) {
1333
+ return program2.opts();
1334
+ }
1335
+ function registerInboxCommands(program2) {
1336
+ program2.command("inbox:list").description("List inbox conversations").option("--status <status>", "Filter by status: resolved, unresolved, archived").option("--platform <platform>", "Filter by platform: facebook, instagram, twitter, linkedin, tiktok, youtube, threads, whatsapp").option("--type <type>", "Filter by type: message, comment, mention").option("--read", "Show only read conversations").option("--unread", "Show only unread conversations").option("--search <query>", "Search by sender name, message, or page name").addHelpText("after", `
1337
+ Examples:
1338
+ $ so-me inbox:list --status unresolved
1339
+ $ so-me inbox:list --platform facebook --type comment
1340
+ $ so-me inbox:list --unread
1341
+ $ so-me inbox:list --platform instagram --unread --type message
1342
+ $ so-me inbox:list --search "john"`).action(async (opts) => {
1343
+ const globalOpts = getGlobalOpts5(program2);
1344
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1345
+ const spinner = ora6("Fetching conversations...").start();
1346
+ try {
1347
+ const { data } = await client.get("/v1/inbox/conversations");
1348
+ spinner.stop();
1349
+ let results = Array.isArray(data) ? data : [];
1350
+ if (opts.status) {
1351
+ results = results.filter(
1352
+ (t) => t.threadStatus?.toLowerCase() === opts.status.toLowerCase()
1353
+ );
1354
+ }
1355
+ if (opts.platform) {
1356
+ results = results.filter(
1357
+ (t) => t.platform?.toLowerCase() === opts.platform.toLowerCase()
1358
+ );
1359
+ }
1360
+ if (opts.type) {
1361
+ results = results.filter(
1362
+ (t) => t.threadType?.toLowerCase() === opts.type.toLowerCase()
1363
+ );
1364
+ }
1365
+ if (opts.read) {
1366
+ results = results.filter((t) => t.isSeen === true);
1367
+ }
1368
+ if (opts.unread) {
1369
+ results = results.filter((t) => t.isSeen === false);
1370
+ }
1371
+ if (opts.search) {
1372
+ const q = opts.search.toLowerCase();
1373
+ results = results.filter(
1374
+ (t) => t.senderName?.toLowerCase().includes(q) || t.lastMessage?.toLowerCase().includes(q) || t.pageName?.toLowerCase().includes(q)
1375
+ );
1376
+ }
1377
+ printOutput(results, getFormat5(program2));
1378
+ } catch (error) {
1379
+ spinner.fail("Failed to fetch conversations");
1380
+ printError(error);
1381
+ process.exitCode = 1;
1382
+ }
1383
+ });
1384
+ program2.command("inbox:messages").description("Get messages in a conversation thread").argument("<thread-id>", "Thread ID").action(async (threadId) => {
1385
+ const globalOpts = getGlobalOpts5(program2);
1386
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1387
+ const spinner = ora6("Fetching messages...").start();
1388
+ try {
1389
+ const { data } = await client.get(`/v1/inbox/conversations/${threadId}/messages`);
1390
+ spinner.stop();
1391
+ printOutput(data, getFormat5(program2));
1392
+ } catch (error) {
1393
+ spinner.fail("Failed to fetch messages");
1394
+ printError(error);
1395
+ process.exitCode = 1;
1396
+ }
1397
+ });
1398
+ program2.command("inbox:read").description("Mark a conversation as read").argument("<thread-id>", "Thread ID").action(async (threadId) => {
1399
+ const globalOpts = getGlobalOpts5(program2);
1400
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1401
+ const spinner = ora6("Marking as read...").start();
1402
+ try {
1403
+ await client.patch(`/v1/inbox/conversations/${threadId}`, { isSeen: true });
1404
+ spinner.succeed("Conversation marked as read.");
1405
+ } catch (error) {
1406
+ spinner.fail("Failed to mark as read");
1407
+ printError(error);
1408
+ process.exitCode = 1;
1409
+ }
1410
+ });
1411
+ program2.command("inbox:resolve").description("Mark a conversation as resolved, unresolved, or archived").argument("<thread-id>", "Thread ID").requiredOption("--status <status>", "Status: resolved, unresolved, archived").action(async (threadId, opts) => {
1412
+ const validStatuses = ["resolved", "unresolved", "archived"];
1413
+ if (!validStatuses.includes(opts.status.toLowerCase())) {
1414
+ console.error(chalk6.red(`Invalid status: ${opts.status}`));
1415
+ console.error(chalk6.dim(`Valid options: ${validStatuses.join(", ")}`));
1416
+ process.exitCode = 1;
1417
+ return;
1418
+ }
1419
+ const globalOpts = getGlobalOpts5(program2);
1420
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1421
+ const spinner = ora6(`Marking as ${opts.status}...`).start();
1422
+ try {
1423
+ await client.patch(`/v1/inbox/conversations/${threadId}`, {
1424
+ threadStatus: opts.status.toLowerCase()
1425
+ });
1426
+ spinner.succeed(`Conversation marked as ${opts.status}.`);
1427
+ } catch (error) {
1428
+ spinner.fail("Failed to update status");
1429
+ printError(error);
1430
+ process.exitCode = 1;
1431
+ }
1432
+ });
1433
+ program2.command("inbox:delete").description("Delete one or more conversations").argument("<thread-ids...>", "Thread ID(s) to delete").option("-y, --yes", "Skip confirmation").action(async (threadIds, opts) => {
1434
+ const globalOpts = getGlobalOpts5(program2);
1435
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1436
+ if (!opts.yes) {
1437
+ const msg = threadIds.length === 1 ? ` Delete conversation ${threadIds[0]}? (y/N) ` : ` Delete ${threadIds.length} conversations? (y/N) `;
1438
+ const answer = await askQuestion4(msg);
1439
+ if (answer.toLowerCase() !== "y") {
1440
+ console.log(chalk6.dim(" Cancelled."));
1441
+ return;
1442
+ }
1443
+ }
1444
+ const spinner = ora6("Deleting...").start();
1445
+ try {
1446
+ let failed = 0;
1447
+ for (const id of threadIds) {
1448
+ try {
1449
+ await client.delete(`/v1/inbox/conversations/${id}`);
1450
+ } catch {
1451
+ failed++;
1452
+ }
1453
+ }
1454
+ if (failed === 0) {
1455
+ spinner.succeed(`${threadIds.length === 1 ? "Conversation" : `${threadIds.length} conversations`} deleted.`);
1456
+ } else {
1457
+ spinner.warn(`${threadIds.length - failed} deleted, ${failed} failed.`);
1458
+ }
1459
+ } catch (error) {
1460
+ spinner.fail("Failed to delete conversations");
1461
+ printError(error);
1462
+ process.exitCode = 1;
1463
+ }
1464
+ });
1465
+ program2.command("inbox:reply").description("Reply to a conversation with text, files, or both").argument("<thread-id>", "Thread ID").option("--message <text>", "Reply text message").option("--image <urls...>", "Image URLs to attach").option("--video <url>", "Video URL to attach").option("--audio <url>", "Audio URL to attach").option("--file <url>", "Document URL to attach").addHelpText("after", `
1466
+ Examples:
1467
+ $ so-me inbox:reply <thread-id> --message "Thanks!"
1468
+ $ so-me inbox:reply <thread-id> --message "Check this" --image https://cdn.example.com/photo.jpg
1469
+ $ so-me inbox:reply <thread-id> --image <url1> <url2>
1470
+ $ so-me inbox:reply <thread-id> --video https://cdn.example.com/clip.mp4
1471
+ $ so-me inbox:reply <thread-id> --file https://cdn.example.com/doc.pdf
1472
+ $ so-me inbox:reply <thread-id> --message "Here you go \u{1F60A}"`).action(async (threadId, opts) => {
1473
+ if (!opts.message && !opts.image && !opts.video && !opts.audio && !opts.file) {
1474
+ console.error(chalk6.red("Provide --message, --image, --video, --audio, --file, or a combination."));
1475
+ process.exitCode = 1;
1476
+ return;
1477
+ }
1478
+ const attachments = [];
1479
+ if (opts.image) {
1480
+ for (const url of opts.image) {
1481
+ attachments.push({ type: "image", url });
1482
+ }
1483
+ }
1484
+ if (opts.video) {
1485
+ attachments.push({ type: "video", url: opts.video });
1486
+ }
1487
+ if (opts.audio) {
1488
+ attachments.push({ type: "audio", url: opts.audio });
1489
+ }
1490
+ if (opts.file) {
1491
+ attachments.push({ type: "document", url: opts.file });
1492
+ }
1493
+ const globalOpts = getGlobalOpts5(program2);
1494
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1495
+ const spinner = ora6("Sending reply...").start();
1496
+ try {
1497
+ const { data } = await client.post(`/v1/inbox/conversations/${threadId}/reply`, {
1498
+ ...opts.message && { message: opts.message },
1499
+ ...attachments.length > 0 && { attachments }
1500
+ });
1501
+ spinner.succeed("Reply sent");
1502
+ printOutput(data, getFormat5(program2));
1503
+ } catch (error) {
1504
+ spinner.fail("Failed to send reply");
1505
+ printError(error);
1506
+ process.exitCode = 1;
1507
+ }
1508
+ });
1509
+ program2.command("inbox:subscribe").description("Enable inbox for a social account").argument("<id-or-platform>", "Account ID or platform name (twitter, facebook, etc.)").action(async (identifier) => {
1510
+ const globalOpts = getGlobalOpts5(program2);
1511
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1512
+ const spinner = ora6("Resolving account...").start();
1513
+ try {
1514
+ const account = await resolveAccount(client, identifier);
1515
+ if (!account) {
1516
+ spinner.stop();
1517
+ process.exitCode = 1;
1518
+ return;
1519
+ }
1520
+ spinner.text = "Subscribing to inbox...";
1521
+ const { data } = await client.post("/v1/inbox/subscribe", {
1522
+ accountId: account.accountId,
1523
+ platform: account.platform
1524
+ });
1525
+ spinner.succeed(`Inbox enabled for ${account.accountName || account.platform}`);
1526
+ printOutput(data, getFormat5(program2));
1527
+ } catch (error) {
1528
+ spinner.fail("Failed to enable inbox");
1529
+ printError(error);
1530
+ process.exitCode = 1;
1531
+ }
1532
+ });
1533
+ program2.command("inbox:unsubscribe").description("Disable inbox for a social account").argument("<id-or-platform>", "Account ID or platform name (twitter, facebook, etc.)").action(async (identifier) => {
1534
+ const globalOpts = getGlobalOpts5(program2);
1535
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1536
+ const spinner = ora6("Resolving account...").start();
1537
+ try {
1538
+ const account = await resolveAccount(client, identifier);
1539
+ if (!account) {
1540
+ spinner.stop();
1541
+ process.exitCode = 1;
1542
+ return;
1543
+ }
1544
+ spinner.text = "Unsubscribing from inbox...";
1545
+ const { data } = await client.delete("/v1/inbox/unsubscribe", {
1546
+ data: { accountId: account.accountId, platform: account.platform }
1547
+ });
1548
+ spinner.succeed(`Inbox disabled for ${account.accountName || account.platform}`);
1549
+ printOutput(data, getFormat5(program2));
1550
+ } catch (error) {
1551
+ spinner.fail("Failed to disable inbox");
1552
+ printError(error);
1553
+ process.exitCode = 1;
1554
+ }
1555
+ });
1556
+ program2.command("inbox:saved-replies").description("List saved replies or get a single reply").argument("[id]", "Reply ID to get details of").action(async (id) => {
1557
+ const globalOpts = getGlobalOpts5(program2);
1558
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1559
+ const spinner = ora6("Fetching saved replies...").start();
1560
+ try {
1561
+ if (id) {
1562
+ const { data } = await client.get(`/v1/inbox/saved-replies/${id}`);
1563
+ spinner.stop();
1564
+ printOutput(data, getFormat5(program2));
1565
+ } else {
1566
+ const { data } = await client.get("/v1/inbox/saved-replies");
1567
+ spinner.stop();
1568
+ printOutput(data, getFormat5(program2));
1569
+ }
1570
+ } catch (error) {
1571
+ spinner.fail("Failed to fetch saved replies");
1572
+ printError(error);
1573
+ process.exitCode = 1;
1574
+ }
1575
+ });
1576
+ program2.command("inbox:create-reply").description("Create a saved reply").option("--text <text>", "Reply text content").option("--file <file-ids...>", "File IDs to attach (max 4 images)").addHelpText("after", `
1577
+ Examples:
1578
+ $ so-me inbox:create-reply --text "Thanks for reaching out!"
1579
+ $ so-me inbox:create-reply --text "Check this out" --file <file-id>
1580
+ $ so-me inbox:create-reply --file <id1> <id2>`).action(async (opts) => {
1581
+ if (!opts.text && (!opts.file || opts.file.length === 0)) {
1582
+ console.error(chalk6.red("Provide --text, --file, or both."));
1583
+ process.exitCode = 1;
1584
+ return;
1585
+ }
1586
+ const globalOpts = getGlobalOpts5(program2);
1587
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1588
+ const spinner = ora6("Creating saved reply...").start();
1589
+ try {
1590
+ const { data } = await client.post("/v1/inbox/saved-replies", {
1591
+ ...opts.text && { text: opts.text },
1592
+ ...opts.file && { fileIds: opts.file }
1593
+ });
1594
+ spinner.succeed("Saved reply created");
1595
+ printOutput(data, getFormat5(program2));
1596
+ } catch (error) {
1597
+ spinner.fail("Failed to create saved reply");
1598
+ printError(error);
1599
+ process.exitCode = 1;
1600
+ }
1601
+ });
1602
+ program2.command("inbox:edit-reply").description("Edit a saved reply").argument("<id>", "Saved reply ID").option("--text <text>", "New reply text").option("--file <file-ids...>", "New file IDs (replaces existing, max 4 images)").action(async (id, opts) => {
1603
+ if (!opts.text && !opts.file) {
1604
+ console.error(chalk6.red("Provide --text, --file, or both to update."));
1605
+ process.exitCode = 1;
1606
+ return;
1607
+ }
1608
+ const globalOpts = getGlobalOpts5(program2);
1609
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1610
+ const spinner = ora6("Updating saved reply...").start();
1611
+ try {
1612
+ const { data } = await client.patch(`/v1/inbox/saved-replies/${id}`, {
1613
+ ...opts.text && { text: opts.text },
1614
+ ...opts.file && { fileIds: opts.file }
1615
+ });
1616
+ spinner.succeed("Saved reply updated");
1617
+ printOutput(data, getFormat5(program2));
1618
+ } catch (error) {
1619
+ spinner.fail("Failed to update saved reply");
1620
+ printError(error);
1621
+ process.exitCode = 1;
1622
+ }
1623
+ });
1624
+ program2.command("inbox:delete-reply").description("Delete one or more saved replies").argument("<ids...>", "Saved reply ID(s)").option("-y, --yes", "Skip confirmation").action(async (ids, opts) => {
1625
+ const globalOpts = getGlobalOpts5(program2);
1626
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1627
+ if (!opts.yes) {
1628
+ const msg = ids.length === 1 ? ` Delete saved reply ${ids[0]}? (y/N) ` : ` Delete ${ids.length} saved replies? (y/N) `;
1629
+ const answer = await askQuestion4(msg);
1630
+ if (answer.toLowerCase() !== "y") {
1631
+ console.log(chalk6.dim(" Cancelled."));
1632
+ return;
1633
+ }
1634
+ }
1635
+ const spinner = ora6("Deleting...").start();
1636
+ try {
1637
+ let failed = 0;
1638
+ for (const id of ids) {
1639
+ try {
1640
+ await client.delete(`/v1/inbox/saved-replies/${id}`);
1641
+ } catch {
1642
+ failed++;
1643
+ }
1644
+ }
1645
+ if (failed === 0) {
1646
+ spinner.succeed(`${ids.length === 1 ? "Saved reply" : `${ids.length} saved replies`} deleted.`);
1647
+ } else {
1648
+ spinner.warn(`${ids.length - failed} deleted, ${failed} failed.`);
1649
+ }
1650
+ } catch (error) {
1651
+ spinner.fail("Failed to delete saved replies");
1652
+ printError(error);
1653
+ process.exitCode = 1;
1654
+ }
1655
+ });
1656
+ }
1657
+
1658
+ // src/commands/approvals.ts
1659
+ import ora7 from "ora";
1660
+ function getFormat6(program2) {
1661
+ const opts = program2.opts();
1662
+ if (opts.table) return "table";
1663
+ return getConfig().outputFormat || "json";
1664
+ }
1665
+ function getGlobalOpts6(program2) {
1666
+ return program2.opts();
1667
+ }
1668
+ function registerApprovalsCommands(program2) {
1669
+ program2.command("approvals:list").description("List posts pending approval").action(async () => {
1670
+ const globalOpts = getGlobalOpts6(program2);
1671
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1672
+ const spinner = ora7("Fetching pending approvals...").start();
1673
+ try {
1674
+ const { data } = await client.get("/v1/approvals");
1675
+ spinner.stop();
1676
+ printOutput(data, getFormat6(program2));
1677
+ } catch (error) {
1678
+ spinner.fail("Failed to fetch approvals");
1679
+ printError(error);
1680
+ process.exitCode = 1;
1681
+ }
1682
+ });
1683
+ program2.command("approvals:approve").description("Approve a post").argument("<post-id>", "Post ID").option("--comment <text>", "Approval comment").action(async (postId, opts) => {
1684
+ const globalOpts = getGlobalOpts6(program2);
1685
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1686
+ const spinner = ora7("Approving post...").start();
1687
+ try {
1688
+ const body = {};
1689
+ if (opts.comment) body.comment = opts.comment;
1690
+ const { data } = await client.post(`/v1/approvals/${postId}/approve`, body);
1691
+ spinner.succeed(`Post ${postId} approved`);
1692
+ printOutput(data, getFormat6(program2));
1693
+ } catch (error) {
1694
+ spinner.fail("Failed to approve post");
1695
+ printError(error);
1696
+ process.exitCode = 1;
1697
+ }
1698
+ });
1699
+ program2.command("approvals:reject").description("Reject a post").argument("<post-id>", "Post ID").requiredOption("--comment <text>", "Rejection reason").action(async (postId, opts) => {
1700
+ const globalOpts = getGlobalOpts6(program2);
1701
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1702
+ const spinner = ora7("Rejecting post...").start();
1703
+ try {
1704
+ const { data } = await client.post(`/v1/approvals/${postId}/reject`, {
1705
+ comment: opts.comment
1706
+ });
1707
+ spinner.succeed(`Post ${postId} rejected`);
1708
+ printOutput(data, getFormat6(program2));
1709
+ } catch (error) {
1710
+ spinner.fail("Failed to reject post");
1711
+ printError(error);
1712
+ process.exitCode = 1;
1713
+ }
1714
+ });
1715
+ }
1716
+
1717
+ // src/commands/teams.ts
1718
+ import chalk7 from "chalk";
1719
+ import ora8 from "ora";
1720
+ import { createInterface as createInterface5 } from "readline";
1721
+ function getFormat7(program2) {
1722
+ const opts = program2.opts();
1723
+ if (opts.table) return "table";
1724
+ return getConfig().outputFormat || "json";
1725
+ }
1726
+ function getGlobalOpts7(program2) {
1727
+ return program2.opts();
1728
+ }
1729
+ var POST_APPROVALS = ["requires_approval", "auto_approve"];
1730
+ function askQuestion5(question) {
1731
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
1732
+ return new Promise((resolve) => {
1733
+ rl.question(question, (answer) => {
1734
+ rl.close();
1735
+ resolve(answer.trim());
1736
+ });
1737
+ });
1738
+ }
1739
+ function registerTeamsCommands(program2) {
1740
+ program2.command("teams:list").description("List team members").action(async () => {
1741
+ const globalOpts = getGlobalOpts7(program2);
1742
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1743
+ const spinner = ora8("Fetching team members...").start();
1744
+ try {
1745
+ const { data } = await client.get("/v1/teams/members");
1746
+ spinner.stop();
1747
+ printOutput(data, getFormat7(program2));
1748
+ } catch (error) {
1749
+ spinner.fail("Failed to fetch team members");
1750
+ printError(error);
1751
+ process.exitCode = 1;
1752
+ }
1753
+ });
1754
+ program2.command("teams:remove").description("Remove a team member by ID or email").argument("<id-or-email>", "Member ID or email address").action(async (identifier) => {
1755
+ const globalOpts = getGlobalOpts7(program2);
1756
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1757
+ const confirm = await askQuestion5(
1758
+ ` Remove team member "${identifier}"? (y/N) `
1759
+ );
1760
+ if (confirm.toLowerCase() !== "y") {
1761
+ console.log(chalk7.dim(" Cancelled."));
1762
+ return;
1763
+ }
1764
+ const spinner = ora8("Removing member...").start();
1765
+ try {
1766
+ await client.delete(`/v1/teams/members/${encodeURIComponent(identifier)}`);
1767
+ spinner.succeed(`Member "${identifier}" removed.`);
1768
+ } catch (error) {
1769
+ spinner.fail("Failed to remove member");
1770
+ printError(error);
1771
+ process.exitCode = 1;
1772
+ }
1773
+ });
1774
+ program2.command("teams:update").description("Update a team member settings").argument("<id-or-email>", "Member ID or email address").option("--post-approval <approval>", "Post approval: requires_approval or auto_approve").action(async (identifier, opts) => {
1775
+ if (opts.postApproval && !POST_APPROVALS.includes(opts.postApproval)) {
1776
+ console.error(chalk7.red(`Invalid post approval: ${opts.postApproval}`));
1777
+ console.error(chalk7.dim(`Valid options: ${POST_APPROVALS.join(", ")}`));
1778
+ process.exitCode = 1;
1779
+ return;
1780
+ }
1781
+ const globalOpts = getGlobalOpts7(program2);
1782
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1783
+ const spinner = ora8("Updating member...").start();
1784
+ try {
1785
+ await client.put(`/v1/teams/members/${encodeURIComponent(identifier)}/role`, {
1786
+ role: "member",
1787
+ ...opts.postApproval && { postApproval: opts.postApproval }
1788
+ });
1789
+ spinner.succeed(`Updated "${identifier}"`);
1790
+ } catch (error) {
1791
+ spinner.fail("Failed to update member");
1792
+ printError(error);
1793
+ process.exitCode = 1;
1794
+ }
1795
+ });
1796
+ program2.command("teams:invite").description("Invite a team member").requiredOption("--email <email>", "Email address to invite").option("--post-approval <approval>", "Post approval: requires_approval (default) or auto_approve").action(async (opts) => {
1797
+ const globalOpts = getGlobalOpts7(program2);
1798
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1799
+ const spinner = ora8("Sending invitation...").start();
1800
+ try {
1801
+ const { data } = await client.post("/v1/teams/invite", {
1802
+ email: opts.email,
1803
+ role: "member",
1804
+ ...opts.postApproval && { postApproval: opts.postApproval }
1805
+ });
1806
+ spinner.succeed(`Invitation sent to ${opts.email}`);
1807
+ printOutput(data, getFormat7(program2));
1808
+ } catch (error) {
1809
+ spinner.fail("Failed to send invitation");
1810
+ printError(error);
1811
+ process.exitCode = 1;
1812
+ }
1813
+ });
1814
+ program2.command("teams:invitations").description("List pending team invitations").action(async () => {
1815
+ const globalOpts = getGlobalOpts7(program2);
1816
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1817
+ const spinner = ora8("Fetching invitations...").start();
1818
+ try {
1819
+ const { data } = await client.get("/v1/teams/invitations");
1820
+ spinner.stop();
1821
+ printOutput(data, getFormat7(program2));
1822
+ } catch (error) {
1823
+ spinner.fail("Failed to fetch invitations");
1824
+ printError(error);
1825
+ process.exitCode = 1;
1826
+ }
1827
+ });
1828
+ program2.command("teams:cancel-invite").description("Cancel a pending invitation by ID or email").argument("<id-or-email>", "Invitation ID or email address").action(async (identifier) => {
1829
+ const globalOpts = getGlobalOpts7(program2);
1830
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1831
+ const confirm = await askQuestion5(
1832
+ ` Cancel invitation for "${identifier}"? (y/N) `
1833
+ );
1834
+ if (confirm.toLowerCase() !== "y") {
1835
+ console.log(chalk7.dim(" Cancelled."));
1836
+ return;
1837
+ }
1838
+ const spinner = ora8("Cancelling invitation...").start();
1839
+ try {
1840
+ await client.delete(`/v1/teams/invitations/${encodeURIComponent(identifier)}`);
1841
+ spinner.succeed(`Invitation for "${identifier}" cancelled.`);
1842
+ } catch (error) {
1843
+ spinner.fail("Failed to cancel invitation");
1844
+ printError(error);
1845
+ process.exitCode = 1;
1846
+ }
1847
+ });
1848
+ }
1849
+
1850
+ // src/commands/biolink.ts
1851
+ import chalk8 from "chalk";
1852
+ import ora9 from "ora";
1853
+ import { createInterface as createInterface6 } from "readline";
1854
+ function getFormat8(program2) {
1855
+ const opts = program2.opts();
1856
+ if (opts.table) return "table";
1857
+ return getConfig().outputFormat || "json";
1858
+ }
1859
+ function getGlobalOpts8(program2) {
1860
+ return program2.opts();
1861
+ }
1862
+ function askQuestion6(question) {
1863
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
1864
+ return new Promise((resolve) => {
1865
+ rl.question(question, (answer) => {
1866
+ rl.close();
1867
+ resolve(answer.trim());
1868
+ });
1869
+ });
1870
+ }
1871
+ function registerBiolinkCommands(program2) {
1872
+ program2.command("biolink:list").description("List all biolinks").action(async () => {
1873
+ const globalOpts = getGlobalOpts8(program2);
1874
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1875
+ const spinner = ora9("Fetching biolinks...").start();
1876
+ try {
1877
+ const { data } = await client.get("/v1/biolinks");
1878
+ spinner.stop();
1879
+ printOutput(data, getFormat8(program2));
1880
+ } catch (error) {
1881
+ spinner.fail("Failed to fetch biolinks");
1882
+ printError(error);
1883
+ process.exitCode = 1;
1884
+ }
1885
+ });
1886
+ program2.command("biolink:get").description("Get biolink details").argument("<id>", "Biolink ID").action(async (id) => {
1887
+ const globalOpts = getGlobalOpts8(program2);
1888
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1889
+ const spinner = ora9("Fetching biolink...").start();
1890
+ try {
1891
+ const { data } = await client.get(`/v1/biolinks/${id}`);
1892
+ spinner.stop();
1893
+ printOutput(data, getFormat8(program2));
1894
+ } catch (error) {
1895
+ spinner.fail("Failed to fetch biolink");
1896
+ printError(error);
1897
+ process.exitCode = 1;
1898
+ }
1899
+ });
1900
+ program2.command("biolink:create").description("Create a new biolink").option("--slug <slug>", "Custom slug (3-50 alphanumeric chars)").option("--name <name>", "Display name").option("--bio <bio>", "Bio text").action(async (opts) => {
1901
+ const globalOpts = getGlobalOpts8(program2);
1902
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1903
+ const spinner = ora9("Creating biolink...").start();
1904
+ try {
1905
+ const { data } = await client.post("/v1/biolinks", {
1906
+ ...opts.slug && { customSlug: opts.slug },
1907
+ ...opts.name && { displayName: opts.name },
1908
+ ...opts.bio && { bio: opts.bio }
1909
+ });
1910
+ spinner.succeed("Biolink created");
1911
+ printOutput(data, getFormat8(program2));
1912
+ } catch (error) {
1913
+ spinner.fail("Failed to create biolink");
1914
+ printError(error);
1915
+ process.exitCode = 1;
1916
+ }
1917
+ });
1918
+ program2.command("biolink:update").description("Update a biolink").argument("<id>", "Biolink ID").option("--name <name>", "Display name").option("--bio <bio>", "Bio text").option("--slug <slug>", "Custom slug").option("--button-style <style>", "Button style: rounded, square, pill, outline").option("--show-grid", "Show Instagram grid").option("--hide-grid", "Hide Instagram grid").action(async (id, opts) => {
1919
+ const body = {};
1920
+ if (opts.name) body.displayName = opts.name;
1921
+ if (opts.bio) body.bio = opts.bio;
1922
+ if (opts.slug) body.customSlug = opts.slug;
1923
+ if (opts.buttonStyle) body.buttonStyle = opts.buttonStyle;
1924
+ if (opts.showGrid) body.showInstagramGrid = true;
1925
+ if (opts.hideGrid) body.showInstagramGrid = false;
1926
+ if (Object.keys(body).length === 0) {
1927
+ console.error(chalk8.red("Provide at least one option to update."));
1928
+ process.exitCode = 1;
1929
+ return;
1930
+ }
1931
+ const globalOpts = getGlobalOpts8(program2);
1932
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1933
+ const spinner = ora9("Updating biolink...").start();
1934
+ try {
1935
+ const { data } = await client.put(`/v1/biolinks/${id}`, body);
1936
+ spinner.succeed("Biolink updated");
1937
+ printOutput(data, getFormat8(program2));
1938
+ } catch (error) {
1939
+ spinner.fail("Failed to update biolink");
1940
+ printError(error);
1941
+ process.exitCode = 1;
1942
+ }
1943
+ });
1944
+ program2.command("biolink:delete").description("Delete a biolink").argument("<id>", "Biolink ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
1945
+ if (!opts.yes) {
1946
+ const answer = await askQuestion6(` Delete biolink ${id}? (y/N) `);
1947
+ if (answer.toLowerCase() !== "y") {
1948
+ console.log(chalk8.dim(" Cancelled."));
1949
+ return;
1950
+ }
1951
+ }
1952
+ const globalOpts = getGlobalOpts8(program2);
1953
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1954
+ const spinner = ora9("Deleting biolink...").start();
1955
+ try {
1956
+ await client.delete(`/v1/biolinks/${id}`);
1957
+ spinner.succeed("Biolink deleted.");
1958
+ } catch (error) {
1959
+ spinner.fail("Failed to delete biolink");
1960
+ printError(error);
1961
+ process.exitCode = 1;
1962
+ }
1963
+ });
1964
+ program2.command("biolink:publish").description("Toggle publish/unpublish a biolink").argument("<id>", "Biolink ID").action(async (id) => {
1965
+ const globalOpts = getGlobalOpts8(program2);
1966
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1967
+ const spinner = ora9("Toggling publish status...").start();
1968
+ try {
1969
+ const { data } = await client.put(`/v1/biolinks/${id}/publish`);
1970
+ const status = data.isPublished ? "published" : "unpublished";
1971
+ spinner.succeed(`Biolink ${status}.`);
1972
+ printOutput(data, getFormat8(program2));
1973
+ } catch (error) {
1974
+ spinner.fail("Failed to toggle publish status");
1975
+ printError(error);
1976
+ process.exitCode = 1;
1977
+ }
1978
+ });
1979
+ program2.command("biolink:theme").description("Change biolink theme").argument("<id>", "Biolink ID").option("--preset <theme>", "Preset theme: default, dark, gradient, minimal, neon, sunset, ocean, forest").option("--bg <color>", "Custom background color (hex)").option("--text <color>", "Custom text color (hex)").option("--button <color>", "Custom button color (hex)").option("--button-text <color>", "Custom button text color (hex)").action(async (id, opts) => {
1980
+ const globalOpts = getGlobalOpts8(program2);
1981
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
1982
+ if (opts.preset) {
1983
+ const spinner = ora9("Changing theme...").start();
1984
+ try {
1985
+ const { data } = await client.put(`/v1/biolinks/${id}/theme`, {
1986
+ theme: opts.preset.toUpperCase()
1987
+ });
1988
+ spinner.succeed(`Theme set to ${opts.preset}.`);
1989
+ printOutput(data, getFormat8(program2));
1990
+ } catch (error) {
1991
+ spinner.fail("Failed to change theme");
1992
+ printError(error);
1993
+ process.exitCode = 1;
1994
+ }
1995
+ return;
1996
+ }
1997
+ if (opts.bg || opts.text || opts.button || opts.buttonText) {
1998
+ const spinner = ora9("Setting custom theme...").start();
1999
+ try {
2000
+ const { data } = await client.put(`/v1/biolinks/${id}/custom-theme`, {
2001
+ ...opts.bg && { backgroundColor: opts.bg },
2002
+ ...opts.text && { textColor: opts.text },
2003
+ ...opts.button && { buttonColor: opts.button },
2004
+ ...opts.buttonText && { buttonTextColor: opts.buttonText }
2005
+ });
2006
+ spinner.succeed("Custom theme applied.");
2007
+ printOutput(data, getFormat8(program2));
2008
+ } catch (error) {
2009
+ spinner.fail("Failed to set custom theme");
2010
+ printError(error);
2011
+ process.exitCode = 1;
2012
+ }
2013
+ return;
2014
+ }
2015
+ console.error(chalk8.red("Provide --preset or custom colors (--bg, --text, --button, --button-text)."));
2016
+ process.exitCode = 1;
2017
+ });
2018
+ program2.command("biolink:buttons").description("List buttons for a biolink").argument("<biolink-id>", "Biolink ID").action(async (biolinkId) => {
2019
+ const globalOpts = getGlobalOpts8(program2);
2020
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2021
+ const spinner = ora9("Fetching buttons...").start();
2022
+ try {
2023
+ const { data } = await client.get(`/v1/biolinks/${biolinkId}/buttons`);
2024
+ spinner.stop();
2025
+ printOutput(data, getFormat8(program2));
2026
+ } catch (error) {
2027
+ spinner.fail("Failed to fetch buttons");
2028
+ printError(error);
2029
+ process.exitCode = 1;
2030
+ }
2031
+ });
2032
+ program2.command("biolink:add-button").description("Add a button to a biolink").argument("<biolink-id>", "Biolink ID").requiredOption("--type <type>", "Button type: link, social, email, phone, file, text").requiredOption("--text <text>", "Button text (max 100 chars)").option("--url <url>", "URL (for link/social type)").option("--email <email>", "Email (for email type)").option("--phone <phone>", "Phone (for phone type)").option("--file-url <url>", "File URL (for file type)").option("--content <content>", "Content (for text type)").option("--icon <icon>", "Icon name").option("--color <hex>", "Custom button color (hex)").action(async (biolinkId, opts) => {
2033
+ const globalOpts = getGlobalOpts8(program2);
2034
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2035
+ const spinner = ora9("Adding button...").start();
2036
+ try {
2037
+ const { data } = await client.post(`/v1/biolinks/${biolinkId}/buttons`, {
2038
+ type: opts.type.toUpperCase(),
2039
+ text: opts.text,
2040
+ ...opts.url && { url: opts.url },
2041
+ ...opts.email && { email: opts.email },
2042
+ ...opts.phone && { phone: opts.phone },
2043
+ ...opts.fileUrl && { fileUrl: opts.fileUrl },
2044
+ ...opts.content && { content: opts.content },
2045
+ ...opts.icon && { icon: opts.icon },
2046
+ ...opts.color && { customColor: opts.color }
2047
+ });
2048
+ spinner.succeed("Button added");
2049
+ printOutput(data, getFormat8(program2));
2050
+ } catch (error) {
2051
+ spinner.fail("Failed to add button");
2052
+ printError(error);
2053
+ process.exitCode = 1;
2054
+ }
2055
+ });
2056
+ program2.command("biolink:update-button").description("Update a button").argument("<biolink-id>", "Biolink ID").argument("<button-id>", "Button ID").option("--text <text>", "Button text").option("--url <url>", "URL").option("--icon <icon>", "Icon").option("--color <hex>", "Custom color").option("--active", "Set active").option("--inactive", "Set inactive").action(async (biolinkId, buttonId, opts) => {
2057
+ const body = {};
2058
+ if (opts.text) body.text = opts.text;
2059
+ if (opts.url) body.url = opts.url;
2060
+ if (opts.icon) body.icon = opts.icon;
2061
+ if (opts.color) body.customColor = opts.color;
2062
+ if (opts.active) body.isActive = true;
2063
+ if (opts.inactive) body.isActive = false;
2064
+ if (Object.keys(body).length === 0) {
2065
+ console.error(chalk8.red("Provide at least one option to update."));
2066
+ process.exitCode = 1;
2067
+ return;
2068
+ }
2069
+ const globalOpts = getGlobalOpts8(program2);
2070
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2071
+ const spinner = ora9("Updating button...").start();
2072
+ try {
2073
+ const { data } = await client.put(`/v1/biolinks/${biolinkId}/buttons/${buttonId}`, body);
2074
+ spinner.succeed("Button updated");
2075
+ printOutput(data, getFormat8(program2));
2076
+ } catch (error) {
2077
+ spinner.fail("Failed to update button");
2078
+ printError(error);
2079
+ process.exitCode = 1;
2080
+ }
2081
+ });
2082
+ program2.command("biolink:delete-button").description("Delete a button").argument("<biolink-id>", "Biolink ID").argument("<button-id>", "Button ID").option("-y, --yes", "Skip confirmation").action(async (biolinkId, buttonId, opts) => {
2083
+ if (!opts.yes) {
2084
+ const answer = await askQuestion6(` Delete button ${buttonId}? (y/N) `);
2085
+ if (answer.toLowerCase() !== "y") {
2086
+ console.log(chalk8.dim(" Cancelled."));
2087
+ return;
2088
+ }
2089
+ }
2090
+ const globalOpts = getGlobalOpts8(program2);
2091
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2092
+ const spinner = ora9("Deleting button...").start();
2093
+ try {
2094
+ await client.delete(`/v1/biolinks/${biolinkId}/buttons/${buttonId}`);
2095
+ spinner.succeed("Button deleted.");
2096
+ } catch (error) {
2097
+ spinner.fail("Failed to delete button");
2098
+ printError(error);
2099
+ process.exitCode = 1;
2100
+ }
2101
+ });
2102
+ program2.command("biolink:reorder-buttons").description("Reorder buttons").argument("<biolink-id>", "Biolink ID").argument("<button-ids...>", "Button IDs in desired order").action(async (biolinkId, buttonIds) => {
2103
+ const globalOpts = getGlobalOpts8(program2);
2104
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2105
+ const spinner = ora9("Reordering buttons...").start();
2106
+ try {
2107
+ await client.put(`/v1/biolinks/${biolinkId}/buttons/reorder`, {
2108
+ itemIds: buttonIds
2109
+ });
2110
+ spinner.succeed("Buttons reordered.");
2111
+ } catch (error) {
2112
+ spinner.fail("Failed to reorder buttons");
2113
+ printError(error);
2114
+ process.exitCode = 1;
2115
+ }
2116
+ });
2117
+ program2.command("biolink:posts").description("List posts/gallery for a biolink").argument("<biolink-id>", "Biolink ID").action(async (biolinkId) => {
2118
+ const globalOpts = getGlobalOpts8(program2);
2119
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2120
+ const spinner = ora9("Fetching posts...").start();
2121
+ try {
2122
+ const { data } = await client.get(`/v1/biolinks/${biolinkId}/posts`);
2123
+ spinner.stop();
2124
+ printOutput(data, getFormat8(program2));
2125
+ } catch (error) {
2126
+ spinner.fail("Failed to fetch posts");
2127
+ printError(error);
2128
+ process.exitCode = 1;
2129
+ }
2130
+ });
2131
+ program2.command("biolink:add-post").description("Add a post to the gallery").argument("<biolink-id>", "Biolink ID").option("--image <url>", "Image URL").option("--caption <text>", "Caption").option("--link <url>", "Link URL").action(async (biolinkId, opts) => {
2132
+ const globalOpts = getGlobalOpts8(program2);
2133
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2134
+ const spinner = ora9("Adding post...").start();
2135
+ try {
2136
+ const { data } = await client.post(`/v1/biolinks/${biolinkId}/posts`, {
2137
+ ...opts.image && { imageUrl: opts.image },
2138
+ ...opts.caption && { caption: opts.caption },
2139
+ ...opts.link && { linkUrl: opts.link }
2140
+ });
2141
+ spinner.succeed("Post added");
2142
+ printOutput(data, getFormat8(program2));
2143
+ } catch (error) {
2144
+ spinner.fail("Failed to add post");
2145
+ printError(error);
2146
+ process.exitCode = 1;
2147
+ }
2148
+ });
2149
+ program2.command("biolink:update-post").description("Update a gallery post").argument("<biolink-id>", "Biolink ID").argument("<post-id>", "Post ID").option("--caption <text>", "Caption").option("--link <url>", "Link URL").option("--visible", "Set visible").option("--hidden", "Set hidden").action(async (biolinkId, postId, opts) => {
2150
+ const body = {};
2151
+ if (opts.caption) body.caption = opts.caption;
2152
+ if (opts.link) body.linkUrl = opts.link;
2153
+ if (opts.visible) body.isVisible = true;
2154
+ if (opts.hidden) body.isVisible = false;
2155
+ if (Object.keys(body).length === 0) {
2156
+ console.error(chalk8.red("Provide at least one option to update."));
2157
+ process.exitCode = 1;
2158
+ return;
2159
+ }
2160
+ const globalOpts = getGlobalOpts8(program2);
2161
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2162
+ const spinner = ora9("Updating post...").start();
2163
+ try {
2164
+ const { data } = await client.put(`/v1/biolinks/${biolinkId}/posts/${postId}`, body);
2165
+ spinner.succeed("Post updated");
2166
+ printOutput(data, getFormat8(program2));
2167
+ } catch (error) {
2168
+ spinner.fail("Failed to update post");
2169
+ printError(error);
2170
+ process.exitCode = 1;
2171
+ }
2172
+ });
2173
+ program2.command("biolink:delete-post").description("Delete a gallery post").argument("<biolink-id>", "Biolink ID").argument("<post-id>", "Post ID").option("-y, --yes", "Skip confirmation").action(async (biolinkId, postId, opts) => {
2174
+ if (!opts.yes) {
2175
+ const answer = await askQuestion6(` Delete post ${postId}? (y/N) `);
2176
+ if (answer.toLowerCase() !== "y") {
2177
+ console.log(chalk8.dim(" Cancelled."));
2178
+ return;
2179
+ }
2180
+ }
2181
+ const globalOpts = getGlobalOpts8(program2);
2182
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2183
+ const spinner = ora9("Deleting post...").start();
2184
+ try {
2185
+ await client.delete(`/v1/biolinks/${biolinkId}/posts/${postId}`);
2186
+ spinner.succeed("Post deleted.");
2187
+ } catch (error) {
2188
+ spinner.fail("Failed to delete post");
2189
+ printError(error);
2190
+ process.exitCode = 1;
2191
+ }
2192
+ });
2193
+ program2.command("biolink:reorder-posts").description("Reorder gallery posts").argument("<biolink-id>", "Biolink ID").argument("<post-ids...>", "Post IDs in desired order").action(async (biolinkId, postIds) => {
2194
+ const globalOpts = getGlobalOpts8(program2);
2195
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2196
+ const spinner = ora9("Reordering posts...").start();
2197
+ try {
2198
+ await client.put(`/v1/biolinks/${biolinkId}/posts/reorder`, {
2199
+ itemIds: postIds
2200
+ });
2201
+ spinner.succeed("Posts reordered.");
2202
+ } catch (error) {
2203
+ spinner.fail("Failed to reorder posts");
2204
+ printError(error);
2205
+ process.exitCode = 1;
2206
+ }
2207
+ });
2208
+ program2.command("biolink:analytics").description("Get biolink analytics (views, clicks, CTR)").argument("<id>", "Biolink ID").action(async (id) => {
2209
+ const globalOpts = getGlobalOpts8(program2);
2210
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2211
+ const spinner = ora9("Fetching analytics...").start();
2212
+ try {
2213
+ const { data } = await client.get(`/v1/biolinks/${id}/analytics`);
2214
+ spinner.stop();
2215
+ printOutput(data, getFormat8(program2));
2216
+ } catch (error) {
2217
+ spinner.fail("Failed to fetch analytics");
2218
+ printError(error);
2219
+ process.exitCode = 1;
2220
+ }
2221
+ });
2222
+ }
2223
+
2224
+ // src/commands/settings.ts
2225
+ import chalk9 from "chalk";
2226
+ import ora10 from "ora";
2227
+ import { createInterface as createInterface7 } from "readline";
2228
+ function getFormat9(program2) {
2229
+ const opts = program2.opts();
2230
+ if (opts.table) return "table";
2231
+ return getConfig().outputFormat || "json";
2232
+ }
2233
+ function getGlobalOpts9(program2) {
2234
+ return program2.opts();
2235
+ }
2236
+ function askQuestion7(question) {
2237
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
2238
+ return new Promise((resolve) => {
2239
+ rl.question(question, (answer) => {
2240
+ rl.close();
2241
+ resolve(answer.trim());
2242
+ });
2243
+ });
2244
+ }
2245
+ function registerSettingsCommands(program2) {
2246
+ program2.command("settings:profile").description("Get your user profile").action(async () => {
2247
+ const globalOpts = getGlobalOpts9(program2);
2248
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2249
+ const spinner = ora10("Fetching profile...").start();
2250
+ try {
2251
+ const { data } = await client.get("/v1/settings/profile");
2252
+ spinner.stop();
2253
+ printOutput(data, getFormat9(program2));
2254
+ } catch (error) {
2255
+ spinner.fail("Failed to fetch profile");
2256
+ printError(error);
2257
+ process.exitCode = 1;
2258
+ }
2259
+ });
2260
+ program2.command("settings:update-profile").description("Update your user profile").option("--name <name>", "Display name").option("--password <password>", "New password").action(async (opts) => {
2261
+ const body = {};
2262
+ if (opts.name) body.name = opts.name;
2263
+ if (opts.password) body.password = opts.password;
2264
+ if (Object.keys(body).length === 0) {
2265
+ console.error(chalk9.red("Provide --name or --password to update."));
2266
+ process.exitCode = 1;
2267
+ return;
2268
+ }
2269
+ const globalOpts = getGlobalOpts9(program2);
2270
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2271
+ const spinner = ora10("Updating profile...").start();
2272
+ try {
2273
+ const { data } = await client.patch("/v1/settings/profile", body);
2274
+ spinner.succeed("Profile updated");
2275
+ printOutput(data, getFormat9(program2));
2276
+ } catch (error) {
2277
+ spinner.fail("Failed to update profile");
2278
+ printError(error);
2279
+ process.exitCode = 1;
2280
+ }
2281
+ });
2282
+ program2.command("settings:org").description("Get organization details").action(async () => {
2283
+ const globalOpts = getGlobalOpts9(program2);
2284
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2285
+ const spinner = ora10("Fetching organization...").start();
2286
+ try {
2287
+ const { data } = await client.get("/v1/settings/organization");
2288
+ spinner.stop();
2289
+ printOutput(data, getFormat9(program2));
2290
+ } catch (error) {
2291
+ spinner.fail("Failed to fetch organization");
2292
+ printError(error);
2293
+ process.exitCode = 1;
2294
+ }
2295
+ });
2296
+ program2.command("settings:update-org").description("Update organization settings").option("--name <name>", "Organization name").option("--description <text>", "Organization description").option("--color <hex>", "Brand color (hex)").action(async (opts) => {
2297
+ const body = {};
2298
+ if (opts.name) body.name = opts.name;
2299
+ if (opts.description) body.description = opts.description;
2300
+ if (opts.color) body.color = opts.color;
2301
+ if (Object.keys(body).length === 0) {
2302
+ console.error(chalk9.red("Provide --name, --description, or --color to update."));
2303
+ process.exitCode = 1;
2304
+ return;
2305
+ }
2306
+ const globalOpts = getGlobalOpts9(program2);
2307
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2308
+ const spinner = ora10("Updating organization...").start();
2309
+ try {
2310
+ const { data } = await client.patch("/v1/settings/organization", body);
2311
+ spinner.succeed("Organization updated");
2312
+ printOutput(data, getFormat9(program2));
2313
+ } catch (error) {
2314
+ spinner.fail("Failed to update organization");
2315
+ printError(error);
2316
+ process.exitCode = 1;
2317
+ }
2318
+ });
2319
+ program2.command("settings:tenants").description("List all organizations you belong to").action(async () => {
2320
+ const globalOpts = getGlobalOpts9(program2);
2321
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2322
+ const spinner = ora10("Fetching organizations...").start();
2323
+ try {
2324
+ const { data } = await client.get("/v1/settings/tenants");
2325
+ spinner.stop();
2326
+ printOutput(data, getFormat9(program2));
2327
+ } catch (error) {
2328
+ spinner.fail("Failed to fetch organizations");
2329
+ printError(error);
2330
+ process.exitCode = 1;
2331
+ }
2332
+ });
2333
+ program2.command("settings:create-org").description("Create a new organization").requiredOption("--name <name>", "Organization name").option("--description <text>", "Description").action(async (opts) => {
2334
+ const globalOpts = getGlobalOpts9(program2);
2335
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2336
+ const spinner = ora10("Creating organization...").start();
2337
+ try {
2338
+ const { data } = await client.post("/v1/settings/tenants", {
2339
+ name: opts.name,
2340
+ ...opts.description && { description: opts.description }
2341
+ });
2342
+ spinner.succeed(`Organization "${opts.name}" created`);
2343
+ printOutput(data, getFormat9(program2));
2344
+ } catch (error) {
2345
+ spinner.fail("Failed to create organization");
2346
+ printError(error);
2347
+ process.exitCode = 1;
2348
+ }
2349
+ });
2350
+ program2.command("settings:delete-org").description("Delete an organization").argument("<id>", "Organization/tenant ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
2351
+ if (!opts.yes) {
2352
+ const answer = await askQuestion7(` Delete organization ${id}? This cannot be undone. (y/N) `);
2353
+ if (answer.toLowerCase() !== "y") {
2354
+ console.log(chalk9.dim(" Cancelled."));
2355
+ return;
2356
+ }
2357
+ }
2358
+ const globalOpts = getGlobalOpts9(program2);
2359
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2360
+ const spinner = ora10("Deleting organization...").start();
2361
+ try {
2362
+ await client.delete(`/v1/settings/tenants/${id}`);
2363
+ spinner.succeed("Organization deleted.");
2364
+ } catch (error) {
2365
+ spinner.fail("Failed to delete organization");
2366
+ printError(error);
2367
+ process.exitCode = 1;
2368
+ }
2369
+ });
2370
+ program2.command("settings:leave-org").description("Leave an organization").argument("<id>", "Organization/tenant ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
2371
+ if (!opts.yes) {
2372
+ const answer = await askQuestion7(` Leave organization ${id}? (y/N) `);
2373
+ if (answer.toLowerCase() !== "y") {
2374
+ console.log(chalk9.dim(" Cancelled."));
2375
+ return;
2376
+ }
2377
+ }
2378
+ const globalOpts = getGlobalOpts9(program2);
2379
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2380
+ const spinner = ora10("Leaving organization...").start();
2381
+ try {
2382
+ await client.post("/v1/settings/tenants/leave", { tenantId: id });
2383
+ spinner.succeed("Left organization.");
2384
+ } catch (error) {
2385
+ spinner.fail("Failed to leave organization");
2386
+ printError(error);
2387
+ process.exitCode = 1;
2388
+ }
2389
+ });
2390
+ program2.command("settings:api-keys").description("List API keys").action(async () => {
2391
+ const globalOpts = getGlobalOpts9(program2);
2392
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2393
+ const spinner = ora10("Fetching API keys...").start();
2394
+ try {
2395
+ const { data } = await client.get("/v1/settings/api-keys");
2396
+ spinner.stop();
2397
+ printOutput(data, getFormat9(program2));
2398
+ } catch (error) {
2399
+ spinner.fail("Failed to fetch API keys");
2400
+ printError(error);
2401
+ process.exitCode = 1;
2402
+ }
2403
+ });
2404
+ program2.command("settings:create-api-key").description("Create a new API key").requiredOption("--name <name>", "Key name").action(async (opts) => {
2405
+ const globalOpts = getGlobalOpts9(program2);
2406
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2407
+ const spinner = ora10("Creating API key...").start();
2408
+ try {
2409
+ const { data } = await client.post("/v1/settings/api-keys", {
2410
+ name: opts.name
2411
+ });
2412
+ spinner.succeed("API key created");
2413
+ console.log();
2414
+ console.log(chalk9.bold(" Save this key \u2014 it will not be shown again:"));
2415
+ console.log();
2416
+ console.log(` ${chalk9.green.bold(data.fullKey)}`);
2417
+ console.log();
2418
+ printOutput({
2419
+ id: data.id,
2420
+ name: data.name,
2421
+ keyPrefix: data.keyPrefix,
2422
+ createdAt: data.createdAt
2423
+ }, getFormat9(program2));
2424
+ } catch (error) {
2425
+ spinner.fail("Failed to create API key");
2426
+ printError(error);
2427
+ process.exitCode = 1;
2428
+ }
2429
+ });
2430
+ program2.command("settings:revoke-api-key").description("Revoke an API key").argument("<id>", "API key ID").option("-y, --yes", "Skip confirmation").action(async (id, opts) => {
2431
+ if (!opts.yes) {
2432
+ const answer = await askQuestion7(` Revoke API key ${id}? (y/N) `);
2433
+ if (answer.toLowerCase() !== "y") {
2434
+ console.log(chalk9.dim(" Cancelled."));
2435
+ return;
2436
+ }
2437
+ }
2438
+ const globalOpts = getGlobalOpts9(program2);
2439
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2440
+ const spinner = ora10("Revoking API key...").start();
2441
+ try {
2442
+ await client.delete(`/v1/settings/api-keys/${id}`);
2443
+ spinner.succeed("API key revoked.");
2444
+ } catch (error) {
2445
+ spinner.fail("Failed to revoke API key");
2446
+ printError(error);
2447
+ process.exitCode = 1;
2448
+ }
2449
+ });
2450
+ program2.command("settings:billing").description("Get subscription status and plan info").action(async () => {
2451
+ const globalOpts = getGlobalOpts9(program2);
2452
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2453
+ const spinner = ora10("Fetching billing info...").start();
2454
+ try {
2455
+ const { data } = await client.get("/v1/settings/billing");
2456
+ spinner.stop();
2457
+ printOutput(data, getFormat9(program2));
2458
+ } catch (error) {
2459
+ spinner.fail("Failed to fetch billing info");
2460
+ printError(error);
2461
+ process.exitCode = 1;
2462
+ }
2463
+ });
2464
+ program2.command("settings:usage").description("Get usage breakdown for current billing period").action(async () => {
2465
+ const globalOpts = getGlobalOpts9(program2);
2466
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2467
+ const spinner = ora10("Fetching usage...").start();
2468
+ try {
2469
+ const { data } = await client.get("/v1/settings/usage");
2470
+ spinner.stop();
2471
+ printOutput(data, getFormat9(program2));
2472
+ } catch (error) {
2473
+ spinner.fail("Failed to fetch usage");
2474
+ printError(error);
2475
+ process.exitCode = 1;
2476
+ }
2477
+ });
2478
+ }
2479
+
2480
+ // src/commands/drafts.ts
2481
+ import chalk10 from "chalk";
2482
+ import ora11 from "ora";
2483
+ import { createInterface as createInterface8 } from "readline";
2484
+ function getFormat10(program2) {
2485
+ const opts = program2.opts();
2486
+ if (opts.table) return "table";
2487
+ return getConfig().outputFormat || "json";
2488
+ }
2489
+ function getGlobalOpts10(program2) {
2490
+ return program2.opts();
2491
+ }
2492
+ function askQuestion8(question) {
2493
+ const rl = createInterface8({ input: process.stdin, output: process.stdout });
2494
+ return new Promise((resolve) => {
2495
+ rl.question(question, (answer) => {
2496
+ rl.close();
2497
+ resolve(answer.trim());
2498
+ });
2499
+ });
2500
+ }
2501
+ async function resolveAccountForPlatform2(client, platform) {
2502
+ const { data: accounts } = await client.get("/v1/accounts");
2503
+ const matches = accounts.filter(
2504
+ (a) => a.platform === platform.toUpperCase()
2505
+ );
2506
+ if (matches.length === 0) {
2507
+ console.error(chalk10.red(`No ${platform} account found. Connect one first.`));
2508
+ return null;
2509
+ }
2510
+ if (matches.length === 1) return matches[0];
2511
+ console.log(chalk10.bold(` Found ${matches.length} ${platform} accounts:`));
2512
+ console.log();
2513
+ matches.forEach((a, i) => {
2514
+ const name = a.accountName || a.userName || a.accountId;
2515
+ console.log(` ${chalk10.cyan(String(i + 1))}. ${name}`);
2516
+ });
2517
+ console.log();
2518
+ const answer = await askQuestion8(` Select account (1-${matches.length}): `);
2519
+ const index = parseInt(answer, 10) - 1;
2520
+ if (isNaN(index) || index < 0 || index >= matches.length) {
2521
+ console.log(chalk10.yellow(" Invalid selection. Cancelled."));
2522
+ return null;
2523
+ }
2524
+ return matches[index];
2525
+ }
2526
+ function registerDraftsCommands(program2) {
2527
+ program2.command("drafts:list").description("List draft posts").option("--page <number>", "Page number", "1").option("--limit <number>", "Items per page", "20").action(async (opts) => {
2528
+ const globalOpts = getGlobalOpts10(program2);
2529
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2530
+ const spinner = ora11("Fetching drafts...").start();
2531
+ try {
2532
+ const params = {
2533
+ page: parseInt(opts.page),
2534
+ limit: parseInt(opts.limit)
2535
+ };
2536
+ const { data } = await client.get("/v1/drafts", { params });
2537
+ spinner.stop();
2538
+ printOutput(data, getFormat10(program2));
2539
+ } catch (error) {
2540
+ spinner.fail("Failed to fetch drafts");
2541
+ printError(error);
2542
+ if (globalOpts.verbose && error instanceof Error) {
2543
+ console.error(error.stack);
2544
+ }
2545
+ process.exitCode = 1;
2546
+ }
2547
+ });
2548
+ program2.command("drafts:get").description("Get a single draft").argument("<id>", "Draft ID").action(async (id) => {
2549
+ const globalOpts = getGlobalOpts10(program2);
2550
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2551
+ const spinner = ora11("Fetching draft...").start();
2552
+ try {
2553
+ const { data } = await client.get(`/v1/drafts/${id}`);
2554
+ spinner.stop();
2555
+ printOutput(data, getFormat10(program2));
2556
+ } catch (error) {
2557
+ spinner.fail("Failed to fetch draft");
2558
+ printError(error);
2559
+ process.exitCode = 1;
2560
+ }
2561
+ });
2562
+ program2.command("drafts:create").description("Create a draft post").option("-c, --content <text>", "Draft content").option("--title <title>", "Draft title").option("--post-type <type>", "Content type: TEXT, IMAGE, VIDEO, etc.").option("--description <desc>", "Draft description").option("-a, --account <id>", "Social account ID").option("-m, --media <urls...>", "Media URLs (from media:upload or CDN)").option("--metadata <json>", "Metadata as JSON").addHelpText("after", `
2563
+ Examples:
2564
+ $ so-me drafts:create -c "Draft idea" --post-type TEXT
2565
+ $ so-me drafts:create -c "With image" --post-type IMAGE -m https://cdn.example.com/photo.jpg
2566
+ $ so-me drafts:create -c "Multiple media" -m <url1> <url2> <url3> --post-type MULTIPLE_IMAGES
2567
+ $ so-me drafts:create -c "With metadata" --metadata '{"key":"value"}'`).action(async (opts) => {
2568
+ const globalOpts = getGlobalOpts10(program2);
2569
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2570
+ const spinner = ora11("Creating draft...").start();
2571
+ try {
2572
+ const body = {};
2573
+ if (opts.content) body.text = opts.content;
2574
+ if (opts.title) body.title = opts.title;
2575
+ if (opts.postType) body.postType = opts.postType;
2576
+ if (opts.description) body.description = opts.description;
2577
+ if (opts.account) body.accountId = opts.account;
2578
+ const metaData = opts.metadata ? JSON.parse(opts.metadata) : {};
2579
+ if (opts.media) metaData.mediaUrls = opts.media;
2580
+ if (Object.keys(metaData).length > 0) body.metaData = metaData;
2581
+ const { data } = await client.post("/v1/drafts", body);
2582
+ spinner.succeed("Draft created");
2583
+ printOutput(data, getFormat10(program2));
2584
+ } catch (error) {
2585
+ spinner.fail("Failed to create draft");
2586
+ printError(error);
2587
+ process.exitCode = 1;
2588
+ }
2589
+ });
2590
+ program2.command("drafts:update").description("Update a draft post").argument("<id>", "Draft ID").option("-c, --content <text>", "Updated content").option("--title <title>", "Updated title").option("--post-type <type>", "Updated content type").option("--description <desc>", "Updated description").option("-m, --media <urls...>", "Media URLs (replaces existing)").option("--metadata <json>", "Updated metadata as JSON").action(async (id, opts) => {
2591
+ const globalOpts = getGlobalOpts10(program2);
2592
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2593
+ const spinner = ora11("Updating draft...").start();
2594
+ try {
2595
+ const body = {};
2596
+ if (opts.content) body.text = opts.content;
2597
+ if (opts.title) body.title = opts.title;
2598
+ if (opts.postType) body.postType = opts.postType;
2599
+ if (opts.description) body.description = opts.description;
2600
+ const metaData = opts.metadata ? JSON.parse(opts.metadata) : {};
2601
+ if (opts.media) metaData.mediaUrls = opts.media;
2602
+ if (Object.keys(metaData).length > 0) body.metaData = metaData;
2603
+ const { data } = await client.patch(`/v1/drafts/${id}`, body);
2604
+ spinner.succeed("Draft updated");
2605
+ printOutput(data, getFormat10(program2));
2606
+ } catch (error) {
2607
+ spinner.fail("Failed to update draft");
2608
+ printError(error);
2609
+ process.exitCode = 1;
2610
+ }
2611
+ });
2612
+ program2.command("drafts:delete").description("Delete one or more drafts").argument("<ids...>", "Draft ID(s)").option("-y, --yes", "Skip confirmation prompt").action(async (ids, opts) => {
2613
+ const globalOpts = getGlobalOpts10(program2);
2614
+ if (!opts.yes) {
2615
+ const readline = await import("readline");
2616
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2617
+ const label = ids.length === 1 ? `draft ${ids[0]}` : `${ids.length} drafts`;
2618
+ const answer = await new Promise((resolve) => {
2619
+ rl.question(`Are you sure you want to delete ${label}? (y/N) `, resolve);
2620
+ });
2621
+ rl.close();
2622
+ if (answer.toLowerCase() !== "y") {
2623
+ console.log("Cancelled.");
2624
+ return;
2625
+ }
2626
+ }
2627
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2628
+ const spinner = ora11("Deleting draft(s)...").start();
2629
+ try {
2630
+ let deleted = 0;
2631
+ for (const id of ids) {
2632
+ try {
2633
+ await client.delete(`/v1/drafts/${id}`);
2634
+ deleted++;
2635
+ } catch (err) {
2636
+ }
2637
+ }
2638
+ if (ids.length === 1) {
2639
+ spinner.succeed(`Draft ${ids[0]} deleted`);
2640
+ } else {
2641
+ spinner.succeed(`Deleted ${deleted}/${ids.length} drafts`);
2642
+ }
2643
+ } catch (error) {
2644
+ spinner.fail("Failed to delete draft(s)");
2645
+ printError(error);
2646
+ process.exitCode = 1;
2647
+ }
2648
+ });
2649
+ program2.command("drafts:convert").description("Convert a draft to a post").argument("<id>", "Draft ID").option("--platform <platform>", "Social platform: TWITTER, LINKEDIN, INSTAGRAM, etc.").option("--post-type <type>", "Content type: TEXT, IMAGE, VIDEO, etc.").option("-c, --content <text>", "Post content (overrides draft text)").option("-a, --account <id>", "Social account ID").option("-s, --date <date>", "Schedule date (ISO 8601)").addHelpText("after", `
2650
+ Examples:
2651
+ $ so-me drafts:convert <id> --platform TWITTER --post-type TEXT
2652
+ $ so-me drafts:convert <id> --platform LINKEDIN -s 2026-04-10T14:00:00Z
2653
+ $ so-me drafts:convert <id> --platform INSTAGRAM --post-type IMAGE`).action(async (id, opts) => {
2654
+ const globalOpts = getGlobalOpts10(program2);
2655
+ const client = createApiClient({ apiKey: globalOpts.apiKey, apiUrl: globalOpts.apiUrl });
2656
+ let spinner = null;
2657
+ try {
2658
+ const body = {};
2659
+ if (opts.platform) body.platform = opts.platform;
2660
+ if (opts.postType) body.postType = opts.postType;
2661
+ if (opts.content) body.text = opts.content;
2662
+ if (opts.date) body.scheduledAt = opts.date;
2663
+ if (opts.account) {
2664
+ body.accountId = opts.account;
2665
+ } else if (opts.platform) {
2666
+ const account = await resolveAccountForPlatform2(client, opts.platform);
2667
+ if (!account) {
2668
+ process.exitCode = 1;
2669
+ return;
2670
+ }
2671
+ body.accountId = account.accountId;
2672
+ }
2673
+ spinner = ora11("Converting draft to post...").start();
2674
+ const { data } = await client.post(`/v1/drafts/${id}/convert`, body);
2675
+ spinner.succeed(`Draft ${id} converted to post ${data.id}`);
2676
+ printOutput(data, getFormat10(program2));
2677
+ } catch (error) {
2678
+ if (spinner) spinner.fail("Failed to convert draft");
2679
+ else console.error(chalk10.red("Failed to convert draft"));
2680
+ printError(error);
2681
+ process.exitCode = 1;
2682
+ }
2683
+ });
2684
+ }
2685
+
2686
+ // src/index.ts
2687
+ function createProgram() {
2688
+ const program2 = new Command();
2689
+ program2.name("so-me").description("CLI tool for so-me social media scheduler").version("0.1.0").option("--api-key <key>", "API key (overrides stored credentials)").option("--api-url <url>", "API base URL (overrides stored config)").option("--json", "Output as JSON (default)", true).option("--table", "Output as table").option("--verbose", "Show detailed error information");
2690
+ program2.addHelpText("after", `
2691
+ Examples:
2692
+ $ so-me auth:login --api-key sk_live_xxx Save API key
2693
+ $ so-me auth:login Login via browser
2694
+ $ so-me posts:list List all posts (JSON)
2695
+ $ so-me posts:list --table List all posts (table)
2696
+ $ so-me posts:create -c "Hello!" --platform TWITTER
2697
+ $ so-me media:upload ./image.png Upload a file
2698
+ $ so-me analytics:platform acc-123 --days 30
2699
+
2700
+ Environment variables:
2701
+ SOME_API_KEY API key (alternative to --api-key flag)
2702
+ SOME_API_URL API base URL (default: http://localhost:8000)
2703
+ `);
2704
+ registerAuthCommands(program2);
2705
+ registerPostsCommands(program2);
2706
+ registerAccountsCommands(program2);
2707
+ registerMediaCommands(program2);
2708
+ registerAnalyticsCommands(program2);
2709
+ registerInboxCommands(program2);
2710
+ registerApprovalsCommands(program2);
2711
+ registerTeamsCommands(program2);
2712
+ registerBiolinkCommands(program2);
2713
+ registerSettingsCommands(program2);
2714
+ registerDraftsCommands(program2);
2715
+ return program2;
2716
+ }
2717
+
2718
+ // bin/so-me.ts
2719
+ var program = createProgram();
2720
+ program.parseAsync(process.argv).catch((error) => {
2721
+ if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
2722
+ process.exit(0);
2723
+ }
2724
+ console.error(error.message || error);
2725
+ process.exit(1);
2726
+ });
2727
+ //# sourceMappingURL=so-me.js.map