@zalify/cli 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +244 -38
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11308,11 +11308,36 @@ import { homedir } from "node:os";
11308
11308
  import { join } from "node:path";
11309
11309
  var dir = join(homedir(), ".zalify");
11310
11310
  var file = join(dir, "config.json");
11311
+ function migrate(raw) {
11312
+ if (!raw || typeof raw !== "object")
11313
+ return null;
11314
+ const obj = raw;
11315
+ if (obj.version === 3 && obj.userKey && obj.appUrl)
11316
+ return obj;
11317
+ if (obj.version === 2 && obj.workspaces && obj.appUrl)
11318
+ return obj;
11319
+ if (typeof obj.key === "string" && typeof obj.workspaceId === "string") {
11320
+ return {
11321
+ version: 2,
11322
+ appUrl: obj.appUrl ?? "https://app.zalify.com",
11323
+ activeWorkspaceId: obj.workspaceId,
11324
+ workspaces: {
11325
+ [obj.workspaceId]: {
11326
+ key: obj.key,
11327
+ name: obj.workspaceName ?? "",
11328
+ slug: obj.workspaceSlug ?? "",
11329
+ createdAt: obj.createdAt ?? new Date().toISOString()
11330
+ }
11331
+ }
11332
+ };
11333
+ }
11334
+ return null;
11335
+ }
11311
11336
  function readConfig() {
11312
11337
  if (!existsSync(file))
11313
11338
  return null;
11314
11339
  try {
11315
- return JSON.parse(readFileSync(file, "utf8"));
11340
+ return migrate(JSON.parse(readFileSync(file, "utf8")));
11316
11341
  } catch {
11317
11342
  return null;
11318
11343
  }
@@ -11325,12 +11350,38 @@ function writeConfig(config) {
11325
11350
  function deleteConfig() {
11326
11351
  rmSync(file, { force: true });
11327
11352
  }
11328
- function requireConfig() {
11353
+ function requireActive() {
11329
11354
  const config = readConfig();
11330
- if (!config?.key) {
11355
+ if (!config) {
11331
11356
  throw new Error("Not logged in. Run `zalify login` first.");
11332
11357
  }
11333
- return config;
11358
+ if (config.version === 3) {
11359
+ if (!config.active) {
11360
+ throw new Error("No active workspace. Run `zalify workspace list` then `zalify workspace set <slug>`.");
11361
+ }
11362
+ return {
11363
+ appUrl: config.appUrl,
11364
+ key: config.userKey,
11365
+ workspaceId: config.active.id,
11366
+ workspaceName: config.active.name,
11367
+ workspaceSlug: config.active.slug,
11368
+ config
11369
+ };
11370
+ }
11371
+ const ids = Object.keys(config.workspaces);
11372
+ if (ids.length === 0) {
11373
+ throw new Error("Not logged in. Run `zalify login` first.");
11374
+ }
11375
+ const workspaceId = config.activeWorkspaceId && config.workspaces[config.activeWorkspaceId] ? config.activeWorkspaceId : ids[0];
11376
+ const entry = config.workspaces[workspaceId];
11377
+ return {
11378
+ appUrl: config.appUrl,
11379
+ key: entry.key,
11380
+ workspaceId,
11381
+ workspaceName: entry.name,
11382
+ workspaceSlug: entry.slug,
11383
+ config
11384
+ };
11334
11385
  }
11335
11386
 
11336
11387
  // src/auth.ts
@@ -11346,10 +11397,26 @@ function openBrowser(url) {
11346
11397
  detached: true
11347
11398
  }).unref();
11348
11399
  }
11400
+ function decodePayload(encoded) {
11401
+ const b64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
11402
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
11403
+ }
11404
+ async function fetchWorkspaces(appUrl, userKey) {
11405
+ try {
11406
+ const res = await fetch(`${appUrl}/api/cli/workspaces`, {
11407
+ headers: { Authorization: `Bearer ${userKey}` }
11408
+ });
11409
+ if (!res.ok)
11410
+ return [];
11411
+ return (await res.json()).workspaces;
11412
+ } catch {
11413
+ return [];
11414
+ }
11415
+ }
11349
11416
  async function login(options) {
11350
11417
  const base = (options.appUrl || readConfig()?.appUrl || DEFAULT_APP_URL).replace(/\/$/, "");
11351
11418
  const state = randomBytes(32).toString("hex");
11352
- const result = await new Promise((resolvePromise, reject) => {
11419
+ const payload = await new Promise((resolvePromise, reject) => {
11353
11420
  const timer = setTimeout(() => {
11354
11421
  server.close();
11355
11422
  server.closeAllConnections();
@@ -11387,22 +11454,38 @@ async function login(options) {
11387
11454
  });
11388
11455
  return;
11389
11456
  }
11390
- const key = url.searchParams.get("key");
11391
- if (!key) {
11392
- done("Login failed: no key in callback.", () => {
11457
+ const encoded = url.searchParams.get("payload");
11458
+ const legacyKey = url.searchParams.get("key");
11459
+ let parsed = null;
11460
+ if (encoded) {
11461
+ try {
11462
+ parsed = decodePayload(encoded);
11463
+ } catch {
11464
+ parsed = null;
11465
+ }
11466
+ } else if (legacyKey) {
11467
+ parsed = {
11468
+ keys: [
11469
+ {
11470
+ workspaceId: url.searchParams.get("workspace_id") ?? "",
11471
+ workspaceName: url.searchParams.get("workspace_name") ?? "",
11472
+ workspaceSlug: url.searchParams.get("workspace_slug") ?? "",
11473
+ key: legacyKey
11474
+ }
11475
+ ]
11476
+ };
11477
+ }
11478
+ if (!parsed || !parsed.userKey && !parsed.keys?.length) {
11479
+ done("Login failed: no credentials in callback.", () => {
11393
11480
  teardown();
11394
- reject(new Error("Callback did not include a key."));
11481
+ reject(new Error("Callback did not include credentials."));
11395
11482
  });
11396
11483
  return;
11397
11484
  }
11485
+ const result = parsed;
11398
11486
  done("Logged in!", () => {
11399
11487
  teardown();
11400
- resolvePromise({
11401
- key,
11402
- workspaceId: url.searchParams.get("workspace_id") ?? "",
11403
- workspaceName: url.searchParams.get("workspace_name") ?? "",
11404
- workspaceSlug: url.searchParams.get("workspace_slug") ?? ""
11405
- });
11488
+ resolvePromise(result);
11406
11489
  });
11407
11490
  });
11408
11491
  server.listen(0, "127.0.0.1", () => {
@@ -11416,13 +11499,57 @@ async function login(options) {
11416
11499
  });
11417
11500
  server.on("error", reject);
11418
11501
  });
11419
- writeConfig({ appUrl: base, ...result, createdAt: new Date().toISOString() });
11420
- console.log(`✓ Logged in to workspace "${result.workspaceName || result.workspaceId}" (key stored in ~/.zalify/config.json)`);
11502
+ if (payload.userKey && payload.user) {
11503
+ const previous = readConfig();
11504
+ const previousActive = previous?.version === 3 ? previous.active : previous?.version === 2 && previous.activeWorkspaceId ? {
11505
+ id: previous.activeWorkspaceId,
11506
+ name: previous.workspaces[previous.activeWorkspaceId]?.name ?? "",
11507
+ slug: previous.workspaces[previous.activeWorkspaceId]?.slug ?? ""
11508
+ } : null;
11509
+ const workspaces = await fetchWorkspaces(base, payload.userKey);
11510
+ const active = previousActive && workspaces.find((w) => w.id === previousActive.id) || workspaces[0] || previousActive || null;
11511
+ writeConfig({
11512
+ version: 3,
11513
+ appUrl: base,
11514
+ userKey: payload.userKey,
11515
+ user: payload.user,
11516
+ active,
11517
+ workspacesCache: workspaces
11518
+ });
11519
+ console.log(`✓ Logged in as ${payload.user.name ?? payload.user.email ?? payload.user.id} — key covers ${workspaces.length || "your"} workspace(s), expires in 90 days.`);
11520
+ if (active)
11521
+ console.log(` Active workspace: ${active.name} (${active.slug})`);
11522
+ console.log(" Switch with `zalify workspace set <slug>`.");
11523
+ return;
11524
+ }
11525
+ const existing = readConfig();
11526
+ const config = {
11527
+ version: 2,
11528
+ appUrl: base,
11529
+ activeWorkspaceId: existing?.version === 2 ? existing.activeWorkspaceId : null,
11530
+ workspaces: {
11531
+ ...existing?.version === 2 && existing.appUrl === base ? existing.workspaces : {}
11532
+ }
11533
+ };
11534
+ const now = new Date().toISOString();
11535
+ for (const k of payload.keys ?? []) {
11536
+ config.workspaces[k.workspaceId] = {
11537
+ key: k.key,
11538
+ name: k.workspaceName,
11539
+ slug: k.workspaceSlug,
11540
+ createdAt: now
11541
+ };
11542
+ }
11543
+ if (!config.activeWorkspaceId || !config.workspaces[config.activeWorkspaceId]) {
11544
+ config.activeWorkspaceId = payload.keys?.[0]?.workspaceId ?? null;
11545
+ }
11546
+ writeConfig(config);
11547
+ console.log(`✓ Authorized ${payload.keys?.length ?? 0} workspace(s) (legacy keys).`);
11421
11548
  }
11422
11549
  function logout() {
11423
11550
  deleteConfig();
11424
11551
  console.log("✓ Logged out (local credentials deleted).");
11425
- console.log(" To revoke the key itself: app.zalify.com Settings → Developer.");
11552
+ console.log(" CLI keys expire after 90 days; revoke sooner via app.zalify.com if needed.");
11426
11553
  }
11427
11554
  function ago(iso) {
11428
11555
  if (!iso)
@@ -11439,17 +11566,15 @@ function ago(iso) {
11439
11566
  return `${Math.round(hours / 24)}d ago`;
11440
11567
  }
11441
11568
  async function whoami() {
11442
- const config = requireConfig();
11569
+ const auth = requireActive();
11443
11570
  let live = null;
11444
11571
  let liveError = null;
11445
11572
  try {
11446
- const res = await fetch(`${config.appUrl}/api/cli/whoami`, {
11447
- headers: { Authorization: `Bearer ${config.key}` }
11448
- });
11573
+ const res = await fetch(`${auth.appUrl}/api/cli/whoami?workspaceId=${encodeURIComponent(auth.workspaceId)}`, { headers: { Authorization: `Bearer ${auth.key}` } });
11449
11574
  if (res.ok) {
11450
11575
  live = await res.json();
11451
11576
  } else if (res.status === 401) {
11452
- liveError = "key is invalid or was revoked — run `zalify login`";
11577
+ liveError = "key is invalid, expired, or revoked — run `zalify login`";
11453
11578
  } else {
11454
11579
  liveError = `server returned ${res.status}`;
11455
11580
  }
@@ -11457,18 +11582,21 @@ async function whoami() {
11457
11582
  liveError = "could not reach the server (offline?)";
11458
11583
  }
11459
11584
  const ws = live?.workspace;
11460
- console.log(`workspace ${ws?.name ?? config.workspaceName ?? "(unknown)"} (${ws?.slug ?? config.workspaceSlug ?? "?"}) [${ws?.id ?? config.workspaceId}]`);
11585
+ const keyKind = auth.key.startsWith("zalify_cli_") ? "user key" : "workspace key";
11586
+ console.log(`workspace ${ws?.name ?? auth.workspaceName ?? "(unknown)"} (${ws?.slug ?? auth.workspaceSlug ?? "?"}) [${ws?.id ?? auth.workspaceId}]`);
11587
+ if (auth.config.version === 3) {
11588
+ console.log(`user ${auth.config.user.name ?? auth.config.user.email ?? auth.config.user.id}`);
11589
+ } else if (live?.key.createdByName) {
11590
+ console.log(`user ${live.key.createdByName}`);
11591
+ }
11461
11592
  if (live) {
11462
11593
  console.log(`plan ${live.plan.name} (${live.plan.type})`);
11463
- if (live.key.createdByName) {
11464
- console.log(`user ${live.key.createdByName}`);
11465
- }
11466
- console.log(`key ${live.key.name ?? config.key.slice(0, 16) + "…"} — created ${live.key.createdAt ? new Date(live.key.createdAt).toISOString().slice(0, 10) : "?"}, last used ${ago(live.key.lastRequest)}`);
11594
+ console.log(`key ${live.key.name ?? auth.key.slice(0, 16) + "…"} (${keyKind}) — created ${live.key.createdAt ? new Date(live.key.createdAt).toISOString().slice(0, 10) : "?"}, last used ${ago(live.key.lastRequest)}`);
11467
11595
  } else {
11468
- console.log(`key ${config.key.slice(0, 16)}…`);
11596
+ console.log(`key ${auth.key.slice(0, 16)}… (${keyKind})`);
11469
11597
  }
11470
- const slug = ws?.slug ?? config.workspaceSlug;
11471
- console.log(`app ${slug ? `${config.appUrl}/store/${slug}` : config.appUrl}`);
11598
+ const slug = ws?.slug ?? auth.workspaceSlug;
11599
+ console.log(`app ${slug ? `${auth.appUrl}/store/${slug}` : auth.appUrl}`);
11472
11600
  console.log(live ? "status ✓ key verified" : `status ! not verified — ${liveError}`);
11473
11601
  }
11474
11602
 
@@ -11513,23 +11641,23 @@ async function assetsPull(storeDir) {
11513
11641
  console.log(`Done: ${pulled} downloaded, ${Object.keys(index).length - pulled} already local.`);
11514
11642
  }
11515
11643
  async function assetsPush(storeDir) {
11516
- const config = requireConfig();
11644
+ const { appUrl, workspaceId, key, workspaceName, workspaceSlug } = requireActive();
11517
11645
  const imagesDir = imagesDirFor(storeDir);
11518
11646
  const indexPath = join2(imagesDir, "assets.json");
11519
11647
  const index = readIndex(imagesDir);
11520
11648
  async function api(path9, body) {
11521
- const res = await fetch(`${config.appUrl}${path9}`, {
11649
+ const res = await fetch(`${appUrl}${path9}`, {
11522
11650
  method: "POST",
11523
11651
  headers: {
11524
11652
  "Content-Type": "application/json",
11525
- Authorization: `Bearer ${config.key}`
11653
+ Authorization: `Bearer ${key}`
11526
11654
  },
11527
11655
  body: JSON.stringify(body)
11528
11656
  });
11529
11657
  const json = await res.json().catch(() => ({}));
11530
11658
  if (!res.ok) {
11531
11659
  if (json.code === "UPGRADE_REQUIRED") {
11532
- const billingUrl = config.workspaceSlug ? `${config.appUrl}/store/${config.workspaceSlug}/settings/billing` : `${config.appUrl} (Settings → Billing)`;
11660
+ const billingUrl = workspaceSlug ? `${appUrl}/store/${workspaceSlug}/settings/billing` : `${appUrl} (Settings → Billing)`;
11533
11661
  throw new Error(`${json.error ?? "This feature requires a paid plan."}
11534
11662
  ` + ` Upgrade this workspace: ${billingUrl}`);
11535
11663
  }
@@ -11545,11 +11673,11 @@ async function assetsPush(storeDir) {
11545
11673
  console.log("Everything already pushed.");
11546
11674
  return;
11547
11675
  }
11548
- console.log(`Pushing ${files.length} file(s) to "${config.workspaceName || config.workspaceId}"`);
11676
+ console.log(`Pushing ${files.length} file(s) to "${workspaceName || workspaceId}"`);
11549
11677
  for (let i = 0;i < files.length; i += BATCH) {
11550
11678
  const batch = files.slice(i, i + BATCH);
11551
11679
  const { uploads } = await api("/api/assets/presign", {
11552
- workspaceId: config.workspaceId,
11680
+ workspaceId,
11553
11681
  folderId: process.env.ZALIFY_FOLDER_ID ?? null,
11554
11682
  files: batch.map((f) => ({
11555
11683
  id: f.name,
@@ -11589,7 +11717,7 @@ async function assetsPush(storeDir) {
11589
11717
  }
11590
11718
  if (toFinalize.length) {
11591
11719
  const { assets, pending } = await api("/api/assets/finalize", {
11592
- workspaceId: config.workspaceId,
11720
+ workspaceId,
11593
11721
  items: toFinalize.map(({ file: file2, up }) => ({
11594
11722
  assetId: up.assetId,
11595
11723
  checksum: file2.checksum,
@@ -11648,6 +11776,77 @@ Try manually: ${cmd} ${args.join(" ")}`);
11648
11776
  ✓ Done${updated ? ` (version: ${updated})` : ""}`);
11649
11777
  }
11650
11778
 
11779
+ // src/workspace.ts
11780
+ async function fetchWorkspaces2(appUrl, userKey) {
11781
+ try {
11782
+ const res = await fetch(`${appUrl}/api/cli/workspaces`, {
11783
+ headers: { Authorization: `Bearer ${userKey}` }
11784
+ });
11785
+ if (!res.ok)
11786
+ return null;
11787
+ const json = await res.json();
11788
+ return json.workspaces;
11789
+ } catch {
11790
+ return null;
11791
+ }
11792
+ }
11793
+ async function workspaceList() {
11794
+ const config = readConfig();
11795
+ if (!config)
11796
+ throw new Error("Not logged in. Run `zalify login` first.");
11797
+ if (config.version === 3) {
11798
+ const live = await fetchWorkspaces2(config.appUrl, config.userKey);
11799
+ const list = live ?? config.workspacesCache;
11800
+ if (live) {
11801
+ config.workspacesCache = live;
11802
+ writeConfig(config);
11803
+ } else {
11804
+ console.log(`! could not fetch live list — showing cached workspaces
11805
+ `);
11806
+ }
11807
+ for (const w of list) {
11808
+ const marker = w.id === config.active?.id ? "* " : " ";
11809
+ console.log(`${marker}${w.slug.padEnd(36)} ${w.name} [${w.id}]`);
11810
+ }
11811
+ return;
11812
+ }
11813
+ const entries = Object.entries(config.workspaces);
11814
+ if (entries.length === 0)
11815
+ throw new Error("Not logged in. Run `zalify login` first.");
11816
+ for (const [id, w] of entries) {
11817
+ const marker = id === config.activeWorkspaceId ? "* " : " ";
11818
+ console.log(`${marker}${w.slug.padEnd(36)} ${w.name} [${id}]`);
11819
+ }
11820
+ }
11821
+ async function workspaceSet(slugOrId) {
11822
+ const config = readConfig();
11823
+ if (!config)
11824
+ throw new Error("Not logged in. Run `zalify login` first.");
11825
+ if (config.version === 3) {
11826
+ const live = await fetchWorkspaces2(config.appUrl, config.userKey);
11827
+ const list = live ?? config.workspacesCache;
11828
+ if (live)
11829
+ config.workspacesCache = live;
11830
+ const match = list.find((w) => w.id === slugOrId || w.slug === slugOrId || w.name === slugOrId);
11831
+ if (!match) {
11832
+ throw new Error(`No workspace matches "${slugOrId}". Known: ${list.map((w) => w.slug).join(", ")}.`);
11833
+ }
11834
+ config.active = match;
11835
+ writeConfig(config);
11836
+ console.log(`✓ Active workspace: ${match.name} (${match.slug})`);
11837
+ return;
11838
+ }
11839
+ const found = Object.entries(config.workspaces).find(([id, w]) => id === slugOrId || w.slug === slugOrId || w.name === slugOrId);
11840
+ if (!found) {
11841
+ const known = Object.values(config.workspaces).map((w) => w.slug).join(", ");
11842
+ throw new Error(`No authorized workspace matches "${slugOrId}". Known: ${known}.
11843
+ ` + "Run `zalify login` to authorize more workspaces.");
11844
+ }
11845
+ config.activeWorkspaceId = found[0];
11846
+ writeConfig(config);
11847
+ console.log(`✓ Active workspace: ${found[1].name} (${found[1].slug})`);
11848
+ }
11849
+
11651
11850
  // src/cli.ts
11652
11851
  var __dirname4 = dirname(fileURLToPath3(import.meta.url));
11653
11852
  var require2 = createRequire2(import.meta.url);
@@ -11688,6 +11887,13 @@ program2.command("logout").description("Delete the stored CLI credentials").acti
11688
11887
  program2.command("whoami").description("Verify the stored key and show workspace, plan, and key details").action(async () => {
11689
11888
  await whoami();
11690
11889
  });
11890
+ var workspace = program2.command("workspace").description("List authorized workspaces or switch the active one");
11891
+ workspace.command("list", { isDefault: true }).description("List authorized workspaces (* = active)").action(async () => {
11892
+ await workspaceList();
11893
+ });
11894
+ workspace.command("set <slug-or-id>").description("Switch the active workspace (local, no re-login)").action(async (slugOrId) => {
11895
+ await workspaceSet(slugOrId);
11896
+ });
11691
11897
  var assets = program2.command("assets").description("Sync images with the Zalify asset library");
11692
11898
  assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
11693
11899
  await assetsPush(storeDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",