@autoposting.ai/cli 0.3.2 → 0.3.4

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