@autoposting.ai/cli 0.3.3 → 0.3.5
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 +765 -266
- package/dist/cli.cjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -42,11 +42,44 @@ 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
|
-
|
|
45
|
+
function resolvePartSize(fileSize, requested) {
|
|
46
|
+
let size = requested && requested > 0 ? requested : DEFAULT_PART_SIZE;
|
|
47
|
+
if (Math.ceil(fileSize / size) > MAX_PARTS) size = Math.ceil(fileSize / MAX_PARTS);
|
|
48
|
+
return Math.max(size, MIN_PART_SIZE);
|
|
49
|
+
}
|
|
50
|
+
function guessVideoMime(fileName) {
|
|
51
|
+
const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
|
|
52
|
+
return VIDEO_MIME[ext] ?? "video/mp4";
|
|
53
|
+
}
|
|
54
|
+
async function putPart(url, body) {
|
|
55
|
+
let res;
|
|
56
|
+
try {
|
|
57
|
+
res = await fetch(url, { method: "PUT", body });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new AutopostingError(
|
|
60
|
+
`Failed to upload part: ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
+
0,
|
|
62
|
+
"UPLOAD_FAILED"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
throw new AutopostingError(
|
|
67
|
+
`Failed to upload part: HTTP ${res.status} ${res.statusText}`,
|
|
68
|
+
res.status,
|
|
69
|
+
"UPLOAD_FAILED"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const etag = res.headers.get("etag");
|
|
73
|
+
if (!etag) {
|
|
74
|
+
throw new AutopostingError("Upload part response is missing the ETag header.", res.status, "UPLOAD_FAILED");
|
|
75
|
+
}
|
|
76
|
+
return etag;
|
|
77
|
+
}
|
|
78
|
+
var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, ValidationError, RateLimitError, ServerError, Resource, MediaResource, AgentsResource, BillingResource, BrandsResource, CarouselsResource, MIN_PART_SIZE, DEFAULT_PART_SIZE, MAX_PARTS, VIDEO_MIME, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
|
|
46
79
|
var init_dist = __esm({
|
|
47
80
|
"../sdk/dist/index.mjs"() {
|
|
48
81
|
"use strict";
|
|
49
|
-
VERSION = "0.3.
|
|
82
|
+
VERSION = "0.3.5";
|
|
50
83
|
AutopostingError = class extends Error {
|
|
51
84
|
status;
|
|
52
85
|
code;
|
|
@@ -76,20 +109,20 @@ var init_dist = __esm({
|
|
|
76
109
|
this.client = client;
|
|
77
110
|
}
|
|
78
111
|
client;
|
|
79
|
-
get(
|
|
80
|
-
return this.client.request("GET",
|
|
112
|
+
get(path3, query) {
|
|
113
|
+
return this.client.request("GET", path3, void 0, query);
|
|
81
114
|
}
|
|
82
|
-
post(
|
|
83
|
-
return this.client.request("POST",
|
|
115
|
+
post(path3, body) {
|
|
116
|
+
return this.client.request("POST", path3, body);
|
|
84
117
|
}
|
|
85
|
-
put(
|
|
86
|
-
return this.client.request("PUT",
|
|
118
|
+
put(path3, body) {
|
|
119
|
+
return this.client.request("PUT", path3, body);
|
|
87
120
|
}
|
|
88
|
-
patch(
|
|
89
|
-
return this.client.request("PATCH",
|
|
121
|
+
patch(path3, body) {
|
|
122
|
+
return this.client.request("PATCH", path3, body);
|
|
90
123
|
}
|
|
91
|
-
delete(
|
|
92
|
-
return this.client.request("DELETE",
|
|
124
|
+
delete(path3) {
|
|
125
|
+
return this.client.request("DELETE", path3);
|
|
93
126
|
}
|
|
94
127
|
};
|
|
95
128
|
MediaResource = class extends Resource {
|
|
@@ -180,6 +213,17 @@ var init_dist = __esm({
|
|
|
180
213
|
return this.client.request("DELETE", `/carousels/${id}`);
|
|
181
214
|
}
|
|
182
215
|
};
|
|
216
|
+
MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
217
|
+
DEFAULT_PART_SIZE = 16 * 1024 * 1024;
|
|
218
|
+
MAX_PARTS = 1e4;
|
|
219
|
+
VIDEO_MIME = {
|
|
220
|
+
mp4: "video/mp4",
|
|
221
|
+
mov: "video/quicktime",
|
|
222
|
+
webm: "video/webm",
|
|
223
|
+
mkv: "video/x-matroska",
|
|
224
|
+
avi: "video/x-msvideo",
|
|
225
|
+
m4v: "video/x-m4v"
|
|
226
|
+
};
|
|
183
227
|
ClipsResource = class extends Resource {
|
|
184
228
|
/** GET /clips — backend returns `{ clips, pagination }`, not a bare array. */
|
|
185
229
|
list() {
|
|
@@ -188,16 +232,66 @@ var init_dist = __esm({
|
|
|
188
232
|
retrieve(id) {
|
|
189
233
|
return this.client.request("GET", `/clips/${id}`);
|
|
190
234
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Upload a local video file as a clip by driving the backend multipart flow:
|
|
237
|
+
* `init → presign → PUT each part → complete`. On any failure after init, the
|
|
238
|
+
* multipart upload is aborted so it leaves no dangling clip. Returns the created clip.
|
|
239
|
+
*
|
|
240
|
+
* Node-only: reads the file in chunks (memory-bounded for multi-GB videos). `node:fs`
|
|
241
|
+
* is imported dynamically so the rest of the SDK stays runtime-agnostic.
|
|
242
|
+
*/
|
|
243
|
+
async upload(filePath, options) {
|
|
244
|
+
if (!options?.brandId) {
|
|
245
|
+
throw new AutopostingError("brandId is required to upload a clip.", 0, "VALIDATION_ERROR");
|
|
246
|
+
}
|
|
247
|
+
const { open: open2, stat } = await import("fs/promises");
|
|
248
|
+
const { basename } = await import("path");
|
|
249
|
+
const fileName = basename(filePath);
|
|
250
|
+
const title = options.title ?? fileName;
|
|
251
|
+
const mimeType = options.contentType ?? guessVideoMime(fileName);
|
|
252
|
+
const { size: fileSize } = await stat(filePath);
|
|
253
|
+
if (fileSize <= 0) {
|
|
254
|
+
throw new AutopostingError(`File is empty: ${filePath}`, 0, "VALIDATION_ERROR");
|
|
255
|
+
}
|
|
256
|
+
const partSize = resolvePartSize(fileSize, options.partSize);
|
|
257
|
+
const partNumbers = Array.from(
|
|
258
|
+
{ length: Math.max(1, Math.ceil(fileSize / partSize)) },
|
|
259
|
+
(_, i) => i + 1
|
|
200
260
|
);
|
|
261
|
+
const { clipId, uploadId, key } = await this.client.request("POST", "/clips/upload/init", { brandId: options.brandId, title, fileName, fileSize, mimeType });
|
|
262
|
+
let completed;
|
|
263
|
+
const handle = await open2(filePath, "r");
|
|
264
|
+
try {
|
|
265
|
+
const { presignedUrls } = await this.client.request("POST", "/clips/upload/presign", { clipId, uploadId, key, partNumbers });
|
|
266
|
+
const urlByPart = new Map(presignedUrls.map((p) => [p.partNumber, p.url]));
|
|
267
|
+
const parts = [];
|
|
268
|
+
let uploaded = 0;
|
|
269
|
+
for (const partNumber of partNumbers) {
|
|
270
|
+
const url = urlByPart.get(partNumber);
|
|
271
|
+
if (!url) {
|
|
272
|
+
throw new AutopostingError(`Missing presigned URL for part ${partNumber}.`, 0, "UPLOAD_FAILED");
|
|
273
|
+
}
|
|
274
|
+
const offset = (partNumber - 1) * partSize;
|
|
275
|
+
const length = Math.min(partSize, fileSize - offset);
|
|
276
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
277
|
+
await handle.read(buffer, 0, length, offset);
|
|
278
|
+
parts.push({ partNumber, etag: await putPart(url, buffer) });
|
|
279
|
+
uploaded += length;
|
|
280
|
+
options.onProgress?.(Math.round(uploaded / fileSize * 100));
|
|
281
|
+
}
|
|
282
|
+
completed = await this.client.request(
|
|
283
|
+
"POST",
|
|
284
|
+
"/clips/upload/complete",
|
|
285
|
+
{ clipId, uploadId, key, parts }
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
await this.client.request("POST", "/clips/upload/abort", { clipId, uploadId, key }).catch(() => {
|
|
289
|
+
});
|
|
290
|
+
throw err;
|
|
291
|
+
} finally {
|
|
292
|
+
await handle.close();
|
|
293
|
+
}
|
|
294
|
+
return this.retrieve(completed.clipId);
|
|
201
295
|
}
|
|
202
296
|
/** POST /clips/import-url — backend returns `{ clipId }`, not a full Clip. */
|
|
203
297
|
importUrl(params) {
|
|
@@ -425,7 +519,7 @@ var init_dist = __esm({
|
|
|
425
519
|
* failures — network errors, timeouts, and 5xx — with exponential backoff + jitter.
|
|
426
520
|
* Non-idempotent methods (POST/PATCH) are never retried, to avoid duplicate side effects.
|
|
427
521
|
*/
|
|
428
|
-
async request(method,
|
|
522
|
+
async request(method, path3, body, query) {
|
|
429
523
|
let lastErr;
|
|
430
524
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
431
525
|
if (attempt > 0) {
|
|
@@ -433,7 +527,7 @@ var init_dist = __esm({
|
|
|
433
527
|
await sleep(backoff + Math.floor(Math.random() * this.retryBaseMs));
|
|
434
528
|
}
|
|
435
529
|
try {
|
|
436
|
-
return await this._send(method,
|
|
530
|
+
return await this._send(method, path3, body, query);
|
|
437
531
|
} catch (err) {
|
|
438
532
|
lastErr = err;
|
|
439
533
|
if (attempt < this.maxRetries && this.isRetryable(method, err)) continue;
|
|
@@ -448,8 +542,8 @@ var init_dist = __esm({
|
|
|
448
542
|
if (err.code === "NETWORK_ERROR" || err.code === "TIMEOUT") return true;
|
|
449
543
|
return RETRYABLE_STATUS.has(err.status);
|
|
450
544
|
}
|
|
451
|
-
async _send(method,
|
|
452
|
-
let url = `${this.baseUrl}${
|
|
545
|
+
async _send(method, path3, body, query) {
|
|
546
|
+
let url = `${this.baseUrl}${path3}`;
|
|
453
547
|
if (query && Object.keys(query).length > 0) {
|
|
454
548
|
const params = new URLSearchParams();
|
|
455
549
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -483,13 +577,13 @@ var init_dist = __esm({
|
|
|
483
577
|
} catch (err) {
|
|
484
578
|
if (err instanceof Error && err.name === "AbortError") {
|
|
485
579
|
throw new AutopostingError(
|
|
486
|
-
`Request to ${method} ${
|
|
580
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
487
581
|
0,
|
|
488
582
|
"TIMEOUT"
|
|
489
583
|
);
|
|
490
584
|
}
|
|
491
585
|
throw new AutopostingError(
|
|
492
|
-
`Network request to ${method} ${
|
|
586
|
+
`Network request to ${method} ${path3} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
493
587
|
0,
|
|
494
588
|
"NETWORK_ERROR"
|
|
495
589
|
);
|
|
@@ -534,7 +628,7 @@ var init_dist = __esm({
|
|
|
534
628
|
parsed = JSON.parse(text);
|
|
535
629
|
} catch {
|
|
536
630
|
throw new AutopostingError(
|
|
537
|
-
`Expected a JSON response from ${method} ${
|
|
631
|
+
`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
632
|
response.status,
|
|
539
633
|
"INVALID_RESPONSE"
|
|
540
634
|
);
|
|
@@ -547,7 +641,7 @@ var init_dist = __esm({
|
|
|
547
641
|
} catch (err) {
|
|
548
642
|
if (err instanceof Error && err.name === "AbortError") {
|
|
549
643
|
throw new AutopostingError(
|
|
550
|
-
`Request to ${method} ${
|
|
644
|
+
`Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
|
|
551
645
|
0,
|
|
552
646
|
"TIMEOUT"
|
|
553
647
|
);
|
|
@@ -1675,7 +1769,7 @@ var init_server = __esm({
|
|
|
1675
1769
|
});
|
|
1676
1770
|
|
|
1677
1771
|
// src/cli.ts
|
|
1678
|
-
var
|
|
1772
|
+
var import_commander20 = require("commander");
|
|
1679
1773
|
init_dist();
|
|
1680
1774
|
|
|
1681
1775
|
// src/commands/agents.ts
|
|
@@ -1823,6 +1917,67 @@ function formatError(error, mode) {
|
|
|
1823
1917
|
if (mode === "tty") return red(`Error: ${message}`);
|
|
1824
1918
|
return JSON.stringify({ error: message }, null, 2);
|
|
1825
1919
|
}
|
|
1920
|
+
function parseJq(expr) {
|
|
1921
|
+
const e = expr.trim();
|
|
1922
|
+
if (e === "" || e === ".") return [];
|
|
1923
|
+
if (!e.startsWith(".")) {
|
|
1924
|
+
throw new Error(`--jq: expression must start with "." (got "${expr}")`);
|
|
1925
|
+
}
|
|
1926
|
+
const body = e.slice(1);
|
|
1927
|
+
const ops = [];
|
|
1928
|
+
let i = 0;
|
|
1929
|
+
while (i < body.length) {
|
|
1930
|
+
const ch = body[i];
|
|
1931
|
+
if (ch === ".") {
|
|
1932
|
+
i++;
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (ch === "[") {
|
|
1936
|
+
const close = body.indexOf("]", i);
|
|
1937
|
+
if (close === -1) throw new Error(`--jq: unclosed "[" in "${expr}"`);
|
|
1938
|
+
const inner = body.slice(i + 1, close).trim();
|
|
1939
|
+
if (inner === "") {
|
|
1940
|
+
ops.push({ type: "iter" });
|
|
1941
|
+
} else {
|
|
1942
|
+
const n = Number(inner);
|
|
1943
|
+
if (!Number.isInteger(n)) throw new Error(`--jq: invalid index "[${inner}]" in "${expr}"`);
|
|
1944
|
+
ops.push({ type: "index", n });
|
|
1945
|
+
}
|
|
1946
|
+
i = close + 1;
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
const m = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(body.slice(i));
|
|
1950
|
+
if (!m) throw new Error(`--jq: unexpected character "${ch}" in "${expr}"`);
|
|
1951
|
+
ops.push({ type: "prop", name: m[0] });
|
|
1952
|
+
i += m[0].length;
|
|
1953
|
+
}
|
|
1954
|
+
return ops;
|
|
1955
|
+
}
|
|
1956
|
+
function applyJq(data, expr) {
|
|
1957
|
+
const ops = parseJq(expr);
|
|
1958
|
+
let stream = [data];
|
|
1959
|
+
for (const op of ops) {
|
|
1960
|
+
const next = [];
|
|
1961
|
+
for (const v of stream) {
|
|
1962
|
+
if (op.type === "prop") {
|
|
1963
|
+
next.push(v == null ? null : v[op.name] ?? null);
|
|
1964
|
+
} else if (op.type === "index") {
|
|
1965
|
+
if (!Array.isArray(v)) throw new Error(`--jq: cannot index non-array with [${op.n}]`);
|
|
1966
|
+
const idx = op.n < 0 ? v.length + op.n : op.n;
|
|
1967
|
+
next.push(v[idx] ?? null);
|
|
1968
|
+
} else {
|
|
1969
|
+
if (Array.isArray(v)) next.push(...v);
|
|
1970
|
+
else if (v && typeof v === "object") next.push(...Object.values(v));
|
|
1971
|
+
else throw new Error(`--jq: cannot iterate over ${v === null ? "null" : typeof v} \u2014 .[] needs an array or object`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
stream = next;
|
|
1975
|
+
}
|
|
1976
|
+
return stream;
|
|
1977
|
+
}
|
|
1978
|
+
function formatJq(data, expr) {
|
|
1979
|
+
return applyJq(data, expr).map((r) => typeof r === "string" ? r : r === void 0 ? "null" : JSON.stringify(r)).join("\n");
|
|
1980
|
+
}
|
|
1826
1981
|
|
|
1827
1982
|
// src/output/spinner.ts
|
|
1828
1983
|
var NO_OP_SPINNER = {
|
|
@@ -1870,19 +2025,23 @@ function createSpinner(mode) {
|
|
|
1870
2025
|
|
|
1871
2026
|
// src/output/printer.ts
|
|
1872
2027
|
var Printer = class {
|
|
1873
|
-
constructor(mode) {
|
|
2028
|
+
constructor(mode, jq) {
|
|
1874
2029
|
this.mode = mode;
|
|
2030
|
+
this.jq = jq;
|
|
1875
2031
|
}
|
|
1876
2032
|
mode;
|
|
2033
|
+
jq;
|
|
1877
2034
|
/** True only for human-readable output, where extra hint lines are safe to print. */
|
|
1878
2035
|
isTty() {
|
|
1879
|
-
return this.mode === "tty";
|
|
2036
|
+
return this.mode === "tty" && !this.jq;
|
|
1880
2037
|
}
|
|
1881
2038
|
log(data) {
|
|
2039
|
+
if (this.jq) return this.emitJq(data);
|
|
1882
2040
|
const out = formatOutput(data, this.mode);
|
|
1883
2041
|
if (out !== "") console.log(out);
|
|
1884
2042
|
}
|
|
1885
2043
|
table(rows, columns) {
|
|
2044
|
+
if (this.jq) return this.emitJq(rows);
|
|
1886
2045
|
if (this.mode === "quiet") return;
|
|
1887
2046
|
if (this.mode === "json") {
|
|
1888
2047
|
console.log(formatOutput(rows, "json"));
|
|
@@ -1891,19 +2050,23 @@ var Printer = class {
|
|
|
1891
2050
|
const out = formatTable(rows, columns);
|
|
1892
2051
|
if (out !== "") console.log(out);
|
|
1893
2052
|
}
|
|
2053
|
+
emitJq(data) {
|
|
2054
|
+
const out = formatJq(data, this.jq);
|
|
2055
|
+
if (out !== "") console.log(out);
|
|
2056
|
+
}
|
|
1894
2057
|
error(err) {
|
|
1895
|
-
const out = formatError(err, this.mode);
|
|
2058
|
+
const out = formatError(err, this.jq ? "json" : this.mode);
|
|
1896
2059
|
console.error(out);
|
|
1897
2060
|
}
|
|
1898
2061
|
spinner(message) {
|
|
1899
|
-
const s = createSpinner(this.mode);
|
|
2062
|
+
const s = createSpinner(this.jq ? "json" : this.mode);
|
|
1900
2063
|
s.start(message);
|
|
1901
2064
|
return s;
|
|
1902
2065
|
}
|
|
1903
2066
|
};
|
|
1904
2067
|
function createPrinter(options) {
|
|
1905
2068
|
const mode = detectOutputMode(options);
|
|
1906
|
-
return new Printer(mode);
|
|
2069
|
+
return new Printer(mode, options.jq);
|
|
1907
2070
|
}
|
|
1908
2071
|
|
|
1909
2072
|
// src/output/exit-codes.ts
|
|
@@ -2400,6 +2563,195 @@ function createBillingCommand() {
|
|
|
2400
2563
|
// src/commands/brands.ts
|
|
2401
2564
|
var import_commander4 = require("commander");
|
|
2402
2565
|
init_dist();
|
|
2566
|
+
|
|
2567
|
+
// src/lib/media-flags.ts
|
|
2568
|
+
var import_node_fs2 = require("fs");
|
|
2569
|
+
var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
2570
|
+
var EXT_TO_MIME = {
|
|
2571
|
+
jpg: "image/jpeg",
|
|
2572
|
+
jpeg: "image/jpeg",
|
|
2573
|
+
png: "image/png",
|
|
2574
|
+
gif: "image/gif",
|
|
2575
|
+
webp: "image/webp",
|
|
2576
|
+
mp4: "video/mp4",
|
|
2577
|
+
mov: "video/quicktime",
|
|
2578
|
+
webm: "video/webm"
|
|
2579
|
+
};
|
|
2580
|
+
function extToMime(filename) {
|
|
2581
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
2582
|
+
const mime = EXT_TO_MIME[ext];
|
|
2583
|
+
if (!mime) {
|
|
2584
|
+
throw new Error(
|
|
2585
|
+
`Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
return mime;
|
|
2589
|
+
}
|
|
2590
|
+
function parsePairs(flag, vals) {
|
|
2591
|
+
const result = {};
|
|
2592
|
+
for (const val of vals) {
|
|
2593
|
+
const eqIdx = val.indexOf("=");
|
|
2594
|
+
if (eqIdx < 1) {
|
|
2595
|
+
throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
|
|
2596
|
+
}
|
|
2597
|
+
const p = val.slice(0, eqIdx).trim();
|
|
2598
|
+
const v = val.slice(eqIdx + 1);
|
|
2599
|
+
if (!VALID_PLATFORMS.includes(p)) {
|
|
2600
|
+
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2601
|
+
}
|
|
2602
|
+
result[p] = v;
|
|
2603
|
+
}
|
|
2604
|
+
return result;
|
|
2605
|
+
}
|
|
2606
|
+
function parsePlatformMediaPairs(flag, vals) {
|
|
2607
|
+
const result = {};
|
|
2608
|
+
for (const val of vals) {
|
|
2609
|
+
const eqIdx = val.indexOf("=");
|
|
2610
|
+
if (eqIdx < 1) {
|
|
2611
|
+
throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
|
|
2612
|
+
}
|
|
2613
|
+
const p = val.slice(0, eqIdx).trim();
|
|
2614
|
+
const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2615
|
+
if (!VALID_PLATFORMS.includes(p)) {
|
|
2616
|
+
throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
|
|
2617
|
+
}
|
|
2618
|
+
result[p] = paths;
|
|
2619
|
+
}
|
|
2620
|
+
return result;
|
|
2621
|
+
}
|
|
2622
|
+
function validateMediaExtensions(paths) {
|
|
2623
|
+
for (const p of paths) extToMime(p);
|
|
2624
|
+
}
|
|
2625
|
+
function validateMediaCount(paths) {
|
|
2626
|
+
if (paths.length > 10) {
|
|
2627
|
+
throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function validateMediaPaths(paths) {
|
|
2631
|
+
for (const p of paths) {
|
|
2632
|
+
if (!(0, import_node_fs2.existsSync)(p)) {
|
|
2633
|
+
throw new Error(`Media file not found: "${p}"`);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
function alignAltText(mediaPaths, altTexts) {
|
|
2638
|
+
if (altTexts.length > mediaPaths.length) {
|
|
2639
|
+
throw new Error(
|
|
2640
|
+
`--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
return mediaPaths.map((_, i) => altTexts[i]);
|
|
2644
|
+
}
|
|
2645
|
+
function buildYoutubeOptions(opts) {
|
|
2646
|
+
const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
|
|
2647
|
+
const o = {};
|
|
2648
|
+
if (ytTitle) o.title = ytTitle;
|
|
2649
|
+
if (ytDescription) o.description = ytDescription;
|
|
2650
|
+
if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2651
|
+
if (ytPrivacy) o.privacyStatus = ytPrivacy;
|
|
2652
|
+
if (ytCategory) o.categoryId = ytCategory;
|
|
2653
|
+
if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
|
|
2654
|
+
return Object.keys(o).length > 0 ? o : void 0;
|
|
2655
|
+
}
|
|
2656
|
+
function buildInstagramOptions(opts) {
|
|
2657
|
+
const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
|
|
2658
|
+
const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
|
|
2659
|
+
if (!hasAnyReelFlag) return void 0;
|
|
2660
|
+
const reel = {};
|
|
2661
|
+
if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
|
|
2662
|
+
if (igCoverUrl) reel.coverUrl = igCoverUrl;
|
|
2663
|
+
if (igThumbOffsetMs !== void 0) {
|
|
2664
|
+
const n = Number(igThumbOffsetMs);
|
|
2665
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
2666
|
+
throw new Error(
|
|
2667
|
+
`--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
reel.thumbOffsetMs = n;
|
|
2671
|
+
}
|
|
2672
|
+
if (igCollaborators) {
|
|
2673
|
+
reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2674
|
+
}
|
|
2675
|
+
return { reel };
|
|
2676
|
+
}
|
|
2677
|
+
function buildThreadsOptions(opts) {
|
|
2678
|
+
const { threadsReplyTo, threadsReplyControl } = opts;
|
|
2679
|
+
if (!threadsReplyTo && !threadsReplyControl) return void 0;
|
|
2680
|
+
const o = {};
|
|
2681
|
+
if (threadsReplyTo) o.replyToId = threadsReplyTo;
|
|
2682
|
+
if (threadsReplyControl) o.replyControl = threadsReplyControl;
|
|
2683
|
+
return o;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/auth/config-store.ts
|
|
2687
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
2688
|
+
var import_node_os2 = __toESM(require("os"));
|
|
2689
|
+
var import_node_path2 = __toESM(require("path"));
|
|
2690
|
+
function getConfigPath() {
|
|
2691
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2692
|
+
const base = xdg && import_node_path2.default.isAbsolute(xdg) ? xdg : import_node_path2.default.join(import_node_os2.default.homedir(), ".config");
|
|
2693
|
+
return import_node_path2.default.join(base, "autoposting", "config.json");
|
|
2694
|
+
}
|
|
2695
|
+
function readConfig() {
|
|
2696
|
+
try {
|
|
2697
|
+
return JSON.parse(import_node_fs3.default.readFileSync(getConfigPath(), "utf8"));
|
|
2698
|
+
} catch {
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function writeConfig(cfg) {
|
|
2703
|
+
const filePath = getConfigPath();
|
|
2704
|
+
import_node_fs3.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true, mode: 448 });
|
|
2705
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
2706
|
+
try {
|
|
2707
|
+
import_node_fs3.default.writeFileSync(tmp, JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
2708
|
+
import_node_fs3.default.renameSync(tmp, filePath);
|
|
2709
|
+
} catch (err) {
|
|
2710
|
+
try {
|
|
2711
|
+
import_node_fs3.default.unlinkSync(tmp);
|
|
2712
|
+
} catch {
|
|
2713
|
+
}
|
|
2714
|
+
throw err;
|
|
2715
|
+
}
|
|
2716
|
+
import_node_fs3.default.chmodSync(filePath, 384);
|
|
2717
|
+
}
|
|
2718
|
+
function getContextBrand() {
|
|
2719
|
+
return readConfig()?.context?.brand ?? null;
|
|
2720
|
+
}
|
|
2721
|
+
function setContextBrand(brand) {
|
|
2722
|
+
const cfg = readConfig() ?? {};
|
|
2723
|
+
cfg.context = { ...cfg.context, brand };
|
|
2724
|
+
writeConfig(cfg);
|
|
2725
|
+
}
|
|
2726
|
+
function unsetContextBrand() {
|
|
2727
|
+
const cfg = readConfig();
|
|
2728
|
+
if (!cfg?.context?.brand) return;
|
|
2729
|
+
delete cfg.context.brand;
|
|
2730
|
+
writeConfig(cfg);
|
|
2731
|
+
}
|
|
2732
|
+
function resolveBrand(optBrand) {
|
|
2733
|
+
return optBrand ?? getContextBrand();
|
|
2734
|
+
}
|
|
2735
|
+
function getDefaultAccount(brand, platform) {
|
|
2736
|
+
return readConfig()?.defaultAccounts?.[brand]?.[platform] ?? null;
|
|
2737
|
+
}
|
|
2738
|
+
function getDefaultAccounts(brand) {
|
|
2739
|
+
return readConfig()?.defaultAccounts?.[brand] ?? {};
|
|
2740
|
+
}
|
|
2741
|
+
function setDefaultAccount(brand, platform, value) {
|
|
2742
|
+
const cfg = readConfig() ?? {};
|
|
2743
|
+
cfg.defaultAccounts ??= {};
|
|
2744
|
+
cfg.defaultAccounts[brand] = { ...cfg.defaultAccounts[brand], [platform]: value };
|
|
2745
|
+
writeConfig(cfg);
|
|
2746
|
+
}
|
|
2747
|
+
function clearDefaultAccounts(brand) {
|
|
2748
|
+
const cfg = readConfig();
|
|
2749
|
+
if (!cfg?.defaultAccounts?.[brand]) return;
|
|
2750
|
+
delete cfg.defaultAccounts[brand];
|
|
2751
|
+
writeConfig(cfg);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// src/commands/brands.ts
|
|
2403
2755
|
function tokenStatus(c, now = Date.now()) {
|
|
2404
2756
|
if (!c.connected) return "\u2014";
|
|
2405
2757
|
if (c.refreshError) return "expired";
|
|
@@ -2533,6 +2885,28 @@ function createBrandsCommand() {
|
|
|
2533
2885
|
process.exit(exitCodeFromError(err));
|
|
2534
2886
|
}
|
|
2535
2887
|
});
|
|
2888
|
+
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) => {
|
|
2889
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2890
|
+
try {
|
|
2891
|
+
const parsed = parsePairs("--account", pairs);
|
|
2892
|
+
for (const [platform, value] of Object.entries(parsed)) {
|
|
2893
|
+
setDefaultAccount(slug, platform, value);
|
|
2894
|
+
}
|
|
2895
|
+
printer.log(getDefaultAccounts(slug));
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
printer.error(err);
|
|
2898
|
+
process.exit(1);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
brands.command("get-default-account <slug>").description("Show saved default account(s) for a brand").action((slug, _opts, cmd) => {
|
|
2902
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2903
|
+
printer.log(getDefaultAccounts(slug));
|
|
2904
|
+
});
|
|
2905
|
+
brands.command("clear-default-account <slug>").description("Remove all saved default accounts for a brand").action((slug, _opts, cmd) => {
|
|
2906
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
2907
|
+
clearDefaultAccounts(slug);
|
|
2908
|
+
printer.log(getDefaultAccounts(slug));
|
|
2909
|
+
});
|
|
2536
2910
|
return brands;
|
|
2537
2911
|
}
|
|
2538
2912
|
|
|
@@ -2670,6 +3044,17 @@ function resolveExitCode4(err) {
|
|
|
2670
3044
|
if (typeof attached === "number") return attached;
|
|
2671
3045
|
return exitCodeFromError(err);
|
|
2672
3046
|
}
|
|
3047
|
+
async function resolveBrandId(client, explicit) {
|
|
3048
|
+
if (explicit) return explicit;
|
|
3049
|
+
const brands = await client.brands.list();
|
|
3050
|
+
if (brands.length === 1) return brands[0].id;
|
|
3051
|
+
if (brands.length === 0) {
|
|
3052
|
+
throw new Error("No brands found. Create a brand first, then pass --brand <id>.");
|
|
3053
|
+
}
|
|
3054
|
+
throw new Error(
|
|
3055
|
+
`Multiple brands found \u2014 pass --brand <id>. Available: ${brands.map((b) => `${b.name} (${b.id})`).join(", ")}`
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
2673
3058
|
function createClipsCommand() {
|
|
2674
3059
|
const clips = new import_commander6.Command("clips").description("Manage video clips");
|
|
2675
3060
|
clips.command("list").description("List all clips").action(async (_opts, cmd) => {
|
|
@@ -2710,15 +3095,17 @@ function createClipsCommand() {
|
|
|
2710
3095
|
process.exit(resolveExitCode4(err));
|
|
2711
3096
|
}
|
|
2712
3097
|
});
|
|
2713
|
-
clips.command("upload <file>").description("Upload a video file as a clip").option("--name <name>", "Override the clip
|
|
3098
|
+
clips.command("upload <file>").description("Upload a video file as a clip").option("--brand <id>", "Brand to attach the clip to (defaults to your only brand)").option("--name <name>", "Override the clip title (defaults to filename)").action(async (file, opts, cmd) => {
|
|
2714
3099
|
const globals = cmd.optsWithGlobals();
|
|
2715
3100
|
const printer = createPrinter(globals);
|
|
2716
3101
|
const spinner = printer.spinner("Uploading clip\u2026");
|
|
2717
3102
|
try {
|
|
2718
3103
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
2719
3104
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
3105
|
+
const brandId = await resolveBrandId(client, opts.brand);
|
|
2720
3106
|
const clip = await client.clips.upload(file, {
|
|
2721
|
-
|
|
3107
|
+
brandId,
|
|
3108
|
+
...opts.name ? { title: opts.name } : {},
|
|
2722
3109
|
onProgress: (pct) => {
|
|
2723
3110
|
if (pct > 0 && pct < 100) spinner.stop();
|
|
2724
3111
|
}
|
|
@@ -2792,136 +3179,27 @@ function createClipsCommand() {
|
|
|
2792
3179
|
// src/commands/posts.ts
|
|
2793
3180
|
var import_commander7 = require("commander");
|
|
2794
3181
|
init_dist();
|
|
2795
|
-
var import_promises = __toESM(require("fs/promises"));
|
|
2796
|
-
var import_node_path2 = __toESM(require("path"));
|
|
2797
3182
|
|
|
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
|
-
}
|
|
3183
|
+
// src/lib/post-create.ts
|
|
3184
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
3185
|
+
var import_node_path3 = __toESM(require("path"));
|
|
2916
3186
|
|
|
2917
3187
|
// src/lib/account-select.ts
|
|
2918
3188
|
var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
3189
|
+
var FANOUT_CONFIRM_THRESHOLD = 5;
|
|
3190
|
+
var PAGED_PICKER_THRESHOLD = 8;
|
|
3191
|
+
var isAll = (v) => v === "all" || v === "*";
|
|
3192
|
+
function needsFanoutConfirm(count, isTty, explicit) {
|
|
3193
|
+
return isTty && explicit && count > FANOUT_CONFIRM_THRESHOLD;
|
|
3194
|
+
}
|
|
2919
3195
|
async function resolveTargetAccounts({
|
|
2920
3196
|
brandSlug,
|
|
2921
3197
|
platforms,
|
|
2922
3198
|
accountFlags,
|
|
2923
3199
|
client,
|
|
2924
|
-
isTty
|
|
3200
|
+
isTty,
|
|
3201
|
+
emit = (m) => process.stderr.write(`${m}
|
|
3202
|
+
`)
|
|
2925
3203
|
}) {
|
|
2926
3204
|
const accountMap = {};
|
|
2927
3205
|
for (const flag of accountFlags) {
|
|
@@ -2952,8 +3230,24 @@ async function resolveTargetAccounts({
|
|
|
2952
3230
|
const result = {};
|
|
2953
3231
|
for (const platform of platforms) {
|
|
2954
3232
|
const accounts = byPlatform[platform] ?? [];
|
|
2955
|
-
const
|
|
2956
|
-
|
|
3233
|
+
const flagValue = accountMap[platform];
|
|
3234
|
+
const explicit = flagValue !== void 0;
|
|
3235
|
+
const specifiedValue = flagValue ?? getDefaultAccount(brandSlug, platform) ?? void 0;
|
|
3236
|
+
if (specifiedValue !== void 0 && isAll(specifiedValue)) {
|
|
3237
|
+
const ids = accounts.map((a) => a.platformUserId).filter((id) => Boolean(id));
|
|
3238
|
+
emit(`${platform}: posting to all ${ids.length} connected account(s).`);
|
|
3239
|
+
if (needsFanoutConfirm(ids.length, isTty, explicit)) {
|
|
3240
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
3241
|
+
const ok2 = await confirm({
|
|
3242
|
+
message: `Post to all ${ids.length} ${platform} accounts?`,
|
|
3243
|
+
default: false
|
|
3244
|
+
});
|
|
3245
|
+
if (!ok2) {
|
|
3246
|
+
throw Object.assign(new Error("Aborted: fan-out not confirmed."), { exitCode: 1 });
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (ids.length > 0) result[platform] = ids;
|
|
3250
|
+
} else if (specifiedValue !== void 0) {
|
|
2957
3251
|
const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
|
|
2958
3252
|
const match = accounts.find(
|
|
2959
3253
|
(a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
|
|
@@ -2980,7 +3274,12 @@ ${valid || " (none)"}`
|
|
|
2980
3274
|
}));
|
|
2981
3275
|
const selected = await checkbox({
|
|
2982
3276
|
message: `Multiple ${platform} accounts connected \u2014 select which to post to:`,
|
|
2983
|
-
choices
|
|
3277
|
+
choices,
|
|
3278
|
+
// ponytail: paged scroll for long lists (23 LinkedIn orgs). @inquirer's
|
|
3279
|
+
// checkbox has no type-to-filter; if many-account users need it, swap in
|
|
3280
|
+
// a search-capable multiselect dep — or save a default to skip the picker.
|
|
3281
|
+
pageSize: accounts.length >= PAGED_PICKER_THRESHOLD ? 12 : 7,
|
|
3282
|
+
loop: false
|
|
2984
3283
|
});
|
|
2985
3284
|
const valid = selected.filter(Boolean);
|
|
2986
3285
|
if (valid.length > 0) result[platform] = valid;
|
|
@@ -3001,7 +3300,7 @@ Pass --account ${platform}=<handle|id> to select.`
|
|
|
3001
3300
|
return result;
|
|
3002
3301
|
}
|
|
3003
3302
|
|
|
3004
|
-
// src/
|
|
3303
|
+
// src/lib/post-create.ts
|
|
3005
3304
|
var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
|
|
3006
3305
|
function parsePlatforms(raw) {
|
|
3007
3306
|
const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
@@ -3016,13 +3315,6 @@ function parsePlatforms(raw) {
|
|
|
3016
3315
|
}
|
|
3017
3316
|
return parts;
|
|
3018
3317
|
}
|
|
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
3318
|
function validateScheduledAt(value) {
|
|
3027
3319
|
const ms = Date.parse(value);
|
|
3028
3320
|
if (Number.isNaN(ms) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
|
|
@@ -3037,6 +3329,228 @@ function validateScheduledAt(value) {
|
|
|
3037
3329
|
}
|
|
3038
3330
|
return value;
|
|
3039
3331
|
}
|
|
3332
|
+
async function buildAndCreatePost(client, fields, opts) {
|
|
3333
|
+
const platforms = parsePlatforms(fields.platforms);
|
|
3334
|
+
const scheduledAt = fields.at ? validateScheduledAt(fields.at) : void 0;
|
|
3335
|
+
if (fields.media && fields.media.length > 0) {
|
|
3336
|
+
validateMediaCount(fields.media);
|
|
3337
|
+
validateMediaPaths(fields.media);
|
|
3338
|
+
validateMediaExtensions(fields.media);
|
|
3339
|
+
}
|
|
3340
|
+
const platformTexts = fields.platformText && fields.platformText.length > 0 ? parsePairs("--platform-text", fields.platformText) : void 0;
|
|
3341
|
+
const platformMediaPaths = fields.platformMedia && fields.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", fields.platformMedia) : {};
|
|
3342
|
+
for (const paths of Object.values(platformMediaPaths)) {
|
|
3343
|
+
validateMediaPaths(paths);
|
|
3344
|
+
validateMediaExtensions(paths);
|
|
3345
|
+
}
|
|
3346
|
+
const altTexts = alignAltText(fields.media ?? [], fields.altText ?? []);
|
|
3347
|
+
const youtubeOptions = buildYoutubeOptions({
|
|
3348
|
+
ytTitle: fields.ytTitle,
|
|
3349
|
+
ytDescription: fields.ytDescription,
|
|
3350
|
+
ytTags: fields.ytTags,
|
|
3351
|
+
ytPrivacy: fields.ytPrivacy,
|
|
3352
|
+
ytCategory: fields.ytCategory,
|
|
3353
|
+
ytMadeForKids: fields.ytMadeForKids
|
|
3354
|
+
});
|
|
3355
|
+
const instagramOptions = buildInstagramOptions({
|
|
3356
|
+
igReel: fields.igReel,
|
|
3357
|
+
igShareToFeed: fields.igShareToFeed,
|
|
3358
|
+
igCoverUrl: fields.igCoverUrl,
|
|
3359
|
+
igThumbOffsetMs: fields.igThumbOffsetMs,
|
|
3360
|
+
igCollaborators: fields.igCollaborators
|
|
3361
|
+
});
|
|
3362
|
+
const threadsOptions = buildThreadsOptions({
|
|
3363
|
+
threadsReplyTo: fields.threadsReplyTo,
|
|
3364
|
+
threadsReplyControl: fields.threadsReplyControl
|
|
3365
|
+
});
|
|
3366
|
+
const targetAccountIds = await resolveTargetAccounts({
|
|
3367
|
+
brandSlug: fields.brandSlug,
|
|
3368
|
+
platforms,
|
|
3369
|
+
accountFlags: fields.account ?? [],
|
|
3370
|
+
client,
|
|
3371
|
+
isTty: opts.isTty,
|
|
3372
|
+
emit: opts.emit
|
|
3373
|
+
});
|
|
3374
|
+
const common = {
|
|
3375
|
+
brandSlug: fields.brandSlug,
|
|
3376
|
+
text: fields.text,
|
|
3377
|
+
platforms,
|
|
3378
|
+
...scheduledAt ? { scheduledAt } : {},
|
|
3379
|
+
...fields.thread && fields.thread.length > 0 ? { thread: fields.thread } : {},
|
|
3380
|
+
...platformTexts && Object.keys(platformTexts).length > 0 ? { platformTexts } : {},
|
|
3381
|
+
...Object.keys(targetAccountIds).length > 0 ? { targetAccountIds } : {},
|
|
3382
|
+
...instagramOptions ? { instagramOptions } : {},
|
|
3383
|
+
...threadsOptions ? { threadsOptions } : {},
|
|
3384
|
+
...youtubeOptions ? { youtubeOptions } : {},
|
|
3385
|
+
source: "cli"
|
|
3386
|
+
};
|
|
3387
|
+
if (opts.dryRun) {
|
|
3388
|
+
return {
|
|
3389
|
+
dryRun: true,
|
|
3390
|
+
request: {
|
|
3391
|
+
...common,
|
|
3392
|
+
...fields.media && fields.media.length > 0 ? {
|
|
3393
|
+
media: fields.media.map((path3, i) => ({
|
|
3394
|
+
path: path3,
|
|
3395
|
+
...altTexts[i] ? { altText: altTexts[i] } : {}
|
|
3396
|
+
}))
|
|
3397
|
+
} : {},
|
|
3398
|
+
...Object.keys(platformMediaPaths).length > 0 ? { platformMedia: platformMediaPaths } : {}
|
|
3399
|
+
}
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
opts.onBeforeNetwork?.();
|
|
3403
|
+
const mediaInputs = [];
|
|
3404
|
+
for (let i = 0; i < (fields.media ?? []).length; i++) {
|
|
3405
|
+
const filePath = fields.media[i];
|
|
3406
|
+
const data = await import_promises.default.readFile(filePath);
|
|
3407
|
+
const filename = import_node_path3.default.basename(filePath);
|
|
3408
|
+
const uploaded = await client.media.upload({
|
|
3409
|
+
data: new Uint8Array(data),
|
|
3410
|
+
filename,
|
|
3411
|
+
contentType: extToMime(filename)
|
|
3412
|
+
});
|
|
3413
|
+
mediaInputs.push({
|
|
3414
|
+
url: uploaded.url,
|
|
3415
|
+
type: uploaded.type,
|
|
3416
|
+
...altTexts[i] ? { altText: altTexts[i] } : {}
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
const platformMediaResult = {};
|
|
3420
|
+
for (const [p, paths] of Object.entries(platformMediaPaths)) {
|
|
3421
|
+
const uploads = [];
|
|
3422
|
+
for (const filePath of paths) {
|
|
3423
|
+
const data = await import_promises.default.readFile(filePath);
|
|
3424
|
+
const filename = import_node_path3.default.basename(filePath);
|
|
3425
|
+
const uploaded = await client.media.upload({
|
|
3426
|
+
data: new Uint8Array(data),
|
|
3427
|
+
filename,
|
|
3428
|
+
contentType: extToMime(filename)
|
|
3429
|
+
});
|
|
3430
|
+
uploads.push({ url: uploaded.url, type: uploaded.type });
|
|
3431
|
+
}
|
|
3432
|
+
platformMediaResult[p] = uploads;
|
|
3433
|
+
}
|
|
3434
|
+
return client.posts.create({
|
|
3435
|
+
...common,
|
|
3436
|
+
...mediaInputs.length > 0 ? { media: mediaInputs } : {},
|
|
3437
|
+
...Object.keys(platformMediaResult).length > 0 ? { platformMedia: platformMediaResult } : {}
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// src/lib/post-bulk.ts
|
|
3442
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
3443
|
+
function parseBulkFile(filePath) {
|
|
3444
|
+
const raw = import_node_fs4.default.readFileSync(filePath, "utf8");
|
|
3445
|
+
const isJson = filePath.toLowerCase().endsWith(".json") || raw.trimStart().startsWith("[");
|
|
3446
|
+
const rows = isJson ? parseJsonRows(raw) : parseCsvRows(raw);
|
|
3447
|
+
if (rows.length === 0) {
|
|
3448
|
+
throw new Error(`No records found in ${filePath}.`);
|
|
3449
|
+
}
|
|
3450
|
+
return rows;
|
|
3451
|
+
}
|
|
3452
|
+
function parseJsonRows(raw) {
|
|
3453
|
+
let data;
|
|
3454
|
+
try {
|
|
3455
|
+
data = JSON.parse(raw);
|
|
3456
|
+
} catch (e) {
|
|
3457
|
+
throw new Error(`Invalid JSON bulk input: ${e.message}`);
|
|
3458
|
+
}
|
|
3459
|
+
if (!Array.isArray(data)) {
|
|
3460
|
+
throw new Error("JSON bulk input must be an array of post objects.");
|
|
3461
|
+
}
|
|
3462
|
+
return data;
|
|
3463
|
+
}
|
|
3464
|
+
function parseCsvRows(raw) {
|
|
3465
|
+
const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
3466
|
+
if (lines.length === 0) return [];
|
|
3467
|
+
const headers = splitCsvLine(lines[0]);
|
|
3468
|
+
return lines.slice(1).map((line) => {
|
|
3469
|
+
const cells = splitCsvLine(line);
|
|
3470
|
+
const row = {};
|
|
3471
|
+
headers.forEach((h, i) => {
|
|
3472
|
+
if (cells[i] !== void 0 && cells[i] !== "") row[h] = cells[i];
|
|
3473
|
+
});
|
|
3474
|
+
return row;
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
function splitCsvLine(line) {
|
|
3478
|
+
const out = [];
|
|
3479
|
+
let cur = "";
|
|
3480
|
+
let inQuotes = false;
|
|
3481
|
+
for (let i = 0; i < line.length; i++) {
|
|
3482
|
+
const c = line[i];
|
|
3483
|
+
if (inQuotes) {
|
|
3484
|
+
if (c === '"' && line[i + 1] === '"') {
|
|
3485
|
+
cur += '"';
|
|
3486
|
+
i++;
|
|
3487
|
+
} else if (c === '"') {
|
|
3488
|
+
inQuotes = false;
|
|
3489
|
+
} else {
|
|
3490
|
+
cur += c;
|
|
3491
|
+
}
|
|
3492
|
+
} else if (c === '"') {
|
|
3493
|
+
inQuotes = true;
|
|
3494
|
+
} else if (c === ",") {
|
|
3495
|
+
out.push(cur);
|
|
3496
|
+
cur = "";
|
|
3497
|
+
} else {
|
|
3498
|
+
cur += c;
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
out.push(cur);
|
|
3502
|
+
return out.map((s) => s.trim());
|
|
3503
|
+
}
|
|
3504
|
+
function rowToFields(row, cliBrand) {
|
|
3505
|
+
const platformsRaw = row.platforms;
|
|
3506
|
+
const platforms = Array.isArray(platformsRaw) ? platformsRaw.join(",") : platformsRaw;
|
|
3507
|
+
const brand = resolveBrand(row.brand ?? row.brandSlug ?? cliBrand);
|
|
3508
|
+
if (!brand) {
|
|
3509
|
+
throw new Error('No brand: set "brand" in the row, pass --brand, or set a default context.');
|
|
3510
|
+
}
|
|
3511
|
+
if (!row.text) throw new Error('Missing "text".');
|
|
3512
|
+
if (!platforms) throw new Error('Missing "platforms".');
|
|
3513
|
+
for (const key of ["media", "thread", "account", "altText"]) {
|
|
3514
|
+
if (row[key] !== void 0 && !Array.isArray(row[key])) {
|
|
3515
|
+
throw new Error(`"${key}" is multi-value \u2014 use a JSON input file with an array (CSV can't express it).`);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
return {
|
|
3519
|
+
...row,
|
|
3520
|
+
brandSlug: brand,
|
|
3521
|
+
text: String(row.text),
|
|
3522
|
+
platforms: String(platforms)
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
async function createPostsBulk(client, rows, opts = {}) {
|
|
3526
|
+
const results = [];
|
|
3527
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3528
|
+
const row = rows[i];
|
|
3529
|
+
try {
|
|
3530
|
+
const fields = rowToFields(row, opts.cliBrand);
|
|
3531
|
+
const created = await buildAndCreatePost(client, fields, { isTty: false });
|
|
3532
|
+
results.push({ index: i, status: "created", id: created?.id ?? "", text: fields.text, error: "" });
|
|
3533
|
+
} catch (err) {
|
|
3534
|
+
results.push({
|
|
3535
|
+
index: i,
|
|
3536
|
+
status: "failed",
|
|
3537
|
+
id: "",
|
|
3538
|
+
text: String(row.text ?? ""),
|
|
3539
|
+
error: err.message
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
return results;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// src/commands/posts.ts
|
|
3547
|
+
function parsePositiveInt(value, flag) {
|
|
3548
|
+
const n = Number(value);
|
|
3549
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
3550
|
+
throw new Error(`${flag} must be a positive integer (received "${value}").`);
|
|
3551
|
+
}
|
|
3552
|
+
return n;
|
|
3553
|
+
}
|
|
3040
3554
|
function resolveExitCode5(err) {
|
|
3041
3555
|
const attached = err.exitCode;
|
|
3042
3556
|
if (typeof attached === "number") return attached;
|
|
@@ -3044,7 +3558,7 @@ function resolveExitCode5(err) {
|
|
|
3044
3558
|
}
|
|
3045
3559
|
function createPostsCommand() {
|
|
3046
3560
|
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) => {
|
|
3561
|
+
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
3562
|
const globals = cmd.optsWithGlobals();
|
|
3049
3563
|
const printer = createPrinter(globals);
|
|
3050
3564
|
const spinner = printer.spinner("Fetching posts\u2026");
|
|
@@ -3052,7 +3566,7 @@ function createPostsCommand() {
|
|
|
3052
3566
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
3053
3567
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
3054
3568
|
const result = await client.posts.list({
|
|
3055
|
-
brandSlug: opts.brand,
|
|
3569
|
+
brandSlug: resolveBrand(opts.brand) ?? void 0,
|
|
3056
3570
|
status: opts.status,
|
|
3057
3571
|
limit: parsePositiveInt(opts.limit, "--limit"),
|
|
3058
3572
|
page: parsePositiveInt(opts.page, "--page")
|
|
@@ -3089,10 +3603,10 @@ function createPostsCommand() {
|
|
|
3089
3603
|
process.exit(resolveExitCode5(err));
|
|
3090
3604
|
}
|
|
3091
3605
|
});
|
|
3092
|
-
posts.command("create").description("Create a new post").
|
|
3606
|
+
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
3607
|
"--thread <text...>",
|
|
3094
3608
|
"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(
|
|
3609
|
+
).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
3610
|
async (opts, cmd) => {
|
|
3097
3611
|
const globals = cmd.optsWithGlobals();
|
|
3098
3612
|
const printer = createPrinter(globals);
|
|
@@ -3100,99 +3614,57 @@ function createPostsCommand() {
|
|
|
3100
3614
|
try {
|
|
3101
3615
|
const cred = resolveAuth({ apiKey: globals.apiKey });
|
|
3102
3616
|
const client = new Autoposting({ apiKey: cred.apiKey });
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3617
|
+
if (opts.from) {
|
|
3618
|
+
const rows = parseBulkFile(opts.from);
|
|
3619
|
+
const records = await createPostsBulk(client, rows, {
|
|
3620
|
+
cliBrand: resolveBrand(opts.brand) ?? void 0
|
|
3621
|
+
});
|
|
3622
|
+
const truncate = printer.isTty();
|
|
3623
|
+
printer.table(
|
|
3624
|
+
records.map((r) => ({
|
|
3625
|
+
"#": r.index + 1,
|
|
3626
|
+
status: r.status,
|
|
3627
|
+
id: r.id || "\u2014",
|
|
3628
|
+
text: truncate && r.text.length > 40 ? `${r.text.slice(0, 37)}\u2026` : r.text,
|
|
3629
|
+
error: r.error
|
|
3630
|
+
})),
|
|
3631
|
+
["#", "status", "id", "text", "error"]
|
|
3632
|
+
);
|
|
3633
|
+
if (records.some((r) => r.status === "failed")) {
|
|
3634
|
+
process.exit(1);
|
|
3635
|
+
}
|
|
3636
|
+
return;
|
|
3109
3637
|
}
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3638
|
+
const brand = resolveBrand(opts.brand);
|
|
3639
|
+
if (!brand) {
|
|
3640
|
+
throw new Error(
|
|
3641
|
+
"No brand specified. Pass --brand <slug> or set a default with: ap config set-context --brand <slug>"
|
|
3642
|
+
);
|
|
3115
3643
|
}
|
|
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
|
-
});
|
|
3644
|
+
if (!opts.text) {
|
|
3645
|
+
throw new Error("--text is required (or use --from <file> to create posts in bulk).");
|
|
3162
3646
|
}
|
|
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;
|
|
3647
|
+
if (!opts.platforms) {
|
|
3648
|
+
throw new Error("--platforms is required (or use --from <file> to create posts in bulk).");
|
|
3178
3649
|
}
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
text: opts.text,
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3650
|
+
const result = await buildAndCreatePost(
|
|
3651
|
+
client,
|
|
3652
|
+
{ ...opts, brandSlug: brand, text: opts.text, platforms: opts.platforms },
|
|
3653
|
+
{
|
|
3654
|
+
// Picker reads stdin — gate on a real interactive terminal, not the
|
|
3655
|
+
// output mode (printer.isTty() is true even when stdin is piped).
|
|
3656
|
+
isTty: Boolean(process.stdin.isTTY),
|
|
3657
|
+
// --dry-run/--preview: resolve + validate, then return the request body
|
|
3658
|
+
// without uploading media or POSTing (onBeforeNetwork never fires).
|
|
3659
|
+
dryRun: Boolean(opts.dryRun || opts.preview),
|
|
3660
|
+
// Picker done — start the spinner now (covers uploads + create).
|
|
3661
|
+
onBeforeNetwork: () => {
|
|
3662
|
+
spinner = printer.spinner("Creating post\u2026");
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
);
|
|
3666
|
+
spinner?.stop();
|
|
3667
|
+
printer.log(result);
|
|
3196
3668
|
} catch (err) {
|
|
3197
3669
|
spinner?.fail();
|
|
3198
3670
|
printer.error(err);
|
|
@@ -3239,7 +3711,7 @@ function createPostsCommand() {
|
|
|
3239
3711
|
} catch (err) {
|
|
3240
3712
|
spinner.fail();
|
|
3241
3713
|
printer.error(err);
|
|
3242
|
-
if (!(err instanceof NotFoundError)
|
|
3714
|
+
if (!(err instanceof NotFoundError)) {
|
|
3243
3715
|
printer.error(`The post may not have been deleted \u2014 verify with: ap posts get "${id}"`);
|
|
3244
3716
|
}
|
|
3245
3717
|
process.exit(resolveExitCode5(err));
|
|
@@ -3842,7 +4314,7 @@ function createWorkspacesCommand() {
|
|
|
3842
4314
|
// src/commands/doctor.ts
|
|
3843
4315
|
var import_commander13 = require("commander");
|
|
3844
4316
|
init_dist();
|
|
3845
|
-
var
|
|
4317
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
3846
4318
|
var API_HEALTH_URL = "https://app.autoposting.ai/api-proxy/health";
|
|
3847
4319
|
async function runChecks() {
|
|
3848
4320
|
const results = [];
|
|
@@ -3862,7 +4334,7 @@ async function runChecks() {
|
|
|
3862
4334
|
}
|
|
3863
4335
|
const credPath = getCredentialsPath();
|
|
3864
4336
|
try {
|
|
3865
|
-
const stat =
|
|
4337
|
+
const stat = import_node_fs5.default.statSync(credPath);
|
|
3866
4338
|
const mode = stat.mode & 511;
|
|
3867
4339
|
const isSecure = mode === 384;
|
|
3868
4340
|
results.push({
|
|
@@ -4142,8 +4614,34 @@ function createMcpCommand() {
|
|
|
4142
4614
|
});
|
|
4143
4615
|
}
|
|
4144
4616
|
|
|
4617
|
+
// src/commands/config.ts
|
|
4618
|
+
var import_commander19 = require("commander");
|
|
4619
|
+
function createConfigCommand() {
|
|
4620
|
+
const config = new import_commander19.Command("config").description("Manage CLI defaults (stored in config.json)");
|
|
4621
|
+
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) => {
|
|
4622
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4623
|
+
setContextBrand(opts.brand);
|
|
4624
|
+
printer.log({ brand: opts.brand });
|
|
4625
|
+
});
|
|
4626
|
+
config.command("get-context").description("Show the default brand context").action((_opts, cmd) => {
|
|
4627
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4628
|
+
const brand = getContextBrand();
|
|
4629
|
+
if (!brand) {
|
|
4630
|
+
printer.error("No brand context set. Set one with: ap config set-context --brand <slug>");
|
|
4631
|
+
process.exit(1);
|
|
4632
|
+
}
|
|
4633
|
+
printer.log({ brand });
|
|
4634
|
+
});
|
|
4635
|
+
config.command("unset-context").description("Clear the default brand context").action((_opts, cmd) => {
|
|
4636
|
+
const printer = createPrinter(cmd.optsWithGlobals());
|
|
4637
|
+
unsetContextBrand();
|
|
4638
|
+
printer.log({ brand: null });
|
|
4639
|
+
});
|
|
4640
|
+
return config;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4145
4643
|
// src/cli.ts
|
|
4146
|
-
var program = new
|
|
4644
|
+
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
4645
|
const opts = thisCommand.opts();
|
|
4148
4646
|
if (!opts.color) disableColor();
|
|
4149
4647
|
});
|
|
@@ -4165,5 +4663,6 @@ program.addCommand(createOpenCommand());
|
|
|
4165
4663
|
program.addCommand(createUpdateCommand());
|
|
4166
4664
|
program.addCommand(createCompletionCommand());
|
|
4167
4665
|
program.addCommand(createMcpCommand());
|
|
4666
|
+
program.addCommand(createConfigCommand());
|
|
4168
4667
|
program.parse();
|
|
4169
4668
|
//# sourceMappingURL=cli.cjs.map
|