@zalify/cli 0.22.1 → 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 +190 -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,7 @@ 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) {
20447
20557
  for (const n of result.notes ?? []) {
20448
20558
  console.log(`· ${n.file} — ${n.message}`);
20449
20559
  }
@@ -20467,13 +20577,13 @@ function renderPlan(result, folders) {
20467
20577
  }
20468
20578
  const s = result.summary;
20469
20579
  console.log(`
20470
- ${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}` + `${folders ? ` (${folders} folder campaigns skipped — sync is coming)` : ""}`);
20580
+ ${s.creates} to create, ${s.updates} to update, ${s.unchanged} unchanged` + `${result.issues.length ? `, ${result.issues.length} files invalid` : ""}`);
20471
20581
  }
20472
20582
  async function adsDiff() {
20473
20583
  const auth = requireActive();
20474
20584
  const base = root();
20475
- const { files, folders } = collect(base);
20476
- if (files.length === 0 && folders === 0) {
20585
+ const { files } = await collect(base);
20586
+ if (files.length === 0) {
20477
20587
  console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
20478
20588
  return;
20479
20589
  }
@@ -20482,14 +20592,14 @@ async function adsDiff() {
20482
20592
  files,
20483
20593
  bindings: led.nodes
20484
20594
  });
20485
- renderPlan(result, folders);
20595
+ renderPlan(result);
20486
20596
  if (result.issues.length > 0)
20487
20597
  process.exitCode = 1;
20488
20598
  }
20489
20599
  async function adsApply(opts) {
20490
20600
  const auth = requireActive();
20491
20601
  const base = root();
20492
- const { files, folders } = collect(base);
20602
+ const { files } = await collect(base);
20493
20603
  if (files.length === 0) {
20494
20604
  console.log("No campaign files — `zalify ads pull` or `zalify ads new` first.");
20495
20605
  return;
@@ -20499,7 +20609,7 @@ async function adsApply(opts) {
20499
20609
  files,
20500
20610
  bindings: led.nodes
20501
20611
  });
20502
- renderPlan(diff, folders);
20612
+ renderPlan(diff);
20503
20613
  if (diff.issues.length > 0) {
20504
20614
  throw new Error("Fix the invalid files before applying.");
20505
20615
  }
@@ -20534,7 +20644,7 @@ ${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : "
20534
20644
  }
20535
20645
  if (Object.keys(result.newBindings).length > 0) {
20536
20646
  Object.assign(led.nodes, result.newBindings);
20537
- writeFileSync8(join9(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
20647
+ writeFileSync9(join10(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
20538
20648
  `);
20539
20649
  console.log(`
20540
20650
  ${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
@@ -20546,25 +20656,25 @@ ${Object.keys(result.newBindings).length} new bindings recorded — everything c
20546
20656
  // src/ads-pull.ts
20547
20657
  import { createHash as createHash2 } from "node:crypto";
20548
20658
  import {
20549
- existsSync as existsSync10,
20659
+ existsSync as existsSync11,
20550
20660
  mkdirSync as mkdirSync4,
20551
- readFileSync as readFileSync8,
20552
- writeFileSync as writeFileSync9
20661
+ readFileSync as readFileSync9,
20662
+ writeFileSync as writeFileSync10
20553
20663
  } from "node:fs";
20554
- 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";
20555
20665
  var sha = (s) => createHash2("sha256").update(s).digest("hex");
20556
20666
  function checkoutRoot() {
20557
20667
  const cwd = resolve8(".");
20558
- 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"))) {
20559
20669
  return cwd;
20560
20670
  }
20561
20671
  throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
20562
20672
  }
20563
20673
  function readBindings(root2) {
20564
- const path9 = join10(root2, "campaigns", ".bindings.json");
20565
- if (!existsSync10(path9))
20674
+ const path9 = join11(root2, "campaigns", ".bindings.json");
20675
+ if (!existsSync11(path9))
20566
20676
  return { nodes: {}, pulled: {} };
20567
- const raw = JSON.parse(readFileSync8(path9, "utf8"));
20677
+ const raw = JSON.parse(readFileSync9(path9, "utf8"));
20568
20678
  return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
20569
20679
  }
20570
20680
  async function adsPull(opts) {
@@ -20592,11 +20702,11 @@ async function adsPull(opts) {
20592
20702
  let unchanged = 0;
20593
20703
  const skipped = [];
20594
20704
  for (const f of result.files) {
20595
- const abs = join10(root2, f.path);
20705
+ const abs = join11(root2, f.path);
20596
20706
  const nextHash = sha(f.yaml);
20597
20707
  const lastPulled = bindings.pulled[f.path];
20598
- if (existsSync10(abs)) {
20599
- const current = readFileSync8(abs, "utf8");
20708
+ if (existsSync11(abs)) {
20709
+ const current = readFileSync9(abs, "utf8");
20600
20710
  if (sha(current) === nextHash) {
20601
20711
  unchanged += 1;
20602
20712
  bindings.pulled[f.path] = nextHash;
@@ -20609,17 +20719,17 @@ async function adsPull(opts) {
20609
20719
  }
20610
20720
  }
20611
20721
  mkdirSync4(dirname2(abs), { recursive: true });
20612
- writeFileSync9(abs, f.yaml);
20722
+ writeFileSync10(abs, f.yaml);
20613
20723
  bindings.pulled[f.path] = nextHash;
20614
20724
  written += 1;
20615
20725
  }
20616
20726
  Object.assign(bindings.nodes, result.bindings);
20617
- const bindingsPath = join10(root2, "campaigns", ".bindings.json");
20727
+ const bindingsPath = join11(root2, "campaigns", ".bindings.json");
20618
20728
  mkdirSync4(dirname2(bindingsPath), { recursive: true });
20619
- writeFileSync9(bindingsPath, `${JSON.stringify(bindings, null, 2)}
20729
+ writeFileSync10(bindingsPath, `${JSON.stringify(bindings, null, 2)}
20620
20730
  `);
20621
20731
  const exported = new Set(result.files.map((f) => f.path));
20622
- 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)));
20623
20733
  const s = result.summary;
20624
20734
  console.log(`pulled ${s.campaigns} campaigns (${s.adSets} ad sets, ${s.ads} ads): ` + `${written} written, ${unchanged} unchanged, ${skipped.length} kept (locally edited)`);
20625
20735
  for (const p of skipped)
@@ -20640,8 +20750,8 @@ Review with git diff; commit when it reads right.`);
20640
20750
 
20641
20751
  // src/ads-read.ts
20642
20752
  import { spawn as spawn4 } from "node:child_process";
20643
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
20644
- 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";
20645
20755
  async function serviceGet(auth, path9) {
20646
20756
  const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
20647
20757
  headers: {
@@ -20730,11 +20840,11 @@ account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
20730
20840
  }
20731
20841
  function adsDigest(dir2) {
20732
20842
  const root2 = resolve9(dir2 ?? ".");
20733
- const path9 = join11(root2, "signals", "latest.json");
20734
- if (!existsSync11(path9)) {
20843
+ const path9 = join12(root2, "signals", "latest.json");
20844
+ if (!existsSync12(path9)) {
20735
20845
  throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
20736
20846
  }
20737
- const d = JSON.parse(readFileSync9(path9, "utf8"));
20847
+ const d = JSON.parse(readFileSync10(path9, "utf8"));
20738
20848
  console.log(`digest of ${d.window.days} days (${d.window.first_date ?? "—"} … ${d.window.last_date ?? "—"}), generated ${when(d.generated_at)}`);
20739
20849
  const t = d.totals;
20740
20850
  console.log(` spend ${money(t.spend)} purchases ${num(t.purchases)} roas ${t.platform_roas ?? "—"}`);
@@ -20780,19 +20890,19 @@ async function adsDoctor(dir2) {
20780
20890
  console.log(` ✗ ${msg}`);
20781
20891
  failed += 1;
20782
20892
  };
20783
- const linkPath = join11(root2, ".zalify", "brand-repo.json");
20893
+ const linkPath = join12(root2, ".zalify", "brand-repo.json");
20784
20894
  let link = null;
20785
- if (existsSync11(linkPath)) {
20786
- link = JSON.parse(readFileSync9(linkPath, "utf8"));
20895
+ if (existsSync12(linkPath)) {
20896
+ link = JSON.parse(readFileSync10(linkPath, "utf8"));
20787
20897
  ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
20788
20898
  } else {
20789
20899
  console.log(" - not inside a brand-instance checkout (repo checks only)");
20790
20900
  }
20791
20901
  let identity2 = null;
20792
- const identityPath = join11(root2, "identity.yaml");
20793
- if (existsSync11(identityPath)) {
20902
+ const identityPath = join12(root2, "identity.yaml");
20903
+ if (existsSync12(identityPath)) {
20794
20904
  try {
20795
- identity2 = $parse(readFileSync9(identityPath, "utf8"));
20905
+ identity2 = $parse(readFileSync10(identityPath, "utf8"));
20796
20906
  ok("identity.yaml parses");
20797
20907
  if (identity2?.workspace_id && identity2.workspace_id !== auth.workspaceId) {
20798
20908
  bad(`identity.yaml workspace_id (${identity2.workspace_id}) ≠ active workspace (${auth.workspaceId}) — is the CLI on the right workspace?`);
@@ -20858,8 +20968,8 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
20858
20968
  }
20859
20969
 
20860
20970
  // src/brand.ts
20861
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
20862
- import { basename as basename5, join as join12, resolve as resolve10 } from "node:path";
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";
20863
20973
  var AUTHORING_MD = `# Brand authoring guide
20864
20974
 
20865
20975
  The rules for filling in this folder. An author — human or AI agent —
@@ -21162,17 +21272,17 @@ var MANIFEST_JSON = JSON.stringify({
21162
21272
  function brandInit(dir2 = ".", opts = {}) {
21163
21273
  const target = resolve10(dir2);
21164
21274
  const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
21165
- if (existsSync12(target) && readdirSync4(target).length > 0) {
21275
+ if (existsSync13(target) && readdirSync5(target).length > 0) {
21166
21276
  throw new Error(`${dir2} already exists and is not empty`);
21167
21277
  }
21168
21278
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
21169
21279
  const currency = (opts.currency ?? "USD").toUpperCase();
21170
- mkdirSync5(join12(target, "images"), { recursive: true });
21171
- writeFileSync10(join12(target, "AUTHORING.md"), AUTHORING_MD);
21172
- writeFileSync10(join12(target, "brand.md"), BRAND_MD(name));
21173
- writeFileSync10(join12(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
21174
- writeFileSync10(join12(target, "catalog.json"), CATALOG_JSON(name, currency));
21175
- writeFileSync10(join12(target, "images", "manifest.json"), MANIFEST_JSON);
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);
21176
21286
  console.log(`Scaffolded brand data in ${dir2}:`);
21177
21287
  for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
21178
21288
  console.log(` + ${f}`);
@@ -21187,19 +21297,19 @@ function brandValidate(dir2 = ".") {
21187
21297
  const problems = [];
21188
21298
  const warnings = [];
21189
21299
  const readJson2 = (rel) => {
21190
- const path9 = join12(target, rel);
21191
- if (!existsSync12(path9)) {
21300
+ const path9 = join13(target, rel);
21301
+ if (!existsSync13(path9)) {
21192
21302
  problems.push(`missing ${rel}`);
21193
21303
  return null;
21194
21304
  }
21195
21305
  try {
21196
- return JSON.parse(readFileSync10(path9, "utf8"));
21306
+ return JSON.parse(readFileSync11(path9, "utf8"));
21197
21307
  } catch (err) {
21198
21308
  problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
21199
21309
  return null;
21200
21310
  }
21201
21311
  };
21202
- if (!existsSync12(join12(target, "brand.md")))
21312
+ if (!existsSync13(join13(target, "brand.md")))
21203
21313
  problems.push("missing brand.md");
21204
21314
  const store = readJson2("store.json");
21205
21315
  if (store && (!store.name || !store.slug))
@@ -21375,7 +21485,7 @@ function brandValidate(dir2 = ".") {
21375
21485
  // src/cli.ts
21376
21486
  var __dirname4 = dirname3(fileURLToPath3(import.meta.url));
21377
21487
  var require2 = createRequire2(import.meta.url);
21378
- var pkg = require2(join13(__dirname4, "..", "package.json"));
21488
+ var pkg = require2(join14(__dirname4, "..", "package.json"));
21379
21489
  try {
21380
21490
  updateNotifier({ pkg }).notify({
21381
21491
  isGlobal: true,
@@ -21488,6 +21598,10 @@ adsPlan.command("show <id>").description("Print one plan's content (id, or slug@
21488
21598
  ads.command("open").description("Open the Structure canvas for the active workspace").action(() => {
21489
21599
  adsOpen();
21490
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
+ });
21491
21605
  ads.command("doctor [dir]").description("Health checks that have each broken once: slug alignment, consent drift, timezone match, credential expiry").action(async (dir2) => {
21492
21606
  await adsDoctor(dir2);
21493
21607
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.22.1",
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",