@zeroxsolutions/zaku 0.0.3 → 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 +1274 -209
  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, relative2, 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
- relative2 = parse4(serialize(relative2, options), options);
17358
+ relative3 = parse4(serialize(relative3, options), options);
17359
17359
  }
17360
17360
  options = options || {};
17361
- if (!options.tolerant && relative2.scheme) {
17362
- target.scheme = relative2.scheme;
17363
- target.userinfo = relative2.userinfo;
17364
- target.host = relative2.host;
17365
- target.port = relative2.port;
17366
- target.path = removeDotSegments(relative2.path || "");
17367
- target.query = relative2.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 (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
17370
- target.userinfo = relative2.userinfo;
17371
- target.host = relative2.host;
17372
- target.port = relative2.port;
17373
- target.path = removeDotSegments(relative2.path || "");
17374
- target.query = relative2.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 (!relative2.path) {
17376
+ if (!relative3.path) {
17377
17377
  target.path = base.path;
17378
- if (relative2.query !== void 0) {
17379
- target.query = relative2.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 (relative2.path[0] === "/") {
17385
- target.path = removeDotSegments(relative2.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 = "/" + relative2.path;
17388
+ target.path = "/" + relative3.path;
17389
17389
  } else if (!base.path) {
17390
- target.path = relative2.path;
17390
+ target.path = relative3.path;
17391
17391
  } else {
17392
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.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 = relative2.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 = relative2.fragment;
17404
+ target.fragment = relative3.fragment;
17405
17405
  return target;
17406
17406
  }
17407
17407
  function equal(uriA, uriB, options) {
@@ -20681,7 +20681,7 @@ var import_reflect_metadata3 = __toESM(require_Reflect(), 1);
20681
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 join11, 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
@@ -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
 
@@ -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() {
@@ -44611,12 +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);
44630
+ import { relative as relative2 } from "node:path";
44614
44631
 
44615
44632
  // packages/zaku/dist/lib/domain/copy-defaults.js
44616
44633
  var DEFAULT_COPY = { locales: ["en"], currencies: [] };
44617
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
+
44618
44725
  // packages/zaku/dist/lib/schema/zaku-config.js
44619
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"];
44620
44730
  var DEFAULT_INTERACTIVE = [
44621
44731
  "Button",
44622
44732
  "Button Group",
@@ -44643,7 +44753,11 @@ var targetSchema = external_exports.object({
44643
44753
  family: external_exports.enum(PLATFORM_FAMILIES),
44644
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"),
44645
44755
  /** The frame size every frame of this target is drawn at; the frame check compares it. */
44646
- 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()
44647
44761
  }).strict();
44648
44762
  var DEFAULT_SYSTEM_BARS = {
44649
44763
  web: [],
@@ -44663,7 +44777,33 @@ var budgetSchema = external_exports.object({
44663
44777
  mcpPerRun: external_exports.number().int().positive().default(30),
44664
44778
  reserve: external_exports.number().min(0).max(1).default(0.2)
44665
44779
  }).strict();
44666
- 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
+ }
44667
44807
  var shadcnSystem = external_exports.object({ preset: external_exports.string().min(1) }).strict();
44668
44808
  var tokenFiles = external_exports.array(external_exports.string().min(1)).min(1);
44669
44809
  var resolverInputs = external_exports.record(external_exports.string(), external_exports.string());
@@ -44715,7 +44855,7 @@ var copySchema = external_exports.object({
44715
44855
  locales: external_exports.array(external_exports.string().refine(writtenInAScript, "a BCP 47 language tag, such as en or vi")).min(1).default(["en"]),
44716
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([])
44717
44857
  }).strict();
44718
- var zakuConfigSchema = external_exports.object({
44858
+ var configShape = external_exports.object({
44719
44859
  product: external_exports.string().min(1),
44720
44860
  designSystem: designSystemSchema,
44721
44861
  figma: files.default({}),
@@ -44727,7 +44867,8 @@ var zakuConfigSchema = external_exports.object({
44727
44867
  budget: budgetSchema.default({ mcpPerDay: 200, mcpPerRun: 30, reserve: 0.2 }),
44728
44868
  recipe: recipeSchema.optional(),
44729
44869
  copy: copySchema.default(DEFAULT_COPY)
44730
- }).strict().superRefine((config2, ctx) => {
44870
+ }).strict();
44871
+ var zakuConfigSchema = configShape.superRefine((config2, ctx) => {
44731
44872
  for (const key of ["id", "name"]) {
44732
44873
  const seen = /* @__PURE__ */ new Set();
44733
44874
  config2.targets.forEach((target, index) => {
@@ -44741,7 +44882,112 @@ var zakuConfigSchema = external_exports.object({
44741
44882
  seen.add(target[key]);
44742
44883
  });
44743
44884
  }
44885
+ if (byRole(config2.figma))
44886
+ refineFilesByRole(config2, ctx);
44744
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
+ }
44745
44991
 
44746
44992
  // packages/zaku/dist/lib/repositories/design-file.js
44747
44993
  var import_yaml = __toESM(require_dist(), 1);
@@ -44782,6 +45028,12 @@ async function writeDesignFile(path2, value) {
44782
45028
 
44783
45029
  // packages/zaku/dist/lib/repositories/design-paths.js
44784
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
+ }
44785
45037
  function designPaths(root) {
44786
45038
  return {
44787
45039
  root,
@@ -44825,8 +45077,13 @@ var libraryItemSchema = external_exports.object({
44825
45077
  fontFamily: external_exports.string().nullable(),
44826
45078
  textStyleKey: external_exports.string().nullable(),
44827
45079
  componentKey: external_exports.string().nullable(),
44828
- 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()
44829
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();
44830
45087
  var libraryVariantSchema = external_exports.object({
44831
45088
  key: external_exports.string(),
44832
45089
  name: external_exports.string(),
@@ -44837,6 +45094,8 @@ var libraryComponentSchema = external_exports.object({
44837
45094
  name: external_exports.string(),
44838
45095
  key: external_exports.string(),
44839
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(),
44840
45099
  codePath: external_exports.string().nullable(),
44841
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(),
44842
45101
  variants: external_exports.array(libraryVariantSchema).min(1)
@@ -44856,12 +45115,22 @@ var libraryPartSchema = external_exports.object({
44856
45115
  exportedAt: external_exports.string(),
44857
45116
  components: external_exports.array(libraryComponentSchema),
44858
45117
  textStyles: external_exports.array(textStyleSchema),
44859
- 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()
44860
45126
  }).strict();
44861
45127
  var variablePartSchema = external_exports.object({
44862
45128
  exportedAt: external_exports.string(),
44863
45129
  variables: external_exports.record(external_exports.string(), external_exports.string()),
44864
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(),
44865
45134
  tokens: external_exports.array(external_exports.object({
44866
45135
  combo: external_exports.record(external_exports.string(), external_exports.string()),
44867
45136
  values: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]))
@@ -44872,10 +45141,17 @@ var variablePartSchema = external_exports.object({
44872
45141
  key: external_exports.string(),
44873
45142
  name: external_exports.string(),
44874
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(),
44875
45146
  description: external_exports.string(),
44876
45147
  set: external_exports.boolean()
44877
45148
  }).strict()).optional()
44878
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();
44879
45155
  var librarySnapshotSchema = libraryPartSchema.extend({ source: external_exports.string(), version: external_exports.string() }).strict();
44880
45156
 
44881
45157
  // packages/zaku/dist/lib/domain/library-files.js
@@ -44944,7 +45220,9 @@ function expandItem(stored, values) {
44944
45220
  fontFamily: stored.fontFamily ?? null,
44945
45221
  textStyleKey: stored.textStyleKey ?? null,
44946
45222
  componentKey: stored.componentKey ?? null,
44947
- bindings
45223
+ bindings,
45224
+ ...stored.variableIds ? { variableIds: stored.variableIds } : {},
45225
+ ...stored.effectStyleKey ? { effectStyleKey: stored.effectStyleKey } : {}
44948
45226
  };
44949
45227
  }
44950
45228
  function compactItem(item, values) {
@@ -44960,6 +45238,10 @@ function compactItem(item, values) {
44960
45238
  stored[field] = item[field];
44961
45239
  if (item.glyph)
44962
45240
  stored["glyph"] = item.glyph;
45241
+ if (item.variableIds)
45242
+ stored["variableIds"] = item.variableIds;
45243
+ if (item.effectStyleKey)
45244
+ stored["effectStyleKey"] = item.effectStyleKey;
44963
45245
  const bindings = Object.fromEntries(Object.entries(item.bindings).filter(([name, value]) => value !== null || name === "glyph"));
44964
45246
  if (Object.keys(bindings).length > 0)
44965
45247
  stored["bindings"] = bindings;
@@ -45077,7 +45359,7 @@ async function writeText(path2, text) {
45077
45359
  async function readLibrarySnapshot(paths) {
45078
45360
  const index = await readOptionalDesignFile(paths.libraryIndex, libraryIndexSchema);
45079
45361
  if (!index)
45080
- return readOptionalDesignFile(paths.legacyLibrary, librarySnapshotSchema);
45362
+ return paths.legacyLibrary ? readOptionalDesignFile(paths.legacyLibrary, librarySnapshotSchema) : null;
45081
45363
  const files2 = /* @__PURE__ */ new Map();
45082
45364
  for (const component2 of index.components) {
45083
45365
  const path2 = join3(paths.library, component2.file);
@@ -45118,12 +45400,49 @@ async function writeLibrarySnapshot(paths, snapshot) {
45118
45400
  removed.push(file2);
45119
45401
  }
45120
45402
  await writeText(paths.libraryIndex, formatLibraryFile(index));
45121
- if (await readText(paths.legacyLibrary) !== null) {
45403
+ if (paths.legacyLibrary && await readText(paths.legacyLibrary) !== null) {
45122
45404
  await rm(paths.legacyLibrary);
45123
45405
  removed.push(relative(paths.root, paths.legacyLibrary));
45124
45406
  }
45125
45407
  return { written, unchanged, removed };
45126
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
+ }
45127
45446
 
45128
45447
  // packages/zaku/dist/lib/adapters/figma-rest-library.js
45129
45448
  var DOCUMENTATION_PREFIXES = ["DS/"];
@@ -45197,6 +45516,23 @@ function strokeWeightsOf(node2) {
45197
45516
  const weight = node2.strokeWeight ?? 0;
45198
45517
  return [weight, weight, weight, weight];
45199
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
+ }
45200
45536
  var GLYPH_TYPES = /* @__PURE__ */ new Set(["VECTOR", "BOOLEAN_OPERATION", "LINE", "ELLIPSE", "POLYGON", "STAR"]);
45201
45537
  function glyphOf(instance, at) {
45202
45538
  let text = false;
@@ -45236,6 +45572,9 @@ function itemsOf(root, at) {
45236
45572
  const textStyle = node2.styles?.["text"];
45237
45573
  const glyph = path2 !== "" && node2.type === "INSTANCE" ? glyphOf(node2, at) : null;
45238
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;
45239
45578
  out.push({
45240
45579
  nodeId: node2.id,
45241
45580
  path: path2,
@@ -45260,7 +45599,9 @@ function itemsOf(root, at) {
45260
45599
  textColor: isText ? fill.binding : null,
45261
45600
  radius: radiusName,
45262
45601
  ...glyph?.value ? { glyph: glyph.binding } : {}
45263
- }
45602
+ },
45603
+ ...variableIds.length > 0 ? { variableIds } : {},
45604
+ ...effectStyleKey ? { effectStyleKey } : {}
45264
45605
  });
45265
45606
  if (path2 === "" || node2.type !== "INSTANCE") {
45266
45607
  for (const child of node2.children ?? [])
@@ -45279,28 +45620,55 @@ function asPublished(component2) {
45279
45620
  containing_frame: { pageName: component2.page }
45280
45621
  };
45281
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
+ }
45282
45638
  async function readLibrary(options) {
45283
- 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]));
45284
45643
  const listed = part.components ? {
45285
45644
  sets: part.components.filter((component2) => component2.set).map(asPublished),
45286
45645
  components: part.components.filter((component2) => !component2.set).map(asPublished)
45287
- } : await rest.published(fileKey2);
45646
+ } : await rest.published(fileKey3);
45288
45647
  const published = [
45289
45648
  ...listed.sets,
45290
45649
  ...listed.components.filter((component2) => !component2.containing_frame?.containingComponentSet)
45291
45650
  ].filter((meta3) => !DOCUMENTATION_PREFIXES.some((prefix) => meta3.name.startsWith(prefix)));
45292
- const file2 = await rest.file(fileKey2, 1);
45293
- 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: {} };
45294
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);
45295
45660
  for (const meta3 of published) {
45296
45661
  const entry = response.nodes[meta3.node_id];
45297
45662
  if (!entry)
45298
45663
  continue;
45299
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;
45300
45667
  components.push({
45301
45668
  name: meta3.name,
45302
45669
  key: meta3.key,
45303
45670
  page: (meta3.containing_frame?.pageName ?? "").replace(/^\W+\s*/, ""),
45671
+ ...branch ? { branch } : {},
45304
45672
  ...descriptionMap(meta3.description),
45305
45673
  variants: variantNodes.map((variant) => ({
45306
45674
  key: entry.components[variant.id]?.key ?? "",
@@ -45314,11 +45682,16 @@ async function readLibrary(options) {
45314
45682
  });
45315
45683
  }
45316
45684
  return {
45317
- source: fileKey2,
45685
+ source: fileKey3,
45318
45686
  version: file2.version,
45319
45687
  exportedAt: part.exportedAt,
45320
45688
  components,
45321
45689
  textStyles: part.textStyles,
45690
+ ...part.variableKeys ? {
45691
+ variableKeys: part.variableKeys,
45692
+ effectStyles: part.effectStyles ?? [],
45693
+ localComponents: [...localComponents].sort()
45694
+ } : {},
45322
45695
  tokens: part.tokens.map((token) => ({
45323
45696
  combo: token.combo,
45324
45697
  values: Object.fromEntries(Object.entries(token.values).map(([name, value]) => [
@@ -45328,16 +45701,57 @@ async function readLibrary(options) {
45328
45701
  }))
45329
45702
  };
45330
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
+ }
45331
45712
 
45332
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
+ }
45333
45734
  async function saveLibrary(options) {
45334
45735
  const paths = designPaths(options.root);
45335
45736
  const config2 = await readDesignFile(paths.config, zakuConfigSchema);
45336
- const fileKey2 = config2.figma.library;
45337
- if (fileKey2 === void 0)
45338
- throw new FigmaFileKeyMissing("figma.library");
45339
- const snapshot = librarySnapshotSchema.parse(await readLibrary({ rest: options.rest, fileKey: fileKey2, part: options.part }));
45340
- return { snapshot, ...await writeLibrarySnapshot(paths, 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
+ };
45341
45755
  }
45342
45756
  var SaveLibraryHandler = class SaveLibraryHandler2 {
45343
45757
  rest;
@@ -45346,7 +45760,12 @@ var SaveLibraryHandler = class SaveLibraryHandler2 {
45346
45760
  this.rest = rest;
45347
45761
  }
45348
45762
  handle(command) {
45349
- 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
+ });
45350
45769
  }
45351
45770
  };
45352
45771
  SaveLibraryHandler = __decorate2([
@@ -45356,9 +45775,10 @@ SaveLibraryHandler = __decorate2([
45356
45775
 
45357
45776
  // packages/zaku/dist/lib/handlers/write-outline.js
45358
45777
  var import_tsyringe4 = __toESM(require_cjs(), 1);
45778
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
45359
45779
  var import_yaml2 = __toESM(require_dist(), 1);
45360
45780
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
45361
- import { dirname as dirname2 } from "node:path";
45781
+ import { dirname as dirname2, join as join6 } from "node:path";
45362
45782
 
45363
45783
  // packages/zaku/dist/lib/domain/documentation-measures.js
45364
45784
  var COVER = {
@@ -45866,6 +46286,9 @@ function indexTitles(maps) {
45866
46286
  return { titles, duplicates };
45867
46287
  }
45868
46288
 
46289
+ // packages/zaku/dist/lib/adapters/figma-rest-outline.js
46290
+ import { createHash as createHash2 } from "node:crypto";
46291
+
45869
46292
  // packages/zaku/dist/lib/domain/layer-names.js
45870
46293
  var DEFAULT_NAMES = {
45871
46294
  FRAME: ["Frame"],
@@ -45934,7 +46357,7 @@ function outlineFrame(frame, where, ctx) {
45934
46357
  const images2 = [];
45935
46358
  const defaultNames = [];
45936
46359
  const links = [];
45937
- const relative2 = (node2) => {
46360
+ const relative3 = (node2) => {
45938
46361
  const box = node2.absoluteBoundingBox;
45939
46362
  return box ? { x: box.x - origin.x, y: box.y - origin.y, width: box.width, height: box.height } : null;
45940
46363
  };
@@ -45963,7 +46386,7 @@ function outlineFrame(frame, where, ctx) {
45963
46386
  componentKey: ctx.components[child.componentId ?? ""]?.key ?? "",
45964
46387
  component: component2,
45965
46388
  interactive: ctx.interactive.has(component2),
45966
- bounds: relative2(child)
46389
+ bounds: relative3(child)
45967
46390
  });
45968
46391
  } else if (child.type === "TEXT") {
45969
46392
  instance.texts.push({
@@ -46004,7 +46427,7 @@ function outlineFrame(frame, where, ctx) {
46004
46427
  texts: [],
46005
46428
  interactive: ctx.interactive.has(component2),
46006
46429
  local: meta3?.remote === false,
46007
- bounds: relative2(node2),
46430
+ bounds: relative3(node2),
46008
46431
  sizing: {
46009
46432
  horizontal: node2.layoutSizingHorizontal ?? "FIXED",
46010
46433
  vertical: node2.layoutSizingVertical ?? "FIXED"
@@ -46053,6 +46476,17 @@ function outlineFrame(frame, where, ctx) {
46053
46476
  links.push(...linksOf(frame));
46054
46477
  for (const child of shown(frame))
46055
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
+ };
46056
46490
  return {
46057
46491
  nodeId: frame.id,
46058
46492
  name: frame.name,
@@ -46066,7 +46500,8 @@ function outlineFrame(frame, where, ctx) {
46066
46500
  raw,
46067
46501
  images: images2,
46068
46502
  defaultNames,
46069
- links
46503
+ links,
46504
+ digest: createHash2("sha256").update(JSON.stringify(drawn)).digest("hex").slice(0, 16)
46070
46505
  };
46071
46506
  }
46072
46507
 
@@ -46132,36 +46567,78 @@ var frameOutlineSchema = external_exports.object({
46132
46567
  raw: external_exports.array(rawOutlineSchema),
46133
46568
  images: external_exports.array(imageOutlineSchema),
46134
46569
  defaultNames: external_exports.array(external_exports.object({ nodeId: external_exports.string(), name: external_exports.string() }).strict()),
46135
- 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())
46136
46592
  }).strict();
46137
- 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({
46138
46604
  feature: external_exports.string(),
46139
46605
  screen: external_exports.string(),
46140
46606
  /** The Figma file key the frames were read from. */
46141
46607
  source: external_exports.string(),
46142
46608
  fileVersion: external_exports.string(),
46143
46609
  mapDigest: external_exports.string(),
46144
- 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(),
46145
46614
  frames: external_exports.array(frameOutlineSchema)
46146
- }).strict();
46615
+ }).strict());
46147
46616
  var unmappedSchema = external_exports.object({
46148
46617
  fileVersion: external_exports.string(),
46149
- 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()
46150
46621
  }).strict();
46151
46622
 
46152
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
+ }
46153
46629
  function outlinePath(dir, feature, screen) {
46154
46630
  return join5(dir, feature, `${screen}.yaml`);
46155
46631
  }
46156
46632
  async function loadOutlines(dir) {
46157
46633
  const outlines = [];
46634
+ const flows = [];
46158
46635
  const errors = [];
46159
46636
  let features;
46160
46637
  try {
46161
- features = (await readdir3(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();
46162
46639
  } catch (error62) {
46163
46640
  if (error62.code === "ENOENT")
46164
- return { outlines, unmapped: null, errors };
46641
+ return { outlines, flows, unmapped: null, errors };
46165
46642
  throw error62;
46166
46643
  }
46167
46644
  for (const feature of features) {
@@ -46177,6 +46654,24 @@ async function loadOutlines(dir) {
46177
46654
  }
46178
46655
  }
46179
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();
46664
+ for (const file2 of files2) {
46665
+ try {
46666
+ flows.push(await readDesignFile(join5(dir, FLOWS_DIR, source, file2), flowOutlineSchema));
46667
+ } catch (error62) {
46668
+ if (error62 instanceof DesignFileInvalid)
46669
+ errors.push(error62);
46670
+ else
46671
+ throw error62;
46672
+ }
46673
+ }
46674
+ }
46180
46675
  let unmapped = null;
46181
46676
  try {
46182
46677
  unmapped = await readOptionalDesignFile(join5(dir, "unmapped.yaml"), unmappedSchema);
@@ -46186,7 +46681,7 @@ async function loadOutlines(dir) {
46186
46681
  else
46187
46682
  throw error62;
46188
46683
  }
46189
- return { outlines, unmapped, errors };
46684
+ return { outlines, flows, unmapped, errors };
46190
46685
  }
46191
46686
 
46192
46687
  // packages/zaku/dist/lib/handlers/write-outline.js
@@ -46223,95 +46718,202 @@ function resolveFrame(frame, parsed, maps) {
46223
46718
  return candidates.find((ref) => ref.feature === pageFeature);
46224
46719
  return candidates.length === 1 ? candidates[0] : void 0;
46225
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
+ }
46226
46725
  function looksLikeScreenFrame(name) {
46227
46726
  return name.includes("/");
46228
46727
  }
46229
- function holdsScreens(page) {
46728
+ function holdsScreens(page, prototypes = /* @__PURE__ */ new Set()) {
46230
46729
  if (page === COVER.page || page === DOCUMENTATION_COMPONENTS_PAGE || page === DIRECTIONS_PAGE)
46231
46730
  return false;
46731
+ if (prototypes.has(page))
46732
+ return false;
46232
46733
  return !page.startsWith("\u2756 ") && !page.includes(" / ") && !/^-+$/.test(page);
46233
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
+ }
46234
46761
  async function runOutline(options) {
46235
46762
  const paths = designPaths(options.root);
46236
46763
  const config2 = await readDesignFile(paths.config, zakuConfigSchema);
46237
- const fileKey2 = config2.figma.product;
46238
- if (fileKey2 === void 0)
46239
- 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");
46240
46769
  const { maps, errors } = await loadMaps(paths.maps);
46241
46770
  if (errors[0])
46242
46771
  throw errors[0];
46243
46772
  const digests = new Map(maps.map((loaded) => [loaded.map.feature, loaded.digest]));
46244
- const library = await readLibrarySnapshot(paths);
46773
+ const { libraries } = await readDesignSnapshots(paths, config2);
46774
+ const libraryVersions = Object.fromEntries(Object.entries(libraries).map(([key, library]) => [key, library.version]));
46245
46775
  const existing = await loadOutlines(paths.outline);
46246
- const file2 = await options.rest.file(fileKey2, INDEX_DEPTH);
46247
- 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
+ };
46248
46789
  const resolved = /* @__PURE__ */ new Map();
46249
- const unmapped = { fileVersion: file2.version, frames: [] };
46250
- for (const frame of listed.filter((listed2) => holdsScreens(listed2.page))) {
46251
- const parsed = parseFrameName(frame.name, config2.targets);
46252
- if (!parsed) {
46253
- 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
46254
46809
  unmapped.frames.push({ nodeId: frame.id, name: frame.name });
46255
- continue;
46256
46810
  }
46257
- const ref = resolveFrame(frame, parsed, maps);
46258
- if (ref)
46259
- resolved.set(frame.id, { frame, parsed, ref });
46260
- else
46261
- 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);
46262
46821
  }
46822
+ unmapped.fileVersion = Object.values(versions).join(",");
46823
+ if (!legacy)
46824
+ unmapped.components = components;
46263
46825
  const outlined = new Set(existing.outlines.flatMap((outline) => outline.frames.map((frame) => frame.nodeId)));
46264
46826
  const drawnScreens = new Set([...resolved.values()].map(({ ref }) => `${ref.feature}/${ref.id}`));
46265
- 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}`));
46266
46828
  if (options.frames === null && existing.outlines.length > 0 && current) {
46267
46829
  return { skipped: true, read: 0, written: [], unmapped: unmapped.frames.length };
46268
46830
  }
46269
46831
  const ids = options.frames ?? [...resolved.keys()];
46270
- const response = ids.length > 0 ? await options.rest.nodes(fileKey2, ids) : { version: file2.version, nodes: {} };
46271
46832
  const byScreen = /* @__PURE__ */ new Map();
46272
46833
  if (options.frames !== null) {
46273
46834
  for (const outline of existing.outlines)
46274
46835
  byScreen.set(`${outline.feature}/${outline.screen}`, outline);
46275
46836
  }
46276
46837
  const interactive = new Set(config2.interactive);
46277
- const resized = new Set(Object.values(response.nodes).flatMap((entry) => entry ? resizedLocalComponents(entry.document, entry.components) : []));
46278
- const sizes = /* @__PURE__ */ new Map();
46279
- if (resized.size > 0) {
46280
- const read = await options.rest.nodes(fileKey2, [...resized]);
46281
- for (const [id2, entry] of Object.entries(read.nodes)) {
46282
- const box = entry?.document.absoluteBoundingBox;
46283
- if (box)
46284
- sizes.set(id2, { width: box.width, height: box.height });
46285
- }
46286
- }
46287
- for (const [id2, entry] of Object.entries(response.nodes)) {
46288
- const found = resolved.get(id2);
46289
- 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)
46290
46841
  continue;
46291
- const { frame: place, parsed, ref } = found;
46292
- const key = `${ref.feature}/${ref.id}`;
46293
- const outline = byScreen.get(key) ?? {
46294
- feature: ref.feature,
46295
- screen: ref.id,
46296
- source: fileKey2,
46297
- fileVersion: file2.version,
46298
- mapDigest: "",
46299
- libraryVersion: null,
46300
- frames: []
46301
- };
46302
- const frame = outlineFrame(entry.document, {
46303
- target: parsed.target.id,
46304
- state: parsed.state,
46305
- containers: place.containers,
46306
- start: place.start
46307
- }, { ...entry, interactive, sizes });
46308
- outline.frames = [...outline.frames.filter((kept) => kept.nodeId !== id2), frame].sort((a, b) => a.name.localeCompare(b.name));
46309
- outline.fileVersion = file2.version;
46310
- outline.mapDigest = digests.get(ref.feature) ?? "";
46311
- outline.libraryVersion = library?.version ?? null;
46312
- 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
+ }
46313
46885
  }
46314
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
+ }
46315
46917
  for (const outline of existing.outlines) {
46316
46918
  if (!byScreen.has(`${outline.feature}/${outline.screen}`))
46317
46919
  await rm2(outlinePath(paths.outline, outline.feature, outline.screen), { force: true });
@@ -46441,13 +47043,13 @@ RecipeRepository = __decorate2([
46441
47043
  // packages/zaku/dist/lib/repositories/json-schema-files.js
46442
47044
  var import_tsyringe10 = __toESM(require_cjs(), 1);
46443
47045
  import { mkdir as mkdir3 } from "node:fs/promises";
46444
- import { join as join6 } from "node:path";
47046
+ import { join as join7 } from "node:path";
46445
47047
  var JsonSchemaFiles = class JsonSchemaFiles2 {
46446
47048
  async write(dir, schemas) {
46447
47049
  await mkdir3(dir, { recursive: true });
46448
47050
  const written = [];
46449
47051
  for (const [name, schema] of Object.entries(schemas)) {
46450
- const path2 = join6(dir, name);
47052
+ const path2 = join7(dir, name);
46451
47053
  await writeDesignFile(path2, schema);
46452
47054
  written.push(path2);
46453
47055
  }
@@ -46475,10 +47077,10 @@ SourceFiles = __decorate2([
46475
47077
 
46476
47078
  // packages/zaku/dist/lib/repositories/budget-ledger.js
46477
47079
  import { mkdir as mkdir4, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
46478
- import { dirname as dirname3, join as join7 } from "node:path";
47080
+ import { dirname as dirname3, join as join8 } from "node:path";
46479
47081
  function ledgerPath(env, platform, home) {
46480
- const cache = env["XDG_CACHE_HOME"] ?? (platform === "darwin" ? join7(home, "Library", "Caches") : join7(home, ".cache"));
46481
- return join7(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");
46482
47084
  }
46483
47085
  function today(now) {
46484
47086
  return now.toISOString().slice(0, 10);
@@ -46547,6 +47149,30 @@ var pairingSchema = external_exports.object({
46547
47149
  }).strict();
46548
47150
  var pairingsFileSchema = external_exports.object({ pairings: external_exports.array(pairingSchema.extend({ hash: external_exports.string() }).strict()) }).strict();
46549
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
+
46550
47176
  // packages/zaku/dist/lib/adapters/figma-rest.js
46551
47177
  var FigmaRateLimited = class extends Error {
46552
47178
  url;
@@ -46591,26 +47217,30 @@ function createFigmaRest(options) {
46591
47217
  throw new FigmaRateLimited(url2);
46592
47218
  }
46593
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
+ }
46594
47224
  return {
46595
- file: (fileKey2, depth) => call(`${BASE}/files/${fileKey2}?depth=${depth}`),
46596
- async nodes(fileKey2, ids) {
47225
+ file: (fileKey3, depth) => call(`${BASE}/files/${fileKey3}?depth=${depth}`),
47226
+ async nodes(fileKey3, ids) {
46597
47227
  const merged = { version: "", nodes: {} };
46598
47228
  for (const chunk of chunks(ids)) {
46599
- 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(","))}`);
46600
47230
  merged.version = part.version;
46601
47231
  Object.assign(merged.nodes, part.nodes);
46602
47232
  }
46603
47233
  return merged;
46604
47234
  },
46605
- async published(fileKey2) {
46606
- const sets = await call(`${BASE}/files/${fileKey2}/component_sets`);
46607
- const components = await call(`${BASE}/files/${fileKey2}/components`);
46608
- 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) };
46609
47238
  },
46610
- async images(fileKey2, ids) {
47239
+ components,
47240
+ async images(fileKey3, ids) {
46611
47241
  const merged = {};
46612
47242
  for (const chunk of chunks(ids)) {
46613
- 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`);
46614
47244
  Object.assign(merged, part.images);
46615
47245
  }
46616
47246
  return merged;
@@ -46620,17 +47250,40 @@ function createFigmaRest(options) {
46620
47250
 
46621
47251
  // packages/zaku/dist/lib/adapters/figma-variables-script.js
46622
47252
  async function exportVariables(figma, options) {
46623
- 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
+ }
46624
47273
  const semantic = collections.find((collection) => collection.name === "semantic");
46625
47274
  if (!semantic)
46626
47275
  throw new Error("the library has no semantic variable collection");
46627
47276
  const hex3 = (value) => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, "0");
46628
47277
  const variables = {};
47278
+ const variableKeys = {};
46629
47279
  for (const collection of collections) {
46630
47280
  for (const id2 of collection.variableIds) {
46631
47281
  const variable = await figma.variables.getVariableByIdAsync(id2);
46632
- if (variable)
46633
- 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 };
46634
47287
  }
46635
47288
  }
46636
47289
  const holder = figma.createFrame();
@@ -46652,6 +47305,8 @@ async function exportVariables(figma, options) {
46652
47305
  const values = {};
46653
47306
  for (const id2 of semantic.variableIds) {
46654
47307
  const variable = await figma.variables.getVariableByIdAsync(id2);
47308
+ if (!variable)
47309
+ continue;
46655
47310
  const resolved = variable.resolveForConsumer(holder).value;
46656
47311
  values[variable.name] = typeof resolved === "object" && resolved !== null ? `#${hex3(resolved.r)}${hex3(resolved.g)}${hex3(resolved.b)}${hex3(resolved.a ?? 1)}` : resolved;
46657
47312
  }
@@ -46671,21 +47326,31 @@ async function exportVariables(figma, options) {
46671
47326
  for (const node2 of page.findAllWithCriteria({ types: ["COMPONENT_SET", "COMPONENT"] })) {
46672
47327
  if (node2.type === "COMPONENT" && node2.parent?.type === "COMPONENT_SET")
46673
47328
  continue;
47329
+ let view = node2;
47330
+ while (view.parent && view.parent.type !== "PAGE")
47331
+ view = view.parent;
46674
47332
  components.push({
46675
47333
  id: node2.id,
46676
47334
  key: node2.key,
46677
47335
  name: node2.name,
46678
47336
  page: page.name,
47337
+ ...view !== node2 && view.parent ? { frame: view.name } : {},
46679
47338
  description: node2.description ?? "",
46680
47339
  set: node2.type === "COMPONENT_SET"
46681
47340
  });
46682
47341
  }
46683
47342
  }
46684
47343
  }
47344
+ const effectStyles = (await figma.getLocalEffectStylesAsync()).map((style) => ({
47345
+ key: style.key,
47346
+ name: style.name
47347
+ }));
46685
47348
  return {
46686
47349
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
46687
47350
  variables,
47351
+ variableKeys,
46688
47352
  textStyles,
47353
+ effectStyles,
46689
47354
  tokens,
46690
47355
  ...options.local ? { components } : {}
46691
47356
  };
@@ -47277,15 +47942,15 @@ async function startPluginBridge(options = {}) {
47277
47942
  });
47278
47943
  });
47279
47944
  });
47280
- function request(fileKey2, message, timeoutMs) {
47945
+ function request(fileKey3, message, timeoutMs) {
47281
47946
  if (port === null)
47282
47947
  return Promise.resolve({
47283
47948
  ok: false,
47284
47949
  error: `no port from ${ports[0]} to ${ports[ports.length - 1]} was free, so zaku mcp could not listen for the plugin`
47285
47950
  });
47286
- const socket = holderOf(fileKey2);
47951
+ const socket = holderOf(fileKey3);
47287
47952
  if (!socket)
47288
- 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` });
47289
47954
  const id2 = `c${++next}`;
47290
47955
  return new Promise((resolve3) => {
47291
47956
  const timer = setTimeout(() => settle2(id2, { ok: false, error: `no answer from the zaku panel in ${timeoutMs / 1e3} s; ${PARTIAL}` }), timeoutMs);
@@ -47295,8 +47960,8 @@ async function startPluginBridge(options = {}) {
47295
47960
  }
47296
47961
  return {
47297
47962
  port,
47298
- run: (fileKey2, call, timeoutMs) => request(fileKey2, { type: "run", ...call }, timeoutMs),
47299
- 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),
47300
47965
  close: () => new Promise((done) => {
47301
47966
  if (!server || !bound)
47302
47967
  return done();
@@ -47307,12 +47972,66 @@ async function startPluginBridge(options = {}) {
47307
47972
  };
47308
47973
  }
47309
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
+
47310
48027
  // packages/zaku/dist/lib/domain/checks/binding.js
47311
48028
  var PAINTS = ["fill", "stroke", "textColor", "glyph"];
47312
48029
  var SEMANTIC = /^(color|mode|radius)\//;
47313
- var binding = ({ library }) => {
47314
- if (!library)
47315
- 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;
47316
48035
  const findings = [];
47317
48036
  for (const component2 of library.components) {
47318
48037
  for (const variant of component2.variants) {
@@ -47678,8 +48397,9 @@ var frameCheck = ({ config: config2, outlines }) => {
47678
48397
  }
47679
48398
  return { findings };
47680
48399
  };
47681
- var systemBars = ({ config: config2, outlines }) => {
48400
+ var systemBars = ({ config: config2, outlines, kits }) => {
47682
48401
  const families2 = new Map(config2.targets.map((target) => [target.id, target.family]));
48402
+ const graph = designGraph(config2);
47683
48403
  const bars = { ...DEFAULT_SYSTEM_BARS, ...config2.systemBars };
47684
48404
  const findings = [];
47685
48405
  for (const outline of outlines) {
@@ -47687,7 +48407,10 @@ var systemBars = ({ config: config2, outlines }) => {
47687
48407
  const family = families2.get(frame.target);
47688
48408
  if (!family)
47689
48409
  continue;
47690
- 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));
47691
48414
  for (const names of bars[family]) {
47692
48415
  if (!names.some((name) => held.has(name))) {
47693
48416
  findings.push({
@@ -47696,7 +48419,7 @@ var systemBars = ({ config: config2, outlines }) => {
47696
48419
  screen: outline.screen,
47697
48420
  frame: frame.name,
47698
48421
  nodeId: frame.nodeId,
47699
- message: `no ${names.join(" or ")} instance on a ${family} frame`
48422
+ message: `no ${names.join(" or ")} instance${from} on a ${family} frame`
47700
48423
  });
47701
48424
  }
47702
48425
  }
@@ -47710,6 +48433,18 @@ var placement = ({ maps, outlines }) => {
47710
48433
  for (const outline of outlines) {
47711
48434
  const title = titles.get(`${outline.feature}/${outline.screen}`);
47712
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
+ }
47713
48448
  const expected = [title, frame.state];
47714
48449
  if (frame.containers.length !== 2 || frame.containers[0] !== expected[0] || frame.containers[1] !== expected[1]) {
47715
48450
  findings.push({
@@ -47743,8 +48478,19 @@ var naming = ({ outlines }) => {
47743
48478
  }
47744
48479
  return { findings };
47745
48480
  };
47746
- var component = ({ outlines }) => {
48481
+ var component = ({ config: config2, outlines, productComponents }) => {
47747
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
+ }
47748
48494
  for (const outline of outlines) {
47749
48495
  const seen = /* @__PURE__ */ new Map();
47750
48496
  for (const frame of outline.frames) {
@@ -47776,13 +48522,16 @@ var component = ({ outlines }) => {
47776
48522
  };
47777
48523
 
47778
48524
  // packages/zaku/dist/lib/domain/checks/font.js
47779
- var font = ({ outlines, library }) => {
47780
- if (!library)
47781
- return { notRun: "library.json is missing" };
47782
- 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();
47783
48530
  const findings = [];
47784
- for (const outline of outlines) {
48531
+ for (const outline of input2.outlines) {
47785
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);
47786
48535
  const where = { feature: outline.feature, screen: outline.screen, frame: frame.name };
47787
48536
  for (const text of frame.texts) {
47788
48537
  if (text.styleKey === null) {
@@ -47792,7 +48541,7 @@ var font = ({ outlines, library }) => {
47792
48541
  nodeId: text.nodeId,
47793
48542
  message: "text outside a component uses no library text style"
47794
48543
  });
47795
- } else if (!keys.has(text.styleKey)) {
48544
+ } else if (!styles.has(text.styleKey)) {
47796
48545
  findings.push({
47797
48546
  check: "font",
47798
48547
  ...where,
@@ -47807,8 +48556,9 @@ var font = ({ outlines, library }) => {
47807
48556
  };
47808
48557
 
47809
48558
  // packages/zaku/dist/lib/domain/checks/freshness.js
47810
- var freshness = ({ maps, outlines, library }) => {
48559
+ var freshness = ({ config: config2, maps, outlines, flows, libraries }) => {
47811
48560
  const digests = new Map(maps.map((loaded) => [loaded.map.feature, loaded.digest]));
48561
+ const graph = designGraph(config2);
47812
48562
  const findings = [];
47813
48563
  for (const outline of outlines) {
47814
48564
  const where = { feature: outline.feature, screen: outline.screen };
@@ -47820,11 +48570,35 @@ var freshness = ({ maps, outlines, library }) => {
47820
48570
  message: "the map changed after this outline was read; run zaku outline"
47821
48571
  });
47822
48572
  }
47823
- 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;
47824
48577
  findings.push({
47825
48578
  check: "freshness",
47826
48579
  ...where,
47827
- 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`
47828
48602
  });
47829
48603
  }
47830
48604
  }
@@ -47832,13 +48606,43 @@ var freshness = ({ maps, outlines, library }) => {
47832
48606
  };
47833
48607
 
47834
48608
  // packages/zaku/dist/lib/domain/checks/library.js
47835
- var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47836
- if (!library)
47837
- 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;
47838
48615
  const bars = systemBarNames(config2);
47839
48616
  const owned = outlines.flatMap((outline) => outline.frames.flatMap((frame) => frame.instances.filter((instance) => instance.local).map((instance) => instance.component)));
47840
48617
  const names = /* @__PURE__ */ new Set([...library.components.map((component2) => component2.name), ...owned]);
47841
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
+ };
47842
48646
  const findings = [];
47843
48647
  for (const { map: map2 } of maps) {
47844
48648
  for (const [id2, screen] of Object.entries(map2.screens)) {
@@ -47858,8 +48662,22 @@ var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47858
48662
  for (const outline of outlines) {
47859
48663
  for (const frame of outline.frames) {
47860
48664
  const where = { feature: outline.feature, screen: outline.screen, frame: frame.name };
48665
+ const target = config2.targets.find((candidate) => candidate.id === frame.target);
47861
48666
  for (const instance of frame.instances) {
47862
- 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) {
47863
48681
  findings.push({
47864
48682
  check: "library",
47865
48683
  ...where,
@@ -47879,6 +48697,82 @@ var libraryCheck = ({ config: config2, maps, outlines, library }) => {
47879
48697
  }
47880
48698
  }
47881
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] };
47882
48776
  };
47883
48777
 
47884
48778
  // packages/zaku/dist/lib/domain/checks/navigation.js
@@ -47949,7 +48843,11 @@ var ALLOWED_OVERRIDES = /* @__PURE__ */ new Set([
47949
48843
  "name",
47950
48844
  "componentProperties",
47951
48845
  "componentPropertyReferences",
47952
- "exposedInstances"
48846
+ "exposedInstances",
48847
+ "reactions",
48848
+ "transitionNodeID",
48849
+ "transitionDuration",
48850
+ "transitionEasing"
47953
48851
  ]);
47954
48852
  var SIZING_MODES = /* @__PURE__ */ new Set(["primaryAxisSizingMode", "counterAxisSizingMode"]);
47955
48853
  function componentSized(field, size2, main2) {
@@ -47963,9 +48861,12 @@ function stretches(sizing) {
47963
48861
  function insideSwap(instance, nodeId) {
47964
48862
  return instance.nested.some((nested) => nodeId.startsWith(`${nested.nodeId};`) && instance.swaps.includes(nested.componentKey));
47965
48863
  }
47966
- var overrides = ({ outlines, library }) => {
47967
- if (!library)
47968
- 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;
47969
48870
  const nestedOf = /* @__PURE__ */ new Map();
47970
48871
  const componentOf = /* @__PURE__ */ new Map();
47971
48872
  for (const component2 of library.components) {
@@ -48026,18 +48927,58 @@ var overrides = ({ outlines, library }) => {
48026
48927
  return { findings };
48027
48928
  };
48028
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
+
48029
48953
  // packages/zaku/dist/lib/domain/checks/prototype.js
48030
- var prototype = ({ maps, outlines }) => {
48031
- if (outlines.length === 0)
48032
- 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 [];
48033
48974
  const screenOfFrame = /* @__PURE__ */ new Map();
48034
- const outlineOf = new Map(outlines.map((outline) => [`${outline.feature}/${outline.screen}`, outline]));
48035
- for (const outline of outlines) {
48975
+ const outlineOf = new Map(screens.map((outline) => [`${outline.feature}/${outline.screen}`, outline]));
48976
+ for (const outline of screens) {
48036
48977
  for (const frame of outline.frames)
48037
48978
  screenOfFrame.set(frame.nodeId, `${outline.feature}/${outline.screen}`);
48038
48979
  }
48039
48980
  const findings = [];
48040
- for (const { map: map2 } of maps) {
48981
+ for (const { map: map2 } of input2.maps) {
48041
48982
  for (const [id2, screen] of Object.entries(map2.screens)) {
48042
48983
  const outline = outlineOf.get(`${map2.feature}/${id2}`);
48043
48984
  if (!outline)
@@ -48074,8 +49015,64 @@ var prototype = ({ maps, outlines }) => {
48074
49015
  }
48075
49016
  }
48076
49017
  }
48077
- return { findings };
48078
- };
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
+ });
48079
49076
 
48080
49077
  // packages/zaku/dist/lib/domain/checks/recipe.js
48081
49078
  function propsLabel(props) {
@@ -48155,9 +49152,14 @@ function comparePart(at, part) {
48155
49152
  });
48156
49153
  }
48157
49154
  }
48158
- var recipe = ({ config: config2, library, recipe: doc }) => {
48159
- if (!doc || !library)
48160
- 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;
48161
49163
  const findings = [];
48162
49164
  for (const code of doc.components) {
48163
49165
  const component2 = library.components.find((candidate) => candidate.name === code.name);
@@ -48248,9 +49250,12 @@ var MIN_TARGET = {
48248
49250
  function controlsOf(frame) {
48249
49251
  return frame.instances.flatMap((instance) => [instance, ...instance.nested].flatMap((control) => control.interactive && control.bounds ? [{ nodeId: control.nodeId, componentKey: control.componentKey, bounds: control.bounds }] : []));
48250
49252
  }
48251
- var targetSize = ({ config: config2, outlines, library }) => {
48252
- if (!library)
48253
- 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;
48254
49259
  const presetHeight = /* @__PURE__ */ new Map();
48255
49260
  for (const component2 of library.components) {
48256
49261
  for (const variant of component2.variants) {
@@ -48332,9 +49337,14 @@ function rgbaOf(token) {
48332
49337
  const [r, g, b] = token.$value.components;
48333
49338
  return { r, g, b, a: token.$value.alpha };
48334
49339
  }
48335
- var tokensCheck = ({ config: config2, library, tokens }) => {
48336
- if (!library || !tokens)
48337
- 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;
48338
49348
  const sets = tokenSets(tokens);
48339
49349
  const findings = [];
48340
49350
  for (const [scheme, set2] of [
@@ -48638,13 +49648,25 @@ async function loadDesign(root) {
48638
49648
  message: `two screens share the title ${title}; a frame name could not tell them apart`
48639
49649
  });
48640
49650
  }
48641
- const { outlines, unmapped, errors: outlineErrors } = await loadOutlines(paths.outline);
49651
+ const { outlines, flows, unmapped, errors: outlineErrors } = await loadOutlines(paths.outline);
48642
49652
  findings.push(...outlineErrors.map(schemaFinding));
48643
- const library = await optional3(() => readLibrarySnapshot(paths), findings);
49653
+ const { libraries, kits, errors: snapshotErrors } = await readDesignSnapshots(paths, config2);
49654
+ findings.push(...snapshotErrors.map(schemaFinding));
48644
49655
  const tokens = await optional3(() => readOptionalDesignFile(paths.tokens, tokenDocumentSchema), findings);
48645
49656
  const recipe2 = await optional3(() => readOptionalDesignFile(paths.recipe, recipeDocumentSchema), findings);
48646
49657
  return {
48647
- 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
+ },
48648
49670
  findings
48649
49671
  };
48650
49672
  }
@@ -48679,6 +49701,7 @@ function refusalOf(error62) {
48679
49701
  }
48680
49702
  if (error62 instanceof FigmaFileKeyMissing) return { code: 3, line: error62.message };
48681
49703
  if (error62 instanceof FigmaTokenMissing) return { code: 3, line: error62.message };
49704
+ if (error62 instanceof LibraryKeyMissing) return { code: 3, line: error62.message };
48682
49705
  if (error62 instanceof RecipePageFailed) return { code: 2, line: `zaku recipe: ${error62.message}` };
48683
49706
  return null;
48684
49707
  }
@@ -58368,11 +59391,11 @@ var StdioServerTransport = class {
58368
59391
  // apps/zaku-cli/src/mcp/server.ts
58369
59392
  import { mkdir as mkdir5 } from "node:fs/promises";
58370
59393
  import { tmpdir } from "node:os";
58371
- import { join as join10 } from "node:path";
59394
+ import { join as join11 } from "node:path";
58372
59395
 
58373
59396
  // apps/zaku-cli/src/mcp/screenshots.ts
58374
59397
  import { readdir as readdir4, rm as rm3, stat } from "node:fs/promises";
58375
- import { join as join8 } from "node:path";
59398
+ import { join as join9 } from "node:path";
58376
59399
  var KEPT_MS = 24 * 60 * 60 * 1e3;
58377
59400
  async function pruneScreenshots(dir, now) {
58378
59401
  let names;
@@ -58382,7 +59405,7 @@ async function pruneScreenshots(dir, now) {
58382
59405
  return;
58383
59406
  }
58384
59407
  for (const name of names.filter((entry) => entry.endsWith(".png"))) {
58385
- const path2 = join8(dir, name);
59408
+ const path2 = join9(dir, name);
58386
59409
  const written = await stat(path2).then(
58387
59410
  (stats) => stats.mtimeMs,
58388
59411
  () => now
@@ -58393,8 +59416,8 @@ async function pruneScreenshots(dir, now) {
58393
59416
 
58394
59417
  // apps/zaku-cli/src/mcp/tools.ts
58395
59418
  import { writeFile as writeFile5 } from "node:fs/promises";
58396
- import { join as join9 } from "node:path";
58397
- 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(
58398
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."
58399
59422
  );
58400
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.";
@@ -58406,7 +59429,7 @@ function registerFigmaTools(server, bridge, options) {
58406
59429
  inputSchema: external_exports.object({
58407
59430
  code: external_exports.string().max(5e4).describe("JavaScript code to execute. Has access to the `figma` global (Figma Plugin API)"),
58408
59431
  description: external_exports.string().max(2e3).describe("A concise description of what the code aims to do"),
58409
- fileKey,
59432
+ fileKey: fileKey2,
58410
59433
  skillNames: external_exports.string().optional().describe("A comma-separated list of Figma skill names being followed, if any.")
58411
59434
  }).strict()
58412
59435
  },
@@ -58424,7 +59447,7 @@ function registerFigmaTools(server, bridge, options) {
58424
59447
  inputSchema: external_exports.object({
58425
59448
  contentsOnly: external_exports.boolean().optional().describe("When true, renders the node in isolation. Defaults to false."),
58426
59449
  enableBase64Response: external_exports.boolean().default(false).describe("When true, the response also includes the screenshot inline as a base64-encoded image entry."),
58427
- fileKey,
59450
+ fileKey: fileKey2,
58428
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."),
58429
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".')
58430
59453
  }).strict()
@@ -58437,7 +59460,7 @@ function registerFigmaTools(server, bridge, options) {
58437
59460
  options.timeoutMs
58438
59461
  );
58439
59462
  if (!answer.ok) return { isError: true, content: [{ type: "text", text: answer.error }] };
58440
- const path2 = join9(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
59463
+ const path2 = join10(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
58441
59464
  await writeFile5(path2, Buffer.from(answer.png, "base64"));
58442
59465
  const text = JSON.stringify({
58443
59466
  path: path2,
@@ -58463,7 +59486,7 @@ async function serveMcp(log) {
58463
59486
  log(
58464
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}`
58465
59488
  );
58466
- const imageDir = join10(tmpdir(), "zaku-screenshots");
59489
+ const imageDir = join11(tmpdir(), "zaku-screenshots");
58467
59490
  await mkdir5(imageDir, { recursive: true });
58468
59491
  await pruneScreenshots(imageDir, Date.now());
58469
59492
  const server = new McpServer({ name: "zaku", version: "1" });
@@ -58480,7 +59503,7 @@ async function serveMcp(log) {
58480
59503
 
58481
59504
  // apps/zaku-cli/src/cli.ts
58482
59505
  var USE_FIGMA_CODE_LIMIT = 5e4;
58483
- 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]";
58484
59507
  async function sendOrRefuse(cli, io, finish, command) {
58485
59508
  try {
58486
59509
  return await cli.bus.send(command);
@@ -58498,6 +59521,14 @@ async function recordRestCalls(cli, io) {
58498
59521
  const { seat, run } = seatAndRun(io.env);
58499
59522
  await cli.bus.send(new RecordBudgetCalls({ day: today(clock.now()), seat, run, kind: "rest", count }));
58500
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
+ };
58501
59532
  var schemaCommand = (program2, io, finish, cli) => {
58502
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) => {
58503
59534
  const written = await cli.bus.send(new WriteSchemas({ out: resolve2(io.cwd, options.out) }));
@@ -58524,7 +59555,7 @@ var checkCommand = (program2, io, finish) => {
58524
59555
  });
58525
59556
  };
58526
59557
  async function productPlaywright(cwd) {
58527
- const require2 = createRequire(join11(cwd, "package.json"));
59558
+ const require2 = createRequire(join12(cwd, "package.json"));
58528
59559
  let resolved;
58529
59560
  try {
58530
59561
  resolved = require2.resolve("playwright");
@@ -58536,7 +59567,7 @@ async function productPlaywright(cwd) {
58536
59567
  }
58537
59568
  function fromProductTree(cwd, resolved) {
58538
59569
  for (let dir = resolve2(cwd); ; dir = dirname4(dir)) {
58539
- if (resolved.startsWith(join11(dir, "node_modules") + sep)) return true;
59570
+ if (resolved.startsWith(join12(dir, "node_modules") + sep)) return true;
58540
59571
  if (dirname4(dir) === dir) return false;
58541
59572
  }
58542
59573
  }
@@ -58661,13 +59692,26 @@ var libraryCommand = (program2, io, finish, cli) => {
58661
59692
  finish(0);
58662
59693
  }
58663
59694
  );
58664
- library.command("save").description("write library/: 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(
58665
59698
  "--input <variables.json>",
58666
59699
  "what use_figma returned for zaku library script; once per page export for a library in the product file",
58667
59700
  (value, previous = []) => [...previous, value]
58668
59701
  ).action(async (options) => {
58669
59702
  const root = resolve2(io.cwd, options.root);
58670
- 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)) {
58671
59715
  io.err("zaku library save needs --input, the variables use_figma returned for zaku library script");
58672
59716
  finish(3);
58673
59717
  return;
@@ -58677,26 +59721,36 @@ var libraryCommand = (program2, io, finish, cli) => {
58677
59721
  finish(3);
58678
59722
  return;
58679
59723
  }
58680
- const part = mergeParts(
59724
+ const part = target.kind === "library" ? mergeParts(
58681
59725
  await Promise.all(
58682
- options.input.map(
59726
+ (options.input ?? []).map(
58683
59727
  async (input2) => parseDesignFile(input2, await readFile6(resolve2(io.cwd, input2), "utf8"), variablePartSchema)
58684
59728
  )
58685
59729
  )
58686
- );
59730
+ ) : void 0;
58687
59731
  try {
58688
- const { snapshot, written, unchanged, removed } = await cli.bus.send(
58689
- 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
+ })
58690
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;
58691
59745
  if (snapshot.components.length === 0) {
58692
59746
  io.err(
58693
- "library/ 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`
58694
59748
  );
58695
59749
  finish(1);
58696
59750
  return;
58697
59751
  }
58698
59752
  io.out(
58699
- `wrote library/: ${snapshot.components.length} components at version ${snapshot.version}; ${written.length} component files written, ${unchanged} unchanged, ${removed.length} removed`
59753
+ `wrote ${saved.dir}/: ${snapshot.components.length} components at version ${snapshot.version}; ${written.length} component files written, ${unchanged} unchanged, ${removed.length} removed`
58700
59754
  );
58701
59755
  finish(0);
58702
59756
  } finally {
@@ -58706,7 +59760,7 @@ var libraryCommand = (program2, io, finish, cli) => {
58706
59760
  };
58707
59761
  var rulesCommand = (program2, io, finish) => {
58708
59762
  const rules = program2.command("rules").description("zaku's rule checks, for a run that reaches Figma through use_figma");
58709
- 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) => {
58710
59764
  if (options.page === void 0 === (options.node === void 0)) {
58711
59765
  io.err("zaku rules script needs --page or --node, one of them");
58712
59766
  finish(3);
@@ -58714,7 +59768,17 @@ var rulesCommand = (program2, io, finish) => {
58714
59768
  }
58715
59769
  const config2 = await readDesignFile(designPaths(resolve2(io.cwd, options.root)).config, zakuConfigSchema);
58716
59770
  const scope = options.node !== void 0 ? { nodeId: options.node } : { page: options.page };
58717
- 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 } : {} })})`;
58718
59782
  const library = await runners.rulesLibrary();
58719
59783
  if (options.after === void 0) {
58720
59784
  io.out(`${library}
@@ -58753,6 +59817,7 @@ var COMMANDS = [
58753
59817
  recipeCommand,
58754
59818
  budgetCommand,
58755
59819
  schemaCommand,
59820
+ librariesCommand,
58756
59821
  mcpCommand
58757
59822
  ];
58758
59823
  async function main(argv, io) {