@adsuploader/cli 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,7 @@ This package includes a `SKILL.md` file that describes every command and option
42
42
  | `ads adset <id>` | Show ads in an ad set |
43
43
  | `ads ad <id>` | Ad details and creative |
44
44
  | `ads presets` | List saved presets |
45
+ | `ads presets:save --from-ad <id> --name <name>` | Save an existing ad as an API preset |
45
46
  | `ads upload <files...>` | Upload images and videos |
46
47
  | `ads uploads` | List recent upload batches |
47
48
  | `ads create spec.json` | Create ads from spec |
package/SKILL.md CHANGED
@@ -84,6 +84,7 @@ Ads are created **ACTIVE** by default. Use `--status PAUSED` to create them paus
84
84
  | `adset <adSetId>` | Show ads in an ad set |
85
85
  | `ad <adId>` | Ad details + creative config |
86
86
  | `presets` | List saved API presets (or `presets <id>` for details) |
87
+ | `presets:save --from-ad <adId> --name <name>` | Save an existing ad as an API preset |
87
88
  | `text-presets` | List saved text presets (or `text-presets <id>` for details) |
88
89
  | `uploads` | List recent upload batches (or `uploads <batchId>` for details) |
89
90
 
@@ -115,6 +116,12 @@ Ads are created **ACTIVE** by default. Use `--status PAUSED` to create them paus
115
116
  | `--daily-budget <amount>` | Override daily budget (currency units) |
116
117
  | `--bid-amount <amount>` | Override bid/cost cap (currency units) |
117
118
  | `--text-file <path>` | Load text config from a JSON file |
119
+ | `--expanded` | Show full headline, primary text, and description values in previews |
120
+
121
+ ### Detail Flags
122
+ | Flag | Description |
123
+ |------|-------------|
124
+ | `ad --expanded` | Show full headline, primary text, and description values |
118
125
 
119
126
  ### Common Flags
120
127
  | Flag | Description |
package/dist/cli.cjs CHANGED
@@ -4210,7 +4210,8 @@ function createClient(opts = {}) {
4210
4210
  },
4211
4211
  presets: {
4212
4212
  list: (query) => request("GET", "/presets", { query }),
4213
- get: (id) => request("GET", `/presets/${id}`)
4213
+ get: (id) => request("GET", `/presets/${id}`),
4214
+ create: (body) => request("POST", "/presets", { body })
4214
4215
  },
4215
4216
  uploads: {
4216
4217
  list: (query) => request("GET", "/uploads", { query }),
@@ -4706,12 +4707,12 @@ function adsetCommand() {
4706
4707
 
4707
4708
  // src/commands/ad.js
4708
4709
  var import_picocolors7 = __toESM(require_picocolors(), 1);
4709
- function truncate(str, max = 120) {
4710
- if (!str || str.length <= max) return str;
4710
+ function truncate(str, max = 120, expanded = false) {
4711
+ if (!str || expanded || str.length <= max) return str;
4711
4712
  return str.slice(0, max) + "...";
4712
4713
  }
4713
4714
  function adCommand() {
4714
- return new Command("ad").description("View ad details and creative").argument("<id>", "Ad ID").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").action(async (id, opts) => {
4715
+ return new Command("ad").description("View ad details and creative").argument("<id>", "Ad ID").option("--account <id>", "Ad account ID").option("--expanded", "Show full headline / primary text / description without truncation").option("--json", "Output as JSON").action(async (id, opts) => {
4715
4716
  const client = createClient({ accountId: opts.account });
4716
4717
  const { ad } = await client.ads.get(id);
4717
4718
  if (shouldOutputJson(opts)) {
@@ -4727,14 +4728,15 @@ function adCommand() {
4727
4728
  console.log(` ${import_picocolors7.default.bold("Ad Set:")} ${ad.adSetId}`);
4728
4729
  if (ad.creative) {
4729
4730
  const label = (name) => import_picocolors7.default.bold(name.padEnd(15));
4731
+ const exp = !!opts.expanded;
4730
4732
  console.log("");
4731
4733
  console.log(` ${import_picocolors7.default.bold("Creative")}`);
4732
- if (ad.creative.headline) console.log(` ${label("Headline:")}${truncate(ad.creative.headline)}`);
4733
- if (ad.creative.headlines) console.log(` ${label("Headlines:")}${ad.creative.headlines.map((h2) => truncate(h2, 60)).join(" | ")}`);
4734
- if (ad.creative.primaryText) console.log(` ${label("Primary Text:")}${truncate(ad.creative.primaryText)}`);
4735
- if (ad.creative.primaryTexts) console.log(` ${label("Primary Texts:")}${ad.creative.primaryTexts.map((t) => truncate(t, 60)).join(" | ")}`);
4736
- if (ad.creative.description) console.log(` ${label("Description:")}${truncate(ad.creative.description)}`);
4737
- if (ad.creative.descriptions) console.log(` ${label("Descriptions:")}${ad.creative.descriptions.map((d3) => truncate(d3, 60)).join(" | ")}`);
4734
+ if (ad.creative.headline) console.log(` ${label("Headline:")}${truncate(ad.creative.headline, 120, exp)}`);
4735
+ if (ad.creative.headlines) console.log(` ${label("Headlines:")}${ad.creative.headlines.map((h2) => truncate(h2, 60, exp)).join(" | ")}`);
4736
+ if (ad.creative.primaryText) console.log(` ${label("Primary Text:")}${truncate(ad.creative.primaryText, 120, exp)}`);
4737
+ if (ad.creative.primaryTexts) console.log(` ${label("Primary Texts:")}${ad.creative.primaryTexts.map((t) => truncate(t, 60, exp)).join(" | ")}`);
4738
+ if (ad.creative.description) console.log(` ${label("Description:")}${truncate(ad.creative.description, 120, exp)}`);
4739
+ if (ad.creative.descriptions) console.log(` ${label("Descriptions:")}${ad.creative.descriptions.map((d3) => truncate(d3, 60, exp)).join(" | ")}`);
4738
4740
  if (ad.creative.cta) console.log(` ${label("CTA:")}${ad.creative.cta}`);
4739
4741
  if (ad.creative.link) console.log(` ${label("Link:")}${ad.creative.link}`);
4740
4742
  if (ad.creative.displayUrl) console.log(` ${label("Display URL:")}${ad.creative.displayUrl}`);
@@ -4833,6 +4835,50 @@ function textPresetsCommand() {
4833
4835
  console.log("");
4834
4836
  });
4835
4837
  }
4838
+ function presetsSaveCommand() {
4839
+ return new Command("presets:save").description("Save an existing ad as a reusable API preset").option("--account <id>", "Ad account ID").option("--from-ad <adId>", "Ad ID to save settings from (required)").option("--name <name>", "Preset name (prompted if omitted)").option("--share", "Share with team (if you are on a team plan)").option("--json", "Output as JSON").action(async (opts) => {
4840
+ const client = createClient({ accountId: opts.account });
4841
+ if (!opts.fromAd) {
4842
+ printError("--from-ad <adId> is required.");
4843
+ process.exit(1);
4844
+ }
4845
+ let name = opts.name;
4846
+ if (!name && isInteractive()) {
4847
+ name = await he({
4848
+ message: "Preset name",
4849
+ placeholder: "e.g. Spring 2026 - Standard Purchase",
4850
+ validate: (v2) => v2?.trim() ? void 0 : "Name is required"
4851
+ });
4852
+ if (pD(name)) process.exit(0);
4853
+ }
4854
+ if (!name?.trim()) {
4855
+ printError("--name <name> is required (or run in an interactive TTY to be prompted).");
4856
+ process.exit(1);
4857
+ }
4858
+ try {
4859
+ const { preset } = await client.presets.create({
4860
+ type: "api",
4861
+ fromAdId: opts.fromAd,
4862
+ name: name.trim(),
4863
+ shareWithTeam: !!opts.share
4864
+ });
4865
+ if (shouldOutputJson(opts)) {
4866
+ printJson(preset);
4867
+ return;
4868
+ }
4869
+ printSuccess(`Saved preset "${preset.name}"`);
4870
+ console.log(` ${import_picocolors8.default.bold("ID:")} ${preset.id}`);
4871
+ console.log(` ${import_picocolors8.default.bold("Type:")} ${preset.type}`);
4872
+ if (preset.shared) console.log(` ${import_picocolors8.default.bold("Shared:")} ${import_picocolors8.default.cyan("team")}`);
4873
+ console.log(`
4874
+ ${import_picocolors8.default.dim("Use it:")} ads create --preset ${preset.id} ...
4875
+ `);
4876
+ } catch (err) {
4877
+ printError(err.message || "Failed to save preset");
4878
+ process.exit(1);
4879
+ }
4880
+ });
4881
+ }
4836
4882
 
4837
4883
  // src/commands/upload.js
4838
4884
  var import_fs2 = __toESM(require("fs"), 1);
@@ -5341,24 +5387,6 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5341
5387
  console.log(` ${parts.join(", ")} ${import_picocolors11.default.dim("\xB7")} ${statusStr}${enhPart}
5342
5388
  `);
5343
5389
  if (result.ads?.length) {
5344
- let wrapLine = function(text, startCol) {
5345
- const width = Math.max(termWidth - startCol, 20);
5346
- if (text.length <= width) return text;
5347
- const pad = " ".repeat(startCol);
5348
- const lines = [];
5349
- let remaining = text;
5350
- while (remaining.length > 0) {
5351
- if (remaining.length <= width) {
5352
- lines.push(remaining);
5353
- break;
5354
- }
5355
- let breakAt = remaining.lastIndexOf(" ", width);
5356
- if (breakAt <= 0) breakAt = width;
5357
- lines.push(remaining.slice(0, breakAt));
5358
- remaining = remaining.slice(breakAt + 1);
5359
- }
5360
- return lines.join("\n" + pad);
5361
- };
5362
5390
  const tableRows = [];
5363
5391
  let lastCampaign = null;
5364
5392
  let lastAdSet = null;
@@ -5380,6 +5408,24 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5380
5408
  ]);
5381
5409
  console.log("");
5382
5410
  const termWidth = process.stdout.columns || 80;
5411
+ const wrapLine = (text, startCol) => {
5412
+ const width = Math.max(termWidth - startCol, 20);
5413
+ if (text.length <= width) return text;
5414
+ const pad = " ".repeat(startCol);
5415
+ const lines = [];
5416
+ let remaining = text;
5417
+ while (remaining.length > 0) {
5418
+ if (remaining.length <= width) {
5419
+ lines.push(remaining);
5420
+ break;
5421
+ }
5422
+ let breakAt = remaining.lastIndexOf(" ", width);
5423
+ if (breakAt <= 0) breakAt = width;
5424
+ lines.push(remaining.slice(0, breakAt));
5425
+ remaining = remaining.slice(breakAt + 1);
5426
+ }
5427
+ return lines.join("\n" + pad);
5428
+ };
5383
5429
  const allLinks = result.ads.map((a) => a.link).filter(Boolean);
5384
5430
  const allUrlTags = result.ads.map((a) => a.urlTags).filter(Boolean);
5385
5431
  const allCtas = result.ads.map((a) => a.cta).filter(Boolean);
@@ -5387,7 +5433,7 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5387
5433
  const sharedUrlTags = allUrlTags.length > 0 && allUrlTags.every((t) => t === allUrlTags[0]) ? allUrlTags[0] : null;
5388
5434
  const sharedCta = allCtas.length > 0 && allCtas.every((c) => c === allCtas[0]) ? allCtas[0] : null;
5389
5435
  const maxText = 80;
5390
- const trunc = (s) => s.length > maxText ? s.slice(0, maxText - 1) + "\u2026" : s;
5436
+ const trunc = (s) => opts.expanded || s.length <= maxText ? s : s.slice(0, maxText - 1) + "\u2026";
5391
5437
  let lastDetailAdSet = null;
5392
5438
  const distinctAdSets = new Set(result.ads.map((a) => a.adSetName).filter(Boolean));
5393
5439
  const showAdSetHeaders = distinctAdSets.size > 1;
@@ -5473,6 +5519,19 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5473
5519
  process.exit(1);
5474
5520
  }
5475
5521
  }
5522
+ async function assertCopyFromAdIsUsable(client, adId) {
5523
+ let ad;
5524
+ try {
5525
+ ({ ad } = await client.ads.get(adId));
5526
+ } catch (err) {
5527
+ printError(`Could not fetch ad ${adId}: ${err.message}`);
5528
+ process.exit(1);
5529
+ }
5530
+ if (ad?.creative && ad.creative.canCopySettingsFrom === false) {
5531
+ printError(ad.creative.cannotCopyReason || "This ad cannot be used as a template.");
5532
+ process.exit(1);
5533
+ }
5534
+ }
5476
5535
  function buildBodyFromOpts(specFile, opts) {
5477
5536
  let body;
5478
5537
  if (specFile) {
@@ -5526,12 +5585,13 @@ function buildBodyFromOpts(specFile, opts) {
5526
5585
  }
5527
5586
  return body;
5528
5587
  }
5529
- var sharedOpts = (cmd) => cmd.option("--account <id>", "Ad account ID").option("--preset <id>", "API preset ID").option("--text-preset <id>", "Text preset ID").option("--copy-from <adId>", "Ad ID to copy settings from").option("--upload <batchId>", "Upload batch ID").option("--status <status>", "PAUSED or ACTIVE (default: ACTIVE)").option("--daily-budget <amount>", "Override daily budget per ad set (in currency units, e.g. 50 for $50)").option("--bid-amount <amount>", "Override bid/cost cap per ad set (in currency units, e.g. 5 for $5)").option("--pause-at <level>", "Pause level: ad (default), adSet, or campaign").option("--text-file <path>", "Load text configuration from JSON file").option("--json", "Output as JSON");
5588
+ var sharedOpts = (cmd) => cmd.option("--account <id>", "Ad account ID").option("--preset <id>", "API preset ID").option("--text-preset <id>", "Text preset ID").option("--copy-from <adId>", "Ad ID to copy settings from").option("--upload <batchId>", "Upload batch ID").option("--status <status>", "PAUSED or ACTIVE (default: ACTIVE)").option("--daily-budget <amount>", "Override daily budget per ad set (in currency units, e.g. 50 for $50)").option("--bid-amount <amount>", "Override bid/cost cap per ad set (in currency units, e.g. 5 for $5)").option("--pause-at <level>", "Pause level: ad (default), adSet, or campaign").option("--text-file <path>", "Load text configuration from JSON file").option("--expanded", "Show full headline / primary text / description without truncation").option("--json", "Output as JSON");
5530
5589
  function createCommand2() {
5531
5590
  const cmd = new Command("create").description("Create ads from spec file or flags").argument("[specFile]", "JSON spec file with ad configuration");
5532
5591
  sharedOpts(cmd);
5533
5592
  return cmd.action(async (specFile, opts) => {
5534
5593
  const body = buildBodyFromOpts(specFile, opts);
5594
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5535
5595
  await executeCreate(body, opts);
5536
5596
  });
5537
5597
  }
@@ -5549,6 +5609,7 @@ function createPreviewCommand() {
5549
5609
  sharedOpts(cmd);
5550
5610
  return cmd.action(async (specFile, opts) => {
5551
5611
  const body = buildBodyFromOpts(specFile, opts);
5612
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5552
5613
  await executeCreate(body, opts, { preview: true });
5553
5614
  });
5554
5615
  }
@@ -5557,6 +5618,7 @@ function createTestCommand() {
5557
5618
  sharedOpts(cmd);
5558
5619
  return cmd.action(async (specFile, opts) => {
5559
5620
  const body = buildBodyFromOpts(specFile, opts);
5621
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5560
5622
  await executeCreate(body, opts, { test: true });
5561
5623
  });
5562
5624
  }
@@ -5622,6 +5684,7 @@ async function interactiveCreate(client) {
5622
5684
  });
5623
5685
  if (pD(adChoice)) process.exit(0);
5624
5686
  body.copyFromAd = adChoice;
5687
+ await assertCopyFromAdIsUsable(client, adChoice);
5625
5688
  }
5626
5689
  const uploadId = await he({
5627
5690
  message: "Upload batch ID (from `ads upload`)",
@@ -5735,7 +5798,7 @@ async function interactiveCreate(client) {
5735
5798
  if (pD(textStrategy)) process.exit(0);
5736
5799
  M2.info(import_picocolors11.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
5737
5800
  const headlines = [];
5738
- while (true) {
5801
+ for (; ; ) {
5739
5802
  const h2 = await he({
5740
5803
  message: `Headline ${headlines.length + 1}`,
5741
5804
  placeholder: headlines.length === 0 ? "Your headline..." : "(blank to finish)"
@@ -5746,7 +5809,7 @@ async function interactiveCreate(client) {
5746
5809
  }
5747
5810
  M2.info(import_picocolors11.default.dim("Enter primary text (one per line). Leave blank to finish."));
5748
5811
  const bodies = [];
5749
- while (true) {
5812
+ for (; ; ) {
5750
5813
  const b4 = await he({
5751
5814
  message: `Primary text ${bodies.length + 1}`,
5752
5815
  placeholder: bodies.length === 0 ? "Your ad copy..." : "(blank to finish)"
@@ -5940,7 +6003,7 @@ function statusLabel(status) {
5940
6003
  }
5941
6004
 
5942
6005
  // src/cli.js
5943
- var VERSION = true ? "0.1.7" : "0.0.0";
6006
+ var VERSION = true ? "0.1.8" : "0.0.0";
5944
6007
  var apiUrl = process.env.ADS_API_URL || getBaseUrl();
5945
6008
  if (apiUrl && (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1"))) {
5946
6009
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -5970,6 +6033,7 @@ program2.addCommand(adsetsCommand());
5970
6033
  program2.addCommand(adsetCommand());
5971
6034
  program2.addCommand(adCommand());
5972
6035
  program2.addCommand(presetsCommand());
6036
+ program2.addCommand(presetsSaveCommand());
5973
6037
  program2.addCommand(textPresetsCommand());
5974
6038
  program2.addCommand(uploadsCommand());
5975
6039
  program2.addCommand(uploadCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adsuploader/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Create Facebook ads from the command line — bulk upload media, preview configurations, and launch ads at scale",
5
5
  "author": "Ads Uploader <support@adsuploader.com> (https://adsuploader.com)",
6
6
  "license": "UNLICENSED",