@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.
- package/README.md +1 -0
- package/dist/cli.js +69 -9
- 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
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
});
|