@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.
- package/README.md +106 -33
- package/dist/cli.js +362 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @zalify/cli
|
|
2
2
|
|
|
3
|
-
Zalify CLI
|
|
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
|
-
|
|
14
|
+
Already installed? `zalify self-update`.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
## The command model
|
|
16
17
|
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
zalify
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11753
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
11784
|
+
return {
|
|
11785
|
+
cmd: "bun",
|
|
11786
|
+
update: (pkg) => ["add", "-g", `${pkg}@latest`],
|
|
11787
|
+
remove: `bun remove -g ${PACKAGES.join(" ")}`
|
|
11788
|
+
};
|
|
11762
11789
|
}
|
|
11763
|
-
return {
|
|
11790
|
+
return {
|
|
11791
|
+
cmd: "npm",
|
|
11792
|
+
update: (pkg) => ["install", "-g", `${pkg}@latest`],
|
|
11793
|
+
remove: `npm rm -g ${PACKAGES.join(" ")}`
|
|
11794
|
+
};
|
|
11764
11795
|
}
|
|
11765
|
-
function
|
|
11766
|
-
|
|
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} ${
|
|
11823
|
+
console.log(`Updating via: ${manager.cmd} ${manager.update(pkg).join(" ")}
|
|
11769
11824
|
`);
|
|
11770
|
-
const result = spawnSync(cmd,
|
|
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} ${
|
|
11831
|
+
Try manually: ${manager.cmd} ${manager.update(pkg).join(" ")}`);
|
|
11775
11832
|
process.exit(result.status ?? 1);
|
|
11776
11833
|
}
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
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
|
|
11857
|
-
import { join as
|
|
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(
|
|
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 =
|
|
12023
|
-
const manifestPath =
|
|
12024
|
-
if (!
|
|
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 =
|
|
12029
|
-
const index =
|
|
12030
|
-
const jobStatePath =
|
|
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 (
|
|
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) => !
|
|
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
|
|
12065
|
-
import { basename as basename3, join as
|
|
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
|
|
12070
|
-
import { basename as basename2, join as
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
12128
|
-
if (!
|
|
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(
|
|
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 =
|
|
12147
|
-
if (!
|
|
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 =
|
|
12165
|
-
if (
|
|
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 =
|
|
12171
|
-
if (
|
|
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 =
|
|
12178
|
-
if (!
|
|
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 (
|
|
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(
|
|
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 =
|
|
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 =
|
|
12205
|
-
const useEditor = Boolean(opts.editor) &&
|
|
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 =
|
|
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,
|
|
12317
|
+
renameSync(full, join5(d, ".gitignore"));
|
|
12219
12318
|
}
|
|
12220
12319
|
})(targetDir);
|
|
12221
|
-
const pkgPath =
|
|
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(
|
|
12334
|
+
writeFileSync5(join5(targetDir, ".env"), lines.join(`
|
|
12236
12335
|
`) + `
|
|
12237
12336
|
`);
|
|
12238
12337
|
}
|
|
12239
12338
|
const manifest = readProjectManifest(targetDir);
|
|
12240
|
-
writeJson(
|
|
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 =
|
|
12281
|
-
if (!
|
|
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 =
|
|
12316
|
-
const newDir =
|
|
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 =
|
|
12335
|
-
const localExists =
|
|
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(
|
|
12375
|
-
const basePath =
|
|
12376
|
-
const newPath =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
12630
|
-
if (
|
|
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 =
|
|
12636
|
-
if (
|
|
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 &&
|
|
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 =
|
|
12706
|
-
if (
|
|
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 (!
|
|
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(
|
|
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(
|
|
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 =
|
|
13006
|
-
if (!
|
|
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
|
|
13080
|
-
import { basename as
|
|
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 =
|
|
13382
|
-
const slug =
|
|
13383
|
-
if (
|
|
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
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
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 =
|
|
13654
|
+
const target = resolve7(dir2);
|
|
13405
13655
|
const problems = [];
|
|
13406
13656
|
const warnings = [];
|
|
13407
13657
|
const readJson2 = (rel) => {
|
|
13408
|
-
const path9 =
|
|
13409
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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(() =>
|
|
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) => {
|