@zeroxsolutions/zaku 0.0.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/zaku.mjs CHANGED
@@ -1187,11 +1187,11 @@ function __metadata(metadataKey, metadataValue) {
1187
1187
  }
1188
1188
  function __awaiter(thisArg, _arguments, P, generator) {
1189
1189
  function adopt(value) {
1190
- return value instanceof P ? value : new P(function(resolve3) {
1191
- resolve3(value);
1190
+ return value instanceof P ? value : new P(function(resolve4) {
1191
+ resolve4(value);
1192
1192
  });
1193
1193
  }
1194
- return new (P || (P = Promise))(function(resolve3, reject) {
1194
+ return new (P || (P = Promise))(function(resolve4, reject) {
1195
1195
  function fulfilled(value) {
1196
1196
  try {
1197
1197
  step(generator.next(value));
@@ -1207,7 +1207,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
1207
1207
  }
1208
1208
  }
1209
1209
  function step(result) {
1210
- result.done ? resolve3(result.value) : adopt(result.value).then(fulfilled, rejected);
1210
+ result.done ? resolve4(result.value) : adopt(result.value).then(fulfilled, rejected);
1211
1211
  }
1212
1212
  step((generator = generator.apply(thisArg, _arguments || [])).next());
1213
1213
  });
@@ -1385,14 +1385,14 @@ function __asyncValues(o) {
1385
1385
  }, i);
1386
1386
  function verb(n) {
1387
1387
  i[n] = o[n] && function(v) {
1388
- return new Promise(function(resolve3, reject) {
1389
- v = o[n](v), settle2(resolve3, reject, v.done, v.value);
1388
+ return new Promise(function(resolve4, reject) {
1389
+ v = o[n](v), settle2(resolve4, reject, v.done, v.value);
1390
1390
  });
1391
1391
  };
1392
1392
  }
1393
- function settle2(resolve3, reject, d, v) {
1393
+ function settle2(resolve4, reject, d, v) {
1394
1394
  Promise.resolve(v).then(function(v2) {
1395
- resolve3({ value: v2, done: d });
1395
+ resolve4({ value: v2, done: d });
1396
1396
  }, reject);
1397
1397
  }
1398
1398
  }
@@ -2217,7 +2217,7 @@ var require_injectable = __commonJS({
2217
2217
  var reflection_helpers_1 = require_reflection_helpers();
2218
2218
  var dependency_container_1 = require_dependency_container();
2219
2219
  var dependency_container_2 = require_dependency_container();
2220
- function injectable12(options) {
2220
+ function injectable11(options) {
2221
2221
  return function(target) {
2222
2222
  dependency_container_1.typeInfo.set(target, reflection_helpers_1.getParamInfo(target));
2223
2223
  if (options && options.token) {
@@ -2231,7 +2231,7 @@ var require_injectable = __commonJS({
2231
2231
  }
2232
2232
  };
2233
2233
  }
2234
- exports.default = injectable12;
2234
+ exports.default = injectable11;
2235
2235
  }
2236
2236
  });
2237
2237
 
@@ -16461,7 +16461,7 @@ var require_compile = __commonJS({
16461
16461
  const schOrFunc = root.refs[ref];
16462
16462
  if (schOrFunc)
16463
16463
  return schOrFunc;
16464
- let _sch = resolve3.call(this, root, ref);
16464
+ let _sch = resolve4.call(this, root, ref);
16465
16465
  if (_sch === void 0) {
16466
16466
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
16467
16467
  const { schemaId } = this.opts;
@@ -16488,7 +16488,7 @@ var require_compile = __commonJS({
16488
16488
  function sameSchemaEnv(s1, s2) {
16489
16489
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
16490
16490
  }
16491
- function resolve3(root, ref) {
16491
+ function resolve4(root, ref) {
16492
16492
  let sch;
16493
16493
  while (typeof (sch = this.refs[ref]) == "string")
16494
16494
  ref = sch;
@@ -17318,7 +17318,7 @@ var require_fast_uri = __commonJS({
17318
17318
  }
17319
17319
  return uri;
17320
17320
  }
17321
- function resolve3(baseURI, relativeURI, options) {
17321
+ function resolve4(baseURI, relativeURI, options) {
17322
17322
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
17323
17323
  const {
17324
17324
  parsed: baseParsed,
@@ -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, relative3, options, skipNormalization) {
17354
+ function resolveComponent(base, relative4, options, skipNormalization) {
17355
17355
  const target = {};
17356
17356
  if (!skipNormalization) {
17357
17357
  base = parse4(serialize(base, options), options);
17358
- relative3 = parse4(serialize(relative3, options), options);
17358
+ relative4 = parse4(serialize(relative4, options), options);
17359
17359
  }
17360
17360
  options = options || {};
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;
17361
+ if (!options.tolerant && relative4.scheme) {
17362
+ target.scheme = relative4.scheme;
17363
+ target.userinfo = relative4.userinfo;
17364
+ target.host = relative4.host;
17365
+ target.port = relative4.port;
17366
+ target.path = removeDotSegments(relative4.path || "");
17367
+ target.query = relative4.query;
17368
17368
  } else {
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;
17369
+ if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
17370
+ target.userinfo = relative4.userinfo;
17371
+ target.host = relative4.host;
17372
+ target.port = relative4.port;
17373
+ target.path = removeDotSegments(relative4.path || "");
17374
+ target.query = relative4.query;
17375
17375
  } else {
17376
- if (!relative3.path) {
17376
+ if (!relative4.path) {
17377
17377
  target.path = base.path;
17378
- if (relative3.query !== void 0) {
17379
- target.query = relative3.query;
17378
+ if (relative4.query !== void 0) {
17379
+ target.query = relative4.query;
17380
17380
  } else {
17381
17381
  target.query = base.query;
17382
17382
  }
17383
17383
  } else {
17384
- if (relative3.path[0] === "/") {
17385
- target.path = removeDotSegments(relative3.path);
17384
+ if (relative4.path[0] === "/") {
17385
+ target.path = removeDotSegments(relative4.path);
17386
17386
  } else {
17387
17387
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
17388
- target.path = "/" + relative3.path;
17388
+ target.path = "/" + relative4.path;
17389
17389
  } else if (!base.path) {
17390
- target.path = relative3.path;
17390
+ target.path = relative4.path;
17391
17391
  } else {
17392
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
17392
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
17393
17393
  }
17394
17394
  target.path = removeDotSegments(target.path);
17395
17395
  }
17396
- target.query = relative3.query;
17396
+ target.query = relative4.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 = relative3.fragment;
17404
+ target.fragment = relative4.fragment;
17405
17405
  return target;
17406
17406
  }
17407
17407
  function equal(uriA, uriB, options) {
@@ -17687,7 +17687,7 @@ var require_fast_uri = __commonJS({
17687
17687
  var fastUri = {
17688
17688
  SCHEMES,
17689
17689
  normalize,
17690
- resolve: resolve3,
17690
+ resolve: resolve4,
17691
17691
  resolveComponent,
17692
17692
  equal,
17693
17693
  serialize,
@@ -20678,10 +20678,10 @@ var require_dist2 = __commonJS({
20678
20678
 
20679
20679
  // apps/zaku-cli/src/cli.ts
20680
20680
  var import_reflect_metadata3 = __toESM(require_Reflect(), 1);
20681
- import { readFile as readFile6 } from "node:fs/promises";
20681
+ import { readFile as readFile7 } 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 join12, resolve as resolve2, sep } from "node:path";
20684
+ import { dirname as dirname5, join as join13, relative as relative3, resolve as resolve3, 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
@@ -24111,7 +24111,7 @@ function bootstrap(scope, tokens) {
24111
24111
  }
24112
24112
 
24113
24113
  // apps/zaku-cli/src/dependencies.ts
24114
- var import_tsyringe12 = __toESM(require_cjs(), 1);
24114
+ var import_tsyringe11 = __toESM(require_cjs(), 1);
24115
24115
  import { homedir } from "node:os";
24116
24116
 
24117
24117
  // packages/zaku/dist/index.js
@@ -24154,11 +24154,13 @@ async function decodePreset(code, run) {
24154
24154
  // packages/zaku/dist/lib/commands/write-tokens.js
24155
24155
  var WriteTokens = class extends DomainCommand {
24156
24156
  root;
24157
+ config;
24157
24158
  cwd;
24158
24159
  css;
24159
24160
  constructor(input2) {
24160
24161
  super();
24161
24162
  this.root = input2.root;
24163
+ this.config = input2.config;
24162
24164
  this.cwd = input2.cwd;
24163
24165
  this.css = input2.css;
24164
24166
  }
@@ -24167,10 +24169,12 @@ var WriteTokens = class extends DomainCommand {
24167
24169
  // packages/zaku/dist/lib/commands/write-recipe.js
24168
24170
  var WriteRecipe = class extends DomainCommand {
24169
24171
  root;
24172
+ config;
24170
24173
  url;
24171
24174
  constructor(input2) {
24172
24175
  super();
24173
24176
  this.root = input2.root;
24177
+ this.config = input2.config;
24174
24178
  this.url = input2.url;
24175
24179
  }
24176
24180
  };
@@ -24178,11 +24182,13 @@ var WriteRecipe = class extends DomainCommand {
24178
24182
  // packages/zaku/dist/lib/commands/save-library.js
24179
24183
  var SaveLibrary = class extends DomainCommand {
24180
24184
  root;
24185
+ config;
24181
24186
  key;
24182
24187
  part;
24183
24188
  constructor(input2) {
24184
24189
  super();
24185
24190
  this.root = input2.root;
24191
+ this.config = input2.config;
24186
24192
  if (input2.key !== void 0)
24187
24193
  this.key = input2.key;
24188
24194
  if (input2.part !== void 0)
@@ -24193,10 +24199,12 @@ var SaveLibrary = class extends DomainCommand {
24193
24199
  // packages/zaku/dist/lib/commands/write-outline.js
24194
24200
  var WriteOutline = class extends DomainCommand {
24195
24201
  root;
24202
+ config;
24196
24203
  frames;
24197
24204
  constructor(input2) {
24198
24205
  super();
24199
24206
  this.root = input2.root;
24207
+ this.config = input2.config;
24200
24208
  this.frames = input2.frames;
24201
24209
  }
24202
24210
  };
@@ -24236,7 +24244,6 @@ var TOKENS = {
24236
24244
  RUN_COMMAND: "zaku.runCommand",
24237
24245
  RECIPE_PAGE_READER: "zaku.recipePageReader",
24238
24246
  BUDGET_LEDGER: "zaku.budgetLedger",
24239
- DESIGN_CONFIG_REPOSITORY: "zaku.designConfigRepository",
24240
24247
  TOKEN_DOCUMENT_REPOSITORY: "zaku.tokenDocumentRepository",
24241
24248
  RECIPE_REPOSITORY: "zaku.recipeRepository",
24242
24249
  JSON_SCHEMA_FILES: "zaku.jsonSchemaFiles",
@@ -29385,7 +29392,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
29385
29392
  var NONE = 0;
29386
29393
  var ASSUMED = 1;
29387
29394
  var PROVEN = 2;
29388
- function isRecursive(inst, stack, resolve3) {
29395
+ function isRecursive(inst, stack, resolve4) {
29389
29396
  const cached2 = recursive.get(inst);
29390
29397
  if (cached2 !== void 0)
29391
29398
  return cached2 ? PROVEN : NONE;
@@ -29395,7 +29402,7 @@ function isRecursive(inst, stack, resolve3) {
29395
29402
  let result = NONE;
29396
29403
  const check2 = (child) => {
29397
29404
  if (result !== PROVEN && child?._zod) {
29398
- const answer = isRecursive(child, stack, resolve3);
29405
+ const answer = isRecursive(child, stack, resolve4);
29399
29406
  if (answer > result)
29400
29407
  result = answer;
29401
29408
  }
@@ -29406,7 +29413,7 @@ function isRecursive(inst, stack, resolve3) {
29406
29413
  const desc = Object.getOwnPropertyDescriptor(sh, key);
29407
29414
  if (spread && !desc.enumerable)
29408
29415
  continue;
29409
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
29416
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve4) : NONE;
29410
29417
  if (child > answer)
29411
29418
  answer = child;
29412
29419
  }
@@ -29470,7 +29477,7 @@ function isRecursive(inst, stack, resolve3) {
29470
29477
  break;
29471
29478
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
29472
29479
  case "lazy": {
29473
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
29480
+ const inner = def._cachedInner ?? (resolve4 ? inst._zod.innerType : void 0);
29474
29481
  merge3(inner ? isRecursive(inner, stack, false) : ASSUMED);
29475
29482
  break;
29476
29483
  }
@@ -44362,7 +44369,7 @@ function dtcgTokenDocument(system, schemes, name) {
44362
44369
  // packages/zaku/dist/lib/domain/errors/css-required.js
44363
44370
  var CssRequired = class extends Error {
44364
44371
  constructor() {
44365
- super("zaku tokens needs --css, the stylesheet that declares :root and .dark");
44372
+ super("zaku tokens needs designSystem.shadcn.css, the stylesheet that declares :root and .dark, or --css when it runs on one product");
44366
44373
  this.name = "CssRequired";
44367
44374
  }
44368
44375
  };
@@ -44417,6 +44424,34 @@ var PlaywrightMissing = class extends Error {
44417
44424
  }
44418
44425
  };
44419
44426
 
44427
+ // packages/zaku/dist/lib/domain/errors/product-config-missing.js
44428
+ var ProductConfigMissing = class extends Error {
44429
+ path;
44430
+ product;
44431
+ constructor(path2, product) {
44432
+ super(`no ${path2}, the zaku.yaml of the product ${product} the workspace config lists`);
44433
+ this.path = path2;
44434
+ this.product = product;
44435
+ this.name = "ProductConfigMissing";
44436
+ }
44437
+ };
44438
+
44439
+ // packages/zaku/dist/lib/domain/errors/product-required.js
44440
+ var ProductRequired = class extends Error {
44441
+ constructor(command, listed) {
44442
+ super(`${command} acts on one product; name one with --product: ${listed.join(", ")}`);
44443
+ this.name = "ProductRequired";
44444
+ }
44445
+ };
44446
+
44447
+ // packages/zaku/dist/lib/domain/errors/product-unknown.js
44448
+ var ProductUnknown = class extends Error {
44449
+ constructor(name, listed) {
44450
+ super(`no product ${name}; the config lists ${listed.join(", ")}`);
44451
+ this.name = "ProductUnknown";
44452
+ }
44453
+ };
44454
+
44420
44455
  // packages/zaku/dist/lib/domain/errors/recipe-page-failed.js
44421
44456
  var RecipePageFailed = class extends Error {
44422
44457
  constructor(cause) {
@@ -44433,6 +44468,18 @@ var RecipeUrlRequired = class extends Error {
44433
44468
  }
44434
44469
  };
44435
44470
 
44471
+ // packages/zaku/dist/lib/domain/errors/workspace-config-missing.js
44472
+ var WorkspaceConfigMissing = class extends Error {
44473
+ path;
44474
+ legacy;
44475
+ constructor(path2, legacy) {
44476
+ super(legacy === null ? `no ${path2}` : `no ${path2}, and no ${legacy} to fall back on; run zaku where zaku.config.yaml is, or name it with --config`);
44477
+ this.path = path2;
44478
+ this.legacy = legacy;
44479
+ this.name = "WorkspaceConfigMissing";
44480
+ }
44481
+ };
44482
+
44436
44483
  // packages/zaku/dist/lib/domain/theme-css.js
44437
44484
  function declarations(css, selector) {
44438
44485
  const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -44452,19 +44499,17 @@ function parseThemeCss(css) {
44452
44499
 
44453
44500
  // packages/zaku/dist/lib/handlers/write-tokens.js
44454
44501
  var WriteTokensHandler = class WriteTokensHandler2 {
44455
- configs;
44456
44502
  documents;
44457
44503
  files;
44458
44504
  run;
44459
44505
  command = WriteTokens;
44460
- constructor(configs, documents, files2, run) {
44461
- this.configs = configs;
44506
+ constructor(documents, files2, run) {
44462
44507
  this.documents = documents;
44463
44508
  this.files = files2;
44464
44509
  this.run = run;
44465
44510
  }
44466
44511
  async handle(command) {
44467
- const config2 = await this.configs.read(command.root);
44512
+ const { config: config2 } = command;
44468
44513
  const system = config2.designSystem;
44469
44514
  let doc;
44470
44515
  let from;
@@ -44489,10 +44534,9 @@ var WriteTokensHandler = class WriteTokensHandler2 {
44489
44534
  };
44490
44535
  WriteTokensHandler = __decorate2([
44491
44536
  (0, import_tsyringe.injectable)({ token: TOKENS.COMMAND_HANDLER }),
44492
- __param2(0, (0, import_tsyringe.inject)(TOKENS.DESIGN_CONFIG_REPOSITORY)),
44493
- __param2(1, (0, import_tsyringe.inject)(TOKENS.TOKEN_DOCUMENT_REPOSITORY)),
44494
- __param2(2, (0, import_tsyringe.inject)(TOKENS.SOURCE_FILES)),
44495
- __param2(3, (0, import_tsyringe.inject)(TOKENS.RUN_COMMAND))
44537
+ __param2(0, (0, import_tsyringe.inject)(TOKENS.TOKEN_DOCUMENT_REPOSITORY)),
44538
+ __param2(1, (0, import_tsyringe.inject)(TOKENS.SOURCE_FILES)),
44539
+ __param2(2, (0, import_tsyringe.inject)(TOKENS.RUN_COMMAND))
44496
44540
  ], WriteTokensHandler);
44497
44541
 
44498
44542
  // packages/zaku/dist/lib/handlers/write-recipe.js
@@ -44587,17 +44631,15 @@ ${JSON.stringify(Object.entries(props).sort(([a], [b]) => a.localeCompare(b)))}`
44587
44631
 
44588
44632
  // packages/zaku/dist/lib/handlers/write-recipe.js
44589
44633
  var WriteRecipeHandler = class WriteRecipeHandler2 {
44590
- configs;
44591
44634
  recipes;
44592
44635
  readPage;
44593
44636
  command = WriteRecipe;
44594
- constructor(configs, recipes, readPage) {
44595
- this.configs = configs;
44637
+ constructor(recipes, readPage) {
44596
44638
  this.recipes = recipes;
44597
44639
  this.readPage = readPage;
44598
44640
  }
44599
44641
  async handle(command) {
44600
- const config2 = await this.configs.read(command.root);
44642
+ const { config: config2 } = command;
44601
44643
  const url2 = command.url ?? config2.recipe?.url;
44602
44644
  if (!url2)
44603
44645
  throw new RecipeUrlRequired();
@@ -44620,17 +44662,41 @@ var WriteRecipeHandler = class WriteRecipeHandler2 {
44620
44662
  };
44621
44663
  WriteRecipeHandler = __decorate2([
44622
44664
  (0, import_tsyringe2.injectable)({ token: TOKENS.COMMAND_HANDLER }),
44623
- __param2(0, (0, import_tsyringe2.inject)(TOKENS.DESIGN_CONFIG_REPOSITORY)),
44624
- __param2(1, (0, import_tsyringe2.inject)(TOKENS.RECIPE_REPOSITORY)),
44625
- __param2(2, (0, import_tsyringe2.inject)(TOKENS.RECIPE_PAGE_READER))
44665
+ __param2(0, (0, import_tsyringe2.inject)(TOKENS.RECIPE_REPOSITORY)),
44666
+ __param2(1, (0, import_tsyringe2.inject)(TOKENS.RECIPE_PAGE_READER))
44626
44667
  ], WriteRecipeHandler);
44627
44668
 
44628
44669
  // packages/zaku/dist/lib/handlers/save-library.js
44629
44670
  var import_tsyringe3 = __toESM(require_cjs(), 1);
44630
44671
  import { relative as relative2 } from "node:path";
44631
44672
 
44632
- // packages/zaku/dist/lib/domain/copy-defaults.js
44633
- var DEFAULT_COPY = { locales: ["en"], currencies: [] };
44673
+ // packages/zaku/dist/lib/repositories/design-paths.js
44674
+ import { join as join2 } from "node:path";
44675
+ function librarySnapshotDir(paths, key) {
44676
+ if (key === null)
44677
+ return paths;
44678
+ const library = join2(paths.library, key);
44679
+ return { root: paths.root, library, libraryIndex: join2(library, "index.json"), legacyLibrary: null };
44680
+ }
44681
+ function designPaths(root) {
44682
+ return {
44683
+ root,
44684
+ config: join2(root, "zaku.yaml"),
44685
+ maps: join2(root, "map"),
44686
+ outline: join2(root, "outline"),
44687
+ unmapped: join2(root, "outline", "unmapped.yaml"),
44688
+ files: join2(root, "outline", "files.yaml"),
44689
+ library: join2(root, "library"),
44690
+ libraryIndex: join2(root, "library", "index.json"),
44691
+ legacyLibrary: join2(root, "library.json"),
44692
+ tokens: join2(root, "tokens.json"),
44693
+ recipe: join2(root, "recipe.json")
44694
+ };
44695
+ }
44696
+
44697
+ // packages/zaku/dist/lib/repositories/library-snapshot.js
44698
+ import { mkdir, readdir, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "node:fs/promises";
44699
+ import { join as join3, relative } from "node:path";
44634
44700
 
44635
44701
  // packages/zaku/dist/lib/domain/design-graph.js
44636
44702
  var DESKTOP_FAMILIES = /* @__PURE__ */ new Set(["macos", "windows", "gnome", "kde"]);
@@ -44639,21 +44705,15 @@ function defaultBranch(family) {
44639
44705
  return "ui";
44640
44706
  return DESKTOP_FAMILIES.has(family) ? void 0 : "native";
44641
44707
  }
44642
- function defaultPlatform(family) {
44643
- if (family === "web")
44644
- return "web-app";
44645
- return DESKTOP_FAMILIES.has(family) ? "desktop" : "mobile";
44646
- }
44647
44708
  var branchOf = (target) => target.branch ?? defaultBranch(target.family);
44648
- var platformOf = (target) => target.platform ?? defaultPlatform(target.family);
44649
44709
  var unique = (values) => [...new Set(values)];
44650
44710
  function legacyFiles(config2) {
44651
44711
  const branches = unique(config2.targets.flatMap((target) => branchOf(target) ?? []));
44652
- const platforms = unique(config2.targets.map(platformOf));
44712
+ const families2 = unique(config2.targets.map((target) => target.family));
44653
44713
  const { library, product } = config2.figma;
44654
44714
  return {
44655
44715
  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 }))
44716
+ products: product === void 0 ? [] : families2.map((family) => ({ key: product, family }))
44657
44717
  };
44658
44718
  }
44659
44719
  function designGraph(config2) {
@@ -44682,7 +44742,7 @@ function designGraph(config2) {
44682
44742
  prototypes,
44683
44743
  legacy,
44684
44744
  branchOf,
44685
- platformOf,
44745
+ targetsOf: (family) => config2.targets.filter((target) => target.family === family),
44686
44746
  librariesFor: (target) => {
44687
44747
  if (legacy)
44688
44748
  return libraries;
@@ -44694,7 +44754,7 @@ function designGraph(config2) {
44694
44754
  multiBranch: (key) => (byKey.get(key)?.branches.length ?? 0) > 1,
44695
44755
  productOf: (prototypeKey) => {
44696
44756
  const prototype2 = prototypes.find((candidate) => candidate.key === prototypeKey);
44697
- return prototype2?.product ?? (products.length === 1 ? products[0]?.key : void 0);
44757
+ return products.find((product) => product.family === prototype2?.family)?.key;
44698
44758
  }
44699
44759
  };
44700
44760
  }
@@ -44722,337 +44782,6 @@ function snapshotsFor(input2, target) {
44722
44782
  return { libraries, kits, missing };
44723
44783
  }
44724
44784
 
44725
- // packages/zaku/dist/lib/schema/zaku-config.js
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"];
44730
- var DEFAULT_INTERACTIVE = [
44731
- "Button",
44732
- "Button Group",
44733
- "Checkbox",
44734
- "Combobox",
44735
- "Dropdown Menu",
44736
- "Input",
44737
- "Input Group",
44738
- "Input OTP",
44739
- "Native Select",
44740
- "Pagination",
44741
- "Radio Group",
44742
- "Select",
44743
- "Slider",
44744
- "Switch",
44745
- "Tabs",
44746
- "Textarea",
44747
- "Toggle",
44748
- "Toggle Group"
44749
- ];
44750
- var id = external_exports.string().regex(/^[a-z][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting with a letter");
44751
- var targetSchema = external_exports.object({
44752
- id,
44753
- family: external_exports.enum(PLATFORM_FAMILIES),
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"),
44755
- /** The frame size every frame of this target is drawn at; the frame check compares it. */
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()
44761
- }).strict();
44762
- var DEFAULT_SYSTEM_BARS = {
44763
- web: [],
44764
- ios: [["Status Bar"], ["Home Indicator"]],
44765
- android: [["Status Bar"], ["Navigation Bar", "Gesture Handle"]],
44766
- macos: [["Window Controls"]],
44767
- windows: [["Title Bar"]],
44768
- gnome: [["Header Bar"]],
44769
- kde: [["Title Bar"]]
44770
- };
44771
- function systemBarNames(config2) {
44772
- const bars = { ...DEFAULT_SYSTEM_BARS, ...config2.systemBars };
44773
- return new Set(Object.values(bars).flat(2));
44774
- }
44775
- var budgetSchema = external_exports.object({
44776
- mcpPerDay: external_exports.number().int().positive().default(200),
44777
- mcpPerRun: external_exports.number().int().positive().default(30),
44778
- reserve: external_exports.number().min(0).max(1).default(0.2)
44779
- }).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
- }
44807
- var shadcnSystem = external_exports.object({ preset: external_exports.string().min(1) }).strict();
44808
- var tokenFiles = external_exports.array(external_exports.string().min(1)).min(1);
44809
- var resolverInputs = external_exports.record(external_exports.string(), external_exports.string());
44810
- var dtcgSystem = external_exports.object({
44811
- resolver: external_exports.string().min(1).optional(),
44812
- light: external_exports.union([tokenFiles, resolverInputs]),
44813
- dark: external_exports.union([tokenFiles, resolverInputs]),
44814
- /** The group whose tokens are the colour roles, and the one whose tokens are the radius steps. */
44815
- colors: external_exports.string().min(1).default("color"),
44816
- radii: external_exports.string().min(1).default("radius"),
44817
- /** Foreground and background token pairs a text is read in, each needing 4.5:1. */
44818
- contrast: external_exports.array(external_exports.tuple([external_exports.string().min(1), external_exports.string().min(1)])).default([])
44819
- }).strict().superRefine((dtcg, ctx) => {
44820
- for (const scheme of ["light", "dark"]) {
44821
- const files2 = Array.isArray(dtcg[scheme]);
44822
- if (dtcg.resolver !== void 0 && files2) {
44823
- ctx.addIssue({
44824
- code: "custom",
44825
- path: [scheme],
44826
- message: "with a resolver, name the modifier contexts, not files"
44827
- });
44828
- }
44829
- if (dtcg.resolver === void 0 && !files2) {
44830
- ctx.addIssue({
44831
- code: "custom",
44832
- path: [scheme],
44833
- message: "without a resolver, list the token files"
44834
- });
44835
- }
44836
- }
44837
- });
44838
- var designSystemSchema = external_exports.union([
44839
- external_exports.object({ shadcn: shadcnSystem }).strict(),
44840
- external_exports.object({ dtcg: dtcgSystem }).strict()
44841
- ]);
44842
- var recipeSchema = external_exports.object({
44843
- url: external_exports.url().optional(),
44844
- dark: external_exports.enum(["class", "media"]).default("class"),
44845
- darkClass: external_exports.string().min(1).default("dark")
44846
- }).strict();
44847
- function writtenInAScript(tag) {
44848
- try {
44849
- return new Intl.Locale(tag).maximize().script !== void 0;
44850
- } catch {
44851
- return false;
44852
- }
44853
- }
44854
- var copySchema = external_exports.object({
44855
- locales: external_exports.array(external_exports.string().refine(writtenInAScript, "a BCP 47 language tag, such as en or vi")).min(1).default(["en"]),
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([])
44857
- }).strict();
44858
- var configShape = external_exports.object({
44859
- product: external_exports.string().min(1),
44860
- designSystem: designSystemSchema,
44861
- figma: files.default({}),
44862
- targets: external_exports.array(targetSchema).min(1),
44863
- modes: external_exports.record(external_exports.string(), external_exports.string()).default({}),
44864
- interactive: external_exports.array(external_exports.string().min(1)).default([...DEFAULT_INTERACTIVE]),
44865
- /** The bars of the families whose kit names them its own way; every family left out keeps its default. */
44866
- systemBars: external_exports.partialRecord(external_exports.enum(PLATFORM_FAMILIES), external_exports.array(external_exports.array(external_exports.string().min(1)).min(1))).optional(),
44867
- budget: budgetSchema.default({ mcpPerDay: 200, mcpPerRun: 30, reserve: 0.2 }),
44868
- recipe: recipeSchema.optional(),
44869
- copy: copySchema.default(DEFAULT_COPY)
44870
- }).strict();
44871
- var zakuConfigSchema = configShape.superRefine((config2, ctx) => {
44872
- for (const key of ["id", "name"]) {
44873
- const seen = /* @__PURE__ */ new Set();
44874
- config2.targets.forEach((target, index) => {
44875
- if (seen.has(target[key])) {
44876
- ctx.addIssue({
44877
- code: "custom",
44878
- path: ["targets", index, key],
44879
- message: `a second target with ${key} ${target[key]}`
44880
- });
44881
- }
44882
- seen.add(target[key]);
44883
- });
44884
- }
44885
- if (byRole(config2.figma))
44886
- refineFilesByRole(config2, ctx);
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
- }
44991
-
44992
- // packages/zaku/dist/lib/repositories/design-file.js
44993
- var import_yaml = __toESM(require_dist(), 1);
44994
- import { readFile, rename, writeFile } from "node:fs/promises";
44995
- function parseDesignFile(path2, text, schema) {
44996
- let data;
44997
- try {
44998
- data = path2.endsWith(".json") ? JSON.parse(text) : (0, import_yaml.parse)(text);
44999
- } catch (error62) {
45000
- throw new DesignFileInvalid(path2, [error62.message]);
45001
- }
45002
- const result = schema.safeParse(data);
45003
- if (!result.success) {
45004
- throw new DesignFileInvalid(path2, result.error.issues.map((issue2) => `${issue2.path.map(String).join(".") || "(root)"}: ${issue2.message}`));
45005
- }
45006
- return result.data;
45007
- }
45008
- async function readDesignFile(path2, schema) {
45009
- return parseDesignFile(path2, await readFile(path2, "utf8"), schema);
45010
- }
45011
- async function readOptionalDesignFile(path2, schema) {
45012
- let text;
45013
- try {
45014
- text = await readFile(path2, "utf8");
45015
- } catch (error62) {
45016
- if (error62.code === "ENOENT")
45017
- return null;
45018
- throw error62;
45019
- }
45020
- return parseDesignFile(path2, text, schema);
45021
- }
45022
- async function writeDesignFile(path2, value) {
45023
- const temporary = `${path2}.${process.pid}.tmp`;
45024
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}
45025
- `);
45026
- await rename(temporary, path2);
45027
- }
45028
-
45029
- // packages/zaku/dist/lib/repositories/design-paths.js
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
- }
45037
- function designPaths(root) {
45038
- return {
45039
- root,
45040
- config: join2(root, "zaku.yaml"),
45041
- maps: join2(root, "map"),
45042
- outline: join2(root, "outline"),
45043
- unmapped: join2(root, "outline", "unmapped.yaml"),
45044
- library: join2(root, "library"),
45045
- libraryIndex: join2(root, "library", "index.json"),
45046
- legacyLibrary: join2(root, "library.json"),
45047
- tokens: join2(root, "tokens.json"),
45048
- recipe: join2(root, "recipe.json")
45049
- };
45050
- }
45051
-
45052
- // packages/zaku/dist/lib/repositories/library-snapshot.js
45053
- import { mkdir, readdir, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "node:fs/promises";
45054
- import { join as join3, relative } from "node:path";
45055
-
45056
44785
  // packages/zaku/dist/lib/domain/library.js
45057
44786
  var rgbaSchema = external_exports.object({ r: external_exports.number(), g: external_exports.number(), b: external_exports.number(), a: external_exports.number() }).strict();
45058
44787
  var libraryItemSchema = external_exports.object({
@@ -45341,6 +45070,43 @@ function formatLibraryFile(value) {
45341
45070
  `;
45342
45071
  }
45343
45072
 
45073
+ // packages/zaku/dist/lib/repositories/design-file.js
45074
+ var import_yaml = __toESM(require_dist(), 1);
45075
+ import { readFile, rename, writeFile } from "node:fs/promises";
45076
+ function parseDesignFile(path2, text, schema) {
45077
+ let data;
45078
+ try {
45079
+ data = path2.endsWith(".json") ? JSON.parse(text) : (0, import_yaml.parse)(text);
45080
+ } catch (error62) {
45081
+ throw new DesignFileInvalid(path2, [error62.message]);
45082
+ }
45083
+ const result = schema.safeParse(data);
45084
+ if (!result.success) {
45085
+ throw new DesignFileInvalid(path2, result.error.issues.map((issue2) => `${issue2.path.map(String).join(".") || "(root)"}: ${issue2.message}`));
45086
+ }
45087
+ return result.data;
45088
+ }
45089
+ async function readDesignFile(path2, schema) {
45090
+ return parseDesignFile(path2, await readFile(path2, "utf8"), schema);
45091
+ }
45092
+ async function readOptionalDesignFile(path2, schema) {
45093
+ let text;
45094
+ try {
45095
+ text = await readFile(path2, "utf8");
45096
+ } catch (error62) {
45097
+ if (error62.code === "ENOENT")
45098
+ return null;
45099
+ throw error62;
45100
+ }
45101
+ return parseDesignFile(path2, text, schema);
45102
+ }
45103
+ async function writeDesignFile(path2, value) {
45104
+ const temporary = `${path2}.${process.pid}.tmp`;
45105
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}
45106
+ `);
45107
+ await rename(temporary, path2);
45108
+ }
45109
+
45344
45110
  // packages/zaku/dist/lib/repositories/library-snapshot.js
45345
45111
  async function readText(path2) {
45346
45112
  try {
@@ -45733,8 +45499,7 @@ function librarySaveTarget(config2, key) {
45733
45499
  }
45734
45500
  async function saveLibrary(options) {
45735
45501
  const paths = designPaths(options.root);
45736
- const config2 = await readDesignFile(paths.config, zakuConfigSchema);
45737
- const target = librarySaveTarget(config2, options.key);
45502
+ const target = librarySaveTarget(options.config, options.key);
45738
45503
  if (target.kind === "kit") {
45739
45504
  const at2 = librarySnapshotDir(paths, target.kit.key);
45740
45505
  const kit = await readKit({ rest: options.rest, key: target.kit.key, family: target.kit.family });
@@ -45762,6 +45527,7 @@ var SaveLibraryHandler = class SaveLibraryHandler2 {
45762
45527
  handle(command) {
45763
45528
  return saveLibrary({
45764
45529
  root: command.root,
45530
+ config: command.config,
45765
45531
  rest: this.rest,
45766
45532
  ...command.key !== void 0 ? { key: command.key } : {},
45767
45533
  ...command.part !== void 0 ? { part: command.part } : {}
@@ -45780,6 +45546,85 @@ var import_yaml2 = __toESM(require_dist(), 1);
45780
45546
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
45781
45547
  import { dirname as dirname2, join as join6 } from "node:path";
45782
45548
 
45549
+ // packages/zaku/dist/lib/adapters/figma-rest.js
45550
+ var FigmaRateLimited = class extends Error {
45551
+ url;
45552
+ constructor(url2) {
45553
+ super(`Figma answered 429 twice for ${url2}; wait and run again`);
45554
+ this.url = url2;
45555
+ this.name = "FigmaRateLimited";
45556
+ }
45557
+ };
45558
+ var FigmaRequestFailed = class extends Error {
45559
+ url;
45560
+ status;
45561
+ constructor(url2, status, body) {
45562
+ super(`Figma answered ${status} for ${url2}: ${body.slice(0, 200)}`);
45563
+ this.url = url2;
45564
+ this.status = status;
45565
+ this.name = "FigmaRequestFailed";
45566
+ }
45567
+ };
45568
+ var BASE = "https://api.figma.com/v1";
45569
+ var BATCH = 50;
45570
+ var MAX_WAIT_MS = 12e4;
45571
+ function createFigmaRest(options) {
45572
+ const send = options.fetch ?? globalThis.fetch;
45573
+ const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
45574
+ const onCall = options.onCall ?? (() => void 0);
45575
+ async function call(url2) {
45576
+ for (let attempt = 0; attempt < 2; attempt += 1) {
45577
+ const res = await send(url2, { headers: { "X-Figma-Token": options.token } });
45578
+ onCall();
45579
+ if (res.status === 429) {
45580
+ if (attempt === 1)
45581
+ break;
45582
+ const seconds = Number(res.headers.get("Retry-After") ?? "60");
45583
+ await sleep(Math.min(Number.isFinite(seconds) ? seconds * 1e3 : 6e4, MAX_WAIT_MS));
45584
+ continue;
45585
+ }
45586
+ if (!res.ok)
45587
+ throw new FigmaRequestFailed(url2, res.status, await res.text());
45588
+ return await res.json();
45589
+ }
45590
+ throw new FigmaRateLimited(url2);
45591
+ }
45592
+ const chunks = (ids) => Array.from({ length: Math.ceil(ids.length / BATCH) }, (_, index) => ids.slice(index * BATCH, (index + 1) * BATCH));
45593
+ async function components(fileKey3) {
45594
+ const listed = await call(`${BASE}/files/${fileKey3}/components`);
45595
+ return listed.meta.components;
45596
+ }
45597
+ return {
45598
+ file: (fileKey3, depth) => call(`${BASE}/files/${fileKey3}?depth=${depth}`),
45599
+ async fileName(fileKey3) {
45600
+ const meta3 = await call(`${BASE}/files/${fileKey3}/meta`);
45601
+ return meta3.file.name;
45602
+ },
45603
+ async nodes(fileKey3, ids) {
45604
+ const merged = { version: "", nodes: {} };
45605
+ for (const chunk of chunks(ids)) {
45606
+ const part = await call(`${BASE}/files/${fileKey3}/nodes?ids=${encodeURIComponent(chunk.join(","))}`);
45607
+ merged.version = part.version;
45608
+ Object.assign(merged.nodes, part.nodes);
45609
+ }
45610
+ return merged;
45611
+ },
45612
+ async published(fileKey3) {
45613
+ const sets = await call(`${BASE}/files/${fileKey3}/component_sets`);
45614
+ return { sets: sets.meta.component_sets, components: await components(fileKey3) };
45615
+ },
45616
+ components,
45617
+ async images(fileKey3, ids) {
45618
+ const merged = {};
45619
+ for (const chunk of chunks(ids)) {
45620
+ const part = await call(`${BASE}/images/${fileKey3}?ids=${encodeURIComponent(chunk.join(","))}&format=png&scale=1`);
45621
+ Object.assign(merged, part.images);
45622
+ }
45623
+ return merged;
45624
+ }
45625
+ };
45626
+ }
45627
+
45783
45628
  // packages/zaku/dist/lib/domain/documentation-measures.js
45784
45629
  var COVER = {
45785
45630
  page: "Thumbnail",
@@ -46357,7 +46202,7 @@ function outlineFrame(frame, where, ctx) {
46357
46202
  const images2 = [];
46358
46203
  const defaultNames = [];
46359
46204
  const links = [];
46360
- const relative3 = (node2) => {
46205
+ const relative4 = (node2) => {
46361
46206
  const box = node2.absoluteBoundingBox;
46362
46207
  return box ? { x: box.x - origin.x, y: box.y - origin.y, width: box.width, height: box.height } : null;
46363
46208
  };
@@ -46386,7 +46231,7 @@ function outlineFrame(frame, where, ctx) {
46386
46231
  componentKey: ctx.components[child.componentId ?? ""]?.key ?? "",
46387
46232
  component: component2,
46388
46233
  interactive: ctx.interactive.has(component2),
46389
- bounds: relative3(child)
46234
+ bounds: relative4(child)
46390
46235
  });
46391
46236
  } else if (child.type === "TEXT") {
46392
46237
  instance.texts.push({
@@ -46427,7 +46272,7 @@ function outlineFrame(frame, where, ctx) {
46427
46272
  texts: [],
46428
46273
  interactive: ctx.interactive.has(component2),
46429
46274
  local: meta3?.remote === false,
46430
- bounds: relative3(node2),
46275
+ bounds: relative4(node2),
46431
46276
  sizing: {
46432
46277
  horizontal: node2.layoutSizingHorizontal ?? "FIXED",
46433
46278
  vertical: node2.layoutSizingVertical ?? "FIXED"
@@ -46609,7 +46454,7 @@ var screenOutlineSchema = external_exports.preprocess(legacyVersion, external_ex
46609
46454
  mapDigest: external_exports.string(),
46610
46455
  /** The version of each library snapshot the frames were read against, by key. */
46611
46456
  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. */
46457
+ /** The version of every product file zaku.yaml lists, when it lists them by family. */
46613
46458
  fileVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
46614
46459
  frames: external_exports.array(frameOutlineSchema)
46615
46460
  }).strict());
@@ -46619,6 +46464,7 @@ var unmappedSchema = external_exports.object({
46619
46464
  /** A component drawn on a product page, which belongs in a Components library. */
46620
46465
  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()
46621
46466
  }).strict();
46467
+ var outlineFilesSchema = external_exports.object({ files: external_exports.array(external_exports.object({ key: external_exports.string(), name: external_exports.string() }).strict()) }).strict();
46622
46468
 
46623
46469
  // packages/zaku/dist/lib/repositories/outlines.js
46624
46470
  var FLOWS_DIR = "flows";
@@ -46638,12 +46484,12 @@ async function loadOutlines(dir) {
46638
46484
  features = (await readdir3(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== FLOWS_DIR).map((entry) => entry.name).sort();
46639
46485
  } catch (error62) {
46640
46486
  if (error62.code === "ENOENT")
46641
- return { outlines, flows, unmapped: null, errors };
46487
+ return { outlines, flows, unmapped: null, files: null, errors };
46642
46488
  throw error62;
46643
46489
  }
46644
46490
  for (const feature of features) {
46645
- const files2 = (await readdir3(join5(dir, feature))).filter((name) => name.endsWith(".yaml")).sort();
46646
- for (const file2 of files2) {
46491
+ const files3 = (await readdir3(join5(dir, feature))).filter((name) => name.endsWith(".yaml")).sort();
46492
+ for (const file2 of files3) {
46647
46493
  try {
46648
46494
  outlines.push(await readDesignFile(join5(dir, feature, file2), screenOutlineSchema));
46649
46495
  } catch (error62) {
@@ -46660,8 +46506,8 @@ async function loadOutlines(dir) {
46660
46506
  throw error62;
46661
46507
  });
46662
46508
  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) {
46509
+ const files3 = (await readdir3(join5(dir, FLOWS_DIR, source))).filter((name) => name.endsWith(".yaml")).sort();
46510
+ for (const file2 of files3) {
46665
46511
  try {
46666
46512
  flows.push(await readDesignFile(join5(dir, FLOWS_DIR, source, file2), flowOutlineSchema));
46667
46513
  } catch (error62) {
@@ -46681,7 +46527,16 @@ async function loadOutlines(dir) {
46681
46527
  else
46682
46528
  throw error62;
46683
46529
  }
46684
- return { outlines, flows, unmapped, errors };
46530
+ let files2 = null;
46531
+ try {
46532
+ files2 = await readOptionalDesignFile(join5(dir, "files.yaml"), outlineFilesSchema);
46533
+ } catch (error62) {
46534
+ if (error62 instanceof DesignFileInvalid)
46535
+ errors.push(error62);
46536
+ else
46537
+ throw error62;
46538
+ }
46539
+ return { outlines, flows, unmapped, files: files2, errors };
46685
46540
  }
46686
46541
 
46687
46542
  // packages/zaku/dist/lib/handlers/write-outline.js
@@ -46755,14 +46610,33 @@ function productFiles(config2) {
46755
46610
  return config2.figma.product === void 0 ? [] : [{ key: config2.figma.product, targets: config2.targets }];
46756
46611
  return graph.products.map((product) => ({
46757
46612
  key: product.key,
46758
- targets: config2.targets.filter((target) => graph.platformOf(target) === product.platform)
46613
+ targets: graph.targetsOf(product.family)
46759
46614
  }));
46760
46615
  }
46616
+ async function readFileNames(rest, config2, read) {
46617
+ const graph = designGraph(config2);
46618
+ const files2 = [];
46619
+ for (const file2 of [...graph.products, ...graph.prototypes]) {
46620
+ const name = read.get(file2.key);
46621
+ if (name !== void 0)
46622
+ files2.push({ key: file2.key, name });
46623
+ }
46624
+ for (const library of graph.libraries.filter((each) => each.layer === "components")) {
46625
+ try {
46626
+ files2.push({ key: library.key, name: await rest.fileName(library.key) });
46627
+ } catch (error62) {
46628
+ if (error62 instanceof FigmaRequestFailed && (error62.status === 403 || error62.status === 404))
46629
+ continue;
46630
+ throw error62;
46631
+ }
46632
+ }
46633
+ return { files: files2 };
46634
+ }
46761
46635
  async function runOutline(options) {
46762
46636
  const paths = designPaths(options.root);
46763
- const config2 = await readDesignFile(paths.config, zakuConfigSchema);
46764
- const { libraries: listed, kits, products: byPlatform, prototypes: flows } = config2.figma;
46765
- const legacy = [listed, kits, byPlatform, flows].every((files2) => files2 === void 0);
46637
+ const { config: config2 } = options;
46638
+ const { libraries: listed, kits, products: byFamily, prototypes: flows } = config2.figma;
46639
+ const legacy = [listed, kits, byFamily, flows].every((files2) => files2 === void 0);
46766
46640
  const products = productFiles(config2);
46767
46641
  if (products.length === 0)
46768
46642
  throw new FigmaFileKeyMissing(legacy ? "figma.product" : "figma.products");
@@ -46775,6 +46649,7 @@ async function runOutline(options) {
46775
46649
  const existing = await loadOutlines(paths.outline);
46776
46650
  const prototypes = new Set(legacy ? config2.targets.map((target) => `${target.name} prototype`) : []);
46777
46651
  const versions = {};
46652
+ const names = /* @__PURE__ */ new Map();
46778
46653
  const flowPages = /* @__PURE__ */ new Map();
46779
46654
  const flowFrame = (source, frame, targets) => {
46780
46655
  const parsed = parseFrameName(frame.name, targets);
@@ -46792,6 +46667,7 @@ ${frame.page}`;
46792
46667
  for (const product of products) {
46793
46668
  const file2 = await options.rest.file(product.key, INDEX_DEPTH);
46794
46669
  versions[product.key] = file2.version;
46670
+ names.set(product.key, file2.name);
46795
46671
  const listed2 = listFrames(file2.document);
46796
46672
  for (const frame of listed2.filter((candidate) => prototypes.has(candidate.page)))
46797
46673
  flowFrame(product.key, frame, product.targets);
@@ -46815,13 +46691,18 @@ ${frame.page}`;
46815
46691
  for (const file2 of legacy ? [] : graph.prototypes) {
46816
46692
  const read = await options.rest.file(file2.key, INDEX_DEPTH);
46817
46693
  versions[file2.key] = read.version;
46818
- const targets = config2.targets.filter((target) => graph.platformOf(target) === file2.platform);
46694
+ names.set(file2.key, read.name);
46695
+ const targets = graph.targetsOf(file2.family);
46819
46696
  for (const frame of listFrames(read.document).filter((candidate) => holdsScreens(candidate.page)))
46820
46697
  flowFrame(file2.key, frame, targets);
46821
46698
  }
46822
46699
  unmapped.fileVersion = Object.values(versions).join(",");
46823
46700
  if (!legacy)
46824
46701
  unmapped.components = components;
46702
+ if (!legacy) {
46703
+ await mkdir2(paths.outline, { recursive: true });
46704
+ await writeFile3(paths.files, (0, import_yaml2.stringify)(await readFileNames(options.rest, config2, names)));
46705
+ }
46825
46706
  const outlined = new Set(existing.outlines.flatMap((outline) => outline.frames.map((frame) => frame.nodeId)));
46826
46707
  const drawnScreens = new Set([...resolved.values()].map(({ ref }) => `${ref.feature}/${ref.id}`));
46827
46708
  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}`));
@@ -46942,7 +46823,7 @@ var WriteOutlineHandler = class WriteOutlineHandler2 {
46942
46823
  this.rest = rest;
46943
46824
  }
46944
46825
  handle(command) {
46945
- return runOutline({ root: command.root, rest: this.rest, frames: command.frames });
46826
+ return runOutline({ root: command.root, config: command.config, rest: this.rest, frames: command.frames });
46946
46827
  }
46947
46828
  };
46948
46829
  WriteOutlineHandler = __decorate2([
@@ -46953,10 +46834,389 @@ WriteOutlineHandler = __decorate2([
46953
46834
  // packages/zaku/dist/lib/handlers/write-schemas.js
46954
46835
  var import_tsyringe5 = __toESM(require_cjs(), 1);
46955
46836
 
46837
+ // packages/zaku/dist/lib/schema/zaku-config.js
46838
+ import { posix } from "node:path";
46839
+
46840
+ // packages/zaku/dist/lib/domain/copy-defaults.js
46841
+ var DEFAULT_COPY = { locales: ["en"], currencies: [] };
46842
+
46843
+ // packages/zaku/dist/lib/schema/zaku-config.js
46844
+ var PLATFORM_FAMILIES = ["web", "ios", "android", "macos", "windows", "gnome", "kde"];
46845
+ var LAYERS = ["foundations", "components"];
46846
+ var BRANCHES = ["ui", "native"];
46847
+ var DEFAULT_INTERACTIVE = [
46848
+ "Button",
46849
+ "Button Group",
46850
+ "Checkbox",
46851
+ "Combobox",
46852
+ "Dropdown Menu",
46853
+ "Input",
46854
+ "Input Group",
46855
+ "Input OTP",
46856
+ "Native Select",
46857
+ "Pagination",
46858
+ "Radio Group",
46859
+ "Select",
46860
+ "Slider",
46861
+ "Switch",
46862
+ "Tabs",
46863
+ "Textarea",
46864
+ "Toggle",
46865
+ "Toggle Group"
46866
+ ];
46867
+ var id = external_exports.string().regex(/^[a-z][a-z0-9-]*$/, "lowercase letters, digits and dashes, starting with a letter");
46868
+ var targetSchema = external_exports.object({
46869
+ id,
46870
+ family: external_exports.enum(PLATFORM_FAMILIES),
46871
+ 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"),
46872
+ /** The frame size every frame of this target is drawn at; the frame check compares it. */
46873
+ size: external_exports.object({ width: external_exports.number().positive(), height: external_exports.number().positive() }).strict().optional(),
46874
+ /** The renderer whose libraries its frames draw from; `defaultBranch` in the design graph gives it when left out. */
46875
+ branch: external_exports.enum(BRANCHES).optional()
46876
+ }).strict();
46877
+ var DEFAULT_SYSTEM_BARS = {
46878
+ web: [],
46879
+ ios: [["Status Bar"], ["Home Indicator"]],
46880
+ android: [["Status Bar"], ["Navigation Bar", "Gesture Handle"]],
46881
+ macos: [["Window Controls"]],
46882
+ windows: [["Title Bar"]],
46883
+ gnome: [["Header Bar"]],
46884
+ kde: [["Title Bar"]]
46885
+ };
46886
+ function systemBarNames(config2) {
46887
+ const bars = { ...DEFAULT_SYSTEM_BARS, ...config2.systemBars };
46888
+ return new Set(Object.values(bars).flat(2));
46889
+ }
46890
+ var budgetSchema = external_exports.object({
46891
+ mcpPerDay: external_exports.number().int().positive().default(200),
46892
+ mcpPerRun: external_exports.number().int().positive().default(30),
46893
+ reserve: external_exports.number().min(0).max(1).default(0.2)
46894
+ }).strict();
46895
+ var fileKey = external_exports.string().min(1);
46896
+ var kitSchema = external_exports.object({ key: fileKey, family: external_exports.enum(PLATFORM_FAMILIES) }).strict();
46897
+ var librarySchema = external_exports.object({
46898
+ key: fileKey,
46899
+ layer: external_exports.enum(LAYERS),
46900
+ branches: external_exports.array(external_exports.enum(BRANCHES)).min(1),
46901
+ /** The libraries and kits this one is built on, by key. */
46902
+ extends: external_exports.array(fileKey).default([])
46903
+ }).strict();
46904
+ var productSchema = external_exports.object({ key: fileKey, family: external_exports.enum(PLATFORM_FAMILIES) }).strict();
46905
+ var prototypeSchema = external_exports.object({ key: fileKey, family: external_exports.enum(PLATFORM_FAMILIES) }).strict();
46906
+ var files = external_exports.object({
46907
+ library: fileKey.optional(),
46908
+ product: fileKey.optional(),
46909
+ kits: external_exports.array(kitSchema).optional(),
46910
+ libraries: external_exports.array(librarySchema).optional(),
46911
+ products: external_exports.array(productSchema).optional(),
46912
+ prototypes: external_exports.array(prototypeSchema).optional()
46913
+ }).strict();
46914
+ function byRole(figma) {
46915
+ return [figma.kits, figma.libraries, figma.products, figma.prototypes].some((list) => list !== void 0);
46916
+ }
46917
+ var shadcnSystem = external_exports.object({
46918
+ preset: external_exports.string().min(1),
46919
+ /** The stylesheet `zaku tokens` reads, relative to the directory of the workspace config. */
46920
+ css: external_exports.string().min(1).optional()
46921
+ }).strict();
46922
+ var tokenFiles = external_exports.array(external_exports.string().min(1)).min(1);
46923
+ var resolverInputs = external_exports.record(external_exports.string(), external_exports.string());
46924
+ var dtcgSystem = external_exports.object({
46925
+ resolver: external_exports.string().min(1).optional(),
46926
+ light: external_exports.union([tokenFiles, resolverInputs]),
46927
+ dark: external_exports.union([tokenFiles, resolverInputs]),
46928
+ /** The group whose tokens are the colour roles, and the one whose tokens are the radius steps. */
46929
+ colors: external_exports.string().min(1).default("color"),
46930
+ radii: external_exports.string().min(1).default("radius"),
46931
+ /** Foreground and background token pairs a text is read in, each needing 4.5:1. */
46932
+ contrast: external_exports.array(external_exports.tuple([external_exports.string().min(1), external_exports.string().min(1)])).default([])
46933
+ }).strict().superRefine((dtcg, ctx) => {
46934
+ for (const scheme of ["light", "dark"]) {
46935
+ const files2 = Array.isArray(dtcg[scheme]);
46936
+ if (dtcg.resolver !== void 0 && files2) {
46937
+ ctx.addIssue({
46938
+ code: "custom",
46939
+ path: [scheme],
46940
+ message: "with a resolver, name the modifier contexts, not files"
46941
+ });
46942
+ }
46943
+ if (dtcg.resolver === void 0 && !files2) {
46944
+ ctx.addIssue({
46945
+ code: "custom",
46946
+ path: [scheme],
46947
+ message: "without a resolver, list the token files"
46948
+ });
46949
+ }
46950
+ }
46951
+ });
46952
+ var designSystemSchema = external_exports.union([
46953
+ external_exports.object({ shadcn: shadcnSystem }).strict(),
46954
+ external_exports.object({ dtcg: dtcgSystem }).strict()
46955
+ ]);
46956
+ var recipeSchema = external_exports.object({
46957
+ url: external_exports.url().optional(),
46958
+ dark: external_exports.enum(["class", "media"]).default("class"),
46959
+ darkClass: external_exports.string().min(1).default("dark")
46960
+ }).strict();
46961
+ function writtenInAScript(tag) {
46962
+ try {
46963
+ return new Intl.Locale(tag).maximize().script !== void 0;
46964
+ } catch {
46965
+ return false;
46966
+ }
46967
+ }
46968
+ var copySchema = external_exports.object({
46969
+ locales: external_exports.array(external_exports.string().refine(writtenInAScript, "a BCP 47 language tag, such as en or vi")).min(1).default(["en"]),
46970
+ currencies: external_exports.array(external_exports.string().refine((code) => Intl.supportedValuesOf("currency").includes(code), "an ISO 4217 code, such as VND or USD")).default([])
46971
+ }).strict();
46972
+ var configShape = external_exports.object({
46973
+ product: external_exports.string().min(1),
46974
+ designSystem: designSystemSchema,
46975
+ figma: files.default({}),
46976
+ targets: external_exports.array(targetSchema).min(1),
46977
+ modes: external_exports.record(external_exports.string(), external_exports.string()).default({}),
46978
+ interactive: external_exports.array(external_exports.string().min(1)).default([...DEFAULT_INTERACTIVE]),
46979
+ /** The bars of the families whose kit names them its own way; every family left out keeps its default. */
46980
+ systemBars: external_exports.partialRecord(external_exports.enum(PLATFORM_FAMILIES), external_exports.array(external_exports.array(external_exports.string().min(1)).min(1))).optional(),
46981
+ budget: budgetSchema.default({ mcpPerDay: 200, mcpPerRun: 30, reserve: 0.2 }),
46982
+ recipe: recipeSchema.optional(),
46983
+ copy: copySchema.default(DEFAULT_COPY)
46984
+ }).strict();
46985
+ var zakuConfigSchema = configShape.superRefine((config2, ctx) => {
46986
+ for (const key of ["id", "name"]) {
46987
+ const seen = /* @__PURE__ */ new Set();
46988
+ config2.targets.forEach((target, index) => {
46989
+ if (seen.has(target[key])) {
46990
+ ctx.addIssue({
46991
+ code: "custom",
46992
+ path: ["targets", index, key],
46993
+ message: `a second target with ${key} ${target[key]}`
46994
+ });
46995
+ }
46996
+ seen.add(target[key]);
46997
+ });
46998
+ }
46999
+ if (byRole(config2.figma))
47000
+ refineFilesByRole(config2, ctx);
47001
+ });
47002
+ function refineFilesByRole(config2, ctx) {
47003
+ const { figma } = config2;
47004
+ if (figma.library !== void 0 || figma.product !== void 0) {
47005
+ ctx.addIssue({
47006
+ code: "custom",
47007
+ path: ["figma"],
47008
+ message: "figma.library and figma.product describe one library and one product; list files by role instead"
47009
+ });
47010
+ }
47011
+ const kits = figma.kits ?? [];
47012
+ const libraries = figma.libraries ?? [];
47013
+ const products = figma.products ?? [];
47014
+ const seen = /* @__PURE__ */ new Set();
47015
+ const lists = [
47016
+ ["kits", kits],
47017
+ ["libraries", libraries],
47018
+ ["products", products],
47019
+ ["prototypes", figma.prototypes ?? []]
47020
+ ];
47021
+ for (const [list, entries] of lists) {
47022
+ entries.forEach((entry, index) => {
47023
+ if (seen.has(entry.key))
47024
+ ctx.addIssue({
47025
+ code: "custom",
47026
+ path: ["figma", list, index, "key"],
47027
+ message: `a second file with key ${entry.key}`
47028
+ });
47029
+ seen.add(entry.key);
47030
+ });
47031
+ }
47032
+ const byKey = new Map(libraries.map((library) => [library.key, library]));
47033
+ const kitKeys = new Set(kits.map((kit) => kit.key));
47034
+ libraries.forEach((library, index) => {
47035
+ const path2 = ["figma", "libraries", index, "extends"];
47036
+ for (const key of library.extends) {
47037
+ const base = byKey.get(key);
47038
+ if (!base && !kitKeys.has(key)) {
47039
+ ctx.addIssue({
47040
+ code: "custom",
47041
+ path: path2,
47042
+ message: `${library.key} extends ${key}, which is no library or kit listed`
47043
+ });
47044
+ } else if (base?.layer === "components" && library.layer === "foundations") {
47045
+ ctx.addIssue({ code: "custom", path: path2, message: `foundations ${library.key} extends components ${key}` });
47046
+ } else if (base && !base.branches.some((branch) => library.branches.includes(branch))) {
47047
+ ctx.addIssue({
47048
+ code: "custom",
47049
+ path: path2,
47050
+ message: `${library.key} extends ${key}, which holds none of its branches`
47051
+ });
47052
+ }
47053
+ }
47054
+ });
47055
+ const cycle = findCycle(libraries);
47056
+ if (cycle)
47057
+ ctx.addIssue({ code: "custom", path: ["figma", "libraries"], message: `extends comes back: ${cycle}` });
47058
+ for (const [list, entries] of [
47059
+ ["products", products],
47060
+ ["prototypes", figma.prototypes ?? []]
47061
+ ]) {
47062
+ const families2 = /* @__PURE__ */ new Set();
47063
+ entries.forEach((entry, index) => {
47064
+ if (families2.has(entry.family))
47065
+ ctx.addIssue({
47066
+ code: "custom",
47067
+ path: ["figma", list, index, "family"],
47068
+ message: `a second ${list === "products" ? "product" : "prototype"} file for ${entry.family}`
47069
+ });
47070
+ families2.add(entry.family);
47071
+ });
47072
+ }
47073
+ (figma.prototypes ?? []).forEach((prototype2, index) => {
47074
+ if (!products.some((product) => product.family === prototype2.family))
47075
+ ctx.addIssue({
47076
+ code: "custom",
47077
+ path: ["figma", "prototypes", index, "family"],
47078
+ message: `no product file for ${prototype2.family} to copy its frames from`
47079
+ });
47080
+ });
47081
+ config2.targets.forEach((target, index) => {
47082
+ const branch = target.branch ?? defaultBranch(target.family);
47083
+ const path2 = ["targets", index, "branch"];
47084
+ if (branch === void 0) {
47085
+ ctx.addIssue({ code: "custom", path: path2, message: `a ${target.family} target names its branch, ui or native` });
47086
+ } else if (libraries.length > 0 && !libraries.some((library) => library.branches.includes(branch))) {
47087
+ ctx.addIssue({ code: "custom", path: path2, message: `no library holds the ${branch} branch` });
47088
+ }
47089
+ });
47090
+ }
47091
+ function findCycle(libraries) {
47092
+ const byKey = new Map(libraries.map((library) => [library.key, library]));
47093
+ const visit2 = (key, path2) => {
47094
+ if (path2.includes(key))
47095
+ return [...path2.slice(path2.indexOf(key)), key].join(" -> ");
47096
+ for (const next of byKey.get(key)?.extends ?? []) {
47097
+ const found = visit2(next, [...path2, key]);
47098
+ if (found)
47099
+ return found;
47100
+ }
47101
+ return void 0;
47102
+ };
47103
+ for (const library of libraries) {
47104
+ const found = visit2(library.key, []);
47105
+ if (found)
47106
+ return found;
47107
+ }
47108
+ return void 0;
47109
+ }
47110
+ var SHARED_KEYS = ["designSystem", "modes", "copy", "interactive", "systemBars", "budget"];
47111
+ var sharedShape = {
47112
+ designSystem: designSystemSchema.optional(),
47113
+ modes: configShape.shape.modes.unwrap().optional(),
47114
+ copy: copySchema.optional(),
47115
+ interactive: configShape.shape.interactive.unwrap().optional(),
47116
+ systemBars: configShape.shape.systemBars,
47117
+ budget: budgetSchema.optional()
47118
+ };
47119
+ var PRODUCT_NAME = /^[A-Z0-9][A-Za-z0-9]*( [A-Z0-9][A-Za-z0-9]*)*$/;
47120
+ var workspaceProduct = external_exports.object({
47121
+ /** Title-case ASCII words: what people call the product and what its Figma files are named for. */
47122
+ name: external_exports.string().regex(PRODUCT_NAME, "title-case words of ASCII letters and digits, such as Back Office"),
47123
+ /** The directory of the product's design, relative to the config and inside its directory. */
47124
+ root: external_exports.string().min(1)
47125
+ }).strict();
47126
+ function cleanRoot(root) {
47127
+ if (root.includes("\\"))
47128
+ return { problem: "use forward slashes" };
47129
+ if (root.startsWith("/"))
47130
+ return { problem: "a root is relative to the config, not absolute" };
47131
+ const path2 = posix.normalize(root).replace(/\/$/, "");
47132
+ if (path2 === ".." || path2.startsWith("../"))
47133
+ return { problem: "a root stays inside the config's directory" };
47134
+ return { path: path2 };
47135
+ }
47136
+ function overlaps(a, b) {
47137
+ return a === "." || b === "." || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
47138
+ }
47139
+ var workspaceConfigSchema = external_exports.object({
47140
+ ...sharedShape,
47141
+ products: external_exports.array(workspaceProduct).min(1).optional(),
47142
+ product: external_exports.string().min(1).optional(),
47143
+ figma: files.optional(),
47144
+ targets: external_exports.array(targetSchema).optional(),
47145
+ recipe: recipeSchema.optional()
47146
+ }).strict().superRefine((config2, ctx) => {
47147
+ const { products, ...single } = config2;
47148
+ if (products === void 0) {
47149
+ const result = zakuConfigSchema.safeParse(single);
47150
+ if (!result.success) {
47151
+ for (const issue2 of result.error.issues) {
47152
+ ctx.addIssue({ code: "custom", path: issue2.path, message: issue2.message });
47153
+ }
47154
+ }
47155
+ return;
47156
+ }
47157
+ for (const key of ["product", "figma", "targets", "recipe"]) {
47158
+ if (config2[key] !== void 0) {
47159
+ ctx.addIssue({
47160
+ code: "custom",
47161
+ path: [key],
47162
+ message: `a workspace that lists products keeps ${key} in each product`
47163
+ });
47164
+ }
47165
+ }
47166
+ const names = /* @__PURE__ */ new Set();
47167
+ const roots = [];
47168
+ products.forEach((product, index) => {
47169
+ if (names.has(product.name)) {
47170
+ ctx.addIssue({
47171
+ code: "custom",
47172
+ path: ["products", index, "name"],
47173
+ message: `a second product named ${product.name}`
47174
+ });
47175
+ }
47176
+ names.add(product.name);
47177
+ const root = cleanRoot(product.root);
47178
+ if ("problem" in root) {
47179
+ ctx.addIssue({ code: "custom", path: ["products", index, "root"], message: root.problem });
47180
+ return;
47181
+ }
47182
+ const clash = roots.find((other) => overlaps(other, root.path));
47183
+ if (clash !== void 0) {
47184
+ ctx.addIssue({
47185
+ code: "custom",
47186
+ path: ["products", index, "root"],
47187
+ message: `${product.root} overlaps ${clash}, which another product holds`
47188
+ });
47189
+ }
47190
+ roots.push(root.path);
47191
+ });
47192
+ });
47193
+ var productConfigSchema = external_exports.object({
47194
+ ...sharedShape,
47195
+ figma: files.default({}),
47196
+ targets: external_exports.array(targetSchema).min(1),
47197
+ recipe: recipeSchema.optional()
47198
+ }).strict();
47199
+ function mergeProductConfig(shared, name, product) {
47200
+ const own2 = productConfigSchema.safeParse(product);
47201
+ if (!own2.success)
47202
+ throw new DesignFileInvalid(`zaku.yaml of ${name}`, issueLines(own2.error));
47203
+ const merged = { ...own2.data, product: name };
47204
+ for (const key of SHARED_KEYS)
47205
+ merged[key] = own2.data[key] ?? shared[key];
47206
+ const result = zakuConfigSchema.safeParse(merged);
47207
+ if (!result.success)
47208
+ throw new DesignFileInvalid(`zaku.yaml of ${name}`, issueLines(result.error));
47209
+ return result.data;
47210
+ }
47211
+ function issueLines(error62) {
47212
+ return error62.issues.map((issue2) => `${issue2.path.map(String).join(".") || "(root)"}: ${issue2.message}`);
47213
+ }
47214
+
46956
47215
  // packages/zaku/dist/lib/schema/json-schemas.js
46957
47216
  function designJsonSchemas() {
46958
47217
  return {
46959
47218
  "zaku.schema.json": external_exports.toJSONSchema(zakuConfigSchema, { io: "input" }),
47219
+ "zaku-config.schema.json": external_exports.toJSONSchema(workspaceConfigSchema, { io: "input" }),
46960
47220
  "map.schema.json": external_exports.toJSONSchema(featureMapSchema, { io: "input" })
46961
47221
  };
46962
47222
  }
@@ -47000,22 +47260,8 @@ RecordBudgetCallsHandler = __decorate2([
47000
47260
  __param2(0, (0, import_tsyringe6.inject)(TOKENS.BUDGET_LEDGER))
47001
47261
  ], RecordBudgetCallsHandler);
47002
47262
 
47003
- // packages/zaku/dist/lib/repositories/design-config-repository.js
47004
- var import_tsyringe7 = __toESM(require_cjs(), 1);
47005
- var DesignConfigRepository = class DesignConfigRepository2 {
47006
- read(root) {
47007
- return readDesignFile(designPaths(root).config, zakuConfigSchema);
47008
- }
47009
- readOptional(root) {
47010
- return readOptionalDesignFile(designPaths(root).config, zakuConfigSchema);
47011
- }
47012
- };
47013
- DesignConfigRepository = __decorate2([
47014
- (0, import_tsyringe7.injectable)({ token: TOKENS.DESIGN_CONFIG_REPOSITORY })
47015
- ], DesignConfigRepository);
47016
-
47017
47263
  // packages/zaku/dist/lib/repositories/token-document-repository.js
47018
- var import_tsyringe8 = __toESM(require_cjs(), 1);
47264
+ var import_tsyringe7 = __toESM(require_cjs(), 1);
47019
47265
  var TokenDocumentRepository = class TokenDocumentRepository2 {
47020
47266
  async save(root, doc) {
47021
47267
  const path2 = designPaths(root).tokens;
@@ -47024,11 +47270,11 @@ var TokenDocumentRepository = class TokenDocumentRepository2 {
47024
47270
  }
47025
47271
  };
47026
47272
  TokenDocumentRepository = __decorate2([
47027
- (0, import_tsyringe8.injectable)({ token: TOKENS.TOKEN_DOCUMENT_REPOSITORY })
47273
+ (0, import_tsyringe7.injectable)({ token: TOKENS.TOKEN_DOCUMENT_REPOSITORY })
47028
47274
  ], TokenDocumentRepository);
47029
47275
 
47030
47276
  // packages/zaku/dist/lib/repositories/recipe-repository.js
47031
- var import_tsyringe9 = __toESM(require_cjs(), 1);
47277
+ var import_tsyringe8 = __toESM(require_cjs(), 1);
47032
47278
  var RecipeRepository = class RecipeRepository2 {
47033
47279
  async save(root, doc) {
47034
47280
  const path2 = designPaths(root).recipe;
@@ -47037,11 +47283,11 @@ var RecipeRepository = class RecipeRepository2 {
47037
47283
  }
47038
47284
  };
47039
47285
  RecipeRepository = __decorate2([
47040
- (0, import_tsyringe9.injectable)({ token: TOKENS.RECIPE_REPOSITORY })
47286
+ (0, import_tsyringe8.injectable)({ token: TOKENS.RECIPE_REPOSITORY })
47041
47287
  ], RecipeRepository);
47042
47288
 
47043
47289
  // packages/zaku/dist/lib/repositories/json-schema-files.js
47044
- var import_tsyringe10 = __toESM(require_cjs(), 1);
47290
+ var import_tsyringe9 = __toESM(require_cjs(), 1);
47045
47291
  import { mkdir as mkdir3 } from "node:fs/promises";
47046
47292
  import { join as join7 } from "node:path";
47047
47293
  var JsonSchemaFiles = class JsonSchemaFiles2 {
@@ -47057,11 +47303,11 @@ var JsonSchemaFiles = class JsonSchemaFiles2 {
47057
47303
  }
47058
47304
  };
47059
47305
  JsonSchemaFiles = __decorate2([
47060
- (0, import_tsyringe10.injectable)({ token: TOKENS.JSON_SCHEMA_FILES })
47306
+ (0, import_tsyringe9.injectable)({ token: TOKENS.JSON_SCHEMA_FILES })
47061
47307
  ], JsonSchemaFiles);
47062
47308
 
47063
47309
  // packages/zaku/dist/lib/repositories/source-files.js
47064
- var import_tsyringe11 = __toESM(require_cjs(), 1);
47310
+ var import_tsyringe10 = __toESM(require_cjs(), 1);
47065
47311
  import { readFile as readFile4 } from "node:fs/promises";
47066
47312
  var SourceFiles = class SourceFiles2 {
47067
47313
  readText(path2) {
@@ -47072,7 +47318,7 @@ var SourceFiles = class SourceFiles2 {
47072
47318
  }
47073
47319
  };
47074
47320
  SourceFiles = __decorate2([
47075
- (0, import_tsyringe11.injectable)({ token: TOKENS.SOURCE_FILES })
47321
+ (0, import_tsyringe10.injectable)({ token: TOKENS.SOURCE_FILES })
47076
47322
  ], SourceFiles);
47077
47323
 
47078
47324
  // packages/zaku/dist/lib/repositories/budget-ledger.js
@@ -47159,7 +47405,8 @@ function printGraph(config2, snapshots) {
47159
47405
  ...graph.kitsFor(target).map((kit) => `${kit.key} ${kit.family} kit`)
47160
47406
  ];
47161
47407
  const branch = graph.branchOf(target);
47162
- lines.push(` ${target.name} (${target.id}, ${branch ? `branch ${branch}` : "no branch"}, ${graph.platformOf(target)}): ${files2.join(", ") || "nothing"}`);
47408
+ const product = graph.products.find((file2) => file2.family === target.family)?.key;
47409
+ lines.push(` ${target.name} (${target.id}, ${branch ? `branch ${branch}` : "no branch"}, ${product ? `product ${product}` : "no product"}): ${files2.join(", ") || "nothing"}`);
47163
47410
  }
47164
47411
  lines.push("files");
47165
47412
  const extendedBy = (key) => graph.libraries.filter((library) => library.extends.includes(key)).map((library) => library.key).join(", ") || "nothing";
@@ -47173,81 +47420,6 @@ function printGraph(config2, snapshots) {
47173
47420
  return lines;
47174
47421
  }
47175
47422
 
47176
- // packages/zaku/dist/lib/adapters/figma-rest.js
47177
- var FigmaRateLimited = class extends Error {
47178
- url;
47179
- constructor(url2) {
47180
- super(`Figma answered 429 twice for ${url2}; wait and run again`);
47181
- this.url = url2;
47182
- this.name = "FigmaRateLimited";
47183
- }
47184
- };
47185
- var FigmaRequestFailed = class extends Error {
47186
- url;
47187
- status;
47188
- constructor(url2, status, body) {
47189
- super(`Figma answered ${status} for ${url2}: ${body.slice(0, 200)}`);
47190
- this.url = url2;
47191
- this.status = status;
47192
- this.name = "FigmaRequestFailed";
47193
- }
47194
- };
47195
- var BASE = "https://api.figma.com/v1";
47196
- var BATCH = 50;
47197
- var MAX_WAIT_MS = 12e4;
47198
- function createFigmaRest(options) {
47199
- const send = options.fetch ?? globalThis.fetch;
47200
- const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
47201
- const onCall = options.onCall ?? (() => void 0);
47202
- async function call(url2) {
47203
- for (let attempt = 0; attempt < 2; attempt += 1) {
47204
- const res = await send(url2, { headers: { "X-Figma-Token": options.token } });
47205
- onCall();
47206
- if (res.status === 429) {
47207
- if (attempt === 1)
47208
- break;
47209
- const seconds = Number(res.headers.get("Retry-After") ?? "60");
47210
- await sleep(Math.min(Number.isFinite(seconds) ? seconds * 1e3 : 6e4, MAX_WAIT_MS));
47211
- continue;
47212
- }
47213
- if (!res.ok)
47214
- throw new FigmaRequestFailed(url2, res.status, await res.text());
47215
- return await res.json();
47216
- }
47217
- throw new FigmaRateLimited(url2);
47218
- }
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
- }
47224
- return {
47225
- file: (fileKey3, depth) => call(`${BASE}/files/${fileKey3}?depth=${depth}`),
47226
- async nodes(fileKey3, ids) {
47227
- const merged = { version: "", nodes: {} };
47228
- for (const chunk of chunks(ids)) {
47229
- const part = await call(`${BASE}/files/${fileKey3}/nodes?ids=${encodeURIComponent(chunk.join(","))}`);
47230
- merged.version = part.version;
47231
- Object.assign(merged.nodes, part.nodes);
47232
- }
47233
- return merged;
47234
- },
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) };
47238
- },
47239
- components,
47240
- async images(fileKey3, ids) {
47241
- const merged = {};
47242
- for (const chunk of chunks(ids)) {
47243
- const part = await call(`${BASE}/images/${fileKey3}?ids=${encodeURIComponent(chunk.join(","))}&format=png&scale=1`);
47244
- Object.assign(merged, part.images);
47245
- }
47246
- return merged;
47247
- }
47248
- };
47249
- }
47250
-
47251
47423
  // packages/zaku/dist/lib/adapters/figma-variables-script.js
47252
47424
  async function exportVariables(figma, options) {
47253
47425
  const collections = [...await figma.variables.getLocalVariableCollectionsAsync()];
@@ -47952,9 +48124,9 @@ async function startPluginBridge(options = {}) {
47952
48124
  if (!socket)
47953
48125
  return Promise.resolve({ ok: false, error: `file ${fileKey3} is not open in Figma with the zaku plugin` });
47954
48126
  const id2 = `c${++next}`;
47955
- return new Promise((resolve3) => {
48127
+ return new Promise((resolve4) => {
47956
48128
  const timer = setTimeout(() => settle2(id2, { ok: false, error: `no answer from the zaku panel in ${timeoutMs / 1e3} s; ${PARTIAL}` }), timeoutMs);
47957
- pending.set(id2, { socket, resolve: resolve3, timer });
48129
+ pending.set(id2, { socket, resolve: resolve4, timer });
47958
48130
  send(socket, { ...message, id: id2 });
47959
48131
  });
47960
48132
  }
@@ -48273,6 +48445,71 @@ var copy = ({ config: config2, maps, outlines }) => {
48273
48445
  return { findings };
48274
48446
  };
48275
48447
 
48448
+ // packages/zaku/dist/lib/domain/file-labels.js
48449
+ var FAMILY_LABELS = {
48450
+ web: "Web",
48451
+ ios: "iOS",
48452
+ android: "Android",
48453
+ macos: "macOS",
48454
+ windows: "Windows",
48455
+ gnome: "Linux",
48456
+ kde: "Linux KDE"
48457
+ };
48458
+ var BRANCH_LABELS = { ui: "UI", native: "Native" };
48459
+ function branchLabel(branches) {
48460
+ const [only2] = branches;
48461
+ return branches.length === 1 && only2 ? BRANCH_LABELS[only2] : null;
48462
+ }
48463
+ function expectedFileName(product, role) {
48464
+ if (role.kind !== "library") {
48465
+ const family = `${product} - ${FAMILY_LABELS[role.family]}`;
48466
+ return role.kind === "prototype" ? `${family} Prototype` : family;
48467
+ }
48468
+ if (role.layer === "foundations")
48469
+ return null;
48470
+ const branch = branchLabel(role.branches);
48471
+ return branch ? `${product} - ${branch} Components` : `${product} - Components`;
48472
+ }
48473
+ function fileRoleLabel(role) {
48474
+ if (role.kind !== "library")
48475
+ return `${FAMILY_LABELS[role.family]} ${role.kind} file`;
48476
+ const branch = branchLabel(role.branches);
48477
+ return branch ? `${branch} ${role.layer} library` : `${role.layer} library`;
48478
+ }
48479
+
48480
+ // packages/zaku/dist/lib/domain/checks/file-names.js
48481
+ var fileNames = (input2) => {
48482
+ if (input2.fileNames === null)
48483
+ return { findings: [] };
48484
+ const graph = designGraph(input2.config);
48485
+ if (graph.legacy)
48486
+ return { findings: [] };
48487
+ const roles = [
48488
+ ...graph.products.map((file2) => [file2.key, { kind: "product", family: file2.family }]),
48489
+ ...graph.prototypes.map((file2) => [file2.key, { kind: "prototype", family: file2.family }]),
48490
+ ...graph.libraries.map((file2) => [
48491
+ file2.key,
48492
+ { kind: "library", layer: file2.layer, branches: file2.branches }
48493
+ ])
48494
+ ];
48495
+ const names = new Map(input2.fileNames.map((file2) => [file2.key, file2.name]));
48496
+ const { product } = input2.config;
48497
+ const findings = [];
48498
+ for (const [key, role] of roles) {
48499
+ const actual = names.get(key);
48500
+ const expected = expectedFileName(product, role);
48501
+ if (actual === void 0 || expected === null || actual === expected)
48502
+ continue;
48503
+ const label2 = fileRoleLabel(role);
48504
+ const article = /^(iOS|Android) /.test(label2) ? "an" : "a";
48505
+ findings.push({
48506
+ check: "file-names",
48507
+ message: `${key} is named ${actual}; ${article} ${label2} of ${product} is named ${expected}; rename it in Figma, then run zaku outline`
48508
+ });
48509
+ }
48510
+ return { findings };
48511
+ };
48512
+
48276
48513
  // packages/zaku/dist/lib/domain/checks/coverage.js
48277
48514
  function frameKey(feature, name) {
48278
48515
  return `${feature}
@@ -48482,8 +48719,8 @@ var component = ({ config: config2, outlines, productComponents }) => {
48482
48719
  const findings = [];
48483
48720
  const graph = designGraph(config2);
48484
48721
  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) ?? []));
48722
+ const families2 = graph.products.filter((product) => product.key === drawn.source).map((product) => product.family);
48723
+ const branches = new Set(families2.flatMap((family) => graph.targetsOf(family)).flatMap((target) => graph.branchOf(target) ?? []));
48487
48724
  const homes = graph.libraries.filter((library) => library.layer === "components" && library.branches.some((branch) => branches.has(branch))).map((library) => library.key);
48488
48725
  findings.push({
48489
48726
  check: "component",
@@ -49470,6 +49707,7 @@ var ALL_CHECKS = {
49470
49707
  "system-bars": systemBars,
49471
49708
  placement,
49472
49709
  naming,
49710
+ "file-names": fileNames,
49473
49711
  component,
49474
49712
  prototype
49475
49713
  };
@@ -49510,6 +49748,7 @@ var CHECK_IDS = [
49510
49748
  "empty",
49511
49749
  "documentation",
49512
49750
  "naming",
49751
+ "file-names",
49513
49752
  "component",
49514
49753
  "prototype"
49515
49754
  ];
@@ -49627,17 +49866,10 @@ async function optional3(read, findings) {
49627
49866
  throw error62;
49628
49867
  }
49629
49868
  }
49630
- async function loadDesign(root) {
49631
- const paths = designPaths(root);
49869
+ async function loadDesign(product) {
49870
+ const paths = designPaths(product.root);
49632
49871
  const findings = [];
49633
- let config2;
49634
- try {
49635
- config2 = await readDesignFile(paths.config, zakuConfigSchema);
49636
- } catch (error62) {
49637
- if (error62 instanceof DesignFileInvalid)
49638
- return { input: null, findings: [schemaFinding(error62)] };
49639
- throw error62;
49640
- }
49872
+ const { config: config2 } = product;
49641
49873
  const { maps, errors: mapErrors } = await loadMaps(paths.maps);
49642
49874
  findings.push(...mapErrors.map(schemaFinding));
49643
49875
  for (const key of indexTitles(maps).duplicates) {
@@ -49648,7 +49880,7 @@ async function loadDesign(root) {
49648
49880
  message: `two screens share the title ${title}; a frame name could not tell them apart`
49649
49881
  });
49650
49882
  }
49651
- const { outlines, flows, unmapped, errors: outlineErrors } = await loadOutlines(paths.outline);
49883
+ const { outlines, flows, unmapped, files: files2, errors: outlineErrors } = await loadOutlines(paths.outline);
49652
49884
  findings.push(...outlineErrors.map(schemaFinding));
49653
49885
  const { libraries, kits, errors: snapshotErrors } = await readDesignSnapshots(paths, config2);
49654
49886
  findings.push(...snapshotErrors.map(schemaFinding));
@@ -49665,15 +49897,80 @@ async function loadDesign(root) {
49665
49897
  libraries,
49666
49898
  kits,
49667
49899
  tokens,
49668
- recipe: recipe2
49900
+ recipe: recipe2,
49901
+ fileNames: files2?.files ?? null
49669
49902
  },
49670
49903
  findings
49671
49904
  };
49672
49905
  }
49673
49906
 
49907
+ // packages/zaku/dist/lib/repositories/workspace.js
49908
+ import { readFile as readFile6 } from "node:fs/promises";
49909
+ import { dirname as dirname4, join as join9, resolve as resolve2 } from "node:path";
49910
+ var WORKSPACE_CONFIG = "zaku.config.yaml";
49911
+ async function readOptionalText(path2) {
49912
+ try {
49913
+ return await readFile6(path2, "utf8");
49914
+ } catch (error62) {
49915
+ if (error62.code === "ENOENT")
49916
+ return null;
49917
+ throw error62;
49918
+ }
49919
+ }
49920
+ function only(products, name) {
49921
+ if (name === void 0)
49922
+ return products;
49923
+ const named = products.filter((product) => product.name === name);
49924
+ if (named.length === 0)
49925
+ throw new ProductUnknown(name, products.map((product) => product.name));
49926
+ return named;
49927
+ }
49928
+ async function readWorkspace(cwd, options) {
49929
+ const configPath = resolve2(cwd, options.config ?? WORKSPACE_CONFIG);
49930
+ const text = await readOptionalText(configPath);
49931
+ if (text === null) {
49932
+ if (options.config !== void 0)
49933
+ throw new WorkspaceConfigMissing(configPath, null);
49934
+ const root = resolve2(cwd, "docs", "design");
49935
+ const legacyPath = designPaths(root).config;
49936
+ const config2 = await readOptionalDesignFile(legacyPath, zakuConfigSchema);
49937
+ if (config2 === null)
49938
+ throw new WorkspaceConfigMissing(configPath, legacyPath);
49939
+ const products2 = only([{ name: config2.product, root, config: config2 }], options.product);
49940
+ return { configPath: null, dir: resolve2(cwd), products: products2, legacy: true, budget: config2.budget };
49941
+ }
49942
+ const dir = dirname4(configPath);
49943
+ const workspace = parseDesignFile(configPath, text, workspaceConfigSchema);
49944
+ if (workspace.products === void 0) {
49945
+ const config2 = parseDesignFile(configPath, text, zakuConfigSchema);
49946
+ const products2 = only([{ name: config2.product, root: join9(dir, "design"), config: config2 }], options.product);
49947
+ return { configPath, dir, products: products2, legacy: false, budget: config2.budget };
49948
+ }
49949
+ const listed = workspace.products.map((product) => ({ name: product.name, root: resolve2(dir, product.root) }));
49950
+ const products = [];
49951
+ for (const { name, root } of only(listed, options.product)) {
49952
+ const path2 = designPaths(root).config;
49953
+ try {
49954
+ const own2 = await readOptionalDesignFile(path2, external_exports.unknown());
49955
+ if (own2 === null)
49956
+ throw new ProductConfigMissing(path2, name);
49957
+ products.push({ name, root, config: mergeProductConfig(workspace, name, own2) });
49958
+ } catch (error62) {
49959
+ if (error62 instanceof ProductConfigMissing)
49960
+ products.push({ name, root, refusal: error62 });
49961
+ else if (error62 instanceof DesignFileInvalid) {
49962
+ const refusal = error62.path === path2 ? error62 : new DesignFileInvalid(path2, error62.issues);
49963
+ products.push({ name, root, refusal });
49964
+ } else
49965
+ throw error62;
49966
+ }
49967
+ }
49968
+ return { configPath, dir, products, legacy: false, budget: workspace.budget ?? budgetSchema.parse({}) };
49969
+ }
49970
+
49674
49971
  // apps/zaku-cli/src/dependencies.ts
49675
49972
  function compose(seams) {
49676
- const scope = import_tsyringe12.container.createChildContainer();
49973
+ const scope = import_tsyringe11.container.createChildContainer();
49677
49974
  scope.register(TOKENS.CLOCK, { useValue: { now: seams.now } });
49678
49975
  scope.register(TOKENS.RUN_COMMAND, { useValue: seams.run });
49679
49976
  scope.register(TOKENS.BUDGET_LEDGER, {
@@ -49702,6 +49999,9 @@ function refusalOf(error62) {
49702
49999
  if (error62 instanceof FigmaFileKeyMissing) return { code: 3, line: error62.message };
49703
50000
  if (error62 instanceof FigmaTokenMissing) return { code: 3, line: error62.message };
49704
50001
  if (error62 instanceof LibraryKeyMissing) return { code: 3, line: error62.message };
50002
+ if (error62 instanceof WorkspaceConfigMissing) return { code: 3, line: error62.message };
50003
+ if (error62 instanceof ProductConfigMissing) return { code: 3, line: error62.message };
50004
+ if (error62 instanceof ProductUnknown || error62 instanceof ProductRequired) return { code: 3, line: error62.message };
49705
50005
  if (error62 instanceof RecipePageFailed) return { code: 2, line: `zaku recipe: ${error62.message}` };
49706
50006
  return null;
49707
50007
  }
@@ -57211,7 +57511,7 @@ var Protocol = class {
57211
57511
  return;
57212
57512
  }
57213
57513
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
57214
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
57514
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
57215
57515
  options?.signal?.throwIfAborted();
57216
57516
  }
57217
57517
  } catch (error62) {
@@ -57228,7 +57528,7 @@ var Protocol = class {
57228
57528
  */
57229
57529
  request(request, resultSchema, options) {
57230
57530
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
57231
- return new Promise((resolve3, reject) => {
57531
+ return new Promise((resolve4, reject) => {
57232
57532
  const earlyReject = (error62) => {
57233
57533
  reject(error62);
57234
57534
  };
@@ -57306,7 +57606,7 @@ var Protocol = class {
57306
57606
  if (!parseResult.success) {
57307
57607
  reject(parseResult.error);
57308
57608
  } else {
57309
- resolve3(parseResult.data);
57609
+ resolve4(parseResult.data);
57310
57610
  }
57311
57611
  } catch (error62) {
57312
57612
  reject(error62);
@@ -57568,12 +57868,12 @@ var Protocol = class {
57568
57868
  }
57569
57869
  } catch {
57570
57870
  }
57571
- return new Promise((resolve3, reject) => {
57871
+ return new Promise((resolve4, reject) => {
57572
57872
  if (signal.aborted) {
57573
57873
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
57574
57874
  return;
57575
57875
  }
57576
- const timeoutId = setTimeout(resolve3, interval);
57876
+ const timeoutId = setTimeout(resolve4, interval);
57577
57877
  signal.addEventListener("abort", () => {
57578
57878
  clearTimeout(timeoutId);
57579
57879
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -58713,7 +59013,7 @@ var McpServer = class {
58713
59013
  let task = createTaskResult.task;
58714
59014
  const pollInterval = task.pollInterval ?? 5e3;
58715
59015
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
58716
- await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
59016
+ await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
58717
59017
  const updatedTask = await extra.taskStore.getTask(taskId);
58718
59018
  if (!updatedTask) {
58719
59019
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -59377,12 +59677,12 @@ var StdioServerTransport = class {
59377
59677
  this.onclose?.();
59378
59678
  }
59379
59679
  send(message) {
59380
- return new Promise((resolve3) => {
59680
+ return new Promise((resolve4) => {
59381
59681
  const json2 = serializeMessage(message);
59382
59682
  if (this._stdout.write(json2)) {
59383
- resolve3();
59683
+ resolve4();
59384
59684
  } else {
59385
- this._stdout.once("drain", resolve3);
59685
+ this._stdout.once("drain", resolve4);
59386
59686
  }
59387
59687
  });
59388
59688
  }
@@ -59391,11 +59691,11 @@ var StdioServerTransport = class {
59391
59691
  // apps/zaku-cli/src/mcp/server.ts
59392
59692
  import { mkdir as mkdir5 } from "node:fs/promises";
59393
59693
  import { tmpdir } from "node:os";
59394
- import { join as join11 } from "node:path";
59694
+ import { join as join12 } from "node:path";
59395
59695
 
59396
59696
  // apps/zaku-cli/src/mcp/screenshots.ts
59397
59697
  import { readdir as readdir4, rm as rm3, stat } from "node:fs/promises";
59398
- import { join as join9 } from "node:path";
59698
+ import { join as join10 } from "node:path";
59399
59699
  var KEPT_MS = 24 * 60 * 60 * 1e3;
59400
59700
  async function pruneScreenshots(dir, now) {
59401
59701
  let names;
@@ -59405,7 +59705,7 @@ async function pruneScreenshots(dir, now) {
59405
59705
  return;
59406
59706
  }
59407
59707
  for (const name of names.filter((entry) => entry.endsWith(".png"))) {
59408
- const path2 = join9(dir, name);
59708
+ const path2 = join10(dir, name);
59409
59709
  const written = await stat(path2).then(
59410
59710
  (stats) => stats.mtimeMs,
59411
59711
  () => now
@@ -59416,7 +59716,7 @@ async function pruneScreenshots(dir, now) {
59416
59716
 
59417
59717
  // apps/zaku-cli/src/mcp/tools.ts
59418
59718
  import { writeFile as writeFile5 } from "node:fs/promises";
59419
- import { join as join10 } from "node:path";
59719
+ import { join as join11 } from "node:path";
59420
59720
  var fileKey2 = external_exports.string().min(1).regex(/^[0-9a-zA-Z]{22,128}$/).describe(
59421
59721
  "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."
59422
59722
  );
@@ -59460,7 +59760,7 @@ function registerFigmaTools(server, bridge, options) {
59460
59760
  options.timeoutMs
59461
59761
  );
59462
59762
  if (!answer.ok) return { isError: true, content: [{ type: "text", text: answer.error }] };
59463
- const path2 = join10(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
59763
+ const path2 = join11(options.imageDir, `${id2.replace(":", "-")}-${Date.now()}.png`);
59464
59764
  await writeFile5(path2, Buffer.from(answer.png, "base64"));
59465
59765
  const text = JSON.stringify({
59466
59766
  path: path2,
@@ -59486,7 +59786,7 @@ async function serveMcp(log) {
59486
59786
  log(
59487
59787
  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}`
59488
59788
  );
59489
- const imageDir = join11(tmpdir(), "zaku-screenshots");
59789
+ const imageDir = join12(tmpdir(), "zaku-screenshots");
59490
59790
  await mkdir5(imageDir, { recursive: true });
59491
59791
  await pruneScreenshots(imageDir, Date.now());
59492
59792
  const server = new McpServer({ name: "zaku", version: "1" });
@@ -59503,59 +59803,175 @@ async function serveMcp(log) {
59503
59803
 
59504
59804
  // apps/zaku-cli/src/cli.ts
59505
59805
  var USE_FIGMA_CODE_LIMIT = 5e4;
59506
- var USAGE = "usage: zaku <check|outline|tokens|recipe|library|libraries|rules|budget|schema|mcp> [--root docs/design]";
59507
- async function sendOrRefuse(cli, io, finish, command) {
59508
- try {
59509
- return await cli.bus.send(command);
59510
- } catch (error62) {
59511
- const refusal = refusalOf(error62);
59512
- if (!refusal) throw error62;
59513
- io.err(refusal.line);
59514
- finish(refusal.code);
59515
- return null;
59516
- }
59517
- }
59806
+ var USAGE = "usage: zaku <check|outline|tokens|recipe|library|libraries|products|rules|budget|schema|mcp> [--config zaku.config.yaml] [--product <name>]";
59807
+ var LEGACY_WARNING = "zaku: read docs/design/zaku.yaml; move it to zaku.config.yaml at the repository root and the design to design/";
59518
59808
  async function recordRestCalls(cli, io) {
59519
59809
  const count = cli.restCalls();
59520
59810
  if (count === 0) return;
59521
59811
  const { seat, run } = seatAndRun(io.env);
59522
59812
  await cli.bus.send(new RecordBudgetCalls({ day: today(clock.now()), seat, run, kind: "rest", count }));
59523
59813
  }
59814
+ var CONFIG_OPTION = ["--config <path>", "the workspace config, when it is not zaku.config.yaml here"];
59815
+ var PRODUCT_OPTION = ["--product <name>", "only this product, by the name the workspace config gives it"];
59816
+ function warnLegacy(io, workspace) {
59817
+ if (workspace.legacy) io.err(LEGACY_WARNING);
59818
+ }
59819
+ async function workspaceOf(io, options) {
59820
+ const workspace = await readWorkspace(io.cwd, options);
59821
+ warnLegacy(io, workspace);
59822
+ return workspace;
59823
+ }
59824
+ function oneProduct(workspace, command) {
59825
+ const [product, ...rest] = workspace.products;
59826
+ if (product === void 0 || rest.length > 0) {
59827
+ throw new ProductRequired(
59828
+ command,
59829
+ workspace.products.map((each) => each.name)
59830
+ );
59831
+ }
59832
+ if ("refusal" in product) throw product.refusal;
59833
+ return product;
59834
+ }
59835
+ function onlyWithOne(workspace, command, option, given) {
59836
+ if (given !== void 0 && workspace.products.length > 1) {
59837
+ throw new ProductRequired(
59838
+ `${command} ${option}`,
59839
+ workspace.products.map((product) => product.name)
59840
+ );
59841
+ }
59842
+ }
59843
+ function productFailure(error62) {
59844
+ const refusal = refusalOf(error62);
59845
+ if (refusal) return refusal;
59846
+ if (error62 instanceof DesignFileInvalid || error62 instanceof FigmaRequestFailed) {
59847
+ return { code: 1, line: `zaku: ${error62.message}` };
59848
+ }
59849
+ throw error62;
59850
+ }
59851
+ function reportRefusal(io, error62) {
59852
+ const failure2 = productFailure(error62);
59853
+ io.err(failure2.line);
59854
+ return failure2.code;
59855
+ }
59856
+ async function forEachProduct(io, workspace, run) {
59857
+ let code = 0;
59858
+ for (const product of workspace.products) {
59859
+ if (workspace.products.length > 1) io.out(`== ${product.name}`);
59860
+ let exit;
59861
+ try {
59862
+ if ("refusal" in product) throw product.refusal;
59863
+ exit = await run(product);
59864
+ } catch (error62) {
59865
+ exit = reportRefusal(io, error62);
59866
+ }
59867
+ code = Math.max(code, exit);
59868
+ }
59869
+ return code;
59870
+ }
59524
59871
  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);
59872
+ program2.command("libraries").description("print the design graph: what each target draws from, and what each file extends and is extended by").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).action(async (options) => {
59873
+ const workspace = await workspaceOf(io, options);
59874
+ finish(
59875
+ await forEachProduct(io, workspace, async ({ root, config: config2 }) => {
59876
+ for (const line of printGraph(config2, await readDesignSnapshots(designPaths(root), config2))) io.out(line);
59877
+ return 0;
59878
+ })
59879
+ );
59880
+ });
59881
+ };
59882
+ var productsCommand = (program2, io, finish) => {
59883
+ program2.command("products").description("print each product the workspace config lists, with its design directory").option(...CONFIG_OPTION).option("--json", "print them as JSON").action(async (options) => {
59884
+ const workspace = await workspaceOf(io, options.config === void 0 ? {} : { config: options.config });
59885
+ const rows = workspace.products.map((product) => ({
59886
+ name: product.name,
59887
+ root: relative3(io.cwd, product.root) || "."
59888
+ }));
59889
+ if (options.json) io.out(JSON.stringify(rows, null, 2));
59890
+ else for (const row of rows) io.out(`${row.name} ${row.root}`);
59529
59891
  finish(0);
59530
59892
  });
59531
59893
  };
59532
59894
  var schemaCommand = (program2, io, finish, cli) => {
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) => {
59534
- const written = await cli.bus.send(new WriteSchemas({ out: resolve2(io.cwd, options.out) }));
59895
+ program2.command("schema").description("write the JSON Schemas an editor reads for the design files").option("--out <dir>", "where to write them; the workspace config's directory when left out").option(...CONFIG_OPTION).action(async (options) => {
59896
+ const out = options.out !== void 0 ? resolve3(io.cwd, options.out) : options.config !== void 0 ? dirname5(resolve3(io.cwd, options.config)) : resolve3(io.cwd);
59897
+ const written = await cli.bus.send(new WriteSchemas({ out }));
59535
59898
  for (const path2 of written) io.out(`wrote ${path2}`);
59536
59899
  finish(0);
59537
59900
  });
59538
59901
  };
59902
+ function checkExit(codes) {
59903
+ const rank = (code) => code === 1 ? 2 : code === 2 ? 1 : code;
59904
+ return codes.reduce((worst, code) => rank(code) > rank(worst) ? code : worst, 0);
59905
+ }
59906
+ async function checkProduct(product, ran) {
59907
+ let loaded;
59908
+ if ("refusal" in product) {
59909
+ if (!(product.refusal instanceof DesignFileInvalid)) return { refusal: product.refusal };
59910
+ loaded = { input: null, findings: [{ check: "schema", message: product.refusal.message }] };
59911
+ } else {
59912
+ try {
59913
+ loaded = await loadDesign(product);
59914
+ } catch (error62) {
59915
+ if (!(error62 instanceof DesignFileInvalid)) throw error62;
59916
+ loaded = { input: null, findings: [{ check: "schema", message: error62.message }] };
59917
+ }
59918
+ }
59919
+ const report = loaded.input ? runChecks(loaded.input, ALL_CHECKS) : { findings: [], notRun: [], passed: [] };
59920
+ report.findings.unshift(...loaded.findings);
59921
+ if (ran) report.findings.push(...ran.findings);
59922
+ const left = ran ? leftOut(ran) : [];
59923
+ return {
59924
+ report: { ...report, ...left.length > 0 ? { leftOut: left } : {} },
59925
+ code: loaded.input ? exitCode(report) : 1
59926
+ };
59927
+ }
59539
59928
  var checkCommand = (program2, io, finish) => {
59540
- program2.command("check").description("run every check over the committed design files").option("--root <dir>", "the design directory", "docs/design").option("--json", "print the report as JSON").option("--input <rules.json>", "what use_figma returned for zaku rules script, with or without --after").action(async (options) => {
59541
- const loaded = await loadDesign(resolve2(io.cwd, options.root));
59542
- const report = loaded.input ? runChecks(loaded.input, ALL_CHECKS) : { findings: [], notRun: [], passed: [] };
59543
- report.findings.unshift(...loaded.findings);
59544
- const ran = options.input ? parseDesignFile(options.input, await readFile6(resolve2(io.cwd, options.input), "utf8"), rulesAnswerSchema) : null;
59545
- if (ran && "rulesError" in ran) {
59546
- io.err(`zaku check: the rules did not run after the write: ${ran.rulesError}`);
59929
+ program2.command("check").description("run every check over each product's committed design files").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).option("--json", "print the report as JSON, keyed by product when there are several").option("--input <rules.json>", "what use_figma returned for zaku rules script, with or without --after").action(async (options) => {
59930
+ let workspace;
59931
+ try {
59932
+ workspace = await workspaceOf(io, options);
59933
+ } catch (error62) {
59934
+ if (!(error62 instanceof DesignFileInvalid)) throw error62;
59935
+ const report = { findings: [{ check: "schema", message: error62.message }], notRun: [], passed: [] };
59936
+ if (options.json) io.out(JSON.stringify(report, null, 2));
59937
+ else for (const line of formatReport(report)) io.out(line);
59938
+ finish(1);
59939
+ return;
59940
+ }
59941
+ onlyWithOne(workspace, "zaku check", "--input", options.input);
59942
+ const answer = options.input ? parseDesignFile(options.input, await readFile7(resolve3(io.cwd, options.input), "utf8"), rulesAnswerSchema) : null;
59943
+ if (answer && "rulesError" in answer) {
59944
+ io.err(`zaku check: the rules did not run after the write: ${answer.rulesError}`);
59547
59945
  finish(1);
59548
59946
  return;
59549
59947
  }
59550
- if (ran) report.findings.push(...ran.findings);
59551
- const left = ran ? leftOut(ran) : [];
59552
- if (options.json) io.out(JSON.stringify({ ...report, ...left.length > 0 ? { leftOut: left } : {} }, null, 2));
59553
- else for (const line of [...left, ...formatReport(report)]) io.out(line);
59554
- finish(loaded.input ? exitCode(report) : 1);
59948
+ const several = workspace.products.length > 1;
59949
+ const reports = {};
59950
+ const codes = [];
59951
+ for (const product of workspace.products) {
59952
+ if (several && !options.json) io.out(`== ${product.name}`);
59953
+ const checked = await checkProduct(product, answer);
59954
+ if ("refusal" in checked) {
59955
+ const failure2 = productFailure(checked.refusal);
59956
+ io.err(failure2.line);
59957
+ codes.push(failure2.code);
59958
+ if (options.json) reports[product.name] = { refusal: failure2.line };
59959
+ continue;
59960
+ }
59961
+ codes.push(checked.code);
59962
+ if (options.json) reports[product.name] = checked.report;
59963
+ else for (const line of [...checked.report.leftOut ?? [], ...formatReport(checked.report)]) io.out(line);
59964
+ }
59965
+ if (options.json) {
59966
+ const [only2] = Object.values(reports);
59967
+ if (several) io.out(JSON.stringify(reports, null, 2));
59968
+ else if (only2) io.out(JSON.stringify(only2, null, 2));
59969
+ }
59970
+ finish(checkExit(codes));
59555
59971
  });
59556
59972
  };
59557
59973
  async function productPlaywright(cwd) {
59558
- const require2 = createRequire(join12(cwd, "package.json"));
59974
+ const require2 = createRequire(join13(cwd, "package.json"));
59559
59975
  let resolved;
59560
59976
  try {
59561
59977
  resolved = require2.resolve("playwright");
@@ -59566,15 +59982,15 @@ async function productPlaywright(cwd) {
59566
59982
  return require2("playwright");
59567
59983
  }
59568
59984
  function fromProductTree(cwd, resolved) {
59569
- for (let dir = resolve2(cwd); ; dir = dirname4(dir)) {
59570
- if (resolved.startsWith(join12(dir, "node_modules") + sep)) return true;
59571
- if (dirname4(dir) === dir) return false;
59985
+ for (let dir = resolve3(cwd); ; dir = dirname5(dir)) {
59986
+ if (resolved.startsWith(join13(dir, "node_modules") + sep)) return true;
59987
+ if (dirname5(dir) === dir) return false;
59572
59988
  }
59573
59989
  }
59574
59990
  var runners = {
59575
59991
  run: execRun,
59576
59992
  loadPlaywright: productPlaywright,
59577
- rulesLibrary: () => readFile6(fileURLToPath(new URL("./rules.js", import.meta.url)), "utf8")
59993
+ rulesLibrary: () => readFile7(fileURLToPath(new URL("./rules.js", import.meta.url)), "utf8")
59578
59994
  };
59579
59995
  var mcpCommand = (program2, io, finish) => {
59580
59996
  program2.command("mcp").description(
@@ -59585,30 +60001,35 @@ var mcpCommand = (program2, io, finish) => {
59585
60001
  });
59586
60002
  };
59587
60003
  var tokensCommand = (program2, io, finish, cli) => {
59588
- program2.command("tokens").description("write the code's tokens to tokens.json, from the design system zaku.yaml names").option("--css <path>", "the stylesheet that declares :root and .dark (shadcn)").option("--root <dir>", "the design directory", "docs/design").action(async (options) => {
59589
- const command = new WriteTokens({
59590
- root: resolve2(io.cwd, options.root),
59591
- cwd: io.cwd,
59592
- css: options.css ? resolve2(io.cwd, options.css) : null
59593
- });
59594
- const written = await sendOrRefuse(cli, io, finish, command);
59595
- if (!written) return;
59596
- io.out(`wrote ${written.path}: ${written.colours} colours, ${written.from}`);
59597
- finish(0);
60004
+ program2.command("tokens").description("write the code's tokens to tokens.json, from the design system zaku.yaml names").option("--css <path>", "the stylesheet that declares :root and .dark (shadcn), over designSystem.shadcn.css").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).action(async (options) => {
60005
+ const workspace = await workspaceOf(io, options);
60006
+ onlyWithOne(workspace, "zaku tokens", "--css", options.css);
60007
+ finish(
60008
+ await forEachProduct(io, workspace, async ({ root, config: config2 }) => {
60009
+ const named = "shadcn" in config2.designSystem ? config2.designSystem.shadcn.css : void 0;
60010
+ const css = options.css ? resolve3(io.cwd, options.css) : named !== void 0 ? resolve3(workspace.dir, named) : null;
60011
+ const command = new WriteTokens({ root, config: config2, cwd: workspace.dir, css });
60012
+ const written = await cli.bus.send(command);
60013
+ io.out(`wrote ${written.path}: ${written.colours} colours, ${written.from}`);
60014
+ return 0;
60015
+ })
60016
+ );
59598
60017
  });
59599
60018
  };
59600
60019
  var recipeCommand = (program2, io, finish, cli) => {
59601
- program2.command("recipe").description("write what the code computes for every variant to recipe.json, from the product's recipe page").option("--url <url>", "the recipe page, when it is not zaku.yaml's recipe.url").option("--root <dir>", "the design directory", "docs/design").action(async (options) => {
59602
- const command = new WriteRecipe({
59603
- root: resolve2(io.cwd, options.root),
59604
- url: options.url ?? null
59605
- });
59606
- const written = await sendOrRefuse(cli, io, finish, command);
59607
- if (!written) return;
59608
- io.out(
59609
- `wrote ${written.path}: ${written.components} components, ${written.variants} variants, in light and dark`
60020
+ program2.command("recipe").description("write what the code computes for every variant to recipe.json, from the product's recipe page").option("--url <url>", "the recipe page, when it is not zaku.yaml's recipe.url").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).action(async (options) => {
60021
+ const workspace = await workspaceOf(io, options);
60022
+ onlyWithOne(workspace, "zaku recipe", "--url", options.url);
60023
+ finish(
60024
+ await forEachProduct(io, workspace, async ({ root, config: config2 }) => {
60025
+ const command = new WriteRecipe({ root, config: config2, url: options.url ?? null });
60026
+ const written = await cli.bus.send(command);
60027
+ io.out(
60028
+ `wrote ${written.path}: ${written.components} components, ${written.variants} variants, in light and dark`
60029
+ );
60030
+ return 0;
60031
+ })
59610
60032
  );
59611
- finish(0);
59612
60033
  });
59613
60034
  };
59614
60035
  var clock = { now: () => /* @__PURE__ */ new Date() };
@@ -59629,10 +60050,21 @@ var budgetCommand = (program2, io, finish, cli) => {
59629
60050
  await cli.bus.send(new RecordBudgetCalls({ day, seat, run, kind: options.kind, count: options.count }));
59630
60051
  finish(0);
59631
60052
  });
59632
- budget.command("status").description("exit 4 when the budget refuses another MCP call").option("--root <dir>", "the design directory", "docs/design").action(async (options) => {
60053
+ budget.command("status").description("exit 4 when the budget refuses another MCP call").option(...CONFIG_OPTION).action(async (options) => {
59633
60054
  const { path: path2, seat, run, day } = ledger();
59634
- const config2 = await readOptionalDesignFile(designPaths(resolve2(io.cwd, options.root)).config, zakuConfigSchema);
59635
- const allowance = mcpAllowance(await readLedger(path2, day), seat, run, config2?.budget ?? budgetSchema.parse({}));
60055
+ let budgets;
60056
+ try {
60057
+ const workspace = await workspaceOf(io, options.config === void 0 ? {} : { config: options.config });
60058
+ budgets = workspace.products.map(
60059
+ (product) => "refusal" in product ? workspace.budget : product.config.budget
60060
+ );
60061
+ } catch (error62) {
60062
+ if (!(error62 instanceof WorkspaceConfigMissing) || options.config !== void 0) throw error62;
60063
+ budgets = [budgetSchema.parse({})];
60064
+ }
60065
+ const read = await readLedger(path2, day);
60066
+ const allowances = budgets.map((budget2) => mcpAllowance(read, seat, run, budget2));
60067
+ const allowance = allowances.reduce((chosen, each) => chosen.ok && !each.ok ? each : chosen);
59636
60068
  io.out(
59637
60069
  `mcp ${allowance.usedToday} today, ${allowance.usedThisRun} this run${allowance.reason ? `: ${allowance.reason}` : ""}`
59638
60070
  );
@@ -59640,8 +60072,7 @@ var budgetCommand = (program2, io, finish, cli) => {
59640
60072
  });
59641
60073
  };
59642
60074
  var outlineCommand = (program2, io, finish, cli) => {
59643
- program2.command("outline").description("read the product file's frames into docs/design/outline").option("--root <dir>", "the design directory", "docs/design").option("--frames <ids>", "only these frame ids, comma separated").action(async (options) => {
59644
- const root = resolve2(io.cwd, options.root);
60075
+ program2.command("outline").description("read each product file's frames into its design's outline/").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).option("--frames <ids>", "only these frame ids, comma separated").action(async (options) => {
59645
60076
  const report = (summary) => io.out(
59646
60077
  summary.skipped ? "outline is current; nothing read" : `read ${summary.read} frames, wrote ${summary.written.length} outlines, ${summary.unmapped} unmapped`
59647
60078
  );
@@ -59651,9 +60082,15 @@ var outlineCommand = (program2, io, finish, cli) => {
59651
60082
  return;
59652
60083
  }
59653
60084
  try {
60085
+ const workspace = await workspaceOf(io, options);
60086
+ onlyWithOne(workspace, "zaku outline", "--frames", options.frames);
59654
60087
  const frames = options.frames ? options.frames.split(",") : null;
59655
- report(await cli.bus.send(new WriteOutline({ root, frames })));
59656
- finish(0);
60088
+ finish(
60089
+ await forEachProduct(io, workspace, async ({ root, config: config2 }) => {
60090
+ report(await cli.bus.send(new WriteOutline({ root, config: config2, frames })));
60091
+ return 0;
60092
+ })
60093
+ );
59657
60094
  } finally {
59658
60095
  await recordRestCalls(cli, io);
59659
60096
  }
@@ -59661,8 +60098,8 @@ var outlineCommand = (program2, io, finish, cli) => {
59661
60098
  };
59662
60099
  var libraryCommand = (program2, io, finish, cli) => {
59663
60100
  const library = program2.command("library").description("snapshot the library's components and tokens");
59664
- library.command("script").description("print the Plugin API code that exports the library variables, for use_figma").option("--root <dir>", "the design directory", "docs/design").option("--local", "also list the file's own components, for a library in the product file").option("--page <name>", "with --local, list only this page; one use_figma call per page").action(async (options) => {
59665
- const config2 = await readDesignFile(designPaths(resolve2(io.cwd, options.root)).config, zakuConfigSchema);
60101
+ library.command("script").description("print the Plugin API code that exports the library variables, for use_figma").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).option("--local", "also list the file's own components, for a library in the product file").option("--page <name>", "with --local, list only this page; one use_figma call per page").action(async (options) => {
60102
+ const { config: config2 } = oneProduct(await workspaceOf(io, options), "zaku library script");
59666
60103
  io.out(
59667
60104
  libraryExportScript({
59668
60105
  combos: [config2.modes],
@@ -59694,23 +60131,13 @@ var libraryCommand = (program2, io, finish, cli) => {
59694
60131
  );
59695
60132
  library.command("save").description(
59696
60133
  "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(
60134
+ ).option(...CONFIG_OPTION).option(...PRODUCT_OPTION).option("--key <key>", "the library or kit to save, as zaku.yaml lists it; needed when it lists several").option(
59698
60135
  "--input <variables.json>",
59699
60136
  "what use_figma returned for zaku library script; once per page export for a library in the product file",
59700
60137
  (value, previous = []) => [...previous, value]
59701
60138
  ).action(async (options) => {
59702
- const root = resolve2(io.cwd, options.root);
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
- }
60139
+ const { root, config: config2 } = oneProduct(await workspaceOf(io, options), "zaku library save");
60140
+ const target = librarySaveTarget(config2, options.key);
59714
60141
  if (target.kind === "library" && (!options.input || options.input.length === 0)) {
59715
60142
  io.err("zaku library save needs --input, the variables use_figma returned for zaku library script");
59716
60143
  finish(3);
@@ -59724,7 +60151,7 @@ var libraryCommand = (program2, io, finish, cli) => {
59724
60151
  const part = target.kind === "library" ? mergeParts(
59725
60152
  await Promise.all(
59726
60153
  (options.input ?? []).map(
59727
- async (input2) => parseDesignFile(input2, await readFile6(resolve2(io.cwd, input2), "utf8"), variablePartSchema)
60154
+ async (input2) => parseDesignFile(input2, await readFile7(resolve3(io.cwd, input2), "utf8"), variablePartSchema)
59728
60155
  )
59729
60156
  )
59730
60157
  ) : void 0;
@@ -59732,6 +60159,7 @@ var libraryCommand = (program2, io, finish, cli) => {
59732
60159
  const saved = await cli.bus.send(
59733
60160
  new SaveLibrary({
59734
60161
  root,
60162
+ config: config2,
59735
60163
  ...options.key !== void 0 ? { key: options.key } : {},
59736
60164
  ...part ? { part } : {}
59737
60165
  })
@@ -59760,13 +60188,13 @@ var libraryCommand = (program2, io, finish, cli) => {
59760
60188
  };
59761
60189
  var rulesCommand = (program2, io, finish) => {
59762
60190
  const rules = program2.command("rules").description("zaku's rule checks, for a run that reaches Figma through use_figma");
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) => {
60191
+ rules.command("script").description("print the Plugin API code that runs the rules over a page or a node, for use_figma").option(...CONFIG_OPTION).option(...PRODUCT_OPTION).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) => {
59764
60192
  if (options.page === void 0 === (options.node === void 0)) {
59765
60193
  io.err("zaku rules script needs --page or --node, one of them");
59766
60194
  finish(3);
59767
60195
  return;
59768
60196
  }
59769
- const config2 = await readDesignFile(designPaths(resolve2(io.cwd, options.root)).config, zakuConfigSchema);
60197
+ const { config: config2 } = oneProduct(await workspaceOf(io, options), "zaku rules script");
59770
60198
  const scope = options.node !== void 0 ? { nodeId: options.node } : { page: options.page };
59771
60199
  const graph = designGraph(config2);
59772
60200
  const file2 = options.key === void 0 ? graph.libraries.length === 1 ? graph.libraries[0] : void 0 : graph.libraries.find((library2) => library2.key === options.key);
@@ -59786,7 +60214,7 @@ return ${check2};`);
59786
60214
  finish(0);
59787
60215
  return;
59788
60216
  }
59789
- const write2 = await readFile6(resolve2(io.cwd, options.after), "utf8");
60217
+ const write2 = await readFile7(resolve3(io.cwd, options.after), "utf8");
59790
60218
  const body = [
59791
60219
  library,
59792
60220
  `const zakuWrite = async () => {
@@ -59818,6 +60246,7 @@ var COMMANDS = [
59818
60246
  budgetCommand,
59819
60247
  schemaCommand,
59820
60248
  librariesCommand,
60249
+ productsCommand,
59821
60250
  mcpCommand
59822
60251
  ];
59823
60252
  async function main(argv, io) {
@@ -59854,6 +60283,11 @@ async function main(argv, io) {
59854
60283
  io.err(USAGE);
59855
60284
  return 3;
59856
60285
  }
60286
+ const refusal = refusalOf(error62);
60287
+ if (refusal) {
60288
+ io.err(refusal.line);
60289
+ return refusal.code;
60290
+ }
59857
60291
  io.err(`zaku: ${error62 instanceof Error ? error62.message : String(error62)}`);
59858
60292
  return 1;
59859
60293
  }