@autoposting.ai/cli 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/dist/cli.cjs +767 -58
- package/dist/cli.cjs.map +1 -1
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -42,11 +42,11 @@ function createError(status, body) {
|
|
|
42
42
|
if (status >= 500) return new ServerError(message, status, code);
|
|
43
43
|
return new AutopostingError(message, status, code);
|
|
44
44
|
}
|
|
45
|
-
var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, ValidationError, RateLimitError, ServerError, Resource, AgentsResource, BillingResource, BrandsResource, CarouselsResource, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
|
|
45
|
+
var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, ValidationError, RateLimitError, ServerError, Resource, MediaResource, AgentsResource, BillingResource, BrandsResource, CarouselsResource, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
|
|
46
46
|
var init_dist = __esm({
|
|
47
47
|
"../sdk/dist/index.mjs"() {
|
|
48
48
|
"use strict";
|
|
49
|
-
VERSION = "0.3.
|
|
49
|
+
VERSION = "0.3.4";
|
|
50
50
|
AutopostingError = class extends Error {
|
|
51
51
|
status;
|
|
52
52
|
code;
|
|
@@ -76,20 +76,29 @@ var init_dist = __esm({
|
|
|
76
76
|
this.client = client;
|
|
77
77
|
}
|
|
78
78
|
client;
|
|
79
|
-
get(
|
|
80
|
-
return this.client.request("GET",
|
|
79
|
+
get(path3, query) {
|
|
80
|
+
return this.client.request("GET", path3, void 0, query);
|
|
81
81
|
}
|
|
82
|
-
post(
|
|
83
|
-
return this.client.request("POST",
|
|
82
|
+
post(path3, body) {
|
|
83
|
+
return this.client.request("POST", path3, body);
|
|
84
84
|
}
|
|
85
|
-
put(
|
|
86
|
-
return this.client.request("PUT",
|
|
85
|
+
put(path3, body) {
|
|
86
|
+
return this.client.request("PUT", path3, body);
|
|
87
87
|
}
|
|
88
|
-
patch(
|
|
89
|
-
return this.client.request("PATCH",
|
|
88
|
+
patch(path3, body) {
|
|
89
|
+
return this.client.request("PATCH", path3, body);
|
|
90
90
|
}
|
|
91
|
-
delete(
|
|
92
|
-
return this.client.request("DELETE",
|
|
91
|
+
delete(path3) {
|
|
92
|
+
return this.client.request("DELETE", path3);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
MediaResource = class extends Resource {
|
|
96
|
+
/** POST /media/upload — multipart/form-data, ≤20MB, image/* | video/*. */
|
|
97
|
+
upload(file) {
|
|
98
|
+
const form = new FormData();
|
|
99
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data], { type: file.contentType });
|
|
100
|
+
form.append("file", blob, file.filename);
|
|
101
|
+
return this.client.request("POST", "/media/upload", form);
|
|
93
102
|
}
|
|
94
103
|
};
|
|
95
104
|
AgentsResource = class extends Resource {
|
|
@@ -233,6 +242,10 @@ var init_dist = __esm({
|
|
|
233
242
|
schedule(id, scheduledAt) {
|
|
234
243
|
return this.client.request("PUT", `/posts/${id}/schedule`, { scheduledAt });
|
|
235
244
|
}
|
|
245
|
+
/** PUT /posts/:id/schedule with { cancel: true } — returns the post to draft. */
|
|
246
|
+
unschedule(id) {
|
|
247
|
+
return this.client.request("PUT", `/posts/${id}/schedule`, { cancel: true });
|
|
248
|
+
}
|
|
236
249
|
retry(id) {
|
|
237
250
|
return this.client.request("POST", `/posts/${id}/retry`);
|
|
238
251
|
}
|
|
@@ -361,6 +374,7 @@ var init_dist = __esm({
|
|
|
361
374
|
authSource;
|
|
362
375
|
maxRetries;
|
|
363
376
|
retryBaseMs;
|
|
377
|
+
media;
|
|
364
378
|
agents;
|
|
365
379
|
billing;
|
|
366
380
|
brands;
|
|
@@ -386,6 +400,7 @@ var init_dist = __esm({
|
|
|
386
400
|
this.authSource = config.authSource ?? "api-key";
|
|
387
401
|
this.maxRetries = Math.max(0, config.maxRetries ?? 2);
|
|
388
402
|
this.retryBaseMs = Math.max(0, config.retryBaseMs ?? 300);
|
|
403
|
+
this.media = new MediaResource(this);
|
|
389
404
|
this.agents = new AgentsResource(this);
|
|
390
405
|
this.billing = new BillingResource(this);
|
|
391
406
|
this.brands = new BrandsResource(this);
|
|
@@ -410,7 +425,7 @@ var init_dist = __esm({
|
|
|
410
425
|
* failures — network errors, timeouts, and 5xx — with exponential backoff + jitter.
|
|
411
426
|
* Non-idempotent methods (POST/PATCH) are never retried, to avoid duplicate side effects.
|
|
412
427
|
*/
|
|
413
|
-
async request(method,
|
|
428
|
+
async request(method, path3, body, query) {
|
|
414
429
|
let lastErr;
|
|
415
430
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
416
431
|
if (attempt > 0) {
|
|
@@ -418,7 +433,7 @@ var init_dist = __esm({
|
|
|
418
433
|
await sleep(backoff + Math.floor(Math.random() * this.retryBaseMs));
|
|
419
434
|
}
|
|
420
435
|
try {
|
|
421
|
-
return await this._send(method,
|
|
436
|
+
return await this._send(method, path3, body, query);
|
|
422
437
|
} catch (err) {
|
|
423
438
|
lastErr = err;
|
|
424
439
|
if (attempt < this.maxRetries && this.isRetryable(method, err)) continue;
|
|
@@ -433,8 +448,8 @@ var init_dist = __esm({
|
|
|
433
448
|
if (err.code === "NETWORK_ERROR" || err.code === "TIMEOUT") return true;
|
|
434
449
|
return RETRYABLE_STATUS.has(err.status);
|
|
435
450
|
}
|
|
436
|
-
async _send(method,
|
|
437
|
-
let url = `${this.baseUrl}${
|
|
451
|
+
async _send(method, path3, body, query) {
|
|
452
|
+
let url = `${this.baseUrl}${path3}`;
|
|
438
453
|
if (query && Object.keys(query).length > 0) {
|
|
439
454
|
const params = new URLSearchParams();
|
|
440
455
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -468,13 +483,13 @@ var init_dist = __esm({
|
|
|
468
483
|
} catch (err) {
|
|
469
484
|
if (err instanceof Error && err.name === "AbortError") {
|
|
470
485
|
throw new AutopostingError(
|
|
471
|
-
`Request to ${method} ${
|
|
486
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
472
487
|
0,
|
|
473
488
|
"TIMEOUT"
|
|
474
489
|
);
|
|
475
490
|
}
|
|
476
491
|
throw new AutopostingError(
|
|
477
|
-
`Network request to ${method} ${
|
|
492
|
+
`Network request to ${method} ${path3} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
478
493
|
0,
|
|
479
494
|
"NETWORK_ERROR"
|
|
480
495
|
);
|
|
@@ -519,7 +534,7 @@ var init_dist = __esm({
|
|
|
519
534
|
parsed = JSON.parse(text);
|
|
520
535
|
} catch {
|
|
521
536
|
throw new AutopostingError(
|
|
522
|
-
`Expected a JSON response from ${method} ${
|
|
537
|
+
`Expected a JSON response from ${method} ${path3} but received non-JSON (HTTP ${response.status}). Verify the API base URL (AUTOPOSTING_BASE_URL / --base-url) points at the API, not the web app.`,
|
|
523
538
|
response.status,
|
|
524
539
|
"INVALID_RESPONSE"
|
|
525
540
|
);
|
|
@@ -532,7 +547,7 @@ var init_dist = __esm({
|
|
|
532
547
|
} catch (err) {
|
|
533
548
|
if (err instanceof Error && err.name === "AbortError") {
|
|
534
549
|
throw new AutopostingError(
|
|
535
|
-
`Request to ${method} ${
|
|
550
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
536
551
|
0,
|
|
537
552
|
"TIMEOUT"
|
|
538
553
|
);
|
|
@@ -1660,7 +1675,7 @@ var init_server = __esm({
|
|
|
1660
1675
|
});
|
|
1661
1676
|
|
|
1662
1677
|
// src/cli.ts
|
|
1663
|
-
var
|
|
1678
|
+
var import_commander20 = require("commander");
|
|
1664
1679
|
init_dist();
|
|
1665
1680
|
|
|
1666
1681
|
// src/commands/agents.ts
|
|
@@ -1808,6 +1823,67 @@ function formatError(error, mode) {
|
|
|
1808
1823
|
if (mode === "tty") return red(`Error: ${message}`);
|
|
1809
1824
|
return JSON.stringify({ error: message }, null, 2);
|
|
1810
1825
|
}
|
|
1826
|
+
function parseJq(expr) {
|
|
1827
|
+
const e = expr.trim();
|
|
1828
|
+
if (e === "" || e === ".") return [];
|
|
1829
|
+
if (!e.startsWith(".")) {
|
|
1830
|
+
throw new Error(`--jq: expression must start with "." (got "${expr}")`);
|
|
1831
|
+
}
|
|
1832
|
+
const body = e.slice(1);
|
|
1833
|
+
const ops = [];
|
|
1834
|
+
let i = 0;
|
|
1835
|
+
while (i < body.length) {
|
|
1836
|
+
const ch = body[i];
|
|
1837
|
+
if (ch === ".") {
|
|
1838
|
+
i++;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (ch === "[") {
|
|
1842
|
+
const close = body.indexOf("]", i);
|
|
1843
|
+
if (close === -1) throw new Error(`--jq: unclosed "[" in "${expr}"`);
|
|
1844
|
+
const inner = body.slice(i + 1, close).trim();
|
|
1845
|
+
if (inner === "") {
|
|
1846
|
+
ops.push({ type: "iter" });
|
|
1847
|
+
} else {
|
|
1848
|
+
const n = Number(inner);
|
|
1849
|
+
if (!Number.isInteger(n)) throw new Error(`--jq: invalid index "[${inner}]" in "${expr}"`);
|
|
1850
|
+
ops.push({ type: "index", n });
|
|
1851
|
+
}
|
|
1852
|
+
i = close + 1;
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
const m = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(body.slice(i));
|
|
1856
|
+
if (!m) throw new Error(`--jq: unexpected character "${ch}" in "${expr}"`);
|
|
1857
|
+
ops.push({ type: "prop", name: m[0] });
|
|
1858
|
+
i += m[0].length;
|
|
1859
|
+
}
|
|
1860
|
+
return ops;
|
|
1861
|
+
}
|
|
1862
|
+
function applyJq(data, expr) {
|
|
1863
|
+
const ops = parseJq(expr);
|
|
1864
|
+
let stream = [data];
|
|
1865
|
+
for (const op of ops) {
|
|
1866
|
+
const next = [];
|
|
1867
|
+
for (const v of stream) {
|
|
1868
|
+
if (op.type === "prop") {
|
|
1869
|
+
next.push(v == null ? null : v[op.name] ?? null);
|
|
1870
|
+
} else if (op.type === "index") {
|
|
1871
|
+
if (!Array.isArray(v)) throw new Error(`--jq: cannot index non-array with [${op.n}]`);
|
|
1872
|
+
const idx = op.n < 0 ? v.length + op.n : op.n;
|
|
1873
|
+
next.push(v[idx] ?? null);
|
|
1874
|
+
} else {
|
|
1875
|
+
if (Array.isArray(v)) next.push(...v);
|
|
1876
|
+
else if (v && typeof v === "object") next.push(...Object.values(v));
|
|
1877
|
+
else throw new Error(`--jq: cannot iterate over ${v === null ? "null" : typeof v} \u2014 .[] needs an array or object`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
stream = next;
|
|
1881
|
+
}
|
|
1882
|
+
return stream;
|
|
1883
|
+
}
|
|
1884
|
+
function formatJq(data, expr) {
|
|
1885
|
+
return applyJq(data, expr).map((r) => typeof r === "string" ? r : r === void 0 ? "null" : JSON.stringify(r)).join("\n");
|
|
1886
|
+
}
|
|
1811
1887
|
|
|
1812
1888
|
// src/output/spinner.ts
|
|
1813
1889
|
var NO_OP_SPINNER = {
|
|
@@ -1855,19 +1931,23 @@ function createSpinner(mode) {
|
|
|
1855
1931
|
|
|
1856
1932
|
// src/output/printer.ts
|
|
1857
1933
|
var Printer = class {
|
|
1858
|
-
constructor(mode) {
|
|
1934
|
+
constructor(mode, jq) {
|
|
1859
1935
|
this.mode = mode;
|
|
1936
|
+
this.jq = jq;
|
|
1860
1937
|
}
|
|
1861
1938
|
mode;
|
|
1939
|
+
jq;
|
|
1862
1940
|
/** True only for human-readable output, where extra hint lines are safe to print. */
|
|
1863
1941
|
isTty() {
|
|
1864
|
-
return this.mode === "tty";
|
|
1942
|
+
return this.mode === "tty" && !this.jq;
|
|
1865
1943
|
}
|
|
1866
1944
|
log(data) {
|
|
1945
|
+
if (this.jq) return this.emitJq(data);
|
|
1867
1946
|
const out = formatOutput(data, this.mode);
|
|
1868
1947
|
if (out !== "") console.log(out);
|
|
1869
1948
|
}
|
|
1870
1949
|
table(rows, columns) {
|
|
1950
|
+
if (this.jq) return this.emitJq(rows);
|
|
1871
1951
|
if (this.mode === "quiet") return;
|
|
1872
1952
|
if (this.mode === "json") {
|
|
1873
1953
|
console.log(formatOutput(rows, "json"));
|
|
@@ -1876,19 +1956,23 @@ var Printer = class {
|
|
|
1876
1956
|
const out = formatTable(rows, columns);
|
|
1877
1957
|
if (out !== "") console.log(out);
|
|
1878
1958
|
}
|
|
1959
|
+
emitJq(data) {
|
|
1960
|
+
const out = formatJq(data, this.jq);
|
|
1961
|
+
if (out !== "") console.log(out);
|
|
1962
|
+
}
|
|
1879
1963
|
error(err) {
|
|
1880
|
-
const out = formatError(err, this.mode);
|
|
1964
|
+
const out = formatError(err, this.jq ? "json" : this.mode);
|
|
1881
1965
|
console.error(out);
|
|
1882
1966
|
}
|
|
1883
1967
|
spinner(message) {
|
|
1884
|
-
const s = createSpinner(this.mode);
|
|
1968
|
+
const s = createSpinner(this.jq ? "json" : this.mode);
|
|
1885
1969
|
s.start(message);
|
|
1886
1970
|
return s;
|
|
1887
1971
|
}
|
|
1888
1972
|
};
|
|
1889
1973
|
function createPrinter(options) {
|
|
1890
1974
|
const mode = detectOutputMode(options);
|
|
1891
|
-
return new Printer(mode);
|
|
1975
|
+
return new Printer(mode, options.jq);
|
|
1892
1976
|
}
|
|
1893
1977
|
|
|
1894
1978
|
// src/output/exit-codes.ts
|
|
@@ -2385,6 +2469,195 @@ function createBillingCommand() {
|
|
|
2385
2469
|
// src/commands/brands.ts
|
|
2386
2470
|
var import_commander4 = require("commander");
|
|
2387
2471
|
init_dist();
|
|
2472
|
+
|
|
2473
|
+
// src/lib/media-flags.ts
|
|
2474
|
+
var import_node_fs2 = require("fs");
|
|
2475
|
+
var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
2476
|
+
var EXT_TO_MIME = {
|
|
2477
|
+
jpg: "image/jpeg",
|
|
2478
|
+
jpeg: "image/jpeg",
|
|
2479
|
+
png: "image/png",
|
|
2480
|
+
gif: "image/gif",
|
|
2481
|
+
webp: "image/webp",
|
|
2482
|
+
mp4: "video/mp4",
|
|
2483
|
+
mov: "video/quicktime",
|
|
2484
|
+
webm: "video/webm"
|
|
2485
|
+
};
|
|
2486
|
+
function extToMime(filename) {
|
|
2487
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
2488
|
+
const mime = EXT_TO_MIME[ext];
|
|
2489
|
+
if (!mime) {
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
`Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
return mime;
|
|
2495
|
+
}
|
|
2496
|
+
function parsePairs(flag, vals) {
|
|
2497
|
+
const result = {};
|
|
2498
|
+
for (const val of vals) {
|
|
2499
|
+
const eqIdx = val.indexOf("=");
|
|
2500
|
+
if (eqIdx < 1) {
|
|
2501
|
+
throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
|
|
2502
|
+
}
|
|
2503
|
+
const p = val.slice(0, eqIdx).trim();
|
|
2504
|
+
const v = val.slice(eqIdx + 1);
|
|
2505
|
+
if (!VALID_PLATFORMS.includes(p)) {
|
|
2506
|
+
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2507
|
+
}
|
|
2508
|
+
result[p] = v;
|
|
2509
|
+
}
|
|
2510
|
+
return result;
|
|
2511
|
+
}
|
|
2512
|
+
function parsePlatformMediaPairs(flag, vals) {
|
|
2513
|
+
const result = {};
|
|
2514
|
+
for (const val of vals) {
|
|
2515
|
+
const eqIdx = val.indexOf("=");
|
|
2516
|
+
if (eqIdx < 1) {
|
|
2517
|
+
throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
|
|
2518
|
+
}
|
|
2519
|
+
const p = val.slice(0, eqIdx).trim();
|
|
2520
|
+
const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2521
|
+
if (!VALID_PLATFORMS.includes(p)) {
|
|
2522
|
+
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2523
|
+
}
|
|
2524
|
+
result[p] = paths;
|
|
2525
|
+
}
|
|
2526
|
+
return result;
|
|
2527
|
+
}
|
|
2528
|
+
function validateMediaExtensions(paths) {
|
|
2529
|
+
for (const p of paths) extToMime(p);
|
|
2530
|
+
}
|
|
2531
|
+
function validateMediaCount(paths) {
|
|
2532
|
+
if (paths.length > 10) {
|
|
2533
|
+
throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
function validateMediaPaths(paths) {
|
|
2537
|
+
for (const p of paths) {
|
|
2538
|
+
if (!(0, import_node_fs2.existsSync)(p)) {
|
|
2539
|
+
throw new Error(`Media file not found: "${p}"`);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
function alignAltText(mediaPaths, altTexts) {
|
|
2544
|
+
if (altTexts.length > mediaPaths.length) {
|
|
2545
|
+
throw new Error(
|
|
2546
|
+
`--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
return mediaPaths.map((_, i) => altTexts[i]);
|
|
2550
|
+
}
|
|
2551
|
+
function buildYoutubeOptions(opts) {
|
|
2552
|
+
const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
|
|
2553
|
+
const o = {};
|
|
2554
|
+
if (ytTitle) o.title = ytTitle;
|
|
2555
|
+
if (ytDescription) o.description = ytDescription;
|
|
2556
|
+
if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2557
|
+
if (ytPrivacy) o.privacyStatus = ytPrivacy;
|
|
2558
|
+
if (ytCategory) o.categoryId = ytCategory;
|
|
2559
|
+
if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
|
|
2560
|
+
return Object.keys(o).length > 0 ? o : void 0;
|
|
2561
|
+
}
|
|
2562
|
+
function buildInstagramOptions(opts) {
|
|
2563
|
+
const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
|
|
2564
|
+
const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
|
|
2565
|
+
if (!hasAnyReelFlag) return void 0;
|
|
2566
|
+
const reel = {};
|
|
2567
|
+
if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
|
|
2568
|
+
if (igCoverUrl) reel.coverUrl = igCoverUrl;
|
|
2569
|
+
if (igThumbOffsetMs !== void 0) {
|
|
2570
|
+
const n = Number(igThumbOffsetMs);
|
|
2571
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
2572
|
+
throw new Error(
|
|
2573
|
+
`--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
reel.thumbOffsetMs = n;
|
|
2577
|
+
}
|
|
2578
|
+
if (igCollaborators) {
|
|
2579
|
+
reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2580
|
+
}
|
|
2581
|
+
return { reel };
|
|
2582
|
+
}
|
|
2583
|
+
function buildThreadsOptions(opts) {
|
|
2584
|
+
const { threadsReplyTo, threadsReplyControl } = opts;
|
|
2585
|
+
if (!threadsReplyTo && !threadsReplyControl) return void 0;
|
|
2586
|
+
const o = {};
|
|
2587
|
+
if (threadsReplyTo) o.replyToId = threadsReplyTo;
|
|
2588
|
+
if (threadsReplyControl) o.replyControl = threadsReplyControl;
|
|
2589
|
+
return o;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/auth/config-store.ts
|
|
2593
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
2594
|
+
var import_node_os2 = __toESM(require("os"));
|
|
2595
|
+
var import_node_path2 = __toESM(require("path"));
|
|
2596
|
+
function getConfigPath() {
|
|
2597
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2598
|
+
const base = xdg && import_node_path2.default.isAbsolute(xdg) ? xdg : import_node_path2.default.join(import_node_os2.default.homedir(), ".config");
|
|
2599
|
+
return import_node_path2.default.join(base, "autoposting", "config.json");
|
|
2600
|
+
}
|
|
2601
|
+
function readConfig() {
|
|
2602
|
+
try {
|
|
2603
|
+
return JSON.parse(import_node_fs3.default.readFileSync(getConfigPath(), "utf8"));
|
|
2604
|
+
} catch {
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
function writeConfig(cfg) {
|
|
2609
|
+
const filePath = getConfigPath();
|
|
2610
|
+
import_node_fs3.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true, mode: 448 });
|
|
2611
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
2612
|
+
try {
|
|
2613
|
+
import_node_fs3.default.writeFileSync(tmp, JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
2614
|
+
import_node_fs3.default.renameSync(tmp, filePath);
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
try {
|
|
2617
|
+
import_node_fs3.default.unlinkSync(tmp);
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
throw err;
|
|
2621
|
+
}
|
|
2622
|
+
import_node_fs3.default.chmodSync(filePath, 384);
|
|
2623
|
+
}
|
|
2624
|
+
function getContextBrand() {
|
|
2625
|
+
return readConfig()?.context?.brand ?? null;
|
|
2626
|
+
}
|
|
2627
|
+
function setContextBrand(brand) {
|
|
2628
|
+
const cfg = readConfig() ?? {};
|
|
2629
|
+
cfg.context = { ...cfg.context, brand };
|
|
2630
|
+
writeConfig(cfg);
|
|
2631
|
+
}
|
|
2632
|
+
function unsetContextBrand() {
|
|
2633
|
+
const cfg = readConfig();
|
|
2634
|
+
if (!cfg?.context?.brand) return;
|
|
2635
|
+
delete cfg.context.brand;
|
|
2636
|
+
writeConfig(cfg);
|
|
2637
|
+
}
|
|
2638
|
+
function resolveBrand(optBrand) {
|
|
2639
|
+
return optBrand ?? getContextBrand();
|
|
2640
|
+
}
|
|
2641
|
+
function getDefaultAccount(brand, platform) {
|
|
2642
|
+
return readConfig()?.defaultAccounts?.[brand]?.[platform] ?? null;
|
|
2643
|
+
}
|
|
2644
|
+
function getDefaultAccounts(brand) {
|
|
2645
|
+
return readConfig()?.defaultAccounts?.[brand] ?? {};
|
|
2646
|
+
}
|
|
2647
|
+
function setDefaultAccount(brand, platform, value) {
|
|
2648
|
+
const cfg = readConfig() ?? {};
|
|
2649
|
+
cfg.defaultAccounts ??= {};
|
|
2650
|
+
cfg.defaultAccounts[brand] = { ...cfg.defaultAccounts[brand], [platform]: value };
|
|
2651
|
+
writeConfig(cfg);
|
|
2652
|
+
}
|
|
2653
|
+
function clearDefaultAccounts(brand) {
|
|
2654
|
+
const cfg = readConfig();
|
|
2655
|
+
if (!cfg?.defaultAccounts?.[brand]) return;
|
|
2656
|
+
delete cfg.defaultAccounts[brand];
|
|
2657
|
+
writeConfig(cfg);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// src/commands/brands.ts
|
|
2388
2661
|
function tokenStatus(c, now = Date.now()) {
|
|
2389
2662
|
if (!c.connected) return "\u2014";
|
|
2390
2663
|
if (c.refreshError) return "expired";
|
|
@@ -2518,6 +2791,28 @@ function createBrandsCommand() {
|
|
|
2518
2791
|
process.exit(exitCodeFromError(err));
|
|
2519
2792
|
}
|
|
2520
2793
|
});
|
|
2794
|
+
brands.command("set-default-account <slug> <pairs...>").description("Save default target account(s) for a brand, e.g. x=@handle linkedin=all").action((slug, pairs, _opts, cmd) => {
|
|
2795
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2796
|
+
try {
|
|
2797
|
+
const parsed = parsePairs("--account", pairs);
|
|
2798
|
+
for (const [platform, value] of Object.entries(parsed)) {
|
|
2799
|
+
setDefaultAccount(slug, platform, value);
|
|
2800
|
+
}
|
|
2801
|
+
printer.log(getDefaultAccounts(slug));
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
printer.error(err);
|
|
2804
|
+
process.exit(1);
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
brands.command("get-default-account <slug>").description("Show saved default account(s) for a brand").action((slug, _opts, cmd) => {
|
|
2808
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2809
|
+
printer.log(getDefaultAccounts(slug));
|
|
2810
|
+
});
|
|
2811
|
+
brands.command("clear-default-account <slug>").description("Remove all saved default accounts for a brand").action((slug, _opts, cmd) => {
|
|
2812
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2813
|
+
clearDefaultAccounts(slug);
|
|
2814
|
+
printer.log(getDefaultAccounts(slug));
|
|
2815
|
+
});
|
|
2521
2816
|
return brands;
|
|
2522
2817
|
}
|
|
2523
2818
|
|
|
@@ -2777,27 +3072,142 @@ function createClipsCommand() {
|
|
|
2777
3072
|
// src/commands/posts.ts
|
|
2778
3073
|
var import_commander7 = require("commander");
|
|
2779
3074
|
init_dist();
|
|
2780
|
-
|
|
3075
|
+
|
|
3076
|
+
// src/lib/post-create.ts
|
|
3077
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
3078
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3079
|
+
|
|
3080
|
+
// src/lib/account-select.ts
|
|
3081
|
+
var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
3082
|
+
var FANOUT_CONFIRM_THRESHOLD = 5;
|
|
3083
|
+
var PAGED_PICKER_THRESHOLD = 8;
|
|
3084
|
+
var isAll = (v) => v === "all" || v === "*";
|
|
3085
|
+
function needsFanoutConfirm(count, isTty, explicit) {
|
|
3086
|
+
return isTty && explicit && count > FANOUT_CONFIRM_THRESHOLD;
|
|
3087
|
+
}
|
|
3088
|
+
async function resolveTargetAccounts({
|
|
3089
|
+
brandSlug,
|
|
3090
|
+
platforms,
|
|
3091
|
+
accountFlags,
|
|
3092
|
+
client,
|
|
3093
|
+
isTty,
|
|
3094
|
+
emit = (m) => process.stderr.write(`${m}
|
|
3095
|
+
`)
|
|
3096
|
+
}) {
|
|
3097
|
+
const accountMap = {};
|
|
3098
|
+
for (const flag of accountFlags) {
|
|
3099
|
+
const eqIdx = flag.indexOf("=");
|
|
3100
|
+
if (eqIdx < 1) {
|
|
3101
|
+
throw Object.assign(
|
|
3102
|
+
new Error(`--account: expected "platform=handle|id" format, got "${flag}"`),
|
|
3103
|
+
{ exitCode: 1 }
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
const p = flag.slice(0, eqIdx).trim();
|
|
3107
|
+
if (!VALID_PLATFORMS2.includes(p)) {
|
|
3108
|
+
throw Object.assign(
|
|
3109
|
+
new Error(`--account: unknown platform "${p}". Valid: ${VALID_PLATFORMS2.join(", ")}`),
|
|
3110
|
+
{ exitCode: 1 }
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
accountMap[p] = flag.slice(eqIdx + 1).trim();
|
|
3114
|
+
}
|
|
3115
|
+
const connections = await client.brands.authStatus(brandSlug);
|
|
3116
|
+
const byPlatform = {};
|
|
3117
|
+
for (const conn of connections) {
|
|
3118
|
+
if (!conn.connected) continue;
|
|
3119
|
+
const p = conn.platform;
|
|
3120
|
+
if (!byPlatform[p]) byPlatform[p] = [];
|
|
3121
|
+
byPlatform[p].push(conn);
|
|
3122
|
+
}
|
|
3123
|
+
const result = {};
|
|
3124
|
+
for (const platform of platforms) {
|
|
3125
|
+
const accounts = byPlatform[platform] ?? [];
|
|
3126
|
+
const flagValue = accountMap[platform];
|
|
3127
|
+
const explicit = flagValue !== void 0;
|
|
3128
|
+
const specifiedValue = flagValue ?? getDefaultAccount(brandSlug, platform) ?? void 0;
|
|
3129
|
+
if (specifiedValue !== void 0 && isAll(specifiedValue)) {
|
|
3130
|
+
const ids = accounts.map((a) => a.platformUserId).filter((id) => Boolean(id));
|
|
3131
|
+
emit(`${platform}: posting to all ${ids.length} connected account(s).`);
|
|
3132
|
+
if (needsFanoutConfirm(ids.length, isTty, explicit)) {
|
|
3133
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
3134
|
+
const ok2 = await confirm({
|
|
3135
|
+
message: `Post to all ${ids.length} ${platform} accounts?`,
|
|
3136
|
+
default: false
|
|
3137
|
+
});
|
|
3138
|
+
if (!ok2) {
|
|
3139
|
+
throw Object.assign(new Error("Aborted: fan-out not confirmed."), { exitCode: 1 });
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
if (ids.length > 0) result[platform] = ids;
|
|
3143
|
+
} else if (specifiedValue !== void 0) {
|
|
3144
|
+
const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
|
|
3145
|
+
const match = accounts.find(
|
|
3146
|
+
(a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
|
|
3147
|
+
);
|
|
3148
|
+
if (!match?.platformUserId) {
|
|
3149
|
+
const valid = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
|
|
3150
|
+
throw Object.assign(
|
|
3151
|
+
new Error(
|
|
3152
|
+
`--account: unknown ${platform} account "${specifiedValue}".
|
|
3153
|
+
Connected accounts:
|
|
3154
|
+
${valid || " (none)"}`
|
|
3155
|
+
),
|
|
3156
|
+
{ exitCode: 1 }
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
if (!result[platform]) result[platform] = [];
|
|
3160
|
+
result[platform].push(match.platformUserId);
|
|
3161
|
+
} else if (accounts.length >= 2) {
|
|
3162
|
+
if (isTty) {
|
|
3163
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
3164
|
+
const choices = accounts.map((a) => ({
|
|
3165
|
+
name: `@${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`,
|
|
3166
|
+
value: a.platformUserId ?? ""
|
|
3167
|
+
}));
|
|
3168
|
+
const selected = await checkbox({
|
|
3169
|
+
message: `Multiple ${platform} accounts connected \u2014 select which to post to:`,
|
|
3170
|
+
choices,
|
|
3171
|
+
// ponytail: paged scroll for long lists (23 LinkedIn orgs). @inquirer's
|
|
3172
|
+
// checkbox has no type-to-filter; if many-account users need it, swap in
|
|
3173
|
+
// a search-capable multiselect dep — or save a default to skip the picker.
|
|
3174
|
+
pageSize: accounts.length >= PAGED_PICKER_THRESHOLD ? 12 : 7,
|
|
3175
|
+
loop: false
|
|
3176
|
+
});
|
|
3177
|
+
const valid = selected.filter(Boolean);
|
|
3178
|
+
if (valid.length > 0) result[platform] = valid;
|
|
3179
|
+
} else {
|
|
3180
|
+
const list = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
|
|
3181
|
+
throw Object.assign(
|
|
3182
|
+
new Error(
|
|
3183
|
+
`Multiple ${platform} accounts connected but no --account specified (non-interactive mode).
|
|
3184
|
+
Connected accounts:
|
|
3185
|
+
${list}
|
|
3186
|
+
Pass --account ${platform}=<handle|id> to select.`
|
|
3187
|
+
),
|
|
3188
|
+
{ exitCode: 1 }
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
return result;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/lib/post-create.ts
|
|
3197
|
+
var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
2781
3198
|
function parsePlatforms(raw) {
|
|
2782
3199
|
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2783
3200
|
if (parts.length === 0) {
|
|
2784
3201
|
throw new Error("No platforms provided. Pass a comma-separated list, e.g. --platforms x,linkedin");
|
|
2785
3202
|
}
|
|
2786
|
-
const invalid = parts.filter((p) => !
|
|
3203
|
+
const invalid = parts.filter((p) => !VALID_PLATFORMS3.includes(p));
|
|
2787
3204
|
if (invalid.length > 0) {
|
|
2788
3205
|
throw new Error(
|
|
2789
|
-
`Unsupported platform(s): ${invalid.join(", ")}. Valid platforms: ${
|
|
3206
|
+
`Unsupported platform(s): ${invalid.join(", ")}. Valid platforms: ${VALID_PLATFORMS3.join(", ")}`
|
|
2790
3207
|
);
|
|
2791
3208
|
}
|
|
2792
3209
|
return parts;
|
|
2793
3210
|
}
|
|
2794
|
-
function parsePositiveInt(value, flag) {
|
|
2795
|
-
const n = Number(value);
|
|
2796
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
2797
|
-
throw new Error(`${flag} must be a positive integer (received "${value}").`);
|
|
2798
|
-
}
|
|
2799
|
-
return n;
|
|
2800
|
-
}
|
|
2801
3211
|
function validateScheduledAt(value) {
|
|
2802
3212
|
const ms = Date.parse(value);
|
|
2803
3213
|
if (Number.isNaN(ms) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
|
|
@@ -2812,6 +3222,228 @@ function validateScheduledAt(value) {
|
|
|
2812
3222
|
}
|
|
2813
3223
|
return value;
|
|
2814
3224
|
}
|
|
3225
|
+
async function buildAndCreatePost(client, fields, opts) {
|
|
3226
|
+
const platforms = parsePlatforms(fields.platforms);
|
|
3227
|
+
const scheduledAt = fields.at ? validateScheduledAt(fields.at) : void 0;
|
|
3228
|
+
if (fields.media && fields.media.length > 0) {
|
|
3229
|
+
validateMediaCount(fields.media);
|
|
3230
|
+
validateMediaPaths(fields.media);
|
|
3231
|
+
validateMediaExtensions(fields.media);
|
|
3232
|
+
}
|
|
3233
|
+
const platformTexts = fields.platformText && fields.platformText.length > 0 ? parsePairs("--platform-text", fields.platformText) : void 0;
|
|
3234
|
+
const platformMediaPaths = fields.platformMedia && fields.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", fields.platformMedia) : {};
|
|
3235
|
+
for (const paths of Object.values(platformMediaPaths)) {
|
|
3236
|
+
validateMediaPaths(paths);
|
|
3237
|
+
validateMediaExtensions(paths);
|
|
3238
|
+
}
|
|
3239
|
+
const altTexts = alignAltText(fields.media ?? [], fields.altText ?? []);
|
|
3240
|
+
const youtubeOptions = buildYoutubeOptions({
|
|
3241
|
+
ytTitle: fields.ytTitle,
|
|
3242
|
+
ytDescription: fields.ytDescription,
|
|
3243
|
+
ytTags: fields.ytTags,
|
|
3244
|
+
ytPrivacy: fields.ytPrivacy,
|
|
3245
|
+
ytCategory: fields.ytCategory,
|
|
3246
|
+
ytMadeForKids: fields.ytMadeForKids
|
|
3247
|
+
});
|
|
3248
|
+
const instagramOptions = buildInstagramOptions({
|
|
3249
|
+
igReel: fields.igReel,
|
|
3250
|
+
igShareToFeed: fields.igShareToFeed,
|
|
3251
|
+
igCoverUrl: fields.igCoverUrl,
|
|
3252
|
+
igThumbOffsetMs: fields.igThumbOffsetMs,
|
|
3253
|
+
igCollaborators: fields.igCollaborators
|
|
3254
|
+
});
|
|
3255
|
+
const threadsOptions = buildThreadsOptions({
|
|
3256
|
+
threadsReplyTo: fields.threadsReplyTo,
|
|
3257
|
+
threadsReplyControl: fields.threadsReplyControl
|
|
3258
|
+
});
|
|
3259
|
+
const targetAccountIds = await resolveTargetAccounts({
|
|
3260
|
+
brandSlug: fields.brandSlug,
|
|
3261
|
+
platforms,
|
|
3262
|
+
accountFlags: fields.account ?? [],
|
|
3263
|
+
client,
|
|
3264
|
+
isTty: opts.isTty,
|
|
3265
|
+
emit: opts.emit
|
|
3266
|
+
});
|
|
3267
|
+
const common = {
|
|
3268
|
+
brandSlug: fields.brandSlug,
|
|
3269
|
+
text: fields.text,
|
|
3270
|
+
platforms,
|
|
3271
|
+
...scheduledAt ? { scheduledAt } : {},
|
|
3272
|
+
...fields.thread && fields.thread.length > 0 ? { thread: fields.thread } : {},
|
|
3273
|
+
...platformTexts && Object.keys(platformTexts).length > 0 ? { platformTexts } : {},
|
|
3274
|
+
...Object.keys(targetAccountIds).length > 0 ? { targetAccountIds } : {},
|
|
3275
|
+
...instagramOptions ? { instagramOptions } : {},
|
|
3276
|
+
...threadsOptions ? { threadsOptions } : {},
|
|
3277
|
+
...youtubeOptions ? { youtubeOptions } : {},
|
|
3278
|
+
source: "cli"
|
|
3279
|
+
};
|
|
3280
|
+
if (opts.dryRun) {
|
|
3281
|
+
return {
|
|
3282
|
+
dryRun: true,
|
|
3283
|
+
request: {
|
|
3284
|
+
...common,
|
|
3285
|
+
...fields.media && fields.media.length > 0 ? {
|
|
3286
|
+
media: fields.media.map((path3, i) => ({
|
|
3287
|
+
path: path3,
|
|
3288
|
+
...altTexts[i] ? { altText: altTexts[i] } : {}
|
|
3289
|
+
}))
|
|
3290
|
+
} : {},
|
|
3291
|
+
...Object.keys(platformMediaPaths).length > 0 ? { platformMedia: platformMediaPaths } : {}
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
opts.onBeforeNetwork?.();
|
|
3296
|
+
const mediaInputs = [];
|
|
3297
|
+
for (let i = 0; i < (fields.media ?? []).length; i++) {
|
|
3298
|
+
const filePath = fields.media[i];
|
|
3299
|
+
const data = await import_promises.default.readFile(filePath);
|
|
3300
|
+
const filename = import_node_path3.default.basename(filePath);
|
|
3301
|
+
const uploaded = await client.media.upload({
|
|
3302
|
+
data: new Uint8Array(data),
|
|
3303
|
+
filename,
|
|
3304
|
+
contentType: extToMime(filename)
|
|
3305
|
+
});
|
|
3306
|
+
mediaInputs.push({
|
|
3307
|
+
url: uploaded.url,
|
|
3308
|
+
type: uploaded.type,
|
|
3309
|
+
...altTexts[i] ? { altText: altTexts[i] } : {}
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
const platformMediaResult = {};
|
|
3313
|
+
for (const [p, paths] of Object.entries(platformMediaPaths)) {
|
|
3314
|
+
const uploads = [];
|
|
3315
|
+
for (const filePath of paths) {
|
|
3316
|
+
const data = await import_promises.default.readFile(filePath);
|
|
3317
|
+
const filename = import_node_path3.default.basename(filePath);
|
|
3318
|
+
const uploaded = await client.media.upload({
|
|
3319
|
+
data: new Uint8Array(data),
|
|
3320
|
+
filename,
|
|
3321
|
+
contentType: extToMime(filename)
|
|
3322
|
+
});
|
|
3323
|
+
uploads.push({ url: uploaded.url, type: uploaded.type });
|
|
3324
|
+
}
|
|
3325
|
+
platformMediaResult[p] = uploads;
|
|
3326
|
+
}
|
|
3327
|
+
return client.posts.create({
|
|
3328
|
+
...common,
|
|
3329
|
+
...mediaInputs.length > 0 ? { media: mediaInputs } : {},
|
|
3330
|
+
...Object.keys(platformMediaResult).length > 0 ? { platformMedia: platformMediaResult } : {}
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/lib/post-bulk.ts
|
|
3335
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
3336
|
+
function parseBulkFile(filePath) {
|
|
3337
|
+
const raw = import_node_fs4.default.readFileSync(filePath, "utf8");
|
|
3338
|
+
const isJson = filePath.toLowerCase().endsWith(".json") || raw.trimStart().startsWith("[");
|
|
3339
|
+
const rows = isJson ? parseJsonRows(raw) : parseCsvRows(raw);
|
|
3340
|
+
if (rows.length === 0) {
|
|
3341
|
+
throw new Error(`No records found in ${filePath}.`);
|
|
3342
|
+
}
|
|
3343
|
+
return rows;
|
|
3344
|
+
}
|
|
3345
|
+
function parseJsonRows(raw) {
|
|
3346
|
+
let data;
|
|
3347
|
+
try {
|
|
3348
|
+
data = JSON.parse(raw);
|
|
3349
|
+
} catch (e) {
|
|
3350
|
+
throw new Error(`Invalid JSON bulk input: ${e.message}`);
|
|
3351
|
+
}
|
|
3352
|
+
if (!Array.isArray(data)) {
|
|
3353
|
+
throw new Error("JSON bulk input must be an array of post objects.");
|
|
3354
|
+
}
|
|
3355
|
+
return data;
|
|
3356
|
+
}
|
|
3357
|
+
function parseCsvRows(raw) {
|
|
3358
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
3359
|
+
if (lines.length === 0) return [];
|
|
3360
|
+
const headers = splitCsvLine(lines[0]);
|
|
3361
|
+
return lines.slice(1).map((line) => {
|
|
3362
|
+
const cells = splitCsvLine(line);
|
|
3363
|
+
const row = {};
|
|
3364
|
+
headers.forEach((h, i) => {
|
|
3365
|
+
if (cells[i] !== void 0 && cells[i] !== "") row[h] = cells[i];
|
|
3366
|
+
});
|
|
3367
|
+
return row;
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
function splitCsvLine(line) {
|
|
3371
|
+
const out = [];
|
|
3372
|
+
let cur = "";
|
|
3373
|
+
let inQuotes = false;
|
|
3374
|
+
for (let i = 0; i < line.length; i++) {
|
|
3375
|
+
const c = line[i];
|
|
3376
|
+
if (inQuotes) {
|
|
3377
|
+
if (c === '"' && line[i + 1] === '"') {
|
|
3378
|
+
cur += '"';
|
|
3379
|
+
i++;
|
|
3380
|
+
} else if (c === '"') {
|
|
3381
|
+
inQuotes = false;
|
|
3382
|
+
} else {
|
|
3383
|
+
cur += c;
|
|
3384
|
+
}
|
|
3385
|
+
} else if (c === '"') {
|
|
3386
|
+
inQuotes = true;
|
|
3387
|
+
} else if (c === ",") {
|
|
3388
|
+
out.push(cur);
|
|
3389
|
+
cur = "";
|
|
3390
|
+
} else {
|
|
3391
|
+
cur += c;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
out.push(cur);
|
|
3395
|
+
return out.map((s) => s.trim());
|
|
3396
|
+
}
|
|
3397
|
+
function rowToFields(row, cliBrand) {
|
|
3398
|
+
const platformsRaw = row.platforms;
|
|
3399
|
+
const platforms = Array.isArray(platformsRaw) ? platformsRaw.join(",") : platformsRaw;
|
|
3400
|
+
const brand = resolveBrand(row.brand ?? row.brandSlug ?? cliBrand);
|
|
3401
|
+
if (!brand) {
|
|
3402
|
+
throw new Error('No brand: set "brand" in the row, pass --brand, or set a default context.');
|
|
3403
|
+
}
|
|
3404
|
+
if (!row.text) throw new Error('Missing "text".');
|
|
3405
|
+
if (!platforms) throw new Error('Missing "platforms".');
|
|
3406
|
+
for (const key of ["media", "thread", "account", "altText"]) {
|
|
3407
|
+
if (row[key] !== void 0 && !Array.isArray(row[key])) {
|
|
3408
|
+
throw new Error(`"${key}" is multi-value \u2014 use a JSON input file with an array (CSV can't express it).`);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
return {
|
|
3412
|
+
...row,
|
|
3413
|
+
brandSlug: brand,
|
|
3414
|
+
text: String(row.text),
|
|
3415
|
+
platforms: String(platforms)
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
async function createPostsBulk(client, rows, opts = {}) {
|
|
3419
|
+
const results = [];
|
|
3420
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3421
|
+
const row = rows[i];
|
|
3422
|
+
try {
|
|
3423
|
+
const fields = rowToFields(row, opts.cliBrand);
|
|
3424
|
+
const created = await buildAndCreatePost(client, fields, { isTty: false });
|
|
3425
|
+
results.push({ index: i, status: "created", id: created?.id ?? "", text: fields.text, error: "" });
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
results.push({
|
|
3428
|
+
index: i,
|
|
3429
|
+
status: "failed",
|
|
3430
|
+
id: "",
|
|
3431
|
+
text: String(row.text ?? ""),
|
|
3432
|
+
error: err.message
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
return results;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/commands/posts.ts
|
|
3440
|
+
function parsePositiveInt(value, flag) {
|
|
3441
|
+
const n = Number(value);
|
|
3442
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
3443
|
+
throw new Error(`${flag} must be a positive integer (received "${value}").`);
|
|
3444
|
+
}
|
|
3445
|
+
return n;
|
|
3446
|
+
}
|
|
2815
3447
|
function resolveExitCode5(err) {
|
|
2816
3448
|
const attached = err.exitCode;
|
|
2817
3449
|
if (typeof attached === "number") return attached;
|
|
@@ -2819,7 +3451,7 @@ function resolveExitCode5(err) {
|
|
|
2819
3451
|
}
|
|
2820
3452
|
function createPostsCommand() {
|
|
2821
3453
|
const posts = new import_commander7.Command("posts").description("Manage posts");
|
|
2822
|
-
posts.command("list").description("List posts").option("--brand <slug>", "Filter by brand slug").option("--status <status>", "Filter by status (draft|scheduled|published|failed)").option("--limit <n>", "Max results per page", "20").option("--page <n>", "Page number", "1").action(async (opts, cmd) => {
|
|
3454
|
+
posts.command("list").description("List posts").option("--brand <slug>", "Filter by brand slug (defaults to the saved context)").option("--status <status>", "Filter by status (draft|scheduled|published|failed)").option("--limit <n>", "Max results per page", "20").option("--page <n>", "Page number", "1").action(async (opts, cmd) => {
|
|
2823
3455
|
const globals = cmd.optsWithGlobals();
|
|
2824
3456
|
const printer = createPrinter(globals);
|
|
2825
3457
|
const spinner = printer.spinner("Fetching posts\u2026");
|
|
@@ -2827,7 +3459,7 @@ function createPostsCommand() {
|
|
|
2827
3459
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
2828
3460
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
2829
3461
|
const result = await client.posts.list({
|
|
2830
|
-
brandSlug: opts.brand,
|
|
3462
|
+
brandSlug: resolveBrand(opts.brand) ?? void 0,
|
|
2831
3463
|
status: opts.status,
|
|
2832
3464
|
limit: parsePositiveInt(opts.limit, "--limit"),
|
|
2833
3465
|
page: parsePositiveInt(opts.page, "--page")
|
|
@@ -2864,28 +3496,70 @@ function createPostsCommand() {
|
|
|
2864
3496
|
process.exit(resolveExitCode5(err));
|
|
2865
3497
|
}
|
|
2866
3498
|
});
|
|
2867
|
-
posts.command("create").description("Create a new post").
|
|
3499
|
+
posts.command("create").description("Create a new post").option("--brand <slug>", "Brand slug (defaults to the saved context: ap config set-context)").option("--text <text>", "Post text content (required unless --from)").option("--platforms <list>", "Comma-separated platforms, e.g. x,linkedin (required unless --from)").option("--from <file>", "Create posts in bulk from a CSV or JSON file (one post per row)").option("--at <iso>", "Schedule date/time (ISO 8601)").option(
|
|
2868
3500
|
"--thread <text...>",
|
|
2869
3501
|
"Additional posts appended after --text to form a thread (X/Threads only, max 25)"
|
|
2870
|
-
).action(
|
|
3502
|
+
).option("--media <path...>", "Media file paths to attach (max 10)").option("--alt-text <text...>", "Alt text for --media files, aligned by index").option("--platform-text <pairs...>", "Per-platform text, e.g. x=Hello linkedin=World").option("--platform-media <pairs...>", "Per-platform media, e.g. instagram=photo.jpg x=banner.png").option("--yt-title <t>", "YouTube video title").option("--yt-description <d>", "YouTube video description").option("--yt-tags <list>", "YouTube tags (comma-separated)").option("--yt-privacy <v>", "YouTube privacy status (public|unlisted|private)").option("--yt-category <id>", "YouTube category ID").option("--yt-made-for-kids", "Mark YouTube video as made for kids").option("--ig-reel", "Post as an Instagram Reel").option("--ig-share-to-feed", "Share Reel to feed").option("--ig-cover-url <url>", "Reel cover image URL").option("--ig-thumb-offset-ms <n>", "Reel thumbnail offset in milliseconds").option("--ig-collaborators <list>", "Reel collaborator handles (comma-separated)").option("--threads-reply-to <id>", "Threads post ID to reply to").option("--threads-reply-control <v>", "Threads reply control (everyone|accounts_you_follow|mentioned_only)").option("--account <p=handle|id...>", "Target specific account per platform, e.g. x=@handle").option("--dry-run", "Print the resolved request body without uploading media or creating the post").option("--preview", "Alias for --dry-run").action(
|
|
2871
3503
|
async (opts, cmd) => {
|
|
2872
3504
|
const globals = cmd.optsWithGlobals();
|
|
2873
3505
|
const printer = createPrinter(globals);
|
|
2874
|
-
|
|
3506
|
+
let spinner;
|
|
2875
3507
|
try {
|
|
2876
3508
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
2877
3509
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3510
|
+
if (opts.from) {
|
|
3511
|
+
const rows = parseBulkFile(opts.from);
|
|
3512
|
+
const records = await createPostsBulk(client, rows, {
|
|
3513
|
+
cliBrand: resolveBrand(opts.brand) ?? void 0
|
|
3514
|
+
});
|
|
3515
|
+
const truncate = printer.isTty();
|
|
3516
|
+
printer.table(
|
|
3517
|
+
records.map((r) => ({
|
|
3518
|
+
"#": r.index + 1,
|
|
3519
|
+
status: r.status,
|
|
3520
|
+
id: r.id || "\u2014",
|
|
3521
|
+
text: truncate && r.text.length > 40 ? `${r.text.slice(0, 37)}\u2026` : r.text,
|
|
3522
|
+
error: r.error
|
|
3523
|
+
})),
|
|
3524
|
+
["#", "status", "id", "text", "error"]
|
|
3525
|
+
);
|
|
3526
|
+
if (records.some((r) => r.status === "failed")) {
|
|
3527
|
+
process.exit(1);
|
|
3528
|
+
}
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
const brand = resolveBrand(opts.brand);
|
|
3532
|
+
if (!brand) {
|
|
3533
|
+
throw new Error(
|
|
3534
|
+
"No brand specified. Pass --brand <slug> or set a default with: ap config set-context --brand <slug>"
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
if (!opts.text) {
|
|
3538
|
+
throw new Error("--text is required (or use --from <file> to create posts in bulk).");
|
|
3539
|
+
}
|
|
3540
|
+
if (!opts.platforms) {
|
|
3541
|
+
throw new Error("--platforms is required (or use --from <file> to create posts in bulk).");
|
|
3542
|
+
}
|
|
3543
|
+
const result = await buildAndCreatePost(
|
|
3544
|
+
client,
|
|
3545
|
+
{ ...opts, brandSlug: brand, text: opts.text, platforms: opts.platforms },
|
|
3546
|
+
{
|
|
3547
|
+
// Picker reads stdin — gate on a real interactive terminal, not the
|
|
3548
|
+
// output mode (printer.isTty() is true even when stdin is piped).
|
|
3549
|
+
isTty: Boolean(process.stdin.isTTY),
|
|
3550
|
+
// --dry-run/--preview: resolve + validate, then return the request body
|
|
3551
|
+
// without uploading media or POSTing (onBeforeNetwork never fires).
|
|
3552
|
+
dryRun: Boolean(opts.dryRun || opts.preview),
|
|
3553
|
+
// Picker done — start the spinner now (covers uploads + create).
|
|
3554
|
+
onBeforeNetwork: () => {
|
|
3555
|
+
spinner = printer.spinner("Creating post\u2026");
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
);
|
|
3559
|
+
spinner?.stop();
|
|
3560
|
+
printer.log(result);
|
|
2887
3561
|
} catch (err) {
|
|
2888
|
-
spinner
|
|
3562
|
+
spinner?.fail();
|
|
2889
3563
|
printer.error(err);
|
|
2890
3564
|
process.exit(resolveExitCode5(err));
|
|
2891
3565
|
}
|
|
@@ -2930,7 +3604,7 @@ function createPostsCommand() {
|
|
|
2930
3604
|
} catch (err) {
|
|
2931
3605
|
spinner.fail();
|
|
2932
3606
|
printer.error(err);
|
|
2933
|
-
if (!(err instanceof NotFoundError)
|
|
3607
|
+
if (!(err instanceof NotFoundError)) {
|
|
2934
3608
|
printer.error(`The post may not have been deleted \u2014 verify with: ap posts get "${id}"`);
|
|
2935
3609
|
}
|
|
2936
3610
|
process.exit(resolveExitCode5(err));
|
|
@@ -2952,14 +3626,22 @@ function createPostsCommand() {
|
|
|
2952
3626
|
process.exit(resolveExitCode5(err));
|
|
2953
3627
|
}
|
|
2954
3628
|
});
|
|
2955
|
-
posts.command("schedule <id>").description("Schedule a post for a specific time").
|
|
3629
|
+
posts.command("schedule <id>").description("Schedule a post for a specific time, or --cancel to unschedule it").option("--at <iso>", "ISO 8601 datetime to schedule the post").option("--cancel", "Unschedule the post (return it to draft)").action(async (id, opts, cmd) => {
|
|
2956
3630
|
const globals = cmd.optsWithGlobals();
|
|
2957
3631
|
const printer = createPrinter(globals);
|
|
2958
|
-
const spinner = printer.spinner(
|
|
3632
|
+
const spinner = printer.spinner(
|
|
3633
|
+
opts.cancel ? `Unscheduling post "${id}"\u2026` : `Scheduling post "${id}"\u2026`
|
|
3634
|
+
);
|
|
2959
3635
|
try {
|
|
3636
|
+
if (opts.cancel && opts.at) {
|
|
3637
|
+
throw new Error("Cannot use --at together with --cancel \u2014 choose one.");
|
|
3638
|
+
}
|
|
3639
|
+
if (!opts.cancel && !opts.at) {
|
|
3640
|
+
throw new Error("Provide either --at <iso> to schedule, or --cancel to unschedule.");
|
|
3641
|
+
}
|
|
2960
3642
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
2961
3643
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
2962
|
-
const post = await client.posts.schedule(id, validateScheduledAt(opts.at));
|
|
3644
|
+
const post = opts.cancel ? await client.posts.unschedule(id) : await client.posts.schedule(id, validateScheduledAt(opts.at));
|
|
2963
3645
|
spinner.stop();
|
|
2964
3646
|
printer.log(post);
|
|
2965
3647
|
} catch (err) {
|
|
@@ -3525,7 +4207,7 @@ function createWorkspacesCommand() {
|
|
|
3525
4207
|
// src/commands/doctor.ts
|
|
3526
4208
|
var import_commander13 = require("commander");
|
|
3527
4209
|
init_dist();
|
|
3528
|
-
var
|
|
4210
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
3529
4211
|
var API_HEALTH_URL = "https://app.autoposting.ai/api-proxy/health";
|
|
3530
4212
|
async function runChecks() {
|
|
3531
4213
|
const results = [];
|
|
@@ -3545,7 +4227,7 @@ async function runChecks() {
|
|
|
3545
4227
|
}
|
|
3546
4228
|
const credPath = getCredentialsPath();
|
|
3547
4229
|
try {
|
|
3548
|
-
const stat =
|
|
4230
|
+
const stat = import_node_fs5.default.statSync(credPath);
|
|
3549
4231
|
const mode = stat.mode & 511;
|
|
3550
4232
|
const isSecure = mode === 384;
|
|
3551
4233
|
results.push({
|
|
@@ -3825,8 +4507,34 @@ function createMcpCommand() {
|
|
|
3825
4507
|
});
|
|
3826
4508
|
}
|
|
3827
4509
|
|
|
4510
|
+
// src/commands/config.ts
|
|
4511
|
+
var import_commander19 = require("commander");
|
|
4512
|
+
function createConfigCommand() {
|
|
4513
|
+
const config = new import_commander19.Command("config").description("Manage CLI defaults (stored in config.json)");
|
|
4514
|
+
config.command("set-context").description("Set the default brand used when --brand is omitted").requiredOption("--brand <slug>", "Brand slug to use as the default context").action((opts, cmd) => {
|
|
4515
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4516
|
+
setContextBrand(opts.brand);
|
|
4517
|
+
printer.log({ brand: opts.brand });
|
|
4518
|
+
});
|
|
4519
|
+
config.command("get-context").description("Show the default brand context").action((_opts, cmd) => {
|
|
4520
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4521
|
+
const brand = getContextBrand();
|
|
4522
|
+
if (!brand) {
|
|
4523
|
+
printer.error("No brand context set. Set one with: ap config set-context --brand <slug>");
|
|
4524
|
+
process.exit(1);
|
|
4525
|
+
}
|
|
4526
|
+
printer.log({ brand });
|
|
4527
|
+
});
|
|
4528
|
+
config.command("unset-context").description("Clear the default brand context").action((_opts, cmd) => {
|
|
4529
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4530
|
+
unsetContextBrand();
|
|
4531
|
+
printer.log({ brand: null });
|
|
4532
|
+
});
|
|
4533
|
+
return config;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
3828
4536
|
// src/cli.ts
|
|
3829
|
-
var program = new
|
|
4537
|
+
var program = new import_commander20.Command().name("autoposting").description("Autoposting CLI \u2014 manage social media from the terminal").version(VERSION).option("--json", "Output as JSON").option("--quiet", "Suppress spinners and non-essential output").option("--format <type>", "Output format: auto (table on a terminal, JSON when piped), table, json", "auto").option("--jq <expr>", 'Filter JSON output with a minimal jq expression (e.g. ".[].id")').option("--api-key <key>", "API key (overrides env/stored credentials)").option("--no-color", "Disable color output").hook("preAction", (thisCommand) => {
|
|
3830
4538
|
const opts = thisCommand.opts();
|
|
3831
4539
|
if (!opts.color) disableColor();
|
|
3832
4540
|
});
|
|
@@ -3848,5 +4556,6 @@ program.addCommand(createOpenCommand());
|
|
|
3848
4556
|
program.addCommand(createUpdateCommand());
|
|
3849
4557
|
program.addCommand(createCompletionCommand());
|
|
3850
4558
|
program.addCommand(createMcpCommand());
|
|
4559
|
+
program.addCommand(createConfigCommand());
|
|
3851
4560
|
program.parse();
|
|
3852
4561
|
//# sourceMappingURL=cli.cjs.map
|