@zalify/cli 0.22.1 → 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 +4 -3
- package/dist/cli.js +270 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,9 +101,10 @@ 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
|
|
106
|
-
| `ads
|
|
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
|
+
| `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 {
|
|
@@ -20118,8 +20144,8 @@ async function shopifyUploadImages(storeDir, options = {}) {
|
|
|
20118
20144
|
|
|
20119
20145
|
// src/ads.ts
|
|
20120
20146
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
20121
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
20122
|
-
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";
|
|
20123
20149
|
import { createInterface } from "node:readline/promises";
|
|
20124
20150
|
function apiError3(status, json) {
|
|
20125
20151
|
if (json.code === "SLUG_TAKEN") {
|
|
@@ -20257,14 +20283,22 @@ Next steps:
|
|
|
20257
20283
|
` + ` - the Autopilot service picks the instance up from there`);
|
|
20258
20284
|
}
|
|
20259
20285
|
var CAMPAIGN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
20260
|
-
function
|
|
20286
|
+
function campaignParent(account) {
|
|
20261
20287
|
const cwd = resolve6(".");
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
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>.`);
|
|
20268
20302
|
}
|
|
20269
20303
|
async function ask(question, fallback) {
|
|
20270
20304
|
if (!process.stdin.isTTY)
|
|
@@ -20278,9 +20312,10 @@ async function adsNew(name, opts) {
|
|
|
20278
20312
|
if (!CAMPAIGN_NAME_RE.test(name) || name.length > 63) {
|
|
20279
20313
|
throw new Error(`"${name}" — campaign names are lowercase letters, digits, and inner hyphens.`);
|
|
20280
20314
|
}
|
|
20281
|
-
const
|
|
20315
|
+
const parent = campaignParent(opts.account);
|
|
20316
|
+
const dir2 = join8(parent, name);
|
|
20282
20317
|
if (existsSync8(dir2))
|
|
20283
|
-
throw new Error(
|
|
20318
|
+
throw new Error(`${dir2} already exists.`);
|
|
20284
20319
|
const objective = opts.objective ?? await ask("objective (sales/leads/traffic/awareness)", "sales");
|
|
20285
20320
|
const budget = opts.budget ?? await ask("budget", "$20/day");
|
|
20286
20321
|
const country = opts.country ?? await ask("country", "US");
|
|
@@ -20307,7 +20342,7 @@ ad_sets:
|
|
|
20307
20342
|
age: 18-65
|
|
20308
20343
|
advantage: true
|
|
20309
20344
|
`);
|
|
20310
|
-
console.log(`✓
|
|
20345
|
+
console.log(`✓ ${relative(resolve6("."), dir2)}/ is ready`);
|
|
20311
20346
|
console.log(" 1. drag images into creatives/");
|
|
20312
20347
|
console.log(" 2. fill primary_text and link in campaign.yaml");
|
|
20313
20348
|
console.log(" 3. `zalify ads sync` when it lands — validate, diff, apply");
|
|
@@ -20339,8 +20374,14 @@ async function adsList() {
|
|
|
20339
20374
|
}
|
|
20340
20375
|
|
|
20341
20376
|
// src/ads-diff.ts
|
|
20342
|
-
import {
|
|
20343
|
-
|
|
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";
|
|
20384
|
+
import { join as join10, resolve as resolve7 } from "node:path";
|
|
20344
20385
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
20345
20386
|
|
|
20346
20387
|
// node_modules/yaml/dist/index.js
|
|
@@ -20389,44 +20430,159 @@ var $stringify = publicApi.stringify;
|
|
|
20389
20430
|
var $visit = visit.visit;
|
|
20390
20431
|
var $visitAsync = visit.visitAsync;
|
|
20391
20432
|
|
|
20433
|
+
// src/ads-sync.ts
|
|
20434
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
20435
|
+
import { join as join9 } from "node:path";
|
|
20436
|
+
var CREATIVE_EXT = /\.(png|jpe?g|webp)$/i;
|
|
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);
|
|
20464
|
+
const doc = $parse(readFileSync7(join9(dir2, "campaign.yaml"), "utf8"));
|
|
20465
|
+
const creativesDir = join9(dir2, "creatives");
|
|
20466
|
+
const sources = [];
|
|
20467
|
+
const gather = async (sub, adSet) => {
|
|
20468
|
+
const abs = join9(creativesDir, sub);
|
|
20469
|
+
if (!existsSync9(abs))
|
|
20470
|
+
return;
|
|
20471
|
+
const entries = readdirSync4(abs, { withFileTypes: true });
|
|
20472
|
+
const hasImages = entries.some((e) => e.isFile() && CREATIVE_EXT.test(e.name));
|
|
20473
|
+
const index = hasImages ? await pushImagesIn(abs) : {};
|
|
20474
|
+
for (const e of entries) {
|
|
20475
|
+
if (e.isDirectory() && sub === "") {
|
|
20476
|
+
await gather(e.name, e.name);
|
|
20477
|
+
continue;
|
|
20478
|
+
}
|
|
20479
|
+
if (!e.isFile() || !CREATIVE_EXT.test(e.name))
|
|
20480
|
+
continue;
|
|
20481
|
+
const sidecarPath = join9(abs, `${e.name.replace(CREATIVE_EXT, "")}.copy.yaml`);
|
|
20482
|
+
sources.push({
|
|
20483
|
+
filename: e.name,
|
|
20484
|
+
adSet,
|
|
20485
|
+
...existsSync9(sidecarPath) ? { sidecar: $parse(readFileSync7(sidecarPath, "utf8")) } : {},
|
|
20486
|
+
...index[e.name] ? { asset: index[e.name].url } : {}
|
|
20487
|
+
});
|
|
20488
|
+
}
|
|
20489
|
+
};
|
|
20490
|
+
await gather("", "");
|
|
20491
|
+
return { path: `${rel}/campaign.yaml`, doc, sources };
|
|
20492
|
+
}
|
|
20493
|
+
async function adsCreativeGenerate(opts) {
|
|
20494
|
+
const auth = requireActive();
|
|
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");
|
|
20503
|
+
if (!existsSync9(dir2)) {
|
|
20504
|
+
throw new Error(`${matches[0]}/creatives/ not found.`);
|
|
20505
|
+
}
|
|
20506
|
+
console.log("Generating (the canvas pipeline: safe zones + product fidelity)…");
|
|
20507
|
+
const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/creative`, {
|
|
20508
|
+
method: "POST",
|
|
20509
|
+
headers: {
|
|
20510
|
+
Authorization: `Bearer ${auth.key}`,
|
|
20511
|
+
"x-zalify-workspace-id": auth.workspaceId,
|
|
20512
|
+
"Content-Type": "application/json"
|
|
20513
|
+
},
|
|
20514
|
+
body: JSON.stringify({
|
|
20515
|
+
prompt: opts.prompt,
|
|
20516
|
+
orientation: opts.orientation ?? "square",
|
|
20517
|
+
refUrls: opts.ref ?? []
|
|
20518
|
+
})
|
|
20519
|
+
});
|
|
20520
|
+
const json = await res.json().catch(() => ({}));
|
|
20521
|
+
if (!res.ok || !json.image) {
|
|
20522
|
+
throw new Error(`generation failed (${res.status}): ${json.error ?? ""}`);
|
|
20523
|
+
}
|
|
20524
|
+
const download = await fetch(json.image.url);
|
|
20525
|
+
if (!download.ok)
|
|
20526
|
+
throw new Error(`download failed (${download.status})`);
|
|
20527
|
+
const bytes = Buffer.from(await download.arrayBuffer());
|
|
20528
|
+
const filename = `${(opts.name ?? json.image.name ?? "generated").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, 60)}.png`;
|
|
20529
|
+
writeFileSync8(join9(dir2, filename), bytes);
|
|
20530
|
+
const indexPath = join9(dir2, "assets.json");
|
|
20531
|
+
const index = existsSync9(indexPath) ? JSON.parse(readFileSync7(indexPath, "utf8")) : {};
|
|
20532
|
+
const { sha256: sha2562 } = await Promise.resolve().then(() => (init_hash(), exports_hash));
|
|
20533
|
+
index[filename] = {
|
|
20534
|
+
assetId: json.image.assetId,
|
|
20535
|
+
url: json.image.url,
|
|
20536
|
+
checksum: sha2562(bytes)
|
|
20537
|
+
};
|
|
20538
|
+
writeFileSync8(indexPath, `${JSON.stringify(index, null, 2)}
|
|
20539
|
+
`);
|
|
20540
|
+
console.log(`✓ creatives/${filename} (asset ${json.image.assetId})`);
|
|
20541
|
+
console.log(" Review the image; delete the file to reject it. `zalify ads diff` when ready.");
|
|
20542
|
+
}
|
|
20543
|
+
|
|
20392
20544
|
// src/ads-diff.ts
|
|
20393
20545
|
function root() {
|
|
20394
20546
|
const cwd = resolve7(".");
|
|
20395
|
-
if (
|
|
20547
|
+
if (existsSync10(join10(cwd, ".zalify", "brand-repo.json")) || existsSync10(join10(cwd, "campaigns"))) {
|
|
20396
20548
|
return cwd;
|
|
20397
20549
|
}
|
|
20398
20550
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20399
20551
|
}
|
|
20400
|
-
function collect(base) {
|
|
20401
|
-
const dir2 = join9(base, "campaigns");
|
|
20402
|
-
if (!existsSync9(dir2))
|
|
20403
|
-
return { files: [], folders: 0 };
|
|
20552
|
+
async function collect(base) {
|
|
20404
20553
|
const files = [];
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
|
|
20408
|
-
if (
|
|
20409
|
-
|
|
20410
|
-
|
|
20411
|
-
|
|
20412
|
-
|
|
20413
|
-
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
throw new Error(`${path9} is not valid YAML: ${String(err).slice(0, 120)}`);
|
|
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
|
+
}
|
|
20420
20568
|
}
|
|
20421
20569
|
}
|
|
20422
|
-
|
|
20570
|
+
for (const rel of campaignDirs(base)) {
|
|
20571
|
+
files.push(await collectFolder(base, rel));
|
|
20572
|
+
}
|
|
20573
|
+
return { files };
|
|
20574
|
+
}
|
|
20575
|
+
function ledgerPath(base) {
|
|
20576
|
+
return join10(base, "accounts", ".bindings.json");
|
|
20423
20577
|
}
|
|
20424
20578
|
function ledger(base) {
|
|
20425
|
-
const path9
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
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: {} };
|
|
20430
20586
|
}
|
|
20431
20587
|
async function post(auth, path9, payload) {
|
|
20432
20588
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
@@ -20443,7 +20599,7 @@ async function post(auth, path9, payload) {
|
|
|
20443
20599
|
throw new Error(`${path9}: ${res.status} ${json.error ?? ""}`.trim());
|
|
20444
20600
|
return json;
|
|
20445
20601
|
}
|
|
20446
|
-
function renderPlan(result
|
|
20602
|
+
function renderPlan(result) {
|
|
20447
20603
|
for (const n of result.notes ?? []) {
|
|
20448
20604
|
console.log(`· ${n.file} — ${n.message}`);
|
|
20449
20605
|
}
|
|
@@ -20467,13 +20623,13 @@ function renderPlan(result, folders) {
|
|
|
20467
20623
|
}
|
|
20468
20624
|
const s = result.summary;
|
|
20469
20625
|
console.log(`
|
|
20470
|
-
${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}`
|
|
20626
|
+
${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}`);
|
|
20471
20627
|
}
|
|
20472
20628
|
async function adsDiff() {
|
|
20473
20629
|
const auth = requireActive();
|
|
20474
20630
|
const base = root();
|
|
20475
|
-
const { files
|
|
20476
|
-
if (files.length === 0
|
|
20631
|
+
const { files } = await collect(base);
|
|
20632
|
+
if (files.length === 0) {
|
|
20477
20633
|
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20478
20634
|
return;
|
|
20479
20635
|
}
|
|
@@ -20482,14 +20638,14 @@ async function adsDiff() {
|
|
|
20482
20638
|
files,
|
|
20483
20639
|
bindings: led.nodes
|
|
20484
20640
|
});
|
|
20485
|
-
renderPlan(result
|
|
20641
|
+
renderPlan(result);
|
|
20486
20642
|
if (result.issues.length > 0)
|
|
20487
20643
|
process.exitCode = 1;
|
|
20488
20644
|
}
|
|
20489
20645
|
async function adsApply(opts) {
|
|
20490
20646
|
const auth = requireActive();
|
|
20491
20647
|
const base = root();
|
|
20492
|
-
const { files
|
|
20648
|
+
const { files } = await collect(base);
|
|
20493
20649
|
if (files.length === 0) {
|
|
20494
20650
|
console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
|
|
20495
20651
|
return;
|
|
@@ -20499,7 +20655,7 @@ async function adsApply(opts) {
|
|
|
20499
20655
|
files,
|
|
20500
20656
|
bindings: led.nodes
|
|
20501
20657
|
});
|
|
20502
|
-
renderPlan(diff
|
|
20658
|
+
renderPlan(diff);
|
|
20503
20659
|
if (diff.issues.length > 0) {
|
|
20504
20660
|
throw new Error("Fix the invalid files before applying.");
|
|
20505
20661
|
}
|
|
@@ -20534,7 +20690,8 @@ ${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : "
|
|
|
20534
20690
|
}
|
|
20535
20691
|
if (Object.keys(result.newBindings).length > 0) {
|
|
20536
20692
|
Object.assign(led.nodes, result.newBindings);
|
|
20537
|
-
|
|
20693
|
+
mkdirSync4(join10(base, "accounts"), { recursive: true });
|
|
20694
|
+
writeFileSync9(ledgerPath(base), `${JSON.stringify(led, null, 2)}
|
|
20538
20695
|
`);
|
|
20539
20696
|
console.log(`
|
|
20540
20697
|
${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
|
|
@@ -20546,26 +20703,31 @@ ${Object.keys(result.newBindings).length} new bindings recorded — everything c
|
|
|
20546
20703
|
// src/ads-pull.ts
|
|
20547
20704
|
import { createHash as createHash2 } from "node:crypto";
|
|
20548
20705
|
import {
|
|
20549
|
-
existsSync as
|
|
20550
|
-
mkdirSync as
|
|
20551
|
-
readFileSync as
|
|
20552
|
-
writeFileSync as
|
|
20706
|
+
existsSync as existsSync11,
|
|
20707
|
+
mkdirSync as mkdirSync5,
|
|
20708
|
+
readFileSync as readFileSync9,
|
|
20709
|
+
writeFileSync as writeFileSync10
|
|
20553
20710
|
} from "node:fs";
|
|
20554
|
-
import { dirname as dirname2, join as
|
|
20711
|
+
import { dirname as dirname2, join as join11, resolve as resolve8 } from "node:path";
|
|
20555
20712
|
var sha = (s) => createHash2("sha256").update(s).digest("hex");
|
|
20556
20713
|
function checkoutRoot() {
|
|
20557
20714
|
const cwd = resolve8(".");
|
|
20558
|
-
if (
|
|
20715
|
+
if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "accounts")) || existsSync11(join11(cwd, "campaigns"))) {
|
|
20559
20716
|
return cwd;
|
|
20560
20717
|
}
|
|
20561
20718
|
throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
|
|
20562
20719
|
}
|
|
20563
20720
|
function readBindings(root2) {
|
|
20564
|
-
const path9
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
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: {} };
|
|
20569
20731
|
}
|
|
20570
20732
|
async function adsPull(opts) {
|
|
20571
20733
|
const auth = requireActive();
|
|
@@ -20592,11 +20754,11 @@ async function adsPull(opts) {
|
|
|
20592
20754
|
let unchanged = 0;
|
|
20593
20755
|
const skipped = [];
|
|
20594
20756
|
for (const f of result.files) {
|
|
20595
|
-
const abs =
|
|
20757
|
+
const abs = join11(root2, f.path);
|
|
20596
20758
|
const nextHash = sha(f.yaml);
|
|
20597
20759
|
const lastPulled = bindings.pulled[f.path];
|
|
20598
|
-
if (
|
|
20599
|
-
const current =
|
|
20760
|
+
if (existsSync11(abs)) {
|
|
20761
|
+
const current = readFileSync9(abs, "utf8");
|
|
20600
20762
|
if (sha(current) === nextHash) {
|
|
20601
20763
|
unchanged += 1;
|
|
20602
20764
|
bindings.pulled[f.path] = nextHash;
|
|
@@ -20608,18 +20770,18 @@ async function adsPull(opts) {
|
|
|
20608
20770
|
continue;
|
|
20609
20771
|
}
|
|
20610
20772
|
}
|
|
20611
|
-
|
|
20612
|
-
|
|
20773
|
+
mkdirSync5(dirname2(abs), { recursive: true });
|
|
20774
|
+
writeFileSync10(abs, f.yaml);
|
|
20613
20775
|
bindings.pulled[f.path] = nextHash;
|
|
20614
20776
|
written += 1;
|
|
20615
20777
|
}
|
|
20616
20778
|
Object.assign(bindings.nodes, result.bindings);
|
|
20617
|
-
const bindingsPath =
|
|
20618
|
-
|
|
20619
|
-
|
|
20779
|
+
const bindingsPath = join11(root2, "accounts", ".bindings.json");
|
|
20780
|
+
mkdirSync5(dirname2(bindingsPath), { recursive: true });
|
|
20781
|
+
writeFileSync10(bindingsPath, `${JSON.stringify(bindings, null, 2)}
|
|
20620
20782
|
`);
|
|
20621
20783
|
const exported = new Set(result.files.map((f) => f.path));
|
|
20622
|
-
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) &&
|
|
20784
|
+
const gone = Object.keys(bindings.pulled).filter((p) => !exported.has(p) && existsSync11(join11(root2, p)));
|
|
20623
20785
|
const s = result.summary;
|
|
20624
20786
|
console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
|
|
20625
20787
|
for (const p of skipped)
|
|
@@ -20640,8 +20802,8 @@ Review with git diff; commit when it reads right.`);
|
|
|
20640
20802
|
|
|
20641
20803
|
// src/ads-read.ts
|
|
20642
20804
|
import { spawn as spawn4 } from "node:child_process";
|
|
20643
|
-
import { existsSync as
|
|
20644
|
-
import { join as
|
|
20805
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
20806
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
20645
20807
|
async function serviceGet(auth, path9) {
|
|
20646
20808
|
const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
|
|
20647
20809
|
headers: {
|
|
@@ -20730,11 +20892,11 @@ account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
|
|
|
20730
20892
|
}
|
|
20731
20893
|
function adsDigest(dir2) {
|
|
20732
20894
|
const root2 = resolve9(dir2 ?? ".");
|
|
20733
|
-
const path9 =
|
|
20734
|
-
if (!
|
|
20895
|
+
const path9 = join12(root2, "signals", "latest.json");
|
|
20896
|
+
if (!existsSync12(path9)) {
|
|
20735
20897
|
throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
|
|
20736
20898
|
}
|
|
20737
|
-
const d = JSON.parse(
|
|
20899
|
+
const d = JSON.parse(readFileSync10(path9, "utf8"));
|
|
20738
20900
|
console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
|
|
20739
20901
|
const t = d.totals;
|
|
20740
20902
|
console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
|
|
@@ -20780,19 +20942,19 @@ async function adsDoctor(dir2) {
|
|
|
20780
20942
|
console.log(` ✗ ${msg}`);
|
|
20781
20943
|
failed += 1;
|
|
20782
20944
|
};
|
|
20783
|
-
const linkPath =
|
|
20945
|
+
const linkPath = join12(root2, ".zalify", "brand-repo.json");
|
|
20784
20946
|
let link = null;
|
|
20785
|
-
if (
|
|
20786
|
-
link = JSON.parse(
|
|
20947
|
+
if (existsSync12(linkPath)) {
|
|
20948
|
+
link = JSON.parse(readFileSync10(linkPath, "utf8"));
|
|
20787
20949
|
ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
|
|
20788
20950
|
} else {
|
|
20789
20951
|
console.log(" - not inside a brand-instance checkout (repo checks only)");
|
|
20790
20952
|
}
|
|
20791
20953
|
let identity2 = null;
|
|
20792
|
-
const identityPath =
|
|
20793
|
-
if (
|
|
20954
|
+
const identityPath = join12(root2, "identity.yaml");
|
|
20955
|
+
if (existsSync12(identityPath)) {
|
|
20794
20956
|
try {
|
|
20795
|
-
identity2 = $parse(
|
|
20957
|
+
identity2 = $parse(readFileSync10(identityPath, "utf8"));
|
|
20796
20958
|
ok("identity.yaml parses");
|
|
20797
20959
|
if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
|
|
20798
20960
|
bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
|
|
@@ -20858,8 +21020,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
|
|
|
20858
21020
|
}
|
|
20859
21021
|
|
|
20860
21022
|
// src/brand.ts
|
|
20861
|
-
import { existsSync as
|
|
20862
|
-
import { basename as basename5, join as
|
|
21023
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
21024
|
+
import { basename as basename5, join as join13, resolve as resolve10 } from "node:path";
|
|
20863
21025
|
var AUTHORING_MD = `# Brand authoring guide
|
|
20864
21026
|
|
|
20865
21027
|
The rules for filling in this folder. An author — human or AI agent —
|
|
@@ -21162,17 +21324,17 @@ var MANIFEST_JSON = JSON.stringify({
|
|
|
21162
21324
|
function brandInit(dir2 = ".", opts = {}) {
|
|
21163
21325
|
const target = resolve10(dir2);
|
|
21164
21326
|
const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
21165
|
-
if (
|
|
21327
|
+
if (existsSync13(target) && readdirSync6(target).length > 0) {
|
|
21166
21328
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
21167
21329
|
}
|
|
21168
21330
|
const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
21169
21331
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21332
|
+
mkdirSync6(join13(target, "images"), { recursive: true });
|
|
21333
|
+
writeFileSync11(join13(target, "AUTHORING.md"), AUTHORING_MD);
|
|
21334
|
+
writeFileSync11(join13(target, "brand.md"), BRAND_MD(name));
|
|
21335
|
+
writeFileSync11(join13(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
|
|
21336
|
+
writeFileSync11(join13(target, "catalog.json"), CATALOG_JSON(name, currency));
|
|
21337
|
+
writeFileSync11(join13(target, "images", "manifest.json"), MANIFEST_JSON);
|
|
21176
21338
|
console.log(`Scaffolded brand data in ${dir2}:`);
|
|
21177
21339
|
for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
|
|
21178
21340
|
console.log(` + ${f}`);
|
|
@@ -21187,19 +21349,19 @@ function brandValidate(dir2 = ".") {
|
|
|
21187
21349
|
const problems = [];
|
|
21188
21350
|
const warnings = [];
|
|
21189
21351
|
const readJson2 = (rel) => {
|
|
21190
|
-
const path9 =
|
|
21191
|
-
if (!
|
|
21352
|
+
const path9 = join13(target, rel);
|
|
21353
|
+
if (!existsSync13(path9)) {
|
|
21192
21354
|
problems.push(`missing ${rel}`);
|
|
21193
21355
|
return null;
|
|
21194
21356
|
}
|
|
21195
21357
|
try {
|
|
21196
|
-
return JSON.parse(
|
|
21358
|
+
return JSON.parse(readFileSync11(path9, "utf8"));
|
|
21197
21359
|
} catch (err) {
|
|
21198
21360
|
problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
|
|
21199
21361
|
return null;
|
|
21200
21362
|
}
|
|
21201
21363
|
};
|
|
21202
|
-
if (!
|
|
21364
|
+
if (!existsSync13(join13(target, "brand.md")))
|
|
21203
21365
|
problems.push("missing brand.md");
|
|
21204
21366
|
const store = readJson2("store.json");
|
|
21205
21367
|
if (store && (!store.name || !store.slug))
|
|
@@ -21375,7 +21537,7 @@ function brandValidate(dir2 = ".") {
|
|
|
21375
21537
|
// src/cli.ts
|
|
21376
21538
|
var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
|
|
21377
21539
|
var require2 = createRequire2(import.meta.url);
|
|
21378
|
-
var pkg = require2(
|
|
21540
|
+
var pkg = require2(join14(__dirname4, "..", "package.json"));
|
|
21379
21541
|
try {
|
|
21380
21542
|
updateNotifier({ pkg }).notify({
|
|
21381
21543
|
isGlobal: true,
|
|
@@ -21457,7 +21619,7 @@ ads.command("list", { isDefault: true }).description("List the workspace's brand
|
|
|
21457
21619
|
ads.command("remove <slug>").description("Deprecate a brand instance (record soft-deleted; the GitHub repo is left untouched)").action(async (slug) => {
|
|
21458
21620
|
await adsRemove(slug);
|
|
21459
21621
|
});
|
|
21460
|
-
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) => {
|
|
21461
21623
|
await adsNew(name, options);
|
|
21462
21624
|
});
|
|
21463
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) => {
|
|
@@ -21488,6 +21650,10 @@ adsPlan.command("show <id>").description("Print one plan's content (id, or slug@
|
|
|
21488
21650
|
ads.command("open").description("Open the Structure canvas for the active workspace").action(() => {
|
|
21489
21651
|
adsOpen();
|
|
21490
21652
|
});
|
|
21653
|
+
var adsCreative = ads.command("creative").description("Creatives for folder campaigns — generated through the canvas pipeline");
|
|
21654
|
+
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) => {
|
|
21655
|
+
await adsCreativeGenerate({ campaign, ...options });
|
|
21656
|
+
});
|
|
21491
21657
|
ads.command("doctor [dir]").description("Health checks that have each broken once: slug alignment, consent drift, timezone match, credential expiry").action(async (dir2) => {
|
|
21492
21658
|
await adsDoctor(dir2);
|
|
21493
21659
|
});
|