@zalify/cli 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +190 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ its new home under `shop`.
|
|
|
101
101
|
| --- | --- |
|
|
102
102
|
| `ads create <dir>` | Provision an Autopilot brand-instance GitHub repo (POST `/api/repos`, GitHub App), scaffold the starter locally (`identity.yaml`, `brand.yaml`, `policy/`, `campaigns/`, `media-plans/`, `signals/`, `decisions/`), and push with a short-lived repo-scoped token. Writes `.zalify/brand-repo.json` so later commands run arg-free. The directory name becomes the slug — use the workspace's store slug so the service's digest writer can find the repo |
|
|
103
103
|
| `ads` / `ads list` | List the workspace's brand instances and their provisioning status |
|
|
104
|
+
| `ads new <name> [--objective] [--budget] [--country] [--link]` | Scaffold a campaign FOLDER — the drag-and-drop form: `campaign.yaml` for budget/audience/copy defaults, `creatives/` for the files that become ads. Three questions when interactive, flags otherwise. `cp -r` an existing folder to duplicate a campaign |
|
|
105
|
+
| `ads pull [--account <act_id>] [--days <n>] [--force]` | The existing account as files: every ingested campaign lands as `campaigns/<slug>.yaml` (imported form — names in `label:`, budgets, statuses, core audience) plus `campaigns/.bindings.json` mapping YAML nodes to platform ids. A hash ledger protects local edits: unedited files fast-forward, edited ones are kept and named, deletions never happen automatically |
|
|
104
106
|
| `ads remove <slug>` | Deprecate a brand instance — soft-deletes the record (drops out of `ads list` and the service's repo-token minting; survives for history). The GitHub repo is deliberately untouched: deleting real campaign intent is a human-on-GitHub decision |
|
|
105
107
|
| `ads status` | The instance at a glance: brand + consent, platform access and credential expiry, ingestion freshness (with per-account failures), first-party join coverage, plan and pending-proposal counts. Exits non-zero when the service reports problems |
|
|
106
108
|
| `ads report [--date <YYYY-MM-DD>] [--platform <all\|meta\|google>]` | The daily report in the terminal — totals vs the day before, where the money moved, what changed in the account |
|
package/dist/cli.js
CHANGED
|
@@ -18234,7 +18234,7 @@ function updateNotifier(options) {
|
|
|
18234
18234
|
|
|
18235
18235
|
// src/cli.ts
|
|
18236
18236
|
import { createRequire as createRequire2 } from "node:module";
|
|
18237
|
-
import { dirname as
|
|
18237
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
18238
18238
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
18239
18239
|
|
|
18240
18240
|
// src/auth.ts
|
|
@@ -20118,8 +20118,9 @@ async function shopifyUploadImages(storeDir, options = {}) {
|
|
|
20118
20118
|
|
|
20119
20119
|
// src/ads.ts
|
|
20120
20120
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
20121
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
20121
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
20122
20122
|
import { basename as basename4, join as join8, resolve as resolve6 } from "node:path";
|
|
20123
|
+
import { createInterface } from "node:readline/promises";
|
|
20123
20124
|
function apiError3(status, json) {
|
|
20124
20125
|
if (json.code === "SLUG_TAKEN") {
|
|
20125
20126
|
return new Error(`${json.error}
|
|
@@ -20255,6 +20256,62 @@ Next steps:
|
|
|
20255
20256
|
` + ` - flip consent: true once the brand owner has signed off
|
|
20256
20257
|
` + ` - the Autopilot service picks the instance up from there`);
|
|
20257
20258
|
}
|
|
20259
|
+
var CAMPAIGN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
20260
|
+
function campaignsDir() {
|
|
20261
|
+
const cwd = resolve6(".");
|
|
20262
|
+
if (existsSync8(join8(cwd, "campaigns")))
|
|
20263
|
+
return join8(cwd, "campaigns");
|
|
20264
|
+
if (existsSync8(join8(cwd, ".zalify", "brand-repo.json"))) {
|
|
20265
|
+
return join8(cwd, "campaigns");
|
|
20266
|
+
}
|
|
20267
|
+
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20268
|
+
}
|
|
20269
|
+
async function ask(question, fallback) {
|
|
20270
|
+
if (!process.stdin.isTTY)
|
|
20271
|
+
return fallback;
|
|
20272
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20273
|
+
const answer = (await rl.question(`${question} [${fallback}]: `)).trim();
|
|
20274
|
+
rl.close();
|
|
20275
|
+
return answer || fallback;
|
|
20276
|
+
}
|
|
20277
|
+
async function adsNew(name, opts) {
|
|
20278
|
+
if (!CAMPAIGN_NAME_RE.test(name) || name.length > 63) {
|
|
20279
|
+
throw new Error(`"${name}" — campaign names are lowercase letters, digits, and inner hyphens.`);
|
|
20280
|
+
}
|
|
20281
|
+
const dir2 = join8(campaignsDir(), name);
|
|
20282
|
+
if (existsSync8(dir2))
|
|
20283
|
+
throw new Error(`campaigns/${name}/ already exists.`);
|
|
20284
|
+
const objective = opts.objective ?? await ask("objective (sales/leads/traffic/awareness)", "sales");
|
|
20285
|
+
const budget = opts.budget ?? await ask("budget", "$20/day");
|
|
20286
|
+
const country = opts.country ?? await ask("country", "US");
|
|
20287
|
+
const link = opts.link ?? "";
|
|
20288
|
+
mkdirSync3(join8(dir2, "creatives"), { recursive: true });
|
|
20289
|
+
writeFileSync7(join8(dir2, "creatives", ".gitkeep"), "");
|
|
20290
|
+
writeFileSync7(join8(dir2, "campaign.yaml"), `# ${name} — drop images into creatives/, fill the copy below, then
|
|
20291
|
+
# \`zalify ads sync\`. Each file becomes an ad named after itself; give
|
|
20292
|
+
# one ad its own copy with creatives/<file>.copy.yaml.
|
|
20293
|
+
campaign: ${name}
|
|
20294
|
+
objective: ${objective}
|
|
20295
|
+
budget: ${budget} # "$60/day" or "$900 total"
|
|
20296
|
+
|
|
20297
|
+
# Copy every derived ad inherits.
|
|
20298
|
+
creative_defaults:
|
|
20299
|
+
primary_text: ""${link ? "" : " # ← fill me"}
|
|
20300
|
+
link: ${link || '"" # ← your product URL'}
|
|
20301
|
+
cta: shop_now
|
|
20302
|
+
|
|
20303
|
+
ad_sets:
|
|
20304
|
+
- name: main
|
|
20305
|
+
audience:
|
|
20306
|
+
countries: [${country.split(",").map((c) => c.trim().toUpperCase()).filter(Boolean).join(", ")}]
|
|
20307
|
+
age: 18-65
|
|
20308
|
+
advantage: true
|
|
20309
|
+
`);
|
|
20310
|
+
console.log(`✓ campaigns/${name}/ is ready`);
|
|
20311
|
+
console.log(" 1. drag images into creatives/");
|
|
20312
|
+
console.log(" 2. fill primary_text and link in campaign.yaml");
|
|
20313
|
+
console.log(" 3. `zalify ads sync` when it lands — validate, diff, apply");
|
|
20314
|
+
}
|
|
20258
20315
|
async function adsRemove(slug) {
|
|
20259
20316
|
const auth = requireActive();
|
|
20260
20317
|
const json = await request2(auth, "GET", "/api/repos");
|
|
@@ -20281,10 +20338,105 @@ async function adsList() {
|
|
|
20281
20338
|
}
|
|
20282
20339
|
}
|
|
20283
20340
|
|
|
20341
|
+
// src/ads-pull.ts
|
|
20342
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
20343
|
+
import {
|
|
20344
|
+
existsSync as existsSync9,
|
|
20345
|
+
mkdirSync as mkdirSync4,
|
|
20346
|
+
readFileSync as readFileSync7,
|
|
20347
|
+
writeFileSync as writeFileSync8
|
|
20348
|
+
} from "node:fs";
|
|
20349
|
+
import { dirname as dirname2, join as join9, resolve as resolve7 } from "node:path";
|
|
20350
|
+
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20351
|
+
function checkoutRoot() {
|
|
20352
|
+
const cwd = resolve7(".");
|
|
20353
|
+
if (existsSync9(join9(cwd, ".zalify", "brand-repo.json")) || existsSync9(join9(cwd, "campaigns"))) {
|
|
20354
|
+
return cwd;
|
|
20355
|
+
}
|
|
20356
|
+
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20357
|
+
}
|
|
20358
|
+
function readBindings(root) {
|
|
20359
|
+
const path9 = join9(root, "campaigns", ".bindings.json");
|
|
20360
|
+
if (!existsSync9(path9))
|
|
20361
|
+
return { nodes: {}, pulled: {} };
|
|
20362
|
+
const raw = JSON.parse(readFileSync7(path9, "utf8"));
|
|
20363
|
+
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20364
|
+
}
|
|
20365
|
+
async function adsPull(opts) {
|
|
20366
|
+
const auth = requireActive();
|
|
20367
|
+
const root = checkoutRoot();
|
|
20368
|
+
const params = new URLSearchParams;
|
|
20369
|
+
if (opts.account)
|
|
20370
|
+
params.set("account", opts.account);
|
|
20371
|
+
if (opts.days)
|
|
20372
|
+
params.set("days", opts.days);
|
|
20373
|
+
const q = params.toString();
|
|
20374
|
+
const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/export${q ? `?${q}` : ""}`, {
|
|
20375
|
+
headers: {
|
|
20376
|
+
Authorization: `Bearer ${auth.key}`,
|
|
20377
|
+
"x-zalify-workspace-id": auth.workspaceId
|
|
20378
|
+
}
|
|
20379
|
+
});
|
|
20380
|
+
if (!res.ok) {
|
|
20381
|
+
const body = await res.json().catch(() => ({}));
|
|
20382
|
+
throw new Error(`export failed (${res.status}): ${body.error ?? ""}`);
|
|
20383
|
+
}
|
|
20384
|
+
const result = await res.json();
|
|
20385
|
+
const bindings = readBindings(root);
|
|
20386
|
+
let written = 0;
|
|
20387
|
+
let unchanged = 0;
|
|
20388
|
+
const skipped = [];
|
|
20389
|
+
for (const f of result.files) {
|
|
20390
|
+
const abs = join9(root, f.path);
|
|
20391
|
+
const nextHash = sha(f.yaml);
|
|
20392
|
+
const lastPulled = bindings.pulled[f.path];
|
|
20393
|
+
if (existsSync9(abs)) {
|
|
20394
|
+
const current = readFileSync7(abs, "utf8");
|
|
20395
|
+
if (sha(current) === nextHash) {
|
|
20396
|
+
unchanged += 1;
|
|
20397
|
+
bindings.pulled[f.path] = nextHash;
|
|
20398
|
+
continue;
|
|
20399
|
+
}
|
|
20400
|
+
const locallyEdited = lastPulled !== undefined && sha(current) !== lastPulled;
|
|
20401
|
+
if (locallyEdited && !opts.force) {
|
|
20402
|
+
skipped.push(f.path);
|
|
20403
|
+
continue;
|
|
20404
|
+
}
|
|
20405
|
+
}
|
|
20406
|
+
mkdirSync4(dirname2(abs), { recursive: true });
|
|
20407
|
+
writeFileSync8(abs, f.yaml);
|
|
20408
|
+
bindings.pulled[f.path] = nextHash;
|
|
20409
|
+
written += 1;
|
|
20410
|
+
}
|
|
20411
|
+
Object.assign(bindings.nodes, result.bindings);
|
|
20412
|
+
const bindingsPath = join9(root, "campaigns", ".bindings.json");
|
|
20413
|
+
mkdirSync4(dirname2(bindingsPath), { recursive: true });
|
|
20414
|
+
writeFileSync8(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20415
|
+
`);
|
|
20416
|
+
const exported = new Set(result.files.map((f) => f.path));
|
|
20417
|
+
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) && existsSync9(join9(root, p)));
|
|
20418
|
+
const s = result.summary;
|
|
20419
|
+
console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
|
|
20420
|
+
for (const p of skipped)
|
|
20421
|
+
console.log(` kept ${p} (edited — --force overwrites)`);
|
|
20422
|
+
if (gone.length > 0) {
|
|
20423
|
+
console.log(`${gone.length} file${gone.length > 1 ? "s" : ""} no longer upstream (left in place):`);
|
|
20424
|
+
for (const p of gone.slice(0, 10))
|
|
20425
|
+
console.log(` gone ${p}`);
|
|
20426
|
+
if (gone.length > 10)
|
|
20427
|
+
console.log(` … and ${gone.length - 10} more`);
|
|
20428
|
+
}
|
|
20429
|
+
if (s.invalid.length > 0) {
|
|
20430
|
+
console.log(`${s.invalid.length} campaigns failed export-side validation — report this; they were not written.`);
|
|
20431
|
+
}
|
|
20432
|
+
console.log(`
|
|
20433
|
+
Review with git diff; commit when it reads right.`);
|
|
20434
|
+
}
|
|
20435
|
+
|
|
20284
20436
|
// src/ads-read.ts
|
|
20285
20437
|
import { spawn as spawn4 } from "node:child_process";
|
|
20286
|
-
import { existsSync as
|
|
20287
|
-
import { join as
|
|
20438
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
20439
|
+
import { join as join10, resolve as resolve8 } from "node:path";
|
|
20288
20440
|
|
|
20289
20441
|
// node_modules/yaml/dist/index.js
|
|
20290
20442
|
var composer = require_composer();
|
|
@@ -20420,12 +20572,12 @@ account changes: not tracked for this platform yet` : `
|
|
|
20420
20572
|
account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
|
|
20421
20573
|
}
|
|
20422
20574
|
function adsDigest(dir2) {
|
|
20423
|
-
const root =
|
|
20424
|
-
const path9 =
|
|
20425
|
-
if (!
|
|
20575
|
+
const root = resolve8(dir2 ?? ".");
|
|
20576
|
+
const path9 = join10(root, "signals", "latest.json");
|
|
20577
|
+
if (!existsSync10(path9)) {
|
|
20426
20578
|
throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
|
|
20427
20579
|
}
|
|
20428
|
-
const d = JSON.parse(
|
|
20580
|
+
const d = JSON.parse(readFileSync8(path9, "utf8"));
|
|
20429
20581
|
console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
|
|
20430
20582
|
const t = d.totals;
|
|
20431
20583
|
console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
|
|
@@ -20464,26 +20616,26 @@ function adsOpen() {
|
|
|
20464
20616
|
}
|
|
20465
20617
|
async function adsDoctor(dir2) {
|
|
20466
20618
|
const auth = requireActive();
|
|
20467
|
-
const root =
|
|
20619
|
+
const root = resolve8(dir2 ?? ".");
|
|
20468
20620
|
let failed = 0;
|
|
20469
20621
|
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
20470
20622
|
const bad = (msg) => {
|
|
20471
20623
|
console.log(` ✗ ${msg}`);
|
|
20472
20624
|
failed += 1;
|
|
20473
20625
|
};
|
|
20474
|
-
const linkPath =
|
|
20626
|
+
const linkPath = join10(root, ".zalify", "brand-repo.json");
|
|
20475
20627
|
let link = null;
|
|
20476
|
-
if (
|
|
20477
|
-
link = JSON.parse(
|
|
20628
|
+
if (existsSync10(linkPath)) {
|
|
20629
|
+
link = JSON.parse(readFileSync8(linkPath, "utf8"));
|
|
20478
20630
|
ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
|
|
20479
20631
|
} else {
|
|
20480
20632
|
console.log(" - not inside a brand-instance checkout (repo checks only)");
|
|
20481
20633
|
}
|
|
20482
20634
|
let identity2 = null;
|
|
20483
|
-
const identityPath =
|
|
20484
|
-
if (
|
|
20635
|
+
const identityPath = join10(root, "identity.yaml");
|
|
20636
|
+
if (existsSync10(identityPath)) {
|
|
20485
20637
|
try {
|
|
20486
|
-
identity2 = $parse(
|
|
20638
|
+
identity2 = $parse(readFileSync8(identityPath, "utf8"));
|
|
20487
20639
|
ok("identity.yaml parses");
|
|
20488
20640
|
if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
|
|
20489
20641
|
bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
|
|
@@ -20549,8 +20701,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20549
20701
|
}
|
|
20550
20702
|
|
|
20551
20703
|
// src/brand.ts
|
|
20552
|
-
import { existsSync as
|
|
20553
|
-
import { basename as basename5, join as
|
|
20704
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
|
|
20705
|
+
import { basename as basename5, join as join11, resolve as resolve9 } from "node:path";
|
|
20554
20706
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20555
20707
|
|
|
20556
20708
|
The rules for filling in this folder. An author — human or AI agent —
|
|
@@ -20851,19 +21003,19 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
20851
21003
|
}, null, 2) + `
|
|
20852
21004
|
`;
|
|
20853
21005
|
function brandInit(dir2 = ".", opts = {}) {
|
|
20854
|
-
const target =
|
|
21006
|
+
const target = resolve9(dir2);
|
|
20855
21007
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
20856
|
-
if (
|
|
21008
|
+
if (existsSync11(target) && readdirSync3(target).length > 0) {
|
|
20857
21009
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
20858
21010
|
}
|
|
20859
21011
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
20860
21012
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
20861
|
-
|
|
20862
|
-
|
|
20863
|
-
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
21013
|
+
mkdirSync5(join11(target, "images"), { recursive: true });
|
|
21014
|
+
writeFileSync9(join11(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21015
|
+
writeFileSync9(join11(target, "brand.md"), BRAND_MD(name));
|
|
21016
|
+
writeFileSync9(join11(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
21017
|
+
writeFileSync9(join11(target, "catalog.json"), CATALOG_JSON(name, currency));
|
|
21018
|
+
writeFileSync9(join11(target, "images", "manifest.json"), MANIFEST_JSON);
|
|
20867
21019
|
console.log(`Scaffolded brand data in ${dir2}:`);
|
|
20868
21020
|
for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
|
|
20869
21021
|
console.log(` + ${f}`);
|
|
@@ -20874,23 +21026,23 @@ Author in this order: brand.md → catalog.json → images/manifest.json` + `
|
|
|
20874
21026
|
}
|
|
20875
21027
|
var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
|
|
20876
21028
|
function brandValidate(dir2 = ".") {
|
|
20877
|
-
const target =
|
|
21029
|
+
const target = resolve9(dir2);
|
|
20878
21030
|
const problems = [];
|
|
20879
21031
|
const warnings = [];
|
|
20880
21032
|
const readJson2 = (rel) => {
|
|
20881
|
-
const path9 =
|
|
20882
|
-
if (!
|
|
21033
|
+
const path9 = join11(target, rel);
|
|
21034
|
+
if (!existsSync11(path9)) {
|
|
20883
21035
|
problems.push(`missing ${rel}`);
|
|
20884
21036
|
return null;
|
|
20885
21037
|
}
|
|
20886
21038
|
try {
|
|
20887
|
-
return JSON.parse(
|
|
21039
|
+
return JSON.parse(readFileSync9(path9, "utf8"));
|
|
20888
21040
|
} catch (err) {
|
|
20889
21041
|
problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
|
|
20890
21042
|
return null;
|
|
20891
21043
|
}
|
|
20892
21044
|
};
|
|
20893
|
-
if (!
|
|
21045
|
+
if (!existsSync11(join11(target, "brand.md")))
|
|
20894
21046
|
problems.push("missing brand.md");
|
|
20895
21047
|
const store = readJson2("store.json");
|
|
20896
21048
|
if (store && (!store.name || !store.slug))
|
|
@@ -21064,9 +21216,9 @@ function brandValidate(dir2 = ".") {
|
|
|
21064
21216
|
}
|
|
21065
21217
|
|
|
21066
21218
|
// src/cli.ts
|
|
21067
|
-
var __dirname4 =
|
|
21219
|
+
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
21068
21220
|
var require2 = createRequire2(import.meta.url);
|
|
21069
|
-
var pkg = require2(
|
|
21221
|
+
var pkg = require2(join12(__dirname4, "..", "package.json"));
|
|
21070
21222
|
try {
|
|
21071
21223
|
updateNotifier({ pkg }).notify({
|
|
21072
21224
|
isGlobal: true,
|
|
@@ -21148,6 +21300,12 @@ ads.command("list", { isDefault: true }).description("List the workspace's brand
|
|
|
21148
21300
|
ads.command("remove <slug>").description("Deprecate a brand instance (record soft-deleted; the GitHub repo is left untouched)").action(async (slug) => {
|
|
21149
21301
|
await adsRemove(slug);
|
|
21150
21302
|
});
|
|
21303
|
+
ads.command("new <name>").description("Scaffold a campaign folder — drop images into creatives/, then sync").option("--objective <o>", "sales | leads | traffic | awareness").option("--budget <b>", '"$60/day" or "$900 total"').option("--country <cc>", "comma-separated ISO codes, e.g. US,CA").option("--link <url>", "destination URL for creative_defaults").action(async (name, options) => {
|
|
21304
|
+
await adsNew(name, options);
|
|
21305
|
+
});
|
|
21306
|
+
ads.command("pull").description("The existing account as files — campaigns/<slug>.yaml + bindings, from what the service has ingested").option("--account <act_id>", "one ad account only").option("--days <n>", "ads seen in the trailing window (default 30)").option("--force", "overwrite locally edited files").action(async (options) => {
|
|
21307
|
+
await adsPull(options);
|
|
21308
|
+
});
|
|
21151
21309
|
ads.command("status").description("The brand instance at a glance: access, ingestion freshness, plans, pending proposals").action(async () => {
|
|
21152
21310
|
await adsStatus();
|
|
21153
21311
|
});
|