@zalify/cli 0.16.0 → 0.17.1

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 +106 -33
  2. package/dist/cli.js +362 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @zalify/cli
2
2
 
3
- Zalify CLI command-line interface for Zalify.
3
+ Zalify CLI author a brand, launch its shop, run its ads, keep the theme
4
+ current.
4
5
 
5
6
  ## Install
6
7
 
@@ -10,45 +11,119 @@ npm i -g zalify
10
11
  npm i -g @zalify/cli@latest
11
12
  ```
12
13
 
13
- ## Usage
14
+ Already installed? `zalify self-update`.
14
15
 
15
- ### Authentication
16
+ ## The command model
16
17
 
17
- ```bash
18
- zalify login # opens the browser: pick a workspace, authorize
19
- zalify login --app-url http://localhost:3000 # against a local app.zalify.com
20
- zalify whoami # show the authorized workspace
21
- zalify logout # delete local credentials
22
- ```
18
+ Four nouns, four objects:
23
19
 
24
- `login` runs a loopback flow (like `gh auth login`): the CLI listens on a
25
- random localhost port, the browser opens `app.zalify.com/cli/auth`, and on
26
- approval a **workspace-scoped API key** is handed back to the CLI and stored
27
- in `~/.zalify/config.json` (0600). Keys are ordinary Zalify Public API keys
28
- list and revoke them in **Settings Developer**.
20
+ | Noun | The object | What lives under it |
21
+ | --- | --- | --- |
22
+ | `brand` | the **data** | a brand folder: catalog, voice guide, image manifest |
23
+ | `shop` | the **shop** | Zalify-hosted at `<slug>.z1.shop`: repo + deployment + domain |
24
+ | `ads` | the **ad ops** | Autopilot brand instance: campaign intent repo (media plans, ads-as-code, policy) |
25
+ | `theme` | the **code** | Shopify-theme model: scaffold a copy, measure drift, upgrade |
29
26
 
30
- ### Assets
27
+ Everything acting on a brand folder lives under `brand`. Brand and theme
28
+ commands default their directory to the cwd; `shop refresh`/`open` resolve
29
+ their shop from the cwd (`.zalify/site.json` in a scaffold, `store.json` in
30
+ a brand folder); `ads` commands run arg-free from a checkout carrying
31
+ `.zalify/brand-repo.json`.
31
32
 
32
- Sync a directory's `images/*.png` with the workspace asset library
33
- (the same storage that backs Assets in the Zalify app):
33
+ ## The golden path (new brand, start to live)
34
34
 
35
35
  ```bash
36
- zalify assets push <dir> # upload new/changed files (sha256 dedup)
37
- zalify assets pull <dir> # download files listed in images/assets.json
36
+ zalify brand init stores/acme # 1. author the data (no login needed)
37
+ zalify login # 2. authorize; then: zalify workspace set <workspace>
38
+ zalify shop create acme # 3. live at acme.z1.shop in ~4 minutes
39
+ cd stores/acme && zalify brand push --prune
40
+ zalify brand images generate && zalify brand images attach
41
+ zalify theme status # 6. stay current; `theme upgrade` keeps your edits
38
42
  ```
39
43
 
40
- `push` writes `<dir>/images/assets.json` — a committed index mapping each
41
- file to its asset id, public URL, and checksum. `pull` restores the binaries
42
- on a fresh clone, so image masters stay out of git.
44
+ ## Authentication
43
45
 
44
- ## Commands
46
+ ```bash
47
+ zalify login # opens the browser: authorize once
48
+ zalify login --app-url http://localhost:3000 # against a local app.zalify.com
49
+ zalify whoami # workspace, plan, and key details
50
+ zalify logout # delete local credentials
51
+ ```
45
52
 
46
- - **login** `[--app-url <url>]` Browser-based authorization; stores a workspace API key locally.
47
- - **logout** Deletes `~/.zalify/config.json` (revoke the key itself in Settings → Developer).
48
- - **whoami** Shows the authorized workspace, app origin, and key prefix.
49
- - **assets push `<dir>`** – Uploads `<dir>/images/*.png` via presigned R2 PUTs; sha256 dedup; updates `assets.json`.
50
- - **assets pull `<dir>`** Downloads files listed in `<dir>/images/assets.json` that are missing locally.
51
- - **version** Prints `Zalify CLI version: x.x.x` and, if a newer version exists on npm, suggests running `npm i -g @zalify/cli@latest` to update.
53
+ `login` runs a loopback flow (like `gh auth login`): the CLI listens on a
54
+ random localhost port, the browser opens `app.zalify.com/cli/auth`, and on
55
+ approval a **user key covering all your workspaces** is handed back and
56
+ stored in `~/.zalify/config.json` (0600). Keys are ordinary Zalify Public
57
+ API keys list and revoke them in **Settings Developer**. One login
58
+ covers every workspace; `zalify workspace set` switches between them
59
+ locally, no re-login.
60
+
61
+ ## Command reference
62
+
63
+ ### Auth & workspace
64
+
65
+ | Command | What it does |
66
+ | --- | --- |
67
+ | `login [--app-url <url>]` | Browser-based authorization; stores a user key locally |
68
+ | `logout` | Deletes `~/.zalify/config.json` (revoke the key itself in Settings → Developer) |
69
+ | `whoami` | Verifies the stored key; shows workspace, plan, app origin, key prefix |
70
+ | `workspace` / `workspace list` | List authorized workspaces (`*` = active) |
71
+ | `workspace set <slug-or-id>` | Switch the active workspace (local, no re-login) |
72
+
73
+ ### `brand` — the data
74
+
75
+ | Command | What it does |
76
+ | --- | --- |
77
+ | `brand init [dir] [--name <name>] [--vertical <v>] [--currency <code>]` | Scaffold a brand folder: catalog, voice guide, image manifest, `AUTHORING.md` rules. No login needed |
78
+ | `brand validate [dir]` | Lint the brand folder against the authoring rules |
79
+ | `brand push [dir] [--prune] [--shop <slug>]` | Push `catalog.json` to the connected Shopify store — products, smart collections, publish; idempotent. `--prune` deletes store products (by catalog vendor) absent from the catalog; `--shop` narrows the post-push refresh to one shop |
80
+ | `brand images generate [dir]` | Generate the manifest's missing images on Zalify infrastructure (resumable) |
81
+ | `brand images attach [dir] [--shop <slug>]` | Attach generated images to their Shopify products per the manifest (skips already-attached) |
82
+ | `brand images push [dir]` | Upload image masters to the Zalify asset library — presigned R2 PUTs, sha256 dedup; writes `images/assets.json`, the committed index that keeps binaries out of git |
83
+ | `brand images pull [dir]` | Download masters listed in `images/assets.json` that are missing locally (fresh-clone restore) |
84
+
85
+ ### `shop` — the hosted shop
86
+
87
+ | Command | What it does |
88
+ | --- | --- |
89
+ | `shop create <dir> [-t <theme>] [--editor] [--to <version>] [--no-install] [--no-git]` | One-command launch: provision repo + deployment + domain + storefront token, scaffold locally, push, go live at `<slug>.z1.shop`. `--editor` wires in the Zalify canvas editor (z1) |
90
+ | `shop` / `shop list` | List the workspace's hosted shops |
91
+ | `shop refresh [slug-or-dir]` | Refresh the live shop's data cache (defaults to the shop linked to cwd) |
92
+ | `shop open [slug-or-dir]` | Open the live shop in the browser (defaults to the shop linked to cwd) |
93
+ | `shop domain-sync [slug-or-dir]` | Re-point the shop at the current root domain (the old domain 308-redirects) |
94
+
95
+ `site …` remains as a hidden alias for one release and prints a pointer to
96
+ its new home under `shop`.
97
+
98
+ ### `ads` — the ad ops
99
+
100
+ | Command | What it does |
101
+ | --- | --- |
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
+ | `ads` / `ads list` | List the workspace's brand instances and their provisioning status |
104
+
105
+ The engine commands — `plan` / `apply` / `creative` / `report` — join this
106
+ group as `@zalify/ads-engine` merges in. Until then, drafting and applying
107
+ happen on the Structure canvas in app.zalify.com; the instance repo is the
108
+ durable intent layer that work lands in.
109
+
110
+ ### `theme` — the code
111
+
112
+ | Command | What it does |
113
+ | --- | --- |
114
+ | `theme create <dir> [-t <template>] [--editor] [--store-domain <domain>] [--storefront-token <token>] [--to <version>] [--no-install] [--no-git] [--tarball <path>]` | Scaffold only — no Zalify hosting (the self-host path; `shop create` is the fully-hosted launch). Templates: `liquid` \| `hydrogen` \| `nextjs`. Without `--store-domain` it runs on mock.shop demo data |
115
+ | `theme status [dir]` | Which theme files have you edited? Checksums vs `.zalify/theme.json` |
116
+ | `theme upgrade [dir] [--to <version>] [--dry-run] [--install] [--tarball <path>] [--base-tarball <path>]` | Move to a newer theme version: untouched files overwritten, edited files 3-way-merged, `theme/` + `.env` never touched. `--dry-run` classifies and reports without writing |
117
+
118
+ ### Maintenance
119
+
120
+ | Command | What it does |
121
+ | --- | --- |
122
+ | `version` | Print the CLI version and check npm for a newer one |
123
+ | `self-update` | Update the CLI to the latest version (auto-detects pnpm/bun/npm) |
124
+
125
+ Every invocation also runs a passive, non-blocking update check and prints
126
+ a one-line notice when a newer version exists.
52
127
 
53
128
  ## Development
54
129
 
@@ -76,8 +151,6 @@ Release flow:
76
151
 
77
152
  ```bash
78
153
  npm version minor --no-git-tag-version # or patch/major
79
- git commit -am "chore: release v0.x.0" && git push
154
+ # bump zalify/package.json in step and pin its @zalify/cli dependency
155
+ git commit -am "0.x.0" && git push
80
156
  ```
81
-
82
- `bun run release` (bump + local `npm publish`, needs `NODE_AUTH_TOKEN` in
83
- `.env`) remains as a manual fallback if CI is unavailable.
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 join10 } 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);
@@ -11748,37 +11762,122 @@ async function assetsPush(storeDir) {
11748
11762
 
11749
11763
  // src/self-update.ts
11750
11764
  import { spawnSync } from "node:child_process";
11751
- import { realpathSync } from "node:fs";
11752
- function detectManager() {
11753
- let binPath = process.argv[1] ?? "";
11765
+ import { existsSync as existsSync3, realpathSync } from "node:fs";
11766
+ import { delimiter, join as join3 } from "node:path";
11767
+ var PACKAGES = ["zalify", "@zalify/cli"];
11768
+ var real = (p) => {
11754
11769
  try {
11755
- binPath = realpathSync(binPath);
11756
- } catch {}
11770
+ return realpathSync(p);
11771
+ } catch {
11772
+ return p;
11773
+ }
11774
+ };
11775
+ function managerFor(binPath) {
11757
11776
  if (binPath.includes("/pnpm/") || binPath.includes("/.pnpm/")) {
11758
- return { cmd: "pnpm", args: ["add", "-g", "@zalify/cli@latest"] };
11777
+ return {
11778
+ cmd: "pnpm",
11779
+ update: (pkg) => ["add", "-g", `${pkg}@latest`],
11780
+ remove: `pnpm rm -g ${PACKAGES.join(" ")}`
11781
+ };
11759
11782
  }
11760
11783
  if (binPath.includes("/.bun/")) {
11761
- return { cmd: "bun", args: ["add", "-g", "@zalify/cli@latest"] };
11784
+ return {
11785
+ cmd: "bun",
11786
+ update: (pkg) => ["add", "-g", `${pkg}@latest`],
11787
+ remove: `bun remove -g ${PACKAGES.join(" ")}`
11788
+ };
11762
11789
  }
11763
- return { cmd: "npm", args: ["install", "-g", "@zalify/cli@latest"] };
11790
+ return {
11791
+ cmd: "npm",
11792
+ update: (pkg) => ["install", "-g", `${pkg}@latest`],
11793
+ remove: `npm rm -g ${PACKAGES.join(" ")}`
11794
+ };
11764
11795
  }
11765
- function selfUpdate(currentVersion) {
11766
- const { cmd, args } = detectManager();
11796
+ function packageFor(binPath) {
11797
+ return /\/node_modules\/zalify\//.test(binPath) || /\/zalify@\d/.test(binPath) ? "zalify" : "@zalify/cli";
11798
+ }
11799
+ function zalifysOnPath() {
11800
+ const hits = [];
11801
+ for (const dir2 of (process.env.PATH ?? "").split(delimiter)) {
11802
+ if (!dir2)
11803
+ continue;
11804
+ const candidate = join3(dir2, "zalify");
11805
+ if (existsSync3(candidate) && !hits.includes(candidate)) {
11806
+ hits.push(candidate);
11807
+ }
11808
+ }
11809
+ return hits;
11810
+ }
11811
+ function versionOf(bin) {
11812
+ const res = spawnSync(bin, ["--version"], { encoding: "utf8" });
11813
+ if (res.error || res.status !== 0)
11814
+ return null;
11815
+ return res.stdout?.trim() || null;
11816
+ }
11817
+ var HASH_HINT = "If `zalify -v` still shows the old version in this terminal, the shell\n" + "has cached the old location — run `hash -r` (zsh/bash) or open a new\n" + "terminal. A CLI cannot refresh its parent shell for you.";
11818
+ async function selfUpdate(currentVersion) {
11819
+ const running = real(process.argv[1] ?? "");
11820
+ const manager = managerFor(running);
11821
+ const pkg = packageFor(running);
11767
11822
  console.log(`Current version: ${currentVersion}`);
11768
- console.log(`Updating via: ${cmd} ${args.join(" ")}
11823
+ console.log(`Updating via: ${manager.cmd} ${manager.update(pkg).join(" ")}
11769
11824
  `);
11770
- const result = spawnSync(cmd, args, { stdio: "inherit" });
11825
+ const result = spawnSync(manager.cmd, manager.update(pkg), {
11826
+ stdio: "inherit"
11827
+ });
11771
11828
  if (result.error || result.status !== 0) {
11772
11829
  console.error(`
11773
11830
  Update failed${result.error ? `: ${result.error.message}` : ""}.` + `
11774
- Try manually: ${cmd} ${args.join(" ")}`);
11831
+ Try manually: ${manager.cmd} ${manager.update(pkg).join(" ")}`);
11775
11832
  process.exit(result.status ?? 1);
11776
11833
  }
11777
- const check = spawnSync("zalify", ["--version"], { encoding: "utf8" });
11778
- const updated = check.stdout?.trim();
11779
- console.log(updated && updated !== currentVersion ? `
11780
- Updated ${currentVersion} ${updated}` : `
11781
- ✓ Done${updated ? ` (version: ${updated})` : ""}`);
11834
+ let latest = null;
11835
+ try {
11836
+ const info = await updateNotifier({
11837
+ pkg: { name: "@zalify/cli", version: currentVersion }
11838
+ }).fetchInfo();
11839
+ latest = info?.latest ?? null;
11840
+ } catch {}
11841
+ const hits = zalifysOnPath();
11842
+ const first = hits[0];
11843
+ const firstVersion = first ? versionOf(first) : null;
11844
+ const mayBeHashed = hits.length > 1 || first && real(first) !== running;
11845
+ if (!first || !firstVersion) {
11846
+ console.log(`
11847
+ ✓ Done`);
11848
+ return;
11849
+ }
11850
+ const reachedLatest = latest ? firstVersion === latest : firstVersion !== currentVersion;
11851
+ if (reachedLatest) {
11852
+ console.log(firstVersion === currentVersion ? `
11853
+ ✓ Already the latest (${firstVersion})` : `
11854
+ ✓ Updated ${currentVersion} → ${firstVersion}`);
11855
+ if (mayBeHashed)
11856
+ console.log(`
11857
+ ${HASH_HINT}`);
11858
+ return;
11859
+ }
11860
+ if (latest) {
11861
+ console.log(`
11862
+ ✗ ${pkg} is now ${latest}, but your PATH still runs`);
11863
+ console.log(` ${first} (${firstVersion})`);
11864
+ if (hits.length > 1) {
11865
+ console.log(`
11866
+ Every zalify on your PATH:`);
11867
+ for (const hit of hits) {
11868
+ console.log(` ${hit} → ${versionOf(hit) ?? "unreadable"}`);
11869
+ }
11870
+ }
11871
+ console.log(`
11872
+ Remove the stale install (${managerFor(real(first)).remove}),`);
11873
+ console.log("then run `hash -r` or open a new terminal.");
11874
+ process.exit(1);
11875
+ }
11876
+ console.log(`
11877
+ ✓ Done (version: ${firstVersion})`);
11878
+ if (mayBeHashed)
11879
+ console.log(`
11880
+ ${HASH_HINT}`);
11782
11881
  }
11783
11882
 
11784
11883
  // src/workspace.ts
@@ -11853,8 +11952,8 @@ async function workspaceSet(slugOrId) {
11853
11952
  }
11854
11953
 
11855
11954
  // src/images.ts
11856
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, rmSync as rmSync2 } from "node:fs";
11857
- import { join as join3, resolve as resolve2 } from "node:path";
11955
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync4, rmSync as rmSync2 } from "node:fs";
11956
+ import { join as join4, resolve as resolve2 } from "node:path";
11858
11957
  var MAX_JOBS_PER_REQUEST = 40;
11859
11958
  var POLL_INTERVAL_MS = 5000;
11860
11959
  var POLL_TIMEOUT_MS = 30 * 60 * 1000;
@@ -11882,7 +11981,7 @@ async function saveProduced(imagesDir, indexPath, index, file2, asset) {
11882
11981
  return false;
11883
11982
  }
11884
11983
  const bytes = Buffer.from(await img.arrayBuffer());
11885
- writeFileSync4(join3(imagesDir, file2), bytes);
11984
+ writeFileSync4(join4(imagesDir, file2), bytes);
11886
11985
  index[file2] = {
11887
11986
  assetId: asset.id,
11888
11987
  url: asset.url,
@@ -12019,17 +12118,17 @@ async function streamingGenerate(auth, manifest, missing, imagesDir, indexPath,
12019
12118
  }
12020
12119
  async function imagesGenerate(storeDir) {
12021
12120
  const auth = requireActive();
12022
- const imagesDir = join3(resolve2(storeDir), "images");
12023
- const manifestPath = join3(imagesDir, "manifest.json");
12024
- if (!existsSync3(manifestPath)) {
12121
+ const imagesDir = join4(resolve2(storeDir), "images");
12122
+ const manifestPath = join4(imagesDir, "manifest.json");
12123
+ if (!existsSync4(manifestPath)) {
12025
12124
  throw new Error(`No images/manifest.json in ${storeDir}`);
12026
12125
  }
12027
12126
  const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
12028
- const indexPath = join3(imagesDir, "assets.json");
12029
- const index = existsSync3(indexPath) ? JSON.parse(readFileSync3(indexPath, "utf8")) : {};
12030
- const jobStatePath = join3(imagesDir, ".imagegen-job.json");
12127
+ const indexPath = join4(imagesDir, "assets.json");
12128
+ const index = existsSync4(indexPath) ? JSON.parse(readFileSync3(indexPath, "utf8")) : {};
12129
+ const jobStatePath = join4(imagesDir, ".imagegen-job.json");
12031
12130
  const counters = { produced: 0, failed: 0 };
12032
- if (existsSync3(jobStatePath)) {
12131
+ if (existsSync4(jobStatePath)) {
12033
12132
  const state = JSON.parse(readFileSync3(jobStatePath, "utf8"));
12034
12133
  console.log(`Resuming job ${state.jobId}`);
12035
12134
  await pollJob(auth, state.jobId, state.files, imagesDir, indexPath, index, counters);
@@ -12037,7 +12136,7 @@ async function imagesGenerate(storeDir) {
12037
12136
  console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
12038
12137
  return;
12039
12138
  }
12040
- const missing = manifest.images.filter((e) => !existsSync3(join3(imagesDir, e.file)));
12139
+ const missing = manifest.images.filter((e) => !existsSync4(join4(imagesDir, e.file)));
12041
12140
  if (missing.length === 0) {
12042
12141
  console.log("All manifest images already exist locally.");
12043
12142
  return;
@@ -12061,20 +12160,20 @@ async function imagesGenerate(storeDir) {
12061
12160
  }
12062
12161
 
12063
12162
  // src/shopify.ts
12064
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
12065
- import { basename as basename3, join as join6, resolve as resolve5 } from "node:path";
12163
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "node:fs";
12164
+ import { basename as basename3, join as join7, resolve as resolve5 } from "node:path";
12066
12165
 
12067
12166
  // src/shop.ts
12068
12167
  import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
12069
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
12070
- import { basename as basename2, join as join5, resolve as resolve4 } from "node:path";
12168
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
12169
+ import { basename as basename2, join as join6, resolve as resolve4 } from "node:path";
12071
12170
 
12072
12171
  // src/theme.ts
12073
12172
  import { spawnSync as spawnSync2 } from "node:child_process";
12074
12173
  import { randomBytes as randomBytes2 } from "node:crypto";
12075
12174
  import {
12076
12175
  cpSync,
12077
- existsSync as existsSync4,
12176
+ existsSync as existsSync5,
12078
12177
  mkdirSync as mkdirSync2,
12079
12178
  mkdtempSync,
12080
12179
  readdirSync as readdirSync2,
@@ -12086,7 +12185,7 @@ import {
12086
12185
  writeFileSync as writeFileSync5
12087
12186
  } from "node:fs";
12088
12187
  import { tmpdir } from "node:os";
12089
- import { basename, dirname, join as join4, resolve as resolve3 } from "node:path";
12188
+ import { basename, dirname, join as join5, resolve as resolve3 } from "node:path";
12090
12189
  var TEMPLATES_PKG = "@zalify/theme-templates";
12091
12190
  var REGISTRY = "https://registry.npmjs.org";
12092
12191
  var MANIFEST_PATH = ".zalify/theme.json";
@@ -12112,27 +12211,27 @@ async function downloadTarball(url) {
12112
12211
  const res = await fetch(url);
12113
12212
  if (!res.ok)
12114
12213
  throw new Error(`tarball download ${res.status}: ${url}`);
12115
- const file2 = join4(mkdtempSync(join4(tmpdir(), "zalify-theme-")), "pkg.tgz");
12214
+ const file2 = join5(mkdtempSync(join5(tmpdir(), "zalify-theme-")), "pkg.tgz");
12116
12215
  writeFileSync5(file2, Buffer.from(await res.arrayBuffer()));
12117
12216
  return file2;
12118
12217
  }
12119
12218
  function extractTarball(file2) {
12120
- const dir2 = mkdtempSync(join4(tmpdir(), "zalify-theme-x-"));
12219
+ const dir2 = mkdtempSync(join5(tmpdir(), "zalify-theme-x-"));
12121
12220
  const result = spawnSync2("tar", ["-xzf", resolve3(file2), "-C", dir2], {
12122
12221
  encoding: "utf8"
12123
12222
  });
12124
12223
  if (result.error || result.status !== 0) {
12125
12224
  throw new Error(`tar extract failed for ${file2}: ${result.stderr || result.error?.message}`);
12126
12225
  }
12127
- const root = join4(dir2, "package");
12128
- if (!existsSync4(root))
12226
+ const root = join5(dir2, "package");
12227
+ if (!existsSync5(root))
12129
12228
  throw new Error(`unexpected tarball layout in ${file2} (no package/)`);
12130
12229
  return root;
12131
12230
  }
12132
12231
  async function acquireTemplatePkg(localTarball, version) {
12133
12232
  if (localTarball) {
12134
12233
  const root = extractTarball(localTarball);
12135
- const v = JSON.parse(readFileSync4(join4(root, "package.json"), "utf8")).version;
12234
+ const v = JSON.parse(readFileSync4(join5(root, "package.json"), "utf8")).version;
12136
12235
  return { root, version: v };
12137
12236
  }
12138
12237
  const packument = await fetchPackument();
@@ -12143,8 +12242,8 @@ async function acquireTemplatePkg(localTarball, version) {
12143
12242
  return { root: extractTarball(await downloadTarball(entry.dist.tarball)), version: target };
12144
12243
  }
12145
12244
  function readTemplateManifest(pkgRoot, template) {
12146
- const path9 = join4(pkgRoot, "templates", template, MANIFEST_PATH);
12147
- if (!existsSync4(path9)) {
12245
+ const path9 = join5(pkgRoot, "templates", template, MANIFEST_PATH);
12246
+ if (!existsSync5(path9)) {
12148
12247
  throw new Error(`template "${template}" has no ${MANIFEST_PATH} — this ${TEMPLATES_PKG} version predates the upgrade contract`);
12149
12248
  }
12150
12249
  return JSON.parse(readFileSync4(path9, "utf8"));
@@ -12161,21 +12260,21 @@ function templateFilePath(templateDir, rel, variant) {
12161
12260
  const stashed = rel.replace(/(^|\/)\.gitignore$/, "$1gitignore");
12162
12261
  if (variant === "editor") {
12163
12262
  for (const candidate of [rel, stashed]) {
12164
- const overlay = join4(templateDir, "__editor__", candidate);
12165
- if (existsSync4(overlay))
12263
+ const overlay = join5(templateDir, "__editor__", candidate);
12264
+ if (existsSync5(overlay))
12166
12265
  return overlay;
12167
12266
  }
12168
12267
  }
12169
12268
  for (const candidate of [rel, stashed]) {
12170
- const path9 = join4(templateDir, candidate);
12171
- if (existsSync4(path9))
12269
+ const path9 = join5(templateDir, candidate);
12270
+ if (existsSync5(path9))
12172
12271
  return path9;
12173
12272
  }
12174
12273
  throw new Error(`file listed in manifest but missing from template: ${rel}`);
12175
12274
  }
12176
12275
  function readProjectManifest(dir2) {
12177
- const path9 = join4(dir2, MANIFEST_PATH);
12178
- if (!existsSync4(path9)) {
12276
+ const path9 = join5(dir2, MANIFEST_PATH);
12277
+ if (!existsSync5(path9)) {
12179
12278
  throw new Error(`no ${MANIFEST_PATH} in ${dir2} — not a scaffolded Zalify storefront.
12180
12279
  ` + ` Scaffold one with \`zalify theme create\`.`);
12181
12280
  }
@@ -12187,22 +12286,22 @@ function writeJson(path9, value) {
12187
12286
  }
12188
12287
  async function themeCreate(dir2, opts) {
12189
12288
  const targetDir = resolve3(dir2);
12190
- if (existsSync4(targetDir) && readdirSync2(targetDir).length > 0) {
12289
+ if (existsSync5(targetDir) && readdirSync2(targetDir).length > 0) {
12191
12290
  throw new Error(`${dir2} already exists and is not empty`);
12192
12291
  }
12193
12292
  const { root, version } = await acquireTemplatePkg(opts.tarball, opts.to);
12194
- const frameworks = JSON.parse(readFileSync4(join4(root, "templates.json"), "utf8"));
12293
+ const frameworks = JSON.parse(readFileSync4(join5(root, "templates.json"), "utf8"));
12195
12294
  const template = opts.template ?? "nextjs";
12196
12295
  const meta = frameworks.frameworks[template];
12197
12296
  if (!meta) {
12198
12297
  throw new Error(`unknown template "${template}" — expected one of: ${Object.keys(frameworks.frameworks).join(", ")}`);
12199
12298
  }
12200
- const templateDir = join4(root, "templates", template);
12299
+ const templateDir = join5(root, "templates", template);
12201
12300
  console.log(`Scaffolding ${meta.label} (${TEMPLATES_PKG}@${version}) into ${dir2}`);
12202
12301
  mkdirSync2(targetDir, { recursive: true });
12203
12302
  cpSync(templateDir, targetDir, { recursive: true });
12204
- const overlayDir = join4(targetDir, "__editor__");
12205
- const useEditor = Boolean(opts.editor) && existsSync4(overlayDir);
12303
+ const overlayDir = join5(targetDir, "__editor__");
12304
+ const useEditor = Boolean(opts.editor) && existsSync5(overlayDir);
12206
12305
  if (opts.editor && !useEditor) {
12207
12306
  console.log(` ! --editor has no effect for ${template} — no editor overlay in this template`);
12208
12307
  }
@@ -12211,14 +12310,14 @@ async function themeCreate(dir2, opts) {
12211
12310
  rmSync3(overlayDir, { recursive: true, force: true });
12212
12311
  (function restoreGitignores(d) {
12213
12312
  for (const entry of readdirSync2(d)) {
12214
- const full = join4(d, entry);
12313
+ const full = join5(d, entry);
12215
12314
  if (statSync(full).isDirectory())
12216
12315
  restoreGitignores(full);
12217
12316
  else if (entry === "gitignore")
12218
- renameSync(full, join4(d, ".gitignore"));
12317
+ renameSync(full, join5(d, ".gitignore"));
12219
12318
  }
12220
12319
  })(targetDir);
12221
- const pkgPath = join4(targetDir, "package.json");
12320
+ const pkgPath = join5(targetDir, "package.json");
12222
12321
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
12223
12322
  pkg.name = basename(targetDir).toLowerCase().replaceAll(/[^a-z0-9-_.]+/g, "-").replaceAll(/^[-.]+|[-.]+$/g, "") || "zalify-storefront";
12224
12323
  writeJson(pkgPath, pkg);
@@ -12232,12 +12331,12 @@ async function themeCreate(dir2, opts) {
12232
12331
  if (meta.env.sessionSecret) {
12233
12332
  lines.push("# Session cookie signing key — unique to this project.", `${meta.env.sessionSecret}=${randomBytes2(32).toString("hex")}`);
12234
12333
  }
12235
- writeFileSync5(join4(targetDir, ".env"), lines.join(`
12334
+ writeFileSync5(join5(targetDir, ".env"), lines.join(`
12236
12335
  `) + `
12237
12336
  `);
12238
12337
  }
12239
12338
  const manifest = readProjectManifest(targetDir);
12240
- writeJson(join4(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
12339
+ writeJson(join5(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
12241
12340
  if (opts.git !== false) {
12242
12341
  const git = (args) => spawnSync2("git", args, { cwd: targetDir, stdio: "ignore" });
12243
12342
  if (git(["init", "-b", "main"]).status === 0) {
@@ -12277,8 +12376,8 @@ function themeStatus(dir2 = ".") {
12277
12376
  const edited = [];
12278
12377
  const missing = [];
12279
12378
  for (const [rel, expected] of Object.entries(manifest.files)) {
12280
- const full = join4(projectDir, rel);
12281
- if (!existsSync4(full))
12379
+ const full = join5(projectDir, rel);
12380
+ if (!existsSync5(full))
12282
12381
  missing.push(rel);
12283
12382
  else if (sha256(readFileSync4(full)) !== expected)
12284
12383
  edited.push(rel);
@@ -12312,8 +12411,8 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12312
12411
  }
12313
12412
  const oldManifest = resolveVariant(readTemplateManifest(base.root, local.template), variant);
12314
12413
  const newManifest = resolveVariant(readTemplateManifest(target.root, local.template), variant);
12315
- const oldDir = join4(base.root, "templates", local.template);
12316
- const newDir = join4(target.root, "templates", local.template);
12414
+ const oldDir = join5(base.root, "templates", local.template);
12415
+ const newDir = join5(target.root, "templates", local.template);
12317
12416
  console.log(`Upgrading ${local.template} theme ${local.version} → ${target.version}` + (opts.dryRun ? " (dry run)" : ""));
12318
12417
  const counts = {
12319
12418
  overwrite: 0,
@@ -12331,8 +12430,8 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12331
12430
  continue;
12332
12431
  const oldHash = oldManifest.files[rel];
12333
12432
  const newHash = newManifest.files[rel];
12334
- const localPath = join4(projectDir, rel);
12335
- const localExists = existsSync4(localPath);
12433
+ const localPath = join5(projectDir, rel);
12434
+ const localExists = existsSync5(localPath);
12336
12435
  const localBytes = localExists ? readFileSync4(localPath) : null;
12337
12436
  const localHash = localBytes ? sha256(localBytes) : null;
12338
12437
  const write = (bytes) => {
@@ -12371,9 +12470,9 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12371
12470
  console.log(` ~ would merge ${rel}`);
12372
12471
  continue;
12373
12472
  }
12374
- const scratch = mkdtempSync(join4(tmpdir(), "zalify-merge-"));
12375
- const basePath = join4(scratch, "base");
12376
- const newPath = join4(scratch, "new");
12473
+ const scratch = mkdtempSync(join5(tmpdir(), "zalify-merge-"));
12474
+ const basePath = join5(scratch, "base");
12475
+ const newPath = join5(scratch, "new");
12377
12476
  writeFileSync5(basePath, oldBytes);
12378
12477
  writeFileSync5(newPath, newBytes);
12379
12478
  const result = spawnSync2("git", ["merge-file", "-L", "current", "-L", `base (${base.version})`, "-L", `new (${target.version})`, localPath, basePath, newPath], { encoding: "utf8" });
@@ -12417,7 +12516,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12417
12516
  }
12418
12517
  const depsChanged = mergePackageJson(projectDir, oldDir, newDir, opts.dryRun ?? false);
12419
12518
  if (!opts.dryRun) {
12420
- writeJson(join4(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
12519
+ writeJson(join5(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
12421
12520
  }
12422
12521
  const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k.replace("-", " ")}`).join(", ");
12423
12522
  console.log(`
@@ -12438,7 +12537,7 @@ ${opts.dryRun ? "Would apply" : "Done"}: ${summary || "no file changes"}.`);
12438
12537
  }
12439
12538
  }
12440
12539
  function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12441
- const read = (p) => JSON.parse(readFileSync4(join4(p, "package.json"), "utf8"));
12540
+ const read = (p) => JSON.parse(readFileSync4(join5(p, "package.json"), "utf8"));
12442
12541
  const localPkg = read(projectDir);
12443
12542
  const oldPkg = read(oldDir);
12444
12543
  const newPkg = read(newDir);
@@ -12487,7 +12586,7 @@ function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12487
12586
  console.log(` ✓ package.json ${key} updated`);
12488
12587
  }
12489
12588
  if (changed && !dryRun) {
12490
- writeFileSync5(join4(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
12589
+ writeFileSync5(join5(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
12491
12590
  `);
12492
12591
  }
12493
12592
  return depsChanged;
@@ -12583,9 +12682,9 @@ async function shopCreate(dir2, opts) {
12583
12682
  install: opts.install,
12584
12683
  git: opts.git
12585
12684
  });
12586
- writeFileSync6(join5(resolve4(dir2), ".zalify", "site.json"), JSON.stringify({ id: siteId, slug: view.slug, domain: view.domain, repo: view.githubRepo }, null, 2) + `
12685
+ writeFileSync6(join6(resolve4(dir2), ".zalify", "site.json"), JSON.stringify({ id: siteId, slug: view.slug, domain: view.domain, repo: view.githubRepo }, null, 2) + `
12587
12686
  `);
12588
- writeFileSync6(join5(resolve4(dir2), "theme", "pixel.json"), JSON.stringify({ workspaceId: auth.workspaceId }, null, 2) + `
12687
+ writeFileSync6(join6(resolve4(dir2), "theme", "pixel.json"), JSON.stringify({ workspaceId: auth.workspaceId }, null, 2) + `
12589
12688
  `);
12590
12689
  if (opts.git !== false && view.githubRepo) {
12591
12690
  const { token, repo } = await request(auth, "POST", `/api/sites/${siteId}/push-token`, {});
@@ -12626,14 +12725,14 @@ Next steps:
12626
12725
  }
12627
12726
  async function resolveSite(auth, slugOrDir) {
12628
12727
  const fromDir = (dir2) => {
12629
- const link = join5(dir2, ".zalify", "site.json");
12630
- if (existsSync5(link)) {
12728
+ const link = join6(dir2, ".zalify", "site.json");
12729
+ if (existsSync6(link)) {
12631
12730
  const parsed = JSON.parse(readFileSync5(link, "utf8"));
12632
12731
  if (parsed.slug)
12633
12732
  return parsed.slug;
12634
12733
  }
12635
- const storeJson = join5(dir2, "store.json");
12636
- if (existsSync5(storeJson)) {
12734
+ const storeJson = join6(dir2, "store.json");
12735
+ if (existsSync6(storeJson)) {
12637
12736
  const parsed = JSON.parse(readFileSync5(storeJson, "utf8"));
12638
12737
  if (parsed.slug)
12639
12738
  return parsed.slug;
@@ -12641,7 +12740,7 @@ async function resolveSite(auth, slugOrDir) {
12641
12740
  return null;
12642
12741
  };
12643
12742
  let slug = null;
12644
- if (slugOrDir && existsSync5(resolve4(slugOrDir)))
12743
+ if (slugOrDir && existsSync6(resolve4(slugOrDir)))
12645
12744
  slug = fromDir(resolve4(slugOrDir));
12646
12745
  if (!slug && slugOrDir)
12647
12746
  slug = slugOrDir;
@@ -12702,8 +12801,8 @@ async function maybeRefreshShops(auth, brandDir, siteSlugOverride) {
12702
12801
  try {
12703
12802
  let slug = siteSlugOverride ?? null;
12704
12803
  if (!slug) {
12705
- const storeJson = join5(resolve4(brandDir), "store.json");
12706
- if (existsSync5(storeJson)) {
12804
+ const storeJson = join6(resolve4(brandDir), "store.json");
12805
+ if (existsSync6(storeJson)) {
12707
12806
  slug = JSON.parse(readFileSync5(storeJson, "utf8")).slug ?? null;
12708
12807
  }
12709
12808
  }
@@ -12758,7 +12857,7 @@ async function gql(auth, query, variables = {}) {
12758
12857
  return json.data;
12759
12858
  }
12760
12859
  function readJson(path9, label) {
12761
- if (!existsSync6(path9)) {
12860
+ if (!existsSync7(path9)) {
12762
12861
  throw new Error(`No ${label} at ${path9}`);
12763
12862
  }
12764
12863
  return JSON.parse(readFileSync6(path9, "utf8"));
@@ -12788,7 +12887,7 @@ async function findProductId(auth, handle) {
12788
12887
  async function shopifyImport(storeDir, options = {}) {
12789
12888
  const auth = requireActive();
12790
12889
  const dir2 = resolve5(storeDir);
12791
- const catalog = readJson(join6(dir2, "catalog.json"), "catalog.json");
12890
+ const catalog = readJson(join7(dir2, "catalog.json"), "catalog.json");
12792
12891
  const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
12793
12892
  const locationId = locations.nodes[0]?.id;
12794
12893
  console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
@@ -12997,13 +13096,13 @@ async function shopifyImport(storeDir, options = {}) {
12997
13096
  async function shopifyUploadImages(storeDir, options = {}) {
12998
13097
  const auth = requireActive();
12999
13098
  const dir2 = resolve5(storeDir);
13000
- const manifest = readJson(join6(dir2, "images", "manifest.json"), "images/manifest.json");
13099
+ const manifest = readJson(join7(dir2, "images", "manifest.json"), "images/manifest.json");
13001
13100
  console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
13002
13101
  for (const entry of manifest.images) {
13003
13102
  if (entry.type === "banner" || !entry.handle)
13004
13103
  continue;
13005
- const filePath = join6(dir2, "images", entry.file);
13006
- if (!existsSync6(filePath)) {
13104
+ const filePath = join7(dir2, "images", entry.file);
13105
+ if (!existsSync7(filePath)) {
13007
13106
  console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
13008
13107
  continue;
13009
13108
  }
@@ -13075,9 +13174,160 @@ async function shopifyUploadImages(storeDir, options = {}) {
13075
13174
  await maybeRefreshShops(auth, dir2, options.site);
13076
13175
  }
13077
13176
 
13177
+ // src/ads.ts
13178
+ import { spawnSync as spawnSync4 } from "node:child_process";
13179
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
13180
+ import { basename as basename4, join as join8, resolve as resolve6 } from "node:path";
13181
+ function apiError3(status, json) {
13182
+ if (json.code === "SLUG_TAKEN") {
13183
+ return new Error(`${json.error}
13184
+ Pick a different directory/slug name.`);
13185
+ }
13186
+ if (status === 404) {
13187
+ return new Error("The repos API is not deployed yet (404). Update app.zalify.com first.");
13188
+ }
13189
+ if (status === 503) {
13190
+ return new Error(json.error ?? "Brand-repo provisioning is not configured on the server.");
13191
+ }
13192
+ return new Error(`repos API ${status}: ${JSON.stringify(json)}`);
13193
+ }
13194
+ async function request2(auth, method, path9, body) {
13195
+ const url = method === "GET" ? `${auth.appUrl}${path9}${path9.includes("?") ? "&" : "?"}workspaceId=${auth.workspaceId}` : `${auth.appUrl}${path9}`;
13196
+ const res = await fetch(url, {
13197
+ method,
13198
+ headers: {
13199
+ Authorization: `Bearer ${auth.key}`,
13200
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
13201
+ },
13202
+ ...method === "POST" ? { body: JSON.stringify({ workspaceId: auth.workspaceId, ...body }) } : {}
13203
+ });
13204
+ const json = await res.json().catch(() => ({}));
13205
+ if (!res.ok)
13206
+ throw apiError3(res.status, json);
13207
+ return json;
13208
+ }
13209
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
13210
+ async function pollUntilReady2(auth, id) {
13211
+ let lastMessage = "";
13212
+ const deadline = Date.now() + 5 * 60000;
13213
+ while (Date.now() < deadline) {
13214
+ const view = await request2(auth, "GET", `/api/repos/${id}`);
13215
+ const message = view.statusMessage ?? view.status;
13216
+ if (message !== lastMessage) {
13217
+ console.log(` … ${message}`);
13218
+ lastMessage = message;
13219
+ }
13220
+ if (view.status === "READY")
13221
+ return view;
13222
+ if (view.status === "FAILED") {
13223
+ throw new Error(`Provisioning failed: ${view.statusMessage ?? "unknown reason"}`);
13224
+ }
13225
+ await sleep3(3000);
13226
+ }
13227
+ throw new Error("Provisioning timed out after 5 minutes — check the Temporal UI.");
13228
+ }
13229
+ function scaffoldStarter(dir2, slug, workspaceId) {
13230
+ const root = resolve6(dir2);
13231
+ const write = (rel, content) => {
13232
+ const abs = join8(root, rel);
13233
+ mkdirSync3(join8(abs, ".."), { recursive: true });
13234
+ writeFileSync7(abs, content);
13235
+ };
13236
+ write("identity.yaml", `# Brand instance identity — which workspace and which platform accounts
13237
+ # this instance operates. Filled in during onboarding; the Autopilot
13238
+ # service refuses to run an instance whose consent flag is false.
13239
+ workspace_id: ${workspaceId}
13240
+ brand_slug: ${slug}
13241
+ consent: false # flip only with the brand owner's explicit consent
13242
+ platforms:
13243
+ meta:
13244
+ ad_account_ids: [] # act_… — the challenger account goes here
13245
+ page_id: ""
13246
+ pixel_id: ""
13247
+ `);
13248
+ write("brand.yaml", `# Brand voice + guardrails consumed by drafting (media plans, briefs).
13249
+ name: ${slug}
13250
+ vertical: ""
13251
+ voice: ""
13252
+ guidelines: {}
13253
+ special_ad_categories: []
13254
+ `);
13255
+ write("policy/config.yaml", `# Per-brand policy thresholds — data, not code. All values versioned via
13256
+ # git history; the pacing engine refuses to run without this file.
13257
+ version: 1
13258
+ proposal_cap_per_day: 8
13259
+ proposal_expiry_hours: 24
13260
+ cooldown_after_mutation_hours: 24
13261
+ maturity_windows:
13262
+ spend_hours: 2
13263
+ cpa_hours: 24
13264
+ roas_days: 3
13265
+ `);
13266
+ write("README.md", `# ${slug} — Autopilot brand instance
13267
+
13268
+ Campaign intent for this brand: media plans (\`media-plans/\`), ads-as-code
13269
+ campaign definitions (\`campaigns/\`), policy thresholds (\`policy/\`).
13270
+ Written by humans and the Autopilot drafting agent via PRs; the service
13271
+ mirrors decisions into \`decisions/\` and daily signal digests into
13272
+ \`signals/\`. Telemetry and tokens never live here.
13273
+ `);
13274
+ write("campaigns/.gitkeep", "");
13275
+ write("media-plans/.gitkeep", "");
13276
+ write("signals/digests/.gitkeep", "");
13277
+ write("decisions/.gitkeep", "");
13278
+ write(".gitignore", `.env
13279
+ .DS_Store
13280
+ `);
13281
+ }
13282
+ async function adsCreate(dir2) {
13283
+ const auth = requireActive();
13284
+ const slug = basename4(resolve6(dir2)).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "brand";
13285
+ console.log(`Provisioning brand instance "${slug}" (workspace "${auth.workspaceName}")`);
13286
+ const created = await request2(auth, "POST", "/api/repos", { slug });
13287
+ const view = await pollUntilReady2(auth, created.repoId);
13288
+ console.log(` ✓ provisioned: ${view.githubRepo}`);
13289
+ scaffoldStarter(dir2, slug, auth.workspaceId);
13290
+ mkdirSync3(join8(resolve6(dir2), ".zalify"), { recursive: true });
13291
+ writeFileSync7(join8(resolve6(dir2), ".zalify", "brand-repo.json"), JSON.stringify({ id: created.repoId, slug: view.slug, repo: view.githubRepo }, null, 2) + `
13292
+ `);
13293
+ if (view.githubRepo) {
13294
+ const { token, repo } = await request2(auth, "POST", `/api/repos/${created.repoId}/push-token`, {});
13295
+ const run = (args) => spawnSync4("git", args, { cwd: resolve6(dir2), stdio: "ignore" });
13296
+ run(["init", "-b", "main"]);
13297
+ run(["add", "-A"]);
13298
+ run(["commit", "-m", "Scaffold brand instance", "--no-verify"]);
13299
+ run(["remote", "add", "origin", `https://github.com/${repo}.git`]);
13300
+ const push = spawnSync4("git", ["push", `https://x-access-token:${token}@github.com/${repo}.git`, "main"], { cwd: resolve6(dir2), stdio: "inherit" });
13301
+ if (push.status !== 0) {
13302
+ throw new Error(`git push to ${repo} failed — resolve and push manually.`);
13303
+ }
13304
+ run(["update-ref", "refs/remotes/origin/main", "HEAD"]);
13305
+ run(["config", "branch.main.remote", "origin"]);
13306
+ run(["config", "branch.main.merge", "refs/heads/main"]);
13307
+ console.log(` ✓ pushed to github.com/${repo}`);
13308
+ }
13309
+ console.log(`
13310
+ Next steps:
13311
+ ` + ` - fill in identity.yaml (ad account ids) and brand.yaml
13312
+ ` + ` - flip consent: true once the brand owner has signed off
13313
+ ` + ` - the Autopilot service picks the instance up from there`);
13314
+ }
13315
+ async function adsList() {
13316
+ const auth = requireActive();
13317
+ const json = await request2(auth, "GET", "/api/repos");
13318
+ const repos = json.repos ?? [];
13319
+ if (!repos.length) {
13320
+ console.log(`No brand instances in workspace "${auth.workspaceName}".`);
13321
+ return;
13322
+ }
13323
+ for (const r of repos) {
13324
+ console.log(`${r.status === "READY" ? "✓" : r.status === "FAILED" ? "✗" : "…"} ${r.slug} ${r.githubRepo ?? "-"} (${r.status.toLowerCase()})`);
13325
+ }
13326
+ }
13327
+
13078
13328
  // src/brand.ts
13079
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
13080
- import { basename as basename4, join as join7, resolve as resolve6 } from "node:path";
13329
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
13330
+ import { basename as basename5, join as join9, resolve as resolve7 } from "node:path";
13081
13331
  var AUTHORING_MD = `# Brand authoring guide
13082
13332
 
13083
13333
  The rules for filling in this folder. An author — human or AI agent —
@@ -13378,19 +13628,19 @@ var MANIFEST_JSON = JSON.stringify({
13378
13628
  }, null, 2) + `
13379
13629
  `;
13380
13630
  function brandInit(dir2 = ".", opts = {}) {
13381
- const target = resolve6(dir2);
13382
- const slug = basename4(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13383
- if (existsSync7(target) && readdirSync3(target).length > 0) {
13631
+ const target = resolve7(dir2);
13632
+ const slug = basename5(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13633
+ if (existsSync8(target) && readdirSync3(target).length > 0) {
13384
13634
  throw new Error(`${dir2} already exists and is not empty`);
13385
13635
  }
13386
13636
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13387
13637
  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);
13638
+ mkdirSync4(join9(target, "images"), { recursive: true });
13639
+ writeFileSync8(join9(target, "AUTHORING.md"), AUTHORING_MD);
13640
+ writeFileSync8(join9(target, "brand.md"), BRAND_MD(name));
13641
+ writeFileSync8(join9(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
13642
+ writeFileSync8(join9(target, "catalog.json"), CATALOG_JSON(name, currency));
13643
+ writeFileSync8(join9(target, "images", "manifest.json"), MANIFEST_JSON);
13394
13644
  console.log(`Scaffolded brand data in ${dir2}:`);
13395
13645
  for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
13396
13646
  console.log(` + ${f}`);
@@ -13401,12 +13651,12 @@ Author in this order: brand.md → catalog.json → images/manifest.json` + `
13401
13651
  }
13402
13652
  var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
13403
13653
  function brandValidate(dir2 = ".") {
13404
- const target = resolve6(dir2);
13654
+ const target = resolve7(dir2);
13405
13655
  const problems = [];
13406
13656
  const warnings = [];
13407
13657
  const readJson2 = (rel) => {
13408
- const path9 = join7(target, rel);
13409
- if (!existsSync7(path9)) {
13658
+ const path9 = join9(target, rel);
13659
+ if (!existsSync8(path9)) {
13410
13660
  problems.push(`missing ${rel}`);
13411
13661
  return null;
13412
13662
  }
@@ -13417,7 +13667,7 @@ function brandValidate(dir2 = ".") {
13417
13667
  return null;
13418
13668
  }
13419
13669
  };
13420
- if (!existsSync7(join7(target, "brand.md")))
13670
+ if (!existsSync8(join9(target, "brand.md")))
13421
13671
  problems.push("missing brand.md");
13422
13672
  const store = readJson2("store.json");
13423
13673
  if (store && (!store.name || !store.slug))
@@ -13593,7 +13843,7 @@ function brandValidate(dir2 = ".") {
13593
13843
  // src/cli.ts
13594
13844
  var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
13595
13845
  var require2 = createRequire2(import.meta.url);
13596
- var pkg = require2(join8(__dirname4, "..", "package.json"));
13846
+ var pkg = require2(join10(__dirname4, "..", "package.json"));
13597
13847
  try {
13598
13848
  updateNotifier({ pkg }).notify({
13599
13849
  isGlobal: true,
@@ -13665,6 +13915,13 @@ shop.command("open [slug-or-dir]").description("Open the live shop in your brows
13665
13915
  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
13916
  await shopDomainSync(slugOrDir);
13667
13917
  });
13918
+ var ads = program2.command("ads").description("Autopilot brand instances — campaign intent repos (media plans, ads-as-code, policy config)");
13919
+ ads.command("create <dir>").description("Provision the brand-instance GitHub repo, scaffold the starter locally, and push").action(async (dir2) => {
13920
+ await adsCreate(dir2);
13921
+ });
13922
+ ads.command("list", { isDefault: true }).description("List the workspace's brand instances").action(async () => {
13923
+ await adsList();
13924
+ });
13668
13925
  var theme = program2.command("theme").description("Theme source code — scaffold a copy and upgrade it without losing your edits");
13669
13926
  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
13927
  await themeCreate(dir2, options);
@@ -13675,7 +13932,9 @@ theme.command("status [dir]").description("Which theme files have you edited? (c
13675
13932
  theme.command("upgrade [dir]").description("Move to a newer theme version: untouched files overwritten, edited files 3-way-merged, theme/ + .env never touched").option("--to <version>", "target theme version (default: latest)").option("--dry-run", "classify and report without writing anything").option("--install", "run install when dependencies changed").option("--tarball <path>", "target version as a local .tgz (testing)").option("--base-tarball <path>", "base version as a local .tgz (testing)").action(async (dir2, options) => {
13676
13933
  await themeUpgrade(dir2, options);
13677
13934
  });
13678
- program2.command("self-update").description("Update the CLI to the latest version (auto-detects pnpm/bun/npm)").action(() => selfUpdate(pkg.version));
13935
+ program2.command("self-update").description("Update the CLI to the latest version (auto-detects pnpm/bun/npm)").action(async () => {
13936
+ await selfUpdate(pkg.version);
13937
+ });
13679
13938
  program2.command("version").description("Print Zalify CLI version and check for updates").action(async () => {
13680
13939
  console.log(`Zalify CLI version: ${pkg.version}`);
13681
13940
  const isNewer = (a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",