@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.
- package/README.md +2 -0
- package/SKILL.md +43 -0
- package/dist/cli.cjs +436 -197
- 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
|
|
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
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
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
|
-
|
|
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/
|
|
4771
|
+
// src/commands/pages.js
|
|
4708
4772
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
4709
|
-
function
|
|
4710
|
-
if (!
|
|
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
|
-
${
|
|
4817
|
+
${import_picocolors8.default.bold(ad.name)}
|
|
4723
4818
|
`);
|
|
4724
|
-
console.log(` ${
|
|
4725
|
-
console.log(` ${
|
|
4726
|
-
console.log(` ${
|
|
4727
|
-
console.log(` ${
|
|
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) =>
|
|
4824
|
+
const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
|
|
4825
|
+
const exp = !!opts.expanded;
|
|
4730
4826
|
console.log("");
|
|
4731
|
-
console.log(` ${
|
|
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
|
-
${
|
|
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
|
|
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
|
-
${
|
|
4858
|
+
${import_picocolors9.default.bold(preset.name)}
|
|
4763
4859
|
`);
|
|
4764
4860
|
if (preset.config) {
|
|
4765
|
-
console.log(` ${
|
|
4766
|
-
console.log(` ${
|
|
4767
|
-
console.log(` ${
|
|
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
|
-
${
|
|
4875
|
+
${import_picocolors9.default.bold("Ad Template Presets")} (${all.length})
|
|
4780
4876
|
`);
|
|
4781
4877
|
if (all.length === 0) {
|
|
4782
|
-
console.log(
|
|
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 ?
|
|
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
|
-
${
|
|
4899
|
+
${import_picocolors9.default.bold(preset.name)}
|
|
4804
4900
|
`);
|
|
4805
|
-
console.log(` ${
|
|
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(` ${
|
|
4808
|
-
if (preset.text.bodies?.length) console.log(` ${
|
|
4809
|
-
if (preset.text.descriptions?.length) console.log(` ${
|
|
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
|
-
${
|
|
4917
|
+
${import_picocolors9.default.bold("Text Presets")} (${all.length})
|
|
4822
4918
|
`);
|
|
4823
4919
|
if (all.length === 0) {
|
|
4824
|
-
console.log(
|
|
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" ?
|
|
4830
|
-
{ key: "shared", label: "Shared", color: (v2) => v2 ?
|
|
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
|
|
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(
|
|
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({
|
|
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
|
-
${
|
|
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
|
|
4983
|
-
|
|
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(` ${
|
|
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
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
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(` ${
|
|
5222
|
+
console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
|
|
5011
5223
|
}
|
|
5012
|
-
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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
|
-
${
|
|
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" ? "" :
|
|
5288
|
+
const placement = a.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${a.placementType}]`);
|
|
5077
5289
|
return `${a.name}${placement}`;
|
|
5078
5290
|
});
|
|
5079
|
-
console.log(` ${
|
|
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
|
-
${
|
|
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(` ${
|
|
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
|
-
${
|
|
5324
|
+
${import_picocolors10.default.bold("Batch")} ${batch.batchId}
|
|
5113
5325
|
`);
|
|
5114
|
-
console.log(` ${
|
|
5115
|
-
console.log(` ${
|
|
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 ||
|
|
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
|
-
${
|
|
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(` ${
|
|
5345
|
+
console.log(` ${import_picocolors10.default.bold(groupLabel)}`);
|
|
5134
5346
|
for (const asset of group.assets) {
|
|
5135
|
-
const placement = asset.placementType === "default" ? "" :
|
|
5136
|
-
const primary = asset.isPrimary ?
|
|
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
|
-
${
|
|
5362
|
+
${import_picocolors10.default.bold("Recent Uploads")} (${batches.length})
|
|
5151
5363
|
`);
|
|
5152
5364
|
if (batches.length === 0) {
|
|
5153
|
-
console.log(
|
|
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
|
|
5394
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5183
5395
|
|
|
5184
5396
|
// src/lib/poll.js
|
|
5185
|
-
var
|
|
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
|
-
${
|
|
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
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
5271
|
-
console.log(` Resume with: ${
|
|
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
|
|
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:
|
|
5497
|
+
return { icon: "\u2713", color: import_picocolors11.default.green };
|
|
5286
5498
|
case "error":
|
|
5287
|
-
return { icon: "\u2717", color:
|
|
5499
|
+
return { icon: "\u2717", color: import_picocolors11.default.red };
|
|
5288
5500
|
case "retry":
|
|
5289
|
-
return { icon: "\u2212", color:
|
|
5501
|
+
return { icon: "\u2212", color: import_picocolors11.default.yellow };
|
|
5290
5502
|
default:
|
|
5291
|
-
return { icon: "\u2192", color:
|
|
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
|
|
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(`${
|
|
5328
|
-
parts.push(`${
|
|
5329
|
-
parts.push(`${
|
|
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 +=
|
|
5545
|
+
statusStr += import_picocolors12.default.dim(` (${levelLabel})`);
|
|
5334
5546
|
}
|
|
5335
5547
|
let enhLabel = "";
|
|
5336
|
-
if (result.enhancements === "metaDefaults") enhLabel =
|
|
5337
|
-
else if (result.enhancements === "all") enhLabel =
|
|
5338
|
-
else if (result.enhancements === "none") enhLabel =
|
|
5339
|
-
else if (Array.isArray(result.enhancements)) enhLabel =
|
|
5340
|
-
const enhPart = enhLabel ? ` ${
|
|
5341
|
-
console.log(` ${parts.join(", ")} ${
|
|
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 ||
|
|
5367
|
-
const adSetName = ad.adSetName ||
|
|
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
|
|
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(` ${
|
|
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 ?
|
|
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}${
|
|
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}${
|
|
5409
|
-
if (primary) console.log(`${fieldIndent}${
|
|
5410
|
-
if (desc) console.log(`${fieldIndent}${
|
|
5411
|
-
if (ad.cta && !sharedCta) console.log(`${fieldIndent}${
|
|
5412
|
-
if (ad.link && !sharedLink) console.log(`${fieldIndent}${
|
|
5413
|
-
if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${
|
|
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(` ${
|
|
5418
|
-
if (sharedLink) console.log(` ${
|
|
5419
|
-
if (sharedUrlTags) console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
5973
|
+
M2.info(import_picocolors12.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
|
|
5737
5974
|
const headlines = [];
|
|
5738
|
-
|
|
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(
|
|
5984
|
+
M2.info(import_picocolors12.default.dim("Enter primary text (one per line). Leave blank to finish."));
|
|
5748
5985
|
const bodies = [];
|
|
5749
|
-
|
|
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(
|
|
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
|
|
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(
|
|
5883
|
-
console.log(
|
|
5884
|
-
console.log(
|
|
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
|
-
${
|
|
6135
|
+
${import_picocolors13.default.bold("Job")} ${jobId}
|
|
5899
6136
|
`);
|
|
5900
|
-
console.log(` ${
|
|
5901
|
-
console.log(` ${
|
|
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(` ${
|
|
6141
|
+
console.log(` ${import_picocolors13.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
|
|
5905
6142
|
}
|
|
5906
6143
|
if (result.error) {
|
|
5907
|
-
console.log(` ${
|
|
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
|
|
6165
|
+
return import_picocolors13.default.green("Complete");
|
|
5929
6166
|
case "running":
|
|
5930
6167
|
case "in_progress":
|
|
5931
|
-
return
|
|
6168
|
+
return import_picocolors13.default.cyan("Running");
|
|
5932
6169
|
case "cancelled":
|
|
5933
|
-
return
|
|
6170
|
+
return import_picocolors13.default.yellow("Cancelled");
|
|
5934
6171
|
case "error":
|
|
5935
6172
|
case "failed":
|
|
5936
|
-
return
|
|
6173
|
+
return import_picocolors13.default.red("Failed");
|
|
5937
6174
|
default:
|
|
5938
|
-
return
|
|
6175
|
+
return import_picocolors13.default.dim(status || "unknown");
|
|
5939
6176
|
}
|
|
5940
6177
|
}
|
|
5941
6178
|
|
|
5942
6179
|
// src/cli.js
|
|
5943
|
-
var VERSION = true ? "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) =>
|
|
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
|
-
" " +
|
|
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
|
-
${
|
|
6004
|
-
console.error(` Run ${
|
|
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
|
-
${
|
|
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.
|
|
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"
|