@zalify/cli 0.23.0 → 0.24.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 -2
- package/dist/cli.js +114 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,8 +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
|
|
105
|
-
| `ads pull [--account <act_id>] [--days <n>] [--force]` | The existing
|
|
104
|
+
| `ads new <name> [--account <folder>] [--objective] [--budget] [--country] [--link]` | Scaffold a campaign FOLDER at `accounts/<account>/<name>/` (account inferred when only one exists): `campaign.yaml` for budget/audience/copy defaults, `creatives/` for the files that become ads. `cp -r` an existing folder to duplicate a campaign |
|
|
105
|
+
| `ads pull [--account <act_id>] [--days <n>] [--force]` | The existing accounts as a tree: every ingested campaign lands as `accounts/<account-name>/<campaign>/campaign.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 |
|
|
106
106
|
| `ads creative generate <campaign> --prompt <text> [--orientation] [--ref <url...>] [--name]` | One image through the canvas pipeline (safe zones, product fidelity with refs), downloaded INTO the campaign's `creatives/` and recorded in its `assets.json` — the filesystem stays the interface, and the asset is stored once |
|
|
107
107
|
| `ads diff` | Desired (your campaign files) vs actual (the ingested account), keyed through bindings: bound nodes compare budget/status, unbound nodes become creations. Folder campaigns upload their `creatives/` first (presign + checksum dedup — the `brand images push` machinery) and derive server-side. Read-only against the platform |
|
|
108
108
|
| `ads apply [-y] [--activate]` | Execute the diff after showing it and asking. Creations land **PAUSED** — always; budget edits apply to bound entities; `paused→active` is refused without `--activate` (turning spend on is said twice; `active→paused` always executes). Created ids merge into the bindings ledger |
|
package/dist/cli.js
CHANGED
|
@@ -20144,8 +20144,8 @@ async function shopifyUploadImages(storeDir, options = {}) {
|
|
|
20144
20144
|
|
|
20145
20145
|
// src/ads.ts
|
|
20146
20146
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
20147
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
20148
|
-
import { basename as basename4, join as join8, resolve as resolve6 } from "node:path";
|
|
20147
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
20148
|
+
import { basename as basename4, join as join8, relative, resolve as resolve6 } from "node:path";
|
|
20149
20149
|
import { createInterface } from "node:readline/promises";
|
|
20150
20150
|
function apiError3(status, json) {
|
|
20151
20151
|
if (json.code === "SLUG_TAKEN") {
|
|
@@ -20283,14 +20283,22 @@ Next steps:
|
|
|
20283
20283
|
` + ` - the Autopilot service picks the instance up from there`);
|
|
20284
20284
|
}
|
|
20285
20285
|
var CAMPAIGN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
20286
|
-
function
|
|
20286
|
+
function campaignParent(account) {
|
|
20287
20287
|
const cwd = resolve6(".");
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20288
|
+
const inCheckout = existsSync8(join8(cwd, ".zalify", "brand-repo.json")) || existsSync8(join8(cwd, "accounts"));
|
|
20289
|
+
if (!inCheckout) {
|
|
20290
|
+
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20291
|
+
}
|
|
20292
|
+
const accountsDir = join8(cwd, "accounts");
|
|
20293
|
+
if (account)
|
|
20294
|
+
return join8(accountsDir, account);
|
|
20295
|
+
const dirs = existsSync8(accountsDir) ? readdirSync3(accountsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
|
|
20296
|
+
if (dirs.length === 1)
|
|
20297
|
+
return join8(accountsDir, dirs[0]);
|
|
20298
|
+
if (dirs.length === 0) {
|
|
20299
|
+
throw new Error("No account folders yet — `zalify ads pull` once, or pass --account <folder>.");
|
|
20300
|
+
}
|
|
20301
|
+
throw new Error(`Several accounts (${dirs.join(", ")}) — pass --account <folder>.`);
|
|
20294
20302
|
}
|
|
20295
20303
|
async function ask(question, fallback) {
|
|
20296
20304
|
if (!process.stdin.isTTY)
|
|
@@ -20304,9 +20312,10 @@ async function adsNew(name, opts) {
|
|
|
20304
20312
|
if (!CAMPAIGN_NAME_RE.test(name) || name.length > 63) {
|
|
20305
20313
|
throw new Error(`"${name}" — campaign names are lowercase letters, digits, and inner hyphens.`);
|
|
20306
20314
|
}
|
|
20307
|
-
const
|
|
20315
|
+
const parent = campaignParent(opts.account);
|
|
20316
|
+
const dir2 = join8(parent, name);
|
|
20308
20317
|
if (existsSync8(dir2))
|
|
20309
|
-
throw new Error(
|
|
20318
|
+
throw new Error(`${dir2} already exists.`);
|
|
20310
20319
|
const objective = opts.objective ?? await ask("objective (sales/leads/traffic/awareness)", "sales");
|
|
20311
20320
|
const budget = opts.budget ?? await ask("budget", "$20/day");
|
|
20312
20321
|
const country = opts.country ?? await ask("country", "US");
|
|
@@ -20333,7 +20342,7 @@ ad_sets:
|
|
|
20333
20342
|
age: 18-65
|
|
20334
20343
|
advantage: true
|
|
20335
20344
|
`);
|
|
20336
|
-
console.log(`✓
|
|
20345
|
+
console.log(`✓ ${relative(resolve6("."), dir2)}/ is ready`);
|
|
20337
20346
|
console.log(" 1. drag images into creatives/");
|
|
20338
20347
|
console.log(" 2. fill primary_text and link in campaign.yaml");
|
|
20339
20348
|
console.log(" 3. `zalify ads sync` when it lands — validate, diff, apply");
|
|
@@ -20365,7 +20374,13 @@ async function adsList() {
|
|
|
20365
20374
|
}
|
|
20366
20375
|
|
|
20367
20376
|
// src/ads-diff.ts
|
|
20368
|
-
import {
|
|
20377
|
+
import {
|
|
20378
|
+
existsSync as existsSync10,
|
|
20379
|
+
mkdirSync as mkdirSync4,
|
|
20380
|
+
readdirSync as readdirSync5,
|
|
20381
|
+
readFileSync as readFileSync8,
|
|
20382
|
+
writeFileSync as writeFileSync9
|
|
20383
|
+
} from "node:fs";
|
|
20369
20384
|
import { join as join10, resolve as resolve7 } from "node:path";
|
|
20370
20385
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
20371
20386
|
|
|
@@ -20416,11 +20431,36 @@ var $visit = visit.visit;
|
|
|
20416
20431
|
var $visitAsync = visit.visitAsync;
|
|
20417
20432
|
|
|
20418
20433
|
// src/ads-sync.ts
|
|
20419
|
-
import { existsSync as existsSync9, readdirSync as
|
|
20434
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20420
20435
|
import { join as join9 } from "node:path";
|
|
20421
20436
|
var CREATIVE_EXT = /\.(png|jpe?g|webp)$/i;
|
|
20422
|
-
|
|
20423
|
-
const
|
|
20437
|
+
function campaignDirs(base) {
|
|
20438
|
+
const found = [];
|
|
20439
|
+
const accounts = join9(base, "accounts");
|
|
20440
|
+
if (existsSync9(accounts)) {
|
|
20441
|
+
for (const acct of readdirSync4(accounts, { withFileTypes: true })) {
|
|
20442
|
+
if (!acct.isDirectory())
|
|
20443
|
+
continue;
|
|
20444
|
+
const acctDir = join9(accounts, acct.name);
|
|
20445
|
+
for (const c of readdirSync4(acctDir, { withFileTypes: true })) {
|
|
20446
|
+
if (c.isDirectory() && existsSync9(join9(acctDir, c.name, "campaign.yaml"))) {
|
|
20447
|
+
found.push(`accounts/${acct.name}/${c.name}`);
|
|
20448
|
+
}
|
|
20449
|
+
}
|
|
20450
|
+
}
|
|
20451
|
+
}
|
|
20452
|
+
const legacy = join9(base, "campaigns");
|
|
20453
|
+
if (existsSync9(legacy)) {
|
|
20454
|
+
for (const e of readdirSync4(legacy, { withFileTypes: true })) {
|
|
20455
|
+
if (e.isDirectory() && existsSync9(join9(legacy, e.name, "campaign.yaml"))) {
|
|
20456
|
+
found.push(`campaigns/${e.name}`);
|
|
20457
|
+
}
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20460
|
+
return found;
|
|
20461
|
+
}
|
|
20462
|
+
async function collectFolder(base, rel) {
|
|
20463
|
+
const dir2 = join9(base, rel);
|
|
20424
20464
|
const doc = $parse(readFileSync7(join9(dir2, "campaign.yaml"), "utf8"));
|
|
20425
20465
|
const creativesDir = join9(dir2, "creatives");
|
|
20426
20466
|
const sources = [];
|
|
@@ -20428,7 +20468,7 @@ async function collectFolder(base, name) {
|
|
|
20428
20468
|
const abs = join9(creativesDir, sub);
|
|
20429
20469
|
if (!existsSync9(abs))
|
|
20430
20470
|
return;
|
|
20431
|
-
const entries =
|
|
20471
|
+
const entries = readdirSync4(abs, { withFileTypes: true });
|
|
20432
20472
|
const hasImages = entries.some((e) => e.isFile() && CREATIVE_EXT.test(e.name));
|
|
20433
20473
|
const index = hasImages ? await pushImagesIn(abs) : {};
|
|
20434
20474
|
for (const e of entries) {
|
|
@@ -20448,19 +20488,20 @@ async function collectFolder(base, name) {
|
|
|
20448
20488
|
}
|
|
20449
20489
|
};
|
|
20450
20490
|
await gather("", "");
|
|
20451
|
-
return { path:
|
|
20452
|
-
}
|
|
20453
|
-
function folderCampaigns(base) {
|
|
20454
|
-
const dir2 = join9(base, "campaigns");
|
|
20455
|
-
if (!existsSync9(dir2))
|
|
20456
|
-
return [];
|
|
20457
|
-
return readdirSync3(dir2, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync9(join9(dir2, e.name, "campaign.yaml"))).map((e) => e.name);
|
|
20491
|
+
return { path: `${rel}/campaign.yaml`, doc, sources };
|
|
20458
20492
|
}
|
|
20459
20493
|
async function adsCreativeGenerate(opts) {
|
|
20460
20494
|
const auth = requireActive();
|
|
20461
|
-
const
|
|
20495
|
+
const matches = campaignDirs(".").filter((rel) => rel.split("/").pop() === opts.campaign);
|
|
20496
|
+
if (matches.length === 0) {
|
|
20497
|
+
throw new Error(`No campaign folder "${opts.campaign}" — \`zalify ads new ${opts.campaign}\` first.`);
|
|
20498
|
+
}
|
|
20499
|
+
if (matches.length > 1) {
|
|
20500
|
+
throw new Error(`"${opts.campaign}" exists under several accounts: ${matches.join(", ")} — cd closer or rename.`);
|
|
20501
|
+
}
|
|
20502
|
+
const dir2 = join9(".", matches[0], "creatives");
|
|
20462
20503
|
if (!existsSync9(dir2)) {
|
|
20463
|
-
throw new Error(
|
|
20504
|
+
throw new Error(`${matches[0]}/creatives/ not found.`);
|
|
20464
20505
|
}
|
|
20465
20506
|
console.log("Generating (the canvas pipeline: safe zones + product fidelity)…");
|
|
20466
20507
|
const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/creative`, {
|
|
@@ -20509,34 +20550,39 @@ function root() {
|
|
|
20509
20550
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20510
20551
|
}
|
|
20511
20552
|
async function collect(base) {
|
|
20512
|
-
const dir2 = join10(base, "campaigns");
|
|
20513
|
-
if (!existsSync10(dir2))
|
|
20514
|
-
return { files: [] };
|
|
20515
20553
|
const files = [];
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20554
|
+
const legacy = join10(base, "campaigns");
|
|
20555
|
+
if (existsSync10(legacy)) {
|
|
20556
|
+
for (const entry of readdirSync5(legacy, { withFileTypes: true })) {
|
|
20557
|
+
if (entry.isDirectory())
|
|
20558
|
+
continue;
|
|
20559
|
+
if (!entry.name.endsWith(".yaml") || entry.name.endsWith(".copy.yaml")) {
|
|
20560
|
+
continue;
|
|
20561
|
+
}
|
|
20562
|
+
const path9 = `campaigns/${entry.name}`;
|
|
20563
|
+
try {
|
|
20564
|
+
files.push({ path: path9, doc: $parse(readFileSync8(join10(base, path9), "utf8")) });
|
|
20565
|
+
} catch (err) {
|
|
20566
|
+
throw new Error(`${path9} is not valid YAML: ${String(err).slice(0, 120)}`);
|
|
20567
|
+
}
|
|
20527
20568
|
}
|
|
20528
20569
|
}
|
|
20529
|
-
for (const
|
|
20530
|
-
files.push(await collectFolder(base,
|
|
20570
|
+
for (const rel of campaignDirs(base)) {
|
|
20571
|
+
files.push(await collectFolder(base, rel));
|
|
20531
20572
|
}
|
|
20532
20573
|
return { files };
|
|
20533
20574
|
}
|
|
20575
|
+
function ledgerPath(base) {
|
|
20576
|
+
return join10(base, "accounts", ".bindings.json");
|
|
20577
|
+
}
|
|
20534
20578
|
function ledger(base) {
|
|
20535
|
-
const path9
|
|
20536
|
-
|
|
20537
|
-
|
|
20538
|
-
|
|
20539
|
-
|
|
20579
|
+
for (const path9 of [ledgerPath(base), join10(base, "campaigns", ".bindings.json")]) {
|
|
20580
|
+
if (existsSync10(path9)) {
|
|
20581
|
+
const raw = JSON.parse(readFileSync8(path9, "utf8"));
|
|
20582
|
+
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20583
|
+
}
|
|
20584
|
+
}
|
|
20585
|
+
return { nodes: {}, pulled: {} };
|
|
20540
20586
|
}
|
|
20541
20587
|
async function post(auth, path9, payload) {
|
|
20542
20588
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
@@ -20644,7 +20690,8 @@ ${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : "
|
|
|
20644
20690
|
}
|
|
20645
20691
|
if (Object.keys(result.newBindings).length > 0) {
|
|
20646
20692
|
Object.assign(led.nodes, result.newBindings);
|
|
20647
|
-
|
|
20693
|
+
mkdirSync4(join10(base, "accounts"), { recursive: true });
|
|
20694
|
+
writeFileSync9(ledgerPath(base), `${JSON.stringify(led, null, 2)}
|
|
20648
20695
|
`);
|
|
20649
20696
|
console.log(`
|
|
20650
20697
|
${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
|
|
@@ -20657,7 +20704,7 @@ ${Object.keys(result.newBindings).length} new bindings recorded — everything c
|
|
|
20657
20704
|
import { createHash as createHash2 } from "node:crypto";
|
|
20658
20705
|
import {
|
|
20659
20706
|
existsSync as existsSync11,
|
|
20660
|
-
mkdirSync as
|
|
20707
|
+
mkdirSync as mkdirSync5,
|
|
20661
20708
|
readFileSync as readFileSync9,
|
|
20662
20709
|
writeFileSync as writeFileSync10
|
|
20663
20710
|
} from "node:fs";
|
|
@@ -20665,17 +20712,22 @@ import { dirname as dirname2, join as join11, resolve as resolve8 } from "node:p
|
|
|
20665
20712
|
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20666
20713
|
function checkoutRoot() {
|
|
20667
20714
|
const cwd = resolve8(".");
|
|
20668
|
-
if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "campaigns"))) {
|
|
20715
|
+
if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "accounts")) || existsSync11(join11(cwd, "campaigns"))) {
|
|
20669
20716
|
return cwd;
|
|
20670
20717
|
}
|
|
20671
20718
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20672
20719
|
}
|
|
20673
20720
|
function readBindings(root2) {
|
|
20674
|
-
const path9
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
|
|
20678
|
-
|
|
20721
|
+
for (const path9 of [
|
|
20722
|
+
join11(root2, "accounts", ".bindings.json"),
|
|
20723
|
+
join11(root2, "campaigns", ".bindings.json")
|
|
20724
|
+
]) {
|
|
20725
|
+
if (existsSync11(path9)) {
|
|
20726
|
+
const raw = JSON.parse(readFileSync9(path9, "utf8"));
|
|
20727
|
+
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20728
|
+
}
|
|
20729
|
+
}
|
|
20730
|
+
return { nodes: {}, pulled: {} };
|
|
20679
20731
|
}
|
|
20680
20732
|
async function adsPull(opts) {
|
|
20681
20733
|
const auth = requireActive();
|
|
@@ -20718,14 +20770,14 @@ async function adsPull(opts) {
|
|
|
20718
20770
|
continue;
|
|
20719
20771
|
}
|
|
20720
20772
|
}
|
|
20721
|
-
|
|
20773
|
+
mkdirSync5(dirname2(abs), { recursive: true });
|
|
20722
20774
|
writeFileSync10(abs, f.yaml);
|
|
20723
20775
|
bindings.pulled[f.path] = nextHash;
|
|
20724
20776
|
written += 1;
|
|
20725
20777
|
}
|
|
20726
20778
|
Object.assign(bindings.nodes, result.bindings);
|
|
20727
|
-
const bindingsPath = join11(root2, "
|
|
20728
|
-
|
|
20779
|
+
const bindingsPath = join11(root2, "accounts", ".bindings.json");
|
|
20780
|
+
mkdirSync5(dirname2(bindingsPath), { recursive: true });
|
|
20729
20781
|
writeFileSync10(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20730
20782
|
`);
|
|
20731
20783
|
const exported = new Set(result.files.map((f) => f.path));
|
|
@@ -20968,7 +21020,7 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20968
21020
|
}
|
|
20969
21021
|
|
|
20970
21022
|
// src/brand.ts
|
|
20971
|
-
import { existsSync as existsSync13, mkdirSync as
|
|
21023
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
20972
21024
|
import { basename as basename5, join as join13, resolve as resolve10 } from "node:path";
|
|
20973
21025
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20974
21026
|
|
|
@@ -21272,12 +21324,12 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
21272
21324
|
function brandInit(dir2 = ".", opts = {}) {
|
|
21273
21325
|
const target = resolve10(dir2);
|
|
21274
21326
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
21275
|
-
if (existsSync13(target) &&
|
|
21327
|
+
if (existsSync13(target) && readdirSync6(target).length > 0) {
|
|
21276
21328
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
21277
21329
|
}
|
|
21278
21330
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
21279
21331
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
21280
|
-
|
|
21332
|
+
mkdirSync6(join13(target, "images"), { recursive: true });
|
|
21281
21333
|
writeFileSync11(join13(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21282
21334
|
writeFileSync11(join13(target, "brand.md"), BRAND_MD(name));
|
|
21283
21335
|
writeFileSync11(join13(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
@@ -21567,7 +21619,7 @@ ads.command("list", { isDefault: true }).description("List the workspace's brand
|
|
|
21567
21619
|
ads.command("remove <slug>").description("Deprecate a brand instance (record soft-deleted; the GitHub repo is left untouched)").action(async (slug) => {
|
|
21568
21620
|
await adsRemove(slug);
|
|
21569
21621
|
});
|
|
21570
|
-
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) => {
|
|
21622
|
+
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").option("--account <folder>", "account folder under accounts/ (inferred when only one)").action(async (name, options) => {
|
|
21571
21623
|
await adsNew(name, options);
|
|
21572
21624
|
});
|
|
21573
21625
|
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) => {
|