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