@autoposting.ai/cli 0.3.3 → 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 +15 -3
- package/dist/cli.cjs +646 -254
- package/dist/cli.cjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -46,7 +46,7 @@ var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, V
|
|
|
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,20 @@ 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
93
|
}
|
|
94
94
|
};
|
|
95
95
|
MediaResource = class extends Resource {
|
|
@@ -425,7 +425,7 @@ var init_dist = __esm({
|
|
|
425
425
|
* failures — network errors, timeouts, and 5xx — with exponential backoff + jitter.
|
|
426
426
|
* Non-idempotent methods (POST/PATCH) are never retried, to avoid duplicate side effects.
|
|
427
427
|
*/
|
|
428
|
-
async request(method,
|
|
428
|
+
async request(method, path3, body, query) {
|
|
429
429
|
let lastErr;
|
|
430
430
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
431
431
|
if (attempt > 0) {
|
|
@@ -433,7 +433,7 @@ var init_dist = __esm({
|
|
|
433
433
|
await sleep(backoff + Math.floor(Math.random() * this.retryBaseMs));
|
|
434
434
|
}
|
|
435
435
|
try {
|
|
436
|
-
return await this._send(method,
|
|
436
|
+
return await this._send(method, path3, body, query);
|
|
437
437
|
} catch (err) {
|
|
438
438
|
lastErr = err;
|
|
439
439
|
if (attempt < this.maxRetries && this.isRetryable(method, err)) continue;
|
|
@@ -448,8 +448,8 @@ var init_dist = __esm({
|
|
|
448
448
|
if (err.code === "NETWORK_ERROR" || err.code === "TIMEOUT") return true;
|
|
449
449
|
return RETRYABLE_STATUS.has(err.status);
|
|
450
450
|
}
|
|
451
|
-
async _send(method,
|
|
452
|
-
let url = `${this.baseUrl}${
|
|
451
|
+
async _send(method, path3, body, query) {
|
|
452
|
+
let url = `${this.baseUrl}${path3}`;
|
|
453
453
|
if (query && Object.keys(query).length > 0) {
|
|
454
454
|
const params = new URLSearchParams();
|
|
455
455
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -483,13 +483,13 @@ var init_dist = __esm({
|
|
|
483
483
|
} catch (err) {
|
|
484
484
|
if (err instanceof Error && err.name === "AbortError") {
|
|
485
485
|
throw new AutopostingError(
|
|
486
|
-
`Request to ${method} ${
|
|
486
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
487
487
|
0,
|
|
488
488
|
"TIMEOUT"
|
|
489
489
|
);
|
|
490
490
|
}
|
|
491
491
|
throw new AutopostingError(
|
|
492
|
-
`Network request to ${method} ${
|
|
492
|
+
`Network request to ${method} ${path3} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
493
493
|
0,
|
|
494
494
|
"NETWORK_ERROR"
|
|
495
495
|
);
|
|
@@ -534,7 +534,7 @@ var init_dist = __esm({
|
|
|
534
534
|
parsed = JSON.parse(text);
|
|
535
535
|
} catch {
|
|
536
536
|
throw new AutopostingError(
|
|
537
|
-
`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.`,
|
|
538
538
|
response.status,
|
|
539
539
|
"INVALID_RESPONSE"
|
|
540
540
|
);
|
|
@@ -547,7 +547,7 @@ var init_dist = __esm({
|
|
|
547
547
|
} catch (err) {
|
|
548
548
|
if (err instanceof Error && err.name === "AbortError") {
|
|
549
549
|
throw new AutopostingError(
|
|
550
|
-
`Request to ${method} ${
|
|
550
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
551
551
|
0,
|
|
552
552
|
"TIMEOUT"
|
|
553
553
|
);
|
|
@@ -1675,7 +1675,7 @@ var init_server = __esm({
|
|
|
1675
1675
|
});
|
|
1676
1676
|
|
|
1677
1677
|
// src/cli.ts
|
|
1678
|
-
var
|
|
1678
|
+
var import_commander20 = require("commander");
|
|
1679
1679
|
init_dist();
|
|
1680
1680
|
|
|
1681
1681
|
// src/commands/agents.ts
|
|
@@ -1823,6 +1823,67 @@ function formatError(error, mode) {
|
|
|
1823
1823
|
if (mode === "tty") return red(`Error: ${message}`);
|
|
1824
1824
|
return JSON.stringify({ error: message }, null, 2);
|
|
1825
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
|
+
}
|
|
1826
1887
|
|
|
1827
1888
|
// src/output/spinner.ts
|
|
1828
1889
|
var NO_OP_SPINNER = {
|
|
@@ -1870,19 +1931,23 @@ function createSpinner(mode) {
|
|
|
1870
1931
|
|
|
1871
1932
|
// src/output/printer.ts
|
|
1872
1933
|
var Printer = class {
|
|
1873
|
-
constructor(mode) {
|
|
1934
|
+
constructor(mode, jq) {
|
|
1874
1935
|
this.mode = mode;
|
|
1936
|
+
this.jq = jq;
|
|
1875
1937
|
}
|
|
1876
1938
|
mode;
|
|
1939
|
+
jq;
|
|
1877
1940
|
/** True only for human-readable output, where extra hint lines are safe to print. */
|
|
1878
1941
|
isTty() {
|
|
1879
|
-
return this.mode === "tty";
|
|
1942
|
+
return this.mode === "tty" && !this.jq;
|
|
1880
1943
|
}
|
|
1881
1944
|
log(data) {
|
|
1945
|
+
if (this.jq) return this.emitJq(data);
|
|
1882
1946
|
const out = formatOutput(data, this.mode);
|
|
1883
1947
|
if (out !== "") console.log(out);
|
|
1884
1948
|
}
|
|
1885
1949
|
table(rows, columns) {
|
|
1950
|
+
if (this.jq) return this.emitJq(rows);
|
|
1886
1951
|
if (this.mode === "quiet") return;
|
|
1887
1952
|
if (this.mode === "json") {
|
|
1888
1953
|
console.log(formatOutput(rows, "json"));
|
|
@@ -1891,19 +1956,23 @@ var Printer = class {
|
|
|
1891
1956
|
const out = formatTable(rows, columns);
|
|
1892
1957
|
if (out !== "") console.log(out);
|
|
1893
1958
|
}
|
|
1959
|
+
emitJq(data) {
|
|
1960
|
+
const out = formatJq(data, this.jq);
|
|
1961
|
+
if (out !== "") console.log(out);
|
|
1962
|
+
}
|
|
1894
1963
|
error(err) {
|
|
1895
|
-
const out = formatError(err, this.mode);
|
|
1964
|
+
const out = formatError(err, this.jq ? "json" : this.mode);
|
|
1896
1965
|
console.error(out);
|
|
1897
1966
|
}
|
|
1898
1967
|
spinner(message) {
|
|
1899
|
-
const s = createSpinner(this.mode);
|
|
1968
|
+
const s = createSpinner(this.jq ? "json" : this.mode);
|
|
1900
1969
|
s.start(message);
|
|
1901
1970
|
return s;
|
|
1902
1971
|
}
|
|
1903
1972
|
};
|
|
1904
1973
|
function createPrinter(options) {
|
|
1905
1974
|
const mode = detectOutputMode(options);
|
|
1906
|
-
return new Printer(mode);
|
|
1975
|
+
return new Printer(mode, options.jq);
|
|
1907
1976
|
}
|
|
1908
1977
|
|
|
1909
1978
|
// src/output/exit-codes.ts
|
|
@@ -2400,6 +2469,195 @@ function createBillingCommand() {
|
|
|
2400
2469
|
// src/commands/brands.ts
|
|
2401
2470
|
var import_commander4 = require("commander");
|
|
2402
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
|
|
2403
2661
|
function tokenStatus(c, now = Date.now()) {
|
|
2404
2662
|
if (!c.connected) return "\u2014";
|
|
2405
2663
|
if (c.refreshError) return "expired";
|
|
@@ -2533,6 +2791,28 @@ function createBrandsCommand() {
|
|
|
2533
2791
|
process.exit(exitCodeFromError(err));
|
|
2534
2792
|
}
|
|
2535
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
|
+
});
|
|
2536
2816
|
return brands;
|
|
2537
2817
|
}
|
|
2538
2818
|
|
|
@@ -2792,136 +3072,27 @@ function createClipsCommand() {
|
|
|
2792
3072
|
// src/commands/posts.ts
|
|
2793
3073
|
var import_commander7 = require("commander");
|
|
2794
3074
|
init_dist();
|
|
2795
|
-
var import_promises = __toESM(require("fs/promises"));
|
|
2796
|
-
var import_node_path2 = __toESM(require("path"));
|
|
2797
3075
|
|
|
2798
|
-
// src/lib/
|
|
2799
|
-
var
|
|
2800
|
-
var
|
|
2801
|
-
var EXT_TO_MIME = {
|
|
2802
|
-
jpg: "image/jpeg",
|
|
2803
|
-
jpeg: "image/jpeg",
|
|
2804
|
-
png: "image/png",
|
|
2805
|
-
gif: "image/gif",
|
|
2806
|
-
webp: "image/webp",
|
|
2807
|
-
mp4: "video/mp4",
|
|
2808
|
-
mov: "video/quicktime",
|
|
2809
|
-
webm: "video/webm"
|
|
2810
|
-
};
|
|
2811
|
-
function extToMime(filename) {
|
|
2812
|
-
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
2813
|
-
const mime = EXT_TO_MIME[ext];
|
|
2814
|
-
if (!mime) {
|
|
2815
|
-
throw new Error(
|
|
2816
|
-
`Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
|
|
2817
|
-
);
|
|
2818
|
-
}
|
|
2819
|
-
return mime;
|
|
2820
|
-
}
|
|
2821
|
-
function parsePairs(flag, vals) {
|
|
2822
|
-
const result = {};
|
|
2823
|
-
for (const val of vals) {
|
|
2824
|
-
const eqIdx = val.indexOf("=");
|
|
2825
|
-
if (eqIdx < 1) {
|
|
2826
|
-
throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
|
|
2827
|
-
}
|
|
2828
|
-
const p = val.slice(0, eqIdx).trim();
|
|
2829
|
-
const v = val.slice(eqIdx + 1);
|
|
2830
|
-
if (!VALID_PLATFORMS.includes(p)) {
|
|
2831
|
-
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2832
|
-
}
|
|
2833
|
-
result[p] = v;
|
|
2834
|
-
}
|
|
2835
|
-
return result;
|
|
2836
|
-
}
|
|
2837
|
-
function parsePlatformMediaPairs(flag, vals) {
|
|
2838
|
-
const result = {};
|
|
2839
|
-
for (const val of vals) {
|
|
2840
|
-
const eqIdx = val.indexOf("=");
|
|
2841
|
-
if (eqIdx < 1) {
|
|
2842
|
-
throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
|
|
2843
|
-
}
|
|
2844
|
-
const p = val.slice(0, eqIdx).trim();
|
|
2845
|
-
const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2846
|
-
if (!VALID_PLATFORMS.includes(p)) {
|
|
2847
|
-
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2848
|
-
}
|
|
2849
|
-
result[p] = paths;
|
|
2850
|
-
}
|
|
2851
|
-
return result;
|
|
2852
|
-
}
|
|
2853
|
-
function validateMediaExtensions(paths) {
|
|
2854
|
-
for (const p of paths) extToMime(p);
|
|
2855
|
-
}
|
|
2856
|
-
function validateMediaCount(paths) {
|
|
2857
|
-
if (paths.length > 10) {
|
|
2858
|
-
throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
function validateMediaPaths(paths) {
|
|
2862
|
-
for (const p of paths) {
|
|
2863
|
-
if (!(0, import_node_fs2.existsSync)(p)) {
|
|
2864
|
-
throw new Error(`Media file not found: "${p}"`);
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
function alignAltText(mediaPaths, altTexts) {
|
|
2869
|
-
if (altTexts.length > mediaPaths.length) {
|
|
2870
|
-
throw new Error(
|
|
2871
|
-
`--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
|
|
2872
|
-
);
|
|
2873
|
-
}
|
|
2874
|
-
return mediaPaths.map((_, i) => altTexts[i]);
|
|
2875
|
-
}
|
|
2876
|
-
function buildYoutubeOptions(opts) {
|
|
2877
|
-
const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
|
|
2878
|
-
const o = {};
|
|
2879
|
-
if (ytTitle) o.title = ytTitle;
|
|
2880
|
-
if (ytDescription) o.description = ytDescription;
|
|
2881
|
-
if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2882
|
-
if (ytPrivacy) o.privacyStatus = ytPrivacy;
|
|
2883
|
-
if (ytCategory) o.categoryId = ytCategory;
|
|
2884
|
-
if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
|
|
2885
|
-
return Object.keys(o).length > 0 ? o : void 0;
|
|
2886
|
-
}
|
|
2887
|
-
function buildInstagramOptions(opts) {
|
|
2888
|
-
const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
|
|
2889
|
-
const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
|
|
2890
|
-
if (!hasAnyReelFlag) return void 0;
|
|
2891
|
-
const reel = {};
|
|
2892
|
-
if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
|
|
2893
|
-
if (igCoverUrl) reel.coverUrl = igCoverUrl;
|
|
2894
|
-
if (igThumbOffsetMs !== void 0) {
|
|
2895
|
-
const n = Number(igThumbOffsetMs);
|
|
2896
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
2897
|
-
throw new Error(
|
|
2898
|
-
`--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
|
|
2899
|
-
);
|
|
2900
|
-
}
|
|
2901
|
-
reel.thumbOffsetMs = n;
|
|
2902
|
-
}
|
|
2903
|
-
if (igCollaborators) {
|
|
2904
|
-
reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2905
|
-
}
|
|
2906
|
-
return { reel };
|
|
2907
|
-
}
|
|
2908
|
-
function buildThreadsOptions(opts) {
|
|
2909
|
-
const { threadsReplyTo, threadsReplyControl } = opts;
|
|
2910
|
-
if (!threadsReplyTo && !threadsReplyControl) return void 0;
|
|
2911
|
-
const o = {};
|
|
2912
|
-
if (threadsReplyTo) o.replyToId = threadsReplyTo;
|
|
2913
|
-
if (threadsReplyControl) o.replyControl = threadsReplyControl;
|
|
2914
|
-
return o;
|
|
2915
|
-
}
|
|
3076
|
+
// src/lib/post-create.ts
|
|
3077
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
3078
|
+
var import_node_path3 = __toESM(require("path"));
|
|
2916
3079
|
|
|
2917
3080
|
// src/lib/account-select.ts
|
|
2918
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
|
+
}
|
|
2919
3088
|
async function resolveTargetAccounts({
|
|
2920
3089
|
brandSlug,
|
|
2921
3090
|
platforms,
|
|
2922
3091
|
accountFlags,
|
|
2923
3092
|
client,
|
|
2924
|
-
isTty
|
|
3093
|
+
isTty,
|
|
3094
|
+
emit = (m) => process.stderr.write(`${m}
|
|
3095
|
+
`)
|
|
2925
3096
|
}) {
|
|
2926
3097
|
const accountMap = {};
|
|
2927
3098
|
for (const flag of accountFlags) {
|
|
@@ -2952,8 +3123,24 @@ async function resolveTargetAccounts({
|
|
|
2952
3123
|
const result = {};
|
|
2953
3124
|
for (const platform of platforms) {
|
|
2954
3125
|
const accounts = byPlatform[platform] ?? [];
|
|
2955
|
-
const
|
|
2956
|
-
|
|
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) {
|
|
2957
3144
|
const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
|
|
2958
3145
|
const match = accounts.find(
|
|
2959
3146
|
(a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
|
|
@@ -2980,7 +3167,12 @@ ${valid || " (none)"}`
|
|
|
2980
3167
|
}));
|
|
2981
3168
|
const selected = await checkbox({
|
|
2982
3169
|
message: `Multiple ${platform} accounts connected \u2014 select which to post to:`,
|
|
2983
|
-
choices
|
|
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
|
|
2984
3176
|
});
|
|
2985
3177
|
const valid = selected.filter(Boolean);
|
|
2986
3178
|
if (valid.length > 0) result[platform] = valid;
|
|
@@ -3001,7 +3193,7 @@ Pass --account ${platform}=<handle|id> to select.`
|
|
|
3001
3193
|
return result;
|
|
3002
3194
|
}
|
|
3003
3195
|
|
|
3004
|
-
// src/
|
|
3196
|
+
// src/lib/post-create.ts
|
|
3005
3197
|
var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
3006
3198
|
function parsePlatforms(raw) {
|
|
3007
3199
|
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
@@ -3016,13 +3208,6 @@ function parsePlatforms(raw) {
|
|
|
3016
3208
|
}
|
|
3017
3209
|
return parts;
|
|
3018
3210
|
}
|
|
3019
|
-
function parsePositiveInt(value, flag) {
|
|
3020
|
-
const n = Number(value);
|
|
3021
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
3022
|
-
throw new Error(`${flag} must be a positive integer (received "${value}").`);
|
|
3023
|
-
}
|
|
3024
|
-
return n;
|
|
3025
|
-
}
|
|
3026
3211
|
function validateScheduledAt(value) {
|
|
3027
3212
|
const ms = Date.parse(value);
|
|
3028
3213
|
if (Number.isNaN(ms) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
|
|
@@ -3037,6 +3222,228 @@ function validateScheduledAt(value) {
|
|
|
3037
3222
|
}
|
|
3038
3223
|
return value;
|
|
3039
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
|
+
}
|
|
3040
3447
|
function resolveExitCode5(err) {
|
|
3041
3448
|
const attached = err.exitCode;
|
|
3042
3449
|
if (typeof attached === "number") return attached;
|
|
@@ -3044,7 +3451,7 @@ function resolveExitCode5(err) {
|
|
|
3044
3451
|
}
|
|
3045
3452
|
function createPostsCommand() {
|
|
3046
3453
|
const posts = new import_commander7.Command("posts").description("Manage posts");
|
|
3047
|
-
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) => {
|
|
3048
3455
|
const globals = cmd.optsWithGlobals();
|
|
3049
3456
|
const printer = createPrinter(globals);
|
|
3050
3457
|
const spinner = printer.spinner("Fetching posts\u2026");
|
|
@@ -3052,7 +3459,7 @@ function createPostsCommand() {
|
|
|
3052
3459
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
3053
3460
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
3054
3461
|
const result = await client.posts.list({
|
|
3055
|
-
brandSlug: opts.brand,
|
|
3462
|
+
brandSlug: resolveBrand(opts.brand) ?? void 0,
|
|
3056
3463
|
status: opts.status,
|
|
3057
3464
|
limit: parsePositiveInt(opts.limit, "--limit"),
|
|
3058
3465
|
page: parsePositiveInt(opts.page, "--page")
|
|
@@ -3089,10 +3496,10 @@ function createPostsCommand() {
|
|
|
3089
3496
|
process.exit(resolveExitCode5(err));
|
|
3090
3497
|
}
|
|
3091
3498
|
});
|
|
3092
|
-
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(
|
|
3093
3500
|
"--thread <text...>",
|
|
3094
3501
|
"Additional posts appended after --text to form a thread (X/Threads only, max 25)"
|
|
3095
|
-
).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").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(
|
|
3096
3503
|
async (opts, cmd) => {
|
|
3097
3504
|
const globals = cmd.optsWithGlobals();
|
|
3098
3505
|
const printer = createPrinter(globals);
|
|
@@ -3100,99 +3507,57 @@ function createPostsCommand() {
|
|
|
3100
3507
|
try {
|
|
3101
3508
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
3102
3509
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
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;
|
|
3109
3530
|
}
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
+
);
|
|
3115
3536
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
ytTitle: opts.ytTitle,
|
|
3119
|
-
ytDescription: opts.ytDescription,
|
|
3120
|
-
ytTags: opts.ytTags,
|
|
3121
|
-
ytPrivacy: opts.ytPrivacy,
|
|
3122
|
-
ytCategory: opts.ytCategory,
|
|
3123
|
-
ytMadeForKids: opts.ytMadeForKids
|
|
3124
|
-
});
|
|
3125
|
-
const instagramOptions = buildInstagramOptions({
|
|
3126
|
-
igReel: opts.igReel,
|
|
3127
|
-
igShareToFeed: opts.igShareToFeed,
|
|
3128
|
-
igCoverUrl: opts.igCoverUrl,
|
|
3129
|
-
igThumbOffsetMs: opts.igThumbOffsetMs,
|
|
3130
|
-
igCollaborators: opts.igCollaborators
|
|
3131
|
-
});
|
|
3132
|
-
const threadsOptions = buildThreadsOptions({
|
|
3133
|
-
threadsReplyTo: opts.threadsReplyTo,
|
|
3134
|
-
threadsReplyControl: opts.threadsReplyControl
|
|
3135
|
-
});
|
|
3136
|
-
const targetAccountIds = await resolveTargetAccounts({
|
|
3137
|
-
brandSlug: opts.brand,
|
|
3138
|
-
platforms,
|
|
3139
|
-
accountFlags: opts.account ?? [],
|
|
3140
|
-
client,
|
|
3141
|
-
// Picker reads stdin — gate on a real interactive terminal, not the
|
|
3142
|
-
// output mode (printer.isTty() is true even when stdout/stdin is piped).
|
|
3143
|
-
isTty: Boolean(process.stdin.isTTY)
|
|
3144
|
-
});
|
|
3145
|
-
spinner = printer.spinner("Creating post\u2026");
|
|
3146
|
-
const mediaInputs = [];
|
|
3147
|
-
for (let i = 0; i < (opts.media ?? []).length; i++) {
|
|
3148
|
-
const filePath = opts.media[i];
|
|
3149
|
-
const data = await import_promises.default.readFile(filePath);
|
|
3150
|
-
const filename = import_node_path2.default.basename(filePath);
|
|
3151
|
-
const contentType = extToMime(filename);
|
|
3152
|
-
const uploaded = await client.media.upload({
|
|
3153
|
-
data: new Uint8Array(data),
|
|
3154
|
-
filename,
|
|
3155
|
-
contentType
|
|
3156
|
-
});
|
|
3157
|
-
mediaInputs.push({
|
|
3158
|
-
url: uploaded.url,
|
|
3159
|
-
type: uploaded.type,
|
|
3160
|
-
...altTexts[i] ? { altText: altTexts[i] } : {}
|
|
3161
|
-
});
|
|
3537
|
+
if (!opts.text) {
|
|
3538
|
+
throw new Error("--text is required (or use --from <file> to create posts in bulk).");
|
|
3162
3539
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
const uploads = [];
|
|
3166
|
-
for (const filePath of paths) {
|
|
3167
|
-
const data = await import_promises.default.readFile(filePath);
|
|
3168
|
-
const filename = import_node_path2.default.basename(filePath);
|
|
3169
|
-
const contentType = extToMime(filename);
|
|
3170
|
-
const uploaded = await client.media.upload({
|
|
3171
|
-
data: new Uint8Array(data),
|
|
3172
|
-
filename,
|
|
3173
|
-
contentType
|
|
3174
|
-
});
|
|
3175
|
-
uploads.push({ url: uploaded.url, type: uploaded.type });
|
|
3176
|
-
}
|
|
3177
|
-
platformMediaResult[p] = uploads;
|
|
3540
|
+
if (!opts.platforms) {
|
|
3541
|
+
throw new Error("--platforms is required (or use --from <file> to create posts in bulk).");
|
|
3178
3542
|
}
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
text: opts.text,
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
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);
|
|
3196
3561
|
} catch (err) {
|
|
3197
3562
|
spinner?.fail();
|
|
3198
3563
|
printer.error(err);
|
|
@@ -3239,7 +3604,7 @@ function createPostsCommand() {
|
|
|
3239
3604
|
} catch (err) {
|
|
3240
3605
|
spinner.fail();
|
|
3241
3606
|
printer.error(err);
|
|
3242
|
-
if (!(err instanceof NotFoundError)
|
|
3607
|
+
if (!(err instanceof NotFoundError)) {
|
|
3243
3608
|
printer.error(`The post may not have been deleted \u2014 verify with: ap posts get "${id}"`);
|
|
3244
3609
|
}
|
|
3245
3610
|
process.exit(resolveExitCode5(err));
|
|
@@ -3842,7 +4207,7 @@ function createWorkspacesCommand() {
|
|
|
3842
4207
|
// src/commands/doctor.ts
|
|
3843
4208
|
var import_commander13 = require("commander");
|
|
3844
4209
|
init_dist();
|
|
3845
|
-
var
|
|
4210
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
3846
4211
|
var API_HEALTH_URL = "https://app.autoposting.ai/api-proxy/health";
|
|
3847
4212
|
async function runChecks() {
|
|
3848
4213
|
const results = [];
|
|
@@ -3862,7 +4227,7 @@ async function runChecks() {
|
|
|
3862
4227
|
}
|
|
3863
4228
|
const credPath = getCredentialsPath();
|
|
3864
4229
|
try {
|
|
3865
|
-
const stat =
|
|
4230
|
+
const stat = import_node_fs5.default.statSync(credPath);
|
|
3866
4231
|
const mode = stat.mode & 511;
|
|
3867
4232
|
const isSecure = mode === 384;
|
|
3868
4233
|
results.push({
|
|
@@ -4142,8 +4507,34 @@ function createMcpCommand() {
|
|
|
4142
4507
|
});
|
|
4143
4508
|
}
|
|
4144
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
|
+
|
|
4145
4536
|
// src/cli.ts
|
|
4146
|
-
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) => {
|
|
4147
4538
|
const opts = thisCommand.opts();
|
|
4148
4539
|
if (!opts.color) disableColor();
|
|
4149
4540
|
});
|
|
@@ -4165,5 +4556,6 @@ program.addCommand(createOpenCommand());
|
|
|
4165
4556
|
program.addCommand(createUpdateCommand());
|
|
4166
4557
|
program.addCommand(createCompletionCommand());
|
|
4167
4558
|
program.addCommand(createMcpCommand());
|
|
4559
|
+
program.addCommand(createConfigCommand());
|
|
4168
4560
|
program.parse();
|
|
4169
4561
|
//# sourceMappingURL=cli.cjs.map
|