@zalify/cli 0.16.0 → 0.17.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 (2) hide show
  1. package/dist/cli.js +188 -16
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11293,7 +11293,7 @@ function updateNotifier(options) {
11293
11293
 
11294
11294
  // src/cli.ts
11295
11295
  import { createRequire as createRequire2 } from "node:module";
11296
- import { dirname as dirname2, join as join8 } from "node:path";
11296
+ import { dirname as dirname2, join as join9 } from "node:path";
11297
11297
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11298
11298
 
11299
11299
  // src/auth.ts
@@ -11428,9 +11428,20 @@ async function login(options) {
11428
11428
  res.writeHead(404).end();
11429
11429
  return;
11430
11430
  }
11431
+ if (req.method === "OPTIONS") {
11432
+ res.writeHead(204, {
11433
+ "Access-Control-Allow-Origin": "*",
11434
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
11435
+ "Access-Control-Allow-Headers": "*",
11436
+ "Access-Control-Allow-Private-Network": "true",
11437
+ "Access-Control-Max-Age": "600"
11438
+ }).end();
11439
+ return;
11440
+ }
11431
11441
  const done = (message, after) => {
11432
11442
  res.writeHead(200, {
11433
11443
  "Content-Type": "text/html",
11444
+ "Access-Control-Allow-Origin": "*",
11434
11445
  Connection: "close"
11435
11446
  });
11436
11447
  res.end(`<!doctype html><meta charset="utf-8"><title>Zalify CLI</title>
@@ -11490,9 +11501,12 @@ async function login(options) {
11490
11501
  });
11491
11502
  server.listen(0, "127.0.0.1", () => {
11492
11503
  const { port } = server.address();
11493
- const authUrl = `${base}/cli/auth?port=${port}&state=${state}&host=${encodeURIComponent(hostname())}`;
11504
+ const authUrl = `${base}/cli/auth?port=${port}&state=${state}&host=${encodeURIComponent(hostname())}&v=2`;
11505
+ const verificationCode = `${state.slice(0, 4)}-${state.slice(4, 8)}`.toUpperCase();
11494
11506
  console.log(`Opening browser to authorize the Zalify CLI…
11495
11507
  ${authUrl}
11508
+ `);
11509
+ console.log(`Verification code: ${verificationCode}
11496
11510
  `);
11497
11511
  console.log("If the browser doesn't open, visit the URL above manually.");
11498
11512
  openBrowser(authUrl);
@@ -13075,9 +13089,160 @@ async function shopifyUploadImages(storeDir, options = {}) {
13075
13089
  await maybeRefreshShops(auth, dir2, options.site);
13076
13090
  }
13077
13091
 
13078
- // src/brand.ts
13079
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
13092
+ // src/ads.ts
13093
+ import { spawnSync as spawnSync4 } from "node:child_process";
13094
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
13080
13095
  import { basename as basename4, join as join7, resolve as resolve6 } from "node:path";
13096
+ function apiError3(status, json) {
13097
+ if (json.code === "SLUG_TAKEN") {
13098
+ return new Error(`${json.error}
13099
+ Pick a different directory/slug name.`);
13100
+ }
13101
+ if (status === 404) {
13102
+ return new Error("The repos API is not deployed yet (404). Update app.zalify.com first.");
13103
+ }
13104
+ if (status === 503) {
13105
+ return new Error(json.error ?? "Brand-repo provisioning is not configured on the server.");
13106
+ }
13107
+ return new Error(`repos API ${status}: ${JSON.stringify(json)}`);
13108
+ }
13109
+ async function request2(auth, method, path9, body) {
13110
+ const url = method === "GET" ? `${auth.appUrl}${path9}${path9.includes("?") ? "&" : "?"}workspaceId=${auth.workspaceId}` : `${auth.appUrl}${path9}`;
13111
+ const res = await fetch(url, {
13112
+ method,
13113
+ headers: {
13114
+ Authorization: `Bearer ${auth.key}`,
13115
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
13116
+ },
13117
+ ...method === "POST" ? { body: JSON.stringify({ workspaceId: auth.workspaceId, ...body }) } : {}
13118
+ });
13119
+ const json = await res.json().catch(() => ({}));
13120
+ if (!res.ok)
13121
+ throw apiError3(res.status, json);
13122
+ return json;
13123
+ }
13124
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
13125
+ async function pollUntilReady2(auth, id) {
13126
+ let lastMessage = "";
13127
+ const deadline = Date.now() + 5 * 60000;
13128
+ while (Date.now() < deadline) {
13129
+ const view = await request2(auth, "GET", `/api/repos/${id}`);
13130
+ const message = view.statusMessage ?? view.status;
13131
+ if (message !== lastMessage) {
13132
+ console.log(` … ${message}`);
13133
+ lastMessage = message;
13134
+ }
13135
+ if (view.status === "READY")
13136
+ return view;
13137
+ if (view.status === "FAILED") {
13138
+ throw new Error(`Provisioning failed: ${view.statusMessage ?? "unknown reason"}`);
13139
+ }
13140
+ await sleep3(3000);
13141
+ }
13142
+ throw new Error("Provisioning timed out after 5 minutes — check the Temporal UI.");
13143
+ }
13144
+ function scaffoldStarter(dir2, slug, workspaceId) {
13145
+ const root = resolve6(dir2);
13146
+ const write = (rel, content) => {
13147
+ const abs = join7(root, rel);
13148
+ mkdirSync3(join7(abs, ".."), { recursive: true });
13149
+ writeFileSync7(abs, content);
13150
+ };
13151
+ write("identity.yaml", `# Brand instance identity — which workspace and which platform accounts
13152
+ # this instance operates. Filled in during onboarding; the Autopilot
13153
+ # service refuses to run an instance whose consent flag is false.
13154
+ workspace_id: ${workspaceId}
13155
+ brand_slug: ${slug}
13156
+ consent: false # flip only with the brand owner's explicit consent
13157
+ platforms:
13158
+ meta:
13159
+ ad_account_ids: [] # act_… — the challenger account goes here
13160
+ page_id: ""
13161
+ pixel_id: ""
13162
+ `);
13163
+ write("brand.yaml", `# Brand voice + guardrails consumed by drafting (media plans, briefs).
13164
+ name: ${slug}
13165
+ vertical: ""
13166
+ voice: ""
13167
+ guidelines: {}
13168
+ special_ad_categories: []
13169
+ `);
13170
+ write("policy/config.yaml", `# Per-brand policy thresholds — data, not code. All values versioned via
13171
+ # git history; the pacing engine refuses to run without this file.
13172
+ version: 1
13173
+ proposal_cap_per_day: 8
13174
+ proposal_expiry_hours: 24
13175
+ cooldown_after_mutation_hours: 24
13176
+ maturity_windows:
13177
+ spend_hours: 2
13178
+ cpa_hours: 24
13179
+ roas_days: 3
13180
+ `);
13181
+ write("README.md", `# ${slug} — Autopilot brand instance
13182
+
13183
+ Campaign intent for this brand: media plans (\`media-plans/\`), ads-as-code
13184
+ campaign definitions (\`campaigns/\`), policy thresholds (\`policy/\`).
13185
+ Written by humans and the Autopilot drafting agent via PRs; the service
13186
+ mirrors decisions into \`decisions/\` and daily signal digests into
13187
+ \`signals/\`. Telemetry and tokens never live here.
13188
+ `);
13189
+ write("campaigns/.gitkeep", "");
13190
+ write("media-plans/.gitkeep", "");
13191
+ write("signals/digests/.gitkeep", "");
13192
+ write("decisions/.gitkeep", "");
13193
+ write(".gitignore", `.env
13194
+ .DS_Store
13195
+ `);
13196
+ }
13197
+ async function adsCreate(dir2) {
13198
+ const auth = requireActive();
13199
+ const slug = basename4(resolve6(dir2)).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "brand";
13200
+ console.log(`Provisioning brand instance "${slug}" (workspace "${auth.workspaceName}")`);
13201
+ const created = await request2(auth, "POST", "/api/repos", { slug });
13202
+ const view = await pollUntilReady2(auth, created.repoId);
13203
+ console.log(` ✓ provisioned: ${view.githubRepo}`);
13204
+ scaffoldStarter(dir2, slug, auth.workspaceId);
13205
+ mkdirSync3(join7(resolve6(dir2), ".zalify"), { recursive: true });
13206
+ writeFileSync7(join7(resolve6(dir2), ".zalify", "brand-repo.json"), JSON.stringify({ id: created.repoId, slug: view.slug, repo: view.githubRepo }, null, 2) + `
13207
+ `);
13208
+ if (view.githubRepo) {
13209
+ const { token, repo } = await request2(auth, "POST", `/api/repos/${created.repoId}/push-token`, {});
13210
+ const run = (args) => spawnSync4("git", args, { cwd: resolve6(dir2), stdio: "ignore" });
13211
+ run(["init", "-b", "main"]);
13212
+ run(["add", "-A"]);
13213
+ run(["commit", "-m", "Scaffold brand instance", "--no-verify"]);
13214
+ run(["remote", "add", "origin", `https://github.com/${repo}.git`]);
13215
+ const push = spawnSync4("git", ["push", `https://x-access-token:${token}@github.com/${repo}.git`, "main"], { cwd: resolve6(dir2), stdio: "inherit" });
13216
+ if (push.status !== 0) {
13217
+ throw new Error(`git push to ${repo} failed — resolve and push manually.`);
13218
+ }
13219
+ run(["update-ref", "refs/remotes/origin/main", "HEAD"]);
13220
+ run(["config", "branch.main.remote", "origin"]);
13221
+ run(["config", "branch.main.merge", "refs/heads/main"]);
13222
+ console.log(` ✓ pushed to github.com/${repo}`);
13223
+ }
13224
+ console.log(`
13225
+ Next steps:
13226
+ ` + ` - fill in identity.yaml (ad account ids) and brand.yaml
13227
+ ` + ` - flip consent: true once the brand owner has signed off
13228
+ ` + ` - the Autopilot service picks the instance up from there`);
13229
+ }
13230
+ async function adsList() {
13231
+ const auth = requireActive();
13232
+ const json = await request2(auth, "GET", "/api/repos");
13233
+ const repos = json.repos ?? [];
13234
+ if (!repos.length) {
13235
+ console.log(`No brand instances in workspace "${auth.workspaceName}".`);
13236
+ return;
13237
+ }
13238
+ for (const r of repos) {
13239
+ console.log(`${r.status === "READY" ? "✓" : r.status === "FAILED" ? "✗" : "…"} ${r.slug} ${r.githubRepo ?? "-"} (${r.status.toLowerCase()})`);
13240
+ }
13241
+ }
13242
+
13243
+ // src/brand.ts
13244
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
13245
+ import { basename as basename5, join as join8, resolve as resolve7 } from "node:path";
13081
13246
  var AUTHORING_MD = `# Brand authoring guide
13082
13247
 
13083
13248
  The rules for filling in this folder. An author — human or AI agent —
@@ -13378,19 +13543,19 @@ var MANIFEST_JSON = JSON.stringify({
13378
13543
  }, null, 2) + `
13379
13544
  `;
13380
13545
  function brandInit(dir2 = ".", opts = {}) {
13381
- const target = resolve6(dir2);
13382
- const slug = basename4(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13546
+ const target = resolve7(dir2);
13547
+ const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13383
13548
  if (existsSync7(target) && readdirSync3(target).length > 0) {
13384
13549
  throw new Error(`${dir2} already exists and is not empty`);
13385
13550
  }
13386
13551
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13387
13552
  const currency = (opts.currency ?? "USD").toUpperCase();
13388
- mkdirSync3(join7(target, "images"), { recursive: true });
13389
- writeFileSync7(join7(target, "AUTHORING.md"), AUTHORING_MD);
13390
- writeFileSync7(join7(target, "brand.md"), BRAND_MD(name));
13391
- writeFileSync7(join7(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
13392
- writeFileSync7(join7(target, "catalog.json"), CATALOG_JSON(name, currency));
13393
- writeFileSync7(join7(target, "images", "manifest.json"), MANIFEST_JSON);
13553
+ mkdirSync4(join8(target, "images"), { recursive: true });
13554
+ writeFileSync8(join8(target, "AUTHORING.md"), AUTHORING_MD);
13555
+ writeFileSync8(join8(target, "brand.md"), BRAND_MD(name));
13556
+ writeFileSync8(join8(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
13557
+ writeFileSync8(join8(target, "catalog.json"), CATALOG_JSON(name, currency));
13558
+ writeFileSync8(join8(target, "images", "manifest.json"), MANIFEST_JSON);
13394
13559
  console.log(`Scaffolded brand data in ${dir2}:`);
13395
13560
  for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
13396
13561
  console.log(` + ${f}`);
@@ -13401,11 +13566,11 @@ Author in this order: brand.md → catalog.json → images/manifest.json` + `
13401
13566
  }
13402
13567
  var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
13403
13568
  function brandValidate(dir2 = ".") {
13404
- const target = resolve6(dir2);
13569
+ const target = resolve7(dir2);
13405
13570
  const problems = [];
13406
13571
  const warnings = [];
13407
13572
  const readJson2 = (rel) => {
13408
- const path9 = join7(target, rel);
13573
+ const path9 = join8(target, rel);
13409
13574
  if (!existsSync7(path9)) {
13410
13575
  problems.push(`missing ${rel}`);
13411
13576
  return null;
@@ -13417,7 +13582,7 @@ function brandValidate(dir2 = ".") {
13417
13582
  return null;
13418
13583
  }
13419
13584
  };
13420
- if (!existsSync7(join7(target, "brand.md")))
13585
+ if (!existsSync7(join8(target, "brand.md")))
13421
13586
  problems.push("missing brand.md");
13422
13587
  const store = readJson2("store.json");
13423
13588
  if (store && (!store.name || !store.slug))
@@ -13593,7 +13758,7 @@ function brandValidate(dir2 = ".") {
13593
13758
  // src/cli.ts
13594
13759
  var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
13595
13760
  var require2 = createRequire2(import.meta.url);
13596
- var pkg = require2(join8(__dirname4, "..", "package.json"));
13761
+ var pkg = require2(join9(__dirname4, "..", "package.json"));
13597
13762
  try {
13598
13763
  updateNotifier({ pkg }).notify({
13599
13764
  isGlobal: true,
@@ -13665,6 +13830,13 @@ shop.command("open [slug-or-dir]").description("Open the live shop in your brows
13665
13830
  shop.command("domain-sync [slug-or-dir]").description("Re-point the shop at the current root domain (the old domain 308-redirects)").action(async (slugOrDir) => {
13666
13831
  await shopDomainSync(slugOrDir);
13667
13832
  });
13833
+ var ads = program2.command("ads").description("Autopilot brand instances — campaign intent repos (media plans, ads-as-code, policy config)");
13834
+ ads.command("create <dir>").description("Provision the brand-instance GitHub repo, scaffold the starter locally, and push").action(async (dir2) => {
13835
+ await adsCreate(dir2);
13836
+ });
13837
+ ads.command("list", { isDefault: true }).description("List the workspace's brand instances").action(async () => {
13838
+ await adsList();
13839
+ });
13668
13840
  var theme = program2.command("theme").description("Theme source code — scaffold a copy and upgrade it without losing your edits");
13669
13841
  theme.command("create <dir>").description("Scaffold only — no Zalify hosting (self-host path; `site create` is the fully-hosted launch)").option("-t, --template <name>", "liquid | hydrogen | nextjs", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--store-domain <domain>", "your-store.myshopify.com (default: mock.shop demo data)").option("--storefront-token <token>", "public Storefront API access token").option("--to <version>", "theme version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init + initial commit").option("--tarball <path>", "use a local @zalify/theme-templates .tgz instead of npm").action(async (dir2, options) => {
13670
13842
  await themeCreate(dir2, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",