@zeroxsolutions/zaku 0.0.2 → 0.0.4

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/dist/rules.js +1 -1
  2. package/dist/zaku.mjs +1581 -262
  3. package/package.json +1 -1
package/dist/zaku.mjs CHANGED
@@ -12066,7 +12066,7 @@ var require_websocket = __commonJS({
12066
12066
  var http = __require("http");
12067
12067
  var net = __require("net");
12068
12068
  var tls = __require("tls");
12069
- var { randomBytes, createHash: createHash2 } = __require("crypto");
12069
+ var { randomBytes, createHash: createHash3 } = __require("crypto");
12070
12070
  var { Duplex, Readable } = __require("stream");
12071
12071
  var { URL: URL2 } = __require("url");
12072
12072
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -12747,7 +12747,7 @@ var require_websocket = __commonJS({
12747
12747
  abortHandshake(websocket, socket, "Invalid Upgrade header");
12748
12748
  return;
12749
12749
  }
12750
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
12750
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
12751
12751
  if (res.headers["sec-websocket-accept"] !== digest) {
12752
12752
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
12753
12753
  return;
@@ -13116,7 +13116,7 @@ var require_websocket_server = __commonJS({
13116
13116
  var EventEmitter2 = __require("events");
13117
13117
  var http = __require("http");
13118
13118
  var { Duplex } = __require("stream");
13119
- var { createHash: createHash2 } = __require("crypto");
13119
+ var { createHash: createHash3 } = __require("crypto");
13120
13120
  var extension2 = require_extension();
13121
13121
  var PerMessageDeflate2 = require_permessage_deflate();
13122
13122
  var subprotocol2 = require_subprotocol();
@@ -13423,7 +13423,7 @@ var require_websocket_server = __commonJS({
13423
13423
  );
13424
13424
  }
13425
13425
  if (this._state > RUNNING) return abortHandshake(socket, 503);
13426
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
13426
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
13427
13427
  const headers = [
13428
13428
  "HTTP/1.1 101 Switching Protocols",
13429
13429
  "Upgrade: websocket",
@@ -17351,49 +17351,49 @@ var require_fast_uri = __commonJS({
17351
17351
  schemelessOptions.skipEscape = true;
17352
17352
  return serialize(resolved, schemelessOptions);
17353
17353
  }
17354
- function resolveComponent(base, relative, options, skipNormalization) {
17354
+ function resolveComponent(base, relative3, options, skipNormalization) {
17355
17355
  const target = {};
17356
17356
  if (!skipNormalization) {
17357
17357
  base = parse4(serialize(base, options), options);
17358
- relative = parse4(serialize(relative, options), options);
17358
+ relative3 = parse4(serialize(relative3, options), options);
17359
17359
  }
17360
17360
  options = options || {};
17361
- if (!options.tolerant && relative.scheme) {
17362
- target.scheme = relative.scheme;
17363
- target.userinfo = relative.userinfo;
17364
- target.host = relative.host;
17365
- target.port = relative.port;
17366
- target.path = removeDotSegments(relative.path || "");
17367
- target.query = relative.query;
17361
+ if (!options.tolerant && relative3.scheme) {
17362
+ target.scheme = relative3.scheme;
17363
+ target.userinfo = relative3.userinfo;
17364
+ target.host = relative3.host;
17365
+ target.port = relative3.port;
17366
+ target.path = removeDotSegments(relative3.path || "");
17367
+ target.query = relative3.query;
17368
17368
  } else {
17369
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
17370
- target.userinfo = relative.userinfo;
17371
- target.host = relative.host;
17372
- target.port = relative.port;
17373
- target.path = removeDotSegments(relative.path || "");
17374
- target.query = relative.query;
17369
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
17370
+ target.userinfo = relative3.userinfo;
17371
+ target.host = relative3.host;
17372
+ target.port = relative3.port;
17373
+ target.path = removeDotSegments(relative3.path || "");
17374
+ target.query = relative3.query;
17375
17375
  } else {
17376
- if (!relative.path) {
17376
+ if (!relative3.path) {
17377
17377
  target.path = base.path;
17378
- if (relative.query !== void 0) {
17379
- target.query = relative.query;
17378
+ if (relative3.query !== void 0) {
17379
+ target.query = relative3.query;
17380
17380
  } else {
17381
17381
  target.query = base.query;
17382
17382
  }
17383
17383
  } else {
17384
- if (relative.path[0] === "/") {
17385
- target.path = removeDotSegments(relative.path);
17384
+ if (relative3.path[0] === "/") {
17385
+ target.path = removeDotSegments(relative3.path);
17386
17386
  } else {
17387
17387
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
17388
- target.path = "/" + relative.path;
17388
+ target.path = "/" + relative3.path;
17389
17389
  } else if (!base.path) {
17390
- target.path = relative.path;
17390
+ target.path = relative3.path;
17391
17391
  } else {
17392
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
17392
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
17393
17393
  }
17394
17394
  target.path = removeDotSegments(target.path);
17395
17395
  }
17396
- target.query = relative.query;
17396
+ target.query = relative3.query;
17397
17397
  }
17398
17398
  target.userinfo = base.userinfo;
17399
17399
  target.host = base.host;
@@ -17401,7 +17401,7 @@ var require_fast_uri = __commonJS({
17401
17401
  }
17402
17402
  target.scheme = base.scheme;
17403
17403
  }
17404
- target.fragment = relative.fragment;
17404
+ target.fragment = relative3.fragment;
17405
17405
  return target;
17406
17406
  }
17407
17407
  function equal(uriA, uriB, options) {
@@ -20678,10 +20678,10 @@ var require_dist2 = __commonJS({
20678
20678
 
20679
20679
  // apps/zaku-cli/src/cli.ts
20680
20680
  var import_reflect_metadata3 = __toESM(require_Reflect(), 1);
20681
- import { readFile as readFile5 } from "node:fs/promises";
20681
+ import { readFile as readFile6 } from "node:fs/promises";
20682
20682
  import { createRequire } from "node:module";
20683
20683
  import { homedir as homedir2 } from "node:os";
20684
- import { dirname as dirname4, join as join10, resolve as resolve2, sep } from "node:path";
20684
+ import { dirname as dirname4, join as join12, resolve as resolve2, sep } from "node:path";
20685
20685
  import { fileURLToPath } from "node:url";
20686
20686
 
20687
20687
  // node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
@@ -21859,7 +21859,7 @@ var Command = class _Command extends EventEmitter {
21859
21859
  this._outputConfiguration = {
21860
21860
  writeOut: (str) => process2.stdout.write(str),
21861
21861
  writeErr: (str) => process2.stderr.write(str),
21862
- outputError: (str, write) => write(str),
21862
+ outputError: (str, write2) => write2(str),
21863
21863
  getOutHelpWidth: () => process2.stdout.isTTY ? process2.stdout.columns : void 0,
21864
21864
  getErrHelpWidth: () => process2.stderr.isTTY ? process2.stderr.columns : void 0,
21865
21865
  getOutHasColors: () => useColor() ?? (process2.stdout.isTTY && process2.stdout.hasColors?.()),
@@ -23844,11 +23844,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
23844
23844
  hasColors = this._outputConfiguration.getOutHasColors();
23845
23845
  helpWidth = this._outputConfiguration.getOutHelpWidth();
23846
23846
  }
23847
- const write = (str) => {
23847
+ const write2 = (str) => {
23848
23848
  if (!hasColors) str = this._outputConfiguration.stripColor(str);
23849
23849
  return baseWrite(str);
23850
23850
  };
23851
- return { error: error62, write, hasColors, helpWidth };
23851
+ return { error: error62, write: write2, hasColors, helpWidth };
23852
23852
  }
23853
23853
  /**
23854
23854
  * Output help information for this command.
@@ -24178,11 +24178,15 @@ var WriteRecipe = class extends DomainCommand {
24178
24178
  // packages/zaku/dist/lib/commands/save-library.js
24179
24179
  var SaveLibrary = class extends DomainCommand {
24180
24180
  root;
24181
+ key;
24181
24182
  part;
24182
24183
  constructor(input2) {
24183
24184
  super();
24184
24185
  this.root = input2.root;
24185
- this.part = input2.part;
24186
+ if (input2.key !== void 0)
24187
+ this.key = input2.key;
24188
+ if (input2.part !== void 0)
24189
+ this.part = input2.part;
24186
24190
  }
24187
24191
  };
24188
24192
 
@@ -24325,8 +24329,8 @@ function parseCssColour(value) {
24325
24329
  return null;
24326
24330
  return name === "oklch" ? oklchToRgba(l, x, y, round(alpha)) : oklabToRgba(l, x, y, round(alpha));
24327
24331
  }
24328
- function rgbaToOklab(colour2) {
24329
- const [r, g, b] = [decode(colour2.r), decode(colour2.g), decode(colour2.b)];
24332
+ function rgbaToOklab(colour3) {
24333
+ const [r, g, b] = [decode(colour3.r), decode(colour3.g), decode(colour3.b)];
24330
24334
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
24331
24335
  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
24332
24336
  const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
@@ -24341,12 +24345,12 @@ function deltaEOk(a, b) {
24341
24345
  const [l2, a2, b2] = rgbaToOklab(b);
24342
24346
  return Math.round(Math.hypot(l1 - l2, a1 - a2, b1 - b2, a.a - b.a) * 100 * 1e3) / 1e3;
24343
24347
  }
24344
- function toHex(colour2) {
24345
- const channels = [colour2.r, colour2.g, colour2.b].map((value) => Math.round(clamp(value) * 255).toString(16).padStart(2, "0"));
24348
+ function toHex(colour3) {
24349
+ const channels = [colour3.r, colour3.g, colour3.b].map((value) => Math.round(clamp(value) * 255).toString(16).padStart(2, "0"));
24346
24350
  return `#${channels.join("")}`;
24347
24351
  }
24348
- function luminance(colour2) {
24349
- return 0.2126 * decode(colour2.r) + 0.7152 * decode(colour2.g) + 0.0722 * decode(colour2.b);
24352
+ function luminance(colour3) {
24353
+ return 0.2126 * decode(colour3.r) + 0.7152 * decode(colour3.g) + 0.0722 * decode(colour3.b);
24350
24354
  }
24351
24355
  function contrastRatio(fg, bg) {
24352
24356
  const over = (front, back) => front * fg.a + back * (1 - fg.a);
@@ -44393,6 +44397,18 @@ var FigmaTokenMissing = class extends Error {
44393
44397
  }
44394
44398
  };
44395
44399
 
44400
+ // packages/zaku/dist/lib/domain/errors/library-key-missing.js
44401
+ var LibraryKeyMissing = class extends Error {
44402
+ keys;
44403
+ given;
44404
+ constructor(keys, given) {
44405
+ super(given === void 0 ? `zaku.yaml lists several libraries and kits; name one with --key: ${keys.join(", ")}` : `${given} is not a library or kit zaku.yaml lists; name one with --key: ${keys.join(", ")}`);
44406
+ this.keys = keys;
44407
+ this.given = given;
44408
+ this.name = "LibraryKeyMissing";
44409
+ }
44410
+ };
44411
+
44396
44412
  // packages/zaku/dist/lib/domain/errors/playwright-missing.js
44397
44413
  var PlaywrightMissing = class extends Error {
44398
44414
  constructor() {
@@ -44518,18 +44534,18 @@ function radiiOf(raw) {
44518
44534
  return fitRadii(corners, raw.width, raw.height);
44519
44535
  }
44520
44536
  function partOf(raw, at) {
44521
- const colour2 = (field, value) => {
44537
+ const colour3 = (field, value) => {
44522
44538
  const parsed = parseCssColour(value);
44523
44539
  if (!parsed)
44524
44540
  throw new Error(`${at}, part ${raw.part}: ${field} is ${value}, which zaku does not read`);
44525
44541
  return parsed;
44526
44542
  };
44527
- const background = colour2("background", raw.background);
44543
+ const background = colour3("background", raw.background);
44528
44544
  const gap = Number.parseFloat(raw.gap);
44529
44545
  return {
44530
44546
  background: background.a === 0 ? null : background,
44531
- color: colour2("color", raw.color),
44532
- borderColor: raw.borderWidths.some((width) => width > 0) ? colour2("borderColor", raw.borderColor) : null,
44547
+ color: colour3("color", raw.color),
44548
+ borderColor: raw.borderWidths.some((width) => width > 0) ? colour3("borderColor", raw.borderColor) : null,
44533
44549
  borderWidths: raw.borderWidths,
44534
44550
  width: raw.width,
44535
44551
  height: raw.height,
@@ -44611,13 +44627,106 @@ WriteRecipeHandler = __decorate2([
44611
44627
 
44612
44628
  // packages/zaku/dist/lib/handlers/save-library.js
44613
44629
  var import_tsyringe3 = __toESM(require_cjs(), 1);
44614
- import { writeFile as writeFile2 } from "node:fs/promises";
44630
+ import { relative as relative2 } from "node:path";
44615
44631
 
44616
44632
  // packages/zaku/dist/lib/domain/copy-defaults.js
44617
44633
  var DEFAULT_COPY = { locales: ["en"], currencies: [] };
44618
44634
 
44635
+ // packages/zaku/dist/lib/domain/design-graph.js
44636
+ var DESKTOP_FAMILIES = /* @__PURE__ */ new Set(["macos", "windows", "gnome", "kde"]);
44637
+ function defaultBranch(family) {
44638
+ if (family === "web")
44639
+ return "ui";
44640
+ return DESKTOP_FAMILIES.has(family) ? void 0 : "native";
44641
+ }
44642
+ function defaultPlatform(family) {
44643
+ if (family === "web")
44644
+ return "web-app";
44645
+ return DESKTOP_FAMILIES.has(family) ? "desktop" : "mobile";
44646
+ }
44647
+ var branchOf = (target) => target.branch ?? defaultBranch(target.family);
44648
+ var platformOf = (target) => target.platform ?? defaultPlatform(target.family);
44649
+ var unique = (values) => [...new Set(values)];
44650
+ function legacyFiles(config2) {
44651
+ const branches = unique(config2.targets.flatMap((target) => branchOf(target) ?? []));
44652
+ const platforms = unique(config2.targets.map(platformOf));
44653
+ const { library, product } = config2.figma;
44654
+ return {
44655
+ libraries: library === void 0 ? [] : [{ key: library, layer: "foundations", branches: branches.length > 0 ? branches : ["ui"], extends: [] }],
44656
+ products: product === void 0 ? [] : platforms.map((platform) => ({ key: product, platform }))
44657
+ };
44658
+ }
44659
+ function designGraph(config2) {
44660
+ const { figma } = config2;
44661
+ const legacy = figma.library !== void 0 || figma.product !== void 0;
44662
+ const { libraries, products } = legacy ? legacyFiles(config2) : { libraries: figma.libraries ?? [], products: figma.products ?? [] };
44663
+ const kits = figma.kits ?? [];
44664
+ const prototypes = figma.prototypes ?? [];
44665
+ const byKey = new Map(libraries.map((library) => [library.key, library]));
44666
+ const closure = (key) => {
44667
+ const out = [];
44668
+ const visit2 = (next) => {
44669
+ if (out.includes(next))
44670
+ return;
44671
+ out.push(next);
44672
+ for (const base of byKey.get(next)?.extends ?? [])
44673
+ visit2(base);
44674
+ };
44675
+ visit2(key);
44676
+ return out;
44677
+ };
44678
+ return {
44679
+ libraries,
44680
+ kits,
44681
+ products,
44682
+ prototypes,
44683
+ legacy,
44684
+ branchOf,
44685
+ platformOf,
44686
+ librariesFor: (target) => {
44687
+ if (legacy)
44688
+ return libraries;
44689
+ const branch = branchOf(target);
44690
+ return libraries.filter((library) => branch !== void 0 && library.branches.includes(branch));
44691
+ },
44692
+ kitsFor: (target) => kits.filter((kit) => kit.family === target.family),
44693
+ closure,
44694
+ multiBranch: (key) => (byKey.get(key)?.branches.length ?? 0) > 1,
44695
+ productOf: (prototypeKey) => {
44696
+ const prototype2 = prototypes.find((candidate) => candidate.key === prototypeKey);
44697
+ return prototype2?.product ?? (products.length === 1 ? products[0]?.key : void 0);
44698
+ }
44699
+ };
44700
+ }
44701
+ function snapshotsFor(input2, target) {
44702
+ const graph = designGraph(input2.config);
44703
+ const branch = target ? graph.branchOf(target) : void 0;
44704
+ const missing = [];
44705
+ const libraries = (target ? graph.librariesFor(target) : graph.libraries).flatMap((library) => {
44706
+ const snapshot = input2.libraries[library.key];
44707
+ if (!snapshot) {
44708
+ missing.push(library.key);
44709
+ return [];
44710
+ }
44711
+ if (!target || !graph.multiBranch(library.key))
44712
+ return [snapshot];
44713
+ const components = snapshot.components.filter((component2) => component2.branch === void 0 || component2.branch === branch);
44714
+ return [{ ...snapshot, components }];
44715
+ });
44716
+ const kits = (target ? graph.kitsFor(target) : graph.kits).flatMap((kit) => {
44717
+ const snapshot = input2.kits[kit.key];
44718
+ if (!snapshot)
44719
+ missing.push(kit.key);
44720
+ return snapshot ? [snapshot] : [];
44721
+ });
44722
+ return { libraries, kits, missing };
44723
+ }
44724
+
44619
44725
  // packages/zaku/dist/lib/schema/zaku-config.js
44620
44726
  var PLATFORM_FAMILIES = ["web", "ios", "android", "macos", "windows", "gnome", "kde"];
44727
+ var LAYERS = ["foundations", "components"];
44728
+ var BRANCHES = ["ui", "native"];
44729
+ var PLATFORMS = ["web-app", "mobile", "desktop"];
44621
44730
  var DEFAULT_INTERACTIVE = [
44622
44731
  "Button",
44623
44732
  "Button Group",
@@ -44644,7 +44753,11 @@ var targetSchema = external_exports.object({
44644
44753
  family: external_exports.enum(PLATFORM_FAMILIES),
44645
44754
  name: external_exports.string().regex(/^\S(.*\S)?$/, "a name has no leading or trailing space").refine((name) => !name.includes("/"), "a name has no slash, which separates the parts of a frame name"),
44646
44755
  /** The frame size every frame of this target is drawn at; the frame check compares it. */
44647
- size: external_exports.object({ width: external_exports.number().positive(), height: external_exports.number().positive() }).strict().optional()
44756
+ size: external_exports.object({ width: external_exports.number().positive(), height: external_exports.number().positive() }).strict().optional(),
44757
+ /** The renderer whose libraries its frames draw from; `defaultBranch` in the design graph gives it when left out. */
44758
+ branch: external_exports.enum(BRANCHES).optional(),
44759
+ /** What its frames serve; `defaultPlatform` in the design graph gives it when left out. */
44760
+ platform: external_exports.enum(PLATFORMS).optional()
44648
44761
  }).strict();
44649
44762
  var DEFAULT_SYSTEM_BARS = {
44650
44763
  web: [],
@@ -44664,7 +44777,33 @@ var budgetSchema = external_exports.object({
44664
44777
  mcpPerRun: external_exports.number().int().positive().default(30),
44665
44778
  reserve: external_exports.number().min(0).max(1).default(0.2)
44666
44779
  }).strict();
44667
- var files = external_exports.object({ library: external_exports.string().min(1).optional(), product: external_exports.string().min(1).optional() }).strict();
44780
+ var fileKey = external_exports.string().min(1);
44781
+ var kitSchema = external_exports.object({ key: fileKey, family: external_exports.enum(PLATFORM_FAMILIES) }).strict();
44782
+ var librarySchema = external_exports.object({
44783
+ key: fileKey,
44784
+ layer: external_exports.enum(LAYERS),
44785
+ branches: external_exports.array(external_exports.enum(BRANCHES)).min(1),
44786
+ /** The libraries and kits this one is built on, by key. */
44787
+ extends: external_exports.array(fileKey).default([])
44788
+ }).strict();
44789
+ var productSchema = external_exports.object({ key: fileKey, platform: external_exports.enum(PLATFORMS) }).strict();
44790
+ var prototypeSchema = external_exports.object({
44791
+ key: fileKey,
44792
+ platform: external_exports.enum(PLATFORMS),
44793
+ /** The product file its frames are copied from; required when there are several. */
44794
+ product: fileKey.optional()
44795
+ }).strict();
44796
+ var files = external_exports.object({
44797
+ library: fileKey.optional(),
44798
+ product: fileKey.optional(),
44799
+ kits: external_exports.array(kitSchema).optional(),
44800
+ libraries: external_exports.array(librarySchema).optional(),
44801
+ products: external_exports.array(productSchema).optional(),
44802
+ prototypes: external_exports.array(prototypeSchema).optional()
44803
+ }).strict();
44804
+ function byRole(figma) {
44805
+ return [figma.kits, figma.libraries, figma.products, figma.prototypes].some((list) => list !== void 0);
44806
+ }
44668
44807
  var shadcnSystem = external_exports.object({ preset: external_exports.string().min(1) }).strict();
44669
44808
  var tokenFiles = external_exports.array(external_exports.string().min(1)).min(1);
44670
44809
  var resolverInputs = external_exports.record(external_exports.string(), external_exports.string());
@@ -44716,7 +44855,7 @@ var copySchema = external_exports.object({
44716
44855
  locales: external_exports.array(external_exports.string().refine(writtenInAScript, "a BCP 47 language tag, such as en or vi")).min(1).default(["en"]),
44717
44856
  currencies: external_exports.array(external_exports.string().refine((code) => Intl.supportedValuesOf("currency").includes(code), "an ISO 4217 code, such as VND or USD")).default([])
44718
44857
  }).strict();
44719
- var zakuConfigSchema = external_exports.object({
44858
+ var configShape = external_exports.object({
44720
44859
  product: external_exports.string().min(1),
44721
44860
  designSystem: designSystemSchema,
44722
44861
  figma: files.default({}),
@@ -44728,7 +44867,8 @@ var zakuConfigSchema = external_exports.object({
44728
44867
  budget: budgetSchema.default({ mcpPerDay: 200, mcpPerRun: 30, reserve: 0.2 }),
44729
44868
  recipe: recipeSchema.optional(),
44730
44869
  copy: copySchema.default(DEFAULT_COPY)
44731
- }).strict().superRefine((config2, ctx) => {
44870
+ }).strict();
44871
+ var zakuConfigSchema = configShape.superRefine((config2, ctx) => {
44732
44872
  for (const key of ["id", "name"]) {
44733
44873
  const seen = /* @__PURE__ */ new Set();
44734
44874
  config2.targets.forEach((target, index) => {
@@ -44742,7 +44882,112 @@ var zakuConfigSchema = external_exports.object({
44742
44882
  seen.add(target[key]);
44743
44883
  });
44744
44884
  }
44885
+ if (byRole(config2.figma))
44886
+ refineFilesByRole(config2, ctx);
44745
44887
  });
44888
+ function refineFilesByRole(config2, ctx) {
44889
+ const { figma } = config2;
44890
+ if (figma.library !== void 0 || figma.product !== void 0) {
44891
+ ctx.addIssue({
44892
+ code: "custom",
44893
+ path: ["figma"],
44894
+ message: "figma.library and figma.product describe one library and one product; list files by role instead"
44895
+ });
44896
+ }
44897
+ const kits = figma.kits ?? [];
44898
+ const libraries = figma.libraries ?? [];
44899
+ const products = figma.products ?? [];
44900
+ const seen = /* @__PURE__ */ new Set();
44901
+ const lists = [
44902
+ ["kits", kits],
44903
+ ["libraries", libraries],
44904
+ ["products", products],
44905
+ ["prototypes", figma.prototypes ?? []]
44906
+ ];
44907
+ for (const [list, entries] of lists) {
44908
+ entries.forEach((entry, index) => {
44909
+ if (seen.has(entry.key))
44910
+ ctx.addIssue({
44911
+ code: "custom",
44912
+ path: ["figma", list, index, "key"],
44913
+ message: `a second file with key ${entry.key}`
44914
+ });
44915
+ seen.add(entry.key);
44916
+ });
44917
+ }
44918
+ const byKey = new Map(libraries.map((library) => [library.key, library]));
44919
+ const kitKeys = new Set(kits.map((kit) => kit.key));
44920
+ libraries.forEach((library, index) => {
44921
+ const path2 = ["figma", "libraries", index, "extends"];
44922
+ for (const key of library.extends) {
44923
+ const base = byKey.get(key);
44924
+ if (!base && !kitKeys.has(key)) {
44925
+ ctx.addIssue({
44926
+ code: "custom",
44927
+ path: path2,
44928
+ message: `${library.key} extends ${key}, which is no library or kit listed`
44929
+ });
44930
+ } else if (base?.layer === "components" && library.layer === "foundations") {
44931
+ ctx.addIssue({ code: "custom", path: path2, message: `foundations ${library.key} extends components ${key}` });
44932
+ } else if (base && !base.branches.some((branch) => library.branches.includes(branch))) {
44933
+ ctx.addIssue({
44934
+ code: "custom",
44935
+ path: path2,
44936
+ message: `${library.key} extends ${key}, which holds none of its branches`
44937
+ });
44938
+ }
44939
+ }
44940
+ });
44941
+ const cycle = findCycle(libraries);
44942
+ if (cycle)
44943
+ ctx.addIssue({ code: "custom", path: ["figma", "libraries"], message: `extends comes back: ${cycle}` });
44944
+ (figma.prototypes ?? []).forEach((prototype2, index) => {
44945
+ const path2 = ["figma", "prototypes", index];
44946
+ if (prototype2.product === void 0) {
44947
+ if (products.length > 1)
44948
+ ctx.addIssue({ code: "custom", path: [...path2, "product"], message: "name the product its frames come from" });
44949
+ return;
44950
+ }
44951
+ const product = products.find((candidate) => candidate.key === prototype2.product);
44952
+ if (!product) {
44953
+ ctx.addIssue({ code: "custom", path: [...path2, "product"], message: `no product with key ${prototype2.product}` });
44954
+ } else if (product.platform !== prototype2.platform) {
44955
+ ctx.addIssue({
44956
+ code: "custom",
44957
+ path: [...path2, "platform"],
44958
+ message: `${prototype2.platform}, and its product serves ${product.platform}`
44959
+ });
44960
+ }
44961
+ });
44962
+ config2.targets.forEach((target, index) => {
44963
+ const branch = target.branch ?? defaultBranch(target.family);
44964
+ const path2 = ["targets", index, "branch"];
44965
+ if (branch === void 0) {
44966
+ ctx.addIssue({ code: "custom", path: path2, message: `a ${target.family} target names its branch, ui or native` });
44967
+ } else if (libraries.length > 0 && !libraries.some((library) => library.branches.includes(branch))) {
44968
+ ctx.addIssue({ code: "custom", path: path2, message: `no library holds the ${branch} branch` });
44969
+ }
44970
+ });
44971
+ }
44972
+ function findCycle(libraries) {
44973
+ const byKey = new Map(libraries.map((library) => [library.key, library]));
44974
+ const visit2 = (key, path2) => {
44975
+ if (path2.includes(key))
44976
+ return [...path2.slice(path2.indexOf(key)), key].join(" -> ");
44977
+ for (const next of byKey.get(key)?.extends ?? []) {
44978
+ const found = visit2(next, [...path2, key]);
44979
+ if (found)
44980
+ return found;
44981
+ }
44982
+ return void 0;
44983
+ };
44984
+ for (const library of libraries) {
44985
+ const found = visit2(library.key, []);
44986
+ if (found)
44987
+ return found;
44988
+ }
44989
+ return void 0;
44990
+ }
44746
44991
 
44747
44992
  // packages/zaku/dist/lib/repositories/design-file.js
44748
44993
  var import_yaml = __toESM(require_dist(), 1);
@@ -44783,6 +45028,12 @@ async function writeDesignFile(path2, value) {
44783
45028
 
44784
45029
  // packages/zaku/dist/lib/repositories/design-paths.js
44785
45030
  import { join as join2 } from "node:path";
45031
+ function librarySnapshotDir(paths, key) {
45032
+ if (key === null)
45033
+ return paths;
45034
+ const library = join2(paths.library, key);
45035
+ return { root: paths.root, library, libraryIndex: join2(library, "index.json"), legacyLibrary: null };
45036
+ }
44786
45037
  function designPaths(root) {
44787
45038
  return {
44788
45039
  root,
@@ -44790,12 +45041,18 @@ function designPaths(root) {
44790
45041
  maps: join2(root, "map"),
44791
45042
  outline: join2(root, "outline"),
44792
45043
  unmapped: join2(root, "outline", "unmapped.yaml"),
44793
- library: join2(root, "library.json"),
45044
+ library: join2(root, "library"),
45045
+ libraryIndex: join2(root, "library", "index.json"),
45046
+ legacyLibrary: join2(root, "library.json"),
44794
45047
  tokens: join2(root, "tokens.json"),
44795
45048
  recipe: join2(root, "recipe.json")
44796
45049
  };
44797
45050
  }
44798
45051
 
45052
+ // packages/zaku/dist/lib/repositories/library-snapshot.js
45053
+ import { mkdir, readdir, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "node:fs/promises";
45054
+ import { join as join3, relative } from "node:path";
45055
+
44799
45056
  // packages/zaku/dist/lib/domain/library.js
44800
45057
  var rgbaSchema = external_exports.object({ r: external_exports.number(), g: external_exports.number(), b: external_exports.number(), a: external_exports.number() }).strict();
44801
45058
  var libraryItemSchema = external_exports.object({
@@ -44820,8 +45077,13 @@ var libraryItemSchema = external_exports.object({
44820
45077
  fontFamily: external_exports.string().nullable(),
44821
45078
  textStyleKey: external_exports.string().nullable(),
44822
45079
  componentKey: external_exports.string().nullable(),
44823
- bindings: external_exports.record(external_exports.string(), external_exports.string().nullable())
45080
+ bindings: external_exports.record(external_exports.string(), external_exports.string().nullable()),
45081
+ /** Every variable the layer's own properties are bound to, by id without its `VariableID:` prefix. */
45082
+ variableIds: external_exports.array(external_exports.string()).optional(),
45083
+ effectStyleKey: external_exports.string().optional()
44824
45084
  }).strict();
45085
+ var variableKeySchema = external_exports.object({ key: external_exports.string(), local: external_exports.boolean() }).strict();
45086
+ var effectStyleSchema = external_exports.object({ key: external_exports.string(), name: external_exports.string() }).strict();
44825
45087
  var libraryVariantSchema = external_exports.object({
44826
45088
  key: external_exports.string(),
44827
45089
  name: external_exports.string(),
@@ -44832,6 +45094,8 @@ var libraryComponentSchema = external_exports.object({
44832
45094
  name: external_exports.string(),
44833
45095
  key: external_exports.string(),
44834
45096
  page: external_exports.string(),
45097
+ /** The branch of the view that holds it, in a file that holds several; none where it serves every branch. */
45098
+ branch: external_exports.string().optional(),
44835
45099
  codePath: external_exports.string().nullable(),
44836
45100
  map: external_exports.object({ props: external_exports.record(external_exports.string(), external_exports.string()), parts: external_exports.record(external_exports.string(), external_exports.string()) }).strict().nullable(),
44837
45101
  variants: external_exports.array(libraryVariantSchema).min(1)
@@ -44851,12 +45115,22 @@ var libraryPartSchema = external_exports.object({
44851
45115
  exportedAt: external_exports.string(),
44852
45116
  components: external_exports.array(libraryComponentSchema),
44853
45117
  textStyles: external_exports.array(textStyleSchema),
44854
- tokens: external_exports.array(tokenSnapshotSchema)
45118
+ tokens: external_exports.array(tokenSnapshotSchema),
45119
+ /**
45120
+ * What the items' references resolve against, absent from a snapshot saved before they were recorded: each
45121
+ * variable id's key, the file's effect styles, and the keys of the components the file holds unpublished.
45122
+ */
45123
+ variableKeys: external_exports.record(external_exports.string(), variableKeySchema).optional(),
45124
+ effectStyles: external_exports.array(effectStyleSchema).optional(),
45125
+ localComponents: external_exports.array(external_exports.string()).optional()
44855
45126
  }).strict();
44856
45127
  var variablePartSchema = external_exports.object({
44857
45128
  exportedAt: external_exports.string(),
44858
45129
  variables: external_exports.record(external_exports.string(), external_exports.string()),
44859
45130
  textStyles: external_exports.array(textStyleSchema),
45131
+ /** Each variable's key, keyed like `variables`. */
45132
+ variableKeys: external_exports.record(external_exports.string(), variableKeySchema).optional(),
45133
+ effectStyles: external_exports.array(effectStyleSchema).optional(),
44860
45134
  tokens: external_exports.array(external_exports.object({
44861
45135
  combo: external_exports.record(external_exports.string(), external_exports.string()),
44862
45136
  values: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]))
@@ -44867,12 +45141,309 @@ var variablePartSchema = external_exports.object({
44867
45141
  key: external_exports.string(),
44868
45142
  name: external_exports.string(),
44869
45143
  page: external_exports.string(),
45144
+ /** The view that holds it: the frame directly on its page, where it sits in one. */
45145
+ frame: external_exports.string().optional(),
44870
45146
  description: external_exports.string(),
44871
45147
  set: external_exports.boolean()
44872
45148
  }).strict()).optional()
44873
45149
  }).strict();
45150
+ var kitSnapshotSchema = external_exports.object({
45151
+ key: external_exports.string(),
45152
+ family: external_exports.string(),
45153
+ components: external_exports.array(external_exports.object({ key: external_exports.string(), name: external_exports.string() }).strict())
45154
+ }).strict();
44874
45155
  var librarySnapshotSchema = libraryPartSchema.extend({ source: external_exports.string(), version: external_exports.string() }).strict();
44875
45156
 
45157
+ // packages/zaku/dist/lib/domain/library-files.js
45158
+ import { isDeepStrictEqual } from "node:util";
45159
+ var storedItemSchema = libraryItemSchema.partial().required({ nodeId: true, path: true, type: true, width: true, height: true }).extend({ bindings: libraryItemSchema.shape.bindings.optional(), glyph: rgbaSchema.optional() }).strict();
45160
+ var libraryFileSchema = external_exports.object({
45161
+ name: external_exports.string(),
45162
+ key: external_exports.string(),
45163
+ variants: external_exports.array(external_exports.object({
45164
+ key: external_exports.string(),
45165
+ /** The items in the first mode of the index's tokens. */
45166
+ items: external_exports.array(storedItemSchema),
45167
+ /** Only the modes whose stored items differ from the first. */
45168
+ modes: external_exports.array(external_exports.object({ combo: external_exports.record(external_exports.string(), external_exports.string()), items: external_exports.array(storedItemSchema) }).strict()).optional()
45169
+ }).strict())
45170
+ }).strict();
45171
+ var libraryIndexSchema = librarySnapshotSchema.extend({
45172
+ components: external_exports.array(libraryComponentSchema.omit({ variants: true }).extend({
45173
+ file: external_exports.string(),
45174
+ variants: external_exports.array(libraryVariantSchema.pick({ key: true, name: true, props: true }).strict())
45175
+ }).strict())
45176
+ }).strict();
45177
+ var NULLABLE = [
45178
+ "fill",
45179
+ "stroke",
45180
+ "textColor",
45181
+ "padding",
45182
+ "gap",
45183
+ "radii",
45184
+ "strokeWeights",
45185
+ "fontSize",
45186
+ "fontFamily",
45187
+ "textStyleKey",
45188
+ "componentKey"
45189
+ ];
45190
+ var RESOLVED = ["fill", "stroke", "textColor", "glyph", "radii"];
45191
+ function colour(name, values) {
45192
+ const value = name ? values[name] : void 0;
45193
+ return value !== void 0 && typeof value === "object" ? value : null;
45194
+ }
45195
+ function expandItem(stored, values) {
45196
+ const bindings = {
45197
+ fill: null,
45198
+ stroke: null,
45199
+ textColor: null,
45200
+ radius: null,
45201
+ ...stored.bindings
45202
+ };
45203
+ const radius = bindings.radius ? values[bindings.radius] : void 0;
45204
+ const glyph = stored.glyph ?? (bindings["glyph"] ? colour(bindings["glyph"], values) : null);
45205
+ return {
45206
+ nodeId: stored.nodeId,
45207
+ path: stored.path,
45208
+ type: stored.type,
45209
+ fill: stored.fill !== void 0 ? stored.fill : colour(bindings.fill, values),
45210
+ stroke: stored.stroke !== void 0 ? stored.stroke : colour(bindings.stroke, values),
45211
+ textColor: stored.textColor !== void 0 ? stored.textColor : colour(bindings.textColor, values),
45212
+ ...glyph ? { glyph } : {},
45213
+ width: stored.width,
45214
+ height: stored.height,
45215
+ padding: stored.padding ?? null,
45216
+ gap: stored.gap ?? null,
45217
+ radii: stored.radii !== void 0 ? stored.radii : typeof radius === "number" ? [radius, radius, radius, radius] : null,
45218
+ strokeWeights: stored.strokeWeights ?? null,
45219
+ fontSize: stored.fontSize ?? null,
45220
+ fontFamily: stored.fontFamily ?? null,
45221
+ textStyleKey: stored.textStyleKey ?? null,
45222
+ componentKey: stored.componentKey ?? null,
45223
+ bindings,
45224
+ ...stored.variableIds ? { variableIds: stored.variableIds } : {},
45225
+ ...stored.effectStyleKey ? { effectStyleKey: stored.effectStyleKey } : {}
45226
+ };
45227
+ }
45228
+ function compactItem(item, values) {
45229
+ const stored = {
45230
+ nodeId: item.nodeId,
45231
+ path: item.path,
45232
+ type: item.type,
45233
+ width: item.width,
45234
+ height: item.height
45235
+ };
45236
+ for (const field of NULLABLE)
45237
+ if (item[field] !== null)
45238
+ stored[field] = item[field];
45239
+ if (item.glyph)
45240
+ stored["glyph"] = item.glyph;
45241
+ if (item.variableIds)
45242
+ stored["variableIds"] = item.variableIds;
45243
+ if (item.effectStyleKey)
45244
+ stored["effectStyleKey"] = item.effectStyleKey;
45245
+ const bindings = Object.fromEntries(Object.entries(item.bindings).filter(([name, value]) => value !== null || name === "glyph"));
45246
+ if (Object.keys(bindings).length > 0)
45247
+ stored["bindings"] = bindings;
45248
+ for (const field of RESOLVED) {
45249
+ if (!(field in stored))
45250
+ continue;
45251
+ const without = { ...stored };
45252
+ delete without[field];
45253
+ if (isDeepStrictEqual(expandItem(without, values), item))
45254
+ delete stored[field];
45255
+ }
45256
+ return isDeepStrictEqual(expandItem(stored, values), item) ? stored : item;
45257
+ }
45258
+ function comboKey(combo) {
45259
+ return JSON.stringify(Object.entries(combo).sort(([a], [b]) => a.localeCompare(b)));
45260
+ }
45261
+ function fileName(name, key, taken) {
45262
+ const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "component";
45263
+ const file2 = taken.has(slug2) ? `${slug2}-${key.slice(0, 8)}` : slug2;
45264
+ taken.add(file2);
45265
+ return `components/${file2}.json`;
45266
+ }
45267
+ function splitLibrary(snapshot) {
45268
+ const valuesOf = new Map(snapshot.tokens.map((token) => [comboKey(token.combo), token.values]));
45269
+ const taken = /* @__PURE__ */ new Set();
45270
+ const files2 = /* @__PURE__ */ new Map();
45271
+ const components = [];
45272
+ for (const { variants, ...component2 } of snapshot.components) {
45273
+ const file2 = fileName(component2.name, component2.key, taken);
45274
+ files2.set(file2, {
45275
+ name: component2.name,
45276
+ key: component2.key,
45277
+ variants: variants.map((variant) => {
45278
+ const stored = variant.modes.map((mode) => ({
45279
+ combo: mode.combo,
45280
+ items: mode.items.map((item) => compactItem(item, valuesOf.get(comboKey(mode.combo)) ?? {}))
45281
+ }));
45282
+ const [first, ...rest] = stored;
45283
+ const modes = rest.filter((mode) => !isDeepStrictEqual(mode.items, first?.items));
45284
+ return { key: variant.key, items: first?.items ?? [], ...modes.length > 0 ? { modes } : {} };
45285
+ })
45286
+ });
45287
+ components.push({
45288
+ ...component2,
45289
+ file: file2,
45290
+ variants: variants.map(({ key, name, props }) => ({ key, name, props }))
45291
+ });
45292
+ }
45293
+ return { index: { ...snapshot, components }, files: files2 };
45294
+ }
45295
+ function joinLibrary(index, files2) {
45296
+ return {
45297
+ ...index,
45298
+ components: index.components.map(({ file: file2, variants, ...component2 }) => {
45299
+ const stored = files2.get(file2);
45300
+ if (!stored)
45301
+ throw new Error(`library/${file2} is not loaded`);
45302
+ return {
45303
+ ...component2,
45304
+ variants: variants.map((variant, at) => {
45305
+ const items = stored.variants[at];
45306
+ return {
45307
+ ...variant,
45308
+ modes: index.tokens.map((token) => ({
45309
+ combo: token.combo,
45310
+ items: (items?.modes?.find((mode) => comboKey(mode.combo) === comboKey(token.combo))?.items ?? items?.items ?? []).map((item) => expandItem(item, token.values))
45311
+ }))
45312
+ };
45313
+ })
45314
+ };
45315
+ })
45316
+ };
45317
+ }
45318
+ var ONE_LINE = 100;
45319
+ function write(value, pad, key) {
45320
+ const line = JSON.stringify(value);
45321
+ if (value === null || typeof value !== "object" || line.length <= ONE_LINE)
45322
+ return line;
45323
+ const inner = `${pad} `;
45324
+ if (Array.isArray(value)) {
45325
+ if (value.length === 0)
45326
+ return "[]";
45327
+ const element = (child) => key === "items" ? JSON.stringify(child) : write(child, inner, null);
45328
+ return `[
45329
+ ${value.map((child) => `${inner}${element(child)}`).join(",\n")}
45330
+ ${pad}]`;
45331
+ }
45332
+ const entries = Object.entries(value).filter(([, child]) => child !== void 0);
45333
+ if (entries.length === 0)
45334
+ return "{}";
45335
+ return `{
45336
+ ${entries.map(([name, child]) => `${inner}${JSON.stringify(name)}: ${write(child, inner, name)}`).join(",\n")}
45337
+ ${pad}}`;
45338
+ }
45339
+ function formatLibraryFile(value) {
45340
+ return `${write(value, "", null)}
45341
+ `;
45342
+ }
45343
+
45344
+ // packages/zaku/dist/lib/repositories/library-snapshot.js
45345
+ async function readText(path2) {
45346
+ try {
45347
+ return await readFile2(path2, "utf8");
45348
+ } catch (error62) {
45349
+ if (error62.code === "ENOENT")
45350
+ return null;
45351
+ throw error62;
45352
+ }
45353
+ }
45354
+ async function writeText(path2, text) {
45355
+ const temporary = `${path2}.${process.pid}.tmp`;
45356
+ await writeFile2(temporary, text);
45357
+ await rename2(temporary, path2);
45358
+ }
45359
+ async function readLibrarySnapshot(paths) {
45360
+ const index = await readOptionalDesignFile(paths.libraryIndex, libraryIndexSchema);
45361
+ if (!index)
45362
+ return paths.legacyLibrary ? readOptionalDesignFile(paths.legacyLibrary, librarySnapshotSchema) : null;
45363
+ const files2 = /* @__PURE__ */ new Map();
45364
+ for (const component2 of index.components) {
45365
+ const path2 = join3(paths.library, component2.file);
45366
+ const text = await readText(path2);
45367
+ if (text === null)
45368
+ throw new DesignFileInvalid(path2, ["the index names it and it is missing; run zaku library save"]);
45369
+ const file2 = parseDesignFile(path2, text, libraryFileSchema);
45370
+ const keys = file2.variants.map((variant) => variant.key);
45371
+ if (keys.join("\n") !== component2.variants.map((variant) => variant.key).join("\n")) {
45372
+ throw new DesignFileInvalid(path2, ["its variants are not the index's; run zaku library save"]);
45373
+ }
45374
+ files2.set(component2.file, file2);
45375
+ }
45376
+ return joinLibrary(index, files2);
45377
+ }
45378
+ async function writeLibrarySnapshot(paths, snapshot) {
45379
+ const { index, files: files2 } = splitLibrary(snapshot);
45380
+ const directory = join3(paths.library, "components");
45381
+ await mkdir(directory, { recursive: true });
45382
+ const written = [];
45383
+ let unchanged = 0;
45384
+ for (const [file2, content] of files2) {
45385
+ const path2 = join3(paths.library, file2);
45386
+ const text = formatLibraryFile(content);
45387
+ if (await readText(path2) === text) {
45388
+ unchanged += 1;
45389
+ continue;
45390
+ }
45391
+ await writeText(path2, text);
45392
+ written.push(file2);
45393
+ }
45394
+ const removed = [];
45395
+ for (const name of (await readdir(directory)).sort()) {
45396
+ const file2 = `components/${name}`;
45397
+ if (!name.endsWith(".json") || files2.has(file2))
45398
+ continue;
45399
+ await rm(join3(directory, name));
45400
+ removed.push(file2);
45401
+ }
45402
+ await writeText(paths.libraryIndex, formatLibraryFile(index));
45403
+ if (paths.legacyLibrary && await readText(paths.legacyLibrary) !== null) {
45404
+ await rm(paths.legacyLibrary);
45405
+ removed.push(relative(paths.root, paths.legacyLibrary));
45406
+ }
45407
+ return { written, unchanged, removed };
45408
+ }
45409
+ async function writeKitSnapshot(paths, kit) {
45410
+ await mkdir(paths.library, { recursive: true });
45411
+ const path2 = join3(paths.library, "kit.json");
45412
+ await writeText(path2, formatLibraryFile(kit));
45413
+ return path2;
45414
+ }
45415
+ async function readDesignSnapshots(paths, config2) {
45416
+ const graph = designGraph(config2);
45417
+ const libraries = {};
45418
+ const kits = {};
45419
+ const errors = [];
45420
+ const attempt = async (read) => {
45421
+ try {
45422
+ await read();
45423
+ } catch (error62) {
45424
+ if (!(error62 instanceof DesignFileInvalid))
45425
+ throw error62;
45426
+ errors.push(error62);
45427
+ }
45428
+ };
45429
+ for (const library of graph.libraries) {
45430
+ await attempt(async () => {
45431
+ const snapshot = await readLibrarySnapshot(librarySnapshotDir(paths, graph.legacy ? null : library.key));
45432
+ if (snapshot)
45433
+ libraries[library.key] = snapshot;
45434
+ });
45435
+ }
45436
+ for (const kit of graph.kits) {
45437
+ await attempt(async () => {
45438
+ const path2 = join3(librarySnapshotDir(paths, kit.key).library, "kit.json");
45439
+ const snapshot = await readOptionalDesignFile(path2, kitSnapshotSchema);
45440
+ if (snapshot)
45441
+ kits[kit.key] = snapshot;
45442
+ });
45443
+ }
45444
+ return { libraries, kits, errors };
45445
+ }
45446
+
44876
45447
  // packages/zaku/dist/lib/adapters/figma-rest-library.js
44877
45448
  var DOCUMENTATION_PREFIXES = ["DS/"];
44878
45449
  var round3 = (value) => Math.round(value * 1e4) / 1e4;
@@ -44945,6 +45516,23 @@ function strokeWeightsOf(node2) {
44945
45516
  const weight = node2.strokeWeight ?? 0;
44946
45517
  return [weight, weight, weight, weight];
44947
45518
  }
45519
+ function aliasIds(node2) {
45520
+ const out = [];
45521
+ const visit2 = (value) => {
45522
+ if (Array.isArray(value))
45523
+ value.forEach(visit2);
45524
+ else if (value && typeof value === "object") {
45525
+ if ("id" in value && typeof value.id === "string")
45526
+ out.push(value.id.replace(/^VariableID:/, ""));
45527
+ else
45528
+ Object.values(value).forEach(visit2);
45529
+ }
45530
+ };
45531
+ visit2(node2.boundVariables);
45532
+ for (const paint of [...node2.fills ?? [], ...node2.strokes ?? []])
45533
+ visit2(paint.boundVariables);
45534
+ return [...new Set(out)];
45535
+ }
44948
45536
  var GLYPH_TYPES = /* @__PURE__ */ new Set(["VECTOR", "BOOLEAN_OPERATION", "LINE", "ELLIPSE", "POLYGON", "STAR"]);
44949
45537
  function glyphOf(instance, at) {
44950
45538
  let text = false;
@@ -44984,6 +45572,9 @@ function itemsOf(root, at) {
44984
45572
  const textStyle = node2.styles?.["text"];
44985
45573
  const glyph = path2 !== "" && node2.type === "INSTANCE" ? glyphOf(node2, at) : null;
44986
45574
  const componentId = node2.type === "INSTANCE" ? node2.componentId : path2 === "" ? node2.id : void 0;
45575
+ const variableIds = aliasIds(node2);
45576
+ const effectStyle = node2.styles?.["effect"];
45577
+ const effectStyleKey = effectStyle ? at.entry.styles[effectStyle]?.key : void 0;
44987
45578
  out.push({
44988
45579
  nodeId: node2.id,
44989
45580
  path: path2,
@@ -45008,7 +45599,9 @@ function itemsOf(root, at) {
45008
45599
  textColor: isText ? fill.binding : null,
45009
45600
  radius: radiusName,
45010
45601
  ...glyph?.value ? { glyph: glyph.binding } : {}
45011
- }
45602
+ },
45603
+ ...variableIds.length > 0 ? { variableIds } : {},
45604
+ ...effectStyleKey ? { effectStyleKey } : {}
45012
45605
  });
45013
45606
  if (path2 === "" || node2.type !== "INSTANCE") {
45014
45607
  for (const child of node2.children ?? [])
@@ -45027,28 +45620,55 @@ function asPublished(component2) {
45027
45620
  containing_frame: { pageName: component2.page }
45028
45621
  };
45029
45622
  }
45623
+ function viewBranch(view, branches) {
45624
+ const prefix = view?.split(" / ")[0];
45625
+ return view?.includes(" / ") && prefix !== void 0 && branches.includes(prefix) ? prefix : void 0;
45626
+ }
45627
+ function viewsByPage(document) {
45628
+ return new Map((document.children ?? []).map((page) => [
45629
+ page.name,
45630
+ (page.children ?? []).flatMap((view) => view.absoluteBoundingBox ? [{ name: view.name, box: view.absoluteBoundingBox }] : [])
45631
+ ]));
45632
+ }
45633
+ function viewHolding(views, box) {
45634
+ if (!box)
45635
+ return void 0;
45636
+ return views.find(({ box: view }) => box.x >= view.x && box.y >= view.y && box.x < view.x + view.width && box.y < view.y + view.height)?.name;
45637
+ }
45030
45638
  async function readLibrary(options) {
45031
- const { rest, fileKey: fileKey2, part } = options;
45639
+ const { rest, fileKey: fileKey3, part } = options;
45640
+ const branches = options.branches ?? [];
45641
+ const multiBranch = branches.length > 1;
45642
+ const exported = new Map((part.components ?? []).map((component2) => [component2.id, component2.frame]));
45032
45643
  const listed = part.components ? {
45033
45644
  sets: part.components.filter((component2) => component2.set).map(asPublished),
45034
45645
  components: part.components.filter((component2) => !component2.set).map(asPublished)
45035
- } : await rest.published(fileKey2);
45646
+ } : await rest.published(fileKey3);
45036
45647
  const published = [
45037
45648
  ...listed.sets,
45038
45649
  ...listed.components.filter((component2) => !component2.containing_frame?.containingComponentSet)
45039
45650
  ].filter((meta3) => !DOCUMENTATION_PREFIXES.some((prefix) => meta3.name.startsWith(prefix)));
45040
- const file2 = await rest.file(fileKey2, 1);
45041
- const response = published.length > 0 ? await rest.nodes(fileKey2, published.map((meta3) => meta3.node_id)) : { version: file2.version, nodes: {} };
45651
+ const file2 = await rest.file(fileKey3, multiBranch ? 2 : 1);
45652
+ const views = multiBranch ? viewsByPage(file2.document) : /* @__PURE__ */ new Map();
45653
+ const response = published.length > 0 ? await rest.nodes(fileKey3, published.map((meta3) => meta3.node_id)) : { version: file2.version, nodes: {} };
45042
45654
  const components = [];
45655
+ const localComponents = /* @__PURE__ */ new Set();
45656
+ for (const entry of Object.values(response.nodes))
45657
+ for (const meta3 of Object.values(entry?.components ?? {}))
45658
+ if (meta3.remote === false)
45659
+ localComponents.add(meta3.key);
45043
45660
  for (const meta3 of published) {
45044
45661
  const entry = response.nodes[meta3.node_id];
45045
45662
  if (!entry)
45046
45663
  continue;
45047
45664
  const variantNodes = entry.document.type === "COMPONENT_SET" ? (entry.document.children ?? []).filter((child) => child.type === "COMPONENT") : [entry.document];
45665
+ const view = exported.has(meta3.node_id) ? exported.get(meta3.node_id) : viewHolding(views.get(meta3.containing_frame?.pageName ?? "") ?? [], entry.document.absoluteBoundingBox);
45666
+ const branch = multiBranch ? viewBranch(view, branches) : void 0;
45048
45667
  components.push({
45049
45668
  name: meta3.name,
45050
45669
  key: meta3.key,
45051
45670
  page: (meta3.containing_frame?.pageName ?? "").replace(/^\W+\s*/, ""),
45671
+ ...branch ? { branch } : {},
45052
45672
  ...descriptionMap(meta3.description),
45053
45673
  variants: variantNodes.map((variant) => ({
45054
45674
  key: entry.components[variant.id]?.key ?? "",
@@ -45062,11 +45682,16 @@ async function readLibrary(options) {
45062
45682
  });
45063
45683
  }
45064
45684
  return {
45065
- source: fileKey2,
45685
+ source: fileKey3,
45066
45686
  version: file2.version,
45067
45687
  exportedAt: part.exportedAt,
45068
45688
  components,
45069
45689
  textStyles: part.textStyles,
45690
+ ...part.variableKeys ? {
45691
+ variableKeys: part.variableKeys,
45692
+ effectStyles: part.effectStyles ?? [],
45693
+ localComponents: [...localComponents].sort()
45694
+ } : {},
45070
45695
  tokens: part.tokens.map((token) => ({
45071
45696
  combo: token.combo,
45072
45697
  values: Object.fromEntries(Object.entries(token.values).map(([name, value]) => [
@@ -45076,18 +45701,57 @@ async function readLibrary(options) {
45076
45701
  }))
45077
45702
  };
45078
45703
  }
45704
+ async function readKit(options) {
45705
+ const components = await options.rest.components(options.key);
45706
+ return {
45707
+ key: options.key,
45708
+ family: options.family,
45709
+ components: components.map((component2) => ({ key: component2.key, name: component2.name }))
45710
+ };
45711
+ }
45079
45712
 
45080
45713
  // packages/zaku/dist/lib/handlers/save-library.js
45714
+ function librarySaveTarget(config2, key) {
45715
+ const graph = designGraph(config2);
45716
+ const keys = [...graph.libraries, ...graph.kits].map((file2) => file2.key);
45717
+ if (keys.length === 0) {
45718
+ const byRole2 = config2.figma.libraries !== void 0 || config2.figma.kits !== void 0;
45719
+ throw new FigmaFileKeyMissing(byRole2 ? "figma.libraries" : "figma.library");
45720
+ }
45721
+ const library = key === void 0 ? graph.libraries.length === 1 ? graph.libraries[0] : void 0 : void 0;
45722
+ if (library)
45723
+ return { kind: "library", library, legacy: graph.legacy };
45724
+ if (key === void 0)
45725
+ throw new LibraryKeyMissing(keys);
45726
+ const named = graph.libraries.find((candidate) => candidate.key === key);
45727
+ if (named)
45728
+ return { kind: "library", library: named, legacy: graph.legacy };
45729
+ const kit = graph.kits.find((candidate) => candidate.key === key);
45730
+ if (kit)
45731
+ return { kind: "kit", kit };
45732
+ throw new LibraryKeyMissing(keys, key);
45733
+ }
45081
45734
  async function saveLibrary(options) {
45082
45735
  const paths = designPaths(options.root);
45083
45736
  const config2 = await readDesignFile(paths.config, zakuConfigSchema);
45084
- const fileKey2 = config2.figma.library;
45085
- if (fileKey2 === void 0)
45086
- throw new FigmaFileKeyMissing("figma.library");
45087
- const snapshot = librarySnapshotSchema.parse(await readLibrary({ rest: options.rest, fileKey: fileKey2, part: options.part }));
45088
- await writeFile2(paths.library, `${JSON.stringify(snapshot, null, 2)}
45089
- `);
45090
- return snapshot;
45737
+ const target = librarySaveTarget(config2, options.key);
45738
+ if (target.kind === "kit") {
45739
+ const at2 = librarySnapshotDir(paths, target.kit.key);
45740
+ const kit = await readKit({ rest: options.rest, key: target.kit.key, family: target.kit.family });
45741
+ await writeKitSnapshot(at2, kit);
45742
+ return { kind: "kit", dir: relative2(paths.root, at2.library), kit };
45743
+ }
45744
+ if (!options.part)
45745
+ throw new TypeError(`saving library ${target.library.key} needs the variables export`);
45746
+ const { key, branches } = target.library;
45747
+ const at = librarySnapshotDir(paths, target.legacy ? null : key);
45748
+ const snapshot = librarySnapshotSchema.parse(await readLibrary({ rest: options.rest, fileKey: key, part: options.part, branches }));
45749
+ return {
45750
+ kind: "library",
45751
+ dir: relative2(paths.root, at.library),
45752
+ snapshot,
45753
+ ...await writeLibrarySnapshot(at, snapshot)
45754
+ };
45091
45755
  }
45092
45756
  var SaveLibraryHandler = class SaveLibraryHandler2 {
45093
45757
  rest;
@@ -45096,7 +45760,12 @@ var SaveLibraryHandler = class SaveLibraryHandler2 {
45096
45760
  this.rest = rest;
45097
45761
  }
45098
45762
  handle(command) {
45099
- return saveLibrary({ root: command.root, part: command.part, rest: this.rest });
45763
+ return saveLibrary({
45764
+ root: command.root,
45765
+ rest: this.rest,
45766
+ ...command.key !== void 0 ? { key: command.key } : {},
45767
+ ...command.part !== void 0 ? { part: command.part } : {}
45768
+ });
45100
45769
  }
45101
45770
  };
45102
45771
  SaveLibraryHandler = __decorate2([
@@ -45106,9 +45775,10 @@ SaveLibraryHandler = __decorate2([
45106
45775
 
45107
45776
  // packages/zaku/dist/lib/handlers/write-outline.js
45108
45777
  var import_tsyringe4 = __toESM(require_cjs(), 1);
45778
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
45109
45779
  var import_yaml2 = __toESM(require_dist(), 1);
45110
- import { mkdir, rm, writeFile as writeFile3 } from "node:fs/promises";
45111
- import { dirname as dirname2 } from "node:path";
45780
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
45781
+ import { dirname as dirname2, join as join6 } from "node:path";
45112
45782
 
45113
45783
  // packages/zaku/dist/lib/domain/documentation-measures.js
45114
45784
  var COVER = {
@@ -45496,8 +46166,8 @@ var DOCUMENTATION_PARTS = [
45496
46166
 
45497
46167
  // packages/zaku/dist/lib/repositories/feature-maps.js
45498
46168
  import { createHash } from "node:crypto";
45499
- import { readdir, readFile as readFile2 } from "node:fs/promises";
45500
- import { basename, join as join3 } from "node:path";
46169
+ import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
46170
+ import { basename, join as join4 } from "node:path";
45501
46171
 
45502
46172
  // packages/zaku/dist/lib/domain/feature-map.js
45503
46173
  var slug = external_exports.string().regex(/^[a-z][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting with a letter");
@@ -45565,7 +46235,7 @@ function digestOf(text) {
45565
46235
  async function loadMaps(dir) {
45566
46236
  let names;
45567
46237
  try {
45568
- names = (await readdir(dir)).filter((name) => name.endsWith(".yaml")).sort();
46238
+ names = (await readdir2(dir)).filter((name) => name.endsWith(".yaml")).sort();
45569
46239
  } catch (error62) {
45570
46240
  if (error62.code === "ENOENT")
45571
46241
  return { maps: [], errors: [] };
@@ -45574,8 +46244,8 @@ async function loadMaps(dir) {
45574
46244
  const maps = [];
45575
46245
  const errors = [];
45576
46246
  for (const name of names) {
45577
- const path2 = join3(dir, name);
45578
- const text = await readFile2(path2, "utf8");
46247
+ const path2 = join4(dir, name);
46248
+ const text = await readFile3(path2, "utf8");
45579
46249
  try {
45580
46250
  const map2 = parseDesignFile(path2, text, featureMapSchema);
45581
46251
  if (`${map2.feature}.yaml` !== basename(path2)) {
@@ -45616,6 +46286,9 @@ function indexTitles(maps) {
45616
46286
  return { titles, duplicates };
45617
46287
  }
45618
46288
 
46289
+ // packages/zaku/dist/lib/adapters/figma-rest-outline.js
46290
+ import { createHash as createHash2 } from "node:crypto";
46291
+
45619
46292
  // packages/zaku/dist/lib/domain/layer-names.js
45620
46293
  var DEFAULT_NAMES = {
45621
46294
  FRAME: ["Frame"],
@@ -45684,7 +46357,7 @@ function outlineFrame(frame, where, ctx) {
45684
46357
  const images2 = [];
45685
46358
  const defaultNames = [];
45686
46359
  const links = [];
45687
- const relative = (node2) => {
46360
+ const relative3 = (node2) => {
45688
46361
  const box = node2.absoluteBoundingBox;
45689
46362
  return box ? { x: box.x - origin.x, y: box.y - origin.y, width: box.width, height: box.height } : null;
45690
46363
  };
@@ -45713,7 +46386,7 @@ function outlineFrame(frame, where, ctx) {
45713
46386
  componentKey: ctx.components[child.componentId ?? ""]?.key ?? "",
45714
46387
  component: component2,
45715
46388
  interactive: ctx.interactive.has(component2),
45716
- bounds: relative(child)
46389
+ bounds: relative3(child)
45717
46390
  });
45718
46391
  } else if (child.type === "TEXT") {
45719
46392
  instance.texts.push({
@@ -45754,7 +46427,7 @@ function outlineFrame(frame, where, ctx) {
45754
46427
  texts: [],
45755
46428
  interactive: ctx.interactive.has(component2),
45756
46429
  local: meta3?.remote === false,
45757
- bounds: relative(node2),
46430
+ bounds: relative3(node2),
45758
46431
  sizing: {
45759
46432
  horizontal: node2.layoutSizingHorizontal ?? "FIXED",
45760
46433
  vertical: node2.layoutSizingVertical ?? "FIXED"
@@ -45803,6 +46476,17 @@ function outlineFrame(frame, where, ctx) {
45803
46476
  links.push(...linksOf(frame));
45804
46477
  for (const child of shown(frame))
45805
46478
  walk(child);
46479
+ const drawn = {
46480
+ size: [origin.width, origin.height],
46481
+ instances: instances.map((instance) => [
46482
+ instance.componentKey,
46483
+ instance.variant,
46484
+ instance.properties,
46485
+ instance.texts.map((text) => text.characters),
46486
+ instance.bounds
46487
+ ]),
46488
+ texts: texts.map((text) => text.characters)
46489
+ };
45806
46490
  return {
45807
46491
  nodeId: frame.id,
45808
46492
  name: frame.name,
@@ -45816,13 +46500,14 @@ function outlineFrame(frame, where, ctx) {
45816
46500
  raw,
45817
46501
  images: images2,
45818
46502
  defaultNames,
45819
- links
46503
+ links,
46504
+ digest: createHash2("sha256").update(JSON.stringify(drawn)).digest("hex").slice(0, 16)
45820
46505
  };
45821
46506
  }
45822
46507
 
45823
46508
  // packages/zaku/dist/lib/repositories/outlines.js
45824
- import { readdir as readdir2 } from "node:fs/promises";
45825
- import { join as join4 } from "node:path";
46509
+ import { readdir as readdir3 } from "node:fs/promises";
46510
+ import { join as join5 } from "node:path";
45826
46511
 
45827
46512
  // packages/zaku/dist/lib/domain/outline.js
45828
46513
  var boundsSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).strict();
@@ -45882,43 +46567,103 @@ var frameOutlineSchema = external_exports.object({
45882
46567
  raw: external_exports.array(rawOutlineSchema),
45883
46568
  images: external_exports.array(imageOutlineSchema),
45884
46569
  defaultNames: external_exports.array(external_exports.object({ nodeId: external_exports.string(), name: external_exports.string() }).strict()),
45885
- links: external_exports.array(linkOutlineSchema)
46570
+ links: external_exports.array(linkOutlineSchema),
46571
+ /** In a product file of one page per screen: the page the frame is on, and the product file it is in. */
46572
+ page: external_exports.string().optional(),
46573
+ source: external_exports.string().optional(),
46574
+ /** What the frame draws, without its node ids, so a copy and its source digest alike. */
46575
+ digest: external_exports.string().optional()
46576
+ }).strict();
46577
+ var flowOutlineSchema = external_exports.object({
46578
+ /** The Figma file key the page is in. */
46579
+ source: external_exports.string(),
46580
+ page: external_exports.string(),
46581
+ fileVersion: external_exports.string(),
46582
+ frames: external_exports.array(external_exports.object({
46583
+ nodeId: external_exports.string(),
46584
+ name: external_exports.string(),
46585
+ screen: external_exports.string().nullable(),
46586
+ target: external_exports.string(),
46587
+ state: external_exports.string(),
46588
+ start: external_exports.boolean(),
46589
+ links: external_exports.array(linkOutlineSchema),
46590
+ digest: external_exports.string()
46591
+ }).strict())
45886
46592
  }).strict();
45887
- var screenOutlineSchema = external_exports.object({
46593
+ var LEGACY_LIBRARY_VERSION = "figma.library";
46594
+ function legacyVersion(value) {
46595
+ if (value === null || typeof value !== "object" || !("libraryVersion" in value))
46596
+ return value;
46597
+ const { libraryVersion, ...rest } = value;
46598
+ return {
46599
+ ...rest,
46600
+ libraryVersions: typeof libraryVersion === "string" ? { [LEGACY_LIBRARY_VERSION]: libraryVersion } : {}
46601
+ };
46602
+ }
46603
+ var screenOutlineSchema = external_exports.preprocess(legacyVersion, external_exports.object({
45888
46604
  feature: external_exports.string(),
45889
46605
  screen: external_exports.string(),
45890
46606
  /** The Figma file key the frames were read from. */
45891
46607
  source: external_exports.string(),
45892
46608
  fileVersion: external_exports.string(),
45893
46609
  mapDigest: external_exports.string(),
45894
- libraryVersion: external_exports.string().nullable(),
46610
+ /** The version of each library snapshot the frames were read against, by key. */
46611
+ libraryVersions: external_exports.record(external_exports.string(), external_exports.string()),
46612
+ /** The version of every product file zaku.yaml lists, when it lists them by platform. */
46613
+ fileVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
45895
46614
  frames: external_exports.array(frameOutlineSchema)
45896
- }).strict();
46615
+ }).strict());
45897
46616
  var unmappedSchema = external_exports.object({
45898
46617
  fileVersion: external_exports.string(),
45899
- frames: external_exports.array(external_exports.object({ nodeId: external_exports.string(), name: external_exports.string() }).strict())
46618
+ frames: external_exports.array(external_exports.object({ nodeId: external_exports.string(), name: external_exports.string() }).strict()),
46619
+ /** A component drawn on a product page, which belongs in a Components library. */
46620
+ components: external_exports.array(external_exports.object({ nodeId: external_exports.string(), name: external_exports.string(), page: external_exports.string(), source: external_exports.string() }).strict()).optional()
45900
46621
  }).strict();
45901
46622
 
45902
46623
  // packages/zaku/dist/lib/repositories/outlines.js
46624
+ var FLOWS_DIR = "flows";
46625
+ function flowOutlinePath(dir, source, page) {
46626
+ const slug2 = page.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "page";
46627
+ return join5(dir, FLOWS_DIR, source, `${slug2}.yaml`);
46628
+ }
45903
46629
  function outlinePath(dir, feature, screen) {
45904
- return join4(dir, feature, `${screen}.yaml`);
46630
+ return join5(dir, feature, `${screen}.yaml`);
45905
46631
  }
45906
46632
  async function loadOutlines(dir) {
45907
46633
  const outlines = [];
46634
+ const flows = [];
45908
46635
  const errors = [];
45909
46636
  let features;
45910
46637
  try {
45911
- features = (await readdir2(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
46638
+ features = (await readdir3(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== FLOWS_DIR).map((entry) => entry.name).sort();
45912
46639
  } catch (error62) {
45913
46640
  if (error62.code === "ENOENT")
45914
- return { outlines, unmapped: null, errors };
46641
+ return { outlines, flows, unmapped: null, errors };
45915
46642
  throw error62;
45916
46643
  }
45917
46644
  for (const feature of features) {
45918
- const files2 = (await readdir2(join4(dir, feature))).filter((name) => name.endsWith(".yaml")).sort();
46645
+ const files2 = (await readdir3(join5(dir, feature))).filter((name) => name.endsWith(".yaml")).sort();
46646
+ for (const file2 of files2) {
46647
+ try {
46648
+ outlines.push(await readDesignFile(join5(dir, feature, file2), screenOutlineSchema));
46649
+ } catch (error62) {
46650
+ if (error62 instanceof DesignFileInvalid)
46651
+ errors.push(error62);
46652
+ else
46653
+ throw error62;
46654
+ }
46655
+ }
46656
+ }
46657
+ const sources = await readdir3(join5(dir, FLOWS_DIR)).catch((error62) => {
46658
+ if (error62.code === "ENOENT")
46659
+ return [];
46660
+ throw error62;
46661
+ });
46662
+ for (const source of sources.sort()) {
46663
+ const files2 = (await readdir3(join5(dir, FLOWS_DIR, source))).filter((name) => name.endsWith(".yaml")).sort();
45919
46664
  for (const file2 of files2) {
45920
46665
  try {
45921
- outlines.push(await readDesignFile(join4(dir, feature, file2), screenOutlineSchema));
46666
+ flows.push(await readDesignFile(join5(dir, FLOWS_DIR, source, file2), flowOutlineSchema));
45922
46667
  } catch (error62) {
45923
46668
  if (error62 instanceof DesignFileInvalid)
45924
46669
  errors.push(error62);
@@ -45929,14 +46674,14 @@ async function loadOutlines(dir) {
45929
46674
  }
45930
46675
  let unmapped = null;
45931
46676
  try {
45932
- unmapped = await readOptionalDesignFile(join4(dir, "unmapped.yaml"), unmappedSchema);
46677
+ unmapped = await readOptionalDesignFile(join5(dir, "unmapped.yaml"), unmappedSchema);
45933
46678
  } catch (error62) {
45934
46679
  if (error62 instanceof DesignFileInvalid)
45935
46680
  errors.push(error62);
45936
46681
  else
45937
46682
  throw error62;
45938
46683
  }
45939
- return { outlines, unmapped, errors };
46684
+ return { outlines, flows, unmapped, errors };
45940
46685
  }
45941
46686
 
45942
46687
  // packages/zaku/dist/lib/handlers/write-outline.js
@@ -45973,108 +46718,215 @@ function resolveFrame(frame, parsed, maps) {
45973
46718
  return candidates.find((ref) => ref.feature === pageFeature);
45974
46719
  return candidates.length === 1 ? candidates[0] : void 0;
45975
46720
  }
46721
+ function resolveTitle(parsed, maps) {
46722
+ const candidates = [...indexScreens(maps).values()].filter((ref) => ref.screen.title === parsed.title);
46723
+ return candidates.length === 1 ? candidates[0] : void 0;
46724
+ }
45976
46725
  function looksLikeScreenFrame(name) {
45977
46726
  return name.includes("/");
45978
46727
  }
45979
- function holdsScreens(page) {
46728
+ function holdsScreens(page, prototypes = /* @__PURE__ */ new Set()) {
45980
46729
  if (page === COVER.page || page === DOCUMENTATION_COMPONENTS_PAGE || page === DIRECTIONS_PAGE)
45981
46730
  return false;
46731
+ if (prototypes.has(page))
46732
+ return false;
45982
46733
  return !page.startsWith("\u2756 ") && !page.includes(" / ") && !/^-+$/.test(page);
45983
46734
  }
46735
+ function listComponents(document, source) {
46736
+ const out = [];
46737
+ for (const page of document.children ?? []) {
46738
+ if (page.type !== "CANVAS" || !holdsScreens(page.name))
46739
+ continue;
46740
+ const walk = (node2) => {
46741
+ for (const child of node2.children ?? []) {
46742
+ if (child.type === "COMPONENT" || child.type === "COMPONENT_SET")
46743
+ out.push({ nodeId: child.id, name: child.name, page: page.name, source });
46744
+ else if (child.type === "SECTION")
46745
+ walk(child);
46746
+ }
46747
+ };
46748
+ walk(page);
46749
+ }
46750
+ return out;
46751
+ }
46752
+ function productFiles(config2) {
46753
+ const graph = designGraph(config2);
46754
+ if (graph.legacy)
46755
+ return config2.figma.product === void 0 ? [] : [{ key: config2.figma.product, targets: config2.targets }];
46756
+ return graph.products.map((product) => ({
46757
+ key: product.key,
46758
+ targets: config2.targets.filter((target) => graph.platformOf(target) === product.platform)
46759
+ }));
46760
+ }
45984
46761
  async function runOutline(options) {
45985
46762
  const paths = designPaths(options.root);
45986
46763
  const config2 = await readDesignFile(paths.config, zakuConfigSchema);
45987
- const fileKey2 = config2.figma.product;
45988
- if (fileKey2 === void 0)
45989
- throw new FigmaFileKeyMissing("figma.product");
46764
+ const { libraries: listed, kits, products: byPlatform, prototypes: flows } = config2.figma;
46765
+ const legacy = [listed, kits, byPlatform, flows].every((files2) => files2 === void 0);
46766
+ const products = productFiles(config2);
46767
+ if (products.length === 0)
46768
+ throw new FigmaFileKeyMissing(legacy ? "figma.product" : "figma.products");
45990
46769
  const { maps, errors } = await loadMaps(paths.maps);
45991
46770
  if (errors[0])
45992
46771
  throw errors[0];
45993
46772
  const digests = new Map(maps.map((loaded) => [loaded.map.feature, loaded.digest]));
45994
- const library = await readOptionalDesignFile(paths.library, librarySnapshotSchema);
46773
+ const { libraries } = await readDesignSnapshots(paths, config2);
46774
+ const libraryVersions = Object.fromEntries(Object.entries(libraries).map(([key, library]) => [key, library.version]));
45995
46775
  const existing = await loadOutlines(paths.outline);
45996
- const file2 = await options.rest.file(fileKey2, INDEX_DEPTH);
45997
- const listed = listFrames(file2.document);
46776
+ const prototypes = new Set(legacy ? config2.targets.map((target) => `${target.name} prototype`) : []);
46777
+ const versions = {};
46778
+ const flowPages = /* @__PURE__ */ new Map();
46779
+ const flowFrame = (source, frame, targets) => {
46780
+ const parsed = parseFrameName(frame.name, targets);
46781
+ if (!parsed)
46782
+ return;
46783
+ const key = `${source}
46784
+ ${frame.page}`;
46785
+ const page = flowPages.get(key) ?? { source, page: frame.page, frames: [] };
46786
+ page.frames.push({ frame, parsed });
46787
+ flowPages.set(key, page);
46788
+ };
45998
46789
  const resolved = /* @__PURE__ */ new Map();
45999
- const unmapped = { fileVersion: file2.version, frames: [] };
46000
- for (const frame of listed.filter((listed2) => holdsScreens(listed2.page))) {
46001
- const parsed = parseFrameName(frame.name, config2.targets);
46002
- if (!parsed) {
46003
- if (looksLikeScreenFrame(frame.name))
46790
+ const unmapped = { fileVersion: "", frames: [] };
46791
+ const components = [];
46792
+ for (const product of products) {
46793
+ const file2 = await options.rest.file(product.key, INDEX_DEPTH);
46794
+ versions[product.key] = file2.version;
46795
+ const listed2 = listFrames(file2.document);
46796
+ for (const frame of listed2.filter((candidate) => prototypes.has(candidate.page)))
46797
+ flowFrame(product.key, frame, product.targets);
46798
+ for (const frame of listed2.filter((candidate) => holdsScreens(candidate.page, prototypes))) {
46799
+ const parsed = parseFrameName(frame.name, product.targets);
46800
+ if (!parsed) {
46801
+ if (looksLikeScreenFrame(frame.name))
46802
+ unmapped.frames.push({ nodeId: frame.id, name: frame.name });
46803
+ continue;
46804
+ }
46805
+ const ref = legacy ? resolveFrame(frame, parsed, maps) : resolveTitle(parsed, maps);
46806
+ if (ref)
46807
+ resolved.set(frame.id, { frame, parsed, ref, source: product.key });
46808
+ else
46004
46809
  unmapped.frames.push({ nodeId: frame.id, name: frame.name });
46005
- continue;
46006
46810
  }
46007
- const ref = resolveFrame(frame, parsed, maps);
46008
- if (ref)
46009
- resolved.set(frame.id, { frame, parsed, ref });
46010
- else
46011
- unmapped.frames.push({ nodeId: frame.id, name: frame.name });
46811
+ if (!legacy)
46812
+ components.push(...listComponents(file2.document, product.key));
46813
+ }
46814
+ const graph = designGraph(config2);
46815
+ for (const file2 of legacy ? [] : graph.prototypes) {
46816
+ const read = await options.rest.file(file2.key, INDEX_DEPTH);
46817
+ versions[file2.key] = read.version;
46818
+ const targets = config2.targets.filter((target) => graph.platformOf(target) === file2.platform);
46819
+ for (const frame of listFrames(read.document).filter((candidate) => holdsScreens(candidate.page)))
46820
+ flowFrame(file2.key, frame, targets);
46012
46821
  }
46822
+ unmapped.fileVersion = Object.values(versions).join(",");
46823
+ if (!legacy)
46824
+ unmapped.components = components;
46013
46825
  const outlined = new Set(existing.outlines.flatMap((outline) => outline.frames.map((frame) => frame.nodeId)));
46014
46826
  const drawnScreens = new Set([...resolved.values()].map(({ ref }) => `${ref.feature}/${ref.id}`));
46015
- const current = existing.outlines.every((outline) => outline.fileVersion === file2.version && outline.mapDigest === digests.get(outline.feature) && outline.libraryVersion === (library?.version ?? null)) && [...resolved.keys()].every((id2) => outlined.has(id2)) && existing.outlines.every((outline) => drawnScreens.has(`${outline.feature}/${outline.screen}`));
46827
+ const current = existing.outlines.every((outline) => (legacy ? outline.fileVersion === versions[outline.source] : isDeepStrictEqual2(outline.fileVersions, versions)) && outline.mapDigest === digests.get(outline.feature) && isDeepStrictEqual2(outline.libraryVersions, libraryVersions)) && [...resolved.keys()].every((id2) => outlined.has(id2)) && existing.flows.length === flowPages.size && existing.flows.every((flow) => flow.fileVersion === versions[flow.source]) && existing.outlines.every((outline) => drawnScreens.has(`${outline.feature}/${outline.screen}`));
46016
46828
  if (options.frames === null && existing.outlines.length > 0 && current) {
46017
46829
  return { skipped: true, read: 0, written: [], unmapped: unmapped.frames.length };
46018
46830
  }
46019
46831
  const ids = options.frames ?? [...resolved.keys()];
46020
- const response = ids.length > 0 ? await options.rest.nodes(fileKey2, ids) : { version: file2.version, nodes: {} };
46021
46832
  const byScreen = /* @__PURE__ */ new Map();
46022
46833
  if (options.frames !== null) {
46023
46834
  for (const outline of existing.outlines)
46024
46835
  byScreen.set(`${outline.feature}/${outline.screen}`, outline);
46025
46836
  }
46026
46837
  const interactive = new Set(config2.interactive);
46027
- const resized = new Set(Object.values(response.nodes).flatMap((entry) => entry ? resizedLocalComponents(entry.document, entry.components) : []));
46028
- const sizes = /* @__PURE__ */ new Map();
46029
- if (resized.size > 0) {
46030
- const read = await options.rest.nodes(fileKey2, [...resized]);
46031
- for (const [id2, entry] of Object.entries(read.nodes)) {
46032
- const box = entry?.document.absoluteBoundingBox;
46033
- if (box)
46034
- sizes.set(id2, { width: box.width, height: box.height });
46035
- }
46036
- }
46037
- for (const [id2, entry] of Object.entries(response.nodes)) {
46038
- const found = resolved.get(id2);
46039
- if (!entry || !found)
46838
+ for (const product of products) {
46839
+ const own2 = ids.filter((id2) => resolved.has(id2) ? resolved.get(id2)?.source === product.key : product === products[0]);
46840
+ if (own2.length === 0)
46040
46841
  continue;
46041
- const { frame: place, parsed, ref } = found;
46042
- const key = `${ref.feature}/${ref.id}`;
46043
- const outline = byScreen.get(key) ?? {
46044
- feature: ref.feature,
46045
- screen: ref.id,
46046
- source: fileKey2,
46047
- fileVersion: file2.version,
46048
- mapDigest: "",
46049
- libraryVersion: null,
46050
- frames: []
46051
- };
46052
- const frame = outlineFrame(entry.document, {
46053
- target: parsed.target.id,
46054
- state: parsed.state,
46055
- containers: place.containers,
46056
- start: place.start
46057
- }, { ...entry, interactive, sizes });
46058
- outline.frames = [...outline.frames.filter((kept) => kept.nodeId !== id2), frame].sort((a, b) => a.name.localeCompare(b.name));
46059
- outline.fileVersion = file2.version;
46060
- outline.mapDigest = digests.get(ref.feature) ?? "";
46061
- outline.libraryVersion = library?.version ?? null;
46062
- byScreen.set(key, outline);
46842
+ const response = await options.rest.nodes(product.key, own2);
46843
+ const resized = new Set(Object.values(response.nodes).flatMap((entry) => entry ? resizedLocalComponents(entry.document, entry.components) : []));
46844
+ const sizes = /* @__PURE__ */ new Map();
46845
+ if (resized.size > 0) {
46846
+ const read = await options.rest.nodes(product.key, [...resized]);
46847
+ for (const [id2, entry] of Object.entries(read.nodes)) {
46848
+ const box = entry?.document.absoluteBoundingBox;
46849
+ if (box)
46850
+ sizes.set(id2, { width: box.width, height: box.height });
46851
+ }
46852
+ }
46853
+ for (const [id2, entry] of Object.entries(response.nodes)) {
46854
+ const found = resolved.get(id2);
46855
+ if (!entry || !found)
46856
+ continue;
46857
+ const { frame: place, parsed, ref } = found;
46858
+ const key = `${ref.feature}/${ref.id}`;
46859
+ const outline = byScreen.get(key) ?? {
46860
+ feature: ref.feature,
46861
+ screen: ref.id,
46862
+ source: product.key,
46863
+ fileVersion: versions[product.key] ?? "",
46864
+ mapDigest: "",
46865
+ libraryVersions: {},
46866
+ frames: []
46867
+ };
46868
+ const frame = outlineFrame(entry.document, {
46869
+ target: parsed.target.id,
46870
+ state: parsed.state,
46871
+ containers: place.containers,
46872
+ start: place.start
46873
+ }, { ...entry, interactive, sizes });
46874
+ if (!legacy)
46875
+ Object.assign(frame, { page: place.page, source: product.key });
46876
+ outline.frames = [...outline.frames.filter((kept) => kept.nodeId !== id2), frame].sort((a, b) => a.name.localeCompare(b.name));
46877
+ outline.mapDigest = digests.get(ref.feature) ?? "";
46878
+ outline.libraryVersions = libraryVersions;
46879
+ if (legacy)
46880
+ outline.fileVersion = versions[product.key] ?? "";
46881
+ else
46882
+ outline.fileVersions = versions;
46883
+ byScreen.set(key, outline);
46884
+ }
46063
46885
  }
46064
46886
  if (options.frames === null) {
46887
+ await rm2(join6(paths.outline, FLOWS_DIR), { recursive: true, force: true });
46888
+ for (const page of flowPages.values()) {
46889
+ const response = await options.rest.nodes(page.source, page.frames.map(({ frame }) => frame.id));
46890
+ const flow = {
46891
+ source: page.source,
46892
+ page: page.page,
46893
+ fileVersion: versions[page.source] ?? "",
46894
+ frames: []
46895
+ };
46896
+ for (const { frame, parsed } of page.frames) {
46897
+ const entry = response.nodes[frame.id];
46898
+ if (!entry)
46899
+ continue;
46900
+ const read = outlineFrame(entry.document, { target: parsed.target.id, state: parsed.state, containers: frame.containers, start: frame.start }, { ...entry, interactive, sizes: /* @__PURE__ */ new Map() });
46901
+ const ref = resolveTitle(parsed, maps);
46902
+ flow.frames.push({
46903
+ nodeId: frame.id,
46904
+ name: frame.name,
46905
+ screen: ref ? `${ref.feature}/${ref.id}` : null,
46906
+ target: parsed.target.id,
46907
+ state: parsed.state,
46908
+ start: frame.start,
46909
+ links: read.links,
46910
+ digest: read.digest ?? ""
46911
+ });
46912
+ }
46913
+ const path2 = flowOutlinePath(paths.outline, page.source, page.page);
46914
+ await mkdir2(dirname2(path2), { recursive: true });
46915
+ await writeFile3(path2, (0, import_yaml2.stringify)(flow));
46916
+ }
46065
46917
  for (const outline of existing.outlines) {
46066
46918
  if (!byScreen.has(`${outline.feature}/${outline.screen}`))
46067
- await rm(outlinePath(paths.outline, outline.feature, outline.screen), { force: true });
46919
+ await rm2(outlinePath(paths.outline, outline.feature, outline.screen), { force: true });
46068
46920
  }
46069
46921
  }
46070
46922
  const written = [];
46071
46923
  for (const outline of byScreen.values()) {
46072
46924
  const path2 = outlinePath(paths.outline, outline.feature, outline.screen);
46073
- await mkdir(dirname2(path2), { recursive: true });
46925
+ await mkdir2(dirname2(path2), { recursive: true });
46074
46926
  await writeFile3(path2, (0, import_yaml2.stringify)(outline));
46075
46927
  written.push(path2);
46076
46928
  }
46077
- await mkdir(paths.outline, { recursive: true });
46929
+ await mkdir2(paths.outline, { recursive: true });
46078
46930
  await writeFile3(paths.unmapped, (0, import_yaml2.stringify)(unmapped));
46079
46931
  return {
46080
46932
  skipped: false,
@@ -46190,14 +47042,14 @@ RecipeRepository = __decorate2([
46190
47042
 
46191
47043
  // packages/zaku/dist/lib/repositories/json-schema-files.js
46192
47044
  var import_tsyringe10 = __toESM(require_cjs(), 1);
46193
- import { mkdir as mkdir2 } from "node:fs/promises";
46194
- import { join as join5 } from "node:path";
47045
+ import { mkdir as mkdir3 } from "node:fs/promises";
47046
+ import { join as join7 } from "node:path";
46195
47047
  var JsonSchemaFiles = class JsonSchemaFiles2 {
46196
47048
  async write(dir, schemas) {
46197
- await mkdir2(dir, { recursive: true });
47049
+ await mkdir3(dir, { recursive: true });
46198
47050
  const written = [];
46199
47051
  for (const [name, schema] of Object.entries(schemas)) {
46200
- const path2 = join5(dir, name);
47052
+ const path2 = join7(dir, name);
46201
47053
  await writeDesignFile(path2, schema);
46202
47054
  written.push(path2);
46203
47055
  }
@@ -46210,13 +47062,13 @@ JsonSchemaFiles = __decorate2([
46210
47062
 
46211
47063
  // packages/zaku/dist/lib/repositories/source-files.js
46212
47064
  var import_tsyringe11 = __toESM(require_cjs(), 1);
46213
- import { readFile as readFile3 } from "node:fs/promises";
47065
+ import { readFile as readFile4 } from "node:fs/promises";
46214
47066
  var SourceFiles = class SourceFiles2 {
46215
47067
  readText(path2) {
46216
- return readFile3(path2, "utf8");
47068
+ return readFile4(path2, "utf8");
46217
47069
  }
46218
47070
  async readJson(path2) {
46219
- return JSON.parse(await readFile3(path2, "utf8"));
47071
+ return JSON.parse(await readFile4(path2, "utf8"));
46220
47072
  }
46221
47073
  };
46222
47074
  SourceFiles = __decorate2([
@@ -46224,11 +47076,11 @@ SourceFiles = __decorate2([
46224
47076
  ], SourceFiles);
46225
47077
 
46226
47078
  // packages/zaku/dist/lib/repositories/budget-ledger.js
46227
- import { mkdir as mkdir3, readFile as readFile4, rename as rename2, writeFile as writeFile4 } from "node:fs/promises";
46228
- import { dirname as dirname3, join as join6 } from "node:path";
47079
+ import { mkdir as mkdir4, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
47080
+ import { dirname as dirname3, join as join8 } from "node:path";
46229
47081
  function ledgerPath(env, platform, home) {
46230
- const cache = env["XDG_CACHE_HOME"] ?? (platform === "darwin" ? join6(home, "Library", "Caches") : join6(home, ".cache"));
46231
- return join6(cache, "zaku", "ledger.json");
47082
+ const cache = env["XDG_CACHE_HOME"] ?? (platform === "darwin" ? join8(home, "Library", "Caches") : join8(home, ".cache"));
47083
+ return join8(cache, "zaku", "ledger.json");
46232
47084
  }
46233
47085
  function today(now) {
46234
47086
  return now.toISOString().slice(0, 10);
@@ -46238,7 +47090,7 @@ function seatAndRun(env) {
46238
47090
  }
46239
47091
  async function readLedger(path2, day) {
46240
47092
  try {
46241
- const parsed = JSON.parse(await readFile4(path2, "utf8"));
47093
+ const parsed = JSON.parse(await readFile5(path2, "utf8"));
46242
47094
  const isCount = (seat) => {
46243
47095
  const count = seat;
46244
47096
  return typeof count === "object" && count !== null && typeof count.mcp === "number" && typeof count.rest === "number" && typeof count.runs === "object" && count.runs !== null && Object.values(count.runs).every((value) => typeof value === "number");
@@ -46257,11 +47109,11 @@ async function recordCalls(path2, entry) {
46257
47109
  if (entry.kind === "mcp")
46258
47110
  seat.runs[entry.run] = (seat.runs[entry.run] ?? 0) + entry.count;
46259
47111
  ledger.seats[entry.seat] = seat;
46260
- await mkdir3(dirname3(path2), { recursive: true });
47112
+ await mkdir4(dirname3(path2), { recursive: true });
46261
47113
  const temporary = `${path2}.${process.pid}.tmp`;
46262
47114
  await writeFile4(temporary, `${JSON.stringify(ledger)}
46263
47115
  `);
46264
- await rename2(temporary, path2);
47116
+ await rename3(temporary, path2);
46265
47117
  return ledger;
46266
47118
  }
46267
47119
  function mcpAllowance(ledger, seat, run, budget) {
@@ -46297,6 +47149,30 @@ var pairingSchema = external_exports.object({
46297
47149
  }).strict();
46298
47150
  var pairingsFileSchema = external_exports.object({ pairings: external_exports.array(pairingSchema.extend({ hash: external_exports.string() }).strict()) }).strict();
46299
47151
 
47152
+ // packages/zaku/dist/lib/domain/print-graph.js
47153
+ function printGraph(config2, snapshots) {
47154
+ const graph = designGraph(config2);
47155
+ const lines = ["targets"];
47156
+ for (const target of config2.targets) {
47157
+ const files2 = [
47158
+ ...graph.librariesFor(target).map((library) => `${library.key} ${library.layer}`),
47159
+ ...graph.kitsFor(target).map((kit) => `${kit.key} ${kit.family} kit`)
47160
+ ];
47161
+ const branch = graph.branchOf(target);
47162
+ lines.push(` ${target.name} (${target.id}, ${branch ? `branch ${branch}` : "no branch"}, ${graph.platformOf(target)}): ${files2.join(", ") || "nothing"}`);
47163
+ }
47164
+ lines.push("files");
47165
+ const extendedBy = (key) => graph.libraries.filter((library) => library.extends.includes(key)).map((library) => library.key).join(", ") || "nothing";
47166
+ const saved = (key, present) => present ? "" : ` (no snapshot; run zaku library save --key ${key})`;
47167
+ for (const library of graph.libraries) {
47168
+ lines.push(` ${library.key} ${library.layer} [${library.branches.join(", ")}]: extends ${library.extends.join(", ") || "nothing"}; extended by ${extendedBy(library.key)}${saved(library.key, snapshots.libraries[library.key] !== void 0)}`);
47169
+ }
47170
+ for (const kit of graph.kits) {
47171
+ lines.push(` ${kit.key} ${kit.family} kit: extended by ${extendedBy(kit.key)}${saved(kit.key, snapshots.kits[kit.key] !== void 0)}`);
47172
+ }
47173
+ return lines;
47174
+ }
47175
+
46300
47176
  // packages/zaku/dist/lib/adapters/figma-rest.js
46301
47177
  var FigmaRateLimited = class extends Error {
46302
47178
  url;
@@ -46341,26 +47217,30 @@ function createFigmaRest(options) {
46341
47217
  throw new FigmaRateLimited(url2);
46342
47218
  }
46343
47219
  const chunks = (ids) => Array.from({ length: Math.ceil(ids.length / BATCH) }, (_, index) => ids.slice(index * BATCH, (index + 1) * BATCH));
47220
+ async function components(fileKey3) {
47221
+ const listed = await call(`${BASE}/files/${fileKey3}/components`);
47222
+ return listed.meta.components;
47223
+ }
46344
47224
  return {
46345
- file: (fileKey2, depth) => call(`${BASE}/files/${fileKey2}?depth=${depth}`),
46346
- async nodes(fileKey2, ids) {
47225
+ file: (fileKey3, depth) => call(`${BASE}/files/${fileKey3}?depth=${depth}`),
47226
+ async nodes(fileKey3, ids) {
46347
47227
  const merged = { version: "", nodes: {} };
46348
47228
  for (const chunk of chunks(ids)) {
46349
- const part = await call(`${BASE}/files/${fileKey2}/nodes?ids=${encodeURIComponent(chunk.join(","))}`);
47229
+ const part = await call(`${BASE}/files/${fileKey3}/nodes?ids=${encodeURIComponent(chunk.join(","))}`);
46350
47230
  merged.version = part.version;
46351
47231
  Object.assign(merged.nodes, part.nodes);
46352
47232
  }
46353
47233
  return merged;
46354
47234
  },
46355
- async published(fileKey2) {
46356
- const sets = await call(`${BASE}/files/${fileKey2}/component_sets`);
46357
- const components = await call(`${BASE}/files/${fileKey2}/components`);
46358
- return { sets: sets.meta.component_sets, components: components.meta.components };
47235
+ async published(fileKey3) {
47236
+ const sets = await call(`${BASE}/files/${fileKey3}/component_sets`);
47237
+ return { sets: sets.meta.component_sets, components: await components(fileKey3) };
46359
47238
  },
46360
- async images(fileKey2, ids) {
47239
+ components,
47240
+ async images(fileKey3, ids) {
46361
47241
  const merged = {};
46362
47242
  for (const chunk of chunks(ids)) {
46363
- const part = await call(`${BASE}/images/${fileKey2}?ids=${encodeURIComponent(chunk.join(","))}&format=png&scale=1`);
47243
+ const part = await call(`${BASE}/images/${fileKey3}?ids=${encodeURIComponent(chunk.join(","))}&format=png&scale=1`);
46364
47244
  Object.assign(merged, part.images);
46365
47245
  }
46366
47246
  return merged;
@@ -46370,17 +47250,40 @@ function createFigmaRest(options) {
46370
47250
 
46371
47251
  // packages/zaku/dist/lib/adapters/figma-variables-script.js
46372
47252
  async function exportVariables(figma, options) {
46373
- const collections = await figma.variables.getLocalVariableCollectionsAsync();
47253
+ const collections = [...await figma.variables.getLocalVariableCollectionsAsync()];
47254
+ if (!collections.some((collection) => collection.name === "semantic")) {
47255
+ const available = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
47256
+ const bases = [
47257
+ ...new Set(available.filter((entry) => entry.name === "semantic").map((entry) => entry.libraryName))
47258
+ ];
47259
+ if (bases.length === 0)
47260
+ throw new Error("the file has no semantic collection and no enabled library has one");
47261
+ if (bases.length > 1)
47262
+ throw new Error(`several enabled libraries hold a semantic collection: ${bases.join(", ")}; enable only the one this library extends`);
47263
+ const imported = /* @__PURE__ */ new Map();
47264
+ for (const entry of available.filter((candidate) => candidate.libraryName === bases[0])) {
47265
+ for (const listed of await figma.teamLibrary.getVariablesInLibraryCollectionAsync(entry.key)) {
47266
+ const variable = await figma.variables.importVariableByKeyAsync(listed.key);
47267
+ if (!imported.has(variable.variableCollectionId))
47268
+ imported.set(variable.variableCollectionId, await figma.variables.getVariableCollectionByIdAsync(variable.variableCollectionId));
47269
+ }
47270
+ }
47271
+ collections.push(...[...imported.values()].filter(Boolean));
47272
+ }
46374
47273
  const semantic = collections.find((collection) => collection.name === "semantic");
46375
47274
  if (!semantic)
46376
47275
  throw new Error("the library has no semantic variable collection");
46377
47276
  const hex3 = (value) => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, "0");
46378
47277
  const variables = {};
47278
+ const variableKeys = {};
46379
47279
  for (const collection of collections) {
46380
47280
  for (const id2 of collection.variableIds) {
46381
47281
  const variable = await figma.variables.getVariableByIdAsync(id2);
46382
- if (variable)
46383
- variables[String(id2).replace(/^VariableID:/, "")] = variable.name;
47282
+ if (!variable)
47283
+ continue;
47284
+ const short = String(id2).replace(/^VariableID:/, "");
47285
+ variables[short] = variable.name;
47286
+ variableKeys[short] = { key: variable.key, local: variable.remote !== true };
46384
47287
  }
46385
47288
  }
46386
47289
  const holder = figma.createFrame();
@@ -46402,6 +47305,8 @@ async function exportVariables(figma, options) {
46402
47305
  const values = {};
46403
47306
  for (const id2 of semantic.variableIds) {
46404
47307
  const variable = await figma.variables.getVariableByIdAsync(id2);
47308
+ if (!variable)
47309
+ continue;
46405
47310
  const resolved = variable.resolveForConsumer(holder).value;
46406
47311
  values[variable.name] = typeof resolved === "object" && resolved !== null ? `#${hex3(resolved.r)}${hex3(resolved.g)}${hex3(resolved.b)}${hex3(resolved.a ?? 1)}` : resolved;
46407
47312
  }
@@ -46421,21 +47326,31 @@ async function exportVariables(figma, options) {
46421
47326
  for (const node2 of page.findAllWithCriteria({ types: ["COMPONENT_SET", "COMPONENT"] })) {
46422
47327
  if (node2.type === "COMPONENT" && node2.parent?.type === "COMPONENT_SET")
46423
47328
  continue;
47329
+ let view = node2;
47330
+ while (view.parent && view.parent.type !== "PAGE")
47331
+ view = view.parent;
46424
47332
  components.push({
46425
47333
  id: node2.id,
46426
47334
  key: node2.key,
46427
47335
  name: node2.name,
46428
47336
  page: page.name,
47337
+ ...view !== node2 && view.parent ? { frame: view.name } : {},
46429
47338
  description: node2.description ?? "",
46430
47339
  set: node2.type === "COMPONENT_SET"
46431
47340
  });
46432
47341
  }
46433
47342
  }
46434
47343
  }
47344
+ const effectStyles = (await figma.getLocalEffectStylesAsync()).map((style) => ({
47345
+ key: style.key,
47346
+ name: style.name
47347
+ }));
46435
47348
  return {
46436
47349
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
46437
47350
  variables,
47351
+ variableKeys,
46438
47352
  textStyles,
47353
+ effectStyles,
46439
47354
  tokens,
46440
47355
  ...options.local ? { components } : {}
46441
47356
  };
@@ -47027,15 +47942,15 @@ async function startPluginBridge(options = {}) {
47027
47942
  });
47028
47943
  });
47029
47944
  });
47030
- function request(fileKey2, message, timeoutMs) {
47945
+ function request(fileKey3, message, timeoutMs) {
47031
47946
  if (port === null)
47032
47947
  return Promise.resolve({
47033
47948
  ok: false,
47034
47949
  error: `no port from ${ports[0]} to ${ports[ports.length - 1]} was free, so zaku mcp could not listen for the plugin`
47035
47950
  });
47036
- const socket = holderOf(fileKey2);
47951
+ const socket = holderOf(fileKey3);
47037
47952
  if (!socket)
47038
- return Promise.resolve({ ok: false, error: `file ${fileKey2} is not open in Figma with the zaku plugin` });
47953
+ return Promise.resolve({ ok: false, error: `file ${fileKey3} is not open in Figma with the zaku plugin` });
47039
47954
  const id2 = `c${++next}`;
47040
47955
  return new Promise((resolve3) => {
47041
47956
  const timer = setTimeout(() => settle2(id2, { ok: false, error: `no answer from the zaku panel in ${timeoutMs / 1e3} s; ${PARTIAL}` }), timeoutMs);
@@ -47045,8 +47960,8 @@ async function startPluginBridge(options = {}) {
47045
47960
  }
47046
47961
  return {
47047
47962
  port,
47048
- run: (fileKey2, call, timeoutMs) => request(fileKey2, { type: "run", ...call }, timeoutMs),
47049
- screenshot: (fileKey2, call, timeoutMs) => request(fileKey2, { type: "screenshot", ...call }, timeoutMs),
47963
+ run: (fileKey3, call, timeoutMs) => request(fileKey3, { type: "run", ...call }, timeoutMs),
47964
+ screenshot: (fileKey3, call, timeoutMs) => request(fileKey3, { type: "screenshot", ...call }, timeoutMs),
47050
47965
  close: () => new Promise((done) => {
47051
47966
  if (!server || !bound)
47052
47967
  return done();
@@ -47057,12 +47972,66 @@ async function startPluginBridge(options = {}) {
47057
47972
  };
47058
47973
  }
47059
47974
 
47975
+ // packages/zaku/dist/lib/domain/checks/libraries.js
47976
+ var comboKey2 = (combo) => JSON.stringify(Object.entries(combo).sort(([a], [b]) => a.localeCompare(b)));
47977
+ function joinLibraries(snapshots) {
47978
+ if (snapshots.length === 1 && snapshots[0])
47979
+ return snapshots[0];
47980
+ const tokens = /* @__PURE__ */ new Map();
47981
+ for (const snapshot of snapshots) {
47982
+ for (const token of snapshot.tokens) {
47983
+ const key = comboKey2(token.combo);
47984
+ tokens.set(key, { combo: token.combo, values: { ...tokens.get(key)?.values, ...token.values } });
47985
+ }
47986
+ }
47987
+ return {
47988
+ source: snapshots.map((snapshot) => snapshot.source).join(","),
47989
+ version: snapshots.map((snapshot) => snapshot.version).join(","),
47990
+ exportedAt: snapshots.map((snapshot) => snapshot.exportedAt).sort().at(-1) ?? "",
47991
+ components: snapshots.flatMap((snapshot) => snapshot.components),
47992
+ textStyles: snapshots.flatMap((snapshot) => snapshot.textStyles),
47993
+ tokens: [...tokens.values()]
47994
+ };
47995
+ }
47996
+ function missingSnapshots(graph, keys) {
47997
+ const paths = keys.map((key) => graph.legacy ? "library/index.json" : `library/${key}/index.json`);
47998
+ return `${paths.join(", ")} ${paths.length === 1 ? "is" : "are"} missing`;
47999
+ }
48000
+ function checkLibraries(input2) {
48001
+ const graph = designGraph(input2.config);
48002
+ const { libraries, missing } = snapshotsFor(input2);
48003
+ const kits = new Set(graph.kits.map((kit) => kit.key));
48004
+ const absent = missing.filter((key) => !kits.has(key));
48005
+ if (absent.length > 0)
48006
+ return { notRun: missingSnapshots(graph, absent) };
48007
+ if (libraries.length === 0)
48008
+ return { notRun: "library/index.json is missing" };
48009
+ const all = joinLibraries(libraries);
48010
+ const joined = /* @__PURE__ */ new Map();
48011
+ return {
48012
+ all,
48013
+ forTarget(targetId) {
48014
+ const target = input2.config.targets.find((candidate) => candidate.id === targetId);
48015
+ if (!target)
48016
+ return all;
48017
+ const cached2 = joined.get(targetId);
48018
+ if (cached2)
48019
+ return cached2;
48020
+ const own2 = joinLibraries(snapshotsFor(input2, target).libraries);
48021
+ joined.set(targetId, own2);
48022
+ return own2;
48023
+ }
48024
+ };
48025
+ }
48026
+
47060
48027
  // packages/zaku/dist/lib/domain/checks/binding.js
47061
48028
  var PAINTS = ["fill", "stroke", "textColor", "glyph"];
47062
48029
  var SEMANTIC = /^(color|mode|radius)\//;
47063
- var binding = ({ library }) => {
47064
- if (!library)
47065
- return { notRun: "library.json is missing" };
48030
+ var binding = (input2) => {
48031
+ const libraries = checkLibraries(input2);
48032
+ if ("notRun" in libraries)
48033
+ return libraries;
48034
+ const library = libraries.all;
47066
48035
  const findings = [];
47067
48036
  for (const component2 of library.components) {
47068
48037
  for (const variant of component2.variants) {
@@ -47428,8 +48397,9 @@ var frameCheck = ({ config: config2, outlines }) => {
47428
48397
  }
47429
48398
  return { findings };
47430
48399
  };
47431
- var systemBars = ({ config: config2, outlines }) => {
48400
+ var systemBars = ({ config: config2, outlines, kits }) => {
47432
48401
  const families2 = new Map(config2.targets.map((target) => [target.id, target.family]));
48402
+ const graph = designGraph(config2);
47433
48403
  const bars = { ...DEFAULT_SYSTEM_BARS, ...config2.systemBars };
47434
48404
  const findings = [];
47435
48405
  for (const outline of outlines) {
@@ -47437,7 +48407,10 @@ var systemBars = ({ config: config2, outlines }) => {
47437
48407
  const family = families2.get(frame.target);
47438
48408
  if (!family)
47439
48409
  continue;
47440
- const held = new Set(frame.instances.map((instance) => instance.component));
48410
+ const own2 = graph.kits.filter((kit) => kit.family === family && kits[kit.key]);
48411
+ const kitKeys = new Set(own2.flatMap((kit) => kits[kit.key]?.components.map((component2) => component2.key) ?? []));
48412
+ const from = own2.length > 0 ? ` from ${own2.map((kit) => kit.key).join(" or ")}` : "";
48413
+ const held = new Set(frame.instances.filter((instance) => own2.length === 0 || kitKeys.has(instance.componentKey)).map((instance) => instance.component));
47441
48414
  for (const names of bars[family]) {
47442
48415
  if (!names.some((name) => held.has(name))) {
47443
48416
  findings.push({
@@ -47446,7 +48419,7 @@ var systemBars = ({ config: config2, outlines }) => {
47446
48419
  screen: outline.screen,
47447
48420
  frame: frame.name,
47448
48421
  nodeId: frame.nodeId,
47449
- message: `no ${names.join(" or ")} instance on a ${family} frame`
48422
+ message: `no ${names.join(" or ")} instance${from} on a ${family} frame`
47450
48423
  });
47451
48424
  }
47452
48425
  }
@@ -47460,6 +48433,18 @@ var placement = ({ maps, outlines }) => {
47460
48433
  for (const outline of outlines) {
47461
48434
  const title = titles.get(`${outline.feature}/${outline.screen}`);
47462
48435
  for (const frame of outline.frames) {
48436
+ if (frame.page !== void 0) {
48437
+ if (title !== void 0 && frame.page !== title)
48438
+ findings.push({
48439
+ check: "placement",
48440
+ feature: outline.feature,
48441
+ screen: outline.screen,
48442
+ frame: frame.name,
48443
+ nodeId: frame.nodeId,
48444
+ message: `sits on the page ${frame.page}, and its screen is drawn on the page ${title}`
48445
+ });
48446
+ continue;
48447
+ }
47463
48448
  const expected = [title, frame.state];
47464
48449
  if (frame.containers.length !== 2 || frame.containers[0] !== expected[0] || frame.containers[1] !== expected[1]) {
47465
48450
  findings.push({
@@ -47493,8 +48478,19 @@ var naming = ({ outlines }) => {
47493
48478
  }
47494
48479
  return { findings };
47495
48480
  };
47496
- var component = ({ outlines }) => {
48481
+ var component = ({ config: config2, outlines, productComponents }) => {
47497
48482
  const findings = [];
48483
+ const graph = designGraph(config2);
48484
+ for (const drawn of productComponents) {
48485
+ const platform = graph.products.find((product) => product.key === drawn.source)?.platform;
48486
+ const branches = new Set(config2.targets.filter((target) => graph.platformOf(target) === platform).flatMap((target) => graph.branchOf(target) ?? []));
48487
+ const homes = graph.libraries.filter((library) => library.layer === "components" && library.branches.some((branch) => branches.has(branch))).map((library) => library.key);
48488
+ findings.push({
48489
+ check: "component",
48490
+ nodeId: drawn.nodeId,
48491
+ message: `${drawn.name} is a component on the product page ${drawn.page}; a product file draws screens only, so draw it in ${homes.length > 0 ? homes.join(" or ") : `a Components library of the ${[...branches].join(" or ")} branch`}`
48492
+ });
48493
+ }
47498
48494
  for (const outline of outlines) {
47499
48495
  const seen = /* @__PURE__ */ new Map();
47500
48496
  for (const frame of outline.frames) {
@@ -47526,13 +48522,16 @@ var component = ({ outlines }) => {
47526
48522
  };
47527
48523
 
47528
48524
  // packages/zaku/dist/lib/domain/checks/font.js
47529
- var font = ({ outlines, library }) => {
47530
- if (!library)
47531
- return { notRun: "library.json is missing" };
47532
- const keys = new Set(library.textStyles.map((style) => style.key));
48525
+ var font = (input2) => {
48526
+ const libraries = checkLibraries(input2);
48527
+ if ("notRun" in libraries)
48528
+ return libraries;
48529
+ const keys = /* @__PURE__ */ new Map();
47533
48530
  const findings = [];
47534
- for (const outline of outlines) {
48531
+ for (const outline of input2.outlines) {
47535
48532
  for (const frame of outline.frames) {
48533
+ const styles = keys.get(frame.target) ?? new Set(libraries.forTarget(frame.target).textStyles.map((style) => style.key));
48534
+ keys.set(frame.target, styles);
47536
48535
  const where = { feature: outline.feature, screen: outline.screen, frame: frame.name };
47537
48536
  for (const text of frame.texts) {
47538
48537
  if (text.styleKey === null) {
@@ -47542,7 +48541,7 @@ var font = ({ outlines, library }) => {
47542
48541
  nodeId: text.nodeId,
47543
48542
  message: "text outside a component uses no library text style"
47544
48543
  });
47545
- } else if (!keys.has(text.styleKey)) {
48544
+ } else if (!styles.has(text.styleKey)) {
47546
48545
  findings.push({
47547
48546
  check: "font",
47548
48547
  ...where,
@@ -47557,8 +48556,9 @@ var font = ({ outlines, library }) => {
47557
48556
  };
47558
48557
 
47559
48558
  // packages/zaku/dist/lib/domain/checks/freshness.js
47560
- var freshness = ({ maps, outlines, library }) => {
48559
+ var freshness = ({ config: config2, maps, outlines, flows, libraries }) => {
47561
48560
  const digests = new Map(maps.map((loaded) => [loaded.map.feature, loaded.digest]));
48561
+ const graph = designGraph(config2);
47562
48562
  const findings = [];
47563
48563
  for (const outline of outlines) {
47564
48564
  const where = { feature: outline.feature, screen: outline.screen };
@@ -47570,11 +48570,35 @@ var freshness = ({ maps, outlines, library }) => {
47570
48570
  message: "the map changed after this outline was read; run zaku outline"
47571
48571
  });
47572
48572
  }
47573
- if (library && outline.libraryVersion !== library.version) {
48573
+ for (const [key, library] of Object.entries(libraries)) {
48574
+ const read = outline.libraryVersions[key] ?? (graph.legacy ? outline.libraryVersions[LEGACY_LIBRARY_VERSION] : void 0);
48575
+ if (read === library.version)
48576
+ continue;
47574
48577
  findings.push({
47575
48578
  check: "freshness",
47576
48579
  ...where,
47577
- message: "the library changed after this outline was read; run zaku outline"
48580
+ message: graph.legacy ? "the library changed after this outline was read; run zaku outline" : `library ${key} changed after this outline was read; run zaku outline`
48581
+ });
48582
+ }
48583
+ }
48584
+ const drawn = new Map(outlines.flatMap((outline) => outline.frames.map((frame) => [`${outline.feature}/${outline.screen}
48585
+ ${frame.name}`, frame])));
48586
+ for (const page of flows) {
48587
+ for (const copy2 of page.frames) {
48588
+ if (copy2.screen === null)
48589
+ continue;
48590
+ const source = drawn.get(`${copy2.screen}
48591
+ ${copy2.name}`);
48592
+ if (source?.digest === void 0 || source.digest === copy2.digest)
48593
+ continue;
48594
+ const [feature = "", screen = ""] = copy2.screen.split("/");
48595
+ findings.push({
48596
+ check: "freshness",
48597
+ feature,
48598
+ screen,
48599
+ frame: copy2.name,
48600
+ nodeId: copy2.nodeId,
48601
+ message: `the copy on the flow page ${page.page} differs from the frame it copies; copy it again`
47578
48602
  });
47579
48603
  }
47580
48604
  }
@@ -47582,13 +48606,43 @@ var freshness = ({ maps, outlines, library }) => {
47582
48606
  };
47583
48607
 
47584
48608
  // packages/zaku/dist/lib/domain/checks/library.js
47585
- var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47586
- if (!library)
47587
- return { notRun: "library.json is missing" };
48609
+ function frameFindings(input2) {
48610
+ const libraries = checkLibraries(input2);
48611
+ if ("notRun" in libraries)
48612
+ return libraries;
48613
+ const { config: config2, maps, outlines } = input2;
48614
+ const library = libraries.all;
47588
48615
  const bars = systemBarNames(config2);
47589
48616
  const owned = outlines.flatMap((outline) => outline.frames.flatMap((frame) => frame.instances.filter((instance) => instance.local).map((instance) => instance.component)));
47590
48617
  const names = /* @__PURE__ */ new Set([...library.components.map((component2) => component2.name), ...owned]);
47591
48618
  const keys = new Set(library.components.flatMap((component2) => component2.variants.map((variant) => variant.key)));
48619
+ const graph = designGraph(config2);
48620
+ const owners = /* @__PURE__ */ new Map();
48621
+ for (const [key, snapshot] of Object.entries(input2.libraries)) {
48622
+ const holds = graph.libraries.find((candidate) => candidate.key === key)?.branches ?? [];
48623
+ for (const component2 of snapshot.components) {
48624
+ const branches = component2.branch ? [component2.branch] : holds;
48625
+ for (const variant of component2.variants)
48626
+ owners.set(variant.key, `${key}, a ${branches.join(" and ")} library`);
48627
+ }
48628
+ }
48629
+ for (const [key, kit] of Object.entries(input2.kits)) {
48630
+ for (const component2 of kit.components)
48631
+ owners.set(component2.key, `${key}, an ${kit.family} kit`);
48632
+ }
48633
+ const allowed2 = /* @__PURE__ */ new Map();
48634
+ const allowedFor = (target) => {
48635
+ const cached2 = allowed2.get(target.id);
48636
+ if (cached2)
48637
+ return cached2;
48638
+ const { libraries: own2, kits } = snapshotsFor(input2, target);
48639
+ const set2 = /* @__PURE__ */ new Set([
48640
+ ...own2.flatMap((snapshot) => snapshot.components.flatMap((component2) => component2.variants.map((v) => v.key))),
48641
+ ...kits.flatMap((kit) => kit.components.map((component2) => component2.key))
48642
+ ]);
48643
+ allowed2.set(target.id, set2);
48644
+ return set2;
48645
+ };
47592
48646
  const findings = [];
47593
48647
  for (const { map: map2 } of maps) {
47594
48648
  for (const [id2, screen] of Object.entries(map2.screens)) {
@@ -47608,8 +48662,22 @@ var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47608
48662
  for (const outline of outlines) {
47609
48663
  for (const frame of outline.frames) {
47610
48664
  const where = { feature: outline.feature, screen: outline.screen, frame: frame.name };
48665
+ const target = config2.targets.find((candidate) => candidate.id === frame.target);
47611
48666
  for (const instance of frame.instances) {
47612
- if (!instance.local && !bars.has(instance.component) && !keys.has(instance.componentKey)) {
48667
+ if (instance.local)
48668
+ continue;
48669
+ const owner = owners.get(instance.componentKey);
48670
+ if (target && owner && !allowedFor(target).has(instance.componentKey)) {
48671
+ const draws = owner.endsWith(" kit") ? `${target.family} kits` : graph.branchOf(target) ?? "no branch";
48672
+ findings.push({
48673
+ check: "library",
48674
+ ...where,
48675
+ nodeId: instance.nodeId,
48676
+ message: `an instance of ${instance.component} comes from ${owner}; a ${target.name} frame draws from ${draws}`
48677
+ });
48678
+ continue;
48679
+ }
48680
+ if (!bars.has(instance.component) && !keys.has(instance.componentKey) && !owner) {
47613
48681
  findings.push({
47614
48682
  check: "library",
47615
48683
  ...where,
@@ -47629,6 +48697,82 @@ var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47629
48697
  }
47630
48698
  }
47631
48699
  return { findings };
48700
+ }
48701
+ function referable(snapshot) {
48702
+ return {
48703
+ variable: Object.values(snapshot.variableKeys ?? {}).flatMap((variable) => variable.local ? [variable.key] : []),
48704
+ "text style": snapshot.textStyles.map((style) => style.key),
48705
+ "effect style": (snapshot.effectStyles ?? []).map((style) => style.key),
48706
+ instance: snapshot.components.flatMap((component2) => [
48707
+ component2.key,
48708
+ ...component2.variants.map((variant) => variant.key)
48709
+ ])
48710
+ };
48711
+ }
48712
+ function referenceFindings(input2) {
48713
+ const graph = designGraph(input2.config);
48714
+ const findings = [];
48715
+ for (const [key, snapshot] of Object.entries(input2.libraries)) {
48716
+ if (!snapshot.variableKeys)
48717
+ continue;
48718
+ const bases = graph.closure(key).slice(1);
48719
+ const missing = bases.filter((base) => !input2.libraries[base] && !input2.kits[base]);
48720
+ if (missing.length > 0) {
48721
+ const paths = missing.map((base) => graph.kits.some((kit) => kit.key === base) ? `library/${base}/kit.json` : `library/${base}/index.json`);
48722
+ return { notRun: `${paths.join(", ")} ${paths.length === 1 ? "is" : "are"} missing` };
48723
+ }
48724
+ const allowed2 = {
48725
+ variable: /* @__PURE__ */ new Set(),
48726
+ "text style": /* @__PURE__ */ new Set(),
48727
+ "effect style": /* @__PURE__ */ new Set(),
48728
+ instance: new Set(snapshot.localComponents ?? [])
48729
+ };
48730
+ for (const from of [key, ...bases]) {
48731
+ const library = input2.libraries[from];
48732
+ if (library)
48733
+ for (const [kind, keys] of Object.entries(referable(library)))
48734
+ for (const ref of keys)
48735
+ allowed2[kind].add(ref);
48736
+ for (const component2 of input2.kits[from]?.components ?? [])
48737
+ allowed2.instance.add(component2.key);
48738
+ }
48739
+ const extended2 = bases.length > 0 ? `from ${bases.join(", ")}` : "from a library it extends, and it extends none";
48740
+ for (const component2 of snapshot.components) {
48741
+ for (const variant of component2.variants) {
48742
+ for (const item of variant.modes[0]?.items ?? []) {
48743
+ const refs = [
48744
+ ...(item.variableIds ?? []).map((id2) => [
48745
+ "variable",
48746
+ snapshot.variableKeys?.[id2]?.key ?? id2
48747
+ ]),
48748
+ ...item.textStyleKey ? [["text style", item.textStyleKey]] : [],
48749
+ ...item.effectStyleKey ? [["effect style", item.effectStyleKey]] : [],
48750
+ ...item.componentKey ? [["instance", item.componentKey]] : []
48751
+ ];
48752
+ for (const [kind, ref] of refs) {
48753
+ if (allowed2[kind].has(ref))
48754
+ continue;
48755
+ findings.push({
48756
+ check: "library",
48757
+ nodeId: item.nodeId,
48758
+ field: "references",
48759
+ message: `${key}: ${component2.name} ${variant.name} ${item.path || "(root)"}: ${kind} ${ref} is neither the library's own nor ${extended2}`
48760
+ });
48761
+ }
48762
+ }
48763
+ }
48764
+ }
48765
+ }
48766
+ return { findings };
48767
+ }
48768
+ var libraryCheck = (input2) => {
48769
+ const frames = frameFindings(input2);
48770
+ if ("notRun" in frames)
48771
+ return frames;
48772
+ const references = referenceFindings(input2);
48773
+ if ("notRun" in references)
48774
+ return references;
48775
+ return { findings: [...frames.findings, ...references.findings] };
47632
48776
  };
47633
48777
 
47634
48778
  // packages/zaku/dist/lib/domain/checks/navigation.js
@@ -47699,7 +48843,11 @@ var ALLOWED_OVERRIDES = /* @__PURE__ */ new Set([
47699
48843
  "name",
47700
48844
  "componentProperties",
47701
48845
  "componentPropertyReferences",
47702
- "exposedInstances"
48846
+ "exposedInstances",
48847
+ "reactions",
48848
+ "transitionNodeID",
48849
+ "transitionDuration",
48850
+ "transitionEasing"
47703
48851
  ]);
47704
48852
  var SIZING_MODES = /* @__PURE__ */ new Set(["primaryAxisSizingMode", "counterAxisSizingMode"]);
47705
48853
  function componentSized(field, size2, main2) {
@@ -47713,9 +48861,12 @@ function stretches(sizing) {
47713
48861
  function insideSwap(instance, nodeId) {
47714
48862
  return instance.nested.some((nested) => nodeId.startsWith(`${nested.nodeId};`) && instance.swaps.includes(nested.componentKey));
47715
48863
  }
47716
- var overrides = ({ outlines, library }) => {
47717
- if (!library)
47718
- return { notRun: "library.json is missing" };
48864
+ var overrides = (input2) => {
48865
+ const libraries = checkLibraries(input2);
48866
+ if ("notRun" in libraries)
48867
+ return libraries;
48868
+ const { outlines } = input2;
48869
+ const library = libraries.all;
47719
48870
  const nestedOf = /* @__PURE__ */ new Map();
47720
48871
  const componentOf = /* @__PURE__ */ new Map();
47721
48872
  for (const component2 of library.components) {
@@ -47776,18 +48927,58 @@ var overrides = ({ outlines, library }) => {
47776
48927
  return { findings };
47777
48928
  };
47778
48929
 
48930
+ // packages/zaku/dist/lib/domain/flows.js
48931
+ function flowsOf(maps) {
48932
+ const index = indexScreens(maps);
48933
+ const flows = [];
48934
+ for (const root of index.values()) {
48935
+ if (!root.screen.root)
48936
+ continue;
48937
+ const screens = [root];
48938
+ for (let at = 0; at < screens.length; at += 1) {
48939
+ const from = screens[at];
48940
+ if (!from)
48941
+ continue;
48942
+ for (const exit of from.screen.exits) {
48943
+ const next = index.get(qualify(exit.to, from.feature));
48944
+ if (next && !screens.includes(next))
48945
+ screens.push(next);
48946
+ }
48947
+ }
48948
+ flows.push({ root, screens });
48949
+ }
48950
+ return flows;
48951
+ }
48952
+
47779
48953
  // packages/zaku/dist/lib/domain/checks/prototype.js
47780
- var prototype = ({ maps, outlines }) => {
47781
- if (outlines.length === 0)
47782
- return { findings: [] };
48954
+ function legacyScreens(input2) {
48955
+ if (input2.flows.length === 0)
48956
+ return input2.outlines;
48957
+ const byScreen = /* @__PURE__ */ new Map();
48958
+ for (const page of input2.flows) {
48959
+ for (const frame of page.frames) {
48960
+ if (frame.screen === null)
48961
+ continue;
48962
+ const [feature = "", screen = ""] = frame.screen.split("/");
48963
+ const entry = byScreen.get(frame.screen) ?? { feature, screen, frames: [] };
48964
+ entry.frames.push(frame);
48965
+ byScreen.set(frame.screen, entry);
48966
+ }
48967
+ }
48968
+ return [...byScreen.values()];
48969
+ }
48970
+ function legacyFindings(input2) {
48971
+ const screens = legacyScreens(input2);
48972
+ if (screens.length === 0)
48973
+ return [];
47783
48974
  const screenOfFrame = /* @__PURE__ */ new Map();
47784
- const outlineOf = new Map(outlines.map((outline) => [`${outline.feature}/${outline.screen}`, outline]));
47785
- for (const outline of outlines) {
48975
+ const outlineOf = new Map(screens.map((outline) => [`${outline.feature}/${outline.screen}`, outline]));
48976
+ for (const outline of screens) {
47786
48977
  for (const frame of outline.frames)
47787
48978
  screenOfFrame.set(frame.nodeId, `${outline.feature}/${outline.screen}`);
47788
48979
  }
47789
48980
  const findings = [];
47790
- for (const { map: map2 } of maps) {
48981
+ for (const { map: map2 } of input2.maps) {
47791
48982
  for (const [id2, screen] of Object.entries(map2.screens)) {
47792
48983
  const outline = outlineOf.get(`${map2.feature}/${id2}`);
47793
48984
  if (!outline)
@@ -47824,8 +49015,64 @@ var prototype = ({ maps, outlines }) => {
47824
49015
  }
47825
49016
  }
47826
49017
  }
47827
- return { findings };
47828
- };
49018
+ return findings;
49019
+ }
49020
+ function flowFindings(input2) {
49021
+ const prototypes = new Set(designGraph(input2.config).prototypes.map((file2) => file2.key));
49022
+ const findings = [];
49023
+ for (const flow of flowsOf(input2.maps)) {
49024
+ const { root } = flow;
49025
+ const title = root.screen.title;
49026
+ const keyOf = (ref) => `${ref.feature}/${ref.id}`;
49027
+ const pages = input2.flows.filter((page) => page.page === title && prototypes.has(page.source));
49028
+ if (pages.length === 0) {
49029
+ findings.push({
49030
+ check: "prototype",
49031
+ feature: root.feature,
49032
+ screen: root.id,
49033
+ message: `no flow page is named ${title}`
49034
+ });
49035
+ continue;
49036
+ }
49037
+ const inFlow = new Set(flow.screens.map(keyOf));
49038
+ for (const page of pages) {
49039
+ const screenOf = new Map(page.frames.map((frame) => [frame.nodeId, frame.screen]));
49040
+ const first = root.screen.states[0];
49041
+ if (!page.frames.some((frame) => frame.screen === keyOf(root) && frame.state === first && frame.start))
49042
+ findings.push({
49043
+ check: "prototype",
49044
+ feature: root.feature,
49045
+ screen: root.id,
49046
+ message: `the flow page ${title} does not start on ${title}'s first state`
49047
+ });
49048
+ for (const ref of flow.screens) {
49049
+ const where = { check: "prototype", feature: ref.feature, screen: ref.id };
49050
+ const copies = page.frames.filter((frame) => frame.screen === keyOf(ref));
49051
+ for (const exit of ref.screen.exits) {
49052
+ const target = qualify(exit.to, ref.feature);
49053
+ if (!inFlow.has(target))
49054
+ continue;
49055
+ const connected = copies.some((frame) => frame.links.some((link) => link.to !== "back" && screenOf.get(link.to.nodeId) === target));
49056
+ if (!connected)
49057
+ findings.push({
49058
+ ...where,
49059
+ field: "exits",
49060
+ message: `on the flow page ${title}, the exit to ${exit.to} (${exit.via}) has no connection`
49061
+ });
49062
+ }
49063
+ if (ref !== root && copies.length > 0 && !copies.some((frame) => frame.links.some((link) => link.to === "back")))
49064
+ findings.push({
49065
+ ...where,
49066
+ message: `on the flow page ${title}, a screen that is not the root has no Back action`
49067
+ });
49068
+ }
49069
+ }
49070
+ }
49071
+ return findings;
49072
+ }
49073
+ var prototype = (input2) => ({
49074
+ findings: designGraph(input2.config).prototypes.length > 0 ? flowFindings(input2) : legacyFindings(input2)
49075
+ });
47829
49076
 
47830
49077
  // packages/zaku/dist/lib/domain/checks/recipe.js
47831
49078
  function propsLabel(props) {
@@ -47845,7 +49092,7 @@ var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "curs
47845
49092
  function families(stack) {
47846
49093
  return stack.split(",").map((family) => family.trim().replace(/^["']|["']$/g, "")).filter((family) => family !== "" && !GENERIC.has(family.toLowerCase()));
47847
49094
  }
47848
- function colour(at, field, library, code) {
49095
+ function colour2(at, field, library, code) {
47849
49096
  const transparent = { r: 0, g: 0, b: 0, a: 0 };
47850
49097
  const drawn = library ?? transparent;
47851
49098
  const coded = code ?? transparent;
@@ -47874,12 +49121,12 @@ function size(at, field, library, code) {
47874
49121
  }
47875
49122
  function comparePart(at, part) {
47876
49123
  const { item } = at;
47877
- colour(at, "background", item.fill, part.background);
49124
+ colour2(at, "background", item.fill, part.background);
47878
49125
  if (item.type.toUpperCase() === "TEXT")
47879
- colour(at, "color", item.textColor, part.color);
47880
- colour(at, "borderColor", item.stroke, part.borderColor);
49126
+ colour2(at, "color", item.textColor, part.color);
49127
+ colour2(at, "borderColor", item.stroke, part.borderColor);
47881
49128
  if (item.glyph)
47882
- colour(at, "glyph", item.glyph, part.color);
49129
+ colour2(at, "glyph", item.glyph, part.color);
47883
49130
  if (item.path === "") {
47884
49131
  if (!at.fallback)
47885
49132
  size(at, "width", item.width, part.width);
@@ -47905,9 +49152,14 @@ function comparePart(at, part) {
47905
49152
  });
47906
49153
  }
47907
49154
  }
47908
- var recipe = ({ config: config2, library, recipe: doc }) => {
47909
- if (!doc || !library)
47910
- return { notRun: "recipe.json or library.json is missing" };
49155
+ var recipe = (input2) => {
49156
+ const { config: config2, recipe: doc } = input2;
49157
+ const libraries = checkLibraries(input2);
49158
+ if ("notRun" in libraries)
49159
+ return doc ? libraries : { notRun: `recipe.json or ${libraries.notRun}` };
49160
+ if (!doc)
49161
+ return { notRun: "recipe.json is missing" };
49162
+ const library = libraries.all;
47911
49163
  const findings = [];
47912
49164
  for (const code of doc.components) {
47913
49165
  const component2 = library.components.find((candidate) => candidate.name === code.name);
@@ -47998,9 +49250,12 @@ var MIN_TARGET = {
47998
49250
  function controlsOf(frame) {
47999
49251
  return frame.instances.flatMap((instance) => [instance, ...instance.nested].flatMap((control) => control.interactive && control.bounds ? [{ nodeId: control.nodeId, componentKey: control.componentKey, bounds: control.bounds }] : []));
48000
49252
  }
48001
- var targetSize = ({ config: config2, outlines, library }) => {
48002
- if (!library)
48003
- return { notRun: "library.json is missing" };
49253
+ var targetSize = (input2) => {
49254
+ const libraries = checkLibraries(input2);
49255
+ if ("notRun" in libraries)
49256
+ return libraries;
49257
+ const { config: config2, outlines } = input2;
49258
+ const library = libraries.all;
48004
49259
  const presetHeight = /* @__PURE__ */ new Map();
48005
49260
  for (const component2 of library.components) {
48006
49261
  for (const variant of component2.variants) {
@@ -48082,9 +49337,14 @@ function rgbaOf(token) {
48082
49337
  const [r, g, b] = token.$value.components;
48083
49338
  return { r, g, b, a: token.$value.alpha };
48084
49339
  }
48085
- var tokensCheck = ({ config: config2, library, tokens }) => {
48086
- if (!library || !tokens)
48087
- return { notRun: "library.json or tokens.json is missing" };
49340
+ var tokensCheck = (input2) => {
49341
+ const { config: config2, tokens } = input2;
49342
+ const libraries = checkLibraries(input2);
49343
+ if ("notRun" in libraries)
49344
+ return libraries;
49345
+ if (!tokens)
49346
+ return { notRun: "tokens.json is missing" };
49347
+ const library = libraries.all;
48088
49348
  const sets = tokenSets(tokens);
48089
49349
  const findings = [];
48090
49350
  for (const [scheme, set2] of [
@@ -48388,13 +49648,25 @@ async function loadDesign(root) {
48388
49648
  message: `two screens share the title ${title}; a frame name could not tell them apart`
48389
49649
  });
48390
49650
  }
48391
- const { outlines, unmapped, errors: outlineErrors } = await loadOutlines(paths.outline);
49651
+ const { outlines, flows, unmapped, errors: outlineErrors } = await loadOutlines(paths.outline);
48392
49652
  findings.push(...outlineErrors.map(schemaFinding));
48393
- const library = await optional3(() => readOptionalDesignFile(paths.library, librarySnapshotSchema), findings);
49653
+ const { libraries, kits, errors: snapshotErrors } = await readDesignSnapshots(paths, config2);
49654
+ findings.push(...snapshotErrors.map(schemaFinding));
48394
49655
  const tokens = await optional3(() => readOptionalDesignFile(paths.tokens, tokenDocumentSchema), findings);
48395
49656
  const recipe2 = await optional3(() => readOptionalDesignFile(paths.recipe, recipeDocumentSchema), findings);
48396
49657
  return {
48397
- input: { config: config2, maps, outlines, unmapped: unmapped?.frames ?? [], library, tokens, recipe: recipe2 },
49658
+ input: {
49659
+ config: config2,
49660
+ maps,
49661
+ outlines,
49662
+ flows,
49663
+ unmapped: unmapped?.frames ?? [],
49664
+ productComponents: unmapped?.components ?? [],
49665
+ libraries,
49666
+ kits,
49667
+ tokens,
49668
+ recipe: recipe2
49669
+ },
48398
49670
  findings
48399
49671
  };
48400
49672
  }
@@ -48429,6 +49701,7 @@ function refusalOf(error62) {
48429
49701
  }
48430
49702
  if (error62 instanceof FigmaFileKeyMissing) return { code: 3, line: error62.message };
48431
49703
  if (error62 instanceof FigmaTokenMissing) return { code: 3, line: error62.message };
49704
+ if (error62 instanceof LibraryKeyMissing) return { code: 3, line: error62.message };
48432
49705
  if (error62 instanceof RecipePageFailed) return { code: 2, line: `zaku recipe: ${error62.message}` };
48433
49706
  return null;
48434
49707
  }
@@ -58116,35 +59389,35 @@ var StdioServerTransport = class {
58116
59389
  };
58117
59390
 
58118
59391
  // apps/zaku-cli/src/mcp/server.ts
58119
- import { mkdir as mkdir4 } from "node:fs/promises";
59392
+ import { mkdir as mkdir5 } from "node:fs/promises";
58120
59393
  import { tmpdir } from "node:os";
58121
- import { join as join9 } from "node:path";
59394
+ import { join as join11 } from "node:path";
58122
59395
 
58123
59396
  // apps/zaku-cli/src/mcp/screenshots.ts
58124
- import { readdir as readdir3, rm as rm2, stat } from "node:fs/promises";
58125
- import { join as join7 } from "node:path";
59397
+ import { readdir as readdir4, rm as rm3, stat } from "node:fs/promises";
59398
+ import { join as join9 } from "node:path";
58126
59399
  var KEPT_MS = 24 * 60 * 60 * 1e3;
58127
59400
  async function pruneScreenshots(dir, now) {
58128
59401
  let names;
58129
59402
  try {
58130
- names = await readdir3(dir);
59403
+ names = await readdir4(dir);
58131
59404
  } catch {
58132
59405
  return;
58133
59406
  }
58134
59407
  for (const name of names.filter((entry) => entry.endsWith(".png"))) {
58135
- const path2 = join7(dir, name);
59408
+ const path2 = join9(dir, name);
58136
59409
  const written = await stat(path2).then(
58137
59410
  (stats) => stats.mtimeMs,
58138
59411
  () => now
58139
59412
  );
58140
- if (now - written > KEPT_MS) await rm2(path2, { force: true });
59413
+ if (now - written > KEPT_MS) await rm3(path2, { force: true });
58141
59414
  }
58142
59415
  }
58143
59416
 
58144
59417
  // apps/zaku-cli/src/mcp/tools.ts
58145
59418
  import { writeFile as writeFile5 } from "node:fs/promises";
58146
- import { join as join8 } from "node:path";
58147
- var fileKey = external_exports.string().min(1).regex(/^[0-9a-zA-Z]{22,128}$/).describe(
59419
+ import { join as join10 } from "node:path";
59420
+ var fileKey2 = external_exports.string().min(1).regex(/^[0-9a-zA-Z]{22,128}$/).describe(
58148
59421
  "The key of the Figma file to use. If the URL is provided, extract the file key from the URL: https://figma.com/design/:fileKey/:fileName."
58149
59422
  );
58150
59423
  var FALLBACK = "zaku's fallback for Figma's MCP server: the same tool and input, run in the file through the zaku plugin, which the person has open in that file with its key given.";
@@ -58156,7 +59429,7 @@ function registerFigmaTools(server, bridge, options) {
58156
59429
  inputSchema: external_exports.object({
58157
59430
  code: external_exports.string().max(5e4).describe("JavaScript code to execute. Has access to the `figma` global (Figma Plugin API)"),
58158
59431
  description: external_exports.string().max(2e3).describe("A concise description of what the code aims to do"),
58159
- fileKey,
59432
+ fileKey: fileKey2,
58160
59433
  skillNames: external_exports.string().optional().describe("A comma-separated list of Figma skill names being followed, if any.")
58161
59434
  }).strict()
58162
59435
  },
@@ -58174,7 +59447,7 @@ function registerFigmaTools(server, bridge, options) {
58174
59447
  inputSchema: external_exports.object({
58175
59448
  contentsOnly: external_exports.boolean().optional().describe("When true, renders the node in isolation. Defaults to false."),
58176
59449
  enableBase64Response: external_exports.boolean().default(false).describe("When true, the response also includes the screenshot inline as a base64-encoded image entry."),
58177
- fileKey,
59450
+ fileKey: fileKey2,
58178
59451
  maxDimension: external_exports.number().int().max(65536).default(1024).describe("Maximum pixel size of the longer edge of the rendered screenshot. Defaults to 1024."),
58179
59452
  nodeId: external_exports.string().min(1).regex(/^\d+[:-]\d+$/).describe('The ID of the node in the Figma document, eg. "123:456" or "123-456".')
58180
59453
  }).strict()
@@ -58187,7 +59460,7 @@ function registerFigmaTools(server, bridge, options) {
58187
59460
  options.timeoutMs
58188
59461
  );
58189
59462
  if (!answer.ok) return { isError: true, content: [{ type: "text", text: answer.error }] };
58190
- const path2 = join8(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
59463
+ const path2 = join10(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
58191
59464
  await writeFile5(path2, Buffer.from(answer.png, "base64"));
58192
59465
  const text = JSON.stringify({
58193
59466
  path: path2,
@@ -58213,8 +59486,8 @@ async function serveMcp(log) {
58213
59486
  log(
58214
59487
  bridge.port === null ? "zaku mcp: no port from 7337 to 7346 was free; every call will say so" : `zaku mcp: listening for the zaku plugin on localhost:${bridge.port}`
58215
59488
  );
58216
- const imageDir = join9(tmpdir(), "zaku-screenshots");
58217
- await mkdir4(imageDir, { recursive: true });
59489
+ const imageDir = join11(tmpdir(), "zaku-screenshots");
59490
+ await mkdir5(imageDir, { recursive: true });
58218
59491
  await pruneScreenshots(imageDir, Date.now());
58219
59492
  const server = new McpServer({ name: "zaku", version: "1" });
58220
59493
  registerFigmaTools(server, bridge, { timeoutMs: CALL_TIMEOUT_MS, imageDir, log });
@@ -58230,7 +59503,7 @@ async function serveMcp(log) {
58230
59503
 
58231
59504
  // apps/zaku-cli/src/cli.ts
58232
59505
  var USE_FIGMA_CODE_LIMIT = 5e4;
58233
- var USAGE = "usage: zaku <check|outline|tokens|recipe|library|rules|budget|schema|mcp> [--root docs/design]";
59506
+ var USAGE = "usage: zaku <check|outline|tokens|recipe|library|libraries|rules|budget|schema|mcp> [--root docs/design]";
58234
59507
  async function sendOrRefuse(cli, io, finish, command) {
58235
59508
  try {
58236
59509
  return await cli.bus.send(command);
@@ -58248,6 +59521,14 @@ async function recordRestCalls(cli, io) {
58248
59521
  const { seat, run } = seatAndRun(io.env);
58249
59522
  await cli.bus.send(new RecordBudgetCalls({ day: today(clock.now()), seat, run, kind: "rest", count }));
58250
59523
  }
59524
+ var librariesCommand = (program2, io, finish) => {
59525
+ program2.command("libraries").description("print the design graph: what each target draws from, and what each file extends and is extended by").option("--root <dir>", "the design directory", "docs/design").action(async (options) => {
59526
+ const paths = designPaths(resolve2(io.cwd, options.root));
59527
+ const config2 = await readDesignFile(paths.config, zakuConfigSchema);
59528
+ for (const line of printGraph(config2, await readDesignSnapshots(paths, config2))) io.out(line);
59529
+ finish(0);
59530
+ });
59531
+ };
58251
59532
  var schemaCommand = (program2, io, finish, cli) => {
58252
59533
  program2.command("schema").description("write the JSON Schemas an editor reads for the design files").option("--out <dir>", "where to write them", "docs/design").action(async (options) => {
58253
59534
  const written = await cli.bus.send(new WriteSchemas({ out: resolve2(io.cwd, options.out) }));
@@ -58260,7 +59541,7 @@ var checkCommand = (program2, io, finish) => {
58260
59541
  const loaded = await loadDesign(resolve2(io.cwd, options.root));
58261
59542
  const report = loaded.input ? runChecks(loaded.input, ALL_CHECKS) : { findings: [], notRun: [], passed: [] };
58262
59543
  report.findings.unshift(...loaded.findings);
58263
- const ran = options.input ? parseDesignFile(options.input, await readFile5(resolve2(io.cwd, options.input), "utf8"), rulesAnswerSchema) : null;
59544
+ const ran = options.input ? parseDesignFile(options.input, await readFile6(resolve2(io.cwd, options.input), "utf8"), rulesAnswerSchema) : null;
58264
59545
  if (ran && "rulesError" in ran) {
58265
59546
  io.err(`zaku check: the rules did not run after the write: ${ran.rulesError}`);
58266
59547
  finish(1);
@@ -58274,7 +59555,7 @@ var checkCommand = (program2, io, finish) => {
58274
59555
  });
58275
59556
  };
58276
59557
  async function productPlaywright(cwd) {
58277
- const require2 = createRequire(join10(cwd, "package.json"));
59558
+ const require2 = createRequire(join12(cwd, "package.json"));
58278
59559
  let resolved;
58279
59560
  try {
58280
59561
  resolved = require2.resolve("playwright");
@@ -58286,14 +59567,14 @@ async function productPlaywright(cwd) {
58286
59567
  }
58287
59568
  function fromProductTree(cwd, resolved) {
58288
59569
  for (let dir = resolve2(cwd); ; dir = dirname4(dir)) {
58289
- if (resolved.startsWith(join10(dir, "node_modules") + sep)) return true;
59570
+ if (resolved.startsWith(join12(dir, "node_modules") + sep)) return true;
58290
59571
  if (dirname4(dir) === dir) return false;
58291
59572
  }
58292
59573
  }
58293
59574
  var runners = {
58294
59575
  run: execRun,
58295
59576
  loadPlaywright: productPlaywright,
58296
- rulesLibrary: () => readFile5(fileURLToPath(new URL("./rules.js", import.meta.url)), "utf8")
59577
+ rulesLibrary: () => readFile6(fileURLToPath(new URL("./rules.js", import.meta.url)), "utf8")
58297
59578
  };
58298
59579
  var mcpCommand = (program2, io, finish) => {
58299
59580
  program2.command("mcp").description(
@@ -58411,13 +59692,26 @@ var libraryCommand = (program2, io, finish, cli) => {
58411
59692
  finish(0);
58412
59693
  }
58413
59694
  );
58414
- library.command("save").description("write library.json: the library components read over Figma REST, joined to the variables export").option("--root <dir>", "the design directory", "docs/design").option(
59695
+ library.command("save").description(
59696
+ "write a library's snapshot from its components over Figma REST, joined to the variables export, or a kit's published components"
59697
+ ).option("--root <dir>", "the design directory", "docs/design").option("--key <key>", "the library or kit to save, as zaku.yaml lists it; needed when it lists several").option(
58415
59698
  "--input <variables.json>",
58416
59699
  "what use_figma returned for zaku library script; once per page export for a library in the product file",
58417
59700
  (value, previous = []) => [...previous, value]
58418
59701
  ).action(async (options) => {
58419
59702
  const root = resolve2(io.cwd, options.root);
58420
- if (!options.input || options.input.length === 0) {
59703
+ const config2 = await readDesignFile(designPaths(root).config, zakuConfigSchema);
59704
+ let target;
59705
+ try {
59706
+ target = librarySaveTarget(config2, options.key);
59707
+ } catch (error62) {
59708
+ const refusal = refusalOf(error62);
59709
+ if (!refusal) throw error62;
59710
+ io.err(refusal.line);
59711
+ finish(refusal.code);
59712
+ return;
59713
+ }
59714
+ if (target.kind === "library" && (!options.input || options.input.length === 0)) {
58421
59715
  io.err("zaku library save needs --input, the variables use_figma returned for zaku library script");
58422
59716
  finish(3);
58423
59717
  return;
@@ -58427,23 +59721,37 @@ var libraryCommand = (program2, io, finish, cli) => {
58427
59721
  finish(3);
58428
59722
  return;
58429
59723
  }
58430
- const part = mergeParts(
59724
+ const part = target.kind === "library" ? mergeParts(
58431
59725
  await Promise.all(
58432
- options.input.map(
58433
- async (input2) => parseDesignFile(input2, await readFile5(resolve2(io.cwd, input2), "utf8"), variablePartSchema)
59726
+ (options.input ?? []).map(
59727
+ async (input2) => parseDesignFile(input2, await readFile6(resolve2(io.cwd, input2), "utf8"), variablePartSchema)
58434
59728
  )
58435
59729
  )
58436
- );
59730
+ ) : void 0;
58437
59731
  try {
58438
- const snapshot = await cli.bus.send(new SaveLibrary({ root, part }));
59732
+ const saved = await cli.bus.send(
59733
+ new SaveLibrary({
59734
+ root,
59735
+ ...options.key !== void 0 ? { key: options.key } : {},
59736
+ ...part ? { part } : {}
59737
+ })
59738
+ );
59739
+ if (saved.kind === "kit") {
59740
+ io.out(`wrote ${saved.dir}/kit.json: ${saved.kit.components.length} components`);
59741
+ finish(0);
59742
+ return;
59743
+ }
59744
+ const { snapshot, written, unchanged, removed } = saved;
58439
59745
  if (snapshot.components.length === 0) {
58440
59746
  io.err(
58441
- "library.json holds no components: the file publishes none and the export listed none; for a library in the product file, run zaku library script --local"
59747
+ `${saved.dir}/ holds no components: the file publishes none and the export listed none; for a library in the product file, run zaku library script --local`
58442
59748
  );
58443
59749
  finish(1);
58444
59750
  return;
58445
59751
  }
58446
- io.out(`wrote library.json: ${snapshot.components.length} components at version ${snapshot.version}`);
59752
+ io.out(
59753
+ `wrote ${saved.dir}/: ${snapshot.components.length} components at version ${snapshot.version}; ${written.length} component files written, ${unchanged} unchanged, ${removed.length} removed`
59754
+ );
58447
59755
  finish(0);
58448
59756
  } finally {
58449
59757
  await recordRestCalls(cli, io);
@@ -58452,7 +59760,7 @@ var libraryCommand = (program2, io, finish, cli) => {
58452
59760
  };
58453
59761
  var rulesCommand = (program2, io, finish) => {
58454
59762
  const rules = program2.command("rules").description("zaku's rule checks, for a run that reaches Figma through use_figma");
58455
- rules.command("script").description("print the Plugin API code that runs the rules over a page or a node, for use_figma").option("--root <dir>", "the design directory", "docs/design").option("--page <page>", "the page to check, by id or name").option("--node <id>", "the node to check, with everything inside it").option("--after <write.js>", "run this use_figma write first, then the rules, and answer both").action(async (options) => {
59763
+ rules.command("script").description("print the Plugin API code that runs the rules over a page or a node, for use_figma").option("--root <dir>", "the design directory", "docs/design").option("--page <page>", "the page to check, by id or name").option("--node <id>", "the node to check, with everything inside it").option("--after <write.js>", "run this use_figma write first, then the rules, and answer both").option("--key <key>", "the library file the page is in, so its views are held to the branches it holds").action(async (options) => {
58456
59764
  if (options.page === void 0 === (options.node === void 0)) {
58457
59765
  io.err("zaku rules script needs --page or --node, one of them");
58458
59766
  finish(3);
@@ -58460,7 +59768,17 @@ var rulesCommand = (program2, io, finish) => {
58460
59768
  }
58461
59769
  const config2 = await readDesignFile(designPaths(resolve2(io.cwd, options.root)).config, zakuConfigSchema);
58462
59770
  const scope = options.node !== void 0 ? { nodeId: options.node } : { page: options.page };
58463
- const check2 = `await zakuRulesLib.zakuRules(figma, ${JSON.stringify({ scope, copy: resolveCopy(config2.copy) })})`;
59771
+ const graph = designGraph(config2);
59772
+ const file2 = options.key === void 0 ? graph.libraries.length === 1 ? graph.libraries[0] : void 0 : graph.libraries.find((library2) => library2.key === options.key);
59773
+ if (options.key !== void 0 && !file2) {
59774
+ io.err(
59775
+ `zaku rules script: ${options.key} is not a library zaku.yaml lists; name one with --key: ${graph.libraries.map((library2) => library2.key).join(", ")}`
59776
+ );
59777
+ finish(3);
59778
+ return;
59779
+ }
59780
+ const branches = file2?.branches ?? [];
59781
+ const check2 = `await zakuRulesLib.zakuRules(figma, ${JSON.stringify({ scope, copy: resolveCopy(config2.copy), ...branches.length > 1 ? { branches } : {} })})`;
58464
59782
  const library = await runners.rulesLibrary();
58465
59783
  if (options.after === void 0) {
58466
59784
  io.out(`${library}
@@ -58468,11 +59786,11 @@ return ${check2};`);
58468
59786
  finish(0);
58469
59787
  return;
58470
59788
  }
58471
- const write = await readFile5(resolve2(io.cwd, options.after), "utf8");
59789
+ const write2 = await readFile6(resolve2(io.cwd, options.after), "utf8");
58472
59790
  const body = [
58473
59791
  library,
58474
59792
  `const zakuWrite = async () => {
58475
- ${write}
59793
+ ${write2}
58476
59794
  };`,
58477
59795
  "const zakuValue = await zakuWrite();",
58478
59796
  "const value = zakuValue === undefined ? null : zakuValue;",
@@ -58499,6 +59817,7 @@ var COMMANDS = [
58499
59817
  recipeCommand,
58500
59818
  budgetCommand,
58501
59819
  schemaCommand,
59820
+ librariesCommand,
58502
59821
  mcpCommand
58503
59822
  ];
58504
59823
  async function main(argv, io) {