@zalify/cli 0.21.0 → 0.22.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 +2 -0
- package/dist/cli.js +260 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ its new home under `shop`.
|
|
|
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
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 |
|
|
106
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 |
|
|
107
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 |
|
|
108
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 dirname3, join 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,33 +20338,238 @@ async function adsList() {
|
|
|
20338
20338
|
}
|
|
20339
20339
|
}
|
|
20340
20340
|
|
|
20341
|
+
// src/ads-diff.ts
|
|
20342
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20343
|
+
import { join as join9, resolve as resolve7 } from "node:path";
|
|
20344
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
20345
|
+
|
|
20346
|
+
// node_modules/yaml/dist/index.js
|
|
20347
|
+
var composer = require_composer();
|
|
20348
|
+
var Document = require_Document();
|
|
20349
|
+
var Schema = require_Schema();
|
|
20350
|
+
var errors = require_errors();
|
|
20351
|
+
var Alias = require_Alias();
|
|
20352
|
+
var identity = require_identity();
|
|
20353
|
+
var Pair = require_Pair();
|
|
20354
|
+
var Scalar = require_Scalar();
|
|
20355
|
+
var YAMLMap = require_YAMLMap();
|
|
20356
|
+
var YAMLSeq = require_YAMLSeq();
|
|
20357
|
+
var cst = require_cst();
|
|
20358
|
+
var lexer = require_lexer();
|
|
20359
|
+
var lineCounter = require_line_counter();
|
|
20360
|
+
var parser = require_parser();
|
|
20361
|
+
var publicApi = require_public_api();
|
|
20362
|
+
var visit = require_visit();
|
|
20363
|
+
var $Composer = composer.Composer;
|
|
20364
|
+
var $Document = Document.Document;
|
|
20365
|
+
var $Schema = Schema.Schema;
|
|
20366
|
+
var $YAMLError = errors.YAMLError;
|
|
20367
|
+
var $YAMLParseError = errors.YAMLParseError;
|
|
20368
|
+
var $YAMLWarning = errors.YAMLWarning;
|
|
20369
|
+
var $Alias = Alias.Alias;
|
|
20370
|
+
var $isAlias = identity.isAlias;
|
|
20371
|
+
var $isCollection = identity.isCollection;
|
|
20372
|
+
var $isDocument = identity.isDocument;
|
|
20373
|
+
var $isMap = identity.isMap;
|
|
20374
|
+
var $isNode = identity.isNode;
|
|
20375
|
+
var $isPair = identity.isPair;
|
|
20376
|
+
var $isScalar = identity.isScalar;
|
|
20377
|
+
var $isSeq = identity.isSeq;
|
|
20378
|
+
var $Pair = Pair.Pair;
|
|
20379
|
+
var $Scalar = Scalar.Scalar;
|
|
20380
|
+
var $YAMLMap = YAMLMap.YAMLMap;
|
|
20381
|
+
var $YAMLSeq = YAMLSeq.YAMLSeq;
|
|
20382
|
+
var $Lexer = lexer.Lexer;
|
|
20383
|
+
var $LineCounter = lineCounter.LineCounter;
|
|
20384
|
+
var $Parser = parser.Parser;
|
|
20385
|
+
var $parse = publicApi.parse;
|
|
20386
|
+
var $parseAllDocuments = publicApi.parseAllDocuments;
|
|
20387
|
+
var $parseDocument = publicApi.parseDocument;
|
|
20388
|
+
var $stringify = publicApi.stringify;
|
|
20389
|
+
var $visit = visit.visit;
|
|
20390
|
+
var $visitAsync = visit.visitAsync;
|
|
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 n of result.notes ?? []) {
|
|
20448
|
+
console.log(`· ${n.file} — ${n.message}`);
|
|
20449
|
+
}
|
|
20450
|
+
for (const f of result.issues) {
|
|
20451
|
+
console.log(`✗ ${f.file}`);
|
|
20452
|
+
for (const i of f.issues)
|
|
20453
|
+
console.log(` ${i.path} — ${i.message}`);
|
|
20454
|
+
}
|
|
20455
|
+
for (const e of result.plan) {
|
|
20456
|
+
if (e.action === "create_campaign") {
|
|
20457
|
+
console.log(`+ campaign "${e.name}" (${e.adSets} ad sets, ${e.ads} ads) ${e.file}`);
|
|
20458
|
+
} else if (e.action === "create_ad_set") {
|
|
20459
|
+
console.log(`+ ad set "${e.name}" (${e.ads} ads) ${e.file}`);
|
|
20460
|
+
} else if (e.action === "create_ad") {
|
|
20461
|
+
console.log(`+ ad "${e.name}" ${e.file}`);
|
|
20462
|
+
} else if (e.action === "set_budget") {
|
|
20463
|
+
console.log(`~ ${e.kind} "${e.name}": budget ${e.from ?? "—"} → ${e.to}`);
|
|
20464
|
+
} else if (e.action === "set_status") {
|
|
20465
|
+
console.log(`~ ${e.kind} "${e.name}": status ${e.from ?? "—"} → ${e.to}`);
|
|
20466
|
+
}
|
|
20467
|
+
}
|
|
20468
|
+
const s = result.summary;
|
|
20469
|
+
console.log(`
|
|
20470
|
+
${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)` : ""}`);
|
|
20471
|
+
}
|
|
20472
|
+
async function adsDiff() {
|
|
20473
|
+
const auth = requireActive();
|
|
20474
|
+
const base = root();
|
|
20475
|
+
const { files, folders } = collect(base);
|
|
20476
|
+
if (files.length === 0 && folders === 0) {
|
|
20477
|
+
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20478
|
+
return;
|
|
20479
|
+
}
|
|
20480
|
+
const led = ledger(base);
|
|
20481
|
+
const result = await post(auth, "ads-code/diff", {
|
|
20482
|
+
files,
|
|
20483
|
+
bindings: led.nodes
|
|
20484
|
+
});
|
|
20485
|
+
renderPlan(result, folders);
|
|
20486
|
+
if (result.issues.length > 0)
|
|
20487
|
+
process.exitCode = 1;
|
|
20488
|
+
}
|
|
20489
|
+
async function adsApply(opts) {
|
|
20490
|
+
const auth = requireActive();
|
|
20491
|
+
const base = root();
|
|
20492
|
+
const { files, folders } = collect(base);
|
|
20493
|
+
if (files.length === 0) {
|
|
20494
|
+
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20495
|
+
return;
|
|
20496
|
+
}
|
|
20497
|
+
const led = ledger(base);
|
|
20498
|
+
const diff = await post(auth, "ads-code/diff", {
|
|
20499
|
+
files,
|
|
20500
|
+
bindings: led.nodes
|
|
20501
|
+
});
|
|
20502
|
+
renderPlan(diff, folders);
|
|
20503
|
+
if (diff.issues.length > 0) {
|
|
20504
|
+
throw new Error("Fix the invalid files before applying.");
|
|
20505
|
+
}
|
|
20506
|
+
if (diff.plan.length === 0) {
|
|
20507
|
+
console.log("Nothing to apply.");
|
|
20508
|
+
return;
|
|
20509
|
+
}
|
|
20510
|
+
if (!opts.yes) {
|
|
20511
|
+
if (!process.stdin.isTTY) {
|
|
20512
|
+
throw new Error("Not a terminal — pass -y to apply without the prompt.");
|
|
20513
|
+
}
|
|
20514
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
20515
|
+
const answer = (await rl.question(`
|
|
20516
|
+
Apply? [y/N]: `)).trim().toLowerCase();
|
|
20517
|
+
rl.close();
|
|
20518
|
+
if (answer !== "y" && answer !== "yes") {
|
|
20519
|
+
console.log("Nothing applied.");
|
|
20520
|
+
return;
|
|
20521
|
+
}
|
|
20522
|
+
}
|
|
20523
|
+
const result = await post(auth, "ads-code/apply", {
|
|
20524
|
+
files,
|
|
20525
|
+
bindings: led.nodes,
|
|
20526
|
+
activate: Boolean(opts.activate)
|
|
20527
|
+
});
|
|
20528
|
+
for (const r of result.receipts) {
|
|
20529
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.action.replaceAll("_", " ")} ${r.name}` + `${r.id ? ` (${r.id})` : ""}${r.error ? ` — ${r.error}` : ""}`);
|
|
20530
|
+
}
|
|
20531
|
+
if (result.refusedActivations > 0) {
|
|
20532
|
+
console.log(`
|
|
20533
|
+
${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : ""} refused — turning spend on needs --activate.`);
|
|
20534
|
+
}
|
|
20535
|
+
if (Object.keys(result.newBindings).length > 0) {
|
|
20536
|
+
Object.assign(led.nodes, result.newBindings);
|
|
20537
|
+
writeFileSync8(join9(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
|
|
20538
|
+
`);
|
|
20539
|
+
console.log(`
|
|
20540
|
+
${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
|
|
20541
|
+
}
|
|
20542
|
+
if (result.receipts.some((r) => !r.ok))
|
|
20543
|
+
process.exitCode = 1;
|
|
20544
|
+
}
|
|
20545
|
+
|
|
20341
20546
|
// src/ads-pull.ts
|
|
20342
20547
|
import { createHash as createHash2 } from "node:crypto";
|
|
20343
20548
|
import {
|
|
20344
|
-
existsSync as
|
|
20549
|
+
existsSync as existsSync10,
|
|
20345
20550
|
mkdirSync as mkdirSync4,
|
|
20346
|
-
readFileSync as
|
|
20347
|
-
writeFileSync as
|
|
20551
|
+
readFileSync as readFileSync8,
|
|
20552
|
+
writeFileSync as writeFileSync9
|
|
20348
20553
|
} from "node:fs";
|
|
20349
|
-
import { dirname as dirname2, join as
|
|
20554
|
+
import { dirname as dirname2, join as join10, resolve as resolve8 } from "node:path";
|
|
20350
20555
|
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20351
20556
|
function checkoutRoot() {
|
|
20352
|
-
const cwd =
|
|
20353
|
-
if (
|
|
20557
|
+
const cwd = resolve8(".");
|
|
20558
|
+
if (existsSync10(join10(cwd, ".zalify", "brand-repo.json")) || existsSync10(join10(cwd, "campaigns"))) {
|
|
20354
20559
|
return cwd;
|
|
20355
20560
|
}
|
|
20356
20561
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20357
20562
|
}
|
|
20358
|
-
function readBindings(
|
|
20359
|
-
const path9 =
|
|
20360
|
-
if (!
|
|
20563
|
+
function readBindings(root2) {
|
|
20564
|
+
const path9 = join10(root2, "campaigns", ".bindings.json");
|
|
20565
|
+
if (!existsSync10(path9))
|
|
20361
20566
|
return { nodes: {}, pulled: {} };
|
|
20362
|
-
const raw = JSON.parse(
|
|
20567
|
+
const raw = JSON.parse(readFileSync8(path9, "utf8"));
|
|
20363
20568
|
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20364
20569
|
}
|
|
20365
20570
|
async function adsPull(opts) {
|
|
20366
20571
|
const auth = requireActive();
|
|
20367
|
-
const
|
|
20572
|
+
const root2 = checkoutRoot();
|
|
20368
20573
|
const params = new URLSearchParams;
|
|
20369
20574
|
if (opts.account)
|
|
20370
20575
|
params.set("account", opts.account);
|
|
@@ -20382,16 +20587,16 @@ async function adsPull(opts) {
|
|
|
20382
20587
|
throw new Error(`export failed (${res.status}): ${body.error ?? ""}`);
|
|
20383
20588
|
}
|
|
20384
20589
|
const result = await res.json();
|
|
20385
|
-
const bindings = readBindings(
|
|
20590
|
+
const bindings = readBindings(root2);
|
|
20386
20591
|
let written = 0;
|
|
20387
20592
|
let unchanged = 0;
|
|
20388
20593
|
const skipped = [];
|
|
20389
20594
|
for (const f of result.files) {
|
|
20390
|
-
const abs =
|
|
20595
|
+
const abs = join10(root2, f.path);
|
|
20391
20596
|
const nextHash = sha(f.yaml);
|
|
20392
20597
|
const lastPulled = bindings.pulled[f.path];
|
|
20393
|
-
if (
|
|
20394
|
-
const current =
|
|
20598
|
+
if (existsSync10(abs)) {
|
|
20599
|
+
const current = readFileSync8(abs, "utf8");
|
|
20395
20600
|
if (sha(current) === nextHash) {
|
|
20396
20601
|
unchanged += 1;
|
|
20397
20602
|
bindings.pulled[f.path] = nextHash;
|
|
@@ -20404,17 +20609,17 @@ async function adsPull(opts) {
|
|
|
20404
20609
|
}
|
|
20405
20610
|
}
|
|
20406
20611
|
mkdirSync4(dirname2(abs), { recursive: true });
|
|
20407
|
-
|
|
20612
|
+
writeFileSync9(abs, f.yaml);
|
|
20408
20613
|
bindings.pulled[f.path] = nextHash;
|
|
20409
20614
|
written += 1;
|
|
20410
20615
|
}
|
|
20411
20616
|
Object.assign(bindings.nodes, result.bindings);
|
|
20412
|
-
const bindingsPath =
|
|
20617
|
+
const bindingsPath = join10(root2, "campaigns", ".bindings.json");
|
|
20413
20618
|
mkdirSync4(dirname2(bindingsPath), { recursive: true });
|
|
20414
|
-
|
|
20619
|
+
writeFileSync9(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20415
20620
|
`);
|
|
20416
20621
|
const exported = new Set(result.files.map((f) => f.path));
|
|
20417
|
-
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) &&
|
|
20622
|
+
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) && existsSync10(join10(root2, p)));
|
|
20418
20623
|
const s = result.summary;
|
|
20419
20624
|
console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
|
|
20420
20625
|
for (const p of skipped)
|
|
@@ -20435,56 +20640,8 @@ Review with git diff; commit when it reads right.`);
|
|
|
20435
20640
|
|
|
20436
20641
|
// src/ads-read.ts
|
|
20437
20642
|
import { spawn as spawn4 } from "node:child_process";
|
|
20438
|
-
import { existsSync as
|
|
20439
|
-
import { join as
|
|
20440
|
-
|
|
20441
|
-
// node_modules/yaml/dist/index.js
|
|
20442
|
-
var composer = require_composer();
|
|
20443
|
-
var Document = require_Document();
|
|
20444
|
-
var Schema = require_Schema();
|
|
20445
|
-
var errors = require_errors();
|
|
20446
|
-
var Alias = require_Alias();
|
|
20447
|
-
var identity = require_identity();
|
|
20448
|
-
var Pair = require_Pair();
|
|
20449
|
-
var Scalar = require_Scalar();
|
|
20450
|
-
var YAMLMap = require_YAMLMap();
|
|
20451
|
-
var YAMLSeq = require_YAMLSeq();
|
|
20452
|
-
var cst = require_cst();
|
|
20453
|
-
var lexer = require_lexer();
|
|
20454
|
-
var lineCounter = require_line_counter();
|
|
20455
|
-
var parser = require_parser();
|
|
20456
|
-
var publicApi = require_public_api();
|
|
20457
|
-
var visit = require_visit();
|
|
20458
|
-
var $Composer = composer.Composer;
|
|
20459
|
-
var $Document = Document.Document;
|
|
20460
|
-
var $Schema = Schema.Schema;
|
|
20461
|
-
var $YAMLError = errors.YAMLError;
|
|
20462
|
-
var $YAMLParseError = errors.YAMLParseError;
|
|
20463
|
-
var $YAMLWarning = errors.YAMLWarning;
|
|
20464
|
-
var $Alias = Alias.Alias;
|
|
20465
|
-
var $isAlias = identity.isAlias;
|
|
20466
|
-
var $isCollection = identity.isCollection;
|
|
20467
|
-
var $isDocument = identity.isDocument;
|
|
20468
|
-
var $isMap = identity.isMap;
|
|
20469
|
-
var $isNode = identity.isNode;
|
|
20470
|
-
var $isPair = identity.isPair;
|
|
20471
|
-
var $isScalar = identity.isScalar;
|
|
20472
|
-
var $isSeq = identity.isSeq;
|
|
20473
|
-
var $Pair = Pair.Pair;
|
|
20474
|
-
var $Scalar = Scalar.Scalar;
|
|
20475
|
-
var $YAMLMap = YAMLMap.YAMLMap;
|
|
20476
|
-
var $YAMLSeq = YAMLSeq.YAMLSeq;
|
|
20477
|
-
var $Lexer = lexer.Lexer;
|
|
20478
|
-
var $LineCounter = lineCounter.LineCounter;
|
|
20479
|
-
var $Parser = parser.Parser;
|
|
20480
|
-
var $parse = publicApi.parse;
|
|
20481
|
-
var $parseAllDocuments = publicApi.parseAllDocuments;
|
|
20482
|
-
var $parseDocument = publicApi.parseDocument;
|
|
20483
|
-
var $stringify = publicApi.stringify;
|
|
20484
|
-
var $visit = visit.visit;
|
|
20485
|
-
var $visitAsync = visit.visitAsync;
|
|
20486
|
-
|
|
20487
|
-
// src/ads-read.ts
|
|
20643
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
|
|
20644
|
+
import { join as join11, resolve as resolve9 } from "node:path";
|
|
20488
20645
|
async function serviceGet(auth, path9) {
|
|
20489
20646
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
20490
20647
|
headers: {
|
|
@@ -20572,12 +20729,12 @@ account changes: not tracked for this platform yet` : `
|
|
|
20572
20729
|
account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
|
|
20573
20730
|
}
|
|
20574
20731
|
function adsDigest(dir2) {
|
|
20575
|
-
const
|
|
20576
|
-
const path9 =
|
|
20577
|
-
if (!
|
|
20732
|
+
const root2 = resolve9(dir2 ?? ".");
|
|
20733
|
+
const path9 = join11(root2, "signals", "latest.json");
|
|
20734
|
+
if (!existsSync11(path9)) {
|
|
20578
20735
|
throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
|
|
20579
20736
|
}
|
|
20580
|
-
const d = JSON.parse(
|
|
20737
|
+
const d = JSON.parse(readFileSync9(path9, "utf8"));
|
|
20581
20738
|
console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
|
|
20582
20739
|
const t = d.totals;
|
|
20583
20740
|
console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
|
|
@@ -20616,26 +20773,26 @@ function adsOpen() {
|
|
|
20616
20773
|
}
|
|
20617
20774
|
async function adsDoctor(dir2) {
|
|
20618
20775
|
const auth = requireActive();
|
|
20619
|
-
const
|
|
20776
|
+
const root2 = resolve9(dir2 ?? ".");
|
|
20620
20777
|
let failed = 0;
|
|
20621
20778
|
const ok = (msg) => console.log(` ✓ ${msg}`);
|
|
20622
20779
|
const bad = (msg) => {
|
|
20623
20780
|
console.log(` ✗ ${msg}`);
|
|
20624
20781
|
failed += 1;
|
|
20625
20782
|
};
|
|
20626
|
-
const linkPath =
|
|
20783
|
+
const linkPath = join11(root2, ".zalify", "brand-repo.json");
|
|
20627
20784
|
let link = null;
|
|
20628
|
-
if (
|
|
20629
|
-
link = JSON.parse(
|
|
20785
|
+
if (existsSync11(linkPath)) {
|
|
20786
|
+
link = JSON.parse(readFileSync9(linkPath, "utf8"));
|
|
20630
20787
|
ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
|
|
20631
20788
|
} else {
|
|
20632
20789
|
console.log(" - not inside a brand-instance checkout (repo checks only)");
|
|
20633
20790
|
}
|
|
20634
20791
|
let identity2 = null;
|
|
20635
|
-
const identityPath =
|
|
20636
|
-
if (
|
|
20792
|
+
const identityPath = join11(root2, "identity.yaml");
|
|
20793
|
+
if (existsSync11(identityPath)) {
|
|
20637
20794
|
try {
|
|
20638
|
-
identity2 = $parse(
|
|
20795
|
+
identity2 = $parse(readFileSync9(identityPath, "utf8"));
|
|
20639
20796
|
ok("identity.yaml parses");
|
|
20640
20797
|
if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
|
|
20641
20798
|
bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
|
|
@@ -20701,8 +20858,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20701
20858
|
}
|
|
20702
20859
|
|
|
20703
20860
|
// src/brand.ts
|
|
20704
|
-
import { existsSync as
|
|
20705
|
-
import { basename as basename5, join as
|
|
20861
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
|
|
20862
|
+
import { basename as basename5, join as join12, resolve as resolve10 } from "node:path";
|
|
20706
20863
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20707
20864
|
|
|
20708
20865
|
The rules for filling in this folder. An author — human or AI agent —
|
|
@@ -21003,19 +21160,19 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
21003
21160
|
}, null, 2) + `
|
|
21004
21161
|
`;
|
|
21005
21162
|
function brandInit(dir2 = ".", opts = {}) {
|
|
21006
|
-
const target =
|
|
21163
|
+
const target = resolve10(dir2);
|
|
21007
21164
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
21008
|
-
if (
|
|
21165
|
+
if (existsSync12(target) && readdirSync4(target).length > 0) {
|
|
21009
21166
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
21010
21167
|
}
|
|
21011
21168
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
21012
21169
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
21013
|
-
mkdirSync5(
|
|
21014
|
-
|
|
21015
|
-
|
|
21016
|
-
|
|
21017
|
-
|
|
21018
|
-
|
|
21170
|
+
mkdirSync5(join12(target, "images"), { recursive: true });
|
|
21171
|
+
writeFileSync10(join12(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21172
|
+
writeFileSync10(join12(target, "brand.md"), BRAND_MD(name));
|
|
21173
|
+
writeFileSync10(join12(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
21174
|
+
writeFileSync10(join12(target, "catalog.json"), CATALOG_JSON(name, currency));
|
|
21175
|
+
writeFileSync10(join12(target, "images", "manifest.json"), MANIFEST_JSON);
|
|
21019
21176
|
console.log(`Scaffolded brand data in ${dir2}:`);
|
|
21020
21177
|
for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
|
|
21021
21178
|
console.log(` + ${f}`);
|
|
@@ -21026,23 +21183,23 @@ Author in this order: brand.md → catalog.json → images/manifest.json` + `
|
|
|
21026
21183
|
}
|
|
21027
21184
|
var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
|
|
21028
21185
|
function brandValidate(dir2 = ".") {
|
|
21029
|
-
const target =
|
|
21186
|
+
const target = resolve10(dir2);
|
|
21030
21187
|
const problems = [];
|
|
21031
21188
|
const warnings = [];
|
|
21032
21189
|
const readJson2 = (rel) => {
|
|
21033
|
-
const path9 =
|
|
21034
|
-
if (!
|
|
21190
|
+
const path9 = join12(target, rel);
|
|
21191
|
+
if (!existsSync12(path9)) {
|
|
21035
21192
|
problems.push(`missing ${rel}`);
|
|
21036
21193
|
return null;
|
|
21037
21194
|
}
|
|
21038
21195
|
try {
|
|
21039
|
-
return JSON.parse(
|
|
21196
|
+
return JSON.parse(readFileSync10(path9, "utf8"));
|
|
21040
21197
|
} catch (err) {
|
|
21041
21198
|
problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
|
|
21042
21199
|
return null;
|
|
21043
21200
|
}
|
|
21044
21201
|
};
|
|
21045
|
-
if (!
|
|
21202
|
+
if (!existsSync12(join12(target, "brand.md")))
|
|
21046
21203
|
problems.push("missing brand.md");
|
|
21047
21204
|
const store = readJson2("store.json");
|
|
21048
21205
|
if (store && (!store.name || !store.slug))
|
|
@@ -21218,7 +21375,7 @@ function brandValidate(dir2 = ".") {
|
|
|
21218
21375
|
// src/cli.ts
|
|
21219
21376
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
21220
21377
|
var require2 = createRequire2(import.meta.url);
|
|
21221
|
-
var pkg = require2(
|
|
21378
|
+
var pkg = require2(join13(__dirname4, "..", "package.json"));
|
|
21222
21379
|
try {
|
|
21223
21380
|
updateNotifier({ pkg }).notify({
|
|
21224
21381
|
isGlobal: true,
|
|
@@ -21306,6 +21463,12 @@ ads.command("new <name>").description("Scaffold a campaign folder — drop image
|
|
|
21306
21463
|
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
21464
|
await adsPull(options);
|
|
21308
21465
|
});
|
|
21466
|
+
ads.command("diff").description("Desired (your files) vs actual (the ingested account) — what apply would do").action(async () => {
|
|
21467
|
+
await adsDiff();
|
|
21468
|
+
});
|
|
21469
|
+
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) => {
|
|
21470
|
+
await adsApply(options);
|
|
21471
|
+
});
|
|
21309
21472
|
ads.command("status").description("The brand instance at a glance: access, ingestion freshness, plans, pending proposals").action(async () => {
|
|
21310
21473
|
await adsStatus();
|
|
21311
21474
|
});
|