@adsuploader/cli 0.2.0 → 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 +1 -0
- package/SKILL.md +3 -0
- package/dist/cli.cjs +350 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ This package includes a `SKILL.md` file that describes every command and option
|
|
|
45
45
|
| `ads presets` | List saved presets |
|
|
46
46
|
| `ads presets:save --from-ad <id> --name <name>` | Save an existing ad as an API preset |
|
|
47
47
|
| `ads upload <files...>` | Upload images and videos |
|
|
48
|
+
| `ads upload --retry-failed [batchId]` | Retry failed files into the same upload batch |
|
|
48
49
|
| `ads uploads` | List recent upload batches |
|
|
49
50
|
| `ads create spec.json` | Create ads from spec |
|
|
50
51
|
| `ads create:preview spec.json` | Dry run |
|
package/SKILL.md
CHANGED
|
@@ -41,6 +41,8 @@ Every ad creation follows three steps:
|
|
|
41
41
|
```bash
|
|
42
42
|
ads upload hero.jpg banner.mp4
|
|
43
43
|
ads upload ./my-creatives/ # entire directory
|
|
44
|
+
ads upload --retry-failed # retry latest failed batch
|
|
45
|
+
ads upload --retry-failed batch_abc123
|
|
44
46
|
```
|
|
45
47
|
|
|
46
48
|
Returns a **batch ID** (e.g. `batch_abc123`) — you'll need this for the spec file. Files are uploaded directly to the selected ad account's Facebook media library, so the batch ID is tied to that account.
|
|
@@ -52,6 +54,7 @@ Files are staged in parallel and transient network failures are retried automati
|
|
|
52
54
|
| `--concurrency <n>` | Parallel staging uploads, 1-6 (default: 4) |
|
|
53
55
|
| `--upload-timeout <ms>` | Per-file R2 upload timeout (default: 120000) |
|
|
54
56
|
| `--api-timeout <ms>` | API request timeout (default: 60000) |
|
|
57
|
+
| `--retry-failed [batchId]` | Retry failed files into the same batch; defaults to the latest saved failed batch |
|
|
55
58
|
|
|
56
59
|
### 2. Preview (dry run)
|
|
57
60
|
|
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
|
/**
|
|
@@ -4186,8 +4186,8 @@ function createClient(opts = {}) {
|
|
|
4186
4186
|
throw new Error(`Refusing to connect over insecure HTTP: ${baseUrl}. Use HTTPS or localhost.`);
|
|
4187
4187
|
}
|
|
4188
4188
|
}
|
|
4189
|
-
async function request(method,
|
|
4190
|
-
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);
|
|
4191
4191
|
if (query) {
|
|
4192
4192
|
for (const [k3, v2] of Object.entries(query)) {
|
|
4193
4193
|
if (v2 != null) url.searchParams.set(k3, v2);
|
|
@@ -4250,9 +4250,9 @@ function createClient(opts = {}) {
|
|
|
4250
4250
|
throw new Error(`API request failed after ${maxAttempts} attempt(s)`);
|
|
4251
4251
|
}
|
|
4252
4252
|
return {
|
|
4253
|
-
get: (
|
|
4254
|
-
post: (
|
|
4255
|
-
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 }),
|
|
4256
4256
|
accounts: {
|
|
4257
4257
|
list: () => request("GET", "/accounts", { requiresAccount: false })
|
|
4258
4258
|
},
|
|
@@ -4278,7 +4278,7 @@ function createClient(opts = {}) {
|
|
|
4278
4278
|
},
|
|
4279
4279
|
uploads: {
|
|
4280
4280
|
list: (query) => request("GET", "/uploads", { query }),
|
|
4281
|
-
init: (files) => request("POST", "/uploads/init", { body: { files } }),
|
|
4281
|
+
init: (files, { batchId } = {}) => request("POST", "/uploads/init", { body: { files, ...batchId ? { batchId } : {} } }),
|
|
4282
4282
|
process: (batchId, file) => request("POST", `/uploads/${batchId}/process`, { body: file }),
|
|
4283
4283
|
finalize: (batchId) => request("POST", `/uploads/${batchId}/finalize`, { body: {} }),
|
|
4284
4284
|
status: (batchId) => request("GET", `/uploads/${batchId}`)
|
|
@@ -4353,6 +4353,9 @@ function printSuccess(msg) {
|
|
|
4353
4353
|
function printError(msg) {
|
|
4354
4354
|
console.error(` ${import_picocolors3.default.red("\u2716")} ${msg}`);
|
|
4355
4355
|
}
|
|
4356
|
+
function printWarn(msg) {
|
|
4357
|
+
console.log(` ${import_picocolors3.default.yellow("\u26A0")} ${msg}`);
|
|
4358
|
+
}
|
|
4356
4359
|
function printInfo(msg) {
|
|
4357
4360
|
console.log(` ${import_picocolors3.default.blue("\u2139")} ${msg}`);
|
|
4358
4361
|
}
|
|
@@ -4801,6 +4804,23 @@ function pagesCommand() {
|
|
|
4801
4804
|
|
|
4802
4805
|
// src/commands/ad.js
|
|
4803
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
|
|
4804
4824
|
function truncate(str, max = 120, expanded = false) {
|
|
4805
4825
|
if (!str || expanded || str.length <= max) return str;
|
|
4806
4826
|
return str.slice(0, max) + "...";
|
|
@@ -4820,6 +4840,15 @@ function adCommand() {
|
|
|
4820
4840
|
console.log(` ${import_picocolors8.default.bold("Status:")} ${statusColor(ad.status)}`);
|
|
4821
4841
|
console.log(` ${import_picocolors8.default.bold("Campaign:")} ${ad.campaignId}`);
|
|
4822
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
|
+
}
|
|
4823
4852
|
if (ad.creative) {
|
|
4824
4853
|
const label = (name) => import_picocolors8.default.bold(name.padEnd(15));
|
|
4825
4854
|
const exp = !!opts.expanded;
|
|
@@ -4975,9 +5004,70 @@ function presetsSaveCommand() {
|
|
|
4975
5004
|
}
|
|
4976
5005
|
|
|
4977
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
|
|
4978
5012
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
4979
5013
|
var import_path2 = __toESM(require("path"), 1);
|
|
4980
|
-
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
|
|
4981
5071
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"]);
|
|
4982
5072
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]);
|
|
4983
5073
|
var MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024;
|
|
@@ -5001,19 +5091,19 @@ var MAGIC_BYTES = {
|
|
|
5001
5091
|
// ftyp container — check below
|
|
5002
5092
|
};
|
|
5003
5093
|
function validateMagicBytes(filePath, contentType) {
|
|
5004
|
-
const stat =
|
|
5094
|
+
const stat = import_fs3.default.statSync(filePath);
|
|
5005
5095
|
if (stat.size < 12) return false;
|
|
5006
5096
|
let fd;
|
|
5007
5097
|
try {
|
|
5008
|
-
fd =
|
|
5098
|
+
fd = import_fs3.default.openSync(filePath, "r");
|
|
5009
5099
|
const buf = Buffer.alloc(12);
|
|
5010
|
-
|
|
5011
|
-
|
|
5100
|
+
import_fs3.default.readSync(fd, buf, 0, 12, 0);
|
|
5101
|
+
import_fs3.default.closeSync(fd);
|
|
5012
5102
|
fd = null;
|
|
5013
5103
|
return checkMagicBytes(buf, contentType);
|
|
5014
5104
|
} finally {
|
|
5015
5105
|
if (fd != null) try {
|
|
5016
|
-
|
|
5106
|
+
import_fs3.default.closeSync(fd);
|
|
5017
5107
|
} catch {
|
|
5018
5108
|
}
|
|
5019
5109
|
}
|
|
@@ -5118,8 +5208,38 @@ async function putR2WithRetry(uploadUrl, fileBuffer, contentType, {
|
|
|
5118
5208
|
}
|
|
5119
5209
|
throw new Error(`R2 upload failed after ${maxAttempts} attempt(s)`);
|
|
5120
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
|
+
}
|
|
5121
5241
|
function getFileType(filePath) {
|
|
5122
|
-
const ext =
|
|
5242
|
+
const ext = import_path3.default.extname(filePath).toLowerCase();
|
|
5123
5243
|
if (IMAGE_EXTS.has(ext)) return "image";
|
|
5124
5244
|
if (VIDEO_EXTS.has(ext)) return "video";
|
|
5125
5245
|
return null;
|
|
@@ -5128,22 +5248,22 @@ function resolveFiles(inputs) {
|
|
|
5128
5248
|
const files = [];
|
|
5129
5249
|
const skipped = [];
|
|
5130
5250
|
for (const input of inputs) {
|
|
5131
|
-
const resolved =
|
|
5132
|
-
if (!
|
|
5251
|
+
const resolved = import_path3.default.resolve(input);
|
|
5252
|
+
if (!import_fs3.default.existsSync(resolved)) {
|
|
5133
5253
|
throw new Error(`File not found: ${input}`);
|
|
5134
5254
|
}
|
|
5135
|
-
const stat =
|
|
5255
|
+
const stat = import_fs3.default.statSync(resolved);
|
|
5136
5256
|
if (stat.isDirectory()) {
|
|
5137
|
-
const entries =
|
|
5257
|
+
const entries = import_fs3.default.readdirSync(resolved);
|
|
5138
5258
|
for (const entry of entries) {
|
|
5139
|
-
const fullPath =
|
|
5140
|
-
const entryStat =
|
|
5259
|
+
const fullPath = import_path3.default.join(resolved, entry);
|
|
5260
|
+
const entryStat = import_fs3.default.statSync(fullPath);
|
|
5141
5261
|
if (entryStat.isFile() && getFileType(fullPath)) {
|
|
5142
5262
|
if (entryStat.size > MAX_FILE_SIZE || entryStat.size === 0) {
|
|
5143
5263
|
skipped.push(`${entry} (${entryStat.size === 0 ? "empty" : "too large"})`);
|
|
5144
5264
|
continue;
|
|
5145
5265
|
}
|
|
5146
|
-
const ct = CONTENT_TYPES[
|
|
5266
|
+
const ct = CONTENT_TYPES[import_path3.default.extname(fullPath).toLowerCase()];
|
|
5147
5267
|
if (!validateMagicBytes(fullPath, ct)) {
|
|
5148
5268
|
skipped.push(`${entry} (content does not match extension)`);
|
|
5149
5269
|
continue;
|
|
@@ -5156,9 +5276,9 @@ function resolveFiles(inputs) {
|
|
|
5156
5276
|
if (!type) throw new Error(`Unsupported file type: ${input}`);
|
|
5157
5277
|
if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large: ${input} (${formatSize(stat.size)}, max ${formatSize(MAX_FILE_SIZE)})`);
|
|
5158
5278
|
if (stat.size === 0) throw new Error(`File is empty: ${input}`);
|
|
5159
|
-
const ct = CONTENT_TYPES[
|
|
5279
|
+
const ct = CONTENT_TYPES[import_path3.default.extname(resolved).toLowerCase()];
|
|
5160
5280
|
if (!validateMagicBytes(resolved, ct)) throw new Error(`File content does not match its extension: ${input}`);
|
|
5161
|
-
files.push({ path: resolved, name:
|
|
5281
|
+
files.push({ path: resolved, name: import_path3.default.basename(resolved), size: stat.size, type, contentType: ct });
|
|
5162
5282
|
}
|
|
5163
5283
|
}
|
|
5164
5284
|
if (skipped.length > 0) {
|
|
@@ -5166,7 +5286,43 @@ function resolveFiles(inputs) {
|
|
|
5166
5286
|
}
|
|
5167
5287
|
return files;
|
|
5168
5288
|
}
|
|
5169
|
-
|
|
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 } = {}) {
|
|
5170
5326
|
const client = createClient({
|
|
5171
5327
|
accountId: opts.account,
|
|
5172
5328
|
timeoutMs: parsePositiveInt2(opts.apiTimeout, void 0)
|
|
@@ -5175,7 +5331,7 @@ async function stageFiles(paths, opts) {
|
|
|
5175
5331
|
const uploadTimeoutMs = parsePositiveInt2(opts.uploadTimeout, DEFAULT_UPLOAD_TIMEOUT_MS);
|
|
5176
5332
|
let files;
|
|
5177
5333
|
try {
|
|
5178
|
-
files = resolveFiles(paths);
|
|
5334
|
+
files = filesOverride || resolveFiles(paths);
|
|
5179
5335
|
} catch (err) {
|
|
5180
5336
|
printError(err.message);
|
|
5181
5337
|
process.exit(1);
|
|
@@ -5184,27 +5340,27 @@ async function stageFiles(paths, opts) {
|
|
|
5184
5340
|
printError("No supported media files found.");
|
|
5185
5341
|
process.exit(1);
|
|
5186
5342
|
}
|
|
5187
|
-
if (!jsonMode) {
|
|
5188
|
-
console.log(`
|
|
5189
|
-
${import_picocolors10.default.bold("Uploading")} ${files.length} file(s)
|
|
5190
|
-
`);
|
|
5191
|
-
}
|
|
5192
5343
|
const filesMeta = files.map((f) => ({
|
|
5193
5344
|
name: f.name,
|
|
5194
5345
|
size: f.size,
|
|
5195
5346
|
type: f.type,
|
|
5196
5347
|
contentType: f.contentType
|
|
5197
5348
|
}));
|
|
5198
|
-
const initResult = await client.uploads.init(filesMeta);
|
|
5349
|
+
const initResult = await client.uploads.init(filesMeta, { batchId: existingBatchId });
|
|
5199
5350
|
const { batchId } = initResult;
|
|
5200
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
|
+
}
|
|
5201
5357
|
const uploadedFiles = await runBoundedConcurrency(files, concurrency, async (file, i) => {
|
|
5202
5358
|
const r2Info = initResult.files[i];
|
|
5203
5359
|
if (!jsonMode) {
|
|
5204
5360
|
console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(`Uploading ${file.name}... ${formatSize(file.size)}`)}`);
|
|
5205
5361
|
}
|
|
5206
5362
|
try {
|
|
5207
|
-
const fileBuffer =
|
|
5363
|
+
const fileBuffer = import_fs3.default.readFileSync(file.path);
|
|
5208
5364
|
await putR2WithRetry(
|
|
5209
5365
|
r2Info.uploadUrl,
|
|
5210
5366
|
fileBuffer,
|
|
@@ -5212,40 +5368,60 @@ async function stageFiles(paths, opts) {
|
|
|
5212
5368
|
{ timeoutMs: uploadTimeoutMs }
|
|
5213
5369
|
);
|
|
5214
5370
|
return {
|
|
5371
|
+
path: file.path,
|
|
5215
5372
|
name: file.name,
|
|
5216
5373
|
type: file.type,
|
|
5217
5374
|
size: file.size,
|
|
5375
|
+
contentType: file.contentType,
|
|
5218
5376
|
fileKey: r2Info.fileKey
|
|
5219
5377
|
};
|
|
5220
5378
|
} catch (err) {
|
|
5221
5379
|
if (!jsonMode) {
|
|
5222
5380
|
console.log(` ${import_picocolors10.default.red("\u2717")} ${file.name}: ${err.message}`);
|
|
5223
5381
|
}
|
|
5224
|
-
return {
|
|
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
|
+
};
|
|
5225
5391
|
}
|
|
5226
5392
|
});
|
|
5227
|
-
return { client, batchId, uploadedFiles, jsonMode };
|
|
5393
|
+
return { client, batchId, uploadedFiles, files, jsonMode };
|
|
5228
5394
|
}
|
|
5229
|
-
async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
5395
|
+
async function processAndPrint(client, batchId, uploadedFiles, jsonMode, { isRetry = false } = {}) {
|
|
5230
5396
|
const validFiles = uploadedFiles.filter((f) => f.fileKey);
|
|
5231
5397
|
if (validFiles.length === 0) {
|
|
5232
5398
|
printError("All uploads failed.");
|
|
5233
|
-
|
|
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
|
+
};
|
|
5234
5409
|
}
|
|
5235
5410
|
if (!jsonMode) {
|
|
5236
|
-
console.log(` ${import_picocolors10.default.dim("\u2192")} ${import_picocolors10.default.dim(
|
|
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
|
+
}
|
|
5237
5415
|
}
|
|
5238
5416
|
const results = [];
|
|
5239
5417
|
let completed = 0;
|
|
5240
5418
|
let failed = 0;
|
|
5241
|
-
for (const file of validFiles) {
|
|
5419
|
+
for (const [index, file] of validFiles.entries()) {
|
|
5242
5420
|
try {
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
fileKey: file.fileKey
|
|
5248
|
-
});
|
|
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);
|
|
5249
5425
|
results.push(result);
|
|
5250
5426
|
if (result.status === "complete") {
|
|
5251
5427
|
completed++;
|
|
@@ -5268,7 +5444,7 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
|
5268
5444
|
}
|
|
5269
5445
|
}
|
|
5270
5446
|
let groups = [];
|
|
5271
|
-
if (completed > 1) {
|
|
5447
|
+
if (isRetry ? completed >= 1 : completed > 1) {
|
|
5272
5448
|
try {
|
|
5273
5449
|
const finalizeResult = await client.uploads.finalize(batchId);
|
|
5274
5450
|
groups = finalizeResult.groups || [];
|
|
@@ -5277,7 +5453,16 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
|
5277
5453
|
}
|
|
5278
5454
|
if (jsonMode) {
|
|
5279
5455
|
printJson({ batchId, status: failed === 0 ? "complete" : completed === 0 ? "failed" : "partial", completed, failed, total: validFiles.length, files: results, groups });
|
|
5280
|
-
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
|
+
};
|
|
5281
5466
|
}
|
|
5282
5467
|
if (groups.length > 0) {
|
|
5283
5468
|
console.log(`
|
|
@@ -5302,11 +5487,93 @@ async function processAndPrint(client, batchId, uploadedFiles, jsonMode) {
|
|
|
5302
5487
|
console.log(` ${import_picocolors10.default.bold("Groups:")} ${groups.length} variant group(s) detected`);
|
|
5303
5488
|
}
|
|
5304
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 });
|
|
5305
5559
|
}
|
|
5306
5560
|
function uploadCommand() {
|
|
5307
|
-
return new Command("upload").description("Upload media files and process to Meta").argument("
|
|
5308
|
-
const
|
|
5309
|
-
|
|
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);
|
|
5310
5577
|
});
|
|
5311
5578
|
}
|
|
5312
5579
|
function uploadsCommand() {
|
|
@@ -5390,7 +5657,7 @@ function uploadsCommand() {
|
|
|
5390
5657
|
}
|
|
5391
5658
|
|
|
5392
5659
|
// src/commands/create.js
|
|
5393
|
-
var
|
|
5660
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
5394
5661
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5395
5662
|
|
|
5396
5663
|
// src/lib/poll.js
|
|
@@ -5629,6 +5896,13 @@ async function executeCreate(body, opts, { preview = false, test = false } = {})
|
|
|
5629
5896
|
if (sharedCta) console.log(` ${import_picocolors12.default.bold("CTA:")} ${sharedCta}`);
|
|
5630
5897
|
if (sharedLink) console.log(` ${import_picocolors12.default.bold("Link:")} ${wrapLine(sharedLink, valCol)}`);
|
|
5631
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
|
+
}
|
|
5632
5906
|
const enh = result.enhancements;
|
|
5633
5907
|
let enhDetail;
|
|
5634
5908
|
if (enh === "metaDefaults") enhDetail = "Meta Defaults";
|
|
@@ -5702,7 +5976,7 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5702
5976
|
let body;
|
|
5703
5977
|
if (specFile) {
|
|
5704
5978
|
try {
|
|
5705
|
-
const raw =
|
|
5979
|
+
const raw = import_fs4.default.readFileSync(specFile, "utf8");
|
|
5706
5980
|
body = JSON.parse(raw);
|
|
5707
5981
|
} catch (err) {
|
|
5708
5982
|
printError(`Failed to read spec file: ${err.message}`);
|
|
@@ -5716,7 +5990,7 @@ function buildBodyFromOpts(specFile, opts) {
|
|
|
5716
5990
|
if (opts.upload) body.uploadId = opts.upload;
|
|
5717
5991
|
if (opts.textFile) {
|
|
5718
5992
|
try {
|
|
5719
|
-
body.texts = JSON.parse(
|
|
5993
|
+
body.texts = JSON.parse(import_fs4.default.readFileSync(opts.textFile, "utf8"));
|
|
5720
5994
|
} catch (err) {
|
|
5721
5995
|
printError(`Failed to read text file: ${err.message}`);
|
|
5722
5996
|
process.exit(1);
|
|
@@ -6177,7 +6451,7 @@ function statusLabel(status) {
|
|
|
6177
6451
|
}
|
|
6178
6452
|
|
|
6179
6453
|
// src/cli.js
|
|
6180
|
-
var VERSION = true ? "0.2.
|
|
6454
|
+
var VERSION = true ? "0.2.1" : "0.0.0";
|
|
6181
6455
|
var apiUrl = process.env.ADS_API_URL || getBaseUrl();
|
|
6182
6456
|
if (apiUrl && (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1"))) {
|
|
6183
6457
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
@@ -6219,12 +6493,12 @@ program2.addCommand(createTestCommand(), { hidden: true });
|
|
|
6219
6493
|
program2.addCommand(jobsCommand());
|
|
6220
6494
|
async function checkForUpdates() {
|
|
6221
6495
|
try {
|
|
6222
|
-
const
|
|
6223
|
-
const
|
|
6224
|
-
const
|
|
6225
|
-
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");
|
|
6226
6500
|
try {
|
|
6227
|
-
const stat =
|
|
6501
|
+
const stat = fs5.default.statSync(cacheFile);
|
|
6228
6502
|
if (Date.now() - stat.mtimeMs < 24 * 60 * 60 * 1e3) return;
|
|
6229
6503
|
} catch {
|
|
6230
6504
|
}
|
|
@@ -6233,8 +6507,8 @@ async function checkForUpdates() {
|
|
|
6233
6507
|
const data = await resp.json();
|
|
6234
6508
|
const latest = data.version;
|
|
6235
6509
|
try {
|
|
6236
|
-
|
|
6237
|
-
|
|
6510
|
+
fs5.default.mkdirSync(path4.dirname(cacheFile), { recursive: true });
|
|
6511
|
+
fs5.default.writeFileSync(cacheFile, latest);
|
|
6238
6512
|
} catch {
|
|
6239
6513
|
}
|
|
6240
6514
|
if (latest !== VERSION) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adsuploader/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Create Facebook ads from the command line — bulk upload media, preview configurations, and launch ads at scale",
|
|
5
5
|
"author": "Ads Uploader <support@adsuploader.com> (https://adsuploader.com)",
|
|
6
6
|
"license": "UNLICENSED",
|