@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +193 -76
  3. 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 diff` | Desired (your campaign files) vs actual (the ingested account), keyed through bindings: bound nodes compare budget/status, unbound nodes become creations. Read-only |
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 join13 } from "node:path";
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 = imagesDirFor(storeDir);
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) => f.endsWith(".png")).map((name) => {
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 existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
20343
- import { join as join9, resolve as resolve7 } from "node:path";
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 (existsSync9(join9(cwd, ".zalify", "brand-repo.json")) || existsSync9(join9(cwd, "campaigns"))) {
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 = join9(base, "campaigns");
20402
- if (!existsSync9(dir2))
20403
- return { files: [], folders: 0 };
20511
+ async function collect(base) {
20512
+ const dir2 = join10(base, "campaigns");
20513
+ if (!existsSync10(dir2))
20514
+ return { files: [] };
20404
20515
  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;
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(readFileSync7(join9(base, path9), "utf8")) });
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
- return { files, folders };
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 = join9(base, "campaigns", ".bindings.json");
20426
- if (!existsSync9(path9))
20535
+ const path9 = join10(base, "campaigns", ".bindings.json");
20536
+ if (!existsSync10(path9))
20427
20537
  return { nodes: {}, pulled: {} };
20428
- const raw = JSON.parse(readFileSync7(path9, "utf8"));
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, folders) {
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` : ""}` + `${folders ? ` (${folders} folder campaigns skipped — sync is coming)` : ""}`);
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, folders } = collect(base);
20473
- if (files.length === 0 && folders === 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, folders);
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, folders } = collect(base);
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, folders);
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
- writeFileSync8(join9(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
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 existsSync10,
20659
+ existsSync as existsSync11,
20547
20660
  mkdirSync as mkdirSync4,
20548
- readFileSync as readFileSync8,
20549
- writeFileSync as writeFileSync9
20661
+ readFileSync as readFileSync9,
20662
+ writeFileSync as writeFileSync10
20550
20663
  } from "node:fs";
20551
- import { dirname as dirname2, join as join10, resolve as resolve8 } from "node:path";
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 (existsSync10(join10(cwd, ".zalify", "brand-repo.json")) || existsSync10(join10(cwd, "campaigns"))) {
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 = join10(root2, "campaigns", ".bindings.json");
20562
- if (!existsSync10(path9))
20674
+ const path9 = join11(root2, "campaigns", ".bindings.json");
20675
+ if (!existsSync11(path9))
20563
20676
  return { nodes: {}, pulled: {} };
20564
- const raw = JSON.parse(readFileSync8(path9, "utf8"));
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 = join10(root2, f.path);
20705
+ const abs = join11(root2, f.path);
20593
20706
  const nextHash = sha(f.yaml);
20594
20707
  const lastPulled = bindings.pulled[f.path];
20595
- if (existsSync10(abs)) {
20596
- const current = readFileSync8(abs, "utf8");
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
- writeFileSync9(abs, f.yaml);
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 = join10(root2, "campaigns", ".bindings.json");
20727
+ const bindingsPath = join11(root2, "campaigns", ".bindings.json");
20615
20728
  mkdirSync4(dirname2(bindingsPath), { recursive: true });
20616
- writeFileSync9(bindingsPath, `${JSON.stringify(bindings, null, 2)}
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) && existsSync10(join10(root2, 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 existsSync11, readFileSync as readFileSync9 } from "node:fs";
20641
- import { join as join11, resolve as resolve9 } from "node:path";
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 = join11(root2, "signals", "latest.json");
20731
- if (!existsSync11(path9)) {
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(readFileSync9(path9, "utf8"));
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 = join11(root2, ".zalify", "brand-repo.json");
20893
+ const linkPath = join12(root2, ".zalify", "brand-repo.json");
20781
20894
  let link = null;
20782
- if (existsSync11(linkPath)) {
20783
- link = JSON.parse(readFileSync9(linkPath, "utf8"));
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 = join11(root2, "identity.yaml");
20790
- if (existsSync11(identityPath)) {
20902
+ const identityPath = join12(root2, "identity.yaml");
20903
+ if (existsSync12(identityPath)) {
20791
20904
  try {
20792
- identity2 = $parse(readFileSync9(identityPath, "utf8"));
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 existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
20859
- import { basename as basename5, join as join12, resolve as resolve10 } from "node:path";
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 (existsSync12(target) && readdirSync4(target).length > 0) {
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(join12(target, "images"), { recursive: true });
21168
- writeFileSync10(join12(target, "AUTHORING.md"), AUTHORING_MD);
21169
- writeFileSync10(join12(target, "brand.md"), BRAND_MD(name));
21170
- writeFileSync10(join12(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
21171
- writeFileSync10(join12(target, "catalog.json"), CATALOG_JSON(name, currency));
21172
- writeFileSync10(join12(target, "images", "manifest.json"), MANIFEST_JSON);
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 = join12(target, rel);
21188
- if (!existsSync12(path9)) {
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(readFileSync10(path9, "utf8"));
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 (!existsSync12(join12(target, "brand.md")))
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(join13(__dirname4, "..", "package.json"));
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",