@zalify/cli 0.19.0 → 0.20.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 +1 -0
  2. package/dist/cli.js +69 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,6 +101,7 @@ 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 |
104
105
  | `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 |
105
106
  | `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 |
106
107
  | `ads report [--date <YYYY-MM-DD>] [--platform <all\|meta\|google>]` | The daily report in the terminal — totals vs the day before, where the money moved, what changed in the account |
package/dist/cli.js CHANGED
@@ -20118,8 +20118,9 @@ async function shopifyUploadImages(storeDir, options = {}) {
20118
20118
 
20119
20119
  // src/ads.ts
20120
20120
  import { spawnSync as spawnSync4 } from "node:child_process";
20121
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
20121
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
20122
20122
  import { basename as basename4, join as join8, resolve as resolve6 } from "node:path";
20123
+ import { createInterface } from "node:readline/promises";
20123
20124
  function apiError3(status, json) {
20124
20125
  if (json.code === "SLUG_TAKEN") {
20125
20126
  return new Error(`${json.error}
@@ -20255,6 +20256,62 @@ Next steps:
20255
20256
  ` + ` - flip consent: true once the brand owner has signed off
20256
20257
  ` + ` - the Autopilot service picks the instance up from there`);
20257
20258
  }
20259
+ var CAMPAIGN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
20260
+ function campaignsDir() {
20261
+ const cwd = resolve6(".");
20262
+ if (existsSync8(join8(cwd, "campaigns")))
20263
+ return join8(cwd, "campaigns");
20264
+ if (existsSync8(join8(cwd, ".zalify", "brand-repo.json"))) {
20265
+ return join8(cwd, "campaigns");
20266
+ }
20267
+ throw new Error("Not inside a brand-instance checkout — run from the repo `zalify ads create` made.");
20268
+ }
20269
+ async function ask(question, fallback) {
20270
+ if (!process.stdin.isTTY)
20271
+ return fallback;
20272
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
20273
+ const answer = (await rl.question(`${question} [${fallback}]: `)).trim();
20274
+ rl.close();
20275
+ return answer || fallback;
20276
+ }
20277
+ async function adsNew(name, opts) {
20278
+ if (!CAMPAIGN_NAME_RE.test(name) || name.length > 63) {
20279
+ throw new Error(`"${name}" — campaign names are lowercase letters, digits, and inner hyphens.`);
20280
+ }
20281
+ const dir2 = join8(campaignsDir(), name);
20282
+ if (existsSync8(dir2))
20283
+ throw new Error(`campaigns/${name}/ already exists.`);
20284
+ const objective = opts.objective ?? await ask("objective (sales/leads/traffic/awareness)", "sales");
20285
+ const budget = opts.budget ?? await ask("budget", "$20/day");
20286
+ const country = opts.country ?? await ask("country", "US");
20287
+ const link = opts.link ?? "";
20288
+ mkdirSync3(join8(dir2, "creatives"), { recursive: true });
20289
+ writeFileSync7(join8(dir2, "creatives", ".gitkeep"), "");
20290
+ writeFileSync7(join8(dir2, "campaign.yaml"), `# ${name} — drop images into creatives/, fill the copy below, then
20291
+ # \`zalify ads sync\`. Each file becomes an ad named after itself; give
20292
+ # one ad its own copy with creatives/<file>.copy.yaml.
20293
+ campaign: ${name}
20294
+ objective: ${objective}
20295
+ budget: ${budget} # "$60/day" or "$900 total"
20296
+
20297
+ # Copy every derived ad inherits.
20298
+ creative_defaults:
20299
+ primary_text: ""${link ? "" : " # ← fill me"}
20300
+ link: ${link || '"" # ← your product URL'}
20301
+ cta: shop_now
20302
+
20303
+ ad_sets:
20304
+ - name: main
20305
+ audience:
20306
+ countries: [${country.split(",").map((c) => c.trim().toUpperCase()).filter(Boolean).join(", ")}]
20307
+ age: 18-65
20308
+ advantage: true
20309
+ `);
20310
+ console.log(`✓ campaigns/${name}/ is ready`);
20311
+ console.log(" 1. drag images into creatives/");
20312
+ console.log(" 2. fill primary_text and link in campaign.yaml");
20313
+ console.log(" 3. `zalify ads sync` when it lands — validate, diff, apply");
20314
+ }
20258
20315
  async function adsRemove(slug) {
20259
20316
  const auth = requireActive();
20260
20317
  const json = await request2(auth, "GET", "/api/repos");
@@ -20283,7 +20340,7 @@ async function adsList() {
20283
20340
 
20284
20341
  // src/ads-read.ts
20285
20342
  import { spawn as spawn4 } from "node:child_process";
20286
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
20343
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
20287
20344
  import { join as join9, resolve as resolve7 } from "node:path";
20288
20345
 
20289
20346
  // node_modules/yaml/dist/index.js
@@ -20422,7 +20479,7 @@ account changes: ${a.byPerson} by people, ${a.byMeta} automated`);
20422
20479
  function adsDigest(dir2) {
20423
20480
  const root = resolve7(dir2 ?? ".");
20424
20481
  const path9 = join9(root, "signals", "latest.json");
20425
- if (!existsSync8(path9)) {
20482
+ if (!existsSync9(path9)) {
20426
20483
  throw new Error("signals/latest.json not found — run from a brand-instance checkout " + "(and pull: the service commits a digest daily).");
20427
20484
  }
20428
20485
  const d = JSON.parse(readFileSync7(path9, "utf8"));
@@ -20473,7 +20530,7 @@ async function adsDoctor(dir2) {
20473
20530
  };
20474
20531
  const linkPath = join9(root, ".zalify", "brand-repo.json");
20475
20532
  let link = null;
20476
- if (existsSync8(linkPath)) {
20533
+ if (existsSync9(linkPath)) {
20477
20534
  link = JSON.parse(readFileSync7(linkPath, "utf8"));
20478
20535
  ok(`checkout linked to ${link?.repo} (slug ${link?.slug})`);
20479
20536
  } else {
@@ -20481,7 +20538,7 @@ async function adsDoctor(dir2) {
20481
20538
  }
20482
20539
  let identity2 = null;
20483
20540
  const identityPath = join9(root, "identity.yaml");
20484
- if (existsSync8(identityPath)) {
20541
+ if (existsSync9(identityPath)) {
20485
20542
  try {
20486
20543
  identity2 = $parse(readFileSync7(identityPath, "utf8"));
20487
20544
  ok("identity.yaml parses");
@@ -20549,7 +20606,7 @@ ${failed} problem${failed > 1 ? "s" : ""}.`);
20549
20606
  }
20550
20607
 
20551
20608
  // src/brand.ts
20552
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
20609
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
20553
20610
  import { basename as basename5, join as join10, resolve as resolve8 } from "node:path";
20554
20611
  var AUTHORING_MD = `# Brand authoring guide
20555
20612
 
@@ -20853,7 +20910,7 @@ var MANIFEST_JSON = JSON.stringify({
20853
20910
  function brandInit(dir2 = ".", opts = {}) {
20854
20911
  const target = resolve8(dir2);
20855
20912
  const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
20856
- if (existsSync9(target) && readdirSync3(target).length > 0) {
20913
+ if (existsSync10(target) && readdirSync3(target).length > 0) {
20857
20914
  throw new Error(`${dir2} already exists and is not empty`);
20858
20915
  }
20859
20916
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -20879,7 +20936,7 @@ function brandValidate(dir2 = ".") {
20879
20936
  const warnings = [];
20880
20937
  const readJson2 = (rel) => {
20881
20938
  const path9 = join10(target, rel);
20882
- if (!existsSync9(path9)) {
20939
+ if (!existsSync10(path9)) {
20883
20940
  problems.push(`missing ${rel}`);
20884
20941
  return null;
20885
20942
  }
@@ -20890,7 +20947,7 @@ function brandValidate(dir2 = ".") {
20890
20947
  return null;
20891
20948
  }
20892
20949
  };
20893
- if (!existsSync9(join10(target, "brand.md")))
20950
+ if (!existsSync10(join10(target, "brand.md")))
20894
20951
  problems.push("missing brand.md");
20895
20952
  const store = readJson2("store.json");
20896
20953
  if (store && (!store.name || !store.slug))
@@ -21148,6 +21205,9 @@ ads.command("list", { isDefault: true }).description("List the workspace's brand
21148
21205
  ads.command("remove <slug>").description("Deprecate a brand instance (record soft-deleted; the GitHub repo is left untouched)").action(async (slug) => {
21149
21206
  await adsRemove(slug);
21150
21207
  });
21208
+ 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) => {
21209
+ await adsNew(name, options);
21210
+ });
21151
21211
  ads.command("status").description("The brand instance at a glance: access, ingestion freshness, plans, pending proposals").action(async () => {
21152
21212
  await adsStatus();
21153
21213
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",