@adsuploader/cli 0.1.6 → 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 |
@@ -366,7 +373,7 @@ When cherry-picking features, only list ones relevant to the media type. `video_
366
373
 
367
374
  ### Carousel Ads
368
375
 
369
- Group uploaded files into carousel ads with per-card text:
376
+ Group uploaded files into carousel ads with per-card text and optional overall carousel text. `cardTexts` controls individual cards. Overall carousel text can be colocated on the carousel object or set in `texts.perAd` using the carousel `name`; colocated fields win when both are present.
370
377
 
371
378
  ```json
372
379
  {
@@ -374,6 +381,12 @@ Group uploaded files into carousel ads with per-card text:
374
381
  {
375
382
  "name": "My Carousel",
376
383
  "cards": ["slide1.jpg", "slide2.jpg", "slide3.jpg"],
384
+ "headlines": ["Overall Carousel Headline"],
385
+ "bodies": ["Overall primary text"],
386
+ "descriptions": ["Overall description"],
387
+ "cta": "SHOP_NOW",
388
+ "link": "https://example.com/carousel",
389
+ "urlTags": "utm_content=my_carousel",
377
390
  "cardTexts": [
378
391
  { "headline": "Slide 1", "description": "First card", "link": "https://example.com/1" },
379
392
  { "headline": "Slide 2", "description": "Second card", "link": "https://example.com/2" }
@@ -383,6 +396,31 @@ Group uploaded files into carousel ads with per-card text:
383
396
  }
384
397
  ```
385
398
 
399
+ Alternative overall-text shape:
400
+
401
+ ```json
402
+ {
403
+ "texts": {
404
+ "perAd": {
405
+ "My Carousel": {
406
+ "headlines": ["Overall Carousel Headline"],
407
+ "bodies": ["Overall primary text"],
408
+ "descriptions": ["Overall description"],
409
+ "cta": "SHOP_NOW",
410
+ "link": "https://example.com/carousel",
411
+ "urlTags": "utm_content=my_carousel"
412
+ }
413
+ }
414
+ },
415
+ "carousel": [
416
+ {
417
+ "name": "My Carousel",
418
+ "cards": ["slide1.jpg", "slide2.jpg", "slide3.jpg"]
419
+ }
420
+ ]
421
+ }
422
+ ```
423
+
386
424
  Cards must reference filenames from the upload batch. Minimum 2 cards per carousel. Files claimed by a carousel are removed from the standard ad list.
387
425
 
388
426
  ### Flexible Ads
@@ -402,6 +440,32 @@ Group multiple assets into a single flexible ad (Meta picks the best asset per p
402
440
 
403
441
  Minimum 2 assets per group. Files claimed by a flexible group are removed from the standard ad list.
404
442
 
443
+ ### Multi Media Ads
444
+
445
+ Group 2-10 uploaded images or videos into a Meta Multi Media ad:
446
+
447
+ ```json
448
+ {
449
+ "multimedia": [
450
+ {
451
+ "name": "Mixed Media Ad",
452
+ "assets": ["hero.jpg", "promo.mp4", "banner.jpg"],
453
+ "assetTexts": [
454
+ {
455
+ "headline": "Hero headline",
456
+ "primaryText": "Hero primary text",
457
+ "description": "Hero description",
458
+ "link": "https://example.com/hero",
459
+ "displayUrl": "example.com/hero"
460
+ }
461
+ ]
462
+ }
463
+ ]
464
+ }
465
+ ```
466
+
467
+ Assets must reference filenames from the upload batch. `assetTexts` is optional and lines up with `assets` by index; each field is a single override for that asset and blank fields fall back to the ad's main text and URLs. The primary rendered asset uses the ad's main text and URL, and any primary asset text override becomes the first main text option. Videos need a cached public thumbnail URL from upload processing. Files claimed by a Multi Media group are removed from the standard ad list.
468
+
405
469
  ### Ad Naming
406
470
 
407
471
  ```json
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);
@@ -5002,6 +5048,7 @@ async function stageFiles(paths, opts) {
5002
5048
  uploadedFiles.push({
5003
5049
  name: file.name,
5004
5050
  type: file.type,
5051
+ size: file.size,
5005
5052
  fileKey: r2Info.fileKey
5006
5053
  });
5007
5054
  } catch (err) {
@@ -5030,6 +5077,7 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5030
5077
  const result = await client.uploads.process(batchId, {
5031
5078
  name: file.name,
5032
5079
  type: file.type,
5080
+ size: file.size,
5033
5081
  fileKey: file.fileKey
5034
5082
  });
5035
5083
  results.push(result);
@@ -5331,7 +5379,7 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5331
5379
  statusStr += import_picocolors11.default.dim(` (${levelLabel})`);
5332
5380
  }
5333
5381
  let enhLabel = "";
5334
- if (result.enhancements === "metaDefaults") enhLabel = import_picocolors11.default.dim("Meta Defaults");
5382
+ if (result.enhancements === "metaDefaults") enhLabel = import_picocolors11.default.dim("All Off");
5335
5383
  else if (result.enhancements === "all") enhLabel = import_picocolors11.default.dim("All On");
5336
5384
  else if (result.enhancements === "none") enhLabel = import_picocolors11.default.dim("All Off");
5337
5385
  else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors11.default.dim(`${result.enhancements.length} custom`);
@@ -5339,24 +5387,6 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5339
5387
  console.log(` ${parts.join(", ")} ${import_picocolors11.default.dim("\xB7")} ${statusStr}${enhPart}
5340
5388
  `);
5341
5389
  if (result.ads?.length) {
5342
- let wrapLine = function(text, startCol) {
5343
- const width = Math.max(termWidth - startCol, 20);
5344
- if (text.length <= width) return text;
5345
- const pad = " ".repeat(startCol);
5346
- const lines = [];
5347
- let remaining = text;
5348
- while (remaining.length > 0) {
5349
- if (remaining.length <= width) {
5350
- lines.push(remaining);
5351
- break;
5352
- }
5353
- let breakAt = remaining.lastIndexOf(" ", width);
5354
- if (breakAt <= 0) breakAt = width;
5355
- lines.push(remaining.slice(0, breakAt));
5356
- remaining = remaining.slice(breakAt + 1);
5357
- }
5358
- return lines.join("\n" + pad);
5359
- };
5360
5390
  const tableRows = [];
5361
5391
  let lastCampaign = null;
5362
5392
  let lastAdSet = null;
@@ -5378,6 +5408,24 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5378
5408
  ]);
5379
5409
  console.log("");
5380
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
+ };
5381
5429
  const allLinks = result.ads.map((a) => a.link).filter(Boolean);
5382
5430
  const allUrlTags = result.ads.map((a) => a.urlTags).filter(Boolean);
5383
5431
  const allCtas = result.ads.map((a) => a.cta).filter(Boolean);
@@ -5385,7 +5433,7 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5385
5433
  const sharedUrlTags = allUrlTags.length > 0 && allUrlTags.every((t) => t === allUrlTags[0]) ? allUrlTags[0] : null;
5386
5434
  const sharedCta = allCtas.length > 0 && allCtas.every((c) => c === allCtas[0]) ? allCtas[0] : null;
5387
5435
  const maxText = 80;
5388
- 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";
5389
5437
  let lastDetailAdSet = null;
5390
5438
  const distinctAdSets = new Set(result.ads.map((a) => a.adSetName).filter(Boolean));
5391
5439
  const showAdSetHeaders = distinctAdSets.size > 1;
@@ -5471,6 +5519,19 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5471
5519
  process.exit(1);
5472
5520
  }
5473
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
+ }
5474
5535
  function buildBodyFromOpts(specFile, opts) {
5475
5536
  let body;
5476
5537
  if (specFile) {
@@ -5524,12 +5585,13 @@ function buildBodyFromOpts(specFile, opts) {
5524
5585
  }
5525
5586
  return body;
5526
5587
  }
5527
- 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");
5528
5589
  function createCommand2() {
5529
5590
  const cmd = new Command("create").description("Create ads from spec file or flags").argument("[specFile]", "JSON spec file with ad configuration");
5530
5591
  sharedOpts(cmd);
5531
5592
  return cmd.action(async (specFile, opts) => {
5532
5593
  const body = buildBodyFromOpts(specFile, opts);
5594
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5533
5595
  await executeCreate(body, opts);
5534
5596
  });
5535
5597
  }
@@ -5547,6 +5609,7 @@ function createPreviewCommand() {
5547
5609
  sharedOpts(cmd);
5548
5610
  return cmd.action(async (specFile, opts) => {
5549
5611
  const body = buildBodyFromOpts(specFile, opts);
5612
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5550
5613
  await executeCreate(body, opts, { preview: true });
5551
5614
  });
5552
5615
  }
@@ -5555,6 +5618,7 @@ function createTestCommand() {
5555
5618
  sharedOpts(cmd);
5556
5619
  return cmd.action(async (specFile, opts) => {
5557
5620
  const body = buildBodyFromOpts(specFile, opts);
5621
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5558
5622
  await executeCreate(body, opts, { test: true });
5559
5623
  });
5560
5624
  }
@@ -5620,6 +5684,7 @@ async function interactiveCreate(client) {
5620
5684
  });
5621
5685
  if (pD(adChoice)) process.exit(0);
5622
5686
  body.copyFromAd = adChoice;
5687
+ await assertCopyFromAdIsUsable(client, adChoice);
5623
5688
  }
5624
5689
  const uploadId = await he({
5625
5690
  message: "Upload batch ID (from `ads upload`)",
@@ -5733,7 +5798,7 @@ async function interactiveCreate(client) {
5733
5798
  if (pD(textStrategy)) process.exit(0);
5734
5799
  M2.info(import_picocolors11.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
5735
5800
  const headlines = [];
5736
- while (true) {
5801
+ for (; ; ) {
5737
5802
  const h2 = await he({
5738
5803
  message: `Headline ${headlines.length + 1}`,
5739
5804
  placeholder: headlines.length === 0 ? "Your headline..." : "(blank to finish)"
@@ -5744,7 +5809,7 @@ async function interactiveCreate(client) {
5744
5809
  }
5745
5810
  M2.info(import_picocolors11.default.dim("Enter primary text (one per line). Leave blank to finish."));
5746
5811
  const bodies = [];
5747
- while (true) {
5812
+ for (; ; ) {
5748
5813
  const b4 = await he({
5749
5814
  message: `Primary text ${bodies.length + 1}`,
5750
5815
  placeholder: bodies.length === 0 ? "Your ad copy..." : "(blank to finish)"
@@ -5808,14 +5873,12 @@ async function interactiveCreate(client) {
5808
5873
  const enhancementsChoice = await ve({
5809
5874
  message: "Creative enhancements",
5810
5875
  options: [
5811
- { value: "meta", label: "Use Meta Defaults (recommended)" },
5812
- { value: "none", label: "No enhancements" }
5876
+ { value: "none", label: "No enhancements (recommended)" },
5877
+ { value: "all", label: "All enhancements" }
5813
5878
  ]
5814
5879
  });
5815
5880
  if (pD(enhancementsChoice)) process.exit(0);
5816
- if (enhancementsChoice !== "meta") {
5817
- body.creativeEnhancements = "none";
5818
- }
5881
+ body.creativeEnhancements = enhancementsChoice;
5819
5882
  const status = await ve({
5820
5883
  message: "Launch status",
5821
5884
  options: [
@@ -5940,7 +6003,7 @@ function statusLabel(status) {
5940
6003
  }
5941
6004
 
5942
6005
  // src/cli.js
5943
- var VERSION = true ? "0.1.6" : "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.6",
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",