@adsuploader/cli 0.1.8 → 0.2.1
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 +39 -0
- package/dist/cli.cjs +689 -240
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -1142,8 +1142,8 @@ var require_command = __commonJS({
|
|
|
1142
1142
|
"node_modules/commander/lib/command.js"(exports2) {
|
|
1143
1143
|
var EventEmitter = require("node:events").EventEmitter;
|
|
1144
1144
|
var childProcess = require("node:child_process");
|
|
1145
|
-
var
|
|
1146
|
-
var
|
|
1145
|
+
var path4 = require("node:path");
|
|
1146
|
+
var fs5 = require("node:fs");
|
|
1147
1147
|
var process2 = require("node:process");
|
|
1148
1148
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1149
1149
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2124,7 +2124,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2124
2124
|
* @param {string} subcommandName
|
|
2125
2125
|
*/
|
|
2126
2126
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2127
|
-
if (
|
|
2127
|
+
if (fs5.existsSync(executableFile)) return;
|
|
2128
2128
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2129
2129
|
const executableMissing = `'${executableFile}' does not exist
|
|
2130
2130
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2142,11 +2142,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2142
2142
|
let launchWithNode = false;
|
|
2143
2143
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2144
2144
|
function findFile(baseDir, baseName) {
|
|
2145
|
-
const localBin =
|
|
2146
|
-
if (
|
|
2147
|
-
if (sourceExt.includes(
|
|
2145
|
+
const localBin = path4.resolve(baseDir, baseName);
|
|
2146
|
+
if (fs5.existsSync(localBin)) return localBin;
|
|
2147
|
+
if (sourceExt.includes(path4.extname(baseName))) return void 0;
|
|
2148
2148
|
const foundExt = sourceExt.find(
|
|
2149
|
-
(ext) =>
|
|
2149
|
+
(ext) => fs5.existsSync(`${localBin}${ext}`)
|
|
2150
2150
|
);
|
|
2151
2151
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2152
2152
|
return void 0;
|
|
@@ -2158,21 +2158,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2158
2158
|
if (this._scriptPath) {
|
|
2159
2159
|
let resolvedScriptPath;
|
|
2160
2160
|
try {
|
|
2161
|
-
resolvedScriptPath =
|
|
2161
|
+
resolvedScriptPath = fs5.realpathSync(this._scriptPath);
|
|
2162
2162
|
} catch {
|
|
2163
2163
|
resolvedScriptPath = this._scriptPath;
|
|
2164
2164
|
}
|
|
2165
|
-
executableDir =
|
|
2166
|
-
|
|
2165
|
+
executableDir = path4.resolve(
|
|
2166
|
+
path4.dirname(resolvedScriptPath),
|
|
2167
2167
|
executableDir
|
|
2168
2168
|
);
|
|
2169
2169
|
}
|
|
2170
2170
|
if (executableDir) {
|
|
2171
2171
|
let localFile = findFile(executableDir, executableFile);
|
|
2172
2172
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2173
|
-
const legacyName =
|
|
2173
|
+
const legacyName = path4.basename(
|
|
2174
2174
|
this._scriptPath,
|
|
2175
|
-
|
|
2175
|
+
path4.extname(this._scriptPath)
|
|
2176
2176
|
);
|
|
2177
2177
|
if (legacyName !== this._name) {
|
|
2178
2178
|
localFile = findFile(
|
|
@@ -2183,7 +2183,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2183
2183
|
}
|
|
2184
2184
|
executableFile = localFile || executableFile;
|
|
2185
2185
|
}
|
|
2186
|
-
launchWithNode = sourceExt.includes(
|
|
2186
|
+
launchWithNode = sourceExt.includes(path4.extname(executableFile));
|
|
2187
2187
|
let proc;
|
|
2188
2188
|
if (process2.platform !== "win32") {
|
|
2189
2189
|
if (launchWithNode) {
|
|
@@ -3030,7 +3030,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3030
3030
|
* @return {Command}
|
|
3031
3031
|
*/
|
|
3032
3032
|
nameFromFilename(filename) {
|
|
3033
|
-
this._name =
|
|
3033
|
+
this._name = path4.basename(filename, path4.extname(filename));
|
|
3034
3034
|
return this;
|
|
3035
3035
|
}
|
|
3036
3036
|
/**
|
|
@@ -3044,9 +3044,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3044
3044
|
* @param {string} [path]
|
|
3045
3045
|
* @return {(string|null|Command)}
|
|
3046
3046
|
*/
|
|
3047
|
-
executableDir(
|
|
3048
|
-
if (
|
|
3049
|
-
this._executableDir =
|
|
3047
|
+
executableDir(path5) {
|
|
3048
|
+
if (path5 === void 0) return this._executableDir;
|
|
3049
|
+
this._executableDir = path5;
|
|
3050
3050
|
return this;
|
|
3051
3051
|
}
|
|
3052
3052
|
/**
|
|
@@ -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
|
}
|
|
@@ -4145,8 +4186,8 @@ function createClient(opts = {}) {
|
|
|
4145
4186
|
throw new Error(`Refusing to connect over insecure HTTP: ${baseUrl}. Use HTTPS or localhost.`);
|
|
4146
4187
|
}
|
|
4147
4188
|
}
|
|
4148
|
-
async function request(method,
|
|
4149
|
-
const url = new URL(`/api/v1${
|
|
4189
|
+
async function request(method, path4, { body, query, requiresAccount = true, raw = false } = {}) {
|
|
4190
|
+
const url = new URL(`/api/v1${path4}`, baseUrl);
|
|
4150
4191
|
if (query) {
|
|
4151
4192
|
for (const [k3, v2] of Object.entries(query)) {
|
|
4152
4193
|
if (v2 != null) url.searchParams.set(k3, v2);
|
|
@@ -4162,43 +4203,65 @@ 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
|
-
get: (
|
|
4194
|
-
post: (
|
|
4195
|
-
del: (
|
|
4253
|
+
get: (path4, opts2) => request("GET", path4, { ...opts2, requiresAccount: opts2?.requiresAccount ?? true }),
|
|
4254
|
+
post: (path4, body, opts2) => request("POST", path4, { body, ...opts2 }),
|
|
4255
|
+
del: (path4, body, opts2) => request("DELETE", path4, { body, ...opts2 }),
|
|
4196
4256
|
accounts: {
|
|
4197
4257
|
list: () => request("GET", "/accounts", { requiresAccount: false })
|
|
4198
4258
|
},
|
|
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
|
},
|
|
@@ -4215,7 +4278,7 @@ function createClient(opts = {}) {
|
|
|
4215
4278
|
},
|
|
4216
4279
|
uploads: {
|
|
4217
4280
|
list: (query) => request("GET", "/uploads", { query }),
|
|
4218
|
-
init: (files) => request("POST", "/uploads/init", { body: { files } }),
|
|
4281
|
+
init: (files, { batchId } = {}) => request("POST", "/uploads/init", { body: { files, ...batchId ? { batchId } : {} } }),
|
|
4219
4282
|
process: (batchId, file) => request("POST", `/uploads/${batchId}/process`, { body: file }),
|
|
4220
4283
|
finalize: (batchId) => request("POST", `/uploads/${batchId}/finalize`, { body: {} }),
|
|
4221
4284
|
status: (batchId) => request("GET", `/uploads/${batchId}`)
|
|
@@ -4290,6 +4353,9 @@ function printSuccess(msg) {
|
|
|
4290
4353
|
function printError(msg) {
|
|
4291
4354
|
console.error(` ${import_picocolors3.default.red("\u2716")} ${msg}`);
|
|
4292
4355
|
}
|
|
4356
|
+
function printWarn(msg) {
|
|
4357
|
+
console.log(` ${import_picocolors3.default.yellow("\u26A0")} ${msg}`);
|
|
4358
|
+
}
|
|
4293
4359
|
function printInfo(msg) {
|
|
4294
4360
|
console.log(` ${import_picocolors3.default.blue("\u2139")} ${msg}`);
|
|
4295
4361
|
}
|
|
@@ -4705,8 +4771,56 @@ function adsetCommand() {
|
|
|
4705
4771
|
});
|
|
4706
4772
|
}
|
|
4707
4773
|
|
|
4708
|
-
// src/commands/
|
|
4774
|
+
// src/commands/pages.js
|
|
4709
4775
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
4776
|
+
function formatInstagram(page) {
|
|
4777
|
+
if (!page.instagramId && !page.instagramUsername) return "";
|
|
4778
|
+
if (page.instagramUsername && page.instagramId) return `${page.instagramUsername} (${page.instagramId})`;
|
|
4779
|
+
return page.instagramUsername || page.instagramId;
|
|
4780
|
+
}
|
|
4781
|
+
function pagesCommand() {
|
|
4782
|
+
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) => {
|
|
4783
|
+
const client = createClient({ accountId: opts.account });
|
|
4784
|
+
const { pages } = await client.pages.list();
|
|
4785
|
+
if (shouldOutputJson(opts)) {
|
|
4786
|
+
printJson(pages);
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
const rows = pages.map((page) => ({
|
|
4790
|
+
...page,
|
|
4791
|
+
instagram: formatInstagram(page)
|
|
4792
|
+
}));
|
|
4793
|
+
console.log(`
|
|
4794
|
+
${import_picocolors7.default.bold("Pages")} (${pages.length})
|
|
4795
|
+
`);
|
|
4796
|
+
printTable(rows, [
|
|
4797
|
+
{ key: "id", label: "ID", maxWidth: 25 },
|
|
4798
|
+
{ key: "name", label: "Name", maxWidth: 40 },
|
|
4799
|
+
{ key: "instagram", label: "Instagram", maxWidth: 36 }
|
|
4800
|
+
]);
|
|
4801
|
+
console.log("");
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// src/commands/ad.js
|
|
4806
|
+
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
4807
|
+
|
|
4808
|
+
// src/lib/profile-display.js
|
|
4809
|
+
var NOT_SET = "not set";
|
|
4810
|
+
function formatProfileValues(profile) {
|
|
4811
|
+
const pageId = profile?.page?.id || null;
|
|
4812
|
+
const pageName = profile?.page?.name || null;
|
|
4813
|
+
const instagramId = profile?.instagram?.id || null;
|
|
4814
|
+
const instagramUsername = profile?.instagram?.username || null;
|
|
4815
|
+
const threadsId = profile?.threads?.id || null;
|
|
4816
|
+
return {
|
|
4817
|
+
page: pageName && pageId ? `${pageName} (${pageId})` : pageId || NOT_SET,
|
|
4818
|
+
instagram: instagramUsername && instagramId ? `@${instagramUsername} (${instagramId})` : instagramId || NOT_SET,
|
|
4819
|
+
threads: threadsId || NOT_SET
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
// src/commands/ad.js
|
|
4710
4824
|
function truncate(str, max = 120, expanded = false) {
|
|
4711
4825
|
if (!str || expanded || str.length <= max) return str;
|
|
4712
4826
|
return str.slice(0, max) + "...";
|
|
@@ -4720,17 +4834,26 @@ function adCommand() {
|
|
|
4720
4834
|
return;
|
|
4721
4835
|
}
|
|
4722
4836
|
console.log(`
|
|
4723
|
-
${
|
|
4837
|
+
${import_picocolors8.default.bold(ad.name)}
|
|
4724
4838
|
`);
|
|
4725
|
-
console.log(` ${
|
|
4726
|
-
console.log(` ${
|
|
4727
|
-
console.log(` ${
|
|
4728
|
-
console.log(` ${
|
|
4839
|
+
console.log(` ${import_picocolors8.default.bold("ID:")} ${ad.id}`);
|
|
4840
|
+
console.log(` ${import_picocolors8.default.bold("Status:")} ${statusColor(ad.status)}`);
|
|
4841
|
+
console.log(` ${import_picocolors8.default.bold("Campaign:")} ${ad.campaignId}`);
|
|
4842
|
+
console.log(` ${import_picocolors8.default.bold("Ad Set:")} ${ad.adSetId}`);
|
|
4843
|
+
if (ad.profile) {
|
|
4844
|
+
const profile = formatProfileValues(ad.profile);
|
|
4845
|
+
const label = (name) => import_picocolors8.default.bold(name.padEnd(12));
|
|
4846
|
+
const display = (value) => value === "not set" ? import_picocolors8.default.dim(value) : value;
|
|
4847
|
+
console.log("");
|
|
4848
|
+
console.log(` ${label("Page:")}${display(profile.page)}`);
|
|
4849
|
+
console.log(` ${label("Instagram:")}${display(profile.instagram)}`);
|
|
4850
|
+
console.log(` ${label("Threads:")}${display(profile.threads)}`);
|
|
4851
|
+
}
|
|
4729
4852
|
if (ad.creative) {
|
|
4730
|
-
const label = (name) =>
|
|
4853
|
+
const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
|
|
4731
4854
|
const exp = !!opts.expanded;
|
|
4732
4855
|
console.log("");
|
|
4733
|
-
console.log(` ${
|
|
4856
|
+
console.log(` ${import_picocolors8.default.bold("Creative")}`);
|
|
4734
4857
|
if (ad.creative.headline) console.log(` ${label("Headline:")}${truncate(ad.creative.headline, 120, exp)}`);
|
|
4735
4858
|
if (ad.creative.headlines) console.log(` ${label("Headlines:")}${ad.creative.headlines.map((h2) => truncate(h2, 60, exp)).join(" | ")}`);
|
|
4736
4859
|
if (ad.creative.primaryText) console.log(` ${label("Primary Text:")}${truncate(ad.creative.primaryText, 120, exp)}`);
|
|
@@ -4744,13 +4867,13 @@ function adCommand() {
|
|
|
4744
4867
|
if (ad.creative.enhancements) console.log(` ${label("Enhancements:")}${ad.creative.enhancements.join(", ")}`);
|
|
4745
4868
|
}
|
|
4746
4869
|
console.log(`
|
|
4747
|
-
${
|
|
4870
|
+
${import_picocolors8.default.dim("Use this ad as a template:")} ads create --copy-from ${ad.id}
|
|
4748
4871
|
`);
|
|
4749
4872
|
});
|
|
4750
4873
|
}
|
|
4751
4874
|
|
|
4752
4875
|
// src/commands/presets.js
|
|
4753
|
-
var
|
|
4876
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
4754
4877
|
function presetsCommand() {
|
|
4755
4878
|
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
4879
|
const client = createClient({ accountId: opts.account });
|
|
@@ -4761,12 +4884,12 @@ function presetsCommand() {
|
|
|
4761
4884
|
return;
|
|
4762
4885
|
}
|
|
4763
4886
|
console.log(`
|
|
4764
|
-
${
|
|
4887
|
+
${import_picocolors9.default.bold(preset.name)}
|
|
4765
4888
|
`);
|
|
4766
4889
|
if (preset.config) {
|
|
4767
|
-
console.log(` ${
|
|
4768
|
-
console.log(` ${
|
|
4769
|
-
console.log(` ${
|
|
4890
|
+
console.log(` ${import_picocolors9.default.bold("Campaign:")} ${preset.config.campaign?.name || import_picocolors9.default.dim("\u2014")} ${import_picocolors9.default.dim(preset.config.campaign?.id || "")}`);
|
|
4891
|
+
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 || "")}`);
|
|
4892
|
+
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
4893
|
}
|
|
4771
4894
|
console.log("");
|
|
4772
4895
|
return;
|
|
@@ -4778,15 +4901,15 @@ function presetsCommand() {
|
|
|
4778
4901
|
return;
|
|
4779
4902
|
}
|
|
4780
4903
|
console.log(`
|
|
4781
|
-
${
|
|
4904
|
+
${import_picocolors9.default.bold("Ad Template Presets")} (${all.length})
|
|
4782
4905
|
`);
|
|
4783
4906
|
if (all.length === 0) {
|
|
4784
|
-
console.log(
|
|
4907
|
+
console.log(import_picocolors9.default.dim(" No presets. Create one in the web app by saving an ad configuration."));
|
|
4785
4908
|
} else {
|
|
4786
4909
|
printTable(all, [
|
|
4787
4910
|
{ key: "id", label: "ID", maxWidth: 28 },
|
|
4788
4911
|
{ key: "name", label: "Name", maxWidth: 40 },
|
|
4789
|
-
{ key: "shared", label: "Shared", color: (v2) => v2 ?
|
|
4912
|
+
{ key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors9.default.cyan("team") : import_picocolors9.default.dim("\u2014") }
|
|
4790
4913
|
]);
|
|
4791
4914
|
}
|
|
4792
4915
|
console.log("");
|
|
@@ -4802,13 +4925,13 @@ function textPresetsCommand() {
|
|
|
4802
4925
|
return;
|
|
4803
4926
|
}
|
|
4804
4927
|
console.log(`
|
|
4805
|
-
${
|
|
4928
|
+
${import_picocolors9.default.bold(preset.name)}
|
|
4806
4929
|
`);
|
|
4807
|
-
console.log(` ${
|
|
4930
|
+
console.log(` ${import_picocolors9.default.bold("Includes:")} ${preset.includedFields?.join(", ") || "all fields"}`);
|
|
4808
4931
|
if (preset.text) {
|
|
4809
|
-
if (preset.text.titles?.length) console.log(` ${
|
|
4810
|
-
if (preset.text.bodies?.length) console.log(` ${
|
|
4811
|
-
if (preset.text.descriptions?.length) console.log(` ${
|
|
4932
|
+
if (preset.text.titles?.length) console.log(` ${import_picocolors9.default.bold("Headlines:")} ${preset.text.titles.join(" | ")}`);
|
|
4933
|
+
if (preset.text.bodies?.length) console.log(` ${import_picocolors9.default.bold("Bodies:")} ${preset.text.bodies.join(" | ")}`);
|
|
4934
|
+
if (preset.text.descriptions?.length) console.log(` ${import_picocolors9.default.bold("Desc:")} ${preset.text.descriptions.join(" | ")}`);
|
|
4812
4935
|
}
|
|
4813
4936
|
console.log("");
|
|
4814
4937
|
return;
|
|
@@ -4820,16 +4943,16 @@ function textPresetsCommand() {
|
|
|
4820
4943
|
return;
|
|
4821
4944
|
}
|
|
4822
4945
|
console.log(`
|
|
4823
|
-
${
|
|
4946
|
+
${import_picocolors9.default.bold("Text Presets")} (${all.length})
|
|
4824
4947
|
`);
|
|
4825
4948
|
if (all.length === 0) {
|
|
4826
|
-
console.log(
|
|
4949
|
+
console.log(import_picocolors9.default.dim(" No text presets. Create one in the web app."));
|
|
4827
4950
|
} else {
|
|
4828
4951
|
printTable(all, [
|
|
4829
4952
|
{ key: "id", label: "ID", maxWidth: 28 },
|
|
4830
4953
|
{ key: "name", label: "Name", maxWidth: 30 },
|
|
4831
|
-
{ key: "scope", label: "Scope", color: (v2) => v2 === "global" ?
|
|
4832
|
-
{ key: "shared", label: "Shared", color: (v2) => v2 ?
|
|
4954
|
+
{ key: "scope", label: "Scope", color: (v2) => v2 === "global" ? import_picocolors9.default.cyan("global") : import_picocolors9.default.dim("account") },
|
|
4955
|
+
{ key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors9.default.cyan("team") : import_picocolors9.default.dim("\u2014") }
|
|
4833
4956
|
]);
|
|
4834
4957
|
}
|
|
4835
4958
|
console.log("");
|
|
@@ -4867,11 +4990,11 @@ function presetsSaveCommand() {
|
|
|
4867
4990
|
return;
|
|
4868
4991
|
}
|
|
4869
4992
|
printSuccess(`Saved preset "${preset.name}"`);
|
|
4870
|
-
console.log(` ${
|
|
4871
|
-
console.log(` ${
|
|
4872
|
-
if (preset.shared) console.log(` ${
|
|
4993
|
+
console.log(` ${import_picocolors9.default.bold("ID:")} ${preset.id}`);
|
|
4994
|
+
console.log(` ${import_picocolors9.default.bold("Type:")} ${preset.type}`);
|
|
4995
|
+
if (preset.shared) console.log(` ${import_picocolors9.default.bold("Shared:")} ${import_picocolors9.default.cyan("team")}`);
|
|
4873
4996
|
console.log(`
|
|
4874
|
-
${
|
|
4997
|
+
${import_picocolors9.default.dim("Use it:")} ads create --preset ${preset.id} ...
|
|
4875
4998
|
`);
|
|
4876
4999
|
} catch (err) {
|
|
4877
5000
|
printError(err.message || "Failed to save preset");
|
|
@@ -4881,12 +5004,78 @@ function presetsSaveCommand() {
|
|
|
4881
5004
|
}
|
|
4882
5005
|
|
|
4883
5006
|
// src/commands/upload.js
|
|
5007
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
5008
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
5009
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
5010
|
+
|
|
5011
|
+
// src/lib/upload-state.js
|
|
4884
5012
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
4885
5013
|
var import_path2 = __toESM(require("path"), 1);
|
|
4886
|
-
var
|
|
5014
|
+
var import_os2 = __toESM(require("os"), 1);
|
|
5015
|
+
var DEFAULT_CONFIG_DIR = import_path2.default.join(import_os2.default.homedir(), ".config", "adsuploader");
|
|
5016
|
+
function uploadsDir(baseDir = DEFAULT_CONFIG_DIR) {
|
|
5017
|
+
return import_path2.default.join(baseDir, "uploads");
|
|
5018
|
+
}
|
|
5019
|
+
function stateFile(batchId, baseDir = DEFAULT_CONFIG_DIR) {
|
|
5020
|
+
return import_path2.default.join(uploadsDir(baseDir), `${batchId}.json`);
|
|
5021
|
+
}
|
|
5022
|
+
function ensureUploadsDir(baseDir = DEFAULT_CONFIG_DIR) {
|
|
5023
|
+
import_fs2.default.mkdirSync(uploadsDir(baseDir), { recursive: true, mode: 448 });
|
|
5024
|
+
}
|
|
5025
|
+
function saveFailedBatch({ batchId, accountId, failedFiles }, { baseDir } = {}) {
|
|
5026
|
+
ensureUploadsDir(baseDir);
|
|
5027
|
+
const data = {
|
|
5028
|
+
batchId,
|
|
5029
|
+
accountId: accountId || null,
|
|
5030
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5031
|
+
failedFiles: failedFiles.map((file) => ({
|
|
5032
|
+
path: file.path,
|
|
5033
|
+
name: file.name,
|
|
5034
|
+
type: file.type,
|
|
5035
|
+
size: file.size,
|
|
5036
|
+
contentType: file.contentType
|
|
5037
|
+
}))
|
|
5038
|
+
};
|
|
5039
|
+
import_fs2.default.writeFileSync(stateFile(batchId, baseDir), JSON.stringify(data, null, 2), { mode: 384 });
|
|
5040
|
+
if (process.platform !== "win32") {
|
|
5041
|
+
import_fs2.default.chmodSync(stateFile(batchId, baseDir), 384);
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
function loadFailedBatch(batchId, { baseDir } = {}) {
|
|
5045
|
+
const filePath = stateFile(batchId, baseDir);
|
|
5046
|
+
if (!import_fs2.default.existsSync(filePath)) return null;
|
|
5047
|
+
return JSON.parse(import_fs2.default.readFileSync(filePath, "utf8"));
|
|
5048
|
+
}
|
|
5049
|
+
function findLatestFailedBatch({ baseDir } = {}) {
|
|
5050
|
+
const dir = uploadsDir(baseDir);
|
|
5051
|
+
if (!import_fs2.default.existsSync(dir)) return null;
|
|
5052
|
+
const batches = import_fs2.default.readdirSync(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
5053
|
+
try {
|
|
5054
|
+
const data = JSON.parse(import_fs2.default.readFileSync(import_path2.default.join(dir, name), "utf8"));
|
|
5055
|
+
return data?.batchId && data?.createdAt ? data : null;
|
|
5056
|
+
} catch {
|
|
5057
|
+
return null;
|
|
5058
|
+
}
|
|
5059
|
+
}).filter(Boolean).sort((a, b4) => new Date(b4.createdAt) - new Date(a.createdAt));
|
|
5060
|
+
return batches[0] || null;
|
|
5061
|
+
}
|
|
5062
|
+
function clearFailedBatch(batchId, { baseDir } = {}) {
|
|
5063
|
+
try {
|
|
5064
|
+
import_fs2.default.unlinkSync(stateFile(batchId, baseDir));
|
|
5065
|
+
} catch (err) {
|
|
5066
|
+
if (err.code !== "ENOENT") throw err;
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
// src/commands/upload.js
|
|
4887
5071
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]);
|
|
4888
5072
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]);
|
|
4889
5073
|
var MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024;
|
|
5074
|
+
var DEFAULT_UPLOAD_CONCURRENCY = 4;
|
|
5075
|
+
var MAX_UPLOAD_CONCURRENCY = 6;
|
|
5076
|
+
var DEFAULT_UPLOAD_TIMEOUT_MS = 12e4;
|
|
5077
|
+
var DEFAULT_R2_MAX_ATTEMPTS = 4;
|
|
5078
|
+
var DEFAULT_MAX_IN_FLIGHT_BYTES = 512 * 1024 * 1024;
|
|
4890
5079
|
var MAGIC_BYTES = {
|
|
4891
5080
|
"image/jpeg": [[255, 216, 255]],
|
|
4892
5081
|
"image/png": [[137, 80, 78, 71]],
|
|
@@ -4902,19 +5091,19 @@ var MAGIC_BYTES = {
|
|
|
4902
5091
|
// ftyp container — check below
|
|
4903
5092
|
};
|
|
4904
5093
|
function validateMagicBytes(filePath, contentType) {
|
|
4905
|
-
const stat =
|
|
5094
|
+
const stat = import_fs3.default.statSync(filePath);
|
|
4906
5095
|
if (stat.size < 12) return false;
|
|
4907
5096
|
let fd;
|
|
4908
5097
|
try {
|
|
4909
|
-
fd =
|
|
5098
|
+
fd = import_fs3.default.openSync(filePath, "r");
|
|
4910
5099
|
const buf = Buffer.alloc(12);
|
|
4911
|
-
|
|
4912
|
-
|
|
5100
|
+
import_fs3.default.readSync(fd, buf, 0, 12, 0);
|
|
5101
|
+
import_fs3.default.closeSync(fd);
|
|
4913
5102
|
fd = null;
|
|
4914
5103
|
return checkMagicBytes(buf, contentType);
|
|
4915
5104
|
} finally {
|
|
4916
5105
|
if (fd != null) try {
|
|
4917
|
-
|
|
5106
|
+
import_fs3.default.closeSync(fd);
|
|
4918
5107
|
} catch {
|
|
4919
5108
|
}
|
|
4920
5109
|
}
|
|
@@ -4950,8 +5139,107 @@ var CONTENT_TYPES = {
|
|
|
4950
5139
|
".webm": "video/webm",
|
|
4951
5140
|
".m4v": "video/x-m4v"
|
|
4952
5141
|
};
|
|
5142
|
+
function parsePositiveInt2(value, fallback) {
|
|
5143
|
+
const parsed = Number.parseInt(value, 10);
|
|
5144
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
5145
|
+
}
|
|
5146
|
+
function normalizeConcurrency(value) {
|
|
5147
|
+
return Math.min(MAX_UPLOAD_CONCURRENCY, parsePositiveInt2(value, DEFAULT_UPLOAD_CONCURRENCY));
|
|
5148
|
+
}
|
|
5149
|
+
function calculateUploadConcurrency(files, requestedConcurrency, maxInFlightBytes = DEFAULT_MAX_IN_FLIGHT_BYTES) {
|
|
5150
|
+
const requested = normalizeConcurrency(requestedConcurrency);
|
|
5151
|
+
const largestFileSize = Math.max(0, ...files.map((file) => Number(file.size) || 0));
|
|
5152
|
+
if (largestFileSize <= 0) return requested;
|
|
5153
|
+
const memoryLimited = Math.max(1, Math.floor(maxInFlightBytes / largestFileSize));
|
|
5154
|
+
return Math.min(requested, memoryLimited);
|
|
5155
|
+
}
|
|
5156
|
+
function sleep2(ms) {
|
|
5157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5158
|
+
}
|
|
5159
|
+
async function runBoundedConcurrency(items, concurrency, worker) {
|
|
5160
|
+
const limit = Math.max(1, Math.min(items.length || 1, concurrency));
|
|
5161
|
+
const results = new Array(items.length);
|
|
5162
|
+
let nextIndex = 0;
|
|
5163
|
+
async function runNext() {
|
|
5164
|
+
while (nextIndex < items.length) {
|
|
5165
|
+
const index = nextIndex++;
|
|
5166
|
+
results[index] = await worker(items[index], index);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
await Promise.all(Array.from({ length: limit }, runNext));
|
|
5170
|
+
return results;
|
|
5171
|
+
}
|
|
5172
|
+
async function putR2WithRetry(uploadUrl, fileBuffer, contentType, {
|
|
5173
|
+
fetchImpl = fetch,
|
|
5174
|
+
timeoutMs = DEFAULT_UPLOAD_TIMEOUT_MS,
|
|
5175
|
+
maxAttempts = DEFAULT_R2_MAX_ATTEMPTS,
|
|
5176
|
+
delay = sleep2
|
|
5177
|
+
} = {}) {
|
|
5178
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5179
|
+
const controller = new AbortController();
|
|
5180
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
5181
|
+
try {
|
|
5182
|
+
const resp = await fetchImpl(uploadUrl, {
|
|
5183
|
+
method: "PUT",
|
|
5184
|
+
headers: {
|
|
5185
|
+
"Content-Type": contentType,
|
|
5186
|
+
"Content-Length": String(fileBuffer.length)
|
|
5187
|
+
},
|
|
5188
|
+
body: fileBuffer,
|
|
5189
|
+
signal: controller.signal
|
|
5190
|
+
});
|
|
5191
|
+
clearTimeout(timeout);
|
|
5192
|
+
if (resp.ok) return resp;
|
|
5193
|
+
if (isRetryableStatus(resp.status) && attempt < maxAttempts) {
|
|
5194
|
+
const retryAfter = resp.status === 429 ? parsePositiveInt2(resp.headers.get("retry-after"), null) : null;
|
|
5195
|
+
await delay(getRetryDelayMs(attempt, retryAfter));
|
|
5196
|
+
continue;
|
|
5197
|
+
}
|
|
5198
|
+
throw new Error(`R2 upload failed: ${resp.status}`);
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
clearTimeout(timeout);
|
|
5201
|
+
const normalizedError = err?.name === "AbortError" ? new Error(`R2 upload timed out after ${Math.ceil(timeoutMs / 1e3)}s`) : err;
|
|
5202
|
+
if (attempt < maxAttempts && isRetryableNetworkError(normalizedError)) {
|
|
5203
|
+
await delay(getRetryDelayMs(attempt));
|
|
5204
|
+
continue;
|
|
5205
|
+
}
|
|
5206
|
+
throw normalizedError;
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
throw new Error(`R2 upload failed after ${maxAttempts} attempt(s)`);
|
|
5210
|
+
}
|
|
5211
|
+
function isRetryableProcessError(err) {
|
|
5212
|
+
const status = Number.isFinite(err?.status) ? err.status : null;
|
|
5213
|
+
if (status >= 400 && status < 500) return false;
|
|
5214
|
+
if (isRetryableNetworkError(err)) return true;
|
|
5215
|
+
if (status >= 500) return true;
|
|
5216
|
+
const message = String(err?.message || "");
|
|
5217
|
+
return /is not valid JSON|Unexpected token|<!DOCTYPE/i.test(message) || message.includes("Facebook image upload failed");
|
|
5218
|
+
}
|
|
5219
|
+
async function processFileWithRetry(client, batchId, file, {
|
|
5220
|
+
sleep: delay = sleep2,
|
|
5221
|
+
maxAttempts = 4
|
|
5222
|
+
} = {}) {
|
|
5223
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5224
|
+
try {
|
|
5225
|
+
return await client.uploads.process(batchId, {
|
|
5226
|
+
name: file.name,
|
|
5227
|
+
type: file.type,
|
|
5228
|
+
size: file.size,
|
|
5229
|
+
fileKey: file.fileKey
|
|
5230
|
+
});
|
|
5231
|
+
} catch (err) {
|
|
5232
|
+
if (attempt < maxAttempts && isRetryableProcessError(err)) {
|
|
5233
|
+
await delay(getRetryDelayMs(attempt));
|
|
5234
|
+
continue;
|
|
5235
|
+
}
|
|
5236
|
+
throw err;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
throw new Error(`Upload processing failed after ${maxAttempts} attempt(s)`);
|
|
5240
|
+
}
|
|
4953
5241
|
function getFileType(filePath) {
|
|
4954
|
-
const ext =
|
|
5242
|
+
const ext = import_path3.default.extname(filePath).toLowerCase();
|
|
4955
5243
|
if (IMAGE_EXTS.has(ext)) return "image";
|
|
4956
5244
|
if (VIDEO_EXTS.has(ext)) return "video";
|
|
4957
5245
|
return null;
|
|
@@ -4960,22 +5248,22 @@ function resolveFiles(inputs) {
|
|
|
4960
5248
|
const files = [];
|
|
4961
5249
|
const skipped = [];
|
|
4962
5250
|
for (const input of inputs) {
|
|
4963
|
-
const resolved =
|
|
4964
|
-
if (!
|
|
5251
|
+
const resolved = import_path3.default.resolve(input);
|
|
5252
|
+
if (!import_fs3.default.existsSync(resolved)) {
|
|
4965
5253
|
throw new Error(`File not found: ${input}`);
|
|
4966
5254
|
}
|
|
4967
|
-
const stat =
|
|
5255
|
+
const stat = import_fs3.default.statSync(resolved);
|
|
4968
5256
|
if (stat.isDirectory()) {
|
|
4969
|
-
const entries =
|
|
5257
|
+
const entries = import_fs3.default.readdirSync(resolved);
|
|
4970
5258
|
for (const entry of entries) {
|
|
4971
|
-
const fullPath =
|
|
4972
|
-
const entryStat =
|
|
5259
|
+
const fullPath = import_path3.default.join(resolved, entry);
|
|
5260
|
+
const entryStat = import_fs3.default.statSync(fullPath);
|
|
4973
5261
|
if (entryStat.isFile() && getFileType(fullPath)) {
|
|
4974
5262
|
if (entryStat.size > MAX_FILE_SIZE || entryStat.size === 0) {
|
|
4975
5263
|
skipped.push(`${entry} (${entryStat.size === 0 ? "empty" : "too large"})`);
|
|
4976
5264
|
continue;
|
|
4977
5265
|
}
|
|
4978
|
-
const ct = CONTENT_TYPES[
|
|
5266
|
+
const ct = CONTENT_TYPES[import_path3.default.extname(fullPath).toLowerCase()];
|
|
4979
5267
|
if (!validateMagicBytes(fullPath, ct)) {
|
|
4980
5268
|
skipped.push(`${entry} (content does not match extension)`);
|
|
4981
5269
|
continue;
|
|
@@ -4988,22 +5276,62 @@ function resolveFiles(inputs) {
|
|
|
4988
5276
|
if (!type) throw new Error(`Unsupported file type: ${input}`);
|
|
4989
5277
|
if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large: ${input} (${formatSize(stat.size)}, max ${formatSize(MAX_FILE_SIZE)})`);
|
|
4990
5278
|
if (stat.size === 0) throw new Error(`File is empty: ${input}`);
|
|
4991
|
-
const ct = CONTENT_TYPES[
|
|
5279
|
+
const ct = CONTENT_TYPES[import_path3.default.extname(resolved).toLowerCase()];
|
|
4992
5280
|
if (!validateMagicBytes(resolved, ct)) throw new Error(`File content does not match its extension: ${input}`);
|
|
4993
|
-
files.push({ path: resolved, name:
|
|
5281
|
+
files.push({ path: resolved, name: import_path3.default.basename(resolved), size: stat.size, type, contentType: ct });
|
|
4994
5282
|
}
|
|
4995
5283
|
}
|
|
4996
5284
|
if (skipped.length > 0) {
|
|
4997
|
-
console.error(
|
|
5285
|
+
console.error(import_picocolors10.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
|
|
4998
5286
|
}
|
|
4999
5287
|
return files;
|
|
5000
5288
|
}
|
|
5001
|
-
|
|
5002
|
-
const
|
|
5289
|
+
function resolveRetryFiles(failedFiles) {
|
|
5290
|
+
const files = [];
|
|
5291
|
+
const skipped = [];
|
|
5292
|
+
for (const file of failedFiles) {
|
|
5293
|
+
if (!file.path || !import_fs3.default.existsSync(file.path)) {
|
|
5294
|
+
skipped.push(`${file.name || import_path3.default.basename(file.path || "unknown")} (missing)`);
|
|
5295
|
+
continue;
|
|
5296
|
+
}
|
|
5297
|
+
const stat = import_fs3.default.statSync(file.path);
|
|
5298
|
+
if (!stat.isFile()) {
|
|
5299
|
+
skipped.push(`${file.name || import_path3.default.basename(file.path)} (not a file)`);
|
|
5300
|
+
continue;
|
|
5301
|
+
}
|
|
5302
|
+
const type = getFileType(file.path) || file.type;
|
|
5303
|
+
if (!type) {
|
|
5304
|
+
skipped.push(`${file.name || import_path3.default.basename(file.path)} (unsupported)`);
|
|
5305
|
+
continue;
|
|
5306
|
+
}
|
|
5307
|
+
const contentType = file.contentType || CONTENT_TYPES[import_path3.default.extname(file.path).toLowerCase()];
|
|
5308
|
+
if (contentType && !validateMagicBytes(file.path, contentType)) {
|
|
5309
|
+
skipped.push(`${file.name || import_path3.default.basename(file.path)} (content does not match extension)`);
|
|
5310
|
+
continue;
|
|
5311
|
+
}
|
|
5312
|
+
files.push({
|
|
5313
|
+
path: file.path,
|
|
5314
|
+
name: file.name || import_path3.default.basename(file.path),
|
|
5315
|
+
size: stat.size,
|
|
5316
|
+
type,
|
|
5317
|
+
contentType
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
if (skipped.length > 0) {
|
|
5321
|
+
console.error(import_picocolors10.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
|
|
5322
|
+
}
|
|
5323
|
+
return files;
|
|
5324
|
+
}
|
|
5325
|
+
async function stageFiles(paths, opts, { batchId: existingBatchId, files: filesOverride } = {}) {
|
|
5326
|
+
const client = createClient({
|
|
5327
|
+
accountId: opts.account,
|
|
5328
|
+
timeoutMs: parsePositiveInt2(opts.apiTimeout, void 0)
|
|
5329
|
+
});
|
|
5003
5330
|
const jsonMode = shouldOutputJson(opts);
|
|
5331
|
+
const uploadTimeoutMs = parsePositiveInt2(opts.uploadTimeout, DEFAULT_UPLOAD_TIMEOUT_MS);
|
|
5004
5332
|
let files;
|
|
5005
5333
|
try {
|
|
5006
|
-
files = resolveFiles(paths);
|
|
5334
|
+
files = filesOverride || resolveFiles(paths);
|
|
5007
5335
|
} catch (err) {
|
|
5008
5336
|
printError(err.message);
|
|
5009
5337
|
process.exit(1);
|
|
@@ -5012,97 +5340,111 @@ async function stageFiles(paths, opts) {
|
|
|
5012
5340
|
printError("No supported media files found.");
|
|
5013
5341
|
process.exit(1);
|
|
5014
5342
|
}
|
|
5015
|
-
if (!jsonMode) {
|
|
5016
|
-
console.log(`
|
|
5017
|
-
${import_picocolors9.default.bold("Uploading")} ${files.length} file(s)
|
|
5018
|
-
`);
|
|
5019
|
-
}
|
|
5020
5343
|
const filesMeta = files.map((f) => ({
|
|
5021
5344
|
name: f.name,
|
|
5022
5345
|
size: f.size,
|
|
5023
5346
|
type: f.type,
|
|
5024
5347
|
contentType: f.contentType
|
|
5025
5348
|
}));
|
|
5026
|
-
const initResult = await client.uploads.init(filesMeta);
|
|
5349
|
+
const initResult = await client.uploads.init(filesMeta, { batchId: existingBatchId });
|
|
5027
5350
|
const { batchId } = initResult;
|
|
5028
|
-
const
|
|
5029
|
-
|
|
5030
|
-
|
|
5351
|
+
const concurrency = calculateUploadConcurrency(files, opts.concurrency);
|
|
5352
|
+
if (!jsonMode) {
|
|
5353
|
+
console.log(`
|
|
5354
|
+
${import_picocolors10.default.bold("Uploading")} ${files.length} file(s) ${import_picocolors10.default.dim(`(staging concurrency: ${concurrency})`)}
|
|
5355
|
+
`);
|
|
5356
|
+
}
|
|
5357
|
+
const uploadedFiles = await runBoundedConcurrency(files, concurrency, async (file, i) => {
|
|
5031
5358
|
const r2Info = initResult.files[i];
|
|
5032
5359
|
if (!jsonMode) {
|
|
5033
|
-
console.log(` ${
|
|
5360
|
+
console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(`Uploading ${file.name}... ${formatSize(file.size)}`)}`);
|
|
5034
5361
|
}
|
|
5035
5362
|
try {
|
|
5036
|
-
const fileBuffer =
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
if (!resp.ok) {
|
|
5046
|
-
throw new Error(`R2 upload failed: ${resp.status}`);
|
|
5047
|
-
}
|
|
5048
|
-
uploadedFiles.push({
|
|
5363
|
+
const fileBuffer = import_fs3.default.readFileSync(file.path);
|
|
5364
|
+
await putR2WithRetry(
|
|
5365
|
+
r2Info.uploadUrl,
|
|
5366
|
+
fileBuffer,
|
|
5367
|
+
file.contentType || (file.type === "video" ? "video/mp4" : "image/jpeg"),
|
|
5368
|
+
{ timeoutMs: uploadTimeoutMs }
|
|
5369
|
+
);
|
|
5370
|
+
return {
|
|
5371
|
+
path: file.path,
|
|
5049
5372
|
name: file.name,
|
|
5050
5373
|
type: file.type,
|
|
5051
5374
|
size: file.size,
|
|
5375
|
+
contentType: file.contentType,
|
|
5052
5376
|
fileKey: r2Info.fileKey
|
|
5053
|
-
}
|
|
5377
|
+
};
|
|
5054
5378
|
} catch (err) {
|
|
5055
5379
|
if (!jsonMode) {
|
|
5056
|
-
console.log(` ${
|
|
5380
|
+
console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
|
|
5057
5381
|
}
|
|
5058
|
-
|
|
5382
|
+
return {
|
|
5383
|
+
path: file.path,
|
|
5384
|
+
name: file.name,
|
|
5385
|
+
type: file.type,
|
|
5386
|
+
size: file.size,
|
|
5387
|
+
contentType: file.contentType,
|
|
5388
|
+
fileKey: null,
|
|
5389
|
+
error: err.message
|
|
5390
|
+
};
|
|
5059
5391
|
}
|
|
5060
|
-
}
|
|
5061
|
-
return { client, batchId, uploadedFiles, jsonMode };
|
|
5392
|
+
});
|
|
5393
|
+
return { client, batchId, uploadedFiles, files, jsonMode };
|
|
5062
5394
|
}
|
|
5063
|
-
async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
5395
|
+
async function processAndPrint(client, batchId, uploadedFiles, jsonMode, { isRetry = false } = {}) {
|
|
5064
5396
|
const validFiles = uploadedFiles.filter((f) => f.fileKey);
|
|
5065
5397
|
if (validFiles.length === 0) {
|
|
5066
5398
|
printError("All uploads failed.");
|
|
5067
|
-
|
|
5399
|
+
return {
|
|
5400
|
+
batchId,
|
|
5401
|
+
status: "failed",
|
|
5402
|
+
completed: 0,
|
|
5403
|
+
failed: uploadedFiles.length,
|
|
5404
|
+
total: 0,
|
|
5405
|
+
files: [],
|
|
5406
|
+
groups: [],
|
|
5407
|
+
failedUploadedFiles: uploadedFiles
|
|
5408
|
+
};
|
|
5068
5409
|
}
|
|
5069
5410
|
if (!jsonMode) {
|
|
5070
|
-
console.log(` ${
|
|
5411
|
+
console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(`Finalizing ${validFiles.length} upload(s) to Facebook...`)}`);
|
|
5412
|
+
if (validFiles.some((file) => file.type === "video")) {
|
|
5413
|
+
console.log(` ${import_picocolors10.default.dim("Note: video finalization waits on Meta-side processing, so longer waits are expected.")}`);
|
|
5414
|
+
}
|
|
5071
5415
|
}
|
|
5072
5416
|
const results = [];
|
|
5073
5417
|
let completed = 0;
|
|
5074
5418
|
let failed = 0;
|
|
5075
|
-
for (const file of validFiles) {
|
|
5419
|
+
for (const [index, file] of validFiles.entries()) {
|
|
5076
5420
|
try {
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
fileKey: file.fileKey
|
|
5082
|
-
});
|
|
5421
|
+
if (!jsonMode) {
|
|
5422
|
+
console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(`Finalizing ${index + 1} of ${validFiles.length}: ${file.name}...`)}`);
|
|
5423
|
+
}
|
|
5424
|
+
const result = await processFileWithRetry(client, batchId, file);
|
|
5083
5425
|
results.push(result);
|
|
5084
5426
|
if (result.status === "complete") {
|
|
5085
5427
|
completed++;
|
|
5086
5428
|
if (!jsonMode) {
|
|
5087
5429
|
const id = result.videoId || result.mediaHash || "";
|
|
5088
|
-
console.log(` ${
|
|
5430
|
+
console.log(` ${import_picocolors10.default.green("\u2713")} ${file.name} uploaded successfully ${import_picocolors10.default.dim(id)}`);
|
|
5089
5431
|
}
|
|
5090
5432
|
} else {
|
|
5091
5433
|
failed++;
|
|
5092
5434
|
if (!jsonMode) {
|
|
5093
|
-
console.log(` ${
|
|
5435
|
+
console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${result.error}`);
|
|
5094
5436
|
}
|
|
5095
5437
|
}
|
|
5096
5438
|
} catch (err) {
|
|
5097
5439
|
failed++;
|
|
5098
5440
|
results.push({ name: file.name, type: file.type, status: "error", error: err.message });
|
|
5099
5441
|
if (!jsonMode) {
|
|
5100
|
-
console.log(` ${
|
|
5442
|
+
console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
|
|
5101
5443
|
}
|
|
5102
5444
|
}
|
|
5103
5445
|
}
|
|
5104
5446
|
let groups = [];
|
|
5105
|
-
if (completed > 1) {
|
|
5447
|
+
if (isRetry ? completed >= 1 : completed > 1) {
|
|
5106
5448
|
try {
|
|
5107
5449
|
const finalizeResult = await client.uploads.finalize(batchId);
|
|
5108
5450
|
groups = finalizeResult.groups || [];
|
|
@@ -5111,36 +5453,127 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
|
5111
5453
|
}
|
|
5112
5454
|
if (jsonMode) {
|
|
5113
5455
|
printJson({ batchId, status: failed === 0 ? "complete" : completed === 0 ? "failed" : "partial", completed, failed, total: validFiles.length, files: results, groups });
|
|
5114
|
-
return
|
|
5456
|
+
return {
|
|
5457
|
+
batchId,
|
|
5458
|
+
status: failed === 0 ? "complete" : completed === 0 ? "failed" : "partial",
|
|
5459
|
+
completed,
|
|
5460
|
+
failed,
|
|
5461
|
+
total: validFiles.length,
|
|
5462
|
+
files: results,
|
|
5463
|
+
groups,
|
|
5464
|
+
failedUploadedFiles: uploadedFiles.filter((file) => !file.fileKey || results.some((result) => result.name === file.name && result.status === "error"))
|
|
5465
|
+
};
|
|
5115
5466
|
}
|
|
5116
5467
|
if (groups.length > 0) {
|
|
5117
5468
|
console.log(`
|
|
5118
|
-
${
|
|
5469
|
+
${import_picocolors10.default.bold("Variant Groups")} (${groups.length})
|
|
5119
5470
|
`);
|
|
5120
5471
|
for (const group of groups) {
|
|
5121
5472
|
const assets = group.assets.map((a) => {
|
|
5122
|
-
const placement = a.placementType === "default" ? "" :
|
|
5473
|
+
const placement = a.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${a.placementType}]`);
|
|
5123
5474
|
return `${a.name}${placement}`;
|
|
5124
5475
|
});
|
|
5125
|
-
console.log(` ${
|
|
5476
|
+
console.log(` ${import_picocolors10.default.bold(group.baseName || group.groupId)}`);
|
|
5126
5477
|
for (const asset of assets) {
|
|
5127
5478
|
console.log(` ${asset}`);
|
|
5128
5479
|
}
|
|
5129
5480
|
}
|
|
5130
5481
|
}
|
|
5131
5482
|
console.log(`
|
|
5132
|
-
${
|
|
5483
|
+
${import_picocolors10.default.bold("Batch:")} ${batchId}`);
|
|
5133
5484
|
const countStr = `${completed} complete, ${failed} failed`;
|
|
5134
5485
|
console.log(` ${countStr}`);
|
|
5135
5486
|
if (groups.length > 0) {
|
|
5136
|
-
console.log(` ${
|
|
5487
|
+
console.log(` ${import_picocolors10.default.bold("Groups:")} ${groups.length} variant group(s) detected`);
|
|
5137
5488
|
}
|
|
5138
5489
|
console.log("");
|
|
5490
|
+
return {
|
|
5491
|
+
batchId,
|
|
5492
|
+
status: failed === 0 ? "complete" : completed === 0 ? "failed" : "partial",
|
|
5493
|
+
completed,
|
|
5494
|
+
failed,
|
|
5495
|
+
total: validFiles.length,
|
|
5496
|
+
files: results,
|
|
5497
|
+
groups,
|
|
5498
|
+
failedUploadedFiles: uploadedFiles.filter((file) => !file.fileKey || results.some((result) => result.name === file.name && result.status === "error"))
|
|
5499
|
+
};
|
|
5500
|
+
}
|
|
5501
|
+
function failedFileState(uploadedFiles, summary) {
|
|
5502
|
+
const failedProcessNames = new Set((summary.files || []).filter((result) => result.status === "error").map((result) => result.name));
|
|
5503
|
+
return uploadedFiles.filter((file) => !file.fileKey || failedProcessNames.has(file.name)).map((file) => ({
|
|
5504
|
+
path: file.path,
|
|
5505
|
+
name: file.name,
|
|
5506
|
+
type: file.type,
|
|
5507
|
+
size: file.size,
|
|
5508
|
+
contentType: file.contentType
|
|
5509
|
+
}));
|
|
5510
|
+
}
|
|
5511
|
+
function updateFailedBatchState({ batchId, accountId, uploadedFiles, summary }) {
|
|
5512
|
+
const failedFiles = failedFileState(uploadedFiles, summary);
|
|
5513
|
+
if (failedFiles.length > 0) {
|
|
5514
|
+
saveFailedBatch({ batchId, accountId, failedFiles });
|
|
5515
|
+
} else {
|
|
5516
|
+
clearFailedBatch(batchId);
|
|
5517
|
+
}
|
|
5518
|
+
return failedFiles;
|
|
5519
|
+
}
|
|
5520
|
+
async function runUploadFlow(paths, opts, { batchId, files, isRetry = false } = {}) {
|
|
5521
|
+
const { client, batchId: resolvedBatchId, uploadedFiles, jsonMode } = await stageFiles(paths, opts, { batchId, files });
|
|
5522
|
+
const summary = await processAndPrint(client, resolvedBatchId, uploadedFiles, jsonMode, { isRetry });
|
|
5523
|
+
const failedFiles = updateFailedBatchState({
|
|
5524
|
+
batchId: resolvedBatchId,
|
|
5525
|
+
accountId: client.accountId,
|
|
5526
|
+
uploadedFiles,
|
|
5527
|
+
summary
|
|
5528
|
+
});
|
|
5529
|
+
if (failedFiles.length > 0 && summary.completed === 0) {
|
|
5530
|
+
process.exit(1);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
function resolveRetryAccount(savedAccountId, requestedAccountId) {
|
|
5534
|
+
if (requestedAccountId) {
|
|
5535
|
+
if (savedAccountId && requestedAccountId !== savedAccountId) {
|
|
5536
|
+
printWarn(
|
|
5537
|
+
`--account ${requestedAccountId} differs from the failed batch's account ${savedAccountId}; retrying under a different account is not supported and can mix accounts in one batch.`
|
|
5538
|
+
);
|
|
5539
|
+
}
|
|
5540
|
+
return requestedAccountId;
|
|
5541
|
+
}
|
|
5542
|
+
return savedAccountId || void 0;
|
|
5543
|
+
}
|
|
5544
|
+
async function runRetryFailed(opts) {
|
|
5545
|
+
const requestedBatchId = typeof opts.retryFailed === "string" ? opts.retryFailed : null;
|
|
5546
|
+
const failedBatch = requestedBatchId ? loadFailedBatch(requestedBatchId) : findLatestFailedBatch();
|
|
5547
|
+
if (!failedBatch) {
|
|
5548
|
+
printError("No failed upload batch found to retry");
|
|
5549
|
+
process.exit(1);
|
|
5550
|
+
}
|
|
5551
|
+
const files = resolveRetryFiles(failedBatch.failedFiles || []);
|
|
5552
|
+
if (files.length === 0) {
|
|
5553
|
+
printError("No retryable files found in failed upload batch.");
|
|
5554
|
+
clearFailedBatch(failedBatch.batchId);
|
|
5555
|
+
process.exit(1);
|
|
5556
|
+
}
|
|
5557
|
+
const account = resolveRetryAccount(failedBatch.accountId, opts.account);
|
|
5558
|
+
await runUploadFlow([], { ...opts, account }, { batchId: failedBatch.batchId, files, isRetry: true });
|
|
5139
5559
|
}
|
|
5140
5560
|
function uploadCommand() {
|
|
5141
|
-
return new Command("upload").description("Upload media files and process to Meta").argument("
|
|
5142
|
-
const
|
|
5143
|
-
|
|
5561
|
+
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").option("--retry-failed [batchId]", "Retry the latest failed upload batch or the specified batch ID").action(async (paths, opts) => {
|
|
5562
|
+
const hasRetryFailed = opts.retryFailed != null;
|
|
5563
|
+
const hasPaths = Array.isArray(paths) && paths.length > 0;
|
|
5564
|
+
if (hasRetryFailed && hasPaths) {
|
|
5565
|
+
printError("Do not pass file paths with --retry-failed; the failed files are loaded from saved state.");
|
|
5566
|
+
process.exit(1);
|
|
5567
|
+
}
|
|
5568
|
+
if (hasRetryFailed) {
|
|
5569
|
+
await runRetryFailed(opts);
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
if (!hasPaths) {
|
|
5573
|
+
printError("Missing required argument: paths");
|
|
5574
|
+
process.exit(1);
|
|
5575
|
+
}
|
|
5576
|
+
await runUploadFlow(paths, opts);
|
|
5144
5577
|
});
|
|
5145
5578
|
}
|
|
5146
5579
|
function uploadsCommand() {
|
|
@@ -5155,10 +5588,10 @@ function uploadsCommand() {
|
|
|
5155
5588
|
}
|
|
5156
5589
|
const accountLabel = batch.accountName ? `${batch.accountName} (${batch.accountId})` : batch.accountId;
|
|
5157
5590
|
console.log(`
|
|
5158
|
-
${
|
|
5591
|
+
${import_picocolors10.default.bold("Batch")} ${batch.batchId}
|
|
5159
5592
|
`);
|
|
5160
|
-
console.log(` ${
|
|
5161
|
-
console.log(` ${
|
|
5593
|
+
console.log(` ${import_picocolors10.default.bold("Account:")} ${accountLabel}`);
|
|
5594
|
+
console.log(` ${import_picocolors10.default.bold("Files:")} ${batch.total}
|
|
5162
5595
|
`);
|
|
5163
5596
|
const rows = batch.files.map((f) => ({
|
|
5164
5597
|
...f,
|
|
@@ -5167,19 +5600,19 @@ function uploadsCommand() {
|
|
|
5167
5600
|
printTable(rows, [
|
|
5168
5601
|
{ key: "name", label: "Name", maxWidth: 35 },
|
|
5169
5602
|
{ key: "type", label: "Type", maxWidth: 6 },
|
|
5170
|
-
{ key: "id", label: "Hash / Video ID", maxWidth: 35, color: (v2) => v2 ||
|
|
5603
|
+
{ key: "id", label: "Hash / Video ID", maxWidth: 35, color: (v2) => v2 || import_picocolors10.default.dim("\u2014") }
|
|
5171
5604
|
]);
|
|
5172
5605
|
if (batch.groups?.length > 0) {
|
|
5173
5606
|
console.log(`
|
|
5174
|
-
${
|
|
5607
|
+
${import_picocolors10.default.bold("Variant Groups")} (${batch.groups.length})
|
|
5175
5608
|
`);
|
|
5176
5609
|
for (const group of batch.groups) {
|
|
5177
5610
|
const primaryAsset = group.assets.find((a) => a.isPrimary);
|
|
5178
5611
|
const groupLabel = group.baseName || primaryAsset?.name || group.groupId;
|
|
5179
|
-
console.log(` ${
|
|
5612
|
+
console.log(` ${import_picocolors10.default.bold(groupLabel)}`);
|
|
5180
5613
|
for (const asset of group.assets) {
|
|
5181
|
-
const placement = asset.placementType === "default" ? "" :
|
|
5182
|
-
const primary = asset.isPrimary ?
|
|
5614
|
+
const placement = asset.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${asset.placementType}]`);
|
|
5615
|
+
const primary = asset.isPrimary ? import_picocolors10.default.dim(" (primary)") : "";
|
|
5183
5616
|
console.log(` ${asset.name}${placement}${primary}`);
|
|
5184
5617
|
}
|
|
5185
5618
|
}
|
|
@@ -5193,10 +5626,10 @@ function uploadsCommand() {
|
|
|
5193
5626
|
return;
|
|
5194
5627
|
}
|
|
5195
5628
|
console.log(`
|
|
5196
|
-
${
|
|
5629
|
+
${import_picocolors10.default.bold("Recent Uploads")} (${batches.length})
|
|
5197
5630
|
`);
|
|
5198
5631
|
if (batches.length === 0) {
|
|
5199
|
-
console.log(
|
|
5632
|
+
console.log(import_picocolors10.default.dim(" No uploads found for this account.\n"));
|
|
5200
5633
|
return;
|
|
5201
5634
|
}
|
|
5202
5635
|
const formatDate = (d3) => {
|
|
@@ -5224,11 +5657,11 @@ function uploadsCommand() {
|
|
|
5224
5657
|
}
|
|
5225
5658
|
|
|
5226
5659
|
// src/commands/create.js
|
|
5227
|
-
var
|
|
5228
|
-
var
|
|
5660
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
5661
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5229
5662
|
|
|
5230
5663
|
// src/lib/poll.js
|
|
5231
|
-
var
|
|
5664
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
5232
5665
|
var POLL_INTERVAL = 500;
|
|
5233
5666
|
var TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5234
5667
|
var TIMEOUT_MINUTES = TIMEOUT_MS / 6e4;
|
|
@@ -5240,7 +5673,7 @@ async function pollJob(client, jobId, opts = {}) {
|
|
|
5240
5673
|
if (!jsonMode) {
|
|
5241
5674
|
if (tty) {
|
|
5242
5675
|
console.log(`
|
|
5243
|
-
${
|
|
5676
|
+
${import_picocolors11.default.bold("Creating ads")}
|
|
5244
5677
|
`);
|
|
5245
5678
|
} else {
|
|
5246
5679
|
console.log(`[start] Creating ads \u2014 ${jobId}`);
|
|
@@ -5266,7 +5699,7 @@ async function pollJob(client, jobId, opts = {}) {
|
|
|
5266
5699
|
process.exitCode = 2;
|
|
5267
5700
|
return { status: "still_running", jobId };
|
|
5268
5701
|
}
|
|
5269
|
-
await
|
|
5702
|
+
await sleep3(POLL_INTERVAL);
|
|
5270
5703
|
continue;
|
|
5271
5704
|
}
|
|
5272
5705
|
const ops = data.progress?.operations || [];
|
|
@@ -5277,7 +5710,7 @@ async function pollJob(client, jobId, opts = {}) {
|
|
|
5277
5710
|
const time = formatTimestamp(op.timestamp);
|
|
5278
5711
|
const { icon, color } = opStyle(op.type);
|
|
5279
5712
|
if (tty) {
|
|
5280
|
-
console.log(` ${
|
|
5713
|
+
console.log(` ${import_picocolors11.default.dim(time)} ${color(icon)} ${color(op.message)}`);
|
|
5281
5714
|
} else {
|
|
5282
5715
|
const tag = op.type === "error" ? "[err]" : op.type === "retry" ? "[..]" : op.type === "completion" || (op.type || "").endsWith("_complete") ? "[ok ]" : "[..]";
|
|
5283
5716
|
console.log(`${time} ${tag} ${op.message}`);
|
|
@@ -5298,7 +5731,7 @@ async function pollJob(client, jobId, opts = {}) {
|
|
|
5298
5731
|
const failed = data.result?.created?.failedAds?.length || 0;
|
|
5299
5732
|
console.log("");
|
|
5300
5733
|
if (failed > 0 && adCount > 0) {
|
|
5301
|
-
console.log(` ${
|
|
5734
|
+
console.log(` ${import_picocolors11.default.yellow("\u2212")} ${adCount} ads created, ${failed} failed in ${elapsed}`);
|
|
5302
5735
|
} else if (adCount > 0) {
|
|
5303
5736
|
printSuccess(`Created ${adCount} ads in ${elapsed}`);
|
|
5304
5737
|
} else {
|
|
@@ -5313,14 +5746,14 @@ async function pollJob(client, jobId, opts = {}) {
|
|
|
5313
5746
|
console.log(JSON.stringify({ event: "still_running", jobId }));
|
|
5314
5747
|
} else {
|
|
5315
5748
|
console.log("");
|
|
5316
|
-
console.log(` ${
|
|
5317
|
-
console.log(` Resume with: ${
|
|
5749
|
+
console.log(` ${import_picocolors11.default.yellow("\u2212")} ${import_picocolors11.default.yellow(`Still running after ${TIMEOUT_MINUTES} min \u2014 job continues on the server.`)}`);
|
|
5750
|
+
console.log(` Resume with: ${import_picocolors11.default.bold(`ads jobs ${jobId} --follow`)}`);
|
|
5318
5751
|
console.log("");
|
|
5319
5752
|
}
|
|
5320
5753
|
process.exitCode = 2;
|
|
5321
5754
|
return { status: "still_running", jobId };
|
|
5322
5755
|
}
|
|
5323
|
-
await
|
|
5756
|
+
await sleep3(POLL_INTERVAL);
|
|
5324
5757
|
}
|
|
5325
5758
|
}
|
|
5326
5759
|
function opStyle(type) {
|
|
@@ -5328,13 +5761,13 @@ function opStyle(type) {
|
|
|
5328
5761
|
case "completion":
|
|
5329
5762
|
case "campaign_complete":
|
|
5330
5763
|
case "adset_complete":
|
|
5331
|
-
return { icon: "\u2713", color:
|
|
5764
|
+
return { icon: "\u2713", color: import_picocolors11.default.green };
|
|
5332
5765
|
case "error":
|
|
5333
|
-
return { icon: "\u2717", color:
|
|
5766
|
+
return { icon: "\u2717", color: import_picocolors11.default.red };
|
|
5334
5767
|
case "retry":
|
|
5335
|
-
return { icon: "\u2212", color:
|
|
5768
|
+
return { icon: "\u2212", color: import_picocolors11.default.yellow };
|
|
5336
5769
|
default:
|
|
5337
|
-
return { icon: "\u2192", color:
|
|
5770
|
+
return { icon: "\u2192", color: import_picocolors11.default.dim };
|
|
5338
5771
|
}
|
|
5339
5772
|
}
|
|
5340
5773
|
function formatTimestamp(iso) {
|
|
@@ -5346,7 +5779,7 @@ function formatTimestamp(iso) {
|
|
|
5346
5779
|
return " ";
|
|
5347
5780
|
}
|
|
5348
5781
|
}
|
|
5349
|
-
function
|
|
5782
|
+
function sleep3(ms) {
|
|
5350
5783
|
return new Promise((r) => setTimeout(r, ms));
|
|
5351
5784
|
}
|
|
5352
5785
|
|
|
@@ -5370,29 +5803,29 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
|
|
|
5370
5803
|
const ts = result.totalAdSets || 1;
|
|
5371
5804
|
const ta = result.totalAds || result.ads?.length || 0;
|
|
5372
5805
|
const parts = [];
|
|
5373
|
-
if (tc > 1) parts.push(`${
|
|
5374
|
-
parts.push(`${
|
|
5375
|
-
parts.push(`${
|
|
5806
|
+
if (tc > 1) parts.push(`${import_picocolors12.default.blue(tc)} campaigns`);
|
|
5807
|
+
parts.push(`${import_picocolors12.default.blue(ts)} ad set${ts !== 1 ? "s" : ""}`);
|
|
5808
|
+
parts.push(`${import_picocolors12.default.blue(ta)} ad${ta !== 1 ? "s" : ""}`);
|
|
5376
5809
|
let statusStr = statusColor(result.status || "PAUSED");
|
|
5377
5810
|
if (result.status === "PAUSED" && result.pauseAt && result.pauseAt !== "ad") {
|
|
5378
5811
|
const levelLabel = result.pauseAt === "campaign" ? "campaign level" : "ad set level";
|
|
5379
|
-
statusStr +=
|
|
5812
|
+
statusStr += import_picocolors12.default.dim(` (${levelLabel})`);
|
|
5380
5813
|
}
|
|
5381
5814
|
let enhLabel = "";
|
|
5382
|
-
if (result.enhancements === "metaDefaults") enhLabel =
|
|
5383
|
-
else if (result.enhancements === "all") enhLabel =
|
|
5384
|
-
else if (result.enhancements === "none") enhLabel =
|
|
5385
|
-
else if (Array.isArray(result.enhancements)) enhLabel =
|
|
5386
|
-
const enhPart = enhLabel ? ` ${
|
|
5387
|
-
console.log(` ${parts.join(", ")} ${
|
|
5815
|
+
if (result.enhancements === "metaDefaults") enhLabel = import_picocolors12.default.dim("All Off");
|
|
5816
|
+
else if (result.enhancements === "all") enhLabel = import_picocolors12.default.dim("All On");
|
|
5817
|
+
else if (result.enhancements === "none") enhLabel = import_picocolors12.default.dim("All Off");
|
|
5818
|
+
else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors12.default.dim(`${result.enhancements.length} custom`);
|
|
5819
|
+
const enhPart = enhLabel ? ` ${import_picocolors12.default.dim("\xB7")} ${enhLabel}` : "";
|
|
5820
|
+
console.log(` ${parts.join(", ")} ${import_picocolors12.default.dim("\xB7")} ${statusStr}${enhPart}
|
|
5388
5821
|
`);
|
|
5389
5822
|
if (result.ads?.length) {
|
|
5390
5823
|
const tableRows = [];
|
|
5391
5824
|
let lastCampaign = null;
|
|
5392
5825
|
let lastAdSet = null;
|
|
5393
5826
|
for (const ad of result.ads) {
|
|
5394
|
-
const campaignName = ad.campaignName ||
|
|
5395
|
-
const adSetName = ad.adSetName ||
|
|
5827
|
+
const campaignName = ad.campaignName || import_picocolors12.default.dim("\u2014");
|
|
5828
|
+
const adSetName = ad.adSetName || import_picocolors12.default.dim("\u2014");
|
|
5396
5829
|
tableRows.push({
|
|
5397
5830
|
campaign: campaignName === lastCampaign ? "" : campaignName,
|
|
5398
5831
|
adSet: campaignName === lastCampaign && adSetName === lastAdSet ? "" : adSetName,
|
|
@@ -5440,54 +5873,61 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
|
|
|
5440
5873
|
for (const ad of result.ads) {
|
|
5441
5874
|
if (showAdSetHeaders && ad.adSetName && ad.adSetName !== lastDetailAdSet) {
|
|
5442
5875
|
if (lastDetailAdSet !== null) console.log("");
|
|
5443
|
-
console.log(` ${
|
|
5876
|
+
console.log(` ${import_picocolors12.default.dim("\u25B8 Ad Set:")} ${import_picocolors12.default.bold(ad.adSetName)}`);
|
|
5444
5877
|
lastDetailAdSet = ad.adSetName;
|
|
5445
5878
|
}
|
|
5446
|
-
const typeLabel = ad.mediaType ?
|
|
5879
|
+
const typeLabel = ad.mediaType ? import_picocolors12.default.dim(` [${ad.mediaType}]`) : "";
|
|
5447
5880
|
const indent = showAdSetHeaders ? " " : " ";
|
|
5448
5881
|
const fieldIndent = showAdSetHeaders ? " " : " ";
|
|
5449
5882
|
const fieldIndentLen = fieldIndent.length;
|
|
5450
|
-
console.log(`${indent}${
|
|
5883
|
+
console.log(`${indent}${import_picocolors12.default.bold(import_picocolors12.default.blue(ad.name))}${typeLabel}`);
|
|
5451
5884
|
const headline = (ad.headline || []).join(" | ");
|
|
5452
5885
|
const primary = (ad.primaryText || []).join(" | ");
|
|
5453
5886
|
const desc = (ad.description || []).join(" | ");
|
|
5454
|
-
if (headline) console.log(`${fieldIndent}${
|
|
5455
|
-
if (primary) console.log(`${fieldIndent}${
|
|
5456
|
-
if (desc) console.log(`${fieldIndent}${
|
|
5457
|
-
if (ad.cta && !sharedCta) console.log(`${fieldIndent}${
|
|
5458
|
-
if (ad.link && !sharedLink) console.log(`${fieldIndent}${
|
|
5459
|
-
if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${
|
|
5887
|
+
if (headline) console.log(`${fieldIndent}${import_picocolors12.default.dim("Headline:")} ${trunc(headline)}`);
|
|
5888
|
+
if (primary) console.log(`${fieldIndent}${import_picocolors12.default.dim("Primary Text:")} ${trunc(primary)}`);
|
|
5889
|
+
if (desc) console.log(`${fieldIndent}${import_picocolors12.default.dim("Description:")} ${trunc(desc)}`);
|
|
5890
|
+
if (ad.cta && !sharedCta) console.log(`${fieldIndent}${import_picocolors12.default.dim("CTA:")} ${ad.cta}`);
|
|
5891
|
+
if (ad.link && !sharedLink) console.log(`${fieldIndent}${import_picocolors12.default.dim("Link:")} ${wrapLine(ad.link, fieldIndentLen)}`);
|
|
5892
|
+
if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${import_picocolors12.default.dim("URL Tags:")} ${wrapLine(ad.urlTags, fieldIndentLen)}`);
|
|
5460
5893
|
console.log("");
|
|
5461
5894
|
}
|
|
5462
5895
|
const valCol = 18;
|
|
5463
|
-
if (sharedCta) console.log(` ${
|
|
5464
|
-
if (sharedLink) console.log(` ${
|
|
5465
|
-
if (sharedUrlTags) console.log(` ${
|
|
5896
|
+
if (sharedCta) console.log(` ${import_picocolors12.default.bold("CTA:")} ${sharedCta}`);
|
|
5897
|
+
if (sharedLink) console.log(` ${import_picocolors12.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
|
|
5898
|
+
if (sharedUrlTags) console.log(` ${import_picocolors12.default.bold("URL Tags:")} ${wrapLine(sharedUrlTags, valCol)}`);
|
|
5899
|
+
if (result.profile) {
|
|
5900
|
+
const profile = formatProfileValues(result.profile);
|
|
5901
|
+
const display = (value) => value === "not set" ? import_picocolors12.default.dim(value) : value;
|
|
5902
|
+
console.log(` ${import_picocolors12.default.bold("Page:")} ${wrapLine(display(profile.page), valCol)}`);
|
|
5903
|
+
console.log(` ${import_picocolors12.default.bold("Instagram:")} ${wrapLine(display(profile.instagram), valCol)}`);
|
|
5904
|
+
console.log(` ${import_picocolors12.default.bold("Threads:")} ${wrapLine(display(profile.threads), valCol)}`);
|
|
5905
|
+
}
|
|
5466
5906
|
const enh = result.enhancements;
|
|
5467
5907
|
let enhDetail;
|
|
5468
5908
|
if (enh === "metaDefaults") enhDetail = "Meta Defaults";
|
|
5469
5909
|
else if (enh === "all") enhDetail = "All On";
|
|
5470
5910
|
else if (enh === "none") enhDetail = "All Off";
|
|
5471
5911
|
else if (Array.isArray(enh)) enhDetail = enh.join(", ");
|
|
5472
|
-
if (enhDetail) console.log(` ${
|
|
5912
|
+
if (enhDetail) console.log(` ${import_picocolors12.default.bold("Enhancements:")} ${wrapLine(enhDetail, valCol)}`);
|
|
5473
5913
|
if (result.budget) {
|
|
5474
5914
|
const curr = result.budget.currency ? ` ${result.budget.currency}` : "";
|
|
5475
5915
|
const renderLine = (label, amount) => {
|
|
5476
5916
|
const pad = " ".repeat(Math.max(1, 16 - label.length - 1));
|
|
5477
|
-
console.log(` ${
|
|
5917
|
+
console.log(` ${import_picocolors12.default.bold(`${label}:`)}${pad}${amount}${curr}`);
|
|
5478
5918
|
};
|
|
5479
5919
|
if (result.budget.dailyBudget != null) renderLine("Daily Budget", result.budget.dailyBudget);
|
|
5480
5920
|
if (result.budget.bidAmount != null) renderLine("Bid Amount", result.budget.bidAmount);
|
|
5481
5921
|
}
|
|
5482
5922
|
console.log("");
|
|
5483
5923
|
} else if (result.plan?.totals) {
|
|
5484
|
-
console.log(` ${
|
|
5924
|
+
console.log(` ${import_picocolors12.default.bold("Plan:")}`);
|
|
5485
5925
|
console.log(` Campaigns: ${result.plan.totals.campaigns}`);
|
|
5486
5926
|
console.log(` Ad Sets: ${result.plan.totals.adSets}`);
|
|
5487
5927
|
console.log(` Ads: ${result.plan.totals.ads}`);
|
|
5488
5928
|
}
|
|
5489
5929
|
console.log(`
|
|
5490
|
-
${
|
|
5930
|
+
${import_picocolors12.default.dim("This is a preview. No ads were created.")}
|
|
5491
5931
|
`);
|
|
5492
5932
|
return;
|
|
5493
5933
|
}
|
|
@@ -5508,13 +5948,13 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
|
|
|
5508
5948
|
printSuccess(`${result.status || "Complete"}`);
|
|
5509
5949
|
if (result.result?.created?.ads?.length) {
|
|
5510
5950
|
console.log(`
|
|
5511
|
-
${
|
|
5951
|
+
${import_picocolors12.default.bold("Created:")} ${result.result.created.ads.length} ad(s)`);
|
|
5512
5952
|
}
|
|
5513
5953
|
console.log("");
|
|
5514
5954
|
} catch (err) {
|
|
5515
5955
|
printError(err.message);
|
|
5516
5956
|
if (err.details) {
|
|
5517
|
-
console.error(
|
|
5957
|
+
console.error(import_picocolors12.default.dim(` Details: ${JSON.stringify(err.details)}`));
|
|
5518
5958
|
}
|
|
5519
5959
|
process.exit(1);
|
|
5520
5960
|
}
|
|
@@ -5536,7 +5976,7 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5536
5976
|
let body;
|
|
5537
5977
|
if (specFile) {
|
|
5538
5978
|
try {
|
|
5539
|
-
const raw =
|
|
5979
|
+
const raw = import_fs4.default.readFileSync(specFile, "utf8");
|
|
5540
5980
|
body = JSON.parse(raw);
|
|
5541
5981
|
} catch (err) {
|
|
5542
5982
|
printError(`Failed to read spec file: ${err.message}`);
|
|
@@ -5550,7 +5990,7 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5550
5990
|
if (opts.upload) body.uploadId = opts.upload;
|
|
5551
5991
|
if (opts.textFile) {
|
|
5552
5992
|
try {
|
|
5553
|
-
body.texts = JSON.parse(
|
|
5993
|
+
body.texts = JSON.parse(import_fs4.default.readFileSync(opts.textFile, "utf8"));
|
|
5554
5994
|
} catch (err) {
|
|
5555
5995
|
printError(`Failed to read text file: ${err.message}`);
|
|
5556
5996
|
process.exit(1);
|
|
@@ -5575,6 +6015,14 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5575
6015
|
body.adSet = body.adSet || {};
|
|
5576
6016
|
body.adSet.bidAmount = bid;
|
|
5577
6017
|
}
|
|
6018
|
+
if (opts.page || opts.instagram || opts.threads) {
|
|
6019
|
+
body.profile = {
|
|
6020
|
+
...body.profile || {},
|
|
6021
|
+
...opts.page ? { pageId: opts.page } : {},
|
|
6022
|
+
...opts.instagram ? { instagramId: opts.instagram } : {},
|
|
6023
|
+
...opts.threads ? { threadsId: opts.threads } : {}
|
|
6024
|
+
};
|
|
6025
|
+
}
|
|
5578
6026
|
if (opts.pauseAt) {
|
|
5579
6027
|
if (!["ad", "adSet", "campaign"].includes(opts.pauseAt)) {
|
|
5580
6028
|
printError('--pause-at must be "ad", "adSet", or "campaign"');
|
|
@@ -5585,7 +6033,7 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5585
6033
|
}
|
|
5586
6034
|
return body;
|
|
5587
6035
|
}
|
|
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");
|
|
6036
|
+
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
6037
|
function createCommand2() {
|
|
5590
6038
|
const cmd = new Command("create").description("Create ads from spec file or flags").argument("[specFile]", "JSON spec file with ad configuration");
|
|
5591
6039
|
sharedOpts(cmd);
|
|
@@ -5627,7 +6075,7 @@ async function interactiveCreate(client) {
|
|
|
5627
6075
|
printError("Interactive mode requires a TTY. Use a spec file or flags instead.");
|
|
5628
6076
|
process.exit(1);
|
|
5629
6077
|
}
|
|
5630
|
-
Ie(
|
|
6078
|
+
Ie(import_picocolors12.default.bold("Ads Uploader \u2014 Create Ads"));
|
|
5631
6079
|
if (!client.accountId) {
|
|
5632
6080
|
const { accounts } = await client.accounts.list();
|
|
5633
6081
|
const accountChoice = await ve({
|
|
@@ -5764,7 +6212,7 @@ async function interactiveCreate(client) {
|
|
|
5764
6212
|
});
|
|
5765
6213
|
if (pD(namePattern)) process.exit(0);
|
|
5766
6214
|
body.adNamePattern = namePattern;
|
|
5767
|
-
M2.info(
|
|
6215
|
+
M2.info(import_picocolors12.default.dim("Variables: {filename}, {index:01}, {variation}, {campaign}, {date}, {timestamp}"));
|
|
5768
6216
|
}
|
|
5769
6217
|
const textMethod = await ve({
|
|
5770
6218
|
message: "Ad text",
|
|
@@ -5796,7 +6244,7 @@ async function interactiveCreate(client) {
|
|
|
5796
6244
|
]
|
|
5797
6245
|
});
|
|
5798
6246
|
if (pD(textStrategy)) process.exit(0);
|
|
5799
|
-
M2.info(
|
|
6247
|
+
M2.info(import_picocolors12.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
|
|
5800
6248
|
const headlines = [];
|
|
5801
6249
|
for (; ; ) {
|
|
5802
6250
|
const h2 = await he({
|
|
@@ -5807,7 +6255,7 @@ async function interactiveCreate(client) {
|
|
|
5807
6255
|
if (!h2) break;
|
|
5808
6256
|
headlines.push(h2);
|
|
5809
6257
|
}
|
|
5810
|
-
M2.info(
|
|
6258
|
+
M2.info(import_picocolors12.default.dim("Enter primary text (one per line). Leave blank to finish."));
|
|
5811
6259
|
const bodies = [];
|
|
5812
6260
|
for (; ; ) {
|
|
5813
6261
|
const b4 = await he({
|
|
@@ -5916,7 +6364,7 @@ async function interactiveCreate(client) {
|
|
|
5916
6364
|
}
|
|
5917
6365
|
}
|
|
5918
6366
|
console.log("");
|
|
5919
|
-
M2.info(
|
|
6367
|
+
M2.info(import_picocolors12.default.bold("Summary"));
|
|
5920
6368
|
if (body.adPresetId) M2.info(` Preset: ${body.adPresetId}`);
|
|
5921
6369
|
if (body.copyFromAd) M2.info(` Copy from: ${body.copyFromAd}`);
|
|
5922
6370
|
M2.info(` Upload: ${body.uploadId}`);
|
|
@@ -5938,13 +6386,13 @@ async function interactiveCreate(client) {
|
|
|
5938
6386
|
}
|
|
5939
6387
|
|
|
5940
6388
|
// src/commands/jobs.js
|
|
5941
|
-
var
|
|
6389
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
5942
6390
|
function jobsCommand() {
|
|
5943
6391
|
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
6392
|
if (!jobId) {
|
|
5945
|
-
console.log(
|
|
5946
|
-
console.log(
|
|
5947
|
-
console.log(
|
|
6393
|
+
console.log(import_picocolors13.default.dim(" Usage: ads jobs <jobId>"));
|
|
6394
|
+
console.log(import_picocolors13.default.dim(" ads jobs <jobId> --follow"));
|
|
6395
|
+
console.log(import_picocolors13.default.dim(" ads jobs cancel <jobId>"));
|
|
5948
6396
|
return;
|
|
5949
6397
|
}
|
|
5950
6398
|
const client = createClient({});
|
|
@@ -5958,16 +6406,16 @@ function jobsCommand() {
|
|
|
5958
6406
|
return;
|
|
5959
6407
|
}
|
|
5960
6408
|
console.log(`
|
|
5961
|
-
${
|
|
6409
|
+
${import_picocolors13.default.bold("Job")} ${jobId}
|
|
5962
6410
|
`);
|
|
5963
|
-
console.log(` ${
|
|
5964
|
-
console.log(` ${
|
|
6411
|
+
console.log(` ${import_picocolors13.default.bold("Status:")} ${statusLabel(result.status)}`);
|
|
6412
|
+
console.log(` ${import_picocolors13.default.bold("Complete:")} ${result.complete ? import_picocolors13.default.green("yes") : import_picocolors13.default.yellow("no")}`);
|
|
5965
6413
|
if (result.progress) {
|
|
5966
6414
|
const prog = result.progress;
|
|
5967
|
-
console.log(` ${
|
|
6415
|
+
console.log(` ${import_picocolors13.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
|
|
5968
6416
|
}
|
|
5969
6417
|
if (result.error) {
|
|
5970
|
-
console.log(` ${
|
|
6418
|
+
console.log(` ${import_picocolors13.default.bold("Error:")} ${import_picocolors13.default.red(result.error.message || result.error)}`);
|
|
5971
6419
|
}
|
|
5972
6420
|
console.log("");
|
|
5973
6421
|
});
|
|
@@ -5988,27 +6436,27 @@ function jobsCommand() {
|
|
|
5988
6436
|
function statusLabel(status) {
|
|
5989
6437
|
switch (status) {
|
|
5990
6438
|
case "complete":
|
|
5991
|
-
return
|
|
6439
|
+
return import_picocolors13.default.green("Complete");
|
|
5992
6440
|
case "running":
|
|
5993
6441
|
case "in_progress":
|
|
5994
|
-
return
|
|
6442
|
+
return import_picocolors13.default.cyan("Running");
|
|
5995
6443
|
case "cancelled":
|
|
5996
|
-
return
|
|
6444
|
+
return import_picocolors13.default.yellow("Cancelled");
|
|
5997
6445
|
case "error":
|
|
5998
6446
|
case "failed":
|
|
5999
|
-
return
|
|
6447
|
+
return import_picocolors13.default.red("Failed");
|
|
6000
6448
|
default:
|
|
6001
|
-
return
|
|
6449
|
+
return import_picocolors13.default.dim(status || "unknown");
|
|
6002
6450
|
}
|
|
6003
6451
|
}
|
|
6004
6452
|
|
|
6005
6453
|
// src/cli.js
|
|
6006
|
-
var VERSION = true ? "0.1
|
|
6454
|
+
var VERSION = true ? "0.2.1" : "0.0.0";
|
|
6007
6455
|
var apiUrl = process.env.ADS_API_URL || getBaseUrl();
|
|
6008
6456
|
if (apiUrl && (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1"))) {
|
|
6009
6457
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
6010
6458
|
}
|
|
6011
|
-
var b3 = (s) =>
|
|
6459
|
+
var b3 = (s) => import_picocolors14.default.bold(import_picocolors14.default.blue(s));
|
|
6012
6460
|
var BANNER = [
|
|
6013
6461
|
"",
|
|
6014
6462
|
" " + b3(" ___ __ __ __ __ __"),
|
|
@@ -6017,7 +6465,7 @@ var BANNER = [
|
|
|
6017
6465
|
" " + b3("/_/ |_\\_,_/___/ \\____/ .__/_/\\___/\\_,_/\\_,_/\\__/_/"),
|
|
6018
6466
|
" " + b3(" /_/"),
|
|
6019
6467
|
"",
|
|
6020
|
-
" " +
|
|
6468
|
+
" " + import_picocolors14.default.dim("Create Meta ads from the command line"),
|
|
6021
6469
|
""
|
|
6022
6470
|
].join("\n");
|
|
6023
6471
|
var program2 = new Command().name("ads").description("Ads Uploader CLI").version(VERSION).addHelpText("before", BANNER);
|
|
@@ -6027,6 +6475,7 @@ program2.addCommand(whoamiCommand());
|
|
|
6027
6475
|
program2.addCommand(configCommand());
|
|
6028
6476
|
program2.addCommand(accountsCommand());
|
|
6029
6477
|
program2.addCommand(accountCommand());
|
|
6478
|
+
program2.addCommand(pagesCommand());
|
|
6030
6479
|
program2.addCommand(campaignsCommand());
|
|
6031
6480
|
program2.addCommand(campaignCommand());
|
|
6032
6481
|
program2.addCommand(adsetsCommand());
|
|
@@ -6044,12 +6493,12 @@ program2.addCommand(createTestCommand(), { hidden: true });
|
|
|
6044
6493
|
program2.addCommand(jobsCommand());
|
|
6045
6494
|
async function checkForUpdates() {
|
|
6046
6495
|
try {
|
|
6047
|
-
const
|
|
6048
|
-
const
|
|
6049
|
-
const
|
|
6050
|
-
const cacheFile =
|
|
6496
|
+
const fs5 = await import("fs");
|
|
6497
|
+
const path4 = await import("path");
|
|
6498
|
+
const os3 = await import("os");
|
|
6499
|
+
const cacheFile = path4.join(os3.default.homedir(), ".config", "adsuploader", ".update-check");
|
|
6051
6500
|
try {
|
|
6052
|
-
const stat =
|
|
6501
|
+
const stat = fs5.default.statSync(cacheFile);
|
|
6053
6502
|
if (Date.now() - stat.mtimeMs < 24 * 60 * 60 * 1e3) return;
|
|
6054
6503
|
} catch {
|
|
6055
6504
|
}
|
|
@@ -6058,14 +6507,14 @@ async function checkForUpdates() {
|
|
|
6058
6507
|
const data = await resp.json();
|
|
6059
6508
|
const latest = data.version;
|
|
6060
6509
|
try {
|
|
6061
|
-
|
|
6062
|
-
|
|
6510
|
+
fs5.default.mkdirSync(path4.dirname(cacheFile), { recursive: true });
|
|
6511
|
+
fs5.default.writeFileSync(cacheFile, latest);
|
|
6063
6512
|
} catch {
|
|
6064
6513
|
}
|
|
6065
6514
|
if (latest !== VERSION) {
|
|
6066
6515
|
console.error(`
|
|
6067
|
-
${
|
|
6068
|
-
console.error(` Run ${
|
|
6516
|
+
${import_picocolors14.default.yellow("Update available:")} ${import_picocolors14.default.dim(VERSION)} \u2192 ${import_picocolors14.default.green(latest)}`);
|
|
6517
|
+
console.error(` Run ${import_picocolors14.default.cyan("npm update -g @adsuploader/cli")} to update
|
|
6069
6518
|
`);
|
|
6070
6519
|
}
|
|
6071
6520
|
} catch {
|
|
@@ -6089,7 +6538,7 @@ if (process.argv.length <= 2) {
|
|
|
6089
6538
|
}
|
|
6090
6539
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
6091
6540
|
console.error(`
|
|
6092
|
-
${
|
|
6541
|
+
${import_picocolors14.default.red("Error:")} ${err.message}
|
|
6093
6542
|
`);
|
|
6094
6543
|
process.exit(1);
|
|
6095
6544
|
}
|