@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.
Files changed (4) hide show
  1. package/README.md +2 -0
  2. package/SKILL.md +39 -0
  3. package/dist/cli.cjs +689 -240
  4. 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 path3 = require("node:path");
1146
- var fs4 = require("node:fs");
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 (fs4.existsSync(executableFile)) return;
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 = path3.resolve(baseDir, baseName);
2146
- if (fs4.existsSync(localBin)) return localBin;
2147
- if (sourceExt.includes(path3.extname(baseName))) return void 0;
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) => fs4.existsSync(`${localBin}${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 = fs4.realpathSync(this._scriptPath);
2161
+ resolvedScriptPath = fs5.realpathSync(this._scriptPath);
2162
2162
  } catch {
2163
2163
  resolvedScriptPath = this._scriptPath;
2164
2164
  }
2165
- executableDir = path3.resolve(
2166
- path3.dirname(resolvedScriptPath),
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 = path3.basename(
2173
+ const legacyName = path4.basename(
2174
2174
  this._scriptPath,
2175
- path3.extname(this._scriptPath)
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(path3.extname(executableFile));
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 = path3.basename(filename, path3.extname(filename));
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(path4) {
3048
- if (path4 === void 0) return this._executableDir;
3049
- this._executableDir = path4;
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 import_picocolors13 = __toESM(require_picocolors(), 1);
3467
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
3468
3468
 
3469
3469
  // src/lib/config.js
3470
3470
  var import_fs = __toESM(require("fs"), 1);
@@ -4131,10 +4131,51 @@ var import_crypto = __toESM(require("crypto"), 1);
4131
4131
 
4132
4132
  // src/lib/client.js
4133
4133
  var USER_AGENT = "adsuploader-cli/0.1.0";
4134
+ var DEFAULT_API_TIMEOUT_MS = 6e4;
4135
+ var DEFAULT_MAX_ATTEMPTS = 4;
4136
+ function sleep(ms) {
4137
+ return new Promise((resolve) => setTimeout(resolve, ms));
4138
+ }
4139
+ function parsePositiveInt(value, fallback) {
4140
+ const parsed = Number.parseInt(value, 10);
4141
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4142
+ }
4143
+ function isRetryableNetworkError(err) {
4144
+ const message = String(err?.message || "").toLowerCase();
4145
+ return err?.name === "AbortError" || err?.code === "ECONNRESET" || err?.code === "ETIMEDOUT" || message.includes("fetch failed") || message.includes("network") || message.includes("timeout");
4146
+ }
4147
+ function isRetryableStatus(status) {
4148
+ return status === 429 || status >= 500;
4149
+ }
4150
+ function shouldRetryResponse(method, status) {
4151
+ if (status === 429) return true;
4152
+ return method === "GET" && status >= 500;
4153
+ }
4154
+ function timeoutError(timeoutMs) {
4155
+ return new Error(`API request timed out after ${Math.ceil(timeoutMs / 1e3)}s`);
4156
+ }
4157
+ function getRetryDelayMs(attempt, retryAfter, { baseDelayMs = 500, maxDelayMs = 8e3, jitterMs = 250 } = {}) {
4158
+ if (retryAfter != null && Number.isFinite(retryAfter) && retryAfter > 0) {
4159
+ return retryAfter * 1e3;
4160
+ }
4161
+ const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1));
4162
+ return exponential + Math.floor(Math.random() * jitterMs);
4163
+ }
4164
+ function formatApiError(resp, data) {
4165
+ const message = data?.error || data?.message || `API error ${resp.status}`;
4166
+ if (resp.status === 403 && /different IP address|created from a different IP|ads login/i.test(message)) {
4167
+ return "Your network IP changed since this CLI token was created. Run `ads login` again to refresh your session.";
4168
+ }
4169
+ return message;
4170
+ }
4134
4171
  function createClient(opts = {}) {
4135
4172
  const token = opts.token || getToken();
4136
4173
  const baseUrl = opts.baseUrl || getBaseUrl();
4137
4174
  const accountId = opts.accountId || getDefaultAccount();
4175
+ const fetchImpl = opts.fetchImpl || fetch;
4176
+ const delay = opts.sleep || sleep;
4177
+ const maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
4178
+ const timeoutMs = opts.timeoutMs || parsePositiveInt(process.env.ADS_API_TIMEOUT_MS, DEFAULT_API_TIMEOUT_MS);
4138
4179
  if (!token) {
4139
4180
  throw new Error("Not authenticated. Run `ads login` first.");
4140
4181
  }
@@ -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, path3, { body, query, requiresAccount = true, raw = false } = {}) {
4149
- const url = new URL(`/api/v1${path3}`, baseUrl);
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
- const resp = await fetch(url.toString(), {
4166
- method,
4167
- headers,
4168
- body: body ? JSON.stringify(body) : void 0,
4169
- redirect: "manual"
4170
- // Never follow redirects — prevents token leakage to redirect targets
4171
- });
4172
- if (resp.status >= 300 && resp.status < 400) {
4173
- throw new Error(`Unexpected redirect (${resp.status}). This may indicate a configuration issue.`);
4174
- }
4175
- if (resp.status === 429) {
4176
- const retryAfter = resp.headers.get("retry-after");
4177
- const err = new Error(`Rate limited. Retry after ${retryAfter || "60"}s`);
4178
- err.retryAfter = parseInt(retryAfter || "60");
4179
- throw err;
4180
- }
4181
- if (raw) return resp;
4182
- const data = await resp.json();
4183
- if (!resp.ok) {
4184
- const err = new Error(data.error || data.message || `API error ${resp.status}`);
4185
- err.code = data.errorCode;
4186
- err.status = resp.status;
4187
- err.details = data.details;
4188
- throw err;
4206
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4207
+ const controller = new AbortController();
4208
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4209
+ try {
4210
+ const resp = await fetchImpl(url.toString(), {
4211
+ method,
4212
+ headers,
4213
+ body: body ? JSON.stringify(body) : void 0,
4214
+ redirect: "manual",
4215
+ // Never follow redirects — prevents token leakage to redirect targets
4216
+ signal: controller.signal
4217
+ });
4218
+ clearTimeout(timeout);
4219
+ if (resp.status >= 300 && resp.status < 400) {
4220
+ throw new Error(`Unexpected redirect (${resp.status}). This may indicate a configuration issue.`);
4221
+ }
4222
+ if (shouldRetryResponse(method, resp.status) && attempt < maxAttempts) {
4223
+ const retryAfter = resp.status === 429 ? parsePositiveInt(resp.headers.get("retry-after"), null) : null;
4224
+ await delay(getRetryDelayMs(attempt, retryAfter));
4225
+ continue;
4226
+ }
4227
+ if (raw) return resp;
4228
+ const data = await resp.json();
4229
+ if (!resp.ok) {
4230
+ const err = new Error(formatApiError(resp, data));
4231
+ err.code = data.errorCode;
4232
+ err.status = resp.status;
4233
+ err.details = data.details;
4234
+ throw err;
4235
+ }
4236
+ return data;
4237
+ } catch (err) {
4238
+ clearTimeout(timeout);
4239
+ if (err.status) {
4240
+ throw err;
4241
+ }
4242
+ const normalizedError = err?.name === "AbortError" ? timeoutError(timeoutMs) : err;
4243
+ if (method === "GET" && attempt < maxAttempts && isRetryableNetworkError(normalizedError)) {
4244
+ await delay(getRetryDelayMs(attempt));
4245
+ continue;
4246
+ }
4247
+ throw normalizedError;
4248
+ }
4189
4249
  }
4190
- return data;
4250
+ throw new Error(`API request failed after ${maxAttempts} attempt(s)`);
4191
4251
  }
4192
4252
  return {
4193
- get: (path3, opts2) => request("GET", path3, { ...opts2, requiresAccount: opts2?.requiresAccount ?? true }),
4194
- post: (path3, body, opts2) => request("POST", path3, { body, ...opts2 }),
4195
- del: (path3, body, opts2) => request("DELETE", path3, { body, ...opts2 }),
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/ad.js
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
- ${import_picocolors7.default.bold(ad.name)}
4837
+ ${import_picocolors8.default.bold(ad.name)}
4724
4838
  `);
4725
- console.log(` ${import_picocolors7.default.bold("ID:")} ${ad.id}`);
4726
- console.log(` ${import_picocolors7.default.bold("Status:")} ${statusColor(ad.status)}`);
4727
- console.log(` ${import_picocolors7.default.bold("Campaign:")} ${ad.campaignId}`);
4728
- console.log(` ${import_picocolors7.default.bold("Ad Set:")} ${ad.adSetId}`);
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) => import_picocolors7.default.bold(name.padEnd(15));
4853
+ const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
4731
4854
  const exp = !!opts.expanded;
4732
4855
  console.log("");
4733
- console.log(` ${import_picocolors7.default.bold("Creative")}`);
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
- ${import_picocolors7.default.dim("Use this ad as a template:")} ads create --copy-from ${ad.id}
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 import_picocolors8 = __toESM(require_picocolors(), 1);
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
- ${import_picocolors8.default.bold(preset.name)}
4887
+ ${import_picocolors9.default.bold(preset.name)}
4765
4888
  `);
4766
4889
  if (preset.config) {
4767
- console.log(` ${import_picocolors8.default.bold("Campaign:")} ${preset.config.campaign?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.campaign?.id || "")}`);
4768
- console.log(` ${import_picocolors8.default.bold("Ad Set:")} ${preset.config.adSet?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.adSet?.id || "")}`);
4769
- console.log(` ${import_picocolors8.default.bold("Ad:")} ${preset.config.ad?.name || import_picocolors8.default.dim("\u2014")} ${import_picocolors8.default.dim(preset.config.ad?.id || "")}`);
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
- ${import_picocolors8.default.bold("Ad Template Presets")} (${all.length})
4904
+ ${import_picocolors9.default.bold("Ad Template Presets")} (${all.length})
4782
4905
  `);
4783
4906
  if (all.length === 0) {
4784
- console.log(import_picocolors8.default.dim(" No presets. Create one in the web app by saving an ad configuration."));
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 ? import_picocolors8.default.cyan("team") : import_picocolors8.default.dim("\u2014") }
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
- ${import_picocolors8.default.bold(preset.name)}
4928
+ ${import_picocolors9.default.bold(preset.name)}
4806
4929
  `);
4807
- console.log(` ${import_picocolors8.default.bold("Includes:")} ${preset.includedFields?.join(", ") || "all fields"}`);
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(` ${import_picocolors8.default.bold("Headlines:")} ${preset.text.titles.join(" | ")}`);
4810
- if (preset.text.bodies?.length) console.log(` ${import_picocolors8.default.bold("Bodies:")} ${preset.text.bodies.join(" | ")}`);
4811
- if (preset.text.descriptions?.length) console.log(` ${import_picocolors8.default.bold("Desc:")} ${preset.text.descriptions.join(" | ")}`);
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
- ${import_picocolors8.default.bold("Text Presets")} (${all.length})
4946
+ ${import_picocolors9.default.bold("Text Presets")} (${all.length})
4824
4947
  `);
4825
4948
  if (all.length === 0) {
4826
- console.log(import_picocolors8.default.dim(" No text presets. Create one in the web app."));
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" ? import_picocolors8.default.cyan("global") : import_picocolors8.default.dim("account") },
4832
- { key: "shared", label: "Shared", color: (v2) => v2 ? import_picocolors8.default.cyan("team") : import_picocolors8.default.dim("\u2014") }
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(` ${import_picocolors8.default.bold("ID:")} ${preset.id}`);
4871
- console.log(` ${import_picocolors8.default.bold("Type:")} ${preset.type}`);
4872
- if (preset.shared) console.log(` ${import_picocolors8.default.bold("Shared:")} ${import_picocolors8.default.cyan("team")}`);
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
- ${import_picocolors8.default.dim("Use it:")} ads create --preset ${preset.id} ...
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 import_picocolors9 = __toESM(require_picocolors(), 1);
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 = import_fs2.default.statSync(filePath);
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 = import_fs2.default.openSync(filePath, "r");
5098
+ fd = import_fs3.default.openSync(filePath, "r");
4910
5099
  const buf = Buffer.alloc(12);
4911
- import_fs2.default.readSync(fd, buf, 0, 12, 0);
4912
- import_fs2.default.closeSync(fd);
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
- import_fs2.default.closeSync(fd);
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 = import_path2.default.extname(filePath).toLowerCase();
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 = import_path2.default.resolve(input);
4964
- if (!import_fs2.default.existsSync(resolved)) {
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 = import_fs2.default.statSync(resolved);
5255
+ const stat = import_fs3.default.statSync(resolved);
4968
5256
  if (stat.isDirectory()) {
4969
- const entries = import_fs2.default.readdirSync(resolved);
5257
+ const entries = import_fs3.default.readdirSync(resolved);
4970
5258
  for (const entry of entries) {
4971
- const fullPath = import_path2.default.join(resolved, entry);
4972
- const entryStat = import_fs2.default.statSync(fullPath);
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[import_path2.default.extname(fullPath).toLowerCase()];
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[import_path2.default.extname(resolved).toLowerCase()];
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: import_path2.default.basename(resolved), size: stat.size, type, contentType: ct });
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(import_picocolors9.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
5285
+ console.error(import_picocolors10.default.yellow(` Skipped ${skipped.length} file(s): ${skipped.join(", ")}`));
4998
5286
  }
4999
5287
  return files;
5000
5288
  }
5001
- async function stageFiles(paths, opts) {
5002
- const client = createClient({ accountId: opts.account });
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 uploadedFiles = [];
5029
- for (let i = 0; i < files.length; i++) {
5030
- const file = files[i];
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(` ${import_picocolors9.default.dim("\u2192")} ${import_picocolors9.default.dim(`Uploading ${file.name}... ${formatSize(file.size)}`)}`);
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 = import_fs2.default.readFileSync(file.path);
5037
- const resp = await fetch(r2Info.uploadUrl, {
5038
- method: "PUT",
5039
- headers: {
5040
- "Content-Type": file.contentType || (file.type === "video" ? "video/mp4" : "image/jpeg"),
5041
- "Content-Length": String(fileBuffer.length)
5042
- },
5043
- body: fileBuffer
5044
- });
5045
- if (!resp.ok) {
5046
- throw new Error(`R2 upload failed: ${resp.status}`);
5047
- }
5048
- uploadedFiles.push({
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(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
5380
+ console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
5057
5381
  }
5058
- uploadedFiles.push({ name: file.name, type: file.type, fileKey: null, error: err.message });
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
- process.exit(1);
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(` ${import_picocolors9.default.dim("\u2192")} ${import_picocolors9.default.dim("Finalizing uploads to Facebook...")}`);
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
- const result = await client.uploads.process(batchId, {
5078
- name: file.name,
5079
- type: file.type,
5080
- size: file.size,
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(` ${import_picocolors9.default.green("\u2713")} ${file.name} uploaded successfully ${import_picocolors9.default.dim(id)}`);
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(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${result.error}`);
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(` ${import_picocolors9.default.red("\u2717")} ${file.name}: ${err.message}`);
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
- ${import_picocolors9.default.bold("Variant Groups")} (${groups.length})
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" ? "" : import_picocolors9.default.cyan(` [${a.placementType}]`);
5473
+ const placement = a.placementType === "default" ? "" : import_picocolors10.default.cyan(` [${a.placementType}]`);
5123
5474
  return `${a.name}${placement}`;
5124
5475
  });
5125
- console.log(` ${import_picocolors9.default.bold(group.baseName || group.groupId)}`);
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
- ${import_picocolors9.default.bold("Batch:")} ${batchId}`);
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(` ${import_picocolors9.default.bold("Groups:")} ${groups.length} variant group(s) detected`);
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("<paths...>", "Files or directories to upload").option("--account <id>", "Ad account ID").option("--json", "Output as JSON").action(async (paths, opts) => {
5142
- const { client, batchId, uploadedFiles, jsonMode } = await stageFiles(paths, opts);
5143
- await processAndPrint(client, batchId, uploadedFiles, jsonMode);
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
- ${import_picocolors9.default.bold("Batch")} ${batch.batchId}
5591
+ ${import_picocolors10.default.bold("Batch")} ${batch.batchId}
5159
5592
  `);
5160
- console.log(` ${import_picocolors9.default.bold("Account:")} ${accountLabel}`);
5161
- console.log(` ${import_picocolors9.default.bold("Files:")} ${batch.total}
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 || import_picocolors9.default.dim("\u2014") }
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
- ${import_picocolors9.default.bold("Variant Groups")} (${batch.groups.length})
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(` ${import_picocolors9.default.bold(groupLabel)}`);
5612
+ console.log(` ${import_picocolors10.default.bold(groupLabel)}`);
5180
5613
  for (const asset of group.assets) {
5181
- const placement = asset.placementType === "default" ? "" : import_picocolors9.default.cyan(` [${asset.placementType}]`);
5182
- const primary = asset.isPrimary ? import_picocolors9.default.dim(" (primary)") : "";
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
- ${import_picocolors9.default.bold("Recent Uploads")} (${batches.length})
5629
+ ${import_picocolors10.default.bold("Recent Uploads")} (${batches.length})
5197
5630
  `);
5198
5631
  if (batches.length === 0) {
5199
- console.log(import_picocolors9.default.dim(" No uploads found for this account.\n"));
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 import_fs3 = __toESM(require("fs"), 1);
5228
- var import_picocolors11 = __toESM(require_picocolors(), 1);
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 import_picocolors10 = __toESM(require_picocolors(), 1);
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
- ${import_picocolors10.default.bold("Creating ads")}
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 sleep(POLL_INTERVAL);
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(` ${import_picocolors10.default.dim(time)} ${color(icon)} ${color(op.message)}`);
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(` ${import_picocolors10.default.yellow("\u2212")} ${adCount} ads created, ${failed} failed in ${elapsed}`);
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(` ${import_picocolors10.default.yellow("\u2212")} ${import_picocolors10.default.yellow(`Still running after ${TIMEOUT_MINUTES} min \u2014 job continues on the server.`)}`);
5317
- console.log(` Resume with: ${import_picocolors10.default.bold(`ads jobs ${jobId} --follow`)}`);
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 sleep(POLL_INTERVAL);
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: import_picocolors10.default.green };
5764
+ return { icon: "\u2713", color: import_picocolors11.default.green };
5332
5765
  case "error":
5333
- return { icon: "\u2717", color: import_picocolors10.default.red };
5766
+ return { icon: "\u2717", color: import_picocolors11.default.red };
5334
5767
  case "retry":
5335
- return { icon: "\u2212", color: import_picocolors10.default.yellow };
5768
+ return { icon: "\u2212", color: import_picocolors11.default.yellow };
5336
5769
  default:
5337
- return { icon: "\u2192", color: import_picocolors10.default.dim };
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 sleep(ms) {
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(`${import_picocolors11.default.blue(tc)} campaigns`);
5374
- parts.push(`${import_picocolors11.default.blue(ts)} ad set${ts !== 1 ? "s" : ""}`);
5375
- parts.push(`${import_picocolors11.default.blue(ta)} ad${ta !== 1 ? "s" : ""}`);
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 += import_picocolors11.default.dim(` (${levelLabel})`);
5812
+ statusStr += import_picocolors12.default.dim(` (${levelLabel})`);
5380
5813
  }
5381
5814
  let enhLabel = "";
5382
- if (result.enhancements === "metaDefaults") enhLabel = import_picocolors11.default.dim("All Off");
5383
- else if (result.enhancements === "all") enhLabel = import_picocolors11.default.dim("All On");
5384
- else if (result.enhancements === "none") enhLabel = import_picocolors11.default.dim("All Off");
5385
- else if (Array.isArray(result.enhancements)) enhLabel = import_picocolors11.default.dim(`${result.enhancements.length} custom`);
5386
- const enhPart = enhLabel ? ` ${import_picocolors11.default.dim("\xB7")} ${enhLabel}` : "";
5387
- console.log(` ${parts.join(", ")} ${import_picocolors11.default.dim("\xB7")} ${statusStr}${enhPart}
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 || import_picocolors11.default.dim("\u2014");
5395
- const adSetName = ad.adSetName || import_picocolors11.default.dim("\u2014");
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(` ${import_picocolors11.default.dim("\u25B8 Ad Set:")} ${import_picocolors11.default.bold(ad.adSetName)}`);
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 ? import_picocolors11.default.dim(` [${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}${import_picocolors11.default.bold(import_picocolors11.default.blue(ad.name))}${typeLabel}`);
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}${import_picocolors11.default.dim("Headline:")} ${trunc(headline)}`);
5455
- if (primary) console.log(`${fieldIndent}${import_picocolors11.default.dim("Primary Text:")} ${trunc(primary)}`);
5456
- if (desc) console.log(`${fieldIndent}${import_picocolors11.default.dim("Description:")} ${trunc(desc)}`);
5457
- if (ad.cta && !sharedCta) console.log(`${fieldIndent}${import_picocolors11.default.dim("CTA:")} ${ad.cta}`);
5458
- if (ad.link && !sharedLink) console.log(`${fieldIndent}${import_picocolors11.default.dim("Link:")} ${wrapLine(ad.link, fieldIndentLen)}`);
5459
- if (ad.urlTags && !sharedUrlTags) console.log(`${fieldIndent}${import_picocolors11.default.dim("URL Tags:")} ${wrapLine(ad.urlTags, fieldIndentLen)}`);
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(` ${import_picocolors11.default.bold("CTA:")} ${sharedCta}`);
5464
- if (sharedLink) console.log(` ${import_picocolors11.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
5465
- if (sharedUrlTags) console.log(` ${import_picocolors11.default.bold("URL Tags:")} ${wrapLine(sharedUrlTags, valCol)}`);
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(` ${import_picocolors11.default.bold("Enhancements:")} ${wrapLine(enhDetail, valCol)}`);
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(` ${import_picocolors11.default.bold(`${label}:`)}${pad}${amount}${curr}`);
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(` ${import_picocolors11.default.bold("Plan:")}`);
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
- ${import_picocolors11.default.dim("This is a preview. No ads were created.")}
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
- ${import_picocolors11.default.bold("Created:")} ${result.result.created.ads.length} ad(s)`);
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(import_picocolors11.default.dim(` Details: ${JSON.stringify(err.details)}`));
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 = import_fs3.default.readFileSync(specFile, "utf8");
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(import_fs3.default.readFileSync(opts.textFile, "utf8"));
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(import_picocolors11.default.bold("Ads Uploader \u2014 Create Ads"));
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(import_picocolors11.default.dim("Variables: {filename}, {index:01}, {variation}, {campaign}, {date}, {timestamp}"));
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(import_picocolors11.default.dim("Enter headlines (one per line). Leave blank and press enter to finish."));
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(import_picocolors11.default.dim("Enter primary text (one per line). Leave blank to finish."));
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(import_picocolors11.default.bold("Summary"));
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 import_picocolors12 = __toESM(require_picocolors(), 1);
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(import_picocolors12.default.dim(" Usage: ads jobs <jobId>"));
5946
- console.log(import_picocolors12.default.dim(" ads jobs <jobId> --follow"));
5947
- console.log(import_picocolors12.default.dim(" ads jobs cancel <jobId>"));
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
- ${import_picocolors12.default.bold("Job")} ${jobId}
6409
+ ${import_picocolors13.default.bold("Job")} ${jobId}
5962
6410
  `);
5963
- console.log(` ${import_picocolors12.default.bold("Status:")} ${statusLabel(result.status)}`);
5964
- console.log(` ${import_picocolors12.default.bold("Complete:")} ${result.complete ? import_picocolors12.default.green("yes") : import_picocolors12.default.yellow("no")}`);
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(` ${import_picocolors12.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
6415
+ console.log(` ${import_picocolors13.default.bold("Progress:")} ${prog.completed || 0}/${prog.total || "?"}`);
5968
6416
  }
5969
6417
  if (result.error) {
5970
- console.log(` ${import_picocolors12.default.bold("Error:")} ${import_picocolors12.default.red(result.error.message || result.error)}`);
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 import_picocolors12.default.green("Complete");
6439
+ return import_picocolors13.default.green("Complete");
5992
6440
  case "running":
5993
6441
  case "in_progress":
5994
- return import_picocolors12.default.cyan("Running");
6442
+ return import_picocolors13.default.cyan("Running");
5995
6443
  case "cancelled":
5996
- return import_picocolors12.default.yellow("Cancelled");
6444
+ return import_picocolors13.default.yellow("Cancelled");
5997
6445
  case "error":
5998
6446
  case "failed":
5999
- return import_picocolors12.default.red("Failed");
6447
+ return import_picocolors13.default.red("Failed");
6000
6448
  default:
6001
- return import_picocolors12.default.dim(status || "unknown");
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.8" : "0.0.0";
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) => import_picocolors13.default.bold(import_picocolors13.default.blue(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
- " " + import_picocolors13.default.dim("Create Meta ads from the command line"),
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 fs4 = await import("fs");
6048
- const path3 = await import("path");
6049
- const os2 = await import("os");
6050
- const cacheFile = path3.join(os2.default.homedir(), ".config", "adsuploader", ".update-check");
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 = fs4.default.statSync(cacheFile);
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
- fs4.default.mkdirSync(path3.dirname(cacheFile), { recursive: true });
6062
- fs4.default.writeFileSync(cacheFile, latest);
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
- ${import_picocolors13.default.yellow("Update available:")} ${import_picocolors13.default.dim(VERSION)} \u2192 ${import_picocolors13.default.green(latest)}`);
6068
- console.error(` Run ${import_picocolors13.default.cyan("npm update -g @adsuploader/cli")} to update
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
- ${import_picocolors13.default.red("Error:")} ${err.message}
6541
+ ${import_picocolors14.default.red("Error:")} ${err.message}
6093
6542
  `);
6094
6543
  process.exit(1);
6095
6544
  }