@zalify/cli 0.20.0 → 0.22.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 +3 -0
- package/dist/cli.js +290 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,9 @@ its new home under `shop`.
|
|
|
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
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 |
|
|
106
|
+
| `ads diff` | Desired (your campaign files) vs actual (the ingested account), keyed through bindings: bound nodes compare budget/status, unbound nodes become creations. Read-only |
|
|
107
|
+
| `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 |
|
|
105
108
|
| `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 |
|
|
106
109
|
| `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 |
|
|
107
110
|
| `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 join13 } from "node:path";
|
|
18238
18238
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
18239
18239
|
|
|
18240
18240
|
// src/auth.ts
|
|
@@ -20338,10 +20338,10 @@ async function adsList() {
|
|
|
20338
20338
|
}
|
|
20339
20339
|
}
|
|
20340
20340
|
|
|
20341
|
-
// src/ads-
|
|
20342
|
-
import {
|
|
20343
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
20341
|
+
// src/ads-diff.ts
|
|
20342
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20344
20343
|
import { join as join9, resolve as resolve7 } from "node:path";
|
|
20344
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
20345
20345
|
|
|
20346
20346
|
// node_modules/yaml/dist/index.js
|
|
20347
20347
|
var composer = require_composer();
|
|
@@ -20389,7 +20389,256 @@ var $stringify = publicApi.stringify;
|
|
|
20389
20389
|
var $visit = visit.visit;
|
|
20390
20390
|
var $visitAsync = visit.visitAsync;
|
|
20391
20391
|
|
|
20392
|
+
// src/ads-diff.ts
|
|
20393
|
+
function root() {
|
|
20394
|
+
const cwd = resolve7(".");
|
|
20395
|
+
if (existsSync9(join9(cwd, ".zalify", "brand-repo.json")) || existsSync9(join9(cwd, "campaigns"))) {
|
|
20396
|
+
return cwd;
|
|
20397
|
+
}
|
|
20398
|
+
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20399
|
+
}
|
|
20400
|
+
function collect(base) {
|
|
20401
|
+
const dir2 = join9(base, "campaigns");
|
|
20402
|
+
if (!existsSync9(dir2))
|
|
20403
|
+
return { files: [], folders: 0 };
|
|
20404
|
+
const files = [];
|
|
20405
|
+
let folders = 0;
|
|
20406
|
+
for (const entry of readdirSync3(dir2, { withFileTypes: true })) {
|
|
20407
|
+
if (entry.isDirectory()) {
|
|
20408
|
+
if (existsSync9(join9(dir2, entry.name, "campaign.yaml")))
|
|
20409
|
+
folders += 1;
|
|
20410
|
+
continue;
|
|
20411
|
+
}
|
|
20412
|
+
if (!entry.name.endsWith(".yaml") || entry.name.endsWith(".copy.yaml")) {
|
|
20413
|
+
continue;
|
|
20414
|
+
}
|
|
20415
|
+
const path9 = `campaigns/${entry.name}`;
|
|
20416
|
+
try {
|
|
20417
|
+
files.push({ path: path9, doc: $parse(readFileSync7(join9(base, path9), "utf8")) });
|
|
20418
|
+
} catch (err) {
|
|
20419
|
+
throw new Error(`${path9} is not valid YAML: ${String(err).slice(0, 120)}`);
|
|
20420
|
+
}
|
|
20421
|
+
}
|
|
20422
|
+
return { files, folders };
|
|
20423
|
+
}
|
|
20424
|
+
function ledger(base) {
|
|
20425
|
+
const path9 = join9(base, "campaigns", ".bindings.json");
|
|
20426
|
+
if (!existsSync9(path9))
|
|
20427
|
+
return { nodes: {}, pulled: {} };
|
|
20428
|
+
const raw = JSON.parse(readFileSync7(path9, "utf8"));
|
|
20429
|
+
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20430
|
+
}
|
|
20431
|
+
async function post(auth, path9, payload) {
|
|
20432
|
+
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
20433
|
+
method: "POST",
|
|
20434
|
+
headers: {
|
|
20435
|
+
Authorization: `Bearer ${auth.key}`,
|
|
20436
|
+
"x-zalify-workspace-id": auth.workspaceId,
|
|
20437
|
+
"Content-Type": "application/json"
|
|
20438
|
+
},
|
|
20439
|
+
body: JSON.stringify(payload)
|
|
20440
|
+
});
|
|
20441
|
+
const json = await res.json().catch(() => ({}));
|
|
20442
|
+
if (!res.ok)
|
|
20443
|
+
throw new Error(`${path9}: ${res.status} ${json.error ?? ""}`.trim());
|
|
20444
|
+
return json;
|
|
20445
|
+
}
|
|
20446
|
+
function renderPlan(result, folders) {
|
|
20447
|
+
for (const f of result.issues) {
|
|
20448
|
+
console.log(`✗ ${f.file}`);
|
|
20449
|
+
for (const i of f.issues)
|
|
20450
|
+
console.log(` ${i.path} — ${i.message}`);
|
|
20451
|
+
}
|
|
20452
|
+
for (const e of result.plan) {
|
|
20453
|
+
if (e.action === "create_campaign") {
|
|
20454
|
+
console.log(`+ campaign "${e.name}" (${e.adSets} ad sets, ${e.ads} ads) ${e.file}`);
|
|
20455
|
+
} else if (e.action === "create_ad_set") {
|
|
20456
|
+
console.log(`+ ad set "${e.name}" (${e.ads} ads) ${e.file}`);
|
|
20457
|
+
} else if (e.action === "create_ad") {
|
|
20458
|
+
console.log(`+ ad "${e.name}" ${e.file}`);
|
|
20459
|
+
} else if (e.action === "set_budget") {
|
|
20460
|
+
console.log(`~ ${e.kind} "${e.name}": budget ${e.from ?? "—"} → ${e.to}`);
|
|
20461
|
+
} else if (e.action === "set_status") {
|
|
20462
|
+
console.log(`~ ${e.kind} "${e.name}": status ${e.from ?? "—"} → ${e.to}`);
|
|
20463
|
+
}
|
|
20464
|
+
}
|
|
20465
|
+
const s = result.summary;
|
|
20466
|
+
console.log(`
|
|
20467
|
+
${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}` + `${folders ? ` (${folders} folder campaigns skipped — sync is coming)` : ""}`);
|
|
20468
|
+
}
|
|
20469
|
+
async function adsDiff() {
|
|
20470
|
+
const auth = requireActive();
|
|
20471
|
+
const base = root();
|
|
20472
|
+
const { files, folders } = collect(base);
|
|
20473
|
+
if (files.length === 0 && folders === 0) {
|
|
20474
|
+
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20475
|
+
return;
|
|
20476
|
+
}
|
|
20477
|
+
const led = ledger(base);
|
|
20478
|
+
const result = await post(auth, "ads-code/diff", {
|
|
20479
|
+
files,
|
|
20480
|
+
bindings: led.nodes
|
|
20481
|
+
});
|
|
20482
|
+
renderPlan(result, folders);
|
|
20483
|
+
if (result.issues.length > 0)
|
|
20484
|
+
process.exitCode = 1;
|
|
20485
|
+
}
|
|
20486
|
+
async function adsApply(opts) {
|
|
20487
|
+
const auth = requireActive();
|
|
20488
|
+
const base = root();
|
|
20489
|
+
const { files, folders } = collect(base);
|
|
20490
|
+
if (files.length === 0) {
|
|
20491
|
+
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20492
|
+
return;
|
|
20493
|
+
}
|
|
20494
|
+
const led = ledger(base);
|
|
20495
|
+
const diff = await post(auth, "ads-code/diff", {
|
|
20496
|
+
files,
|
|
20497
|
+
bindings: led.nodes
|
|
20498
|
+
});
|
|
20499
|
+
renderPlan(diff, folders);
|
|
20500
|
+
if (diff.issues.length > 0) {
|
|
20501
|
+
throw new Error("Fix the invalid files before applying.");
|
|
20502
|
+
}
|
|
20503
|
+
if (diff.plan.length === 0) {
|
|
20504
|
+
console.log("Nothing to apply.");
|
|
20505
|
+
return;
|
|
20506
|
+
}
|
|
20507
|
+
if (!opts.yes) {
|
|
20508
|
+
if (!process.stdin.isTTY) {
|
|
20509
|
+
throw new Error("Not a terminal — pass -y to apply without the prompt.");
|
|
20510
|
+
}
|
|
20511
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
20512
|
+
const answer = (await rl.question(`
|
|
20513
|
+
Apply? [y/N]: `)).trim().toLowerCase();
|
|
20514
|
+
rl.close();
|
|
20515
|
+
if (answer !== "y" && answer !== "yes") {
|
|
20516
|
+
console.log("Nothing applied.");
|
|
20517
|
+
return;
|
|
20518
|
+
}
|
|
20519
|
+
}
|
|
20520
|
+
const result = await post(auth, "ads-code/apply", {
|
|
20521
|
+
files,
|
|
20522
|
+
bindings: led.nodes,
|
|
20523
|
+
activate: Boolean(opts.activate)
|
|
20524
|
+
});
|
|
20525
|
+
for (const r of result.receipts) {
|
|
20526
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.action.replaceAll("_", " ")} ${r.name}` + `${r.id ? ` (${r.id})` : ""}${r.error ? ` — ${r.error}` : ""}`);
|
|
20527
|
+
}
|
|
20528
|
+
if (result.refusedActivations > 0) {
|
|
20529
|
+
console.log(`
|
|
20530
|
+
${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : ""} refused — turning spend on needs --activate.`);
|
|
20531
|
+
}
|
|
20532
|
+
if (Object.keys(result.newBindings).length > 0) {
|
|
20533
|
+
Object.assign(led.nodes, result.newBindings);
|
|
20534
|
+
writeFileSync8(join9(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
|
|
20535
|
+
`);
|
|
20536
|
+
console.log(`
|
|
20537
|
+
${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
|
|
20538
|
+
}
|
|
20539
|
+
if (result.receipts.some((r) => !r.ok))
|
|
20540
|
+
process.exitCode = 1;
|
|
20541
|
+
}
|
|
20542
|
+
|
|
20543
|
+
// src/ads-pull.ts
|
|
20544
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
20545
|
+
import {
|
|
20546
|
+
existsSync as existsSync10,
|
|
20547
|
+
mkdirSync as mkdirSync4,
|
|
20548
|
+
readFileSync as readFileSync8,
|
|
20549
|
+
writeFileSync as writeFileSync9
|
|
20550
|
+
} from "node:fs";
|
|
20551
|
+
import { dirname as dirname2, join as join10, resolve as resolve8 } from "node:path";
|
|
20552
|
+
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20553
|
+
function checkoutRoot() {
|
|
20554
|
+
const cwd = resolve8(".");
|
|
20555
|
+
if (existsSync10(join10(cwd, ".zalify", "brand-repo.json")) || existsSync10(join10(cwd, "campaigns"))) {
|
|
20556
|
+
return cwd;
|
|
20557
|
+
}
|
|
20558
|
+
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20559
|
+
}
|
|
20560
|
+
function readBindings(root2) {
|
|
20561
|
+
const path9 = join10(root2, "campaigns", ".bindings.json");
|
|
20562
|
+
if (!existsSync10(path9))
|
|
20563
|
+
return { nodes: {}, pulled: {} };
|
|
20564
|
+
const raw = JSON.parse(readFileSync8(path9, "utf8"));
|
|
20565
|
+
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20566
|
+
}
|
|
20567
|
+
async function adsPull(opts) {
|
|
20568
|
+
const auth = requireActive();
|
|
20569
|
+
const root2 = checkoutRoot();
|
|
20570
|
+
const params = new URLSearchParams;
|
|
20571
|
+
if (opts.account)
|
|
20572
|
+
params.set("account", opts.account);
|
|
20573
|
+
if (opts.days)
|
|
20574
|
+
params.set("days", opts.days);
|
|
20575
|
+
const q = params.toString();
|
|
20576
|
+
const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/export${q ? `?${q}` : ""}`, {
|
|
20577
|
+
headers: {
|
|
20578
|
+
Authorization: `Bearer ${auth.key}`,
|
|
20579
|
+
"x-zalify-workspace-id": auth.workspaceId
|
|
20580
|
+
}
|
|
20581
|
+
});
|
|
20582
|
+
if (!res.ok) {
|
|
20583
|
+
const body = await res.json().catch(() => ({}));
|
|
20584
|
+
throw new Error(`export failed (${res.status}): ${body.error ?? ""}`);
|
|
20585
|
+
}
|
|
20586
|
+
const result = await res.json();
|
|
20587
|
+
const bindings = readBindings(root2);
|
|
20588
|
+
let written = 0;
|
|
20589
|
+
let unchanged = 0;
|
|
20590
|
+
const skipped = [];
|
|
20591
|
+
for (const f of result.files) {
|
|
20592
|
+
const abs = join10(root2, f.path);
|
|
20593
|
+
const nextHash = sha(f.yaml);
|
|
20594
|
+
const lastPulled = bindings.pulled[f.path];
|
|
20595
|
+
if (existsSync10(abs)) {
|
|
20596
|
+
const current = readFileSync8(abs, "utf8");
|
|
20597
|
+
if (sha(current) === nextHash) {
|
|
20598
|
+
unchanged += 1;
|
|
20599
|
+
bindings.pulled[f.path] = nextHash;
|
|
20600
|
+
continue;
|
|
20601
|
+
}
|
|
20602
|
+
const locallyEdited = lastPulled !== undefined && sha(current) !== lastPulled;
|
|
20603
|
+
if (locallyEdited && !opts.force) {
|
|
20604
|
+
skipped.push(f.path);
|
|
20605
|
+
continue;
|
|
20606
|
+
}
|
|
20607
|
+
}
|
|
20608
|
+
mkdirSync4(dirname2(abs), { recursive: true });
|
|
20609
|
+
writeFileSync9(abs, f.yaml);
|
|
20610
|
+
bindings.pulled[f.path] = nextHash;
|
|
20611
|
+
written += 1;
|
|
20612
|
+
}
|
|
20613
|
+
Object.assign(bindings.nodes, result.bindings);
|
|
20614
|
+
const bindingsPath = join10(root2, "campaigns", ".bindings.json");
|
|
20615
|
+
mkdirSync4(dirname2(bindingsPath), { recursive: true });
|
|
20616
|
+
writeFileSync9(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20617
|
+
`);
|
|
20618
|
+
const exported = new Set(result.files.map((f) => f.path));
|
|
20619
|
+
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) && existsSync10(join10(root2, p)));
|
|
20620
|
+
const s = result.summary;
|
|
20621
|
+
console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
|
|
20622
|
+
for (const p of skipped)
|
|
20623
|
+
console.log(` kept ${p} (edited — --force overwrites)`);
|
|
20624
|
+
if (gone.length > 0) {
|
|
20625
|
+
console.log(`${gone.length} file${gone.length > 1 ? "s" : ""} no longer upstream (left in place):`);
|
|
20626
|
+
for (const p of gone.slice(0, 10))
|
|
20627
|
+
console.log(` gone ${p}`);
|
|
20628
|
+
if (gone.length > 10)
|
|
20629
|
+
console.log(` … and ${gone.length - 10} more`);
|
|
20630
|
+
}
|
|
20631
|
+
if (s.invalid.length > 0) {
|
|
20632
|
+
console.log(`${s.invalid.length} campaigns failed export-side validation — report this; they were not written.`);
|
|
20633
|
+
}
|
|
20634
|
+
console.log(`
|
|
20635
|
+
Review with git diff; commit when it reads right.`);
|
|
20636
|
+
}
|
|
20637
|
+
|
|
20392
20638
|
// src/ads-read.ts
|
|
20639
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
20640
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
20641
|
+
import { join as join11, resolve as resolve9 } from "node:path";
|
|
20393
20642
|
async function serviceGet(auth, path9) {
|
|
20394
20643
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
20395
20644
|
headers: {
|
|
@@ -20477,12 +20726,12 @@ account changes: not tracked for this platform yet` : `
|
|
|
20477
20726
|
account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
|
|
20478
20727
|
}
|
|
20479
20728
|
function adsDigest(dir2) {
|
|
20480
|
-
const
|
|
20481
|
-
const path9 =
|
|
20482
|
-
if (!
|
|
20729
|
+
const root2 = resolve9(dir2 ?? ".");
|
|
20730
|
+
const path9 = join11(root2, "signals", "latest.json");
|
|
20731
|
+
if (!existsSync11(path9)) {
|
|
20483
20732
|
throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
|
|
20484
20733
|
}
|
|
20485
|
-
const d = JSON.parse(
|
|
20734
|
+
const d = JSON.parse(readFileSync9(path9, "utf8"));
|
|
20486
20735
|
console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
|
|
20487
20736
|
const t = d.totals;
|
|
20488
20737
|
console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
|
|
@@ -20521,26 +20770,26 @@ function adsOpen() {
|
|
|
20521
20770
|
}
|
|
20522
20771
|
async function adsDoctor(dir2) {
|
|
20523
20772
|
const auth = requireActive();
|
|
20524
|
-
const
|
|
20773
|
+
const root2 = resolve9(dir2 ?? ".");
|
|
20525
20774
|
let failed = 0;
|
|
20526
20775
|
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
20527
20776
|
const bad = (msg) => {
|
|
20528
20777
|
console.log(` ✗ ${msg}`);
|
|
20529
20778
|
failed += 1;
|
|
20530
20779
|
};
|
|
20531
|
-
const linkPath =
|
|
20780
|
+
const linkPath = join11(root2, ".zalify", "brand-repo.json");
|
|
20532
20781
|
let link = null;
|
|
20533
|
-
if (
|
|
20534
|
-
link = JSON.parse(
|
|
20782
|
+
if (existsSync11(linkPath)) {
|
|
20783
|
+
link = JSON.parse(readFileSync9(linkPath, "utf8"));
|
|
20535
20784
|
ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
|
|
20536
20785
|
} else {
|
|
20537
20786
|
console.log(" - not inside a brand-instance checkout (repo checks only)");
|
|
20538
20787
|
}
|
|
20539
20788
|
let identity2 = null;
|
|
20540
|
-
const identityPath =
|
|
20541
|
-
if (
|
|
20789
|
+
const identityPath = join11(root2, "identity.yaml");
|
|
20790
|
+
if (existsSync11(identityPath)) {
|
|
20542
20791
|
try {
|
|
20543
|
-
identity2 = $parse(
|
|
20792
|
+
identity2 = $parse(readFileSync9(identityPath, "utf8"));
|
|
20544
20793
|
ok("identity.yaml parses");
|
|
20545
20794
|
if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
|
|
20546
20795
|
bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
|
|
@@ -20606,8 +20855,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20606
20855
|
}
|
|
20607
20856
|
|
|
20608
20857
|
// src/brand.ts
|
|
20609
|
-
import { existsSync as
|
|
20610
|
-
import { basename as basename5, join as
|
|
20858
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
|
|
20859
|
+
import { basename as basename5, join as join12, resolve as resolve10 } from "node:path";
|
|
20611
20860
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20612
20861
|
|
|
20613
20862
|
The rules for filling in this folder. An author — human or AI agent —
|
|
@@ -20908,19 +21157,19 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
20908
21157
|
}, null, 2) + `
|
|
20909
21158
|
`;
|
|
20910
21159
|
function brandInit(dir2 = ".", opts = {}) {
|
|
20911
|
-
const target =
|
|
21160
|
+
const target = resolve10(dir2);
|
|
20912
21161
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
20913
|
-
if (
|
|
21162
|
+
if (existsSync12(target) && readdirSync4(target).length > 0) {
|
|
20914
21163
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
20915
21164
|
}
|
|
20916
21165
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
20917
21166
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
20918
|
-
|
|
20919
|
-
|
|
20920
|
-
|
|
20921
|
-
|
|
20922
|
-
|
|
20923
|
-
|
|
21167
|
+
mkdirSync5(join12(target, "images"), { recursive: true });
|
|
21168
|
+
writeFileSync10(join12(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21169
|
+
writeFileSync10(join12(target, "brand.md"), BRAND_MD(name));
|
|
21170
|
+
writeFileSync10(join12(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
21171
|
+
writeFileSync10(join12(target, "catalog.json"), CATALOG_JSON(name, currency));
|
|
21172
|
+
writeFileSync10(join12(target, "images", "manifest.json"), MANIFEST_JSON);
|
|
20924
21173
|
console.log(`Scaffolded brand data in ${dir2}:`);
|
|
20925
21174
|
for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
|
|
20926
21175
|
console.log(` + ${f}`);
|
|
@@ -20931,23 +21180,23 @@ Author in this order: brand.md → catalog.json → images/manifest.json` + `
|
|
|
20931
21180
|
}
|
|
20932
21181
|
var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
|
|
20933
21182
|
function brandValidate(dir2 = ".") {
|
|
20934
|
-
const target =
|
|
21183
|
+
const target = resolve10(dir2);
|
|
20935
21184
|
const problems = [];
|
|
20936
21185
|
const warnings = [];
|
|
20937
21186
|
const readJson2 = (rel) => {
|
|
20938
|
-
const path9 =
|
|
20939
|
-
if (!
|
|
21187
|
+
const path9 = join12(target, rel);
|
|
21188
|
+
if (!existsSync12(path9)) {
|
|
20940
21189
|
problems.push(`missing ${rel}`);
|
|
20941
21190
|
return null;
|
|
20942
21191
|
}
|
|
20943
21192
|
try {
|
|
20944
|
-
return JSON.parse(
|
|
21193
|
+
return JSON.parse(readFileSync10(path9, "utf8"));
|
|
20945
21194
|
} catch (err) {
|
|
20946
21195
|
problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
|
|
20947
21196
|
return null;
|
|
20948
21197
|
}
|
|
20949
21198
|
};
|
|
20950
|
-
if (!
|
|
21199
|
+
if (!existsSync12(join12(target, "brand.md")))
|
|
20951
21200
|
problems.push("missing brand.md");
|
|
20952
21201
|
const store = readJson2("store.json");
|
|
20953
21202
|
if (store && (!store.name || !store.slug))
|
|
@@ -21121,9 +21370,9 @@ function brandValidate(dir2 = ".") {
|
|
|
21121
21370
|
}
|
|
21122
21371
|
|
|
21123
21372
|
// src/cli.ts
|
|
21124
|
-
var __dirname4 =
|
|
21373
|
+
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
21125
21374
|
var require2 = createRequire2(import.meta.url);
|
|
21126
|
-
var pkg = require2(
|
|
21375
|
+
var pkg = require2(join13(__dirname4, "..", "package.json"));
|
|
21127
21376
|
try {
|
|
21128
21377
|
updateNotifier({ pkg }).notify({
|
|
21129
21378
|
isGlobal: true,
|
|
@@ -21208,6 +21457,15 @@ ads.command("remove <slug>").description("Deprecate a brand instance (record sof
|
|
|
21208
21457
|
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) => {
|
|
21209
21458
|
await adsNew(name, options);
|
|
21210
21459
|
});
|
|
21460
|
+
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) => {
|
|
21461
|
+
await adsPull(options);
|
|
21462
|
+
});
|
|
21463
|
+
ads.command("diff").description("Desired (your files) vs actual (the ingested account) — what apply would do").action(async () => {
|
|
21464
|
+
await adsDiff();
|
|
21465
|
+
});
|
|
21466
|
+
ads.command("apply").description("Execute the diff: creations land PAUSED; budget edits apply; activation needs --activate").option("--activate", "allow paused → active status changes (turning spend ON)").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
|
|
21467
|
+
await adsApply(options);
|
|
21468
|
+
});
|
|
21211
21469
|
ads.command("status").description("The brand instance at a glance: access, ingestion freshness, plans, pending proposals").action(async () => {
|
|
21212
21470
|
await adsStatus();
|
|
21213
21471
|
});
|