@adsuploader/cli 0.1.8 → 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 +1 -0
  2. package/SKILL.md +36 -0
  3. package/dist/cli.cjs +345 -170
  4. package/package.json +2 -1
package/README.md CHANGED
@@ -37,6 +37,7 @@ 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 |
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,6 +86,7 @@ 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`) |
@@ -115,6 +124,9 @@ Ads are created **ACTIVE** by default. Use `--status PAUSED` to create them paus
115
124
  | `--pause-at <level>` | Pause level: `ad` (default), `adSet`, or `campaign` |
116
125
  | `--daily-budget <amount>` | Override daily budget (currency units) |
117
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 |
118
130
  | `--text-file <path>` | Load text config from a JSON file |
119
131
  | `--expanded` | Show full headline, primary text, and description values in previews |
120
132
 
@@ -165,6 +177,30 @@ When using `copyFromAd`, provide the upload batch and optionally campaign/ad set
165
177
  }
166
178
  ```
167
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
+
168
204
  ### Campaign Structure
169
205
 
170
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
  },
@@ -4705,8 +4768,39 @@ function adsetCommand() {
4705
4768
  });
4706
4769
  }
4707
4770
 
4708
- // src/commands/ad.js
4771
+ // src/commands/pages.js
4709
4772
  var import_picocolors7 = __toESM(require_picocolors(), 1);
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);
4710
4804
  function truncate(str, max = 120, expanded = false) {
4711
4805
  if (!str || expanded || str.length <= max) return str;
4712
4806
  return str.slice(0, max) + "...";
@@ -4720,17 +4814,17 @@ function adCommand() {
4720
4814
  return;
4721
4815
  }
4722
4816
  console.log(`
4723
- ${import_picocolors7.default.bold(ad.name)}
4817
+ ${import_picocolors8.default.bold(ad.name)}
4724
4818
  `);
4725
- console.log(` ${import_picocolors7.default.bold("ID:")} ${ad.id}`);
4726
- console.log(` ${import_picocolors7.default.bold("Status:")} ${statusColor(ad.status)}`);
4727
- console.log(` ${import_picocolors7.default.bold("Campaign:")} ${ad.campaignId}`);
4728
- 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}`);
4729
4823
  if (ad.creative) {
4730
- const label = (name) => import_picocolors7.default.bold(name.padEnd(15));
4824
+ const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
4731
4825
  const exp = !!opts.expanded;
4732
4826
  console.log("");
4733
- console.log(` ${import_picocolors7.default.bold("Creative")}`);
4827
+ console.log(` ${import_picocolors8.default.bold("Creative")}`);
4734
4828
  if (ad.creative.headline) console.log(` ${label("Headline:")}${truncate(ad.creative.headline, 120, exp)}`);
4735
4829
  if (ad.creative.headlines) console.log(` ${label("Headlines:")}${ad.creative.headlines.map((h2) => truncate(h2, 60, exp)).join(" | ")}`);
4736
4830
  if (ad.creative.primaryText) console.log(` ${label("Primary Text:")}${truncate(ad.creative.primaryText, 120, exp)}`);
@@ -4744,13 +4838,13 @@ function adCommand() {
4744
4838
  if (ad.creative.enhancements) console.log(` ${label("Enhancements:")}${ad.creative.enhancements.join(", ")}`);
4745
4839
  }
4746
4840
  console.log(`
4747
- ${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}
4748
4842
  `);
4749
4843
  });
4750
4844
  }
4751
4845
 
4752
4846
  // src/commands/presets.js
4753
- var import_picocolors8 = __toESM(require_picocolors(), 1);
4847
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
4754
4848
  function presetsCommand() {
4755
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) => {
4756
4850
  const client = createClient({ accountId: opts.account });
@@ -4761,12 +4855,12 @@ function presetsCommand() {
4761
4855
  return;
4762
4856
  }
4763
4857
  console.log(`
4764
- ${import_picocolors8.default.bold(preset.name)}
4858
+ ${import_picocolors9.default.bold(preset.name)}
4765
4859
  `);
4766
4860
  if (preset.config) {
4767
- console.log(` ${import_picocolors8.default.bold("Campaign:")} ${preset.config.campaign?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.campaign?.id || "")}`);
4768
- 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 || "")}`);
4769
- 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 || "")}`);
4770
4864
  }
4771
4865
  console.log("");
4772
4866
  return;
@@ -4778,15 +4872,15 @@ function presetsCommand() {
4778
4872
  return;
4779
4873
  }
4780
4874
  console.log(`
4781
- ${import_picocolors8.default.bold("Ad Template Presets")} (${all.length})
4875
+ ${import_picocolors9.default.bold("Ad Template Presets")} (${all.length})
4782
4876
  `);
4783
4877
  if (all.length === 0) {
4784
- 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."));
4785
4879
  } else {
4786
4880
  printTable(all, [
4787
4881
  { key: "id", label: "ID", maxWidth: 28 },
4788
4882
  { key: "name", label: "Name", maxWidth: 40 },
4789
- { 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") }
4790
4884
  ]);
4791
4885
  }
4792
4886
  console.log("");
@@ -4802,13 +4896,13 @@ function textPresetsCommand() {
4802
4896
  return;
4803
4897
  }
4804
4898
  console.log(`
4805
- ${import_picocolors8.default.bold(preset.name)}
4899
+ ${import_picocolors9.default.bold(preset.name)}
4806
4900
  `);
4807
- 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"}`);
4808
4902
  if (preset.text) {
4809
- if (preset.text.titles?.length) console.log(` ${import_picocolors8.default.bold("Headlines:")} ${preset.text.titles.join(" | ")}`);
4810
- if (preset.text.bodies?.length) console.log(` ${import_picocolors8.default.bold("Bodies:")} ${preset.text.bodies.join(" | ")}`);
4811
- 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(" | ")}`);
4812
4906
  }
4813
4907
  console.log("");
4814
4908
  return;
@@ -4820,16 +4914,16 @@ function textPresetsCommand() {
4820
4914
  return;
4821
4915
  }
4822
4916
  console.log(`
4823
- ${import_picocolors8.default.bold("Text Presets")} (${all.length})
4917
+ ${import_picocolors9.default.bold("Text Presets")} (${all.length})
4824
4918
  `);
4825
4919
  if (all.length === 0) {
4826
- 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."));
4827
4921
  } else {
4828
4922
  printTable(all, [
4829
4923
  { key: "id", label: "ID", maxWidth: 28 },
4830
4924
  { key: "name", label: "Name", maxWidth: 30 },
4831
- { key: "scope", label: "Scope", color: (v2) => v2 === "global" ? import_picocolors8.default.cyan("global") : import_picocolors8.default.dim("account") },
4832
- { 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") }
4833
4927
  ]);
4834
4928
  }
4835
4929
  console.log("");
@@ -4867,11 +4961,11 @@ function presetsSaveCommand() {
4867
4961
  return;
4868
4962
  }
4869
4963
  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")}`);
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")}`);
4873
4967
  console.log(`
4874
- ${import_picocolors8.default.dim("Use it:")} ads create --preset ${preset.id} ...
4968
+ ${import_picocolors9.default.dim("Use it:")} ads create --preset ${preset.id} ...
4875
4969
  `);
4876
4970
  } catch (err) {
4877
4971
  printError(err.message || "Failed to save preset");
@@ -4883,10 +4977,15 @@ function presetsSaveCommand() {
4883
4977
  // src/commands/upload.js
4884
4978
  var import_fs2 = __toESM(require("fs"), 1);
4885
4979
  var import_path2 = __toESM(require("path"), 1);
4886
- var import_picocolors9 = __toESM(require_picocolors(), 1);
4980
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
4887
4981
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]);
4888
4982
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]);
4889
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;
4890
4989
  var MAGIC_BYTES = {
4891
4990
  "image/jpeg": [[255, 216, 255]],
4892
4991
  "image/png": [[137, 80, 78, 71]],
@@ -4950,6 +5049,75 @@ var CONTENT_TYPES = {
4950
5049
  ".webm": "video/webm",
4951
5050
  ".m4v": "video/x-m4v"
4952
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
+ }
4953
5121
  function getFileType(filePath) {
4954
5122
  const ext = import_path2.default.extname(filePath).toLowerCase();
4955
5123
  if (IMAGE_EXTS.has(ext)) return "image";
@@ -4994,13 +5162,17 @@ function resolveFiles(inputs) {
4994
5162
  }
4995
5163
  }
4996
5164
  if (skipped.length > 0) {
4997
- 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(", ")}`));
4998
5166
  }
4999
5167
  return files;
5000
5168
  }
5001
5169
  async function stageFiles(paths, opts) {
5002
- const client = createClient({ accountId: opts.account });
5170
+ const client = createClient({
5171
+ accountId: opts.account,
5172
+ timeoutMs: parsePositiveInt2(opts.apiTimeout, void 0)
5173
+ });
5003
5174
  const jsonMode = shouldOutputJson(opts);
5175
+ const uploadTimeoutMs = parsePositiveInt2(opts.uploadTimeout, DEFAULT_UPLOAD_TIMEOUT_MS);
5004
5176
  let files;
5005
5177
  try {
5006
5178
  files = resolveFiles(paths);
@@ -5014,7 +5186,7 @@ async function stageFiles(paths, opts) {
5014
5186
  }
5015
5187
  if (!jsonMode) {
5016
5188
  console.log(`
5017
- ${import_picocolors9.default.bold("Uploading")} ${files.length} file(s)
5189
+ ${import_picocolors10.default.bold("Uploading")} ${files.length} file(s)
5018
5190
  `);
5019
5191
  }
5020
5192
  const filesMeta = files.map((f) => ({
@@ -5025,39 +5197,33 @@ async function stageFiles(paths, opts) {
5025
5197
  }));
5026
5198
  const initResult = await client.uploads.init(filesMeta);
5027
5199
  const { batchId } = initResult;
5028
- const uploadedFiles = [];
5029
- for (let i = 0; i < files.length; i++) {
5030
- const file = files[i];
5200
+ const concurrency = calculateUploadConcurrency(files, opts.concurrency);
5201
+ const uploadedFiles = await runBoundedConcurrency(files, concurrency, async (file, i) => {
5031
5202
  const r2Info = initResult.files[i];
5032
5203
  if (!jsonMode) {
5033
- 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)}`)}`);
5034
5205
  }
5035
5206
  try {
5036
5207
  const fileBuffer = import_fs2.default.readFileSync(file.path);
5037
- const resp = await fetch(r2Info.uploadUrl, {
5038
- method: "PUT",
5039
- headers: {
5040
- "Content-Type": file.contentType || (file.type === "video" ? "video/mp4" : "image/jpeg"),
5041
- "Content-Length": String(fileBuffer.length)
5042
- },
5043
- body: fileBuffer
5044
- });
5045
- if (!resp.ok) {
5046
- throw new Error(`R2 upload failed: ${resp.status}`);
5047
- }
5048
- 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 {
5049
5215
  name: file.name,
5050
5216
  type: file.type,
5051
5217
  size: file.size,
5052
5218
  fileKey: r2Info.fileKey
5053
- });
5219
+ };
5054
5220
  } catch (err) {
5055
5221
  if (!jsonMode) {
5056
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
5222
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
5057
5223
  }
5058
- 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 };
5059
5225
  }
5060
- }
5226
+ });
5061
5227
  return { client, batchId, uploadedFiles, jsonMode };
5062
5228
  }
5063
5229
  async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
@@ -5067,7 +5233,7 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5067
5233
  process.exit(1);
5068
5234
  }
5069
5235
  if (!jsonMode) {
5070
- 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...")}`);
5071
5237
  }
5072
5238
  const results = [];
5073
5239
  let completed = 0;
@@ -5085,19 +5251,19 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5085
5251
  completed++;
5086
5252
  if (!jsonMode) {
5087
5253
  const id = result.videoId || result.mediaHash || "";
5088
- 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)}`);
5089
5255
  }
5090
5256
  } else {
5091
5257
  failed++;
5092
5258
  if (!jsonMode) {
5093
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${result.error}`);
5259
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${result.error}`);
5094
5260
  }
5095
5261
  }
5096
5262
  } catch (err) {
5097
5263
  failed++;
5098
5264
  results.push({ name: file.name, type: file.type, status: "error", error: err.message });
5099
5265
  if (!jsonMode) {
5100
- console.log(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
5266
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
5101
5267
  }
5102
5268
  }
5103
5269
  }
@@ -5115,30 +5281,30 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
5115
5281
  }
5116
5282
  if (groups.length > 0) {
5117
5283
  console.log(`
5118
- ${import_picocolors9.default.bold("Variant Groups")} (${groups.length})
5284
+ ${import_picocolors10.default.bold("Variant Groups")} (${groups.length})
5119
5285
  `);
5120
5286
  for (const group of groups) {
5121
5287
  const assets = group.assets.map((a) => {
5122
- const placement = a.placementType === "default" ? "" : import_picocolors9.default.cyan(` [${a.placementType}]`);
5288
+ const placement = a.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${a.placementType}]`);
5123
5289
  return `${a.name}${placement}`;
5124
5290
  });
5125
- console.log(` ${import_picocolors9.default.bold(group.baseName || group.groupId)}`);
5291
+ console.log(` ${import_picocolors10.default.bold(group.baseName || group.groupId)}`);
5126
5292
  for (const asset of assets) {
5127
5293
  console.log(` ${asset}`);
5128
5294
  }
5129
5295
  }
5130
5296
  }
5131
5297
  console.log(`
5132
- ${import_picocolors9.default.bold("Batch:")} ${batchId}`);
5298
+ ${import_picocolors10.default.bold("Batch:")} ${batchId}`);
5133
5299
  const countStr = `${completed} complete, ${failed} failed`;
5134
5300
  console.log(` ${countStr}`);
5135
5301
  if (groups.length > 0) {
5136
- 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`);
5137
5303
  }
5138
5304
  console.log("");
5139
5305
  }
5140
5306
  function uploadCommand() {
5141
- 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) => {
5142
5308
  const { client, batchId, uploadedFiles, jsonMode } = await stageFiles(paths, opts);
5143
5309
  await processAndPrint(client, batchId, uploadedFiles, jsonMode);
5144
5310
  });
@@ -5155,10 +5321,10 @@ function uploadsCommand() {
5155
5321
  }
5156
5322
  const accountLabel = batch.accountName ? `${batch.accountName} (${batch.accountId})` : batch.accountId;
5157
5323
  console.log(`
5158
- ${import_picocolors9.default.bold("Batch")} ${batch.batchId}
5324
+ ${import_picocolors10.default.bold("Batch")} ${batch.batchId}
5159
5325
  `);
5160
- console.log(` ${import_picocolors9.default.bold("Account:")} ${accountLabel}`);
5161
- 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}
5162
5328
  `);
5163
5329
  const rows = batch.files.map((f) => ({
5164
5330
  ...f,
@@ -5167,19 +5333,19 @@ function uploadsCommand() {
5167
5333
  printTable(rows, [
5168
5334
  { key: "name", label: "Name", maxWidth: 35 },
5169
5335
  { key: "type", label: "Type", maxWidth: 6 },
5170
- { 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") }
5171
5337
  ]);
5172
5338
  if (batch.groups?.length > 0) {
5173
5339
  console.log(`
5174
- ${import_picocolors9.default.bold("Variant Groups")} (${batch.groups.length})
5340
+ ${import_picocolors10.default.bold("Variant Groups")} (${batch.groups.length})
5175
5341
  `);
5176
5342
  for (const group of batch.groups) {
5177
5343
  const primaryAsset = group.assets.find((a) => a.isPrimary);
5178
5344
  const groupLabel = group.baseName || primaryAsset?.name || group.groupId;
5179
- console.log(` ${import_picocolors9.default.bold(groupLabel)}`);
5345
+ console.log(` ${import_picocolors10.default.bold(groupLabel)}`);
5180
5346
  for (const asset of group.assets) {
5181
- const placement = asset.placementType === "default" ? "" : import_picocolors9.default.cyan(` [${asset.placementType}]`);
5182
- 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)") : "";
5183
5349
  console.log(` ${asset.name}${placement}${primary}`);
5184
5350
  }
5185
5351
  }
@@ -5193,10 +5359,10 @@ function uploadsCommand() {
5193
5359
  return;
5194
5360
  }
5195
5361
  console.log(`
5196
- ${import_picocolors9.default.bold("Recent Uploads")} (${batches.length})
5362
+ ${import_picocolors10.default.bold("Recent Uploads")} (${batches.length})
5197
5363
  `);
5198
5364
  if (batches.length === 0) {
5199
- 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"));
5200
5366
  return;
5201
5367
  }
5202
5368
  const formatDate = (d3) => {
@@ -5225,10 +5391,10 @@ function uploadsCommand() {
5225
5391
 
5226
5392
  // src/commands/create.js
5227
5393
  var import_fs3 = __toESM(require("fs"), 1);
5228
- var import_picocolors11 = __toESM(require_picocolors(), 1);
5394
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
5229
5395
 
5230
5396
  // src/lib/poll.js
5231
- var import_picocolors10 = __toESM(require_picocolors(), 1);
5397
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
5232
5398
  var POLL_INTERVAL = 500;
5233
5399
  var TIMEOUT_MS = 30 * 60 * 1e3;
5234
5400
  var TIMEOUT_MINUTES = TIMEOUT_MS / 6e4;
@@ -5240,7 +5406,7 @@ async function pollJob(client, jobId, opts = {}) {
5240
5406
  if (!jsonMode) {
5241
5407
  if (tty) {
5242
5408
  console.log(`
5243
- ${import_picocolors10.default.bold("Creating ads")}
5409
+ ${import_picocolors11.default.bold("Creating ads")}
5244
5410
  `);
5245
5411
  } else {
5246
5412
  console.log(`[start] Creating ads \u2014 ${jobId}`);
@@ -5266,7 +5432,7 @@ async function pollJob(client, jobId, opts = {}) {
5266
5432
  process.exitCode = 2;
5267
5433
  return { status: "still_running", jobId };
5268
5434
  }
5269
- await sleep(POLL_INTERVAL);
5435
+ await sleep3(POLL_INTERVAL);
5270
5436
  continue;
5271
5437
  }
5272
5438
  const ops = data.progress?.operations || [];
@@ -5277,7 +5443,7 @@ async function pollJob(client, jobId, opts = {}) {
5277
5443
  const time = formatTimestamp(op.timestamp);
5278
5444
  const { icon, color } = opStyle(op.type);
5279
5445
  if (tty) {
5280
- 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)}`);
5281
5447
  } else {
5282
5448
  const tag = op.type === "error" ? "[err]" : op.type === "retry" ? "[..]" : op.type === "completion" || (op.type || "").endsWith("_complete") ? "[ok ]" : "[..]";
5283
5449
  console.log(`${time} ${tag} ${op.message}`);
@@ -5298,7 +5464,7 @@ async function pollJob(client, jobId, opts = {}) {
5298
5464
  const failed = data.result?.created?.failedAds?.length || 0;
5299
5465
  console.log("");
5300
5466
  if (failed > 0 && adCount > 0) {
5301
- 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}`);
5302
5468
  } else if (adCount > 0) {
5303
5469
  printSuccess(`Created ${adCount} ads in ${elapsed}`);
5304
5470
  } else {
@@ -5313,14 +5479,14 @@ async function pollJob(client, jobId, opts = {}) {
5313
5479
  console.log(JSON.stringify({ event: "still_running", jobId }));
5314
5480
  } else {
5315
5481
  console.log("");
5316
- console.log(` ${import_picocolors10.default.yellow("\u2212")} ${import_picocolors10.default.yellow(`Still running after ${TIMEOUT_MINUTES} min \u2014 job continues on the server.`)}`);
5317
- 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`)}`);
5318
5484
  console.log("");
5319
5485
  }
5320
5486
  process.exitCode = 2;
5321
5487
  return { status: "still_running", jobId };
5322
5488
  }
5323
- await sleep(POLL_INTERVAL);
5489
+ await sleep3(POLL_INTERVAL);
5324
5490
  }
5325
5491
  }
5326
5492
  function opStyle(type) {
@@ -5328,13 +5494,13 @@ function opStyle(type) {
5328
5494
  case "completion":
5329
5495
  case "campaign_complete":
5330
5496
  case "adset_complete":
5331
- return { icon: "\u2713", color: import_picocolors10.default.green };
5497
+ return { icon: "\u2713", color: import_picocolors11.default.green };
5332
5498
  case "error":
5333
- return { icon: "\u2717", color: import_picocolors10.default.red };
5499
+ return { icon: "\u2717", color: import_picocolors11.default.red };
5334
5500
  case "retry":
5335
- return { icon: "\u2212", color: import_picocolors10.default.yellow };
5501
+ return { icon: "\u2212", color: import_picocolors11.default.yellow };
5336
5502
  default:
5337
- return { icon: "\u2192", color: import_picocolors10.default.dim };
5503
+ return { icon: "\u2192", color: import_picocolors11.default.dim };
5338
5504
  }
5339
5505
  }
5340
5506
  function formatTimestamp(iso) {
@@ -5346,7 +5512,7 @@ function formatTimestamp(iso) {
5346
5512
  return " ";
5347
5513
  }
5348
5514
  }
5349
- function sleep(ms) {
5515
+ function sleep3(ms) {
5350
5516
  return new Promise((r) => setTimeout(r, ms));
5351
5517
  }
5352
5518
 
@@ -5370,29 +5536,29 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5370
5536
  const ts = result.totalAdSets || 1;
5371
5537
  const ta = result.totalAds || result.ads?.length || 0;
5372
5538
  const parts = [];
5373
- if (tc > 1) parts.push(`${import_picocolors11.default.blue(tc)} campaigns`);
5374
- parts.push(`${import_picocolors11.default.blue(ts)} ad set${ts !== 1 ? "s" : ""}`);
5375
- 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" : ""}`);
5376
5542
  let statusStr = statusColor(result.status || "PAUSED");
5377
5543
  if (result.status === "PAUSED" && result.pauseAt && result.pauseAt !== "ad") {
5378
5544
  const levelLabel = result.pauseAt === "campaign" ? "campaign level" : "ad set level";
5379
- statusStr += import_picocolors11.default.dim(` (${levelLabel})`);
5545
+ statusStr += import_picocolors12.default.dim(` (${levelLabel})`);
5380
5546
  }
5381
5547
  let enhLabel = "";
5382
- if (result.enhancements === "metaDefaults") enhLabel = import_picocolors11.default.dim("All Off");
5383
- else if (result.enhancements === "all") enhLabel = import_picocolors11.default.dim("All On");
5384
- else if (result.enhancements === "none") enhLabel = import_picocolors11.default.dim("All Off");
5385
- else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors11.default.dim(`${result.enhancements.length} custom`);
5386
- const enhPart = enhLabel ? ` ${import_picocolors11.default.dim("\xB7")} ${enhLabel}` : "";
5387
- 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}
5388
5554
  `);
5389
5555
  if (result.ads?.length) {
5390
5556
  const tableRows = [];
5391
5557
  let lastCampaign = null;
5392
5558
  let lastAdSet = null;
5393
5559
  for (const ad of result.ads) {
5394
- const campaignName = ad.campaignName || import_picocolors11.default.dim("\u2014");
5395
- 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");
5396
5562
  tableRows.push({
5397
5563
  campaign: campaignName === lastCampaign ? "" : campaignName,
5398
5564
  adSet: campaignName === lastCampaign && adSetName === lastAdSet ? "" : adSetName,
@@ -5440,54 +5606,54 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5440
5606
  for (const ad of result.ads) {
5441
5607
  if (showAdSetHeaders && ad.adSetName && ad.adSetName !== lastDetailAdSet) {
5442
5608
  if (lastDetailAdSet !== null) console.log("");
5443
- 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)}`);
5444
5610
  lastDetailAdSet = ad.adSetName;
5445
5611
  }
5446
- const typeLabel = ad.mediaType ? import_picocolors11.default.dim(` [${ad.mediaType}]`) : "";
5612
+ const typeLabel = ad.mediaType ? import_picocolors12.default.dim(` [${ad.mediaType}]`) : "";
5447
5613
  const indent = showAdSetHeaders ? " " : " ";
5448
5614
  const fieldIndent = showAdSetHeaders ? " " : " ";
5449
5615
  const fieldIndentLen = fieldIndent.length;
5450
- 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}`);
5451
5617
  const headline = (ad.headline || []).join(" | ");
5452
5618
  const primary = (ad.primaryText || []).join(" | ");
5453
5619
  const desc = (ad.description || []).join(" | ");
5454
- if (headline) console.log(`${fieldIndent}${import_picocolors11.default.dim("Headline:")} ${trunc(headline)}`);
5455
- if (primary) console.log(`${fieldIndent}${import_picocolors11.default.dim("Primary Text:")} ${trunc(primary)}`);
5456
- if (desc) console.log(`${fieldIndent}${import_picocolors11.default.dim("Description:")} ${trunc(desc)}`);
5457
- if (ad.cta && !sharedCta) console.log(`${fieldIndent}${import_picocolors11.default.dim("CTA:")} ${ad.cta}`);
5458
- if (ad.link && !sharedLink) console.log(`${fieldIndent}${import_picocolors11.default.dim("Link:")} ${wrapLine(ad.link, fieldIndentLen)}`);
5459
- 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)}`);
5460
5626
  console.log("");
5461
5627
  }
5462
5628
  const valCol = 18;
5463
- if (sharedCta) console.log(` ${import_picocolors11.default.bold("CTA:")} ${sharedCta}`);
5464
- if (sharedLink) console.log(` ${import_picocolors11.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
5465
- 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)}`);
5466
5632
  const enh = result.enhancements;
5467
5633
  let enhDetail;
5468
5634
  if (enh === "metaDefaults") enhDetail = "Meta Defaults";
5469
5635
  else if (enh === "all") enhDetail = "All On";
5470
5636
  else if (enh === "none") enhDetail = "All Off";
5471
5637
  else if (Array.isArray(enh)) enhDetail = enh.join(", ");
5472
- 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)}`);
5473
5639
  if (result.budget) {
5474
5640
  const curr = result.budget.currency ? ` ${result.budget.currency}` : "";
5475
5641
  const renderLine = (label, amount) => {
5476
5642
  const pad = " ".repeat(Math.max(1, 16 - label.length - 1));
5477
- console.log(` ${import_picocolors11.default.bold(`${label}:`)}${pad}${amount}${curr}`);
5643
+ console.log(` ${import_picocolors12.default.bold(`${label}:`)}${pad}${amount}${curr}`);
5478
5644
  };
5479
5645
  if (result.budget.dailyBudget != null) renderLine("Daily Budget", result.budget.dailyBudget);
5480
5646
  if (result.budget.bidAmount != null) renderLine("Bid Amount", result.budget.bidAmount);
5481
5647
  }
5482
5648
  console.log("");
5483
5649
  } else if (result.plan?.totals) {
5484
- console.log(` ${import_picocolors11.default.bold("Plan:")}`);
5650
+ console.log(` ${import_picocolors12.default.bold("Plan:")}`);
5485
5651
  console.log(` Campaigns: ${result.plan.totals.campaigns}`);
5486
5652
  console.log(` Ad Sets: ${result.plan.totals.adSets}`);
5487
5653
  console.log(` Ads: ${result.plan.totals.ads}`);
5488
5654
  }
5489
5655
  console.log(`
5490
- ${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.")}
5491
5657
  `);
5492
5658
  return;
5493
5659
  }
@@ -5508,13 +5674,13 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
5508
5674
  printSuccess(`${result.status || "Complete"}`);
5509
5675
  if (result.result?.created?.ads?.length) {
5510
5676
  console.log(`
5511
- ${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)`);
5512
5678
  }
5513
5679
  console.log("");
5514
5680
  } catch (err) {
5515
5681
  printError(err.message);
5516
5682
  if (err.details) {
5517
- console.error(import_picocolors11.default.dim(` Details: ${JSON.stringify(err.details)}`));
5683
+ console.error(import_picocolors12.default.dim(` Details: ${JSON.stringify(err.details)}`));
5518
5684
  }
5519
5685
  process.exit(1);
5520
5686
  }
@@ -5575,6 +5741,14 @@ function buildBodyFromOpts(specFile, opts) {
5575
5741
  body.adSet = body.adSet || {};
5576
5742
  body.adSet.bidAmount = bid;
5577
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
+ }
5578
5752
  if (opts.pauseAt) {
5579
5753
  if (!["ad", "adSet", "campaign"].includes(opts.pauseAt)) {
5580
5754
  printError('--pause-at must be "ad", "adSet", or "campaign"');
@@ -5585,7 +5759,7 @@ function buildBodyFromOpts(specFile, opts) {
5585
5759
  }
5586
5760
  return body;
5587
5761
  }
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");
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");
5589
5763
  function createCommand2() {
5590
5764
  const cmd = new Command("create").description("Create ads from spec file or flags").argument("[specFile]", "JSON spec file with ad configuration");
5591
5765
  sharedOpts(cmd);
@@ -5627,7 +5801,7 @@ async function interactiveCreate(client) {
5627
5801
  printError("Interactive mode requires a TTY. Use a spec file or flags instead.");
5628
5802
  process.exit(1);
5629
5803
  }
5630
- Ie(import_picocolors11.default.bold("Ads Uploader \u2014 Create Ads"));
5804
+ Ie(import_picocolors12.default.bold("Ads Uploader \u2014 Create Ads"));
5631
5805
  if (!client.accountId) {
5632
5806
  const { accounts } = await client.accounts.list();
5633
5807
  const accountChoice = await ve({
@@ -5764,7 +5938,7 @@ async function interactiveCreate(client) {
5764
5938
  });
5765
5939
  if (pD(namePattern)) process.exit(0);
5766
5940
  body.adNamePattern = namePattern;
5767
- 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}"));
5768
5942
  }
5769
5943
  const textMethod = await ve({
5770
5944
  message: "Ad text",
@@ -5796,7 +5970,7 @@ async function interactiveCreate(client) {
5796
5970
  ]
5797
5971
  });
5798
5972
  if (pD(textStrategy)) process.exit(0);
5799
- 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."));
5800
5974
  const headlines = [];
5801
5975
  for (; ; ) {
5802
5976
  const h2 = await he({
@@ -5807,7 +5981,7 @@ async function interactiveCreate(client) {
5807
5981
  if (!h2) break;
5808
5982
  headlines.push(h2);
5809
5983
  }
5810
- 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."));
5811
5985
  const bodies = [];
5812
5986
  for (; ; ) {
5813
5987
  const b4 = await he({
@@ -5916,7 +6090,7 @@ async function interactiveCreate(client) {
5916
6090
  }
5917
6091
  }
5918
6092
  console.log("");
5919
- M2.info(import_picocolors11.default.bold("Summary"));
6093
+ M2.info(import_picocolors12.default.bold("Summary"));
5920
6094
  if (body.adPresetId) M2.info(` Preset: ${body.adPresetId}`);
5921
6095
  if (body.copyFromAd) M2.info(` Copy from: ${body.copyFromAd}`);
5922
6096
  M2.info(` Upload: ${body.uploadId}`);
@@ -5938,13 +6112,13 @@ async function interactiveCreate(client) {
5938
6112
  }
5939
6113
 
5940
6114
  // src/commands/jobs.js
5941
- var import_picocolors12 = __toESM(require_picocolors(), 1);
6115
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
5942
6116
  function jobsCommand() {
5943
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) => {
5944
6118
  if (!jobId) {
5945
- console.log(import_picocolors12.default.dim(" Usage: ads jobs <jobId>"));
5946
- console.log(import_picocolors12.default.dim(" ads jobs <jobId> --follow"));
5947
- 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>"));
5948
6122
  return;
5949
6123
  }
5950
6124
  const client = createClient({});
@@ -5958,16 +6132,16 @@ function jobsCommand() {
5958
6132
  return;
5959
6133
  }
5960
6134
  console.log(`
5961
- ${import_picocolors12.default.bold("Job")} ${jobId}
6135
+ ${import_picocolors13.default.bold("Job")} ${jobId}
5962
6136
  `);
5963
- console.log(` ${import_picocolors12.default.bold("Status:")} ${statusLabel(result.status)}`);
5964
- 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")}`);
5965
6139
  if (result.progress) {
5966
6140
  const prog = result.progress;
5967
- 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 || "?"}`);
5968
6142
  }
5969
6143
  if (result.error) {
5970
- 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)}`);
5971
6145
  }
5972
6146
  console.log("");
5973
6147
  });
@@ -5988,27 +6162,27 @@ function jobsCommand() {
5988
6162
  function statusLabel(status) {
5989
6163
  switch (status) {
5990
6164
  case "complete":
5991
- return import_picocolors12.default.green("Complete");
6165
+ return import_picocolors13.default.green("Complete");
5992
6166
  case "running":
5993
6167
  case "in_progress":
5994
- return import_picocolors12.default.cyan("Running");
6168
+ return import_picocolors13.default.cyan("Running");
5995
6169
  case "cancelled":
5996
- return import_picocolors12.default.yellow("Cancelled");
6170
+ return import_picocolors13.default.yellow("Cancelled");
5997
6171
  case "error":
5998
6172
  case "failed":
5999
- return import_picocolors12.default.red("Failed");
6173
+ return import_picocolors13.default.red("Failed");
6000
6174
  default:
6001
- return import_picocolors12.default.dim(status || "unknown");
6175
+ return import_picocolors13.default.dim(status || "unknown");
6002
6176
  }
6003
6177
  }
6004
6178
 
6005
6179
  // src/cli.js
6006
- var VERSION = true ? "0.1.8" : "0.0.0";
6180
+ var VERSION = true ? "0.2.0" : "0.0.0";
6007
6181
  var apiUrl = process.env.ADS_API_URL || getBaseUrl();
6008
6182
  if (apiUrl && (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1"))) {
6009
6183
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
6010
6184
  }
6011
- 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));
6012
6186
  var BANNER = [
6013
6187
  "",
6014
6188
  " " + b3(" ___ __ __ __ __ __"),
@@ -6017,7 +6191,7 @@ var BANNER = [
6017
6191
  " " + b3("/_/ |_\\_,_/___/ \\____/ .__/_/\\___/\\_,_/\\_,_/\\__/_/"),
6018
6192
  " " + b3(" /_/"),
6019
6193
  "",
6020
- " " + import_picocolors13.default.dim("Create Meta ads from the command line"),
6194
+ " " + import_picocolors14.default.dim("Create Meta ads from the command line"),
6021
6195
  ""
6022
6196
  ].join("\n");
6023
6197
  var program2 = new Command().name("ads").description("Ads Uploader CLI").version(VERSION).addHelpText("before", BANNER);
@@ -6027,6 +6201,7 @@ program2.addCommand(whoamiCommand());
6027
6201
  program2.addCommand(configCommand());
6028
6202
  program2.addCommand(accountsCommand());
6029
6203
  program2.addCommand(accountCommand());
6204
+ program2.addCommand(pagesCommand());
6030
6205
  program2.addCommand(campaignsCommand());
6031
6206
  program2.addCommand(campaignCommand());
6032
6207
  program2.addCommand(adsetsCommand());
@@ -6064,8 +6239,8 @@ async function checkForUpdates() {
6064
6239
  }
6065
6240
  if (latest !== VERSION) {
6066
6241
  console.error(`
6067
- ${import_picocolors13.default.yellow("Update available:")} ${import_picocolors13.default.dim(VERSION)} \u2192 ${import_picocolors13.default.green(latest)}`);
6068
- 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
6069
6244
  `);
6070
6245
  }
6071
6246
  } catch {
@@ -6089,7 +6264,7 @@ if (process.argv.length <= 2) {
6089
6264
  }
6090
6265
  if (err.code !== "commander.executeSubCommandAsync") {
6091
6266
  console.error(`
6092
- ${import_picocolors13.default.red("Error:")} ${err.message}
6267
+ ${import_picocolors14.default.red("Error:")} ${err.message}
6093
6268
  `);
6094
6269
  process.exit(1);
6095
6270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adsuploader/cli",
3
- "version": "0.1.8",
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"