@adsuploader/cli 0.1.7 → 0.2.0

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.
Files changed (4) hide show
  1. package/README.md +2 -0
  2. package/SKILL.md +43 -0
  3. package/dist/cli.cjs +436 -197
  4. package/package.json +2 -1
package/README.md CHANGED
@@ -37,11 +37,13 @@ This package includes a `SKILL.md` file that describes every command and option
37
37
  | `ads login` | Authenticate via browser |
38
38
  | `ads accounts` | List ad accounts |
39
39
  | `ads account <id>` | Set default account |
40
+ | `ads pages` | List Facebook Pages available for profile overrides |
40
41
  | `ads campaigns` | List campaigns |
41
42
  | `ads campaign <id>` | Show ad sets in a campaign |
42
43
  | `ads adset <id>` | Show ads in an ad set |
43
44
  | `ads ad <id>` | Ad details and creative |
44
45
  | `ads presets` | List saved presets |
46
+ | `ads presets:save --from-ad <id> --name <name>` | Save an existing ad as an API preset |
45
47
  | `ads upload <files...>` | Upload images and videos |
46
48
  | `ads uploads` | List recent upload batches |
47
49
  | `ads create spec.json` | Create ads from spec |
package/SKILL.md CHANGED
@@ -45,6 +45,14 @@ ads upload ./my-creatives/ # entire directory
45
45
 
46
46
  Returns a **batch ID** (e.g. `batch_abc123`) — you'll need this for the spec file. Files are uploaded directly to the selected ad account's Facebook media library, so the batch ID is tied to that account.
47
47
 
48
+ Files are staged in parallel and transient network failures are retried automatically. Tune the defaults if needed:
49
+
50
+ | Upload option | Description |
51
+ |---------------|-------------|
52
+ | `--concurrency <n>` | Parallel staging uploads, 1-6 (default: 4) |
53
+ | `--upload-timeout <ms>` | Per-file R2 upload timeout (default: 120000) |
54
+ | `--api-timeout <ms>` | API request timeout (default: 60000) |
55
+
48
56
  ### 2. Preview (dry run)
49
57
 
50
58
  ```bash
@@ -78,12 +86,14 @@ Ads are created **ACTIVE** by default. Use `--status PAUSED` to create them paus
78
86
  |---------|-------------|
79
87
  | `accounts` | List ad accounts |
80
88
  | `account <id>` | Set default account |
89
+ | `pages` | List Facebook Pages available for `--page` / `profile.pageId` |
81
90
  | `campaigns` | List active campaigns (`--status all` for all, `--search <text>` to filter) |
82
91
  | `campaign <id>` | Show ad sets in a campaign |
83
92
  | `adsets --campaign <id>` | List ad sets (supports `--search`, `--status`) |
84
93
  | `adset <adSetId>` | Show ads in an ad set |
85
94
  | `ad <adId>` | Ad details + creative config |
86
95
  | `presets` | List saved API presets (or `presets <id>` for details) |
96
+ | `presets:save --from-ad <adId> --name <name>` | Save an existing ad as an API preset |
87
97
  | `text-presets` | List saved text presets (or `text-presets <id>` for details) |
88
98
  | `uploads` | List recent upload batches (or `uploads <batchId>` for details) |
89
99
 
@@ -114,7 +124,16 @@ Ads are created **ACTIVE** by default. Use `--status PAUSED` to create them paus
114
124
  | `--pause-at <level>` | Pause level: `ad` (default), `adSet`, or `campaign` |
115
125
  | `--daily-budget <amount>` | Override daily budget (currency units) |
116
126
  | `--bid-amount <amount>` | Override bid/cost cap (currency units) |
127
+ | `--page <id>` | Override the Facebook Page ID used by created ads |
128
+ | `--instagram <id>` | Override the Instagram account ID used by created ads |
129
+ | `--threads <id>` | Override the Threads profile ID used by created ads |
117
130
  | `--text-file <path>` | Load text config from a JSON file |
131
+ | `--expanded` | Show full headline, primary text, and description values in previews |
132
+
133
+ ### Detail Flags
134
+ | Flag | Description |
135
+ |------|-------------|
136
+ | `ad --expanded` | Show full headline, primary text, and description values |
118
137
 
119
138
  ### Common Flags
120
139
  | Flag | Description |
@@ -158,6 +177,30 @@ When using `copyFromAd`, provide the upload batch and optionally campaign/ad set
158
177
  }
159
178
  ```
160
179
 
180
+ ### Profile Options
181
+
182
+ By default, created ads inherit the Facebook Page, Instagram account, and Threads profile from the template ad or preset. Override them with `profile`:
183
+
184
+ ```json
185
+ {
186
+ "copyFromAd": "120233848667930472",
187
+ "uploadId": "batch_abc123",
188
+ "profile": {
189
+ "pageId": "123456789012345",
190
+ "instagramId": "17841400000000000",
191
+ "threadsId": "987654321098765"
192
+ }
193
+ }
194
+ ```
195
+
196
+ You can also use flags:
197
+
198
+ ```bash
199
+ ads create:preview spec.json --page 123456789012345 --instagram 17841400000000000 --threads 987654321098765
200
+ ```
201
+
202
+ If you override only `pageId`, the CLI/API does not carry over the template ad's Instagram or Threads IDs, because they may belong to the old page. Add `instagramId` and `threadsId` explicitly when you want them attached. Run `ads pages` to list available Facebook Page IDs for `--page`; use the web app's Profile Options selector or Meta Business settings for Instagram/Threads IDs when needed. Multi-campaign per-campaign profile overrides are not supported in CLI specs yet.
203
+
161
204
  ### Campaign Structure
162
205
 
163
206
  **Single campaign** (default): ads go into the template ad's campaign, or a new campaign if `campaign.name` is provided.
package/dist/cli.cjs CHANGED
@@ -3464,7 +3464,7 @@ var {
3464
3464
  } = import_index.default;
3465
3465
 
3466
3466
  // src/cli.js
3467
- var import_picocolors13 = __toESM(require_picocolors(), 1);
3467
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
3468
3468
 
3469
3469
  // src/lib/config.js
3470
3470
  var import_fs = __toESM(require("fs"), 1);
@@ -4131,10 +4131,51 @@ var import_crypto = __toESM(require("crypto"), 1);
4131
4131
 
4132
4132
  // src/lib/client.js
4133
4133
  var USER_AGENT = "adsuploader-cli/0.1.0";
4134
+ var DEFAULT_API_TIMEOUT_MS = 6e4;
4135
+ var DEFAULT_MAX_ATTEMPTS = 4;
4136
+ function sleep(ms) {
4137
+ return new Promise((resolve) => setTimeout(resolve, ms));
4138
+ }
4139
+ function parsePositiveInt(value, fallback) {
4140
+ const parsed = Number.parseInt(value, 10);
4141
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4142
+ }
4143
+ function isRetryableNetworkError(err) {
4144
+ const message = String(err?.message || "").toLowerCase();
4145
+ return err?.name === "AbortError" || err?.code === "ECONNRESET" || err?.code === "ETIMEDOUT" || message.includes("fetch failed") || message.includes("network") || message.includes("timeout");
4146
+ }
4147
+ function isRetryableStatus(status) {
4148
+ return status === 429 || status >= 500;
4149
+ }
4150
+ function shouldRetryResponse(method, status) {
4151
+ if (status === 429) return true;
4152
+ return method === "GET" && status >= 500;
4153
+ }
4154
+ function timeoutError(timeoutMs) {
4155
+ return new Error(`API request timed out after ${Math.ceil(timeoutMs / 1e3)}s`);
4156
+ }
4157
+ function getRetryDelayMs(attempt, retryAfter, { baseDelayMs = 500, maxDelayMs = 8e3, jitterMs = 250 } = {}) {
4158
+ if (retryAfter != null && Number.isFinite(retryAfter) && retryAfter > 0) {
4159
+ return retryAfter * 1e3;
4160
+ }
4161
+ const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1));
4162
+ return exponential + Math.floor(Math.random() * jitterMs);
4163
+ }
4164
+ function formatApiError(resp, data) {
4165
+ const message = data?.error || data?.message || `API error ${resp.status}`;
4166
+ if (resp.status === 403 && /different IP address|created from a different IP|ads login/i.test(message)) {
4167
+ return "Your network IP changed since this CLI token was created. Run `ads login` again to refresh your session.";
4168
+ }
4169
+ return message;
4170
+ }
4134
4171
  function createClient(opts = {}) {
4135
4172
  const token = opts.token || getToken();
4136
4173
  const baseUrl = opts.baseUrl || getBaseUrl();
4137
4174
  const accountId = opts.accountId || getDefaultAccount();
4175
+ const fetchImpl = opts.fetchImpl || fetch;
4176
+ const delay = opts.sleep || sleep;
4177
+ const maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
4178
+ const timeoutMs = opts.timeoutMs || parsePositiveInt(process.env.ADS_API_TIMEOUT_MS, DEFAULT_API_TIMEOUT_MS);
4138
4179
  if (!token) {
4139
4180
  throw new Error("Not authenticated. Run `ads login` first.");
4140
4181
  }
@@ -4162,32 +4203,51 @@ function createClient(opts = {}) {
4162
4203
  if (body) {
4163
4204
  headers["Content-Type"] = "application/json";
4164
4205
  }
4165
- const resp = await fetch(url.toString(), {
4166
- method,
4167
- headers,
4168
- body: body ? JSON.stringify(body) : void 0,
4169
- redirect: "manual"
4170
- // Never follow redirects — prevents token leakage to redirect targets
4171
- });
4172
- if (resp.status >= 300 && resp.status < 400) {
4173
- throw new Error(`Unexpected redirect (${resp.status}). This may indicate a configuration issue.`);
4174
- }
4175
- if (resp.status === 429) {
4176
- const retryAfter = resp.headers.get("retry-after");
4177
- const err = new Error(`Rate limited. Retry after ${retryAfter || "60"}s`);
4178
- err.retryAfter = parseInt(retryAfter || "60");
4179
- throw err;
4180
- }
4181
- if (raw) return resp;
4182
- const data = await resp.json();
4183
- if (!resp.ok) {
4184
- const err = new Error(data.error || data.message || `API error ${resp.status}`);
4185
- err.code = data.errorCode;
4186
- err.status = resp.status;
4187
- err.details = data.details;
4188
- throw err;
4206
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4207
+ const controller = new AbortController();
4208
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4209
+ try {
4210
+ const resp = await fetchImpl(url.toString(), {
4211
+ method,
4212
+ headers,
4213
+ body: body ? JSON.stringify(body) : void 0,
4214
+ redirect: "manual",
4215
+ // Never follow redirects — prevents token leakage to redirect targets
4216
+ signal: controller.signal
4217
+ });
4218
+ clearTimeout(timeout);
4219
+ if (resp.status >= 300 && resp.status < 400) {
4220
+ throw new Error(`Unexpected redirect (${resp.status}). This may indicate a configuration issue.`);
4221
+ }
4222
+ if (shouldRetryResponse(method, resp.status) && attempt < maxAttempts) {
4223
+ const retryAfter = resp.status === 429 ? parsePositiveInt(resp.headers.get("retry-after"), null) : null;
4224
+ await delay(getRetryDelayMs(attempt, retryAfter));
4225
+ continue;
4226
+ }
4227
+ if (raw) return resp;
4228
+ const data = await resp.json();
4229
+ if (!resp.ok) {
4230
+ const err = new Error(formatApiError(resp, data));
4231
+ err.code = data.errorCode;
4232
+ err.status = resp.status;
4233
+ err.details = data.details;
4234
+ throw err;
4235
+ }
4236
+ return data;
4237
+ } catch (err) {
4238
+ clearTimeout(timeout);
4239
+ if (err.status) {
4240
+ throw err;
4241
+ }
4242
+ const normalizedError = err?.name === "AbortError" ? timeoutError(timeoutMs) : err;
4243
+ if (method === "GET" && attempt < maxAttempts && isRetryableNetworkError(normalizedError)) {
4244
+ await delay(getRetryDelayMs(attempt));
4245
+ continue;
4246
+ }
4247
+ throw normalizedError;
4248
+ }
4189
4249
  }
4190
- return data;
4250
+ throw new Error(`API request failed after ${maxAttempts} attempt(s)`);
4191
4251
  }
4192
4252
  return {
4193
4253
  get: (path3, opts2) => request("GET", path3, { ...opts2, requiresAccount: opts2?.requiresAccount ?? true }),
@@ -4199,6 +4259,9 @@ function createClient(opts = {}) {
4199
4259
  campaigns: {
4200
4260
  list: (query) => request("GET", "/campaigns", { query })
4201
4261
  },
4262
+ pages: {
4263
+ list: (query) => request("GET", "/pages", { query })
4264
+ },
4202
4265
  adsets: {
4203
4266
  list: (campaignId, query) => request("GET", `/campaigns/${campaignId}/adsets`, { query })
4204
4267
  },
@@ -4210,7 +4273,8 @@ function createClient(opts = {}) {
4210
4273
  },
4211
4274
  presets: {
4212
4275
  list: (query) => request("GET", "/presets", { query }),
4213
- get: (id) => request("GET", `/presets/${id}`)
4276
+ get: (id) => request("GET", `/presets/${id}`),
4277
+ create: (body) => request("POST", "/presets", { body })
4214
4278
  },
4215
4279
  uploads: {
4216
4280
  list: (query) => request("GET", "/uploads", { query }),
@@ -4704,14 +4768,45 @@ function adsetCommand() {
4704
4768
  });
4705
4769
  }
4706
4770
 
4707
- // src/commands/ad.js
4771
+ // src/commands/pages.js
4708
4772
  var import_picocolors7 = __toESM(require_picocolors(), 1);
4709
- function truncate(str, max = 120) {
4710
- if (!str || str.length <= max) return str;
4773
+ function formatInstagram(page) {
4774
+ if (!page.instagramId && !page.instagramUsername) return "";
4775
+ if (page.instagramUsername && page.instagramId) return `${page.instagramUsername} (${page.instagramId})`;
4776
+ return page.instagramUsername || page.instagramId;
4777
+ }
4778
+ function pagesCommand() {
4779
+ return new Command("pages").description("List Facebook Pages available for profile overrides").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").action(async (opts) => {
4780
+ const client = createClient({ accountId: opts.account });
4781
+ const { pages } = await client.pages.list();
4782
+ if (shouldOutputJson(opts)) {
4783
+ printJson(pages);
4784
+ return;
4785
+ }
4786
+ const rows = pages.map((page) => ({
4787
+ ...page,
4788
+ instagram: formatInstagram(page)
4789
+ }));
4790
+ console.log(`
4791
+ ${import_picocolors7.default.bold("Pages")} (${pages.length})
4792
+ `);
4793
+ printTable(rows, [
4794
+ { key: "id", label: "ID", maxWidth: 25 },
4795
+ { key: "name", label: "Name", maxWidth: 40 },
4796
+ { key: "instagram", label: "Instagram", maxWidth: 36 }
4797
+ ]);
4798
+ console.log("");
4799
+ });
4800
+ }
4801
+
4802
+ // src/commands/ad.js
4803
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
4804
+ function truncate(str, max = 120, expanded = false) {
4805
+ if (!str || expanded || str.length <= max) return str;
4711
4806
  return str.slice(0, max) + "...";
4712
4807
  }
4713
4808
  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) => {
4809
+ 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
4810
  const client = createClient({ accountId: opts.account });
4716
4811
  const { ad } = await client.ads.get(id);
4717
4812
  if (shouldOutputJson(opts)) {
@@ -4719,22 +4814,23 @@ function adCommand() {
4719
4814
  return;
4720
4815
  }
4721
4816
  console.log(`
4722
- ${import_picocolors7.default.bold(ad.name)}
4817
+ ${import_picocolors8.default.bold(ad.name)}
4723
4818
  `);
4724
- console.log(` ${import_picocolors7.default.bold("ID:")} ${ad.id}`);
4725
- console.log(` ${import_picocolors7.default.bold("Status:")} ${statusColor(ad.status)}`);
4726
- console.log(` ${import_picocolors7.default.bold("Campaign:")} ${ad.campaignId}`);
4727
- console.log(` ${import_picocolors7.default.bold("Ad Set:")} ${ad.adSetId}`);
4819
+ console.log(` ${import_picocolors8.default.bold("ID:")} ${ad.id}`);
4820
+ console.log(` ${import_picocolors8.default.bold("Status:")} ${statusColor(ad.status)}`);
4821
+ console.log(` ${import_picocolors8.default.bold("Campaign:")} ${ad.campaignId}`);
4822
+ console.log(` ${import_picocolors8.default.bold("Ad Set:")} ${ad.adSetId}`);
4728
4823
  if (ad.creative) {
4729
- const label = (name) => import_picocolors7.default.bold(name.padEnd(15));
4824
+ const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
4825
+ const exp = !!opts.expanded;
4730
4826
  console.log("");
4731
- 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(" | ")}`);
4827
+ console.log(` ${import_picocolors8.default.bold("Creative")}`);
4828
+ if (ad.creative.headline) console.log(` ${label("Headline:")}${truncate(ad.creative.headline, 120, exp)}`);
4829
+ if (ad.creative.headlines) console.log(` ${label("Headlines:")}${ad.creative.headlines.map((h2) => truncate(h2, 60, exp)).join(" | ")}`);
4830
+ if (ad.creative.primaryText) console.log(` ${label("Primary Text:")}${truncate(ad.creative.primaryText, 120, exp)}`);
4831
+ if (ad.creative.primaryTexts) console.log(` ${label("Primary Texts:")}${ad.creative.primaryTexts.map((t) => truncate(t, 60, exp)).join(" | ")}`);
4832
+ if (ad.creative.description) console.log(` ${label("Description:")}${truncate(ad.creative.description, 120, exp)}`);
4833
+ if (ad.creative.descriptions) console.log(` ${label("Descriptions:")}${ad.creative.descriptions.map((d3) => truncate(d3, 60, exp)).join(" | ")}`);
4738
4834
  if (ad.creative.cta) console.log(` ${label("CTA:")}${ad.creative.cta}`);
4739
4835
  if (ad.creative.link) console.log(` ${label("Link:")}${ad.creative.link}`);
4740
4836
  if (ad.creative.displayUrl) console.log(` ${label("Display URL:")}${ad.creative.displayUrl}`);
@@ -4742,13 +4838,13 @@ function adCommand() {
4742
4838
  if (ad.creative.enhancements) console.log(` ${label("Enhancements:")}${ad.creative.enhancements.join(", ")}`);
4743
4839
  }
4744
4840
  console.log(`
4745
- ${import_picocolors7.default.dim("Use this ad as a template:")} ads create --copy-from ${ad.id}
4841
+ ${import_picocolors8.default.dim("Use this ad as a template:")} ads create --copy-from ${ad.id}
4746
4842
  `);
4747
4843
  });
4748
4844
  }
4749
4845
 
4750
4846
  // src/commands/presets.js
4751
- var import_picocolors8 = __toESM(require_picocolors(), 1);
4847
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
4752
4848
  function presetsCommand() {
4753
4849
  return new Command("presets").description("List saved ad template presets").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").argument("[id]", "Preset ID to show details").action(async (id, opts) => {
4754
4850
  const client = createClient({ accountId: opts.account });
@@ -4759,12 +4855,12 @@ function presetsCommand() {
4759
4855
  return;
4760
4856
  }
4761
4857
  console.log(`
4762
- ${import_picocolors8.default.bold(preset.name)}
4858
+ ${import_picocolors9.default.bold(preset.name)}
4763
4859
  `);
4764
4860
  if (preset.config) {
4765
- console.log(` ${import_picocolors8.default.bold("Campaign:")} ${preset.config.campaign?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.campaign?.id || "")}`);
4766
- console.log(` ${import_picocolors8.default.bold("Ad Set:")} ${preset.config.adSet?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.adSet?.id || "")}`);
4767
- console.log(` ${import_picocolors8.default.bold("Ad:")} ${preset.config.ad?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.ad?.id || "")}`);
4861
+ console.log(` ${import_picocolors9.default.bold("Campaign:")} ${preset.config.campaign?.name || import_picocolors9.default.dim("\u2014")} ${import_picocolors9.default.dim(preset.config.campaign?.id || "")}`);
4862
+ console.log(` ${import_picocolors9.default.bold("Ad Set:")} ${preset.config.adSet?.name || import_picocolors9.default.dim("\u2014")} ${import_picocolors9.default.dim(preset.config.adSet?.id || "")}`);
4863
+ console.log(` ${import_picocolors9.default.bold("Ad:")} ${preset.config.ad?.name || import_picocolors9.default.dim("\u2014")} ${import_picocolors9.default.dim(preset.config.ad?.id || "")}`);
4768
4864
  }
4769
4865
  console.log("");
4770
4866
  return;
@@ -4776,15 +4872,15 @@ function presetsCommand() {
4776
4872
  return;
4777
4873
  }
4778
4874
  console.log(`
4779
- ${import_picocolors8.default.bold("Ad Template Presets")} (${all.length})
4875
+ ${import_picocolors9.default.bold("Ad Template Presets")} (${all.length})
4780
4876
  `);
4781
4877
  if (all.length === 0) {
4782
- console.log(import_picocolors8.default.dim(" No presets. Create one in the web app by saving an ad configuration."));
4878
+ console.log(import_picocolors9.default.dim(" No presets. Create one in the web app by saving an ad configuration."));
4783
4879
  } else {
4784
4880
  printTable(all, [
4785
4881
  { key: "id", label: "ID", maxWidth: 28 },
4786
4882
  { key: "name", label: "Name", maxWidth: 40 },
4787
- { key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors8.default.cyan("team") : import_picocolors8.default.dim("\u2014") }
4883
+ { key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors9.default.cyan("team") : import_picocolors9.default.dim("\u2014") }
4788
4884
  ]);
4789
4885
  }
4790
4886
  console.log("");
@@ -4800,13 +4896,13 @@ function textPresetsCommand() {
4800
4896
  return;
4801
4897
  }
4802
4898
  console.log(`
4803
- ${import_picocolors8.default.bold(preset.name)}
4899
+ ${import_picocolors9.default.bold(preset.name)}
4804
4900
  `);
4805
- console.log(` ${import_picocolors8.default.bold("Includes:")} ${preset.includedFields?.join(", ") || "all fields"}`);
4901
+ console.log(` ${import_picocolors9.default.bold("Includes:")} ${preset.includedFields?.join(", ") || "all fields"}`);
4806
4902
  if (preset.text) {
4807
- if (preset.text.titles?.length) console.log(` ${import_picocolors8.default.bold("Headlines:")} ${preset.text.titles.join(" | ")}`);
4808
- if (preset.text.bodies?.length) console.log(` ${import_picocolors8.default.bold("Bodies:")} ${preset.text.bodies.join(" | ")}`);
4809
- if (preset.text.descriptions?.length) console.log(` ${import_picocolors8.default.bold("Desc:")} ${preset.text.descriptions.join(" | ")}`);
4903
+ if (preset.text.titles?.length) console.log(` ${import_picocolors9.default.bold("Headlines:")} ${preset.text.titles.join(" | ")}`);
4904
+ if (preset.text.bodies?.length) console.log(` ${import_picocolors9.default.bold("Bodies:")} ${preset.text.bodies.join(" | ")}`);
4905
+ if (preset.text.descriptions?.length) console.log(` ${import_picocolors9.default.bold("Desc:")} ${preset.text.descriptions.join(" | ")}`);
4810
4906
  }
4811
4907
  console.log("");
4812
4908
  return;
@@ -4818,29 +4914,78 @@ function textPresetsCommand() {
4818
4914
  return;
4819
4915
  }
4820
4916
  console.log(`
4821
- ${import_picocolors8.default.bold("Text Presets")} (${all.length})
4917
+ ${import_picocolors9.default.bold("Text Presets")} (${all.length})
4822
4918
  `);
4823
4919
  if (all.length === 0) {
4824
- console.log(import_picocolors8.default.dim(" No text presets. Create one in the web app."));
4920
+ console.log(import_picocolors9.default.dim(" No text presets. Create one in the web app."));
4825
4921
  } else {
4826
4922
  printTable(all, [
4827
4923
  { key: "id", label: "ID", maxWidth: 28 },
4828
4924
  { key: "name", label: "Name", maxWidth: 30 },
4829
- { key: "scope", label: "Scope", color: (v2) => v2 === "global" ? import_picocolors8.default.cyan("global") : import_picocolors8.default.dim("account") },
4830
- { key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors8.default.cyan("team") : import_picocolors8.default.dim("\u2014") }
4925
+ { key: "scope", label: "Scope", color: (v2) => v2 === "global" ? import_picocolors9.default.cyan("global") : import_picocolors9.default.dim("account") },
4926
+ { key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors9.default.cyan("team") : import_picocolors9.default.dim("\u2014") }
4831
4927
  ]);
4832
4928
  }
4833
4929
  console.log("");
4834
4930
  });
4835
4931
  }
4932
+ function presetsSaveCommand() {
4933
+ 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) => {
4934
+ const client = createClient({ accountId: opts.account });
4935
+ if (!opts.fromAd) {
4936
+ printError("--from-ad <adId> is required.");
4937
+ process.exit(1);
4938
+ }
4939
+ let name = opts.name;
4940
+ if (!name && isInteractive()) {
4941
+ name = await he({
4942
+ message: "Preset name",
4943
+ placeholder: "e.g. Spring 2026 - Standard Purchase",
4944
+ validate: (v2) => v2?.trim() ? void 0 : "Name is required"
4945
+ });
4946
+ if (pD(name)) process.exit(0);
4947
+ }
4948
+ if (!name?.trim()) {
4949
+ printError("--name <name> is required (or run in an interactive TTY to be prompted).");
4950
+ process.exit(1);
4951
+ }
4952
+ try {
4953
+ const { preset } = await client.presets.create({
4954
+ type: "api",
4955
+ fromAdId: opts.fromAd,
4956
+ name: name.trim(),
4957
+ shareWithTeam: !!opts.share
4958
+ });
4959
+ if (shouldOutputJson(opts)) {
4960
+ printJson(preset);
4961
+ return;
4962
+ }
4963
+ printSuccess(`Saved preset "${preset.name}"`);
4964
+ console.log(` ${import_picocolors9.default.bold("ID:")} ${preset.id}`);
4965
+ console.log(` ${import_picocolors9.default.bold("Type:")} ${preset.type}`);
4966
+ if (preset.shared) console.log(` ${import_picocolors9.default.bold("Shared:")} ${import_picocolors9.default.cyan("team")}`);
4967
+ console.log(`
4968
+ ${import_picocolors9.default.dim("Use it:")} ads create --preset ${preset.id} ...
4969
+ `);
4970
+ } catch (err) {
4971
+ printError(err.message || "Failed to save preset");
4972
+ process.exit(1);
4973
+ }
4974
+ });
4975
+ }
4836
4976
 
4837
4977
  // src/commands/upload.js
4838
4978
  var import_fs2 = __toESM(require("fs"), 1);
4839
4979
  var import_path2 = __toESM(require("path"), 1);
4840
- var import_picocolors9 = __toESM(require_picocolors(), 1);
4980
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
4841
4981
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]);
4842
4982
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]);
4843
4983
  var MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024;
4984
+ var DEFAULT_UPLOAD_CONCURRENCY = 4;
4985
+ var MAX_UPLOAD_CONCURRENCY = 6;
4986
+ var DEFAULT_UPLOAD_TIMEOUT_MS = 12e4;
4987
+ var DEFAULT_R2_MAX_ATTEMPTS = 4;
4988
+ var DEFAULT_MAX_IN_FLIGHT_BYTES = 512 * 1024 * 1024;
4844
4989
  var MAGIC_BYTES = {
4845
4990
  "image/jpeg": [[255, 216, 255]],
4846
4991
  "image/png": [[137, 80, 78, 71]],
@@ -4904,6 +5049,75 @@ var CONTENT_TYPES = {
4904
5049
  ".webm": "video/webm",
4905
5050
  ".m4v": "video/x-m4v"
4906
5051
  };
5052
+ function parsePositiveInt2(value, fallback) {
5053
+ const parsed = Number.parseInt(value, 10);
5054
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
5055
+ }
5056
+ function normalizeConcurrency(value) {
5057
+ return Math.min(MAX_UPLOAD_CONCURRENCY, parsePositiveInt2(value, DEFAULT_UPLOAD_CONCURRENCY));
5058
+ }
5059
+ function calculateUploadConcurrency(files, requestedConcurrency, maxInFlightBytes = DEFAULT_MAX_IN_FLIGHT_BYTES) {
5060
+ const requested = normalizeConcurrency(requestedConcurrency);
5061
+ const largestFileSize = Math.max(0, ...files.map((file) => Number(file.size) || 0));
5062
+ if (largestFileSize <= 0) return requested;
5063
+ const memoryLimited = Math.max(1, Math.floor(maxInFlightBytes / largestFileSize));
5064
+ return Math.min(requested, memoryLimited);
5065
+ }
5066
+ function sleep2(ms) {
5067
+ return new Promise((resolve) => setTimeout(resolve, ms));
5068
+ }
5069
+ async function runBoundedConcurrency(items, concurrency, worker) {
5070
+ const limit = Math.max(1, Math.min(items.length || 1, concurrency));
5071
+ const results = new Array(items.length);
5072
+ let nextIndex = 0;
5073
+ async function runNext() {
5074
+ while (nextIndex < items.length) {
5075
+ const index = nextIndex++;
5076
+ results[index] = await worker(items[index], index);
5077
+ }
5078
+ }
5079
+ await Promise.all(Array.from({ length: limit }, runNext));
5080
+ return results;
5081
+ }
5082
+ async function putR2WithRetry(uploadUrl, fileBuffer, contentType, {
5083
+ fetchImpl = fetch,
5084
+ timeoutMs = DEFAULT_UPLOAD_TIMEOUT_MS,
5085
+ maxAttempts = DEFAULT_R2_MAX_ATTEMPTS,
5086
+ delay = sleep2
5087
+ } = {}) {
5088
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
5089
+ const controller = new AbortController();
5090
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
5091
+ try {
5092
+ const resp = await fetchImpl(uploadUrl, {
5093
+ method: "PUT",
5094
+ headers: {
5095
+ "Content-Type": contentType,
5096
+ "Content-Length": String(fileBuffer.length)
5097
+ },
5098
+ body: fileBuffer,
5099
+ signal: controller.signal
5100
+ });
5101
+ clearTimeout(timeout);
5102
+ if (resp.ok) return resp;
5103
+ if (isRetryableStatus(resp.status) && attempt < maxAttempts) {
5104
+ const retryAfter = resp.status === 429 ? parsePositiveInt2(resp.headers.get("retry-after"), null) : null;
5105
+ await delay(getRetryDelayMs(attempt, retryAfter));
5106
+ continue;
5107
+ }
5108
+ throw new Error(`R2 upload failed: ${resp.status}`);
5109
+ } catch (err) {
5110
+ clearTimeout(timeout);
5111
+ const normalizedError = err?.name === "AbortError" ? new Error(`R2 upload timed out after ${Math.ceil(timeoutMs / 1e3)}s`) : err;
5112
+ if (attempt < maxAttempts && isRetryableNetworkError(normalizedError)) {
5113
+ await delay(getRetryDelayMs(attempt));
5114
+ continue;
5115
+ }
5116
+ throw normalizedError;
5117
+ }
5118
+ }
5119
+ throw new Error(`R2 upload failed after ${maxAttempts} attempt(s)`);
5120
+ }
4907
5121
  function getFileType(filePath) {
4908
5122
  const ext = import_path2.default.extname(filePath).toLowerCase();
4909
5123
  if (IMAGE_EXTS.has(ext)) return "image";
@@ -4948,13 +5162,17 @@ function resolveFiles(inputs) {
4948
5162
  }
4949
5163
  }
4950
5164
  if (skipped.length > 0) {
4951
- console.error(import_picocolors9.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
5165
+ console.error(import_picocolors10.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
4952
5166
  }
4953
5167
  return files;
4954
5168
  }
4955
5169
  async function stageFiles(paths, opts) {
4956
- const client = createClient({ accountId: opts.account });
5170
+ const client = createClient({
5171
+ accountId: opts.account,
5172
+ timeoutMs: parsePositiveInt2(opts.apiTimeout, void 0)
5173
+ });
4957
5174
  const jsonMode = shouldOutputJson(opts);
5175
+ const uploadTimeoutMs = parsePositiveInt2(opts.uploadTimeout, DEFAULT_UPLOAD_TIMEOUT_MS);
4958
5176
  let files;
4959
5177
  try {
4960
5178
  files = resolveFiles(paths);
@@ -4968,7 +5186,7 @@ async function stageFiles(paths, opts) {
4968
5186
  }
4969
5187
  if (!jsonMode) {
4970
5188
  console.log(`
4971
- ${import_picocolors9.default.bold("Uploading")} ${files.length} file(s)
5189
+ ${import_picocolors10.default.bold("Uploading")} ${files.length} file(s)
4972
5190
  `);
4973
5191
  }
4974
5192
  const filesMeta = files.map((f) => ({
@@ -4979,39 +5197,33 @@ async function stageFiles(paths, opts) {
4979
5197
  }));
4980
5198
  const initResult = await client.uploads.init(filesMeta);
4981
5199
  const { batchId } = initResult;
4982
- const uploadedFiles = [];
4983
- for (let i = 0; i < files.length; i++) {
4984
- const file = files[i];
5200
+ const concurrency = calculateUploadConcurrency(files, opts.concurrency);
5201
+ const uploadedFiles = await runBoundedConcurrency(files, concurrency, async (file, i) => {
4985
5202
  const r2Info = initResult.files[i];
4986
5203
  if (!jsonMode) {
4987
- console.log(` ${import_picocolors9.default.dim("\u2192")} ${import_picocolors9.default.dim(`Uploading ${file.name}... ${formatSize(file.size)}`)}`);
5204
+ console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(`Uploading ${file.name}... ${formatSize(file.size)}`)}`);
4988
5205
  }
4989
5206
  try {
4990
5207
  const fileBuffer = import_fs2.default.readFileSync(file.path);
4991
- const resp = await fetch(r2Info.uploadUrl, {
4992
- method: "PUT",
4993
- headers: {
4994
- "Content-Type": file.contentType || (file.type === "video" ? "video/mp4" : "image/jpeg"),
4995
- "Content-Length": String(fileBuffer.length)
4996
- },
4997
- body: fileBuffer
4998
- });
4999
- if (!resp.ok) {
5000
- throw new Error(`R2 upload failed: ${resp.status}`);
5001
- }
5002
- uploadedFiles.push({
5208
+ await putR2WithRetry(
5209
+ r2Info.uploadUrl,
5210
+ fileBuffer,
5211
+ file.contentType || (file.type === "video" ? "video/mp4" : "image/jpeg"),
5212
+ { timeoutMs: uploadTimeoutMs }
5213
+ );
5214
+ return {
5003
5215
  name: file.name,
5004
5216
  type: file.type,
5005
5217
  size: file.size,
5006
5218
  fileKey: r2Info.fileKey
5007
- });
5219
+ };
5008
5220
  } catch (err) {
5009
5221
  if (!jsonMode) {
5010
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
5222
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
5011
5223
  }
5012
- uploadedFiles.push({ name: file.name, type: file.type, fileKey: null, error: err.message });
5224
+ return { name: file.name, type: file.type, fileKey: null, error: err.message };
5013
5225
  }
5014
- }
5226
+ });
5015
5227
  return { client, batchId, uploadedFiles, jsonMode };
5016
5228
  }
5017
5229
  async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
@@ -5021,7 +5233,7 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5021
5233
  process.exit(1);
5022
5234
  }
5023
5235
  if (!jsonMode) {
5024
- console.log(` ${import_picocolors9.default.dim("\u2192")} ${import_picocolors9.default.dim("Finalizing uploads to Facebook...")}`);
5236
+ console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim("Finalizing uploads to Facebook...")}`);
5025
5237
  }
5026
5238
  const results = [];
5027
5239
  let completed = 0;
@@ -5039,19 +5251,19 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5039
5251
  completed++;
5040
5252
  if (!jsonMode) {
5041
5253
  const id = result.videoId || result.mediaHash || "";
5042
- console.log(` ${import_picocolors9.default.green("\u2713")} ${file.name} uploaded successfully ${import_picocolors9.default.dim(id)}`);
5254
+ console.log(` ${import_picocolors10.default.green("\u2713")} ${file.name} uploaded successfully ${import_picocolors10.default.dim(id)}`);
5043
5255
  }
5044
5256
  } else {
5045
5257
  failed++;
5046
5258
  if (!jsonMode) {
5047
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${result.error}`);
5259
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${result.error}`);
5048
5260
  }
5049
5261
  }
5050
5262
  } catch (err) {
5051
5263
  failed++;
5052
5264
  results.push({ name: file.name, type: file.type, status: "error", error: err.message });
5053
5265
  if (!jsonMode) {
5054
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
5266
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
5055
5267
  }
5056
5268
  }
5057
5269
  }
@@ -5069,30 +5281,30 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5069
5281
  }
5070
5282
  if (groups.length > 0) {
5071
5283
  console.log(`
5072
- ${import_picocolors9.default.bold("Variant Groups")} (${groups.length})
5284
+ ${import_picocolors10.default.bold("Variant Groups")} (${groups.length})
5073
5285
  `);
5074
5286
  for (const group of groups) {
5075
5287
  const assets = group.assets.map((a) => {
5076
- const placement = a.placementType === "default" ? "" : import_picocolors9.default.cyan(` [${a.placementType}]`);
5288
+ const placement = a.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${a.placementType}]`);
5077
5289
  return `${a.name}${placement}`;
5078
5290
  });
5079
- console.log(` ${import_picocolors9.default.bold(group.baseName || group.groupId)}`);
5291
+ console.log(` ${import_picocolors10.default.bold(group.baseName || group.groupId)}`);
5080
5292
  for (const asset of assets) {
5081
5293
  console.log(` ${asset}`);
5082
5294
  }
5083
5295
  }
5084
5296
  }
5085
5297
  console.log(`
5086
- ${import_picocolors9.default.bold("Batch:")} ${batchId}`);
5298
+ ${import_picocolors10.default.bold("Batch:")} ${batchId}`);
5087
5299
  const countStr = `${completed} complete, ${failed} failed`;
5088
5300
  console.log(` ${countStr}`);
5089
5301
  if (groups.length > 0) {
5090
- console.log(` ${import_picocolors9.default.bold("Groups:")} ${groups.length} variant group(s) detected`);
5302
+ console.log(` ${import_picocolors10.default.bold("Groups:")} ${groups.length} variant group(s) detected`);
5091
5303
  }
5092
5304
  console.log("");
5093
5305
  }
5094
5306
  function uploadCommand() {
5095
- return new Command("upload").description("Upload media files and process to Meta").argument("<paths...>", "Files or directories to upload").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").action(async (paths, opts) => {
5307
+ return new Command("upload").description("Upload media files and process to Meta").argument("<paths...>", "Files or directories to upload").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").option("--concurrency <n>", "Concurrent R2 staging uploads (1-6)", String(DEFAULT_UPLOAD_CONCURRENCY)).option("--upload-timeout <ms>", "R2 PUT timeout in milliseconds", String(DEFAULT_UPLOAD_TIMEOUT_MS)).option("--api-timeout <ms>", "API request timeout in milliseconds").action(async (paths, opts) => {
5096
5308
  const { client, batchId, uploadedFiles, jsonMode } = await stageFiles(paths, opts);
5097
5309
  await processAndPrint(client, batchId, uploadedFiles, jsonMode);
5098
5310
  });
@@ -5109,10 +5321,10 @@ function uploadsCommand() {
5109
5321
  }
5110
5322
  const accountLabel = batch.accountName ? `${batch.accountName} (${batch.accountId})` : batch.accountId;
5111
5323
  console.log(`
5112
- ${import_picocolors9.default.bold("Batch")} ${batch.batchId}
5324
+ ${import_picocolors10.default.bold("Batch")} ${batch.batchId}
5113
5325
  `);
5114
- console.log(` ${import_picocolors9.default.bold("Account:")} ${accountLabel}`);
5115
- console.log(` ${import_picocolors9.default.bold("Files:")} ${batch.total}
5326
+ console.log(` ${import_picocolors10.default.bold("Account:")} ${accountLabel}`);
5327
+ console.log(` ${import_picocolors10.default.bold("Files:")} ${batch.total}
5116
5328
  `);
5117
5329
  const rows = batch.files.map((f) => ({
5118
5330
  ...f,
@@ -5121,19 +5333,19 @@ function uploadsCommand() {
5121
5333
  printTable(rows, [
5122
5334
  { key: "name", label: "Name", maxWidth: 35 },
5123
5335
  { key: "type", label: "Type", maxWidth: 6 },
5124
- { key: "id", label: "Hash / Video ID", maxWidth: 35, color: (v2) => v2 || import_picocolors9.default.dim("\u2014") }
5336
+ { key: "id", label: "Hash / Video ID", maxWidth: 35, color: (v2) => v2 || import_picocolors10.default.dim("\u2014") }
5125
5337
  ]);
5126
5338
  if (batch.groups?.length > 0) {
5127
5339
  console.log(`
5128
- ${import_picocolors9.default.bold("Variant Groups")} (${batch.groups.length})
5340
+ ${import_picocolors10.default.bold("Variant Groups")} (${batch.groups.length})
5129
5341
  `);
5130
5342
  for (const group of batch.groups) {
5131
5343
  const primaryAsset = group.assets.find((a) => a.isPrimary);
5132
5344
  const groupLabel = group.baseName || primaryAsset?.name || group.groupId;
5133
- console.log(` ${import_picocolors9.default.bold(groupLabel)}`);
5345
+ console.log(` ${import_picocolors10.default.bold(groupLabel)}`);
5134
5346
  for (const asset of group.assets) {
5135
- const placement = asset.placementType === "default" ? "" : import_picocolors9.default.cyan(` [${asset.placementType}]`);
5136
- const primary = asset.isPrimary ? import_picocolors9.default.dim(" (primary)") : "";
5347
+ const placement = asset.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${asset.placementType}]`);
5348
+ const primary = asset.isPrimary ? import_picocolors10.default.dim(" (primary)") : "";
5137
5349
  console.log(` ${asset.name}${placement}${primary}`);
5138
5350
  }
5139
5351
  }
@@ -5147,10 +5359,10 @@ function uploadsCommand() {
5147
5359
  return;
5148
5360
  }
5149
5361
  console.log(`
5150
- ${import_picocolors9.default.bold("Recent Uploads")} (${batches.length})
5362
+ ${import_picocolors10.default.bold("Recent Uploads")} (${batches.length})
5151
5363
  `);
5152
5364
  if (batches.length === 0) {
5153
- console.log(import_picocolors9.default.dim(" No uploads found for this account.\n"));
5365
+ console.log(import_picocolors10.default.dim(" No uploads found for this account.\n"));
5154
5366
  return;
5155
5367
  }
5156
5368
  const formatDate = (d3) => {
@@ -5179,10 +5391,10 @@ function uploadsCommand() {
5179
5391
 
5180
5392
  // src/commands/create.js
5181
5393
  var import_fs3 = __toESM(require("fs"), 1);
5182
- var import_picocolors11 = __toESM(require_picocolors(), 1);
5394
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
5183
5395
 
5184
5396
  // src/lib/poll.js
5185
- var import_picocolors10 = __toESM(require_picocolors(), 1);
5397
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
5186
5398
  var POLL_INTERVAL = 500;
5187
5399
  var TIMEOUT_MS = 30 * 60 * 1e3;
5188
5400
  var TIMEOUT_MINUTES = TIMEOUT_MS / 6e4;
@@ -5194,7 +5406,7 @@ async function pollJob(client, jobId, opts = {}) {
5194
5406
  if (!jsonMode) {
5195
5407
  if (tty) {
5196
5408
  console.log(`
5197
- ${import_picocolors10.default.bold("Creating ads")}
5409
+ ${import_picocolors11.default.bold("Creating ads")}
5198
5410
  `);
5199
5411
  } else {
5200
5412
  console.log(`[start] Creating ads \u2014 ${jobId}`);
@@ -5220,7 +5432,7 @@ async function pollJob(client, jobId, opts = {}) {
5220
5432
  process.exitCode = 2;
5221
5433
  return { status: "still_running", jobId };
5222
5434
  }
5223
- await sleep(POLL_INTERVAL);
5435
+ await sleep3(POLL_INTERVAL);
5224
5436
  continue;
5225
5437
  }
5226
5438
  const ops = data.progress?.operations || [];
@@ -5231,7 +5443,7 @@ async function pollJob(client, jobId, opts = {}) {
5231
5443
  const time = formatTimestamp(op.timestamp);
5232
5444
  const { icon, color } = opStyle(op.type);
5233
5445
  if (tty) {
5234
- console.log(` ${import_picocolors10.default.dim(time)} ${color(icon)} ${color(op.message)}`);
5446
+ console.log(` ${import_picocolors11.default.dim(time)} ${color(icon)} ${color(op.message)}`);
5235
5447
  } else {
5236
5448
  const tag = op.type === "error" ? "[err]" : op.type === "retry" ? "[..]" : op.type === "completion" || (op.type || "").endsWith("_complete") ? "[ok ]" : "[..]";
5237
5449
  console.log(`${time} ${tag} ${op.message}`);
@@ -5252,7 +5464,7 @@ async function pollJob(client, jobId, opts = {}) {
5252
5464
  const failed = data.result?.created?.failedAds?.length || 0;
5253
5465
  console.log("");
5254
5466
  if (failed > 0 && adCount > 0) {
5255
- console.log(` ${import_picocolors10.default.yellow("\u2212")} ${adCount} ads created, ${failed} failed in ${elapsed}`);
5467
+ console.log(` ${import_picocolors11.default.yellow("\u2212")} ${adCount} ads created, ${failed} failed in ${elapsed}`);
5256
5468
  } else if (adCount > 0) {
5257
5469
  printSuccess(`Created ${adCount} ads in ${elapsed}`);
5258
5470
  } else {
@@ -5267,14 +5479,14 @@ async function pollJob(client, jobId, opts = {}) {
5267
5479
  console.log(JSON.stringify({ event: "still_running", jobId }));
5268
5480
  } else {
5269
5481
  console.log("");
5270
- console.log(` ${import_picocolors10.default.yellow("\u2212")} ${import_picocolors10.default.yellow(`Still running after ${TIMEOUT_MINUTES} min \u2014 job continues on the server.`)}`);
5271
- console.log(` Resume with: ${import_picocolors10.default.bold(`ads jobs ${jobId} --follow`)}`);
5482
+ console.log(` ${import_picocolors11.default.yellow("\u2212")} ${import_picocolors11.default.yellow(`Still running after ${TIMEOUT_MINUTES} min \u2014 job continues on the server.`)}`);
5483
+ console.log(` Resume with: ${import_picocolors11.default.bold(`ads jobs ${jobId} --follow`)}`);
5272
5484
  console.log("");
5273
5485
  }
5274
5486
  process.exitCode = 2;
5275
5487
  return { status: "still_running", jobId };
5276
5488
  }
5277
- await sleep(POLL_INTERVAL);
5489
+ await sleep3(POLL_INTERVAL);
5278
5490
  }
5279
5491
  }
5280
5492
  function opStyle(type) {
@@ -5282,13 +5494,13 @@ function opStyle(type) {
5282
5494
  case "completion":
5283
5495
  case "campaign_complete":
5284
5496
  case "adset_complete":
5285
- return { icon: "\u2713", color: import_picocolors10.default.green };
5497
+ return { icon: "\u2713", color: import_picocolors11.default.green };
5286
5498
  case "error":
5287
- return { icon: "\u2717", color: import_picocolors10.default.red };
5499
+ return { icon: "\u2717", color: import_picocolors11.default.red };
5288
5500
  case "retry":
5289
- return { icon: "\u2212", color: import_picocolors10.default.yellow };
5501
+ return { icon: "\u2212", color: import_picocolors11.default.yellow };
5290
5502
  default:
5291
- return { icon: "\u2192", color: import_picocolors10.default.dim };
5503
+ return { icon: "\u2192", color: import_picocolors11.default.dim };
5292
5504
  }
5293
5505
  }
5294
5506
  function formatTimestamp(iso) {
@@ -5300,7 +5512,7 @@ function formatTimestamp(iso) {
5300
5512
  return " ";
5301
5513
  }
5302
5514
  }
5303
- function sleep(ms) {
5515
+ function sleep3(ms) {
5304
5516
  return new Promise((r) => setTimeout(r, ms));
5305
5517
  }
5306
5518
 
@@ -5324,47 +5536,29 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5324
5536
  const ts = result.totalAdSets || 1;
5325
5537
  const ta = result.totalAds || result.ads?.length || 0;
5326
5538
  const parts = [];
5327
- if (tc > 1) parts.push(`${import_picocolors11.default.blue(tc)} campaigns`);
5328
- parts.push(`${import_picocolors11.default.blue(ts)} ad set${ts !== 1 ? "s" : ""}`);
5329
- parts.push(`${import_picocolors11.default.blue(ta)} ad${ta !== 1 ? "s" : ""}`);
5539
+ if (tc > 1) parts.push(`${import_picocolors12.default.blue(tc)} campaigns`);
5540
+ parts.push(`${import_picocolors12.default.blue(ts)} ad set${ts !== 1 ? "s" : ""}`);
5541
+ parts.push(`${import_picocolors12.default.blue(ta)} ad${ta !== 1 ? "s" : ""}`);
5330
5542
  let statusStr = statusColor(result.status || "PAUSED");
5331
5543
  if (result.status === "PAUSED" && result.pauseAt && result.pauseAt !== "ad") {
5332
5544
  const levelLabel = result.pauseAt === "campaign" ? "campaign level" : "ad set level";
5333
- statusStr += import_picocolors11.default.dim(` (${levelLabel})`);
5545
+ statusStr += import_picocolors12.default.dim(` (${levelLabel})`);
5334
5546
  }
5335
5547
  let enhLabel = "";
5336
- if (result.enhancements === "metaDefaults") enhLabel = import_picocolors11.default.dim("All Off");
5337
- else if (result.enhancements === "all") enhLabel = import_picocolors11.default.dim("All On");
5338
- else if (result.enhancements === "none") enhLabel = import_picocolors11.default.dim("All Off");
5339
- else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors11.default.dim(`${result.enhancements.length} custom`);
5340
- const enhPart = enhLabel ? ` ${import_picocolors11.default.dim("\xB7")} ${enhLabel}` : "";
5341
- console.log(` ${parts.join(", ")} ${import_picocolors11.default.dim("\xB7")} ${statusStr}${enhPart}
5548
+ if (result.enhancements === "metaDefaults") enhLabel = import_picocolors12.default.dim("All Off");
5549
+ else if (result.enhancements === "all") enhLabel = import_picocolors12.default.dim("All On");
5550
+ else if (result.enhancements === "none") enhLabel = import_picocolors12.default.dim("All Off");
5551
+ else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors12.default.dim(`${result.enhancements.length} custom`);
5552
+ const enhPart = enhLabel ? ` ${import_picocolors12.default.dim("\xB7")} ${enhLabel}` : "";
5553
+ console.log(` ${parts.join(", ")} ${import_picocolors12.default.dim("\xB7")} ${statusStr}${enhPart}
5342
5554
  `);
5343
5555
  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
5556
  const tableRows = [];
5363
5557
  let lastCampaign = null;
5364
5558
  let lastAdSet = null;
5365
5559
  for (const ad of result.ads) {
5366
- const campaignName = ad.campaignName || import_picocolors11.default.dim("\u2014");
5367
- const adSetName = ad.adSetName || import_picocolors11.default.dim("\u2014");
5560
+ const campaignName = ad.campaignName || import_picocolors12.default.dim("\u2014");
5561
+ const adSetName = ad.adSetName || import_picocolors12.default.dim("\u2014");
5368
5562
  tableRows.push({
5369
5563
  campaign: campaignName === lastCampaign ? "" : campaignName,
5370
5564
  adSet: campaignName === lastCampaign && adSetName === lastAdSet ? "" : adSetName,
@@ -5380,6 +5574,24 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5380
5574
  ]);
5381
5575
  console.log("");
5382
5576
  const termWidth = process.stdout.columns || 80;
5577
+ const wrapLine = (text, startCol) => {
5578
+ const width = Math.max(termWidth - startCol, 20);
5579
+ if (text.length <= width) return text;
5580
+ const pad = " ".repeat(startCol);
5581
+ const lines = [];
5582
+ let remaining = text;
5583
+ while (remaining.length > 0) {
5584
+ if (remaining.length <= width) {
5585
+ lines.push(remaining);
5586
+ break;
5587
+ }
5588
+ let breakAt = remaining.lastIndexOf(" ", width);
5589
+ if (breakAt <= 0) breakAt = width;
5590
+ lines.push(remaining.slice(0, breakAt));
5591
+ remaining = remaining.slice(breakAt + 1);
5592
+ }
5593
+ return lines.join("\n" + pad);
5594
+ };
5383
5595
  const allLinks = result.ads.map((a) => a.link).filter(Boolean);
5384
5596
  const allUrlTags = result.ads.map((a) => a.urlTags).filter(Boolean);
5385
5597
  const allCtas = result.ads.map((a) => a.cta).filter(Boolean);
@@ -5387,61 +5599,61 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5387
5599
  const sharedUrlTags = allUrlTags.length > 0 && allUrlTags.every((t) => t === allUrlTags[0]) ? allUrlTags[0] : null;
5388
5600
  const sharedCta = allCtas.length > 0 && allCtas.every((c) => c === allCtas[0]) ? allCtas[0] : null;
5389
5601
  const maxText = 80;
5390
- const trunc = (s) => s.length > maxText ? s.slice(0, maxText - 1) + "\u2026" : s;
5602
+ const trunc = (s) => opts.expanded || s.length <= maxText ? s : s.slice(0, maxText - 1) + "\u2026";
5391
5603
  let lastDetailAdSet = null;
5392
5604
  const distinctAdSets = new Set(result.ads.map((a) => a.adSetName).filter(Boolean));
5393
5605
  const showAdSetHeaders = distinctAdSets.size > 1;
5394
5606
  for (const ad of result.ads) {
5395
5607
  if (showAdSetHeaders && ad.adSetName && ad.adSetName !== lastDetailAdSet) {
5396
5608
  if (lastDetailAdSet !== null) console.log("");
5397
- console.log(` ${import_picocolors11.default.dim("\u25B8 Ad Set:")} ${import_picocolors11.default.bold(ad.adSetName)}`);
5609
+ console.log(` ${import_picocolors12.default.dim("\u25B8 Ad Set:")} ${import_picocolors12.default.bold(ad.adSetName)}`);
5398
5610
  lastDetailAdSet = ad.adSetName;
5399
5611
  }
5400
- const typeLabel = ad.mediaType ? import_picocolors11.default.dim(` [${ad.mediaType}]`) : "";
5612
+ const typeLabel = ad.mediaType ? import_picocolors12.default.dim(` [${ad.mediaType}]`) : "";
5401
5613
  const indent = showAdSetHeaders ? " " : " ";
5402
5614
  const fieldIndent = showAdSetHeaders ? " " : " ";
5403
5615
  const fieldIndentLen = fieldIndent.length;
5404
- console.log(`${indent}${import_picocolors11.default.bold(import_picocolors11.default.blue(ad.name))}${typeLabel}`);
5616
+ console.log(`${indent}${import_picocolors12.default.bold(import_picocolors12.default.blue(ad.name))}${typeLabel}`);
5405
5617
  const headline = (ad.headline || []).join(" | ");
5406
5618
  const primary = (ad.primaryText || []).join(" | ");
5407
5619
  const desc = (ad.description || []).join(" | ");
5408
- if (headline) console.log(`${fieldIndent}${import_picocolors11.default.dim("Headline:")} ${trunc(headline)}`);
5409
- if (primary) console.log(`${fieldIndent}${import_picocolors11.default.dim("Primary Text:")} ${trunc(primary)}`);
5410
- if (desc) console.log(`${fieldIndent}${import_picocolors11.default.dim("Description:")} ${trunc(desc)}`);
5411
- if (ad.cta && !sharedCta) console.log(`${fieldIndent}${import_picocolors11.default.dim("CTA:")} ${ad.cta}`);
5412
- if (ad.link && !sharedLink) console.log(`${fieldIndent}${import_picocolors11.default.dim("Link:")} ${wrapLine(ad.link, fieldIndentLen)}`);
5413
- if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${import_picocolors11.default.dim("URL Tags:")} ${wrapLine(ad.urlTags, fieldIndentLen)}`);
5620
+ if (headline) console.log(`${fieldIndent}${import_picocolors12.default.dim("Headline:")} ${trunc(headline)}`);
5621
+ if (primary) console.log(`${fieldIndent}${import_picocolors12.default.dim("Primary Text:")} ${trunc(primary)}`);
5622
+ if (desc) console.log(`${fieldIndent}${import_picocolors12.default.dim("Description:")} ${trunc(desc)}`);
5623
+ if (ad.cta && !sharedCta) console.log(`${fieldIndent}${import_picocolors12.default.dim("CTA:")} ${ad.cta}`);
5624
+ if (ad.link && !sharedLink) console.log(`${fieldIndent}${import_picocolors12.default.dim("Link:")} ${wrapLine(ad.link, fieldIndentLen)}`);
5625
+ if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${import_picocolors12.default.dim("URL Tags:")} ${wrapLine(ad.urlTags, fieldIndentLen)}`);
5414
5626
  console.log("");
5415
5627
  }
5416
5628
  const valCol = 18;
5417
- if (sharedCta) console.log(` ${import_picocolors11.default.bold("CTA:")} ${sharedCta}`);
5418
- if (sharedLink) console.log(` ${import_picocolors11.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
5419
- if (sharedUrlTags) console.log(` ${import_picocolors11.default.bold("URL Tags:")} ${wrapLine(sharedUrlTags, valCol)}`);
5629
+ if (sharedCta) console.log(` ${import_picocolors12.default.bold("CTA:")} ${sharedCta}`);
5630
+ if (sharedLink) console.log(` ${import_picocolors12.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
5631
+ if (sharedUrlTags) console.log(` ${import_picocolors12.default.bold("URL Tags:")} ${wrapLine(sharedUrlTags, valCol)}`);
5420
5632
  const enh = result.enhancements;
5421
5633
  let enhDetail;
5422
5634
  if (enh === "metaDefaults") enhDetail = "Meta Defaults";
5423
5635
  else if (enh === "all") enhDetail = "All On";
5424
5636
  else if (enh === "none") enhDetail = "All Off";
5425
5637
  else if (Array.isArray(enh)) enhDetail = enh.join(", ");
5426
- if (enhDetail) console.log(` ${import_picocolors11.default.bold("Enhancements:")} ${wrapLine(enhDetail, valCol)}`);
5638
+ if (enhDetail) console.log(` ${import_picocolors12.default.bold("Enhancements:")} ${wrapLine(enhDetail, valCol)}`);
5427
5639
  if (result.budget) {
5428
5640
  const curr = result.budget.currency ? ` ${result.budget.currency}` : "";
5429
5641
  const renderLine = (label, amount) => {
5430
5642
  const pad = " ".repeat(Math.max(1, 16 - label.length - 1));
5431
- console.log(` ${import_picocolors11.default.bold(`${label}:`)}${pad}${amount}${curr}`);
5643
+ console.log(` ${import_picocolors12.default.bold(`${label}:`)}${pad}${amount}${curr}`);
5432
5644
  };
5433
5645
  if (result.budget.dailyBudget != null) renderLine("Daily Budget", result.budget.dailyBudget);
5434
5646
  if (result.budget.bidAmount != null) renderLine("Bid Amount", result.budget.bidAmount);
5435
5647
  }
5436
5648
  console.log("");
5437
5649
  } else if (result.plan?.totals) {
5438
- console.log(` ${import_picocolors11.default.bold("Plan:")}`);
5650
+ console.log(` ${import_picocolors12.default.bold("Plan:")}`);
5439
5651
  console.log(` Campaigns: ${result.plan.totals.campaigns}`);
5440
5652
  console.log(` Ad Sets: ${result.plan.totals.adSets}`);
5441
5653
  console.log(` Ads: ${result.plan.totals.ads}`);
5442
5654
  }
5443
5655
  console.log(`
5444
- ${import_picocolors11.default.dim("This is a preview. No ads were created.")}
5656
+ ${import_picocolors12.default.dim("This is a preview. No ads were created.")}
5445
5657
  `);
5446
5658
  return;
5447
5659
  }
@@ -5462,17 +5674,30 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5462
5674
  printSuccess(`${result.status || "Complete"}`);
5463
5675
  if (result.result?.created?.ads?.length) {
5464
5676
  console.log(`
5465
- ${import_picocolors11.default.bold("Created:")} ${result.result.created.ads.length} ad(s)`);
5677
+ ${import_picocolors12.default.bold("Created:")} ${result.result.created.ads.length} ad(s)`);
5466
5678
  }
5467
5679
  console.log("");
5468
5680
  } catch (err) {
5469
5681
  printError(err.message);
5470
5682
  if (err.details) {
5471
- console.error(import_picocolors11.default.dim(` Details: ${JSON.stringify(err.details)}`));
5683
+ console.error(import_picocolors12.default.dim(` Details: ${JSON.stringify(err.details)}`));
5472
5684
  }
5473
5685
  process.exit(1);
5474
5686
  }
5475
5687
  }
5688
+ async function assertCopyFromAdIsUsable(client, adId) {
5689
+ let ad;
5690
+ try {
5691
+ ({ ad } = await client.ads.get(adId));
5692
+ } catch (err) {
5693
+ printError(`Could not fetch ad ${adId}: ${err.message}`);
5694
+ process.exit(1);
5695
+ }
5696
+ if (ad?.creative && ad.creative.canCopySettingsFrom === false) {
5697
+ printError(ad.creative.cannotCopyReason || "This ad cannot be used as a template.");
5698
+ process.exit(1);
5699
+ }
5700
+ }
5476
5701
  function buildBodyFromOpts(specFile, opts) {
5477
5702
  let body;
5478
5703
  if (specFile) {
@@ -5516,6 +5741,14 @@ function buildBodyFromOpts(specFile, opts) {
5516
5741
  body.adSet = body.adSet || {};
5517
5742
  body.adSet.bidAmount = bid;
5518
5743
  }
5744
+ if (opts.page || opts.instagram || opts.threads) {
5745
+ body.profile = {
5746
+ ...body.profile || {},
5747
+ ...opts.page ? { pageId: opts.page } : {},
5748
+ ...opts.instagram ? { instagramId: opts.instagram } : {},
5749
+ ...opts.threads ? { threadsId: opts.threads } : {}
5750
+ };
5751
+ }
5519
5752
  if (opts.pauseAt) {
5520
5753
  if (!["ad", "adSet", "campaign"].includes(opts.pauseAt)) {
5521
5754
  printError('--pause-at must be "ad", "adSet", or "campaign"');
@@ -5526,12 +5759,13 @@ function buildBodyFromOpts(specFile, opts) {
5526
5759
  }
5527
5760
  return body;
5528
5761
  }
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");
5762
+ 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("--page <id>", "Override Facebook Page ID for created ads").option("--instagram <id>", "Override Instagram account ID for created ads").option("--threads <id>", "Override Threads profile ID for created ads").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
5763
  function createCommand2() {
5531
5764
  const cmd = new Command("create").description("Create ads from spec file or flags").argument("[specFile]", "JSON spec file with ad configuration");
5532
5765
  sharedOpts(cmd);
5533
5766
  return cmd.action(async (specFile, opts) => {
5534
5767
  const body = buildBodyFromOpts(specFile, opts);
5768
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5535
5769
  await executeCreate(body, opts);
5536
5770
  });
5537
5771
  }
@@ -5549,6 +5783,7 @@ function createPreviewCommand() {
5549
5783
  sharedOpts(cmd);
5550
5784
  return cmd.action(async (specFile, opts) => {
5551
5785
  const body = buildBodyFromOpts(specFile, opts);
5786
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5552
5787
  await executeCreate(body, opts, { preview: true });
5553
5788
  });
5554
5789
  }
@@ -5557,6 +5792,7 @@ function createTestCommand() {
5557
5792
  sharedOpts(cmd);
5558
5793
  return cmd.action(async (specFile, opts) => {
5559
5794
  const body = buildBodyFromOpts(specFile, opts);
5795
+ if (body.copyFromAd) await assertCopyFromAdIsUsable(createClient({ accountId: opts.account }), body.copyFromAd);
5560
5796
  await executeCreate(body, opts, { test: true });
5561
5797
  });
5562
5798
  }
@@ -5565,7 +5801,7 @@ async function interactiveCreate(client) {
5565
5801
  printError("Interactive mode requires a TTY. Use a spec file or flags instead.");
5566
5802
  process.exit(1);
5567
5803
  }
5568
- Ie(import_picocolors11.default.bold("Ads Uploader \u2014 Create Ads"));
5804
+ Ie(import_picocolors12.default.bold("Ads Uploader \u2014 Create Ads"));
5569
5805
  if (!client.accountId) {
5570
5806
  const { accounts } = await client.accounts.list();
5571
5807
  const accountChoice = await ve({
@@ -5622,6 +5858,7 @@ async function interactiveCreate(client) {
5622
5858
  });
5623
5859
  if (pD(adChoice)) process.exit(0);
5624
5860
  body.copyFromAd = adChoice;
5861
+ await assertCopyFromAdIsUsable(client, adChoice);
5625
5862
  }
5626
5863
  const uploadId = await he({
5627
5864
  message: "Upload batch ID (from `ads upload`)",
@@ -5701,7 +5938,7 @@ async function interactiveCreate(client) {
5701
5938
  });
5702
5939
  if (pD(namePattern)) process.exit(0);
5703
5940
  body.adNamePattern = namePattern;
5704
- M2.info(import_picocolors11.default.dim("Variables: {filename}, {index:01}, {variation}, {campaign}, {date}, {timestamp}"));
5941
+ M2.info(import_picocolors12.default.dim("Variables: {filename}, {index:01}, {variation}, {campaign}, {date}, {timestamp}"));
5705
5942
  }
5706
5943
  const textMethod = await ve({
5707
5944
  message: "Ad text",
@@ -5733,9 +5970,9 @@ async function interactiveCreate(client) {
5733
5970
  ]
5734
5971
  });
5735
5972
  if (pD(textStrategy)) process.exit(0);
5736
- M2.info(import_picocolors11.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
5973
+ M2.info(import_picocolors12.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
5737
5974
  const headlines = [];
5738
- while (true) {
5975
+ for (; ; ) {
5739
5976
  const h2 = await he({
5740
5977
  message: `Headline ${headlines.length + 1}`,
5741
5978
  placeholder: headlines.length === 0 ? "Your headline..." : "(blank to finish)"
@@ -5744,9 +5981,9 @@ async function interactiveCreate(client) {
5744
5981
  if (!h2) break;
5745
5982
  headlines.push(h2);
5746
5983
  }
5747
- M2.info(import_picocolors11.default.dim("Enter primary text (one per line). Leave blank to finish."));
5984
+ M2.info(import_picocolors12.default.dim("Enter primary text (one per line). Leave blank to finish."));
5748
5985
  const bodies = [];
5749
- while (true) {
5986
+ for (; ; ) {
5750
5987
  const b4 = await he({
5751
5988
  message: `Primary text ${bodies.length + 1}`,
5752
5989
  placeholder: bodies.length === 0 ? "Your ad copy..." : "(blank to finish)"
@@ -5853,7 +6090,7 @@ async function interactiveCreate(client) {
5853
6090
  }
5854
6091
  }
5855
6092
  console.log("");
5856
- M2.info(import_picocolors11.default.bold("Summary"));
6093
+ M2.info(import_picocolors12.default.bold("Summary"));
5857
6094
  if (body.adPresetId) M2.info(` Preset: ${body.adPresetId}`);
5858
6095
  if (body.copyFromAd) M2.info(` Copy from: ${body.copyFromAd}`);
5859
6096
  M2.info(` Upload: ${body.uploadId}`);
@@ -5875,13 +6112,13 @@ async function interactiveCreate(client) {
5875
6112
  }
5876
6113
 
5877
6114
  // src/commands/jobs.js
5878
- var import_picocolors12 = __toESM(require_picocolors(), 1);
6115
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
5879
6116
  function jobsCommand() {
5880
6117
  const cmd = new Command("jobs").description("Manage ad creation jobs").option("--json", "Output as JSON").option("--follow", "Follow job progress in real-time").argument("[jobId]", "Job ID to check status").action(async (jobId, opts) => {
5881
6118
  if (!jobId) {
5882
- console.log(import_picocolors12.default.dim(" Usage: ads jobs <jobId>"));
5883
- console.log(import_picocolors12.default.dim(" ads jobs <jobId> --follow"));
5884
- console.log(import_picocolors12.default.dim(" ads jobs cancel <jobId>"));
6119
+ console.log(import_picocolors13.default.dim(" Usage: ads jobs <jobId>"));
6120
+ console.log(import_picocolors13.default.dim(" ads jobs <jobId> --follow"));
6121
+ console.log(import_picocolors13.default.dim(" ads jobs cancel <jobId>"));
5885
6122
  return;
5886
6123
  }
5887
6124
  const client = createClient({});
@@ -5895,16 +6132,16 @@ function jobsCommand() {
5895
6132
  return;
5896
6133
  }
5897
6134
  console.log(`
5898
- ${import_picocolors12.default.bold("Job")} ${jobId}
6135
+ ${import_picocolors13.default.bold("Job")} ${jobId}
5899
6136
  `);
5900
- console.log(` ${import_picocolors12.default.bold("Status:")} ${statusLabel(result.status)}`);
5901
- console.log(` ${import_picocolors12.default.bold("Complete:")} ${result.complete ? import_picocolors12.default.green("yes") : import_picocolors12.default.yellow("no")}`);
6137
+ console.log(` ${import_picocolors13.default.bold("Status:")} ${statusLabel(result.status)}`);
6138
+ console.log(` ${import_picocolors13.default.bold("Complete:")} ${result.complete ? import_picocolors13.default.green("yes") : import_picocolors13.default.yellow("no")}`);
5902
6139
  if (result.progress) {
5903
6140
  const prog = result.progress;
5904
- console.log(` ${import_picocolors12.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
6141
+ console.log(` ${import_picocolors13.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
5905
6142
  }
5906
6143
  if (result.error) {
5907
- console.log(` ${import_picocolors12.default.bold("Error:")} ${import_picocolors12.default.red(result.error.message || result.error)}`);
6144
+ console.log(` ${import_picocolors13.default.bold("Error:")} ${import_picocolors13.default.red(result.error.message || result.error)}`);
5908
6145
  }
5909
6146
  console.log("");
5910
6147
  });
@@ -5925,27 +6162,27 @@ function jobsCommand() {
5925
6162
  function statusLabel(status) {
5926
6163
  switch (status) {
5927
6164
  case "complete":
5928
- return import_picocolors12.default.green("Complete");
6165
+ return import_picocolors13.default.green("Complete");
5929
6166
  case "running":
5930
6167
  case "in_progress":
5931
- return import_picocolors12.default.cyan("Running");
6168
+ return import_picocolors13.default.cyan("Running");
5932
6169
  case "cancelled":
5933
- return import_picocolors12.default.yellow("Cancelled");
6170
+ return import_picocolors13.default.yellow("Cancelled");
5934
6171
  case "error":
5935
6172
  case "failed":
5936
- return import_picocolors12.default.red("Failed");
6173
+ return import_picocolors13.default.red("Failed");
5937
6174
  default:
5938
- return import_picocolors12.default.dim(status || "unknown");
6175
+ return import_picocolors13.default.dim(status || "unknown");
5939
6176
  }
5940
6177
  }
5941
6178
 
5942
6179
  // src/cli.js
5943
- var VERSION = true ? "0.1.7" : "0.0.0";
6180
+ var VERSION = true ? "0.2.0" : "0.0.0";
5944
6181
  var apiUrl = process.env.ADS_API_URL || getBaseUrl();
5945
6182
  if (apiUrl && (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1"))) {
5946
6183
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5947
6184
  }
5948
- var b3 = (s) => import_picocolors13.default.bold(import_picocolors13.default.blue(s));
6185
+ var b3 = (s) => import_picocolors14.default.bold(import_picocolors14.default.blue(s));
5949
6186
  var BANNER = [
5950
6187
  "",
5951
6188
  " " + b3(" ___ __ __ __ __ __"),
@@ -5954,7 +6191,7 @@ var BANNER = [
5954
6191
  " " + b3("/_/ |_\\_,_/___/ \\____/ .__/_/\\___/\\_,_/\\_,_/\\__/_/"),
5955
6192
  " " + b3(" /_/"),
5956
6193
  "",
5957
- " " + import_picocolors13.default.dim("Create Meta ads from the command line"),
6194
+ " " + import_picocolors14.default.dim("Create Meta ads from the command line"),
5958
6195
  ""
5959
6196
  ].join("\n");
5960
6197
  var program2 = new Command().name("ads").description("Ads Uploader CLI").version(VERSION).addHelpText("before", BANNER);
@@ -5964,12 +6201,14 @@ program2.addCommand(whoamiCommand());
5964
6201
  program2.addCommand(configCommand());
5965
6202
  program2.addCommand(accountsCommand());
5966
6203
  program2.addCommand(accountCommand());
6204
+ program2.addCommand(pagesCommand());
5967
6205
  program2.addCommand(campaignsCommand());
5968
6206
  program2.addCommand(campaignCommand());
5969
6207
  program2.addCommand(adsetsCommand());
5970
6208
  program2.addCommand(adsetCommand());
5971
6209
  program2.addCommand(adCommand());
5972
6210
  program2.addCommand(presetsCommand());
6211
+ program2.addCommand(presetsSaveCommand());
5973
6212
  program2.addCommand(textPresetsCommand());
5974
6213
  program2.addCommand(uploadsCommand());
5975
6214
  program2.addCommand(uploadCommand());
@@ -6000,8 +6239,8 @@ async function checkForUpdates() {
6000
6239
  }
6001
6240
  if (latest !== VERSION) {
6002
6241
  console.error(`
6003
- ${import_picocolors13.default.yellow("Update available:")} ${import_picocolors13.default.dim(VERSION)} \u2192 ${import_picocolors13.default.green(latest)}`);
6004
- console.error(` Run ${import_picocolors13.default.cyan("npm update -g @adsuploader/cli")} to update
6242
+ ${import_picocolors14.default.yellow("Update available:")} ${import_picocolors14.default.dim(VERSION)} \u2192 ${import_picocolors14.default.green(latest)}`);
6243
+ console.error(` Run ${import_picocolors14.default.cyan("npm update -g @adsuploader/cli")} to update
6005
6244
  `);
6006
6245
  }
6007
6246
  } catch {
@@ -6025,7 +6264,7 @@ if (process.argv.length <= 2) {
6025
6264
  }
6026
6265
  if (err.code !== "commander.executeSubCommandAsync") {
6027
6266
  console.error(`
6028
- ${import_picocolors13.default.red("Error:")} ${err.message}
6267
+ ${import_picocolors14.default.red("Error:")} ${err.message}
6029
6268
  `);
6030
6269
  process.exit(1);
6031
6270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adsuploader/cli",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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",
@@ -32,6 +32,7 @@
32
32
  "scripts": {
33
33
  "build": "node build.cjs",
34
34
  "dev": "node src/cli.js",
35
+ "test": "node --test test/*.test.mjs",
35
36
  "link": "npm link",
36
37
  "prepublishOnly": "npm run build",
37
38
  "version": "npm run build"