@zalify/cli 0.23.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +114 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,8 +101,8 @@ 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 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
- | `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 |
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
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
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 |
108
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 |
package/dist/cli.js CHANGED
@@ -20144,8 +20144,8 @@ async function shopifyUploadImages(storeDir, options = {}) {
20144
20144
 
20145
20145
  // src/ads.ts
20146
20146
  import { spawnSync as spawnSync4 } from "node:child_process";
20147
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
20148
- 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";
20149
20149
  import { createInterface } from "node:readline/promises";
20150
20150
  function apiError3(status, json) {
20151
20151
  if (json.code === "SLUG_TAKEN") {
@@ -20283,14 +20283,22 @@ Next steps:
20283
20283
  ` + ` - the Autopilot service picks the instance up from there`);
20284
20284
  }
20285
20285
  var CAMPAIGN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
20286
- function campaignsDir() {
20286
+ function campaignParent(account) {
20287
20287
  const cwd = resolve6(".");
20288
- if (existsSync8(join8(cwd, "campaigns")))
20289
- return join8(cwd, "campaigns");
20290
- if (existsSync8(join8(cwd, ".zalify", "brand-repo.json"))) {
20291
- return join8(cwd, "campaigns");
20292
- }
20293
- throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
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>.`);
20294
20302
  }
20295
20303
  async function ask(question, fallback) {
20296
20304
  if (!process.stdin.isTTY)
@@ -20304,9 +20312,10 @@ async function adsNew(name, opts) {
20304
20312
  if (!CAMPAIGN_NAME_RE.test(name) || name.length > 63) {
20305
20313
  throw new Error(`"${name}" — campaign names are lowercase letters, digits, and inner hyphens.`);
20306
20314
  }
20307
- const dir2 = join8(campaignsDir(), name);
20315
+ const parent = campaignParent(opts.account);
20316
+ const dir2 = join8(parent, name);
20308
20317
  if (existsSync8(dir2))
20309
- throw new Error(`campaigns/${name}/ already exists.`);
20318
+ throw new Error(`${dir2} already exists.`);
20310
20319
  const objective = opts.objective ?? await ask("objective (sales/leads/traffic/awareness)", "sales");
20311
20320
  const budget = opts.budget ?? await ask("budget", "$20/day");
20312
20321
  const country = opts.country ?? await ask("country", "US");
@@ -20333,7 +20342,7 @@ ad_sets:
20333
20342
  age: 18-65
20334
20343
  advantage: true
20335
20344
  `);
20336
- console.log(`✓ campaigns/${name}/ is ready`);
20345
+ console.log(`✓ ${relative(resolve6("."), dir2)}/ is ready`);
20337
20346
  console.log(" 1. drag images into creatives/");
20338
20347
  console.log(" 2. fill primary_text and link in campaign.yaml");
20339
20348
  console.log(" 3. `zalify ads sync` when it lands — validate, diff, apply");
@@ -20365,7 +20374,13 @@ async function adsList() {
20365
20374
  }
20366
20375
 
20367
20376
  // src/ads-diff.ts
20368
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "node:fs";
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";
20369
20384
  import { join as join10, resolve as resolve7 } from "node:path";
20370
20385
  import { createInterface as createInterface2 } from "node:readline/promises";
20371
20386
 
@@ -20416,11 +20431,36 @@ var $visit = visit.visit;
20416
20431
  var $visitAsync = visit.visitAsync;
20417
20432
 
20418
20433
  // src/ads-sync.ts
20419
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
20434
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
20420
20435
  import { join as join9 } from "node:path";
20421
20436
  var CREATIVE_EXT = /\.(png|jpe?g|webp)$/i;
20422
- async function collectFolder(base, name) {
20423
- const dir2 = join9(base, "campaigns", name);
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);
20424
20464
  const doc = $parse(readFileSync7(join9(dir2, "campaign.yaml"), "utf8"));
20425
20465
  const creativesDir = join9(dir2, "creatives");
20426
20466
  const sources = [];
@@ -20428,7 +20468,7 @@ async function collectFolder(base, name) {
20428
20468
  const abs = join9(creativesDir, sub);
20429
20469
  if (!existsSync9(abs))
20430
20470
  return;
20431
- const entries = readdirSync3(abs, { withFileTypes: true });
20471
+ const entries = readdirSync4(abs, { withFileTypes: true });
20432
20472
  const hasImages = entries.some((e) => e.isFile() && CREATIVE_EXT.test(e.name));
20433
20473
  const index = hasImages ? await pushImagesIn(abs) : {};
20434
20474
  for (const e of entries) {
@@ -20448,19 +20488,20 @@ async function collectFolder(base, name) {
20448
20488
  }
20449
20489
  };
20450
20490
  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);
20491
+ return { path: `${rel}/campaign.yaml`, doc, sources };
20458
20492
  }
20459
20493
  async function adsCreativeGenerate(opts) {
20460
20494
  const auth = requireActive();
20461
- const dir2 = join9(".", "campaigns", opts.campaign, "creatives");
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");
20462
20503
  if (!existsSync9(dir2)) {
20463
- throw new Error(`campaigns/${opts.campaign}/creatives/ not found — \`zalify ads new ${opts.campaign}\` first.`);
20504
+ throw new Error(`${matches[0]}/creatives/ not found.`);
20464
20505
  }
20465
20506
  console.log("Generating (the canvas pipeline: safe zones + product fidelity)…");
20466
20507
  const res = await fetch(`${auth.appUrl}/api/autopilot/ads-code/creative`, {
@@ -20509,34 +20550,39 @@ function root() {
20509
20550
  throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
20510
20551
  }
20511
20552
  async function collect(base) {
20512
- const dir2 = join10(base, "campaigns");
20513
- if (!existsSync10(dir2))
20514
- return { files: [] };
20515
20553
  const files = [];
20516
- for (const entry of readdirSync4(dir2, { withFileTypes: true })) {
20517
- if (entry.isDirectory())
20518
- continue;
20519
- if (!entry.name.endsWith(".yaml") || entry.name.endsWith(".copy.yaml")) {
20520
- continue;
20521
- }
20522
- const path9 = `campaigns/${entry.name}`;
20523
- try {
20524
- files.push({ path: path9, doc: $parse(readFileSync8(join10(base, path9), "utf8")) });
20525
- } catch (err) {
20526
- 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
+ }
20527
20568
  }
20528
20569
  }
20529
- for (const name of folderCampaigns(base)) {
20530
- files.push(await collectFolder(base, name));
20570
+ for (const rel of campaignDirs(base)) {
20571
+ files.push(await collectFolder(base, rel));
20531
20572
  }
20532
20573
  return { files };
20533
20574
  }
20575
+ function ledgerPath(base) {
20576
+ return join10(base, "accounts", ".bindings.json");
20577
+ }
20534
20578
  function ledger(base) {
20535
- const path9 = join10(base, "campaigns", ".bindings.json");
20536
- if (!existsSync10(path9))
20537
- return { nodes: {}, pulled: {} };
20538
- const raw = JSON.parse(readFileSync8(path9, "utf8"));
20539
- return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
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: {} };
20540
20586
  }
20541
20587
  async function post(auth, path9, payload) {
20542
20588
  const res = await fetch(`${auth.appUrl}/api/autopilot/${path9}`, {
@@ -20644,7 +20690,8 @@ ${result.refusedActivations} activation${result.refusedActivations > 1 ? "s" : "
20644
20690
  }
20645
20691
  if (Object.keys(result.newBindings).length > 0) {
20646
20692
  Object.assign(led.nodes, result.newBindings);
20647
- writeFileSync9(join10(base, "campaigns", ".bindings.json"), `${JSON.stringify(led, null, 2)}
20693
+ mkdirSync4(join10(base, "accounts"), { recursive: true });
20694
+ writeFileSync9(ledgerPath(base), `${JSON.stringify(led, null, 2)}
20648
20695
  `);
20649
20696
  console.log(`
20650
20697
  ${Object.keys(result.newBindings).length} new bindings recorded — everything created is PAUSED.`);
@@ -20657,7 +20704,7 @@ ${Object.keys(result.newBindings).length} new bindings recorded — everything c
20657
20704
  import { createHash as createHash2 } from "node:crypto";
20658
20705
  import {
20659
20706
  existsSync as existsSync11,
20660
- mkdirSync as mkdirSync4,
20707
+ mkdirSync as mkdirSync5,
20661
20708
  readFileSync as readFileSync9,
20662
20709
  writeFileSync as writeFileSync10
20663
20710
  } from "node:fs";
@@ -20665,17 +20712,22 @@ import { dirname as dirname2, join as join11, resolve as resolve8 } from "node:p
20665
20712
  var sha = (s) => createHash2("sha256").update(s).digest("hex");
20666
20713
  function checkoutRoot() {
20667
20714
  const cwd = resolve8(".");
20668
- if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "campaigns"))) {
20715
+ if (existsSync11(join11(cwd, ".zalify", "brand-repo.json")) || existsSync11(join11(cwd, "accounts")) || existsSync11(join11(cwd, "campaigns"))) {
20669
20716
  return cwd;
20670
20717
  }
20671
20718
  throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
20672
20719
  }
20673
20720
  function readBindings(root2) {
20674
- const path9 = join11(root2, "campaigns", ".bindings.json");
20675
- if (!existsSync11(path9))
20676
- return { nodes: {}, pulled: {} };
20677
- const raw = JSON.parse(readFileSync9(path9, "utf8"));
20678
- return { nodes: raw.nodes ?? {}, pulled: raw.pulled ?? {} };
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: {} };
20679
20731
  }
20680
20732
  async function adsPull(opts) {
20681
20733
  const auth = requireActive();
@@ -20718,14 +20770,14 @@ async function adsPull(opts) {
20718
20770
  continue;
20719
20771
  }
20720
20772
  }
20721
- mkdirSync4(dirname2(abs), { recursive: true });
20773
+ mkdirSync5(dirname2(abs), { recursive: true });
20722
20774
  writeFileSync10(abs, f.yaml);
20723
20775
  bindings.pulled[f.path] = nextHash;
20724
20776
  written += 1;
20725
20777
  }
20726
20778
  Object.assign(bindings.nodes, result.bindings);
20727
- const bindingsPath = join11(root2, "campaigns", ".bindings.json");
20728
- mkdirSync4(dirname2(bindingsPath), { recursive: true });
20779
+ const bindingsPath = join11(root2, "accounts", ".bindings.json");
20780
+ mkdirSync5(dirname2(bindingsPath), { recursive: true });
20729
20781
  writeFileSync10(bindingsPath, `${JSON.stringify(bindings, null, 2)}
20730
20782
  `);
20731
20783
  const exported = new Set(result.files.map((f) => f.path));
@@ -20968,7 +21020,7 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
20968
21020
  }
20969
21021
 
20970
21022
  // src/brand.ts
20971
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
21023
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
20972
21024
  import { basename as basename5, join as join13, resolve as resolve10 } from "node:path";
20973
21025
  var AUTHORING_MD = `# Brand authoring guide
20974
21026
 
@@ -21272,12 +21324,12 @@ var MANIFEST_JSON = JSON.stringify({
21272
21324
  function brandInit(dir2 = ".", opts = {}) {
21273
21325
  const target = resolve10(dir2);
21274
21326
  const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
21275
- if (existsSync13(target) && readdirSync5(target).length > 0) {
21327
+ if (existsSync13(target) && readdirSync6(target).length > 0) {
21276
21328
  throw new Error(`${dir2} already exists and is not empty`);
21277
21329
  }
21278
21330
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
21279
21331
  const currency = (opts.currency ?? "USD").toUpperCase();
21280
- mkdirSync5(join13(target, "images"), { recursive: true });
21332
+ mkdirSync6(join13(target, "images"), { recursive: true });
21281
21333
  writeFileSync11(join13(target, "AUTHORING.md"), AUTHORING_MD);
21282
21334
  writeFileSync11(join13(target, "brand.md"), BRAND_MD(name));
21283
21335
  writeFileSync11(join13(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
@@ -21567,7 +21619,7 @@ ads.command("list", { isDefault: true }).description("List the workspace's brand
21567
21619
  ads.command("remove <slug>").description("Deprecate a brand instance (record soft-deleted; the GitHub repo is left untouched)").action(async (slug) => {
21568
21620
  await adsRemove(slug);
21569
21621
  });
21570
- 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) => {
21571
21623
  await adsNew(name, options);
21572
21624
  });
21573
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",