@zalify/cli 0.22.0 → 0.23.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 -1
- package/dist/cli.js +193 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +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
|
|
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
|
+
| `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 |
|
|
107
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 |
|
|
108
109
|
| `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 |
|
|
109
110
|
| `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 |
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,20 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
31
31
|
return to;
|
|
32
32
|
};
|
|
33
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
var __returnValue = (v) => v;
|
|
35
|
+
function __exportSetter(name, newValue) {
|
|
36
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
+
}
|
|
38
|
+
var __export = (target, all) => {
|
|
39
|
+
for (var name in all)
|
|
40
|
+
__defProp(target, name, {
|
|
41
|
+
get: all[name],
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
set: __exportSetter.bind(all, name)
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
34
48
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
35
49
|
|
|
36
50
|
// node_modules/commander/lib/error.js
|
|
@@ -7929,6 +7943,17 @@ var require_ansi_align = __commonJS((exports, module) => {
|
|
|
7929
7943
|
}
|
|
7930
7944
|
});
|
|
7931
7945
|
|
|
7946
|
+
// src/hash.ts
|
|
7947
|
+
var exports_hash = {};
|
|
7948
|
+
__export(exports_hash, {
|
|
7949
|
+
sha256: () => sha256
|
|
7950
|
+
});
|
|
7951
|
+
import { createHash } from "node:crypto";
|
|
7952
|
+
function sha256(bytes) {
|
|
7953
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
7954
|
+
}
|
|
7955
|
+
var init_hash = () => {};
|
|
7956
|
+
|
|
7932
7957
|
// node_modules/yaml/dist/nodes/identity.js
|
|
7933
7958
|
var require_identity = __commonJS((exports) => {
|
|
7934
7959
|
var ALIAS = Symbol.for("yaml.alias");
|
|
@@ -18234,7 +18259,7 @@ function updateNotifier(options) {
|
|
|
18234
18259
|
|
|
18235
18260
|
// src/cli.ts
|
|
18236
18261
|
import { createRequire as createRequire2 } from "node:module";
|
|
18237
|
-
import { dirname as dirname3, join as
|
|
18262
|
+
import { dirname as dirname3, join as join14 } from "node:path";
|
|
18238
18263
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
18239
18264
|
|
|
18240
18265
|
// src/auth.ts
|
|
@@ -18555,13 +18580,8 @@ async function whoami() {
|
|
|
18555
18580
|
console.log(live ? "status ✓ key verified" : `status ! not verified — ${liveError}`);
|
|
18556
18581
|
}
|
|
18557
18582
|
|
|
18558
|
-
// src/hash.ts
|
|
18559
|
-
import { createHash } from "node:crypto";
|
|
18560
|
-
function sha256(bytes) {
|
|
18561
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
18562
|
-
}
|
|
18563
|
-
|
|
18564
18583
|
// src/assets.ts
|
|
18584
|
+
init_hash();
|
|
18565
18585
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2, readdirSync } from "node:fs";
|
|
18566
18586
|
import { join as join2, resolve } from "node:path";
|
|
18567
18587
|
var BATCH = 10;
|
|
@@ -18601,8 +18621,11 @@ async function assetsPull(storeDir) {
|
|
|
18601
18621
|
console.log(`Done: ${pulled} downloaded, ${Object.keys(index).length - pulled} already local.`);
|
|
18602
18622
|
}
|
|
18603
18623
|
async function assetsPush(storeDir) {
|
|
18624
|
+
await pushImagesIn(imagesDirFor(storeDir));
|
|
18625
|
+
}
|
|
18626
|
+
async function pushImagesIn(dir2) {
|
|
18604
18627
|
const { appUrl, workspaceId, key, workspaceName, workspaceSlug } = requireActive();
|
|
18605
|
-
const imagesDir =
|
|
18628
|
+
const imagesDir = dir2;
|
|
18606
18629
|
const indexPath = join2(imagesDir, "assets.json");
|
|
18607
18630
|
const index = readIndex(imagesDir);
|
|
18608
18631
|
async function api(path9, body) {
|
|
@@ -18625,13 +18648,13 @@ async function assetsPush(storeDir) {
|
|
|
18625
18648
|
}
|
|
18626
18649
|
return json;
|
|
18627
18650
|
}
|
|
18628
|
-
const files = readdirSync(imagesDir).filter((f) =>
|
|
18651
|
+
const files = readdirSync(imagesDir).filter((f) => /\.(png|jpe?g|webp)$/i.test(f)).map((name) => {
|
|
18629
18652
|
const bytes = readFileSync2(join2(imagesDir, name));
|
|
18630
18653
|
return { name, bytes, checksum: sha256(bytes) };
|
|
18631
18654
|
}).filter((f) => index[f.name]?.checksum !== f.checksum);
|
|
18632
18655
|
if (!files.length) {
|
|
18633
18656
|
console.log("Everything already pushed.");
|
|
18634
|
-
return;
|
|
18657
|
+
return index;
|
|
18635
18658
|
}
|
|
18636
18659
|
console.log(`Pushing ${files.length} file(s) to "${workspaceName || workspaceId}"`);
|
|
18637
18660
|
for (let i = 0;i < files.length; i += BATCH) {
|
|
@@ -18699,6 +18722,7 @@ async function assetsPush(storeDir) {
|
|
|
18699
18722
|
`);
|
|
18700
18723
|
}
|
|
18701
18724
|
console.log("Done.");
|
|
18725
|
+
return index;
|
|
18702
18726
|
}
|
|
18703
18727
|
|
|
18704
18728
|
// src/self-update.ts
|
|
@@ -18894,6 +18918,7 @@ async function workspaceSet(slugOrId) {
|
|
|
18894
18918
|
}
|
|
18895
18919
|
|
|
18896
18920
|
// src/images.ts
|
|
18921
|
+
init_hash();
|
|
18897
18922
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync4, rmSync as rmSync2 } from "node:fs";
|
|
18898
18923
|
import { join as join4, resolve as resolve2 } from "node:path";
|
|
18899
18924
|
var MAX_JOBS_PER_REQUEST = 40;
|
|
@@ -19111,6 +19136,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync
|
|
|
19111
19136
|
import { basename as basename2, join as join6, resolve as resolve4 } from "node:path";
|
|
19112
19137
|
|
|
19113
19138
|
// src/theme.ts
|
|
19139
|
+
init_hash();
|
|
19114
19140
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
19115
19141
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
19116
19142
|
import {
|
|
@@ -20339,8 +20365,8 @@ async function adsList() {
|
|
|
20339
20365
|
}
|
|
20340
20366
|
|
|
20341
20367
|
// src/ads-diff.ts
|
|
20342
|
-
import { existsSync as
|
|
20343
|
-
import { join as
|
|
20368
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
20369
|
+
import { join as join10, resolve as resolve7 } from "node:path";
|
|
20344
20370
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
20345
20371
|
|
|
20346
20372
|
// node_modules/yaml/dist/index.js
|
|
@@ -20389,43 +20415,127 @@ var $stringify = publicApi.stringify;
|
|
|
20389
20415
|
var $visit = visit.visit;
|
|
20390
20416
|
var $visitAsync = visit.visitAsync;
|
|
20391
20417
|
|
|
20418
|
+
// src/ads-sync.ts
|
|
20419
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20420
|
+
import { join as join9 } from "node:path";
|
|
20421
|
+
var CREATIVE_EXT = /\.(png|jpe?g|webp)$/i;
|
|
20422
|
+
async function collectFolder(base, name) {
|
|
20423
|
+
const dir2 = join9(base, "campaigns", name);
|
|
20424
|
+
const doc = $parse(readFileSync7(join9(dir2, "campaign.yaml"), "utf8"));
|
|
20425
|
+
const creativesDir = join9(dir2, "creatives");
|
|
20426
|
+
const sources = [];
|
|
20427
|
+
const gather = async (sub, adSet) => {
|
|
20428
|
+
const abs = join9(creativesDir, sub);
|
|
20429
|
+
if (!existsSync9(abs))
|
|
20430
|
+
return;
|
|
20431
|
+
const entries = readdirSync3(abs, { withFileTypes: true });
|
|
20432
|
+
const hasImages = entries.some((e) => e.isFile() && CREATIVE_EXT.test(e.name));
|
|
20433
|
+
const index = hasImages ? await pushImagesIn(abs) : {};
|
|
20434
|
+
for (const e of entries) {
|
|
20435
|
+
if (e.isDirectory() && sub === "") {
|
|
20436
|
+
await gather(e.name, e.name);
|
|
20437
|
+
continue;
|
|
20438
|
+
}
|
|
20439
|
+
if (!e.isFile() || !CREATIVE_EXT.test(e.name))
|
|
20440
|
+
continue;
|
|
20441
|
+
const sidecarPath = join9(abs, `${e.name.replace(CREATIVE_EXT, "")}.copy.yaml`);
|
|
20442
|
+
sources.push({
|
|
20443
|
+
filename: e.name,
|
|
20444
|
+
adSet,
|
|
20445
|
+
...existsSync9(sidecarPath) ? { sidecar: $parse(readFileSync7(sidecarPath, "utf8")) } : {},
|
|
20446
|
+
...index[e.name] ? { asset: index[e.name].url } : {}
|
|
20447
|
+
});
|
|
20448
|
+
}
|
|
20449
|
+
};
|
|
20450
|
+
await gather("", "");
|
|
20451
|
+
return { path: `campaigns/${name}/campaign.yaml`, doc, sources };
|
|
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);
|
|
20458
|
+
}
|
|
20459
|
+
async function adsCreativeGenerate(opts) {
|
|
20460
|
+
const auth = requireActive();
|
|
20461
|
+
const dir2 = join9(".", "campaigns", opts.campaign, "creatives");
|
|
20462
|
+
if (!existsSync9(dir2)) {
|
|
20463
|
+
throw new Error(`campaigns/${opts.campaign}/creatives/ not found — \`zalify ads new ${opts.campaign}\` first.`);
|
|
20464
|
+
}
|
|
20465
|
+
console.log("Generating (the canvas pipeline: safe zones + product fidelity)…");
|
|
20466
|
+
const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/creative`, {
|
|
20467
|
+
method: "POST",
|
|
20468
|
+
headers: {
|
|
20469
|
+
Authorization: `Bearer ${auth.key}`,
|
|
20470
|
+
"x-zalify-workspace-id": auth.workspaceId,
|
|
20471
|
+
"Content-Type": "application/json"
|
|
20472
|
+
},
|
|
20473
|
+
body: JSON.stringify({
|
|
20474
|
+
prompt: opts.prompt,
|
|
20475
|
+
orientation: opts.orientation ?? "square",
|
|
20476
|
+
refUrls: opts.ref ?? []
|
|
20477
|
+
})
|
|
20478
|
+
});
|
|
20479
|
+
const json = await res.json().catch(() => ({}));
|
|
20480
|
+
if (!res.ok || !json.image) {
|
|
20481
|
+
throw new Error(`generation failed (${res.status}): ${json.error ?? ""}`);
|
|
20482
|
+
}
|
|
20483
|
+
const download = await fetch(json.image.url);
|
|
20484
|
+
if (!download.ok)
|
|
20485
|
+
throw new Error(`download failed (${download.status})`);
|
|
20486
|
+
const bytes = Buffer.from(await download.arrayBuffer());
|
|
20487
|
+
const filename = `${(opts.name ?? json.image.name ?? "generated").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, 60)}.png`;
|
|
20488
|
+
writeFileSync8(join9(dir2, filename), bytes);
|
|
20489
|
+
const indexPath = join9(dir2, "assets.json");
|
|
20490
|
+
const index = existsSync9(indexPath) ? JSON.parse(readFileSync7(indexPath, "utf8")) : {};
|
|
20491
|
+
const { sha256: sha2562 } = await Promise.resolve().then(() => (init_hash(), exports_hash));
|
|
20492
|
+
index[filename] = {
|
|
20493
|
+
assetId: json.image.assetId,
|
|
20494
|
+
url: json.image.url,
|
|
20495
|
+
checksum: sha2562(bytes)
|
|
20496
|
+
};
|
|
20497
|
+
writeFileSync8(indexPath, `${JSON.stringify(index, null, 2)}
|
|
20498
|
+
`);
|
|
20499
|
+
console.log(`✓ creatives/${filename} (asset ${json.image.assetId})`);
|
|
20500
|
+
console.log(" Review the image; delete the file to reject it. `zalify ads diff` when ready.");
|
|
20501
|
+
}
|
|
20502
|
+
|
|
20392
20503
|
// src/ads-diff.ts
|
|
20393
20504
|
function root() {
|
|
20394
20505
|
const cwd = resolve7(".");
|
|
20395
|
-
if (
|
|
20506
|
+
if (existsSync10(join10(cwd, ".zalify", "brand-repo.json")) || existsSync10(join10(cwd, "campaigns"))) {
|
|
20396
20507
|
return cwd;
|
|
20397
20508
|
}
|
|
20398
20509
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20399
20510
|
}
|
|
20400
|
-
function collect(base) {
|
|
20401
|
-
const dir2 =
|
|
20402
|
-
if (!
|
|
20403
|
-
return { files: []
|
|
20511
|
+
async function collect(base) {
|
|
20512
|
+
const dir2 = join10(base, "campaigns");
|
|
20513
|
+
if (!existsSync10(dir2))
|
|
20514
|
+
return { files: [] };
|
|
20404
20515
|
const files = [];
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
if (entry.isDirectory()) {
|
|
20408
|
-
if (existsSync9(join9(dir2, entry.name, "campaign.yaml")))
|
|
20409
|
-
folders += 1;
|
|
20516
|
+
for (const entry of readdirSync4(dir2, { withFileTypes: true })) {
|
|
20517
|
+
if (entry.isDirectory())
|
|
20410
20518
|
continue;
|
|
20411
|
-
}
|
|
20412
20519
|
if (!entry.name.endsWith(".yaml") || entry.name.endsWith(".copy.yaml")) {
|
|
20413
20520
|
continue;
|
|
20414
20521
|
}
|
|
20415
20522
|
const path9 = `campaigns/${entry.name}`;
|
|
20416
20523
|
try {
|
|
20417
|
-
files.push({ path: path9, doc: $parse(
|
|
20524
|
+
files.push({ path: path9, doc: $parse(readFileSync8(join10(base, path9), "utf8")) });
|
|
20418
20525
|
} catch (err) {
|
|
20419
20526
|
throw new Error(`${path9} is not valid YAML: ${String(err).slice(0, 120)}`);
|
|
20420
20527
|
}
|
|
20421
20528
|
}
|
|
20422
|
-
|
|
20529
|
+
for (const name of folderCampaigns(base)) {
|
|
20530
|
+
files.push(await collectFolder(base, name));
|
|
20531
|
+
}
|
|
20532
|
+
return { files };
|
|
20423
20533
|
}
|
|
20424
20534
|
function ledger(base) {
|
|
20425
|
-
const path9 =
|
|
20426
|
-
if (!
|
|
20535
|
+
const path9 = join10(base, "campaigns", ".bindings.json");
|
|
20536
|
+
if (!existsSync10(path9))
|
|
20427
20537
|
return { nodes: {}, pulled: {} };
|
|
20428
|
-
const raw = JSON.parse(
|
|
20538
|
+
const raw = JSON.parse(readFileSync8(path9, "utf8"));
|
|
20429
20539
|
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20430
20540
|
}
|
|
20431
20541
|
async function post(auth, path9, payload) {
|
|
@@ -20443,7 +20553,10 @@ async function post(auth, path9, payload) {
|
|
|
20443
20553
|
throw new Error(`${path9}: ${res.status} ${json.error ?? ""}`.trim());
|
|
20444
20554
|
return json;
|
|
20445
20555
|
}
|
|
20446
|
-
function renderPlan(result
|
|
20556
|
+
function renderPlan(result) {
|
|
20557
|
+
for (const n of result.notes ?? []) {
|
|
20558
|
+
console.log(`· ${n.file} — ${n.message}`);
|
|
20559
|
+
}
|
|
20447
20560
|
for (const f of result.issues) {
|
|
20448
20561
|
console.log(`✗ ${f.file}`);
|
|
20449
20562
|
for (const i of f.issues)
|
|
@@ -20464,13 +20577,13 @@ function renderPlan(result, folders) {
|
|
|
20464
20577
|
}
|
|
20465
20578
|
const s = result.summary;
|
|
20466
20579
|
console.log(`
|
|
20467
|
-
${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}`
|
|
20580
|
+
${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}`);
|
|
20468
20581
|
}
|
|
20469
20582
|
async function adsDiff() {
|
|
20470
20583
|
const auth = requireActive();
|
|
20471
20584
|
const base = root();
|
|
20472
|
-
const { files
|
|
20473
|
-
if (files.length === 0
|
|
20585
|
+
const { files } = await collect(base);
|
|
20586
|
+
if (files.length === 0) {
|
|
20474
20587
|
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20475
20588
|
return;
|
|
20476
20589
|
}
|
|
@@ -20479,14 +20592,14 @@ async function adsDiff() {
|
|
|
20479
20592
|
files,
|
|
20480
20593
|
bindings: led.nodes
|
|
20481
20594
|
});
|
|
20482
|
-
renderPlan(result
|
|
20595
|
+
renderPlan(result);
|
|
20483
20596
|
if (result.issues.length > 0)
|
|
20484
20597
|
process.exitCode = 1;
|
|
20485
20598
|
}
|
|
20486
20599
|
async function adsApply(opts) {
|
|
20487
20600
|
const auth = requireActive();
|
|
20488
20601
|
const base = root();
|
|
20489
|
-
const { files
|
|
20602
|
+
const { files } = await collect(base);
|
|
20490
20603
|
if (files.length === 0) {
|
|
20491
20604
|
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20492
20605
|
return;
|
|
@@ -20496,7 +20609,7 @@ async function adsApply(opts) {
|
|
|
20496
20609
|
files,
|
|
20497
20610
|
bindings: led.nodes
|
|
20498
20611
|
});
|
|
20499
|
-
renderPlan(diff
|
|
20612
|
+
renderPlan(diff);
|
|
20500
20613
|
if (diff.issues.length > 0) {
|
|
20501
20614
|
throw new Error("Fix the invalid files before applying.");
|
|
20502
20615
|
}
|
|
@@ -20531,7 +20644,7 @@ ${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : "
|
|
|
20531
20644
|
}
|
|
20532
20645
|
if (Object.keys(result.newBindings).length > 0) {
|
|
20533
20646
|
Object.assign(led.nodes, result.newBindings);
|
|
20534
|
-
|
|
20647
|
+
writeFileSync9(join10(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
|
|
20535
20648
|
`);
|
|
20536
20649
|
console.log(`
|
|
20537
20650
|
${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
|
|
@@ -20543,25 +20656,25 @@ ${Object.keys(result.newBindings).length} new bindings recorded — everything c
|
|
|
20543
20656
|
// src/ads-pull.ts
|
|
20544
20657
|
import { createHash as createHash2 } from "node:crypto";
|
|
20545
20658
|
import {
|
|
20546
|
-
existsSync as
|
|
20659
|
+
existsSync as existsSync11,
|
|
20547
20660
|
mkdirSync as mkdirSync4,
|
|
20548
|
-
readFileSync as
|
|
20549
|
-
writeFileSync as
|
|
20661
|
+
readFileSync as readFileSync9,
|
|
20662
|
+
writeFileSync as writeFileSync10
|
|
20550
20663
|
} from "node:fs";
|
|
20551
|
-
import { dirname as dirname2, join as
|
|
20664
|
+
import { dirname as dirname2, join as join11, resolve as resolve8 } from "node:path";
|
|
20552
20665
|
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20553
20666
|
function checkoutRoot() {
|
|
20554
20667
|
const cwd = resolve8(".");
|
|
20555
|
-
if (
|
|
20668
|
+
if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "campaigns"))) {
|
|
20556
20669
|
return cwd;
|
|
20557
20670
|
}
|
|
20558
20671
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20559
20672
|
}
|
|
20560
20673
|
function readBindings(root2) {
|
|
20561
|
-
const path9 =
|
|
20562
|
-
if (!
|
|
20674
|
+
const path9 = join11(root2, "campaigns", ".bindings.json");
|
|
20675
|
+
if (!existsSync11(path9))
|
|
20563
20676
|
return { nodes: {}, pulled: {} };
|
|
20564
|
-
const raw = JSON.parse(
|
|
20677
|
+
const raw = JSON.parse(readFileSync9(path9, "utf8"));
|
|
20565
20678
|
return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
|
|
20566
20679
|
}
|
|
20567
20680
|
async function adsPull(opts) {
|
|
@@ -20589,11 +20702,11 @@ async function adsPull(opts) {
|
|
|
20589
20702
|
let unchanged = 0;
|
|
20590
20703
|
const skipped = [];
|
|
20591
20704
|
for (const f of result.files) {
|
|
20592
|
-
const abs =
|
|
20705
|
+
const abs = join11(root2, f.path);
|
|
20593
20706
|
const nextHash = sha(f.yaml);
|
|
20594
20707
|
const lastPulled = bindings.pulled[f.path];
|
|
20595
|
-
if (
|
|
20596
|
-
const current =
|
|
20708
|
+
if (existsSync11(abs)) {
|
|
20709
|
+
const current = readFileSync9(abs, "utf8");
|
|
20597
20710
|
if (sha(current) === nextHash) {
|
|
20598
20711
|
unchanged += 1;
|
|
20599
20712
|
bindings.pulled[f.path] = nextHash;
|
|
@@ -20606,17 +20719,17 @@ async function adsPull(opts) {
|
|
|
20606
20719
|
}
|
|
20607
20720
|
}
|
|
20608
20721
|
mkdirSync4(dirname2(abs), { recursive: true });
|
|
20609
|
-
|
|
20722
|
+
writeFileSync10(abs, f.yaml);
|
|
20610
20723
|
bindings.pulled[f.path] = nextHash;
|
|
20611
20724
|
written += 1;
|
|
20612
20725
|
}
|
|
20613
20726
|
Object.assign(bindings.nodes, result.bindings);
|
|
20614
|
-
const bindingsPath =
|
|
20727
|
+
const bindingsPath = join11(root2, "campaigns", ".bindings.json");
|
|
20615
20728
|
mkdirSync4(dirname2(bindingsPath), { recursive: true });
|
|
20616
|
-
|
|
20729
|
+
writeFileSync10(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20617
20730
|
`);
|
|
20618
20731
|
const exported = new Set(result.files.map((f) => f.path));
|
|
20619
|
-
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) &&
|
|
20732
|
+
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) && existsSync11(join11(root2, p)));
|
|
20620
20733
|
const s = result.summary;
|
|
20621
20734
|
console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
|
|
20622
20735
|
for (const p of skipped)
|
|
@@ -20637,8 +20750,8 @@ Review with git diff; commit when it reads right.`);
|
|
|
20637
20750
|
|
|
20638
20751
|
// src/ads-read.ts
|
|
20639
20752
|
import { spawn as spawn4 } from "node:child_process";
|
|
20640
|
-
import { existsSync as
|
|
20641
|
-
import { join as
|
|
20753
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
20754
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
20642
20755
|
async function serviceGet(auth, path9) {
|
|
20643
20756
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
20644
20757
|
headers: {
|
|
@@ -20727,11 +20840,11 @@ account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
|
|
|
20727
20840
|
}
|
|
20728
20841
|
function adsDigest(dir2) {
|
|
20729
20842
|
const root2 = resolve9(dir2 ?? ".");
|
|
20730
|
-
const path9 =
|
|
20731
|
-
if (!
|
|
20843
|
+
const path9 = join12(root2, "signals", "latest.json");
|
|
20844
|
+
if (!existsSync12(path9)) {
|
|
20732
20845
|
throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
|
|
20733
20846
|
}
|
|
20734
|
-
const d = JSON.parse(
|
|
20847
|
+
const d = JSON.parse(readFileSync10(path9, "utf8"));
|
|
20735
20848
|
console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
|
|
20736
20849
|
const t = d.totals;
|
|
20737
20850
|
console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
|
|
@@ -20777,19 +20890,19 @@ async function adsDoctor(dir2) {
|
|
|
20777
20890
|
console.log(` ✗ ${msg}`);
|
|
20778
20891
|
failed += 1;
|
|
20779
20892
|
};
|
|
20780
|
-
const linkPath =
|
|
20893
|
+
const linkPath = join12(root2, ".zalify", "brand-repo.json");
|
|
20781
20894
|
let link = null;
|
|
20782
|
-
if (
|
|
20783
|
-
link = JSON.parse(
|
|
20895
|
+
if (existsSync12(linkPath)) {
|
|
20896
|
+
link = JSON.parse(readFileSync10(linkPath, "utf8"));
|
|
20784
20897
|
ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
|
|
20785
20898
|
} else {
|
|
20786
20899
|
console.log(" - not inside a brand-instance checkout (repo checks only)");
|
|
20787
20900
|
}
|
|
20788
20901
|
let identity2 = null;
|
|
20789
|
-
const identityPath =
|
|
20790
|
-
if (
|
|
20902
|
+
const identityPath = join12(root2, "identity.yaml");
|
|
20903
|
+
if (existsSync12(identityPath)) {
|
|
20791
20904
|
try {
|
|
20792
|
-
identity2 = $parse(
|
|
20905
|
+
identity2 = $parse(readFileSync10(identityPath, "utf8"));
|
|
20793
20906
|
ok("identity.yaml parses");
|
|
20794
20907
|
if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
|
|
20795
20908
|
bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
|
|
@@ -20855,8 +20968,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20855
20968
|
}
|
|
20856
20969
|
|
|
20857
20970
|
// src/brand.ts
|
|
20858
|
-
import { existsSync as
|
|
20859
|
-
import { basename as basename5, join as
|
|
20971
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
20972
|
+
import { basename as basename5, join as join13, resolve as resolve10 } from "node:path";
|
|
20860
20973
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20861
20974
|
|
|
20862
20975
|
The rules for filling in this folder. An author — human or AI agent —
|
|
@@ -21159,17 +21272,17 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
21159
21272
|
function brandInit(dir2 = ".", opts = {}) {
|
|
21160
21273
|
const target = resolve10(dir2);
|
|
21161
21274
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
21162
|
-
if (
|
|
21275
|
+
if (existsSync13(target) && readdirSync5(target).length > 0) {
|
|
21163
21276
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
21164
21277
|
}
|
|
21165
21278
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
21166
21279
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
21167
|
-
mkdirSync5(
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
|
|
21280
|
+
mkdirSync5(join13(target, "images"), { recursive: true });
|
|
21281
|
+
writeFileSync11(join13(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21282
|
+
writeFileSync11(join13(target, "brand.md"), BRAND_MD(name));
|
|
21283
|
+
writeFileSync11(join13(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
21284
|
+
writeFileSync11(join13(target, "catalog.json"), CATALOG_JSON(name, currency));
|
|
21285
|
+
writeFileSync11(join13(target, "images", "manifest.json"), MANIFEST_JSON);
|
|
21173
21286
|
console.log(`Scaffolded brand data in ${dir2}:`);
|
|
21174
21287
|
for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
|
|
21175
21288
|
console.log(` + ${f}`);
|
|
@@ -21184,19 +21297,19 @@ function brandValidate(dir2 = ".") {
|
|
|
21184
21297
|
const problems = [];
|
|
21185
21298
|
const warnings = [];
|
|
21186
21299
|
const readJson2 = (rel) => {
|
|
21187
|
-
const path9 =
|
|
21188
|
-
if (!
|
|
21300
|
+
const path9 = join13(target, rel);
|
|
21301
|
+
if (!existsSync13(path9)) {
|
|
21189
21302
|
problems.push(`missing ${rel}`);
|
|
21190
21303
|
return null;
|
|
21191
21304
|
}
|
|
21192
21305
|
try {
|
|
21193
|
-
return JSON.parse(
|
|
21306
|
+
return JSON.parse(readFileSync11(path9, "utf8"));
|
|
21194
21307
|
} catch (err) {
|
|
21195
21308
|
problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
|
|
21196
21309
|
return null;
|
|
21197
21310
|
}
|
|
21198
21311
|
};
|
|
21199
|
-
if (!
|
|
21312
|
+
if (!existsSync13(join13(target, "brand.md")))
|
|
21200
21313
|
problems.push("missing brand.md");
|
|
21201
21314
|
const store = readJson2("store.json");
|
|
21202
21315
|
if (store && (!store.name || !store.slug))
|
|
@@ -21372,7 +21485,7 @@ function brandValidate(dir2 = ".") {
|
|
|
21372
21485
|
// src/cli.ts
|
|
21373
21486
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
21374
21487
|
var require2 = createRequire2(import.meta.url);
|
|
21375
|
-
var pkg = require2(
|
|
21488
|
+
var pkg = require2(join14(__dirname4, "..", "package.json"));
|
|
21376
21489
|
try {
|
|
21377
21490
|
updateNotifier({ pkg }).notify({
|
|
21378
21491
|
isGlobal: true,
|
|
@@ -21485,6 +21598,10 @@ adsPlan.command("show <id>").description("Print one plan's content (id, or slug@
|
|
|
21485
21598
|
ads.command("open").description("Open the Structure canvas for the active workspace").action(() => {
|
|
21486
21599
|
adsOpen();
|
|
21487
21600
|
});
|
|
21601
|
+
var adsCreative = ads.command("creative").description("Creatives for folder campaigns — generated through the canvas pipeline");
|
|
21602
|
+
adsCreative.command("generate <campaign>").description("Generate one image (safe zones + product fidelity) into the campaign's creatives/ — same pipeline, same asset library, stored once").requiredOption("--prompt <text>", "what the image should be").option("--orientation <o>", "square | landscape | portrait", "square").option("--ref <url...>", "product photos the image must stay faithful to").option("--name <name>", "filename stem (default: derived)").action(async (campaign, options) => {
|
|
21603
|
+
await adsCreativeGenerate({ campaign, ...options });
|
|
21604
|
+
});
|
|
21488
21605
|
ads.command("doctor [dir]").description("Health checks that have each broken once: slug alignment, consent drift, timezone match, credential expiry").action(async (dir2) => {
|
|
21489
21606
|
await adsDoctor(dir2);
|
|
21490
21607
|
});
|