@zalify/cli 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +44 -12
  2. package/dist/cli.js +306 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -12,14 +12,42 @@ npm i -g @zalify/cli@latest
12
12
 
13
13
  ## Usage
14
14
 
15
+ ### Authentication
16
+
15
17
  ```bash
16
- zalify version # Print version and check for updates
17
- zalify -v # Same as --version (built-in)
18
- zalify --version # Output current version only
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
19
22
  ```
20
23
 
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**.
29
+
30
+ ### Assets
31
+
32
+ Sync a directory's `images/*.png` with the workspace asset library
33
+ (the same storage that backs Assets in the Zalify app):
34
+
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
38
+ ```
39
+
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.
43
+
21
44
  ## Commands
22
45
 
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.
23
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.
24
52
 
25
53
  ## Development
@@ -35,17 +63,21 @@ bun run typecheck # Type-check only
35
63
 
36
64
  ## Publishing
37
65
 
38
- Publishing requires `NODE_AUTH_TOKEN`. Copy `.env.example` to `.env` and set your token (do not commit `.env`). You can also set `NODE_AUTH_TOKEN` in your shell.
66
+ CI publishes automatically (`.github/workflows/publish.yml`): pushing to
67
+ `main` with a `package.json` version that isn't on npm yet typechecks,
68
+ builds, publishes `@zalify/cli`, and tags the commit `v<version>`. The
69
+ `zalify/` wrapper (for `npm i -g zalify`) is published in the same run
70
+ whenever its own version is new. Auth is npm **Trusted Publishing** (OIDC,
71
+ no token secret): one-time setup per package on npmjs.com — package →
72
+ Settings → Publishing access → Add trusted publisher → GitHub Actions →
73
+ repo `zalify/zalify-cli`, workflow `publish.yml`.
39
74
 
40
- Release only bumps the version in `package.json` (no git commit or tag). Commit and tag the version bump yourself if desired. Bun loads `.env` so `NODE_AUTH_TOKEN` is available.
75
+ Release flow:
41
76
 
42
77
  ```bash
43
- bun run release # Default: patch (x.x.X)
44
- bun run release -- -pa # Patch
45
- bun run release -- -mi # Minor (x.X.0)
46
- bun run release -- -ma # Major (X.0.0)
78
+ npm version minor --no-git-tag-version # or patch/major
79
+ git commit -am "chore: release v0.x.0" && git push
47
80
  ```
48
81
 
49
- The release scripts load `.env` and fail with a clear error if `NODE_AUTH_TOKEN` is missing.
50
-
51
- To allow `npm i -g zalify`, publish the wrapper package after releasing `@zalify/cli`: from the repo root run `npm publish --access public` in the `zalify/` folder (it depends on `@zalify/cli` and exposes the same binary).
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
@@ -5,15 +5,29 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -991,7 +1005,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
991
1005
  this._exitCallback = (err) => {
992
1006
  if (err.code !== "commander.executeSubCommandAsync") {
993
1007
  throw err;
994
- } else {}
1008
+ }
995
1009
  };
996
1010
  }
997
1011
  return this;
@@ -5154,7 +5168,7 @@ var require_ini2 = __commonJS((exports) => {
5154
5168
 
5155
5169
  // node_modules/config-chain/index.js
5156
5170
  var require_config_chain = __commonJS((exports, module) => {
5157
- var __dirname = "/Users/zourenyuan/workspaces/Zalify/Eng/zalify-cli/node_modules/config-chain";
5171
+ var __dirname = "/home/runner/work/zalify-cli/zalify-cli/node_modules/config-chain";
5158
5172
  var ProtoList = require_proto_list();
5159
5173
  var path5 = __require("path");
5160
5174
  var fs3 = __require("fs");
@@ -11279,22 +11293,307 @@ function updateNotifier(options) {
11279
11293
 
11280
11294
  // src/cli.ts
11281
11295
  import { createRequire as createRequire2 } from "node:module";
11282
- import { dirname, join } from "node:path";
11296
+ import { dirname, join as join3 } from "node:path";
11283
11297
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11298
+
11299
+ // src/auth.ts
11300
+ import { randomBytes } from "node:crypto";
11301
+ import { createServer } from "node:http";
11302
+ import { hostname } from "node:os";
11303
+ import { spawn as spawn2 } from "node:child_process";
11304
+
11305
+ // src/config.ts
11306
+ import { readFileSync, writeFileSync as writeFileSync2, mkdirSync, rmSync, existsSync } from "node:fs";
11307
+ import { homedir } from "node:os";
11308
+ import { join } from "node:path";
11309
+ var dir = join(homedir(), ".zalify");
11310
+ var file = join(dir, "config.json");
11311
+ function readConfig() {
11312
+ if (!existsSync(file))
11313
+ return null;
11314
+ try {
11315
+ return JSON.parse(readFileSync(file, "utf8"));
11316
+ } catch {
11317
+ return null;
11318
+ }
11319
+ }
11320
+ function writeConfig(config) {
11321
+ mkdirSync(dir, { recursive: true, mode: 448 });
11322
+ writeFileSync2(file, JSON.stringify(config, null, 2) + `
11323
+ `, { mode: 384 });
11324
+ }
11325
+ function deleteConfig() {
11326
+ rmSync(file, { force: true });
11327
+ }
11328
+ function requireConfig() {
11329
+ const config = readConfig();
11330
+ if (!config?.key) {
11331
+ throw new Error("Not logged in. Run `zalify login` first.");
11332
+ }
11333
+ return config;
11334
+ }
11335
+
11336
+ // src/auth.ts
11337
+ var DEFAULT_APP_URL = "https://app.zalify.com";
11338
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
11339
+ function openBrowser(url) {
11340
+ if (process.env.ZALIFY_NO_BROWSER)
11341
+ return;
11342
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
11343
+ spawn2(cmd, [url], {
11344
+ shell: process.platform === "win32",
11345
+ stdio: "ignore",
11346
+ detached: true
11347
+ }).unref();
11348
+ }
11349
+ async function login(options) {
11350
+ const base = (options.appUrl || readConfig()?.appUrl || DEFAULT_APP_URL).replace(/\/$/, "");
11351
+ const state = randomBytes(32).toString("hex");
11352
+ const result = await new Promise((resolvePromise, reject) => {
11353
+ const timer = setTimeout(() => {
11354
+ server.close();
11355
+ reject(new Error("Login timed out after 5 minutes."));
11356
+ }, LOGIN_TIMEOUT_MS);
11357
+ const server = createServer((req, res) => {
11358
+ const url = new URL(req.url ?? "/", "http://localhost");
11359
+ if (url.pathname !== "/callback") {
11360
+ res.writeHead(404).end();
11361
+ return;
11362
+ }
11363
+ const done = (message) => {
11364
+ res.writeHead(200, { "Content-Type": "text/html" });
11365
+ res.end(`<!doctype html><meta charset="utf-8"><title>Zalify CLI</title>
11366
+ <body style="font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
11367
+ <p>${message} You can close this tab.</p></body>`);
11368
+ };
11369
+ if (url.searchParams.get("state") !== state) {
11370
+ done("Login failed: state mismatch.");
11371
+ return;
11372
+ }
11373
+ clearTimeout(timer);
11374
+ const error = url.searchParams.get("error");
11375
+ if (error) {
11376
+ done("Authorization was denied.");
11377
+ server.close();
11378
+ reject(new Error(`Authorization denied (${error}).`));
11379
+ return;
11380
+ }
11381
+ const key = url.searchParams.get("key");
11382
+ if (!key) {
11383
+ done("Login failed: no key in callback.");
11384
+ server.close();
11385
+ reject(new Error("Callback did not include a key."));
11386
+ return;
11387
+ }
11388
+ done("Logged in!");
11389
+ server.close();
11390
+ resolvePromise({
11391
+ key,
11392
+ workspaceId: url.searchParams.get("workspace_id") ?? "",
11393
+ workspaceName: url.searchParams.get("workspace_name") ?? "",
11394
+ workspaceSlug: url.searchParams.get("workspace_slug") ?? ""
11395
+ });
11396
+ });
11397
+ server.listen(0, "127.0.0.1", () => {
11398
+ const { port } = server.address();
11399
+ const authUrl = `${base}/cli/auth?port=${port}&state=${state}&host=${encodeURIComponent(hostname())}`;
11400
+ console.log(`Opening browser to authorize the Zalify CLI…
11401
+ ${authUrl}
11402
+ `);
11403
+ console.log("If the browser doesn't open, visit the URL above manually.");
11404
+ openBrowser(authUrl);
11405
+ });
11406
+ server.on("error", reject);
11407
+ });
11408
+ writeConfig({ appUrl: base, ...result, createdAt: new Date().toISOString() });
11409
+ console.log(`✓ Logged in to workspace "${result.workspaceName || result.workspaceId}" (key stored in ~/.zalify/config.json)`);
11410
+ }
11411
+ function logout() {
11412
+ deleteConfig();
11413
+ console.log("✓ Logged out (local credentials deleted).");
11414
+ console.log(" To revoke the key itself: app.zalify.com → Settings → Developer.");
11415
+ }
11416
+ function whoami() {
11417
+ const config = requireConfig();
11418
+ console.log(`workspace ${config.workspaceName || "(unknown)"} [${config.workspaceId}]`);
11419
+ console.log(`app ${config.appUrl}`);
11420
+ console.log(`key ${config.key.slice(0, 16)}…`);
11421
+ }
11422
+
11423
+ // src/assets.ts
11424
+ import { createHash } from "node:crypto";
11425
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2, readdirSync } from "node:fs";
11426
+ import { join as join2, resolve } from "node:path";
11427
+ var BATCH = 10;
11428
+ function imagesDirFor(storeDir) {
11429
+ const dir2 = join2(resolve(storeDir), "images");
11430
+ if (!existsSync2(dir2))
11431
+ throw new Error(`No images/ directory in ${storeDir}`);
11432
+ return dir2;
11433
+ }
11434
+ function readIndex(imagesDir) {
11435
+ const path9 = join2(imagesDir, "assets.json");
11436
+ return existsSync2(path9) ? JSON.parse(readFileSync2(path9, "utf8")) : {};
11437
+ }
11438
+ function pngDims(buf) {
11439
+ if (buf.length > 24 && buf.readUInt32BE(12) === 1229472850) {
11440
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
11441
+ }
11442
+ return {};
11443
+ }
11444
+ async function assetsPull(storeDir) {
11445
+ const imagesDir = imagesDirFor(storeDir);
11446
+ const index = readIndex(imagesDir);
11447
+ let pulled = 0;
11448
+ for (const [file2, meta] of Object.entries(index)) {
11449
+ const path9 = join2(imagesDir, file2);
11450
+ if (existsSync2(path9))
11451
+ continue;
11452
+ const res = await fetch(meta.url);
11453
+ if (!res.ok) {
11454
+ console.error(` ✗ ${file2}: ${res.status} from ${meta.url}`);
11455
+ continue;
11456
+ }
11457
+ writeFileSync3(path9, Buffer.from(await res.arrayBuffer()));
11458
+ pulled++;
11459
+ console.log(` ✓ pulled ${file2}`);
11460
+ }
11461
+ console.log(`Done: ${pulled} downloaded, ${Object.keys(index).length - pulled} already local.`);
11462
+ }
11463
+ async function assetsPush(storeDir) {
11464
+ const config = requireConfig();
11465
+ const imagesDir = imagesDirFor(storeDir);
11466
+ const indexPath = join2(imagesDir, "assets.json");
11467
+ const index = readIndex(imagesDir);
11468
+ async function api(path9, body) {
11469
+ const res = await fetch(`${config.appUrl}${path9}`, {
11470
+ method: "POST",
11471
+ headers: {
11472
+ "Content-Type": "application/json",
11473
+ Authorization: `Bearer ${config.key}`
11474
+ },
11475
+ body: JSON.stringify(body)
11476
+ });
11477
+ const json = await res.json().catch(() => ({}));
11478
+ if (!res.ok)
11479
+ throw new Error(`${path9} ${res.status}: ${JSON.stringify(json)}`);
11480
+ return json;
11481
+ }
11482
+ const files = readdirSync(imagesDir).filter((f) => f.endsWith(".png")).map((name) => {
11483
+ const bytes = readFileSync2(join2(imagesDir, name));
11484
+ return { name, bytes, checksum: createHash("sha256").update(bytes).digest("hex") };
11485
+ }).filter((f) => index[f.name]?.checksum !== f.checksum);
11486
+ if (!files.length) {
11487
+ console.log("Everything already pushed.");
11488
+ return;
11489
+ }
11490
+ console.log(`Pushing ${files.length} file(s) to "${config.workspaceName || config.workspaceId}"`);
11491
+ for (let i = 0;i < files.length; i += BATCH) {
11492
+ const batch = files.slice(i, i + BATCH);
11493
+ const { uploads } = await api("/api/assets/presign", {
11494
+ workspaceId: config.workspaceId,
11495
+ folderId: process.env.ZALIFY_FOLDER_ID ?? null,
11496
+ files: batch.map((f) => ({
11497
+ id: f.name,
11498
+ name: f.name,
11499
+ contentType: "image/png",
11500
+ size: f.bytes.length,
11501
+ checksum: f.checksum
11502
+ }))
11503
+ });
11504
+ const toFinalize = [];
11505
+ for (const up of uploads) {
11506
+ const file2 = batch.find((f) => f.name === up.id);
11507
+ if (!file2)
11508
+ continue;
11509
+ if (up.reused) {
11510
+ index[file2.name] = { assetId: up.assetId, url: up.publicUrl, checksum: file2.checksum };
11511
+ console.log(` - ${file2.name}: deduped (already in library)`);
11512
+ continue;
11513
+ }
11514
+ if (!up.uploadUrl) {
11515
+ console.error(` ✗ ${file2.name}: presign returned no uploadUrl`);
11516
+ continue;
11517
+ }
11518
+ const put = await fetch(up.uploadUrl, {
11519
+ method: "PUT",
11520
+ headers: {
11521
+ "Content-Type": "image/png",
11522
+ "Cache-Control": "public, max-age=31536000, immutable"
11523
+ },
11524
+ body: new Uint8Array(file2.bytes)
11525
+ });
11526
+ if (!put.ok) {
11527
+ console.error(` ✗ ${file2.name}: R2 PUT ${put.status}`);
11528
+ continue;
11529
+ }
11530
+ toFinalize.push({ file: file2, up });
11531
+ }
11532
+ if (toFinalize.length) {
11533
+ const { assets, pending } = await api("/api/assets/finalize", {
11534
+ workspaceId: config.workspaceId,
11535
+ items: toFinalize.map(({ file: file2, up }) => ({
11536
+ assetId: up.assetId,
11537
+ checksum: file2.checksum,
11538
+ ...pngDims(file2.bytes)
11539
+ }))
11540
+ });
11541
+ for (const asset of assets) {
11542
+ const hit = toFinalize.find(({ up }) => up.assetId === asset.id);
11543
+ if (!hit)
11544
+ continue;
11545
+ index[hit.file.name] = { assetId: asset.id, url: asset.url, checksum: hit.file.checksum };
11546
+ console.log(` ✓ ${hit.file.name}`);
11547
+ }
11548
+ for (const id of pending ?? []) {
11549
+ console.error(` ✗ finalize pending (PUT missing?) assetId=${id}`);
11550
+ }
11551
+ }
11552
+ writeFileSync3(indexPath, JSON.stringify(index, null, 2) + `
11553
+ `);
11554
+ }
11555
+ console.log("Done.");
11556
+ }
11557
+
11558
+ // src/cli.ts
11284
11559
  var __dirname4 = dirname(fileURLToPath3(import.meta.url));
11285
11560
  var require2 = createRequire2(import.meta.url);
11286
- var pkg = require2(join(__dirname4, "..", "package.json"));
11561
+ var pkg = require2(join3(__dirname4, "..", "package.json"));
11287
11562
  var program2 = new Command;
11288
11563
  program2.name("zalify").description("Zalify CLI - command-line interface for Zalify").version(pkg.version, "-v, --version", "output the current version");
11289
11564
  program2.command("version").description("Print Zalify CLI version and check for updates").action(async () => {
11290
11565
  console.log(`Zalify CLI version: ${pkg.version}`);
11566
+ const isNewer = (a, b) => {
11567
+ const pa = a.split("-")[0].split(".").map(Number);
11568
+ const pb = b.split("-")[0].split(".").map(Number);
11569
+ for (let i = 0;i < 3; i++) {
11570
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0))
11571
+ return (pa[i] ?? 0) > (pb[i] ?? 0);
11572
+ }
11573
+ return false;
11574
+ };
11291
11575
  try {
11292
11576
  const notifier = updateNotifier({ pkg });
11293
11577
  const update = await notifier.fetchInfo();
11294
- if (update && update.type !== "latest" && update.latest !== update.current) {
11578
+ if (update && isNewer(update.latest, update.current)) {
11295
11579
  console.log(`
11296
11580
  A new version (${update.latest}) is available. Run "npm i -g @zalify/cli@latest" to update.`);
11297
11581
  }
11298
11582
  } catch {}
11299
11583
  });
11300
- program2.parse();
11584
+ program2.command("login").description("Authenticate via the browser and store a workspace API key").option("--app-url <url>", "Zalify app origin (default: https://app.zalify.com)").action(async (options) => {
11585
+ await login(options);
11586
+ });
11587
+ program2.command("logout").description("Delete the stored CLI credentials").action(() => logout());
11588
+ program2.command("whoami").description("Show the authorized workspace and key prefix").action(() => whoami());
11589
+ var assets = program2.command("assets").description("Sync images with the Zalify asset library");
11590
+ assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
11591
+ await assetsPush(storeDir);
11592
+ });
11593
+ assets.command("pull <store-dir>").description("Download files listed in <store-dir>/images/assets.json").action(async (storeDir) => {
11594
+ await assetsPull(storeDir);
11595
+ });
11596
+ program2.parseAsync().catch((err) => {
11597
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
11598
+ process.exit(1);
11599
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "engines": {
18
18
  "node": ">=20",
19
- "bun": ">=1.0"
19
+ "bun": ">=1.3"
20
20
  },
21
21
  "keywords": [
22
22
  "zalify",