@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/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.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(path2, query) {
80
- return this.client.request("GET", path2, void 0, query);
79
+ get(path3, query) {
80
+ return this.client.request("GET", path3, void 0, query);
81
81
  }
82
- post(path2, body) {
83
- return this.client.request("POST", path2, body);
82
+ post(path3, body) {
83
+ return this.client.request("POST", path3, body);
84
84
  }
85
- put(path2, body) {
86
- return this.client.request("PUT", path2, body);
85
+ put(path3, body) {
86
+ return this.client.request("PUT", path3, body);
87
87
  }
88
- patch(path2, body) {
89
- return this.client.request("PATCH", path2, body);
88
+ patch(path3, body) {
89
+ return this.client.request("PATCH", path3, body);
90
90
  }
91
- delete(path2) {
92
- return this.client.request("DELETE", path2);
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, path2, body, query) {
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, path2, body, query);
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, path2, body, query) {
452
- let url = `${this.baseUrl}${path2}`;
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} ${path2} timed out after ${this.timeout}ms.`,
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} ${path2} failed: ${err instanceof Error ? err.message : String(err)}`,
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} ${path2} 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.`,
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} ${path2} timed out after ${this.timeout}ms.`,
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 import_commander19 = require("commander");
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/media-flags.ts
2799
- var import_node_fs2 = require("fs");
2800
- var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
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 specifiedValue = accountMap[platform];
2956
- if (specifiedValue !== void 0) {
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/commands/posts.ts
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").requiredOption("--brand <slug>", "Brand slug").requiredOption("--text <text>", "Post text content").requiredOption("--platforms <list>", "Comma-separated platforms (e.g. x,linkedin)").option("--at <iso>", "Schedule date/time (ISO 8601)").option(
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
- const platforms = parsePlatforms(opts.platforms);
3104
- const scheduledAt = opts.at ? validateScheduledAt(opts.at) : void 0;
3105
- if (opts.media && opts.media.length > 0) {
3106
- validateMediaCount(opts.media);
3107
- validateMediaPaths(opts.media);
3108
- validateMediaExtensions(opts.media);
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 platformTexts = opts.platformText && opts.platformText.length > 0 ? parsePairs("--platform-text", opts.platformText) : void 0;
3111
- const platformMediaPaths = opts.platformMedia && opts.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", opts.platformMedia) : {};
3112
- for (const paths of Object.values(platformMediaPaths)) {
3113
- validateMediaPaths(paths);
3114
- validateMediaExtensions(paths);
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
- const altTexts = alignAltText(opts.media ?? [], opts.altText ?? []);
3117
- const youtubeOptions = buildYoutubeOptions({
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
- const platformMediaResult = {};
3164
- for (const [p, paths] of Object.entries(platformMediaPaths)) {
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 post = await client.posts.create({
3180
- brandSlug: opts.brand,
3181
- text: opts.text,
3182
- platforms,
3183
- ...scheduledAt ? { scheduledAt } : {},
3184
- ...opts.thread && opts.thread.length > 0 ? { thread: opts.thread } : {},
3185
- ...mediaInputs.length > 0 ? { media: mediaInputs } : {},
3186
- ...Object.keys(platformMediaResult).length > 0 ? { platformMedia: platformMediaResult } : {},
3187
- ...platformTexts && Object.keys(platformTexts).length > 0 ? { platformTexts } : {},
3188
- ...Object.keys(targetAccountIds).length > 0 ? { targetAccountIds } : {},
3189
- ...instagramOptions ? { instagramOptions } : {},
3190
- ...threadsOptions ? { threadsOptions } : {},
3191
- ...youtubeOptions ? { youtubeOptions } : {},
3192
- source: "cli"
3193
- });
3194
- spinner.stop();
3195
- printer.log(post);
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) && printer.isTty()) {
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 import_node_fs3 = __toESM(require("fs"));
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 = import_node_fs3.default.statSync(credPath);
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 import_commander19.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: table, json", "table").option("--api-key <key>", "API key (overrides env/stored credentials)").option("--no-color", "Disable color output").hook("preAction", (thisCommand) => {
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