wlmaker 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +87 -3
  2. package/dist/cli.mjs +1814 -187
  3. package/package.json +3 -2
package/dist/cli.mjs CHANGED
@@ -47,10 +47,10 @@ import {
47
47
 
48
48
  // src/cli.ts
49
49
  import { createRequire } from "module";
50
- import * as fs24 from "fs";
50
+ import * as fs30 from "fs";
51
51
  import { Command } from "commander";
52
- import chalk26 from "chalk";
53
- import { z as z3 } from "zod";
52
+ import chalk27 from "chalk";
53
+ import { z as z4 } from "zod";
54
54
 
55
55
  // src/generators/bloc/generator.ts
56
56
  import * as fs from "fs";
@@ -491,48 +491,85 @@ function knobForProp(p) {
491
491
  return `(context.knobs.string(label: '${p.name}', initialValue: '') ?? '') // TODO: nullable, review requirements`;
492
492
  }
493
493
  }
494
- function useCaseTemplate(name, pascal2, tierPlural2, tier, properties, hasVariants) {
494
+ function useCaseTemplate(name, pascal2, tierPlural2, tier, properties, variantNames = [], includeSkeleton = false) {
495
495
  const isTemplate = tier === "template";
496
496
  const className = isTemplate && !pascal2.endsWith("Template") ? `Wl${pascal2}Template` : `Wl${pascal2}`;
497
+ const hasKnobs = properties.length > 0;
498
+ const hasVariants = variantNames.length > 1;
497
499
  const knobs = properties.map((p) => {
498
500
  const camel = propNameToCamel(p.name);
499
501
  return ` final ${camel} = ${knobForProp(p)};`;
500
502
  }).join("\n");
501
- const i18nArgs = properties.map((p) => {
503
+ const propArgs = properties.map((p) => {
502
504
  const camel = propNameToCamel(p.name);
503
505
  return ` ${camel}: ${camel},`;
504
506
  }).join("\n");
505
- let widgetCall;
506
- if (isTemplate && properties.length > 0) {
507
- widgetCall = `${className}(
507
+ function factoryCall(factoryName) {
508
+ if (isTemplate && properties.length > 0) {
509
+ return `${className}.${factoryName}(
508
510
  i18n: Wl${pascal2}I18n(
509
- ${i18nArgs}
511
+ ${propArgs}
510
512
  ),
511
513
  )`;
512
- } else if (properties.length > 0) {
513
- widgetCall = `${className}(
514
- ${i18nArgs}
514
+ }
515
+ if (properties.length > 0) {
516
+ return `${className}.${factoryName}(
517
+ ${propArgs}
515
518
  )`;
519
+ }
520
+ return `${className}.${factoryName}()`;
521
+ }
522
+ function defaultCall() {
523
+ if (isTemplate && properties.length > 0) {
524
+ return `${className}(
525
+ i18n: Wl${pascal2}I18n(
526
+ ${propArgs}
527
+ ),
528
+ )`;
529
+ }
530
+ if (properties.length > 0) {
531
+ return `${className}(
532
+ ${propArgs}
533
+ )`;
534
+ }
535
+ return `const ${className}()`;
536
+ }
537
+ const orderedVariants = hasVariants ? [
538
+ ...variantNames.filter((v) => v === "defaultVariant"),
539
+ ...variantNames.filter((v) => v !== "defaultVariant")
540
+ ] : [];
541
+ const childrenLines = [];
542
+ if (hasVariants) {
543
+ for (const v of orderedVariants) {
544
+ childrenLines.push(` ${factoryCall(v)},`);
545
+ }
516
546
  } else {
517
- widgetCall = `const ${className}()`;
547
+ childrenLines.push(` ${defaultCall()},`);
548
+ }
549
+ if (includeSkeleton) {
550
+ childrenLines.push(` ${factoryCall("skeleton")},`);
518
551
  }
552
+ const children = childrenLines.join("\n");
553
+ const knobBlock = hasKnobs ? `${knobs}
554
+
555
+ ` : "";
556
+ const widgetbookImport = hasKnobs ? `import 'package:widgetbook/widgetbook.dart';
557
+ ` : "";
558
+ const useCasePath = `wl_design_system/${tierPlural2}`;
519
559
  return `import 'package:design_system/design_system.dart';
520
560
  import 'package:flutter/material.dart';
521
- import 'package:widgetbook/widgetbook.dart';
522
- import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
561
+ ${widgetbookImport}import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
523
562
 
524
563
  @widgetbook.UseCase(
525
564
  name: '.default',
526
565
  type: ${className},
527
- path: 'wl_design_system/${tierPlural2}/wl_${name}',
566
+ path: '${useCasePath}',
528
567
  )
529
568
  Widget useCase${className}(BuildContext context) {
530
- ${knobs}
531
-
532
- return ListView(
569
+ ${knobBlock} return ListView(
533
570
  padding: const EdgeInsets.all(16),
534
571
  children: [
535
- ${widgetCall},
572
+ ${children}
536
573
  ],
537
574
  );
538
575
  }
@@ -733,7 +770,8 @@ async function createUseCase(name, tierInput, options) {
733
770
  ds.widgetbookDir,
734
771
  "lib",
735
772
  "usecases",
736
- "wl_design_system"
773
+ "wl_design_system",
774
+ plural
737
775
  );
738
776
  if (!fs3.existsSync(useCasesDir)) {
739
777
  fs3.mkdirSync(useCasesDir, { recursive: true });
@@ -750,12 +788,13 @@ async function createUseCase(name, tierInput, options) {
750
788
  plural,
751
789
  tier,
752
790
  options.properties ?? [],
753
- options.hasVariants ?? false
791
+ options.variantNames ?? [],
792
+ options.includeSkeleton ?? false
754
793
  )
755
794
  );
756
795
  console.log(
757
796
  chalk3.green(
758
- `Use-case for "Wl${pascal2}" created in widgetbook/lib/usecases/wl_design_system/`
797
+ `Use-case for "Wl${pascal2}" created in widgetbook/lib/usecases/wl_design_system/${plural}/`
759
798
  )
760
799
  );
761
800
  if (options.buildRunner) {
@@ -909,8 +948,8 @@ function extractPackageName(pagesPath) {
909
948
  }
910
949
 
911
950
  // src/flows/main-menu.ts
912
- import * as clack12 from "@clack/prompts";
913
- import chalk25 from "chalk";
951
+ import * as clack13 from "@clack/prompts";
952
+ import chalk26 from "chalk";
914
953
 
915
954
  // src/flows/project-resolver.ts
916
955
  import * as fs5 from "fs";
@@ -1072,7 +1111,7 @@ import * as fs8 from "fs";
1072
1111
  import * as path8 from "path";
1073
1112
  import * as clack4 from "@clack/prompts";
1074
1113
  import chalk8 from "chalk";
1075
- import { snakeCase as snakeCase2 } from "change-case";
1114
+ import { snakeCase as snakeCase2, pascalCase as pascalCase6 } from "change-case";
1076
1115
 
1077
1116
  // src/shared/json-editor.ts
1078
1117
  import * as fs7 from "fs";
@@ -1217,8 +1256,8 @@ function validateAndParse(schema, content) {
1217
1256
  clack3.log.error("Errores de validaci\xF3n:");
1218
1257
  if (error instanceof z.ZodError) {
1219
1258
  for (const issue of error.issues) {
1220
- const path24 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
1221
- clack3.log.warn(` \u2022 ${path24}: ${issue.message}`);
1259
+ const path27 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
1260
+ clack3.log.warn(` \u2022 ${path27}: ${issue.message}`);
1222
1261
  }
1223
1262
  } else {
1224
1263
  clack3.log.warn(` \u2022 ${error}`);
@@ -1304,33 +1343,109 @@ var WidgetJsonSchema = z2.object({
1304
1343
  import { pascalCase as pascalCase5, snakeCase } from "change-case";
1305
1344
 
1306
1345
  // src/generators/widget/component-ref-aliases.ts
1346
+ var BUTTON_DROP = [
1347
+ "showTrailingIcon",
1348
+ "showLeadingIcon",
1349
+ "state",
1350
+ "type"
1351
+ ];
1352
+ var BUTTON_PROP_MAP = { label: "text" };
1353
+ var BUTTON_VARIANT_MAP = {
1354
+ Primary: "primary",
1355
+ Secondary: "secondary",
1356
+ Tertiary: "tertiary"
1357
+ };
1358
+ var BUTTON_SIZE_RE = /^WlButton(Primary|Secondary|Tertiary)(?:\.(xs|sm|md|lg))?$/i;
1307
1359
  var COMPONENT_REF_ALIASES = {
1308
1360
  "WlButtonPrimary.xs": {
1309
1361
  component: "WlButton.primary",
1310
- props: { label: "text" },
1311
- dropProps: ["showTrailingIcon", "showLeadingIcon", "state", "type"],
1312
- extraProps: { buttonSize: "ButtonSize.xs" }
1362
+ props: { ...BUTTON_PROP_MAP },
1363
+ dropProps: [...BUTTON_DROP],
1364
+ extraProps: { buttonSize: "ButtonSize.xs" },
1365
+ mapLoadingState: true
1313
1366
  },
1314
1367
  WlButtonPrimary: {
1315
1368
  component: "WlButton.primary",
1316
- props: { label: "text" },
1317
- dropProps: ["showTrailingIcon", "showLeadingIcon", "state", "type"]
1369
+ props: { ...BUTTON_PROP_MAP },
1370
+ dropProps: [...BUTTON_DROP],
1371
+ mapLoadingState: true
1372
+ },
1373
+ "WlInputs.textfield": {
1374
+ component: "WlInputText",
1375
+ dropProps: ["state"],
1376
+ mapLoadingState: true,
1377
+ mapStateToLabel: true,
1378
+ // Stub so generated gallery code analyzes; hoist to parent in real widgets
1379
+ extraProps: { controller: "TextEditingController()" },
1380
+ todoComment: "// TODO: hoist TextEditingController to parent (created inline for preview)"
1381
+ },
1382
+ "WlInputs.Textfield": {
1383
+ component: "WlInputText",
1384
+ dropProps: ["state"],
1385
+ mapLoadingState: true,
1386
+ mapStateToLabel: true,
1387
+ extraProps: { controller: "TextEditingController()" },
1388
+ todoComment: "// TODO: hoist TextEditingController to parent (created inline for preview)"
1389
+ },
1390
+ "WlSkeleton.brick": {
1391
+ component: "WlSkeleton"
1318
1392
  }
1319
1393
  };
1394
+ function resolveButtonPattern(rawComponent) {
1395
+ const match = BUTTON_SIZE_RE.exec(rawComponent);
1396
+ if (!match) return null;
1397
+ const normalized = Object.keys(BUTTON_VARIANT_MAP).find(
1398
+ (k) => k.toLowerCase() === match[1].toLowerCase()
1399
+ ) ?? match[1];
1400
+ const factory = BUTTON_VARIANT_MAP[normalized];
1401
+ if (!factory) return null;
1402
+ const size = match[2]?.toLowerCase();
1403
+ const alias = {
1404
+ component: `WlButton.${factory}`,
1405
+ props: { ...BUTTON_PROP_MAP },
1406
+ dropProps: [...BUTTON_DROP],
1407
+ mapLoadingState: true
1408
+ };
1409
+ if (size) {
1410
+ alias.extraProps = { buttonSize: `ButtonSize.${size}` };
1411
+ }
1412
+ return alias;
1413
+ }
1414
+ function lookupAlias(rawComponent) {
1415
+ const exact = COMPONENT_REF_ALIASES[rawComponent];
1416
+ if (exact) return exact;
1417
+ return resolveButtonPattern(rawComponent);
1418
+ }
1419
+ function isLoadingState(value) {
1420
+ if (!value) return false;
1421
+ const cleaned = value.replace(/^['"]|['"]$/g, "").trim();
1422
+ return /^loading$/i.test(cleaned);
1423
+ }
1320
1424
  function resolveComponentRef(rawComponent, rawProps) {
1321
- const alias = COMPONENT_REF_ALIASES[rawComponent];
1425
+ const alias = lookupAlias(rawComponent);
1322
1426
  if (!alias) {
1323
1427
  return { component: rawComponent, props: { ...rawProps ?? {} } };
1324
1428
  }
1325
1429
  const drop = new Set(alias.dropProps ?? []);
1326
1430
  const propMap = alias.props ?? {};
1327
1431
  const props = { ...alias.extraProps ?? {} };
1432
+ const stateValue = rawProps?.state;
1433
+ if (alias.mapLoadingState && isLoadingState(stateValue)) {
1434
+ props.isLoading = "true";
1435
+ } else if (alias.mapStateToLabel && stateValue) {
1436
+ props.label = stateValue.includes("'") || stateValue.includes('"') ? stateValue : `'${stateValue}'`;
1437
+ }
1328
1438
  for (const [key, value] of Object.entries(rawProps ?? {})) {
1329
1439
  if (drop.has(key)) continue;
1440
+ if (key === "state" && alias.mapLoadingState) continue;
1330
1441
  const mappedKey = propMap[key] ?? key;
1331
1442
  props[mappedKey] = value;
1332
1443
  }
1333
- return { component: alias.component, props };
1444
+ return {
1445
+ component: alias.component,
1446
+ props,
1447
+ todoComment: alias.todoComment
1448
+ };
1334
1449
  }
1335
1450
 
1336
1451
  // src/generators/widget/dart-emitter.ts
@@ -1380,7 +1495,7 @@ function normalizeTier2(tier) {
1380
1495
  if (VALID_TIERS.includes(t)) return t;
1381
1496
  return "molecule";
1382
1497
  }
1383
- function generateWidgetCode(json) {
1498
+ function generateWidgetCode(json, options) {
1384
1499
  const rawName = json.componentName.replace(/^Wl/, "");
1385
1500
  const name = snakeCase(rawName);
1386
1501
  const pascal2 = pascalCase5(rawName);
@@ -1388,6 +1503,7 @@ function generateWidgetCode(json) {
1388
1503
  const tier = normalizeTier2(json.tier);
1389
1504
  const isTemplate = tier === "template";
1390
1505
  const isSkeleton = json.mode === "skeleton";
1506
+ const includeSkeleton = options?.includeSkeleton === true || isTemplate;
1391
1507
  const hasTextProps = json.properties.some((p) => p.type === "text" && p.name);
1392
1508
  const hasMultipleVariants = json.variants.length > 1;
1393
1509
  const pattern = isTemplate ? "subdirectory-show" : "single";
@@ -1430,14 +1546,14 @@ function generateWidgetCode(json) {
1430
1546
  parentFlex: null
1431
1547
  };
1432
1548
  if (isSkeleton) {
1433
- const skeleton2 = emitSkeletonFile(name, pascal2);
1434
- return { main: "", skeleton: skeleton2, tier, pattern };
1549
+ const tree = json.variants[0]?.widget;
1550
+ const skeleton = emitSkeletonFile(name, pascal2, tree);
1551
+ return { main: "", skeleton, tier, pattern };
1435
1552
  }
1436
- const main = emitMainFile(name, pascal2, json, ctx, tier);
1553
+ const main = emitMainFile(name, pascal2, json, ctx, tier, includeSkeleton);
1437
1554
  const variant = isTemplate && hasMultipleVariants ? emitVariantFile(name, pascal2, json) : void 0;
1438
1555
  const i18n = isTemplate && hasTextProps ? emitI18nFile(name, pascal2, json) : void 0;
1439
- const skeleton = isTemplate && isSkeleton ? emitSkeletonFile(name, pascal2) : void 0;
1440
- return { main, variant, i18n, skeleton, tier, pattern };
1556
+ return { main, variant, i18n, skeleton: void 0, tier, pattern };
1441
1557
  }
1442
1558
  function placeholderJson() {
1443
1559
  return JSON.stringify({
@@ -1475,13 +1591,15 @@ function placeholderJson() {
1475
1591
  ]
1476
1592
  }, null, 2);
1477
1593
  }
1478
- function emitMainFile(name, pascal2, json, ctx, tier) {
1594
+ function emitMainFile(name, pascal2, json, ctx, tier, includeSkeleton) {
1479
1595
  const isTemplate = tier === "template";
1480
1596
  const hasMultipleVariants = json.variants.length > 1;
1481
1597
  const hasI18n = isTemplate && json.properties.some((p) => p.type === "text");
1482
1598
  const parts = [];
1483
- if (isTemplate) {
1599
+ if (includeSkeleton) {
1484
1600
  parts.push(`part 'wl_${name}_skeleton.dart';`);
1601
+ }
1602
+ if (isTemplate) {
1485
1603
  if (hasMultipleVariants) parts.push(`part 'wl_${name}_variant.dart';`);
1486
1604
  if (hasI18n) parts.push(`part 'wl_${name}_i18n.dart';`);
1487
1605
  }
@@ -1494,25 +1612,40 @@ function emitMainFile(name, pascal2, json, ctx, tier) {
1494
1612
  if (!isTemplate) {
1495
1613
  variantEnumCode = emitVariantEnumInline(name, pascal2, json);
1496
1614
  }
1497
- const variantNames = json.variants.map((v) => normalizeVariantName(v.name, pascal2));
1615
+ const variantNames = json.variants.map(
1616
+ (v) => normalizeVariantName(v.name, pascal2, v.variantProps)
1617
+ );
1618
+ const defaultVariantName = variantNames.find((v) => v === "defaultVariant") ?? variantNames[0];
1498
1619
  const factories = variantNames.map(
1499
1620
  (vn) => ` factory ${ctx.componentName}.${vn}({
1500
- ${isTemplate ? "required Wl" + pascal2 + "I18n i18n,\n " : ""}${emitFactoryParams(otherProps)} Key? key,
1621
+ ${isTemplate ? "required Wl" + pascal2 + "I18n i18n,\n " : ""}${emitFactoryParams(otherProps)}${includeSkeleton ? " bool isLoading = false,\n" : ""} Key? key,
1501
1622
  }) {
1502
1623
  return ${ctx.componentName}._(
1503
1624
  key: key,
1504
- variant: ${ctx.componentName}Variant.${vn},${isTemplate ? "\n i18n: i18n," : ""}
1625
+ variant: ${ctx.componentName}Variant.${vn},${isTemplate ? "\n i18n: i18n," : ""}${includeSkeleton ? "\n isLoading: isLoading," : ""}
1505
1626
  ${emitFactoryArgs(otherProps)}
1506
1627
  );
1507
1628
  }`
1508
1629
  ).join("\n\n");
1630
+ const skeletonFactory = includeSkeleton ? `
1631
+
1632
+ factory ${ctx.componentName}.skeleton({
1633
+ ${isTemplate ? "required Wl" + pascal2 + "I18n i18n,\n " : ""} Key? key,
1634
+ }) {
1635
+ return ${ctx.componentName}._(
1636
+ key: key,
1637
+ variant: ${ctx.componentName}Variant.${defaultVariantName},${isTemplate ? "\n i18n: i18n," : ""}
1638
+ isLoading: true,
1639
+ );
1640
+ }` : "";
1509
1641
  const privateParams = [
1510
1642
  " required this.variant,",
1511
1643
  isTemplate ? " required this.i18n," : "",
1512
1644
  emitConstructorParams(otherProps),
1645
+ includeSkeleton ? " this.isLoading = false," : "",
1513
1646
  " super.key,"
1514
1647
  ].filter(Boolean).join("\n");
1515
- constructorCode = factories + `
1648
+ constructorCode = factories + skeletonFactory + `
1516
1649
 
1517
1650
  const ${ctx.componentName}._({
1518
1651
  ${privateParams}
@@ -1527,22 +1660,34 @@ ${privateParams}
1527
1660
  ` + emitFields(otherProps) : emitFields(json.properties);
1528
1661
  constructorCode = ` const ${ctx.componentName}({
1529
1662
  super.key,
1530
- ${params} });`;
1663
+ ${includeSkeleton ? " this.isLoading = false,\n" : ""}${params} });`;
1664
+ if (includeSkeleton) {
1665
+ constructorCode += `
1666
+
1667
+ factory ${ctx.componentName}.skeleton({
1668
+ ${isTemplate ? "required Wl" + pascal2 + "I18n i18n,\n " : ""} Key? key,
1669
+ }) {
1670
+ return ${ctx.componentName}(
1671
+ key: key,
1672
+ ${isTemplate ? " i18n: i18n,\n" : ""} isLoading: true,
1673
+ );
1674
+ }`;
1675
+ }
1531
1676
  fieldCode = fields;
1532
1677
  }
1533
- const buildBody = emitBuildMethod(json, { ...ctx, indent: 1 });
1534
- if (isTemplate) {
1535
- constructorCode = constructorCode.replace("super.key,", "this.isLoading = false,\n super.key,");
1536
- fieldCode = fieldCode + "\n final bool isLoading;";
1678
+ if (includeSkeleton) {
1679
+ fieldCode = fieldCode + (fieldCode.endsWith("\n") ? "" : "\n") + ` final bool isLoading;`;
1537
1680
  }
1538
- const skeletonCheck = isTemplate ? `
1681
+ const buildBody = emitBuildMethod(json, { ...ctx, indent: 1 });
1682
+ const skeletonCheck = includeSkeleton ? `
1539
1683
  ${indent(1)}if (isLoading) return const ${ctx.componentName}Skeleton();
1684
+ ` : "";
1685
+ const partsBlock = parts.length > 0 ? `${parts.join("\n")}
1540
1686
  ` : "";
1541
1687
  return `import 'package:design_system/design_system.dart';
1542
1688
  import 'package:flutter/material.dart';
1543
1689
 
1544
- ${parts.join("\n")}
1545
- ${variantEnumCode}
1690
+ ${partsBlock}${variantEnumCode}
1546
1691
  class ${ctx.componentName} extends StatelessWidget {
1547
1692
  //#region Constructor
1548
1693
  ${constructorCode}
@@ -1558,9 +1703,107 @@ ${fieldCode}
1558
1703
  }
1559
1704
  `;
1560
1705
  }
1561
- function normalizeVariantName(name, pascal2) {
1562
- const raw = name.replace(new RegExp(`^Wl${pascal2}`), "").trim() || "primary";
1563
- return raw.charAt(0).toLowerCase() + raw.slice(1);
1706
+ var DART_RESERVED = /* @__PURE__ */ new Set([
1707
+ "assert",
1708
+ "break",
1709
+ "case",
1710
+ "catch",
1711
+ "class",
1712
+ "const",
1713
+ "continue",
1714
+ "default",
1715
+ "do",
1716
+ "else",
1717
+ "enum",
1718
+ "extends",
1719
+ "false",
1720
+ "final",
1721
+ "finally",
1722
+ "for",
1723
+ "if",
1724
+ "in",
1725
+ "is",
1726
+ "new",
1727
+ "null",
1728
+ "rethrow",
1729
+ "return",
1730
+ "super",
1731
+ "switch",
1732
+ "this",
1733
+ "throw",
1734
+ "true",
1735
+ "try",
1736
+ "var",
1737
+ "void",
1738
+ "while",
1739
+ "with"
1740
+ ]);
1741
+ function variantSourceLabel(name, variantProps) {
1742
+ const fromProps = variantProps?.Variant ?? variantProps?.State ?? variantProps?.Type;
1743
+ return String(fromProps ?? name).trim();
1744
+ }
1745
+ function normalizeVariantName(name, pascal2, variantProps) {
1746
+ let raw = variantSourceLabel(name, variantProps);
1747
+ if (raw.includes("=")) {
1748
+ raw = raw.split("=").pop().trim();
1749
+ }
1750
+ const stripped = raw.replace(new RegExp(`^Wl${pascal2}`, "i"), "").trim();
1751
+ if (stripped) raw = stripped;
1752
+ raw = raw.replace(/[^a-zA-Z0-9_]+/g, " ").trim().split(/\s+/).filter(Boolean).map((part, i) => {
1753
+ if (i === 0) return part.charAt(0).toLowerCase() + part.slice(1);
1754
+ return part.charAt(0).toUpperCase() + part.slice(1);
1755
+ }).join("");
1756
+ if (!raw) {
1757
+ throw new Error(
1758
+ `Invalid variant name "${name}": could not derive a Dart identifier`
1759
+ );
1760
+ }
1761
+ if (DART_RESERVED.has(raw)) {
1762
+ raw = `${raw}Variant`;
1763
+ }
1764
+ if (!/^[a-z][a-zA-Z0-9_]*$/.test(raw)) {
1765
+ throw new Error(
1766
+ `Invalid variant name "${name}" \u2192 "${raw}" (not a valid Dart identifier)`
1767
+ );
1768
+ }
1769
+ return raw;
1770
+ }
1771
+ function isSkeletonVariant(name, variantProps) {
1772
+ let raw = variantSourceLabel(name, variantProps);
1773
+ if (raw.includes("=")) {
1774
+ raw = raw.split("=").pop().trim();
1775
+ }
1776
+ return /^skeleton$/i.test(raw);
1777
+ }
1778
+ function splitSkeletonVariant(json) {
1779
+ const skeletonVariants = json.variants.filter(
1780
+ (v) => isSkeletonVariant(v.name, v.variantProps)
1781
+ );
1782
+ const variants = json.variants.filter(
1783
+ (v) => !isSkeletonVariant(v.name, v.variantProps)
1784
+ );
1785
+ return {
1786
+ hasSkeleton: skeletonVariants.length > 0,
1787
+ skeletonVariants,
1788
+ componentJson: {
1789
+ ...json,
1790
+ variants,
1791
+ variantCount: variants.length
1792
+ }
1793
+ };
1794
+ }
1795
+ function assertUniqueVariantNames(variants, pascal2) {
1796
+ const seen = /* @__PURE__ */ new Map();
1797
+ for (const v of variants) {
1798
+ const normalized = normalizeVariantName(v.name, pascal2, v.variantProps);
1799
+ const prev = seen.get(normalized);
1800
+ if (prev) {
1801
+ throw new Error(
1802
+ `Duplicate variant name after normalize: "${prev}" and "${v.name}" \u2192 "${normalized}"`
1803
+ );
1804
+ }
1805
+ seen.set(normalized, v.name);
1806
+ }
1564
1807
  }
1565
1808
  function emitConstructorParams(props) {
1566
1809
  if (props.length === 0) return "";
@@ -1603,7 +1846,10 @@ function emitBuildMethod(json, ctx) {
1603
1846
  return `${indent(ctx.indent)}return ${statement.trimStart()}
1604
1847
  `;
1605
1848
  }
1606
- const variantNames = json.variants.map((v) => normalizeVariantName(v.name, ctx.componentName.replace(/^Wl/, "")));
1849
+ const pascal2 = ctx.componentName.replace(/^Wl/, "");
1850
+ const variantNames = json.variants.map(
1851
+ (v) => normalizeVariantName(v.name, pascal2, v.variantProps)
1852
+ );
1607
1853
  let code = `${indent(ctx.indent)}return switch (variant) {
1608
1854
  `;
1609
1855
  for (let i = 0; i < json.variants.length; i++) {
@@ -1643,6 +1889,104 @@ function wrapExpanded(code, i) {
1643
1889
  ${indent(i + 1)}child: ${code.trim()}
1644
1890
  ${indent(i)}),`;
1645
1891
  }
1892
+ var RADIUS_PX_TO_TOKEN = {
1893
+ 0: "theme.borderRadius.radiusNone",
1894
+ 2: "theme.borderRadius.radiusXs",
1895
+ 4: "theme.borderRadius.radiusS",
1896
+ 8: "theme.borderRadius.radiusM",
1897
+ 12: "theme.borderRadius.radiusL",
1898
+ 16: "theme.borderRadius.radiusXl",
1899
+ 20: "theme.borderRadius.radius2xl",
1900
+ 24: "theme.borderRadius.radius3xl"
1901
+ };
1902
+ var SPACING_PX_TO_TOKEN = {
1903
+ 0: "theme.spacing.spacingNone",
1904
+ 2: "theme.spacing.spacingXs",
1905
+ 4: "theme.spacing.spacingS",
1906
+ 6: "theme.spacing.spacingM",
1907
+ 8: "theme.spacing.spacingL",
1908
+ 12: "theme.spacing.spacingXl",
1909
+ 16: "theme.spacing.spacing2xl",
1910
+ 20: "theme.spacing.spacing3xl",
1911
+ 24: "theme.spacing.spacing4xl",
1912
+ 32: "theme.spacing.spacing5xl",
1913
+ 40: "theme.spacing.spacing6xl",
1914
+ 48: "theme.spacing.spacing7xl",
1915
+ 56: "theme.spacing.spacing8xl",
1916
+ 64: "theme.spacing.spacing9xl"
1917
+ };
1918
+ var ICON_TODO_MAP = {
1919
+ "ticket-percent": "LucideIcons.ticketPercent"
1920
+ };
1921
+ function parsePxNumber(raw) {
1922
+ const cleaned = raw.trim().replace(/\.0$/, "");
1923
+ if (!/^-?\d+(\.\d+)?$/.test(cleaned)) return null;
1924
+ const n = Number(cleaned);
1925
+ return Number.isFinite(n) ? n : null;
1926
+ }
1927
+ function resolveRadiusExpr(raw) {
1928
+ const trimmed = raw.trim();
1929
+ const circularMatch = /^BorderRadius\.circular\(\s*([0-9.]+)\s*\)$/.exec(trimmed);
1930
+ if (circularMatch) {
1931
+ const n2 = parsePxNumber(circularMatch[1]);
1932
+ if (n2 !== null) {
1933
+ const token = RADIUS_PX_TO_TOKEN[n2];
1934
+ if (token) return `BorderRadius.circular(${token})`;
1935
+ }
1936
+ return `BorderRadius.circular(${circularMatch[1]})`;
1937
+ }
1938
+ if (trimmed.startsWith("BorderRadius.")) return trimmed;
1939
+ if (trimmed.includes("theme.borderRadius")) {
1940
+ return `BorderRadius.circular(${trimmed})`;
1941
+ }
1942
+ const n = parsePxNumber(trimmed);
1943
+ if (n !== null) {
1944
+ const token = RADIUS_PX_TO_TOKEN[n];
1945
+ if (token) return `BorderRadius.circular(${token})`;
1946
+ return `BorderRadius.circular(${trimmed})`;
1947
+ }
1948
+ return `BorderRadius.circular(${trimmed})`;
1949
+ }
1950
+ function resolveSpacingExpr(raw) {
1951
+ const trimmed = raw.trim();
1952
+ if (trimmed.includes("theme.spacing") || trimmed.startsWith("theme.")) {
1953
+ return trimmed;
1954
+ }
1955
+ const n = parsePxNumber(trimmed);
1956
+ if (n !== null) {
1957
+ return SPACING_PX_TO_TOKEN[n] ?? trimmed;
1958
+ }
1959
+ return trimmed;
1960
+ }
1961
+ function resolvePaddingExpr(raw) {
1962
+ let trimmed = raw.trim().replace(/^const\s+/, "");
1963
+ if (trimmed.includes("theme.spacing")) return trimmed;
1964
+ if (trimmed.startsWith("EdgeInsets.")) {
1965
+ return trimmed.replace(
1966
+ /\b(\d+(?:\.\d+)?)\b/g,
1967
+ (num) => resolveSpacingExpr(num)
1968
+ );
1969
+ }
1970
+ const bare = parsePxNumber(trimmed);
1971
+ if (bare !== null) {
1972
+ return `EdgeInsets.all(${resolveSpacingExpr(trimmed)})`;
1973
+ }
1974
+ return trimmed;
1975
+ }
1976
+ function resolveIconData(rawIcon) {
1977
+ const todoMatch = /(?:\/\/\s*)?TODO:\s*icon\s+"([^"]+)"/i.exec(rawIcon) ?? /(?:\/\/\s*)?TODO:\s*icon\s+'([^']+)'/i.exec(rawIcon);
1978
+ if (todoMatch) {
1979
+ const mapped = ICON_TODO_MAP[todoMatch[1]];
1980
+ if (mapped) {
1981
+ return { iconData: mapped, todoLine: null };
1982
+ }
1983
+ }
1984
+ const isTodo = rawIcon.startsWith("// TODO") || rawIcon.startsWith("TODO");
1985
+ if (isTodo) {
1986
+ return { iconData: "LucideIcons.helpCircle", todoLine: rawIcon };
1987
+ }
1988
+ return { iconData: rawIcon, todoLine: null };
1989
+ }
1646
1990
  function emitContainer(node, ctx) {
1647
1991
  const i = ctx.indent;
1648
1992
  const hasLayout = !!(node.layout && node.children?.length);
@@ -1665,8 +2009,8 @@ function emitContainer(node, ctx) {
1665
2009
  } else if (node.height && !isFillHeight) {
1666
2010
  lines.push(`${indent(i)}height: ${node.height},`);
1667
2011
  }
1668
- if (hasPadding) {
1669
- lines.push(`${indent(i)}padding: ${node.padding},`);
2012
+ if (hasPadding && node.padding) {
2013
+ lines.push(`${indent(i)}padding: ${resolvePaddingExpr(node.padding)},`);
1670
2014
  }
1671
2015
  if (hasDecoration && node.decoration) {
1672
2016
  lines.push(`${indent(i)}decoration: ${emitDecoration(node.decoration, i)},`);
@@ -1724,7 +2068,7 @@ function emitLayoutContainer(node, ctx) {
1724
2068
  const rawCross = node.layout.crossAxisAlignment ?? "start";
1725
2069
  const mainAlign = rawMain.startsWith("MainAxisAlignment") ? rawMain : `MainAxisAlignment.${rawMain}`;
1726
2070
  const crossAlign = rawCross.startsWith("CrossAxisAlignment") ? rawCross : `CrossAxisAlignment.${rawCross}`;
1727
- const gap = node.gap ?? "0";
2071
+ const gap = resolveSpacingExpr(node.gap ?? "0");
1728
2072
  const hasWrapper = !!(node.padding || node.decoration || node.width || node.height);
1729
2073
  const flatChildren = flattenLayoutChildren(node.children, layoutType);
1730
2074
  const needsStack = flatChildren.some((c) => c.positioning === "absolute");
@@ -1760,7 +2104,11 @@ function emitLayoutContainer(node, ctx) {
1760
2104
  } else if (node.height && !isFillHeight) {
1761
2105
  wrapperLines.push(`${indent(base)}height: ${node.height},`);
1762
2106
  }
1763
- if (node.padding) wrapperLines.push(`${indent(base)}padding: ${node.padding},`);
2107
+ if (node.padding) {
2108
+ wrapperLines.push(
2109
+ `${indent(base)}padding: ${resolvePaddingExpr(node.padding)},`
2110
+ );
2111
+ }
1764
2112
  if (node.decoration) {
1765
2113
  wrapperLines.push(`${indent(base)}decoration: ${emitDecoration(node.decoration, base)},`);
1766
2114
  }
@@ -1843,9 +2191,9 @@ function emitIcon(node, ctx) {
1843
2191
  const rawIcon = node.icon ?? "// TODO: icon";
1844
2192
  const color = node.color;
1845
2193
  const hasColor = !!color;
1846
- const isTodo = rawIcon.startsWith("// TODO") || rawIcon.startsWith("TODO");
1847
- const iconData = isTodo ? "LucideIcons.helpCircle" : rawIcon;
1848
- const todoLine = isTodo ? `${indent(i)}${rawIcon}
2194
+ const resolvedIcon = resolveIconData(rawIcon);
2195
+ const iconData = resolvedIcon.iconData;
2196
+ const todoLine = resolvedIcon.todoLine ? `${indent(i)}${resolvedIcon.todoLine}
1849
2197
  ` : "";
1850
2198
  const hasSize = !!(node.width || node.height);
1851
2199
  const w = node.width ? parseFloat(node.width) : null;
@@ -1896,9 +2244,23 @@ function emitComponentRef(node, ctx) {
1896
2244
  const i = ctx.indent;
1897
2245
  const resolved = resolveComponentRef(node.component ?? "SizedBox", node.props);
1898
2246
  const component = resolved.component;
2247
+ const props = { ...resolved.props };
2248
+ if (component === "WlSkeleton" || component.startsWith("WlSkeleton.")) {
2249
+ if (node.width && node.width !== "double.infinity" && props.width == null) {
2250
+ props.width = node.width;
2251
+ }
2252
+ if (node.height && node.height !== "double.infinity" && props.height == null) {
2253
+ props.height = node.height;
2254
+ }
2255
+ if (props.radius == null && !component.includes("custom")) {
2256
+ props.radius = "theme.borderRadius.radiusL";
2257
+ }
2258
+ }
2259
+ const todoPrefix = resolved.todoComment ? `${indent(i)}${resolved.todoComment}
2260
+ ` : "";
1899
2261
  const propEntries = [];
1900
2262
  let optionalPropField = null;
1901
- for (const [key, value] of Object.entries(resolved.props)) {
2263
+ for (const [key, value] of Object.entries(props)) {
1902
2264
  const fieldOrValue = ctx.textDefaults[value] ?? value;
1903
2265
  const isOptionalField = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldOrValue) && ctx.optionalTextFields.has(fieldOrValue);
1904
2266
  propEntries.push(`${key}: ${isOptionalField ? `${fieldOrValue}!` : fieldOrValue}`);
@@ -1915,6 +2277,10 @@ ${indent(i)}),`;
1915
2277
  } else {
1916
2278
  result = `${indent(i)}${component}(),`;
1917
2279
  }
2280
+ if (shouldWrapWithExpanded(node, ctx)) {
2281
+ result = wrapExpanded(result, i);
2282
+ }
2283
+ result = todoPrefix + result;
1918
2284
  if (optionalPropField) {
1919
2285
  return wrapOptionalGuard(result, optionalPropField, ctx);
1920
2286
  }
@@ -1926,7 +2292,7 @@ function emitDecoration(dec, i) {
1926
2292
  parts.push(`color: ${dec.color}`);
1927
2293
  }
1928
2294
  if (dec.borderRadius) {
1929
- parts.push(`borderRadius: BorderRadius.circular(${dec.borderRadius})`);
2295
+ parts.push(`borderRadius: ${resolveRadiusExpr(dec.borderRadius)}`);
1930
2296
  }
1931
2297
  if (dec.border) {
1932
2298
  parts.push(`border: ${dec.border}`);
@@ -1941,7 +2307,9 @@ ${indent(i + 1)}`)},
1941
2307
  ${indent(i)})`;
1942
2308
  }
1943
2309
  function emitVariantEnumInline(name, pascal2, json) {
1944
- const variantNames = json.variants.map((v) => normalizeVariantName(v.name, pascal2));
2310
+ const variantNames = json.variants.map(
2311
+ (v) => normalizeVariantName(v.name, pascal2, v.variantProps)
2312
+ );
1945
2313
  const enumCases = variantNames.map((v) => ` ${v},`).join("\n");
1946
2314
  const extMethods = variantNames.map(
1947
2315
  (v) => ` bool get is${v.charAt(0).toUpperCase() + v.slice(1)} => this == Wl${pascal2}Variant.${v};`
@@ -1957,10 +2325,9 @@ ${extMethods}
1957
2325
  `;
1958
2326
  }
1959
2327
  function emitVariantFile(name, pascal2, json) {
1960
- const variantNames = json.variants.map((v) => {
1961
- const raw = v.name.replace(`Wl${pascal2}`, "").trim() || "primary";
1962
- return raw.charAt(0).toLowerCase() + raw.slice(1);
1963
- });
2328
+ const variantNames = json.variants.map(
2329
+ (v) => normalizeVariantName(v.name, pascal2, v.variantProps)
2330
+ );
1964
2331
  const enumCases = variantNames.map((v) => ` ${v},`).join("\n");
1965
2332
  const extMethods = variantNames.map(
1966
2333
  (v) => ` bool get is${v.charAt(0).toUpperCase() + v.slice(1)} => this == Wl${pascal2}Variant.${v};`
@@ -1997,7 +2364,26 @@ ${fieldDecls || ` const Wl${pascal2}I18n();`}
1997
2364
  }
1998
2365
  `;
1999
2366
  }
2000
- function emitSkeletonFile(name, pascal2) {
2367
+ function emitSkeletonFile(name, pascal2, tree) {
2368
+ let body;
2369
+ if (tree) {
2370
+ const ctx = {
2371
+ componentName: `Wl${pascal2}`,
2372
+ indent: 2,
2373
+ textDefaults: {},
2374
+ optionalTextFields: /* @__PURE__ */ new Set(),
2375
+ showGuards: {},
2376
+ parentFlex: null
2377
+ };
2378
+ const emitted = emitWidgetTree(tree, ctx).trim().replace(/,$/, "");
2379
+ body = `return ${emitted};`;
2380
+ } else {
2381
+ body = `return WlSkeleton(
2382
+ width: double.infinity,
2383
+ height: 120,
2384
+ radius: theme.borderRadius.radiusL,
2385
+ );`;
2386
+ }
2001
2387
  return `part of 'wl_${name}.dart';
2002
2388
 
2003
2389
  class Wl${pascal2}Skeleton extends StatelessWidget {
@@ -2007,11 +2393,7 @@ class Wl${pascal2}Skeleton extends StatelessWidget {
2007
2393
  Widget build(BuildContext context) {
2008
2394
  final theme = context.theme;
2009
2395
 
2010
- return WlSkeleton(
2011
- width: double.infinity,
2012
- height: 120,
2013
- radius: theme.borderRadius.radiusL,
2014
- );
2396
+ ${body}
2015
2397
  }
2016
2398
  }
2017
2399
  `;
@@ -2136,12 +2518,31 @@ async function widgetFlow(jsonParams) {
2136
2518
  clack4.outro(chalk8.red(`Error: ${error}`));
2137
2519
  }
2138
2520
  }
2139
- async function writeGeneratedWidget(json, ds, projectRoot) {
2521
+ async function writeGeneratedWidget(json, ds, projectRoot, options) {
2522
+ const skipPrompts = options?.skipPrompts === true;
2523
+ let working = json;
2524
+ let hasSkeletonRedirect = false;
2525
+ let skeletonVariants = [];
2526
+ if (json.mode !== "skeleton") {
2527
+ const split = splitSkeletonVariant(json);
2528
+ working = split.componentJson;
2529
+ hasSkeletonRedirect = split.hasSkeleton;
2530
+ skeletonVariants = split.skeletonVariants;
2531
+ const pascal2 = pascalCase6(json.componentName.replace(/^Wl/, ""));
2532
+ assertUniqueVariantNames(working.variants, pascal2);
2533
+ if (hasSkeletonRedirect) {
2534
+ clack4.log.info(
2535
+ chalk8.cyan("Detected Skeleton variant \u2192 using skeleton flow (not a UI factory)")
2536
+ );
2537
+ }
2538
+ }
2140
2539
  const genSpinner = clack4.spinner();
2141
2540
  genSpinner.start("Generando desde JSON...");
2142
2541
  try {
2143
- const code = generateWidgetCode(json);
2144
- const rawName = json.componentName.replace(/^Wl/, "");
2542
+ const code = generateWidgetCode(working, {
2543
+ includeSkeleton: hasSkeletonRedirect || working.mode === "skeleton" || working.tier === "template"
2544
+ });
2545
+ const rawName = working.componentName.replace(/^Wl/, "");
2145
2546
  const name = snakeCase2(rawName);
2146
2547
  const tier = code.tier;
2147
2548
  const plural = tierPlural(tier);
@@ -2152,49 +2553,53 @@ async function writeGeneratedWidget(json, ds, projectRoot) {
2152
2553
  fs8.mkdirSync(outDir, { recursive: true });
2153
2554
  }
2154
2555
  const targetFile = path8.join(outDir, `${fileName}.dart`);
2155
- if (json.mode === "skeleton") {
2556
+ if (working.mode === "skeleton") {
2156
2557
  const skeletonPath = path8.join(outDir, `${fileName}_skeleton.dart`);
2157
2558
  if (fs8.existsSync(skeletonPath)) {
2158
- const overwrite = await clack4.confirm({
2159
- message: `El skeleton de "${json.componentName}" ya existe. \xBFReemplazar?`,
2160
- initialValue: false
2161
- });
2162
- if (clack4.isCancel(overwrite) || !overwrite) {
2163
- genSpinner.stop("Cancelado");
2164
- clack4.outro(chalk8.yellow("No se modific\xF3 nada."));
2165
- return;
2559
+ if (!skipPrompts) {
2560
+ const overwrite = await clack4.confirm({
2561
+ message: `El skeleton de "${working.componentName}" ya existe. \xBFReemplazar?`,
2562
+ initialValue: false
2563
+ });
2564
+ if (clack4.isCancel(overwrite) || !overwrite) {
2565
+ genSpinner.stop("Cancelado");
2566
+ clack4.outro(chalk8.yellow("No se modific\xF3 nada."));
2567
+ return false;
2568
+ }
2166
2569
  }
2167
2570
  } else if (fs8.existsSync(targetFile)) {
2168
- const add = await clack4.confirm({
2169
- message: `\xBFAgregar skeleton a "${json.componentName}"?`,
2170
- initialValue: true
2171
- });
2172
- if (clack4.isCancel(add) || !add) {
2173
- genSpinner.stop("Cancelado");
2174
- clack4.outro(chalk8.yellow("No se modific\xF3 nada."));
2175
- return;
2571
+ if (!skipPrompts) {
2572
+ const add = await clack4.confirm({
2573
+ message: `\xBFAgregar skeleton a "${working.componentName}"?`,
2574
+ initialValue: true
2575
+ });
2576
+ if (clack4.isCancel(add) || !add) {
2577
+ genSpinner.stop("Cancelado");
2578
+ clack4.outro(chalk8.yellow("No se modific\xF3 nada."));
2579
+ return false;
2580
+ }
2176
2581
  }
2177
2582
  }
2178
- } else {
2179
- if (fs8.existsSync(targetFile)) {
2583
+ } else if (fs8.existsSync(targetFile)) {
2584
+ if (!skipPrompts) {
2180
2585
  const overwrite = await clack4.confirm({
2181
- message: `El componente "${json.componentName}" ya existe en ${plural}/${fileName}/. \xBFReemplazar?`,
2586
+ message: `El componente "${working.componentName}" ya existe en ${plural}/${fileName}/. \xBFReemplazar?`,
2182
2587
  initialValue: false
2183
2588
  });
2184
2589
  if (clack4.isCancel(overwrite) || !overwrite) {
2185
2590
  genSpinner.stop("Cancelado");
2186
2591
  clack4.outro(chalk8.yellow("No se modific\xF3 nada."));
2187
- return;
2592
+ return false;
2188
2593
  }
2189
2594
  }
2190
2595
  }
2191
- if (json.mode === "skeleton") {
2596
+ if (working.mode === "skeleton") {
2192
2597
  const skeletonPath = path8.join(outDir, `${fileName}_skeleton.dart`);
2193
2598
  fs8.writeFileSync(skeletonPath, code.skeleton ?? "");
2194
2599
  if (fs8.existsSync(targetFile)) {
2195
2600
  let mainContent = fs8.readFileSync(targetFile, "utf-8");
2196
2601
  const partLine = `part '${fileName}_skeleton.dart';`;
2197
- const componentName = json.componentName;
2602
+ const componentName = working.componentName;
2198
2603
  if (!mainContent.includes(partLine)) {
2199
2604
  if (/part '.+';/.test(mainContent)) {
2200
2605
  mainContent = mainContent.replace(/(part '.+';)\n/, `$1
@@ -2240,7 +2645,7 @@ ${partLine}
2240
2645
  if (code.i18n) filesToFormat.push(path8.join(outDir, `${fileName}_i18n.dart`));
2241
2646
  if (code.skeleton) filesToFormat.push(path8.join(outDir, `${fileName}_skeleton.dart`));
2242
2647
  }
2243
- if (json.mode === "skeleton") {
2648
+ if (working.mode === "skeleton") {
2244
2649
  filesToFormat.push(path8.join(outDir, `${fileName}_skeleton.dart`));
2245
2650
  }
2246
2651
  for (const f of filesToFormat) {
@@ -2250,42 +2655,76 @@ ${partLine}
2250
2655
  for (const f of filesToFormat) {
2251
2656
  if (fs8.existsSync(f)) await dartFix(f);
2252
2657
  }
2253
- genSpinner.stop(`${json.componentName} generado en ${plural}/`);
2254
- if (ds.widgetbookDir && json.mode !== "skeleton") {
2658
+ genSpinner.stop(`${working.componentName} generado en ${plural}/`);
2659
+ if (ds.widgetbookDir && working.mode !== "skeleton") {
2255
2660
  genSpinner.start("Creating widgetbook use-case...");
2256
2661
  try {
2257
2662
  await createUseCase(name, code.tier, {
2258
2663
  projectRoot,
2259
2664
  buildRunner: false,
2260
- properties: json.properties,
2261
- hasVariants: json.variants.length > 1
2665
+ properties: working.properties,
2666
+ variantNames: working.variants.length > 1 ? working.variants.map(
2667
+ (v) => normalizeVariantName(
2668
+ v.name,
2669
+ pascalCase6(working.componentName.replace(/^Wl/, "")),
2670
+ v.variantProps
2671
+ )
2672
+ ) : [],
2673
+ includeSkeleton: hasSkeletonRedirect
2262
2674
  });
2263
2675
  genSpinner.stop("Use-case created");
2264
- const useCaseFile = path8.join(ds.widgetbookDir, "lib", "usecases", "wl_design_system", `${fileName}.dart`);
2676
+ const useCaseFile = path8.join(
2677
+ ds.widgetbookDir,
2678
+ "lib",
2679
+ "usecases",
2680
+ "wl_design_system",
2681
+ plural,
2682
+ `${fileName}.dart`
2683
+ );
2265
2684
  if (fs8.existsSync(useCaseFile)) {
2266
2685
  dartFormat(useCaseFile);
2267
2686
  }
2268
- const runBr = await clack4.confirm({
2269
- message: "\xBFCorrer build_runner en el widgetbook?",
2270
- initialValue: true
2271
- });
2272
- if (!clack4.isCancel(runBr) && runBr) {
2273
- genSpinner.start("Corriendo build_runner...");
2274
- try {
2275
- await runBuildRunner(ds.widgetbookDir);
2276
- genSpinner.stop("Widgetbook actualizado");
2277
- } catch {
2278
- genSpinner.stop("build_runner fall\xF3");
2687
+ if (!skipPrompts) {
2688
+ const runBr = await clack4.confirm({
2689
+ message: "\xBFCorrer build_runner en el widgetbook?",
2690
+ initialValue: true
2691
+ });
2692
+ if (!clack4.isCancel(runBr) && runBr) {
2693
+ genSpinner.start("Corriendo build_runner...");
2694
+ try {
2695
+ await runBuildRunner(ds.widgetbookDir);
2696
+ genSpinner.stop("Widgetbook actualizado");
2697
+ } catch {
2698
+ genSpinner.stop("build_runner fall\xF3");
2699
+ }
2279
2700
  }
2280
2701
  }
2281
2702
  } catch (e) {
2282
2703
  genSpinner.stop(`Use-case skipped: ${e}`);
2283
2704
  }
2284
2705
  }
2285
- clack4.outro(chalk8.green("\xA1Listo!"));
2706
+ if (hasSkeletonRedirect) {
2707
+ const ok = await writeGeneratedWidget(
2708
+ {
2709
+ ...working,
2710
+ mode: "skeleton",
2711
+ variants: skeletonVariants,
2712
+ variantCount: skeletonVariants.length
2713
+ },
2714
+ ds,
2715
+ projectRoot,
2716
+ { skipPrompts: true }
2717
+ );
2718
+ if (!ok) return false;
2719
+ }
2720
+ if (!skipPrompts) {
2721
+ clack4.outro(chalk8.green("\xA1Listo!"));
2722
+ }
2723
+ return true;
2286
2724
  } catch (error) {
2287
2725
  genSpinner.stop("Fall\xF3");
2288
2726
  clack4.outro(chalk8.red(`Error: ${error}`));
2727
+ return false;
2289
2728
  }
2290
2729
  }
2291
2730
  async function useCaseFlow() {
@@ -2477,7 +2916,7 @@ import chalk11 from "chalk";
2477
2916
  import * as fs10 from "fs";
2478
2917
  import * as path10 from "path";
2479
2918
  import chalk10 from "chalk";
2480
- import { pascalCase as pascalCase6, camelCase as camelCase2 } from "change-case";
2919
+ import { pascalCase as pascalCase7, camelCase as camelCase2 } from "change-case";
2481
2920
 
2482
2921
  // src/generators/endpoint/templates.ts
2483
2922
  import { camelCase } from "change-case";
@@ -2560,8 +2999,8 @@ ${paramsClass}
2560
2999
  }
2561
3000
  `;
2562
3001
  }
2563
- function retrofitMethod(methodName, path24, httpMethod, params, returnType) {
2564
- const httpAnnotation = `@${httpMethod.toUpperCase()}('${path24}')`;
3002
+ function retrofitMethod(methodName, path27, httpMethod, params, returnType) {
3003
+ const httpAnnotation = `@${httpMethod.toUpperCase()}('${path27}')`;
2565
3004
  const paramList = params.map((p) => {
2566
3005
  if (p.isPath) return `@Path('${p.name}') ${p.type} ${p.name}`;
2567
3006
  if (p.isBody) return `@Body() ${p.type} ${p.name}`;
@@ -2629,17 +3068,17 @@ ${annotation}${useCaseClass} ${useCaseCamel}(${repoInterface} repository) =>
2629
3068
  // src/generators/endpoint/generator.ts
2630
3069
  async function createEndpoint(options) {
2631
3070
  const lib = path10.join(options.projectRoot, "lib");
2632
- const pascal2 = pascalCase6(options.useCaseName);
2633
- const useCasePascal = pascalCase6(options.useCaseName);
3071
+ const pascal2 = pascalCase7(options.useCaseName);
3072
+ const useCasePascal = pascalCase7(options.useCaseName);
2634
3073
  const useCaseSnake = options.useCaseName;
2635
3074
  const needsBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod);
2636
3075
  const domain = extractDomain(options.bffApiFile);
2637
3076
  const datasourceFile = path10.join(lib, "data", "datasources", `${domain}_rest_datasource.dart`);
2638
- const datasourceClassName = `${pascalCase6(domain)}RestDataSource`;
3077
+ const datasourceClassName = `${pascalCase7(domain)}RestDataSource`;
2639
3078
  const repositoryInterfaceFile = path10.join(lib, "domain", "repositories", `${domain}_repository.dart`);
2640
- const repositoryInterfaceName = `${pascalCase6(domain)}Repository`;
3079
+ const repositoryInterfaceName = `${pascalCase7(domain)}Repository`;
2641
3080
  const repositoryImplFile = path10.join(lib, "data", "repositories", `${domain}_repository_data.dart`);
2642
- const repositoryImplClassName = `${pascalCase6(domain)}RepositoryData`;
3081
+ const repositoryImplClassName = `${pascalCase7(domain)}RepositoryData`;
2643
3082
  const feature = domain;
2644
3083
  const pathParams = extractPathParams(options.endpointPath);
2645
3084
  const methodParams = buildMethodParams(options, pathParams, needsBody);
@@ -2769,7 +3208,7 @@ async function createEndpoint(options) {
2769
3208
  spinner11("Registering in DI modules");
2770
3209
  const appBaseDir = findAppBasePackageDir(options.projectRoot, options.diTarget);
2771
3210
  if (appBaseDir) {
2772
- const domainPascal = pascalCase6(domain);
3211
+ const domainPascal = pascalCase7(domain);
2773
3212
  const lazy = options.diLazySingleton !== false;
2774
3213
  const dsModuleFile = path10.join(appBaseDir, "datasources_module.dart");
2775
3214
  const dsRegistration = datasourceModuleRegistration(domainPascal, lazy);
@@ -2824,7 +3263,7 @@ function extractPathParams(endpointPath) {
2824
3263
  function buildMethodParams(options, pathParams, hasRequestBody) {
2825
3264
  const params = [...pathParams];
2826
3265
  if (hasRequestBody) {
2827
- const reqPascal = pascalCase6(options.useCaseName);
3266
+ const reqPascal = pascalCase7(options.useCaseName);
2828
3267
  params.push({ name: "body", type: `${reqPascal}RequestModel` });
2829
3268
  }
2830
3269
  return params;
@@ -2835,7 +3274,7 @@ function buildRetrofitParams(options, pathParams, hasRequestBody) {
2835
3274
  params.push({ ...pp, isPath: true });
2836
3275
  }
2837
3276
  if (hasRequestBody) {
2838
- const reqPascal = pascalCase6(options.useCaseName);
3277
+ const reqPascal = pascalCase7(options.useCaseName);
2839
3278
  params.push({ name: "body", type: `${reqPascal}RequestModel`, isBody: true });
2840
3279
  }
2841
3280
  return params;
@@ -3774,6 +4213,23 @@ function appConfigDefaultValue(dartType) {
3774
4213
  return "const []";
3775
4214
  }
3776
4215
  }
4216
+ function dartDefaultLiteral(defaultValue, dartType) {
4217
+ if (!defaultValue || !defaultValue.trim()) {
4218
+ return appConfigDefaultValue(dartType);
4219
+ }
4220
+ switch (dartType) {
4221
+ case "String":
4222
+ return `'${defaultValue.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
4223
+ case "int":
4224
+ return defaultValue.trim();
4225
+ case "bool":
4226
+ return defaultValue.trim() === "true" ? "true" : "false";
4227
+ case "List<String>": {
4228
+ const items = defaultValue.split(",").map((s) => s.trim()).filter(Boolean).map((s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`);
4229
+ return items.length === 0 ? "const []" : `const [${items.join(", ")}]`;
4230
+ }
4231
+ }
4232
+ }
3777
4233
  function appConfigModelJsonKey(camelName, dartType) {
3778
4234
  switch (dartType) {
3779
4235
  case "bool":
@@ -3909,7 +4365,7 @@ function injectIntoRemoteConfigDefaults(vendorsPath, camelName, dartType) {
3909
4365
  console.log(chalk14.green(` Injected into VendorsModule defaultValues`));
3910
4366
  return vendorsPath;
3911
4367
  }
3912
- function injectAppConfigProperty(configPath, camelName, dartType) {
4368
+ function injectAppConfigProperty(configPath, camelName, dartType, defaultValue) {
3913
4369
  const content = fs15.readFileSync(configPath, "utf8");
3914
4370
  if (content.includes(`final ${dartTypeForGetter(dartType)} ${camelName}`)) {
3915
4371
  console.log(chalk14.yellow(` '${camelName}' already in AppConfig`));
@@ -3924,13 +4380,13 @@ function injectAppConfigProperty(configPath, camelName, dartType) {
3924
4380
  } else {
3925
4381
  newContent = injectBeforeClassClose(newContent, "AppConfig", ` final ${dartTypeForGetter(dartType)} ${camelName};`);
3926
4382
  }
3927
- newContent = injectConstructorParam(newContent, "AppConfig", camelName, appConfigDefaultValue(dartType));
4383
+ newContent = injectConstructorParam(newContent, "AppConfig", camelName, dartDefaultLiteral(defaultValue, dartType));
3928
4384
  newContent = injectIntoPropsList(newContent, camelName);
3929
4385
  fs15.writeFileSync(configPath, newContent);
3930
4386
  console.log(chalk14.green(` Injected into AppConfig entity`));
3931
4387
  return configPath;
3932
4388
  }
3933
- function injectAppConfigModelProperty(modelPath, camelName, dartType) {
4389
+ function injectAppConfigModelProperty(modelPath, camelName, dartType, defaultValue) {
3934
4390
  const content = fs15.readFileSync(modelPath, "utf8");
3935
4391
  if (content.includes(`final ${dartTypeForGetter(dartType)} ${camelName}`)) {
3936
4392
  console.log(chalk14.yellow(` '${camelName}' already in AppConfigModel`));
@@ -3957,7 +4413,12 @@ function injectAppConfigModelProperty(modelPath, camelName, dartType) {
3957
4413
  } else {
3958
4414
  newContent = injectBeforeClassClose(newContent, "AppConfigModel", fieldLines);
3959
4415
  }
3960
- newContent = injectConstructorParam(newContent, "AppConfigModel", camelName, dartType);
4416
+ newContent = injectConstructorParam(
4417
+ newContent,
4418
+ "AppConfigModel",
4419
+ camelName,
4420
+ dartDefaultLiteral(defaultValue, dartType)
4421
+ );
3961
4422
  newContent = injectIntoSuperCall(newContent, camelName);
3962
4423
  fs15.writeFileSync(modelPath, newContent);
3963
4424
  console.log(chalk14.green(` Injected into AppConfigModel`));
@@ -4008,14 +4469,14 @@ Adding environment variable: ${chalk14.cyan(variableName)}
4008
4469
  const jsonValue = typedJsonValue(defaultValue, dartType);
4009
4470
  const appsDir = path15.join(monorepoRoot, "apps");
4010
4471
  for (const app of selectedApps) {
4011
- const appEnvDir = path15.join(appsDir, app, "env");
4472
+ const appEnvDir2 = path15.join(appsDir, app, "env");
4012
4473
  const files = [
4013
4474
  { name: "example.env.json", isTemplate: true },
4014
4475
  { name: "development.env.json", isTemplate: false },
4015
4476
  { name: "production.env.json", isTemplate: false }
4016
4477
  ];
4017
4478
  for (const { name, isTemplate } of files) {
4018
- const modified = injectIntoJsonFile(path15.join(appEnvDir, name), variableName, jsonValue, isTemplate);
4479
+ const modified = injectIntoJsonFile(path15.join(appEnvDir2, name), variableName, jsonValue, isTemplate);
4019
4480
  if (modified) modifiedFiles.push(modified);
4020
4481
  }
4021
4482
  }
@@ -4043,7 +4504,7 @@ Adding environment variable: ${chalk14.cyan(variableName)}
4043
4504
  spinner7("PASO 5: Injecting into AppConfig entity");
4044
4505
  const configPath = findAppConfigFile(monorepoRoot);
4045
4506
  if (configPath) {
4046
- const modified = injectAppConfigProperty(configPath, camelName, dartType);
4507
+ const modified = injectAppConfigProperty(configPath, camelName, dartType, defaultValue);
4047
4508
  if (modified) modifiedFiles.push(modified);
4048
4509
  } else {
4049
4510
  console.log(chalk14.yellow(" app_config.dart not found, skipping AppConfig"));
@@ -4051,7 +4512,7 @@ Adding environment variable: ${chalk14.cyan(variableName)}
4051
4512
  spinner7("PASO 6: Injecting into AppConfigModel");
4052
4513
  const modelPath = findAppConfigModelFile(monorepoRoot);
4053
4514
  if (modelPath) {
4054
- const modified = injectAppConfigModelProperty(modelPath, camelName, dartType);
4515
+ const modified = injectAppConfigModelProperty(modelPath, camelName, dartType, defaultValue);
4055
4516
  if (modified) modifiedFiles.push(modified);
4056
4517
  } else {
4057
4518
  console.log(chalk14.yellow(" app_config_model.dart not found, skipping AppConfigModel"));
@@ -4109,15 +4570,26 @@ async function envVarFlow() {
4109
4570
  return;
4110
4571
  }
4111
4572
  const selectedType = dartType;
4112
- const defaultValue = await clack8.text({
4113
- message: "Default value (leave empty to skip)",
4114
- placeholder: selectedType === "bool" ? "false" : selectedType === "int" ? "0" : "",
4115
- validate: (v) => {
4116
- if (!v || !v.trim()) return void 0;
4117
- if (selectedType === "int" && !/^-?\d+$/.test(v)) return "Must be an integer";
4118
- if (selectedType === "bool" && !["true", "false"].includes(v)) return "Must be true or false";
4119
- }
4120
- });
4573
+ let defaultValue;
4574
+ if (selectedType === "bool") {
4575
+ defaultValue = await clack8.select({
4576
+ message: "Default value",
4577
+ options: [
4578
+ { value: "false", label: "false" },
4579
+ { value: "true", label: "true" }
4580
+ ],
4581
+ initialValue: "false"
4582
+ });
4583
+ } else {
4584
+ defaultValue = await clack8.text({
4585
+ message: "Default value (leave empty to skip)",
4586
+ placeholder: selectedType === "int" ? "0" : "",
4587
+ validate: (v) => {
4588
+ if (!v || !v.trim()) return void 0;
4589
+ if (selectedType === "int" && !/^-?\d+$/.test(v)) return "Must be an integer";
4590
+ }
4591
+ });
4592
+ }
4121
4593
  if (clack8.isCancel(defaultValue)) {
4122
4594
  clack8.cancel("Cancelled");
4123
4595
  return;
@@ -4207,9 +4679,9 @@ import { execSync as execSync4 } from "child_process";
4207
4679
  import chalk16 from "chalk";
4208
4680
 
4209
4681
  // src/generators/app/app-templates.ts
4210
- import { pascalCase as pascalCase7 } from "change-case";
4682
+ import { pascalCase as pascalCase8 } from "change-case";
4211
4683
  function pascal(name) {
4212
- return pascalCase7(name);
4684
+ return pascalCase8(name);
4213
4685
  }
4214
4686
  function pubspecTemplate(params, pubWorkspace = false) {
4215
4687
  if (pubWorkspace) {
@@ -7589,11 +8061,11 @@ import * as fs19 from "fs";
7589
8061
  import * as path19 from "path";
7590
8062
  import { execSync as execSync7 } from "child_process";
7591
8063
  import chalk19 from "chalk";
7592
- import { pascalCase as pascalCase8, camelCase as camelCase4 } from "change-case";
8064
+ import { pascalCase as pascalCase9, camelCase as camelCase4 } from "change-case";
7593
8065
  var SNAKE_CASE_REGEX6 = /^[a-z][a-z0-9_]*$/;
7594
8066
  async function createCollaborativeEndpoint(options) {
7595
8067
  const { featurePath, featureName } = options;
7596
- const pascal2 = pascalCase8(options.useCaseName);
8068
+ const pascal2 = pascalCase9(options.useCaseName);
7597
8069
  const needsBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod);
7598
8070
  const pathParams = extractPathParams2(options.endpointPath);
7599
8071
  const lib = path19.join(featurePath, "lib");
@@ -7629,7 +8101,7 @@ async function createCollaborativeEndpoint(options) {
7629
8101
  fs19.mkdirSync(bffDir, { recursive: true });
7630
8102
  const domain = featureName.replace(/^feature_/, "");
7631
8103
  const bffApiFile = path19.join(bffDir, `bff_${domain}_api.dart`);
7632
- const bffApiClass = `Bff${pascalCase8(domain)}Api`;
8104
+ const bffApiClass = `Bff${pascalCase9(domain)}Api`;
7633
8105
  if (fs19.existsSync(bffApiFile)) {
7634
8106
  const method = bffApiMethodSnippet(
7635
8107
  camelCase4(options.useCaseName),
@@ -7660,7 +8132,7 @@ async function createCollaborativeEndpoint(options) {
7660
8132
  "datasources",
7661
8133
  `${featureName}_datasource_fake.dart`
7662
8134
  );
7663
- const dsClass = `${pascalCase8(featureName)}DatasourceFake`;
8135
+ const dsClass = `${pascalCase9(featureName)}DatasourceFake`;
7664
8136
  if (fs19.existsSync(datasourceFile)) {
7665
8137
  const dsMethod = datasourceMethodSnippet(
7666
8138
  camelCase4(options.useCaseName),
@@ -7676,7 +8148,7 @@ async function createCollaborativeEndpoint(options) {
7676
8148
  "repositories",
7677
8149
  `${featureName}_repository.dart`
7678
8150
  );
7679
- const repoInterface = `${pascalCase8(featureName)}Repository`;
8151
+ const repoInterface = `${pascalCase9(featureName)}Repository`;
7680
8152
  if (fs19.existsSync(repoInterfaceFile)) {
7681
8153
  const ifaceMethod = repositoryInterfaceMethodSnippet(
7682
8154
  camelCase4(options.useCaseName),
@@ -7692,7 +8164,7 @@ async function createCollaborativeEndpoint(options) {
7692
8164
  "repositories",
7693
8165
  `${featureName}_repository_data.dart`
7694
8166
  );
7695
- const repoImpl = `${pascalCase8(featureName)}RepositoryData`;
8167
+ const repoImpl = `${pascalCase9(featureName)}RepositoryData`;
7696
8168
  if (fs19.existsSync(repoImplFile)) {
7697
8169
  const implMethod = repositoryImplMethodSnippet(
7698
8170
  camelCase4(options.useCaseName),
@@ -7878,7 +8350,7 @@ function updateBarrels(lib, useCaseName, needsBody, domain) {
7878
8350
  }
7879
8351
  function registerEndpointDi(lib, featureName, pascal2, params) {
7880
8352
  const diDir = path19.join(lib, "di");
7881
- const featurePascal = pascalCase8(featureName);
8353
+ const featurePascal = pascalCase9(featureName);
7882
8354
  const ucModulePath = path19.join(diDir, "usecases_module.dart");
7883
8355
  if (fs19.existsSync(ucModulePath)) {
7884
8356
  const useCaseClass = `${pascal2}UseCase`;
@@ -8455,8 +8927,8 @@ function displayArchitecture(info) {
8455
8927
  }
8456
8928
 
8457
8929
  // src/flows/docs-flow.ts
8458
- function normalizeChallenge(text11) {
8459
- return text11.trim().toLowerCase().replace(/[áä]/g, "a").replace(/[éë]/g, "e").replace(/[íï]/g, "i").replace(/[óö]/g, "o").replace(/[úü]/g, "u");
8930
+ function normalizeChallenge(text12) {
8931
+ return text12.trim().toLowerCase().replace(/[áä]/g, "a").replace(/[éë]/g, "e").replace(/[íï]/g, "i").replace(/[óö]/g, "o").replace(/[úü]/g, "u");
8460
8932
  }
8461
8933
  async function docsInteractiveMode() {
8462
8934
  clack11.intro(chalk24.bgCyan(chalk24.black(" wlmaker docs ")));
@@ -8556,12 +9028,1093 @@ async function docsInteractiveMode() {
8556
9028
  }
8557
9029
  }
8558
9030
 
9031
+ // src/flows/vault-flow.ts
9032
+ import { execSync as execSync9 } from "child_process";
9033
+ import { randomBytes as randomBytes3 } from "crypto";
9034
+ import * as fs29 from "fs";
9035
+ import * as clack12 from "@clack/prompts";
9036
+ import chalk25 from "chalk";
9037
+
9038
+ // src/vault/types.ts
9039
+ var ENV_NAMES = ["development", "production"];
9040
+
9041
+ // src/vault/identity.ts
9042
+ import * as crypto2 from "crypto";
9043
+ import * as fs24 from "fs";
9044
+ import * as os6 from "os";
9045
+ import * as path24 from "path";
9046
+
9047
+ // src/vault/crypto.ts
9048
+ import * as crypto from "crypto";
9049
+ var DATA_KEY_BYTES = 32;
9050
+ var NONCE_BYTES = 12;
9051
+ var HKDF_HASH = "sha256";
9052
+ var HKDF_KEY_BYTES = 32;
9053
+ var HKDF_WRAP_INFO = Buffer.from("wlmaker-vault-wrap-v1", "utf8");
9054
+ function generateX25519KeyPair() {
9055
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
9056
+ const publicJwk = publicKey.export({ format: "jwk" });
9057
+ const privateJwk = privateKey.export({ format: "jwk" });
9058
+ return {
9059
+ publicKey: Buffer.from(publicJwk.x, "base64url"),
9060
+ privateKey: Buffer.from(privateJwk.d, "base64url")
9061
+ };
9062
+ }
9063
+ function importX25519PublicKey(publicKey) {
9064
+ return crypto.createPublicKey({
9065
+ key: { kty: "OKP", crv: "X25519", x: publicKey.toString("base64url") },
9066
+ format: "jwk"
9067
+ });
9068
+ }
9069
+ function importX25519PrivateKey(privateKey, publicKey) {
9070
+ return crypto.createPrivateKey({
9071
+ key: {
9072
+ kty: "OKP",
9073
+ crv: "X25519",
9074
+ x: publicKey.toString("base64url"),
9075
+ d: privateKey.toString("base64url")
9076
+ },
9077
+ format: "jwk"
9078
+ });
9079
+ }
9080
+ function deriveSharedSecret(privateKey, publicKeyForEcdh, publicKeyForPrivate) {
9081
+ const privateKeyObj = importX25519PrivateKey(privateKey, publicKeyForPrivate);
9082
+ const publicKeyObj = importX25519PublicKey(publicKeyForEcdh);
9083
+ return crypto.diffieHellman({ privateKey: privateKeyObj, publicKey: publicKeyObj });
9084
+ }
9085
+ function hkdf(ikm, info) {
9086
+ return Buffer.from(crypto.hkdfSync(HKDF_HASH, ikm, Buffer.alloc(0), info, HKDF_KEY_BYTES));
9087
+ }
9088
+ function generateDataKey() {
9089
+ return crypto.randomBytes(DATA_KEY_BYTES);
9090
+ }
9091
+ function wrapDataKey(dataKey, recipientPublicKey) {
9092
+ const ephemeral = generateX25519KeyPair();
9093
+ const shared = deriveSharedSecret(ephemeral.privateKey, recipientPublicKey, ephemeral.publicKey);
9094
+ const wrapKey = hkdf(shared, HKDF_WRAP_INFO);
9095
+ const nonce = crypto.randomBytes(NONCE_BYTES);
9096
+ const cipher = crypto.createCipheriv("aes-256-gcm", wrapKey, nonce);
9097
+ const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
9098
+ const tag = cipher.getAuthTag();
9099
+ return {
9100
+ ephemeralPublicKey: ephemeral.publicKey.toString("base64"),
9101
+ nonce: nonce.toString("base64"),
9102
+ ct: ct.toString("base64"),
9103
+ tag: tag.toString("base64")
9104
+ };
9105
+ }
9106
+ function unwrapDataKey(wrapped, recipientPrivateKey, recipientPublicKey) {
9107
+ const ephemeralPublicKey = Buffer.from(wrapped.ephemeralPublicKey, "base64");
9108
+ const shared = deriveSharedSecret(recipientPrivateKey, ephemeralPublicKey, recipientPublicKey);
9109
+ const wrapKey = hkdf(shared, HKDF_WRAP_INFO);
9110
+ const nonce = Buffer.from(wrapped.nonce, "base64");
9111
+ const tag = Buffer.from(wrapped.tag, "base64");
9112
+ const ct = Buffer.from(wrapped.ct, "base64");
9113
+ const decipher = crypto.createDecipheriv("aes-256-gcm", wrapKey, nonce);
9114
+ decipher.setAuthTag(tag);
9115
+ try {
9116
+ return Buffer.concat([decipher.update(ct), decipher.final()]);
9117
+ } catch {
9118
+ throw new Error("Unable to unwrap data key: wrong key or corrupted entry.");
9119
+ }
9120
+ }
9121
+ function buildAad(vaultId, app, env, varName) {
9122
+ return Buffer.from(`${vaultId}|${app}|${env}|${varName}`, "utf8");
9123
+ }
9124
+ function sealValue(value, dataKey, aad) {
9125
+ const plaintext = Buffer.from(JSON.stringify(value), "utf8");
9126
+ const nonce = crypto.randomBytes(NONCE_BYTES);
9127
+ const cipher = crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
9128
+ cipher.setAAD(aad);
9129
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
9130
+ const tag = cipher.getAuthTag();
9131
+ return {
9132
+ nonce: nonce.toString("base64"),
9133
+ ct: ct.toString("base64"),
9134
+ tag: tag.toString("base64")
9135
+ };
9136
+ }
9137
+ function openValue(sealed, dataKey, aad) {
9138
+ const nonce = Buffer.from(sealed.nonce, "base64");
9139
+ const tag = Buffer.from(sealed.tag, "base64");
9140
+ const ct = Buffer.from(sealed.ct, "base64");
9141
+ const decipher = crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
9142
+ decipher.setAAD(aad);
9143
+ decipher.setAuthTag(tag);
9144
+ let plaintext;
9145
+ try {
9146
+ plaintext = Buffer.concat([decipher.update(ct), decipher.final()]);
9147
+ } catch {
9148
+ throw new Error("Unable to open sealed value: wrong key, wrong coordinate, or corrupted entry.");
9149
+ }
9150
+ return JSON.parse(plaintext.toString("utf8"));
9151
+ }
9152
+
9153
+ // src/vault/identity.ts
9154
+ var IDENTITY_FILE_NAME = "identity";
9155
+ var IDENTITY_DIR_MODE = 448;
9156
+ var IDENTITY_FILE_MODE = 384;
9157
+ var SCRYPT_N = 16384;
9158
+ var SCRYPT_R = 8;
9159
+ var SCRYPT_P = 1;
9160
+ var SCRYPT_KEY_BYTES = 32;
9161
+ var SALT_BYTES = 16;
9162
+ var NONCE_BYTES2 = 12;
9163
+ function getIdentityDir() {
9164
+ return path24.join(os6.homedir(), ".wlmaker");
9165
+ }
9166
+ function getIdentityPath() {
9167
+ return path24.join(getIdentityDir(), IDENTITY_FILE_NAME);
9168
+ }
9169
+ function identityExists() {
9170
+ return fs24.existsSync(getIdentityPath());
9171
+ }
9172
+ function fingerprintFor(publicKey) {
9173
+ return crypto2.createHash("sha256").update(publicKey).digest("hex").slice(0, 16);
9174
+ }
9175
+ function deriveKeyEncryptionKey(passphrase, salt, n, r, p) {
9176
+ return crypto2.scryptSync(passphrase, salt, SCRYPT_KEY_BYTES, { N: n, r, p });
9177
+ }
9178
+ function encryptPrivateKey(privateKey, passphrase) {
9179
+ const salt = crypto2.randomBytes(SALT_BYTES);
9180
+ const key = deriveKeyEncryptionKey(passphrase, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
9181
+ const nonce = crypto2.randomBytes(NONCE_BYTES2);
9182
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
9183
+ const ct = Buffer.concat([cipher.update(privateKey), cipher.final()]);
9184
+ const tag = cipher.getAuthTag();
9185
+ return {
9186
+ salt: salt.toString("base64"),
9187
+ n: SCRYPT_N,
9188
+ r: SCRYPT_R,
9189
+ p: SCRYPT_P,
9190
+ nonce: nonce.toString("base64"),
9191
+ ct: ct.toString("base64"),
9192
+ tag: tag.toString("base64")
9193
+ };
9194
+ }
9195
+ function decryptPrivateKey(encrypted, passphrase) {
9196
+ const salt = Buffer.from(encrypted.salt, "base64");
9197
+ const key = deriveKeyEncryptionKey(passphrase, salt, encrypted.n, encrypted.r, encrypted.p);
9198
+ const nonce = Buffer.from(encrypted.nonce, "base64");
9199
+ const tag = Buffer.from(encrypted.tag, "base64");
9200
+ const ct = Buffer.from(encrypted.ct, "base64");
9201
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
9202
+ decipher.setAuthTag(tag);
9203
+ try {
9204
+ return Buffer.concat([decipher.update(ct), decipher.final()]);
9205
+ } catch {
9206
+ throw new Error("Incorrect passphrase, or the identity file is corrupted.");
9207
+ }
9208
+ }
9209
+ function readIdentityFile() {
9210
+ const raw = fs24.readFileSync(getIdentityPath(), "utf8");
9211
+ return JSON.parse(raw);
9212
+ }
9213
+ function writeIdentityFile(file) {
9214
+ const dir = getIdentityDir();
9215
+ fs24.mkdirSync(dir, { recursive: true, mode: IDENTITY_DIR_MODE });
9216
+ const filePath = getIdentityPath();
9217
+ fs24.writeFileSync(filePath, JSON.stringify(file, null, 2) + "\n", { mode: IDENTITY_FILE_MODE });
9218
+ fs24.chmodSync(filePath, IDENTITY_FILE_MODE);
9219
+ }
9220
+ function createIdentity(passphrase, name) {
9221
+ if (identityExists()) {
9222
+ throw new Error(`Identity already exists at ${getIdentityPath()}.`);
9223
+ }
9224
+ const { publicKey, privateKey } = generateX25519KeyPair();
9225
+ const file = {
9226
+ version: 1,
9227
+ name,
9228
+ publicKey: publicKey.toString("base64"),
9229
+ encryptedPrivateKey: encryptPrivateKey(privateKey, passphrase)
9230
+ };
9231
+ writeIdentityFile(file);
9232
+ return { fingerprint: fingerprintFor(publicKey), name, publicKey, privateKey };
9233
+ }
9234
+ function loadIdentity(passphrase) {
9235
+ if (!identityExists()) {
9236
+ throw new Error(`No identity found at ${getIdentityPath()}. Create one first.`);
9237
+ }
9238
+ const file = readIdentityFile();
9239
+ const publicKey = Buffer.from(file.publicKey, "base64");
9240
+ const privateKey = decryptPrivateKey(file.encryptedPrivateKey, passphrase);
9241
+ return { fingerprint: fingerprintFor(publicKey), name: file.name, publicKey, privateKey };
9242
+ }
9243
+ var cachedIdentity = null;
9244
+ function getCachedIdentity() {
9245
+ return cachedIdentity;
9246
+ }
9247
+ function cacheIdentity(identity) {
9248
+ cachedIdentity = identity;
9249
+ }
9250
+
9251
+ // src/vault/vault-file.ts
9252
+ import * as fs25 from "fs";
9253
+ import { z as z3 } from "zod";
9254
+ var sealedValueSchema = z3.object({
9255
+ nonce: z3.string(),
9256
+ ct: z3.string(),
9257
+ tag: z3.string()
9258
+ });
9259
+ var wrappedDataKeySchema = z3.object({
9260
+ ephemeralPublicKey: z3.string(),
9261
+ nonce: z3.string(),
9262
+ ct: z3.string(),
9263
+ tag: z3.string()
9264
+ });
9265
+ var recipientSchema = z3.object({
9266
+ fingerprint: z3.string(),
9267
+ name: z3.string(),
9268
+ publicKey: z3.string(),
9269
+ wrappedDataKey: wrappedDataKeySchema,
9270
+ addedAt: z3.string(),
9271
+ addedBy: z3.string()
9272
+ });
9273
+ var pendingRequestSchema = z3.object({
9274
+ fingerprint: z3.string(),
9275
+ name: z3.string(),
9276
+ publicKey: z3.string(),
9277
+ requestedAt: z3.string()
9278
+ });
9279
+ var envValuesSchema = z3.record(z3.string(), sealedValueSchema);
9280
+ var envRecordSchema = z3.object({
9281
+ development: envValuesSchema.default({}),
9282
+ production: envValuesSchema.default({})
9283
+ });
9284
+ var appsSchema = z3.record(z3.string(), envRecordSchema);
9285
+ var vaultFileSchema = z3.object({
9286
+ version: z3.literal(1),
9287
+ vaultId: z3.string().min(1),
9288
+ keyringVersion: z3.number().int().nonnegative(),
9289
+ recipients: z3.array(recipientSchema),
9290
+ pending: z3.array(pendingRequestSchema),
9291
+ apps: appsSchema
9292
+ });
9293
+ function parseVault(raw) {
9294
+ let json;
9295
+ try {
9296
+ json = JSON.parse(raw);
9297
+ } catch (error) {
9298
+ throw new Error(`Vault file is not valid JSON: ${error.message}`);
9299
+ }
9300
+ const result = vaultFileSchema.safeParse(json);
9301
+ if (!result.success) {
9302
+ throw new Error(`Vault file failed schema validation: ${result.error.message}`);
9303
+ }
9304
+ return result.data;
9305
+ }
9306
+ function sortedKeys(record) {
9307
+ return Object.keys(record).sort((a, b) => a.localeCompare(b));
9308
+ }
9309
+ function canonicalize(vault) {
9310
+ const apps = {};
9311
+ for (const app of sortedKeys(vault.apps)) {
9312
+ const envs = vault.apps[app];
9313
+ const orderedEnvs = {};
9314
+ for (const env of ENV_NAMES) {
9315
+ const values = envs[env] ?? {};
9316
+ const orderedValues = {};
9317
+ for (const key of sortedKeys(values)) {
9318
+ orderedValues[key] = values[key];
9319
+ }
9320
+ orderedEnvs[env] = orderedValues;
9321
+ }
9322
+ apps[app] = orderedEnvs;
9323
+ }
9324
+ return {
9325
+ version: vault.version,
9326
+ vaultId: vault.vaultId,
9327
+ keyringVersion: vault.keyringVersion,
9328
+ recipients: vault.recipients,
9329
+ pending: vault.pending,
9330
+ apps
9331
+ };
9332
+ }
9333
+ function serializeVault(vault) {
9334
+ return JSON.stringify(canonicalize(vault), null, 2) + "\n";
9335
+ }
9336
+ function readVaultFile(filePath) {
9337
+ if (!fs25.existsSync(filePath)) {
9338
+ throw new Error(`Vault file not found at ${filePath}. Run "wlmaker vault init" first.`);
9339
+ }
9340
+ const raw = fs25.readFileSync(filePath, "utf8");
9341
+ return parseVault(raw);
9342
+ }
9343
+ function writeVaultFile(filePath, vault) {
9344
+ fs25.writeFileSync(filePath, serializeVault(vault));
9345
+ }
9346
+ function emptyVault(vaultId) {
9347
+ return {
9348
+ version: 1,
9349
+ vaultId,
9350
+ keyringVersion: 1,
9351
+ recipients: [],
9352
+ pending: [],
9353
+ apps: {}
9354
+ };
9355
+ }
9356
+
9357
+ // src/vault/discovery.ts
9358
+ import * as fs26 from "fs";
9359
+ import * as path25 from "path";
9360
+ var EXAMPLE_ENV_FILE = "example.env.json";
9361
+ var VAULT_FILE_NAME = ".wlmaker.vault.json";
9362
+ function vaultPath(monorepoRoot) {
9363
+ return path25.join(monorepoRoot, VAULT_FILE_NAME);
9364
+ }
9365
+ var ENV_FILE_NAMES = {
9366
+ development: "development.env.json",
9367
+ production: "production.env.json"
9368
+ };
9369
+ function appEnvDir(monorepoRoot, app) {
9370
+ return path25.join(monorepoRoot, "apps", app, "env");
9371
+ }
9372
+ function envFilePath(monorepoRoot, app, env) {
9373
+ return path25.join(appEnvDir(monorepoRoot, app), ENV_FILE_NAMES[env]);
9374
+ }
9375
+ function exampleEnvPath(monorepoRoot, app) {
9376
+ return path25.join(appEnvDir(monorepoRoot, app), EXAMPLE_ENV_FILE);
9377
+ }
9378
+ function readLocalEnvFile(monorepoRoot, app, env) {
9379
+ const filePath = envFilePath(monorepoRoot, app, env);
9380
+ if (!fs26.existsSync(filePath)) return null;
9381
+ const raw = fs26.readFileSync(filePath, "utf8");
9382
+ return JSON.parse(raw);
9383
+ }
9384
+ function readAllowlist(monorepoRoot, app) {
9385
+ const filePath = exampleEnvPath(monorepoRoot, app);
9386
+ if (!fs26.existsSync(filePath)) return [];
9387
+ const raw = fs26.readFileSync(filePath, "utf8");
9388
+ const json = JSON.parse(raw);
9389
+ return Object.keys(json);
9390
+ }
9391
+
9392
+ // src/vault/ops/shared.ts
9393
+ var NotAnApprovedRecipientError = class extends Error {
9394
+ constructor(fingerprint) {
9395
+ super(
9396
+ `Identity ${fingerprint} is not an approved recipient of this vault. Ask an existing recipient to approve your request first.`
9397
+ );
9398
+ this.name = "NotAnApprovedRecipientError";
9399
+ }
9400
+ };
9401
+ function findRecipient(vault, fingerprint) {
9402
+ return vault.recipients.find((recipient) => recipient.fingerprint === fingerprint);
9403
+ }
9404
+ function resolveDataKey(vault, identity) {
9405
+ const recipient = findRecipient(vault, identity.fingerprint);
9406
+ if (!recipient) {
9407
+ throw new NotAnApprovedRecipientError(identity.fingerprint);
9408
+ }
9409
+ return unwrapDataKey(recipient.wrappedDataKey, identity.privateKey, identity.publicKey);
9410
+ }
9411
+
9412
+ // src/vault/ops/capture.ts
9413
+ function emptyAppEnvs() {
9414
+ return { development: {}, production: {} };
9415
+ }
9416
+ function captureApp(vault, identity, options) {
9417
+ const dataKey = resolveDataKey(vault, identity);
9418
+ const envs = options.envs ?? ENV_NAMES;
9419
+ const allowlist = new Set(readAllowlist(options.monorepoRoot, options.app));
9420
+ const existing = vault.apps[options.app] ?? emptyAppEnvs();
9421
+ const nextApp = {
9422
+ development: { ...existing.development },
9423
+ production: { ...existing.production }
9424
+ };
9425
+ const captured = [];
9426
+ const skippedEnvs = [];
9427
+ const refusedKeys = [];
9428
+ for (const env of envs) {
9429
+ const local = readLocalEnvFile(options.monorepoRoot, options.app, env);
9430
+ if (local === null) {
9431
+ skippedEnvs.push({
9432
+ env,
9433
+ reason: `No local file at ${envFilePath(options.monorepoRoot, options.app, env)}; skipping "${env}" (nothing captured, no empty file created).`
9434
+ });
9435
+ continue;
9436
+ }
9437
+ const sealedEnv = { ...nextApp[env] };
9438
+ for (const [key, value] of Object.entries(local)) {
9439
+ if (!allowlist.has(key) && !options.force) {
9440
+ refusedKeys.push({ env, key });
9441
+ continue;
9442
+ }
9443
+ const aad = buildAad(vault.vaultId, options.app, env, key);
9444
+ sealedEnv[key] = sealValue(value, dataKey, aad);
9445
+ captured.push({ env, key });
9446
+ }
9447
+ nextApp[env] = sealedEnv;
9448
+ }
9449
+ return {
9450
+ vault: { ...vault, apps: { ...vault.apps, [options.app]: nextApp } },
9451
+ captured,
9452
+ skippedEnvs,
9453
+ refusedKeys
9454
+ };
9455
+ }
9456
+
9457
+ // src/vault/ops/apply.ts
9458
+ import * as fs28 from "fs";
9459
+
9460
+ // src/vault/gitignore-guard.ts
9461
+ import * as fs27 from "fs";
9462
+ import * as path26 from "path";
9463
+ function escapeRegExpLiteral(char) {
9464
+ return /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
9465
+ }
9466
+ function globToRegExp(pattern) {
9467
+ let source = "";
9468
+ for (const char of pattern) {
9469
+ if (char === "*") source += "[^/]*";
9470
+ else if (char === "?") source += "[^/]";
9471
+ else source += escapeRegExpLiteral(char);
9472
+ }
9473
+ return new RegExp(`^${source}$`);
9474
+ }
9475
+ function parseGitignore(content) {
9476
+ const rules = [];
9477
+ for (const rawLine of content.split("\n")) {
9478
+ const line = rawLine.trim();
9479
+ if (!line || line.startsWith("#")) continue;
9480
+ const negated = line.startsWith("!");
9481
+ const pattern = negated ? line.slice(1) : line;
9482
+ const normalized = pattern.replace(/^\/+/, "").replace(/\/+$/, "");
9483
+ if (!normalized) continue;
9484
+ rules.push({ negated, regex: globToRegExp(normalized) });
9485
+ }
9486
+ return rules;
9487
+ }
9488
+ function isIgnoredByRules(rules, fileName) {
9489
+ let ignored = false;
9490
+ for (const rule of rules) {
9491
+ if (rule.regex.test(fileName)) {
9492
+ ignored = !rule.negated;
9493
+ }
9494
+ }
9495
+ return ignored;
9496
+ }
9497
+ function assertIgnored(dir, fileName) {
9498
+ const gitignorePath = path26.join(dir, ".gitignore");
9499
+ if (!fs27.existsSync(gitignorePath)) {
9500
+ throw new Error(
9501
+ `Refusing to write ${fileName}: no .gitignore found at ${gitignorePath}. Add "*.json" and "!example.env.json" to it before applying.`
9502
+ );
9503
+ }
9504
+ const content = fs27.readFileSync(gitignorePath, "utf8");
9505
+ if (!content.trim()) {
9506
+ throw new Error(
9507
+ `Refusing to write ${fileName}: ${gitignorePath} is empty. Add "*.json" and "!example.env.json" to it before applying.`
9508
+ );
9509
+ }
9510
+ const rules = parseGitignore(content);
9511
+ if (!isIgnoredByRules(rules, fileName)) {
9512
+ throw new Error(
9513
+ `Refusing to write ${fileName}: it is not covered by an ignore rule in ${gitignorePath}. This file could be committed to Git.`
9514
+ );
9515
+ }
9516
+ }
9517
+
9518
+ // src/vault/ops/apply.ts
9519
+ var ENV_FILE_NAME = {
9520
+ development: "development.env.json",
9521
+ production: "production.env.json"
9522
+ };
9523
+ function applyApp(vault, identity, options) {
9524
+ const dataKey = resolveDataKey(vault, identity);
9525
+ const envs = options.envs ?? ENV_NAMES;
9526
+ const appEntry = vault.apps[options.app];
9527
+ const applied = [];
9528
+ const skippedEnvs = [];
9529
+ for (const env of envs) {
9530
+ const sealedValues = appEntry?.[env] ?? {};
9531
+ const keys = Object.keys(sealedValues);
9532
+ if (keys.length === 0) {
9533
+ skippedEnvs.push({
9534
+ env,
9535
+ reason: `Nothing captured for "${options.app}" / "${env}" yet; run capture first.`
9536
+ });
9537
+ continue;
9538
+ }
9539
+ const dir = appEnvDir(options.monorepoRoot, options.app);
9540
+ const fileName = ENV_FILE_NAME[env];
9541
+ assertIgnored(dir, fileName);
9542
+ const plaintext = {};
9543
+ for (const key of keys) {
9544
+ const aad = buildAad(vault.vaultId, options.app, env, key);
9545
+ plaintext[key] = openValue(sealedValues[key], dataKey, aad);
9546
+ }
9547
+ const filePath = envFilePath(options.monorepoRoot, options.app, env);
9548
+ const created = !fs28.existsSync(filePath);
9549
+ fs28.writeFileSync(filePath, JSON.stringify(plaintext, null, 2) + "\n");
9550
+ applied.push({ env, filePath, keysWritten: keys.length, created });
9551
+ }
9552
+ return { applied, skippedEnvs };
9553
+ }
9554
+
9555
+ // src/vault/ops/diff.ts
9556
+ function diffApp(vault, identity, options) {
9557
+ const dataKey = resolveDataKey(vault, identity);
9558
+ const envs = options.envs ?? ENV_NAMES;
9559
+ return envs.map((env) => {
9560
+ const local = readLocalEnvFile(options.monorepoRoot, options.app, env) ?? {};
9561
+ const sealedValues = vault.apps[options.app]?.[env] ?? {};
9562
+ const keys = /* @__PURE__ */ new Set([...Object.keys(local), ...Object.keys(sealedValues)]);
9563
+ const entries = Array.from(keys).sort((a, b) => a.localeCompare(b)).map((key) => {
9564
+ const hasLocal = Object.prototype.hasOwnProperty.call(local, key);
9565
+ const hasVault = Object.prototype.hasOwnProperty.call(sealedValues, key);
9566
+ if (hasLocal && !hasVault) return { key, status: "local-only" };
9567
+ if (!hasLocal && hasVault) return { key, status: "vault-only" };
9568
+ const aad = buildAad(vault.vaultId, options.app, env, key);
9569
+ const vaultValue = openValue(sealedValues[key], dataKey, aad);
9570
+ const same = JSON.stringify(vaultValue) === JSON.stringify(local[key]);
9571
+ return { key, status: same ? "same" : "differs" };
9572
+ });
9573
+ return { env, entries };
9574
+ });
9575
+ }
9576
+
9577
+ // src/vault/ops/status.ts
9578
+ function resolveOwnAccessState(vault, fingerprint) {
9579
+ if (vault.recipients.some((recipient) => recipient.fingerprint === fingerprint)) return "approved";
9580
+ if (vault.pending.some((pending) => pending.fingerprint === fingerprint)) return "pending";
9581
+ return "no-access";
9582
+ }
9583
+ function getVaultStatus(vault, identity) {
9584
+ const apps = Object.keys(vault.apps).sort((a, b) => a.localeCompare(b)).map((app) => ({
9585
+ app,
9586
+ envs: ENV_NAMES.map((env) => ({
9587
+ env,
9588
+ keyCount: Object.keys(vault.apps[app][env] ?? {}).length
9589
+ }))
9590
+ }));
9591
+ return {
9592
+ vaultId: vault.vaultId,
9593
+ keyringVersion: vault.keyringVersion,
9594
+ recipientCount: vault.recipients.length,
9595
+ pendingCount: vault.pending.length,
9596
+ apps,
9597
+ ownFingerprint: identity.fingerprint,
9598
+ ownAccessState: resolveOwnAccessState(vault, identity.fingerprint)
9599
+ };
9600
+ }
9601
+
9602
+ // src/vault/ops/access.ts
9603
+ var MIN_RECOMMENDED_RECIPIENTS = 2;
9604
+ function initVault(existingVault, identity, options) {
9605
+ if (existingVault) {
9606
+ throw new Error(
9607
+ "A vault already exists. Refusing to re-initialize an existing vault \u2014 this would orphan every value already sealed under its current data key."
9608
+ );
9609
+ }
9610
+ const dataKey = generateDataKey();
9611
+ const owner = {
9612
+ fingerprint: identity.fingerprint,
9613
+ name: options.name,
9614
+ publicKey: identity.publicKey.toString("base64"),
9615
+ wrappedDataKey: wrapDataKey(dataKey, identity.publicKey),
9616
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
9617
+ addedBy: identity.fingerprint
9618
+ };
9619
+ const vault = { ...emptyVault(options.vaultId), recipients: [owner] };
9620
+ const warning = vault.recipients.length < MIN_RECOMMENDED_RECIPIENTS ? "Only 1 recipient so far (the operator). This vault is a single point of failure until at least one teammate requests access and is approved." : void 0;
9621
+ return { vault, warning };
9622
+ }
9623
+ function requestAccess(vault, identity, name) {
9624
+ if (findRecipient(vault, identity.fingerprint)) {
9625
+ throw new Error(`Identity ${identity.fingerprint} is already an approved recipient of this vault.`);
9626
+ }
9627
+ if (vault.pending.some((request2) => request2.fingerprint === identity.fingerprint)) {
9628
+ throw new Error(`Identity ${identity.fingerprint} already has a pending request for this vault.`);
9629
+ }
9630
+ const request = {
9631
+ fingerprint: identity.fingerprint,
9632
+ name: name ?? identity.name,
9633
+ publicKey: identity.publicKey.toString("base64"),
9634
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
9635
+ };
9636
+ return { ...vault, pending: [...vault.pending, request] };
9637
+ }
9638
+ function approveAccess(vault, approver, requesterFingerprint) {
9639
+ const dataKey = resolveDataKey(vault, approver);
9640
+ const request = vault.pending.find((pending) => pending.fingerprint === requesterFingerprint);
9641
+ if (!request) {
9642
+ throw new Error(`No pending request with fingerprint ${requesterFingerprint} to approve.`);
9643
+ }
9644
+ const newRecipient = {
9645
+ fingerprint: request.fingerprint,
9646
+ name: request.name,
9647
+ publicKey: request.publicKey,
9648
+ wrappedDataKey: wrapDataKey(dataKey, Buffer.from(request.publicKey, "base64")),
9649
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
9650
+ addedBy: approver.fingerprint
9651
+ };
9652
+ return {
9653
+ ...vault,
9654
+ recipients: [...vault.recipients, newRecipient],
9655
+ pending: vault.pending.filter((pending) => pending.fingerprint !== requesterFingerprint)
9656
+ };
9657
+ }
9658
+ function listRecipients(vault) {
9659
+ return {
9660
+ recipients: vault.recipients.map(({ fingerprint, name, addedAt, addedBy }) => ({
9661
+ fingerprint,
9662
+ name,
9663
+ addedAt,
9664
+ addedBy
9665
+ })),
9666
+ pending: vault.pending.map(({ fingerprint, name, requestedAt }) => ({ fingerprint, name, requestedAt }))
9667
+ };
9668
+ }
9669
+ function describeIdentity(identity) {
9670
+ return {
9671
+ fingerprint: identity.fingerprint,
9672
+ name: identity.name,
9673
+ publicKey: identity.publicKey.toString("base64")
9674
+ };
9675
+ }
9676
+ function revokeAccess(vault, revoker, targetFingerprint) {
9677
+ const target = findRecipient(vault, targetFingerprint);
9678
+ if (!target) {
9679
+ throw new Error(`No approved recipient with fingerprint ${targetFingerprint} to revoke.`);
9680
+ }
9681
+ const oldDataKey = resolveDataKey(vault, revoker);
9682
+ const newDataKey = generateDataKey();
9683
+ const remainingRecipients = vault.recipients.filter((recipient) => recipient.fingerprint !== targetFingerprint).map((recipient) => ({
9684
+ ...recipient,
9685
+ wrappedDataKey: wrapDataKey(newDataKey, Buffer.from(recipient.publicKey, "base64"))
9686
+ }));
9687
+ const nextApps = {};
9688
+ for (const app of Object.keys(vault.apps)) {
9689
+ const envs = vault.apps[app];
9690
+ const nextEnvs = { development: {}, production: {} };
9691
+ for (const env of ENV_NAMES) {
9692
+ const values = envs[env] ?? {};
9693
+ const nextValues = {};
9694
+ for (const [key, sealed] of Object.entries(values)) {
9695
+ const aad = buildAad(vault.vaultId, app, env, key);
9696
+ const plaintext = openValue(sealed, oldDataKey, aad);
9697
+ nextValues[key] = sealValue(plaintext, newDataKey, aad);
9698
+ }
9699
+ nextEnvs[env] = nextValues;
9700
+ }
9701
+ nextApps[app] = nextEnvs;
9702
+ }
9703
+ return {
9704
+ ...vault,
9705
+ keyringVersion: vault.keyringVersion + 1,
9706
+ recipients: remainingRecipients,
9707
+ apps: nextApps
9708
+ };
9709
+ }
9710
+
9711
+ // src/flows/vault-flow.ts
9712
+ function gitUserEmail() {
9713
+ try {
9714
+ return execSync9("git config user.email", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
9715
+ } catch {
9716
+ return "";
9717
+ }
9718
+ }
9719
+ async function promptRecipientName() {
9720
+ const defaultName = gitUserEmail();
9721
+ const name = await clack12.text({
9722
+ message: "Your name/email for this vault (shown to teammates in Who)",
9723
+ placeholder: defaultName || "you@example.com",
9724
+ initialValue: defaultName || void 0,
9725
+ validate: (v) => {
9726
+ if (!v || !v.trim()) return "Name is required";
9727
+ }
9728
+ });
9729
+ if (clack12.isCancel(name)) {
9730
+ clack12.cancel("Cancelled");
9731
+ return null;
9732
+ }
9733
+ return name.trim();
9734
+ }
9735
+ async function ensureIdentity() {
9736
+ const cached = getCachedIdentity();
9737
+ if (cached) return cached;
9738
+ if (!identityExists()) {
9739
+ clack12.log.info("No local identity found yet \u2014 creating one.");
9740
+ const name = await promptRecipientName();
9741
+ if (name === null) return null;
9742
+ const passphrase2 = await clack12.password({ message: "Choose a passphrase to encrypt your identity key" });
9743
+ if (clack12.isCancel(passphrase2)) {
9744
+ clack12.cancel("Cancelled");
9745
+ return null;
9746
+ }
9747
+ const confirmPassphrase = await clack12.password({ message: "Confirm passphrase" });
9748
+ if (clack12.isCancel(confirmPassphrase)) {
9749
+ clack12.cancel("Cancelled");
9750
+ return null;
9751
+ }
9752
+ if (passphrase2 !== confirmPassphrase) {
9753
+ clack12.log.error("Passphrases did not match.");
9754
+ return null;
9755
+ }
9756
+ const identity = createIdentity(passphrase2, name);
9757
+ cacheIdentity(identity);
9758
+ clack12.log.success(
9759
+ `Identity created at ${chalk25.cyan("~/.wlmaker/identity")} (fingerprint ${chalk25.cyan(identity.fingerprint)}).`
9760
+ );
9761
+ return identity;
9762
+ }
9763
+ const passphrase = await clack12.password({ message: "Enter your identity passphrase" });
9764
+ if (clack12.isCancel(passphrase)) {
9765
+ clack12.cancel("Cancelled");
9766
+ return null;
9767
+ }
9768
+ try {
9769
+ const identity = loadIdentity(passphrase);
9770
+ cacheIdentity(identity);
9771
+ return identity;
9772
+ } catch (error) {
9773
+ clack12.log.error(`${error}`);
9774
+ return null;
9775
+ }
9776
+ }
9777
+ function requireVault(filePath) {
9778
+ if (!fs29.existsSync(filePath)) {
9779
+ clack12.log.error(`No vault found at ${filePath}. Run "wlmaker vault init" first.`);
9780
+ return null;
9781
+ }
9782
+ return readVaultFile(filePath);
9783
+ }
9784
+ async function resolveApp(monorepoRoot, provided, message) {
9785
+ const apps = discoverAppsWithEnv(monorepoRoot);
9786
+ if (apps.length === 0) {
9787
+ clack12.log.error("No apps with an env/ directory found in this monorepo.");
9788
+ return null;
9789
+ }
9790
+ if (provided) {
9791
+ if (!apps.includes(provided)) {
9792
+ clack12.log.error(`App "${provided}" not found (or has no env/ directory). Available: ${apps.join(", ")}`);
9793
+ return null;
9794
+ }
9795
+ return provided;
9796
+ }
9797
+ const selection = await clack12.select({ message, options: apps.map((a) => ({ value: a, label: a })) });
9798
+ if (clack12.isCancel(selection)) {
9799
+ clack12.cancel("Cancelled");
9800
+ return null;
9801
+ }
9802
+ return selection;
9803
+ }
9804
+ async function runVaultStatus(monorepoRoot) {
9805
+ const identity = await ensureIdentity();
9806
+ if (!identity) return;
9807
+ const vault = requireVault(vaultPath(monorepoRoot));
9808
+ if (!vault) return;
9809
+ const status = getVaultStatus(vault, identity);
9810
+ clack12.log.info(`Vault ${chalk25.cyan(status.vaultId)} \u2014 keyring v${status.keyringVersion}`);
9811
+ clack12.log.info(`Recipients: ${status.recipientCount} approved, ${status.pendingCount} pending`);
9812
+ clack12.log.info(`Your access: ${chalk25.cyan(status.ownAccessState)}`);
9813
+ if (status.apps.length === 0) {
9814
+ clack12.log.info("No apps captured yet.");
9815
+ return;
9816
+ }
9817
+ for (const app of status.apps) {
9818
+ const summary = app.envs.map((e) => `${e.env}=${e.keyCount}`).join(", ");
9819
+ clack12.log.info(` ${chalk25.cyan(app.app)}: ${summary}`);
9820
+ }
9821
+ }
9822
+ async function runVaultInit(monorepoRoot) {
9823
+ const filePath = vaultPath(monorepoRoot);
9824
+ if (fs29.existsSync(filePath)) {
9825
+ clack12.log.warn(`A vault already exists at ${filePath}.`);
9826
+ return;
9827
+ }
9828
+ const identity = await ensureIdentity();
9829
+ if (!identity) return;
9830
+ const name = await promptRecipientName();
9831
+ if (name === null) return;
9832
+ try {
9833
+ const result = initVault(null, identity, { vaultId: randomBytes3(16).toString("hex"), name });
9834
+ writeVaultFile(filePath, result.vault);
9835
+ clack12.log.success(`Vault created at ${chalk25.cyan(filePath)}.`);
9836
+ if (result.warning) clack12.log.warn(result.warning);
9837
+ } catch (error) {
9838
+ clack12.log.error(`${error}`);
9839
+ }
9840
+ }
9841
+ async function runVaultCapture(monorepoRoot, options = {}) {
9842
+ const identity = await ensureIdentity();
9843
+ if (!identity) return;
9844
+ const filePath = vaultPath(monorepoRoot);
9845
+ const vault = requireVault(filePath);
9846
+ if (!vault) return;
9847
+ const app = await resolveApp(monorepoRoot, options.app, "Select app to capture");
9848
+ if (!app) return;
9849
+ let result;
9850
+ try {
9851
+ result = captureApp(vault, identity, { monorepoRoot, app, envs: options.envs, force: options.force });
9852
+ } catch (error) {
9853
+ clack12.log.error(`${error}`);
9854
+ return;
9855
+ }
9856
+ if (result.refusedKeys.length > 0 && !options.force) {
9857
+ clack12.log.warn(`${result.refusedKeys.length} key(s) are not declared in example.env.json:`);
9858
+ for (const { env, key } of result.refusedKeys) {
9859
+ clack12.log.warn(` ${env}/${key}`);
9860
+ }
9861
+ clack12.log.info('Add them with "wlmaker env-var" first, or force-capture them now.');
9862
+ const typed = await clack12.text({
9863
+ message: 'Type "force" to capture these undeclared key(s) anyway (leave empty to skip them)'
9864
+ });
9865
+ if (!clack12.isCancel(typed) && typed.trim().toLowerCase() === "force") {
9866
+ try {
9867
+ result = captureApp(vault, identity, { monorepoRoot, app, envs: options.envs, force: true });
9868
+ } catch (error) {
9869
+ clack12.log.error(`${error}`);
9870
+ return;
9871
+ }
9872
+ }
9873
+ }
9874
+ for (const { env, reason } of result.skippedEnvs) {
9875
+ clack12.log.info(`Skipped ${env}: ${reason}`);
9876
+ }
9877
+ writeVaultFile(filePath, result.vault);
9878
+ clack12.log.success(`Captured ${result.captured.length} value(s) for ${chalk25.cyan(app)}.`);
9879
+ }
9880
+ async function runVaultApply(monorepoRoot, options = {}) {
9881
+ const identity = await ensureIdentity();
9882
+ if (!identity) return;
9883
+ const filePath = vaultPath(monorepoRoot);
9884
+ const vault = requireVault(filePath);
9885
+ if (!vault) return;
9886
+ const app = await resolveApp(monorepoRoot, options.app, "Select app to apply");
9887
+ if (!app) return;
9888
+ try {
9889
+ const result = applyApp(vault, identity, { monorepoRoot, app, envs: options.envs });
9890
+ for (const outcome of result.applied) {
9891
+ clack12.log.success(
9892
+ `${outcome.env}: wrote ${outcome.keysWritten} key(s) to ${outcome.filePath}${outcome.created ? " (created)" : ""}`
9893
+ );
9894
+ }
9895
+ for (const { env, reason } of result.skippedEnvs) {
9896
+ clack12.log.info(`Skipped ${env}: ${reason}`);
9897
+ }
9898
+ } catch (error) {
9899
+ clack12.log.error(`${error}`);
9900
+ }
9901
+ }
9902
+ async function runVaultDiff(monorepoRoot, options = {}) {
9903
+ const identity = await ensureIdentity();
9904
+ if (!identity) return;
9905
+ const filePath = vaultPath(monorepoRoot);
9906
+ const vault = requireVault(filePath);
9907
+ if (!vault) return;
9908
+ const app = await resolveApp(monorepoRoot, options.app, "Select app to diff");
9909
+ if (!app) return;
9910
+ try {
9911
+ const results = diffApp(vault, identity, { monorepoRoot, app, envs: options.envs });
9912
+ for (const { env, entries } of results) {
9913
+ clack12.log.info(`${chalk25.cyan(env)}:`);
9914
+ if (entries.length === 0) {
9915
+ clack12.log.info(" (nothing)");
9916
+ continue;
9917
+ }
9918
+ for (const { key, status } of entries) {
9919
+ clack12.log.info(` ${key}: ${status}`);
9920
+ }
9921
+ }
9922
+ } catch (error) {
9923
+ clack12.log.error(`${error}`);
9924
+ }
9925
+ }
9926
+ async function runVaultRequestAccess(monorepoRoot) {
9927
+ const identity = await ensureIdentity();
9928
+ if (!identity) return;
9929
+ const filePath = vaultPath(monorepoRoot);
9930
+ const vault = requireVault(filePath);
9931
+ if (!vault) return;
9932
+ const name = await promptRecipientName();
9933
+ if (name === null) return;
9934
+ try {
9935
+ const nextVault = requestAccess(vault, identity, name);
9936
+ writeVaultFile(filePath, nextVault);
9937
+ clack12.log.success(
9938
+ `Access requested (fingerprint ${chalk25.cyan(identity.fingerprint)}). Ask an existing recipient to approve you.`
9939
+ );
9940
+ } catch (error) {
9941
+ clack12.log.error(`${error}`);
9942
+ }
9943
+ }
9944
+ async function runVaultApprove(monorepoRoot) {
9945
+ const identity = await ensureIdentity();
9946
+ if (!identity) return;
9947
+ const filePath = vaultPath(monorepoRoot);
9948
+ const vault = requireVault(filePath);
9949
+ if (!vault) return;
9950
+ if (vault.pending.length === 0) {
9951
+ clack12.log.info("No pending requests.");
9952
+ return;
9953
+ }
9954
+ const selection = await clack12.select({
9955
+ message: "Select a pending request to approve",
9956
+ options: vault.pending.map((p) => ({
9957
+ value: p.fingerprint,
9958
+ label: `${p.name} (${p.fingerprint})`,
9959
+ hint: `requested ${p.requestedAt}`
9960
+ }))
9961
+ });
9962
+ if (clack12.isCancel(selection)) {
9963
+ clack12.cancel("Cancelled");
9964
+ return;
9965
+ }
9966
+ try {
9967
+ const nextVault = approveAccess(vault, identity, selection);
9968
+ writeVaultFile(filePath, nextVault);
9969
+ clack12.log.success(`Approved ${chalk25.cyan(selection)}. They can now capture/apply/diff.`);
9970
+ } catch (error) {
9971
+ clack12.log.error(`${error}`);
9972
+ }
9973
+ }
9974
+ async function runVaultWho(monorepoRoot) {
9975
+ const vault = requireVault(vaultPath(monorepoRoot));
9976
+ if (!vault) return;
9977
+ const { recipients, pending } = listRecipients(vault);
9978
+ clack12.log.info("Approved recipients:");
9979
+ if (recipients.length === 0) {
9980
+ clack12.log.info(" (none)");
9981
+ } else {
9982
+ for (const r of recipients) {
9983
+ clack12.log.info(` ${r.name} \u2014 ${r.fingerprint} (added ${r.addedAt} by ${r.addedBy})`);
9984
+ }
9985
+ }
9986
+ clack12.log.info("Pending requests:");
9987
+ if (pending.length === 0) {
9988
+ clack12.log.info(" (none)");
9989
+ } else {
9990
+ for (const p of pending) {
9991
+ clack12.log.info(` ${p.name} \u2014 ${p.fingerprint} (requested ${p.requestedAt})`);
9992
+ }
9993
+ }
9994
+ }
9995
+ async function runVaultIdentity() {
9996
+ const identity = await ensureIdentity();
9997
+ if (!identity) return;
9998
+ const view = describeIdentity(identity);
9999
+ clack12.log.info(`Name: ${view.name}`);
10000
+ clack12.log.info(`Fingerprint: ${chalk25.cyan(view.fingerprint)}`);
10001
+ clack12.log.info(`Public key: ${view.publicKey}`);
10002
+ clack12.log.info("Share the public key above with an approver when requesting access.");
10003
+ }
10004
+ async function runVaultRevoke(monorepoRoot) {
10005
+ const identity = await ensureIdentity();
10006
+ if (!identity) return;
10007
+ const filePath = vaultPath(monorepoRoot);
10008
+ const vault = requireVault(filePath);
10009
+ if (!vault) return;
10010
+ if (vault.recipients.length === 0) {
10011
+ clack12.log.info("No recipients to revoke.");
10012
+ return;
10013
+ }
10014
+ const selection = await clack12.select({
10015
+ message: "Select a recipient to revoke",
10016
+ options: vault.recipients.map((r) => ({ value: r.fingerprint, label: `${r.name} (${r.fingerprint})` }))
10017
+ });
10018
+ if (clack12.isCancel(selection)) {
10019
+ clack12.cancel("Cancelled");
10020
+ return;
10021
+ }
10022
+ const targetFingerprint = selection;
10023
+ const remainingAfter = vault.recipients.filter((r) => r.fingerprint !== targetFingerprint).length;
10024
+ if (remainingAfter === 0) {
10025
+ clack12.log.warn(chalk25.red("This is the LAST recipient. Revoking will leave NO ONE able to decrypt this vault."));
10026
+ }
10027
+ const confirmRevoke = await clack12.confirm({
10028
+ message: `Revoke ${targetFingerprint}? This rotates the shared data key for everyone else.`,
10029
+ initialValue: false
10030
+ });
10031
+ if (clack12.isCancel(confirmRevoke) || !confirmRevoke) {
10032
+ clack12.cancel("Cancelled");
10033
+ return;
10034
+ }
10035
+ try {
10036
+ const nextVault = revokeAccess(vault, identity, targetFingerprint);
10037
+ writeVaultFile(filePath, nextVault);
10038
+ clack12.log.success(`Revoked ${chalk25.cyan(targetFingerprint)}. Data key rotated (keyring v${nextVault.keyringVersion}).`);
10039
+ } catch (error) {
10040
+ clack12.log.error(`${error}`);
10041
+ }
10042
+ }
10043
+ async function vaultFlow(monorepoRoot) {
10044
+ clack12.intro(chalk25.bgMagenta(chalk25.white(" vault ")));
10045
+ clack12.log.info("Share STG/PROD env values with the team \u2014 encrypted, via Git. See docs/VAULT.md");
10046
+ async function showMenu() {
10047
+ const action = await clack12.select({
10048
+ message: "Vault \u2014 what do you need?",
10049
+ options: [
10050
+ { value: "status", label: "Status", hint: "Apps, recipients, your access" },
10051
+ { value: "init", label: "Init", hint: "Create the vault (once, per monorepo)" },
10052
+ { value: "capture", label: "Capture", hint: "Seal local env values into the vault" },
10053
+ { value: "apply", label: "Apply", hint: "Write vault values to local env files" },
10054
+ { value: "diff", label: "Diff", hint: "Compare local env files vs the vault" },
10055
+ { value: "request-access", label: "Request Access", hint: "Ask to join the shared keyring" },
10056
+ { value: "approve", label: "Approve", hint: "Approve a pending request" },
10057
+ { value: "who", label: "Who", hint: "List recipients and pending requests" },
10058
+ { value: "identity", label: "Identity", hint: "Show your public key / fingerprint" },
10059
+ { value: "revoke", label: "Revoke", hint: "Remove a recipient and rotate the key" },
10060
+ { value: "done", label: "Done", hint: "Back to main menu" }
10061
+ ]
10062
+ });
10063
+ if (clack12.isCancel(action)) {
10064
+ clack12.cancel("Cancelled");
10065
+ return;
10066
+ }
10067
+ switch (action) {
10068
+ case "status":
10069
+ await runVaultStatus(monorepoRoot);
10070
+ break;
10071
+ case "init":
10072
+ await runVaultInit(monorepoRoot);
10073
+ break;
10074
+ case "capture":
10075
+ await runVaultCapture(monorepoRoot, {});
10076
+ break;
10077
+ case "apply":
10078
+ await runVaultApply(monorepoRoot, {});
10079
+ break;
10080
+ case "diff":
10081
+ await runVaultDiff(monorepoRoot, {});
10082
+ break;
10083
+ case "request-access":
10084
+ await runVaultRequestAccess(monorepoRoot);
10085
+ break;
10086
+ case "approve":
10087
+ await runVaultApprove(monorepoRoot);
10088
+ break;
10089
+ case "who":
10090
+ await runVaultWho(monorepoRoot);
10091
+ break;
10092
+ case "identity":
10093
+ await runVaultIdentity();
10094
+ break;
10095
+ case "revoke":
10096
+ await runVaultRevoke(monorepoRoot);
10097
+ break;
10098
+ case "done":
10099
+ clack12.outro(chalk25.green("Done!"));
10100
+ return;
10101
+ }
10102
+ await showMenu();
10103
+ }
10104
+ await showMenu();
10105
+ }
10106
+
8559
10107
  // src/flows/main-menu.ts
8560
10108
  async function interactiveMode() {
8561
- clack12.intro(chalk25.bgCyan(chalk25.black(" wlmaker ")));
8562
- const createType = await clack12.select({
8563
- message: "What do you want to create?",
10109
+ clack13.intro(chalk26.bgCyan(chalk26.black(" wlmaker ")));
10110
+ const createType = await clack13.select({
10111
+ message: "What do you want to do?",
8564
10112
  options: [
10113
+ {
10114
+ value: "vault",
10115
+ label: chalk26.bold("Vault"),
10116
+ hint: chalk26.cyan("NEW \u2014 sync encrypted STG/PROD env values with the team")
10117
+ },
8565
10118
  { value: "app", label: "App", hint: "Create and manage apps" },
8566
10119
  { value: "bloc", label: "BLoC", hint: "State management" },
8567
10120
  { value: "widget", label: "Widget", hint: "Design system component" },
@@ -8574,12 +10127,25 @@ async function interactiveMode() {
8574
10127
  { value: "docs", label: "Docs", hint: "Project documentation tools" }
8575
10128
  ]
8576
10129
  });
8577
- if (clack12.isCancel(createType)) {
8578
- clack12.cancel("Cancelled");
10130
+ if (clack13.isCancel(createType)) {
10131
+ clack13.cancel("Cancelled");
8579
10132
  return;
8580
10133
  }
8581
10134
  const type = createType;
8582
10135
  switch (type) {
10136
+ case "vault": {
10137
+ const monorepoRoot = findMonorepoRoot(process.cwd());
10138
+ if (!monorepoRoot) {
10139
+ clack13.outro(
10140
+ chalk26.red(
10141
+ "Not inside a monorepo. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."
10142
+ )
10143
+ );
10144
+ return;
10145
+ }
10146
+ await vaultFlow(monorepoRoot);
10147
+ break;
10148
+ }
8583
10149
  case "bloc": {
8584
10150
  const project = await resolveProject();
8585
10151
  if (!project) return;
@@ -8632,7 +10198,7 @@ program.command("bloc").description("Create a new BLoC with Freezed sealed class
8632
10198
  try {
8633
10199
  await createBloc(name, options);
8634
10200
  } catch (error) {
8635
- console.error(chalk26.red(`Error: ${error}`));
10201
+ console.error(chalk27.red(`Error: ${error}`));
8636
10202
  process.exit(1);
8637
10203
  }
8638
10204
  }
@@ -8641,7 +10207,7 @@ program.command("widget").description("Create a new widget in the design system"
8641
10207
  async (name, options) => {
8642
10208
  try {
8643
10209
  if (options.json || options.file) {
8644
- const raw = options.json ? JSON.parse(options.json) : JSON.parse(fs24.readFileSync(options.file, "utf-8"));
10210
+ const raw = options.json ? JSON.parse(options.json) : JSON.parse(fs30.readFileSync(options.file, "utf-8"));
8645
10211
  const params = WidgetJsonSchema.parse(raw);
8646
10212
  await widgetFlow(params);
8647
10213
  return;
@@ -8659,20 +10225,20 @@ program.command("widget").description("Create a new widget in the design system"
8659
10225
  projectRoot: options.dir,
8660
10226
  buildRunner: false
8661
10227
  });
8662
- console.log(chalk26.green("Widgetbook use-case created"));
10228
+ console.log(chalk27.green("Widgetbook use-case created"));
8663
10229
  } catch {
8664
- console.log(chalk26.yellow("Use-case skipped (may already exist)"));
10230
+ console.log(chalk27.yellow("Use-case skipped (may already exist)"));
8665
10231
  }
8666
10232
  } catch (error) {
8667
- if (error instanceof z3.ZodError) {
8668
- console.error(chalk26.red("Invalid JSON parameters:"));
10233
+ if (error instanceof z4.ZodError) {
10234
+ console.error(chalk27.red("Invalid JSON parameters:"));
8669
10235
  for (const issue of error.issues) {
8670
10236
  console.error(
8671
- chalk26.yellow(` \u2022 ${issue.path.join(".") || "(root)"}: ${issue.message}`)
10237
+ chalk27.yellow(` \u2022 ${issue.path.join(".") || "(root)"}: ${issue.message}`)
8672
10238
  );
8673
10239
  }
8674
10240
  } else {
8675
- console.error(chalk26.red(`Error: ${error}`));
10241
+ console.error(chalk27.red(`Error: ${error}`));
8676
10242
  }
8677
10243
  process.exit(1);
8678
10244
  }
@@ -8686,7 +10252,7 @@ program.command("usecase").description("Create a Widgetbook use-case for an exis
8686
10252
  buildRunner: options.buildRunner
8687
10253
  });
8688
10254
  } catch (error) {
8689
- console.error(chalk26.red(`Error: ${error}`));
10255
+ console.error(chalk27.red(`Error: ${error}`));
8690
10256
  process.exit(1);
8691
10257
  }
8692
10258
  }
@@ -8700,7 +10266,7 @@ program.command("page").description("Create a new page (GoRoute + View) with bar
8700
10266
  try {
8701
10267
  await createPage(name, { pagesPath: options.path });
8702
10268
  } catch (error) {
8703
- console.error(chalk26.red(`Error: ${error}`));
10269
+ console.error(chalk27.red(`Error: ${error}`));
8704
10270
  process.exit(1);
8705
10271
  }
8706
10272
  }
@@ -8714,7 +10280,68 @@ program.command("package").description("Create a new package in the monorepo").a
8714
10280
  program.command("env-var").description("Add an environment variable across the Flutter monorepo").action(async () => {
8715
10281
  await envVarFlow();
8716
10282
  });
8717
- program.command("app").description("Create and manage apps in the monorepo").action(async () => {
10283
+ async function runVaultCliAction(action, options) {
10284
+ const monorepoRoot = findMonorepoRoot(process.cwd());
10285
+ if (!monorepoRoot) {
10286
+ console.error(chalk27.red("Not inside a monorepo. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."));
10287
+ process.exit(1);
10288
+ }
10289
+ if (!action) {
10290
+ await vaultFlow(monorepoRoot);
10291
+ return;
10292
+ }
10293
+ let envs;
10294
+ if (options.env) {
10295
+ if (options.env !== "development" && options.env !== "production") {
10296
+ console.error(chalk27.red(`Invalid --env "${options.env}". Must be "development" or "production".`));
10297
+ process.exit(1);
10298
+ }
10299
+ envs = [options.env];
10300
+ }
10301
+ switch (action) {
10302
+ case "status":
10303
+ await runVaultStatus(monorepoRoot);
10304
+ break;
10305
+ case "init":
10306
+ await runVaultInit(monorepoRoot);
10307
+ break;
10308
+ case "capture":
10309
+ await runVaultCapture(monorepoRoot, { app: options.app, envs, force: options.force });
10310
+ break;
10311
+ case "apply":
10312
+ await runVaultApply(monorepoRoot, { app: options.app, envs });
10313
+ break;
10314
+ case "diff":
10315
+ await runVaultDiff(monorepoRoot, { app: options.app, envs });
10316
+ break;
10317
+ case "request-access":
10318
+ await runVaultRequestAccess(monorepoRoot);
10319
+ break;
10320
+ case "approve":
10321
+ await runVaultApprove(monorepoRoot);
10322
+ break;
10323
+ case "who":
10324
+ await runVaultWho(monorepoRoot);
10325
+ break;
10326
+ case "identity":
10327
+ await runVaultIdentity();
10328
+ break;
10329
+ case "revoke":
10330
+ await runVaultRevoke(monorepoRoot);
10331
+ break;
10332
+ default:
10333
+ console.error(chalk27.red(`Unknown vault action: ${action}`));
10334
+ process.exit(1);
10335
+ }
10336
+ }
10337
+ program.command("vault").description("Vault: sync encrypted STG/PROD env values with the team via Git").argument("[action]", "status|init|capture|apply|diff|request-access|approve|who|identity|revoke").option("--app <name>", "app to target (capture/apply/diff)").option("--env <development|production>", "limit to one environment").option("--force", "bypass the capture allowlist confirmation").action(async (action, options) => {
10338
+ await runVaultCliAction(action, options);
10339
+ });
10340
+ var appCmd = program.command("app").description("Create and manage apps in the monorepo");
10341
+ appCmd.command("vault").description("(alias) Same as `wlmaker vault` \u2014 prefer the top-level command").argument("[action]", "status|init|capture|apply|diff|request-access|approve|who|identity|revoke").option("--app <name>", "app to target (capture/apply/diff)").option("--env <development|production>", "limit to one environment").option("--force", "bypass the capture allowlist confirmation").action(async (action, options) => {
10342
+ await runVaultCliAction(action, options);
10343
+ });
10344
+ appCmd.action(async () => {
8718
10345
  await appFlow();
8719
10346
  });
8720
10347
  var collabCmd = program.command("collaborative").description("Generate collaborative feature structure (WL collaborative template)");
@@ -8726,14 +10353,14 @@ collabCmd.command("feature").description("Create a complete collaborative featur
8726
10353
  const { findMonorepoRoot: findMonorepoRoot2 } = await import("./project-433ZRUBC.mjs");
8727
10354
  const monorepoRoot = findMonorepoRoot2(process.cwd());
8728
10355
  if (!monorepoRoot) {
8729
- console.error(chalk26.red("Not inside a monorepo. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."));
10356
+ console.error(chalk27.red("Not inside a monorepo. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."));
8730
10357
  process.exit(1);
8731
10358
  }
8732
10359
  try {
8733
10360
  const { createCollaborativeFeature: createCollaborativeFeature2 } = await import("./generator-NQ6GX2L6.mjs");
8734
10361
  await createCollaborativeFeature2({ monorepoRoot, featureName: name });
8735
10362
  } catch (error) {
8736
- console.error(chalk26.red(`Error: ${error}`));
10363
+ console.error(chalk27.red(`Error: ${error}`));
8737
10364
  process.exit(1);
8738
10365
  }
8739
10366
  });
@@ -8746,7 +10373,7 @@ collabCmd.command("page").description("Add a page + view to an existing collabor
8746
10373
  const { createCollaborativePage: createCollaborativePage2 } = await import("./generator-OYLKPDQC.mjs");
8747
10374
  await createCollaborativePage2({ featurePath: options.feature, pageName: name });
8748
10375
  } catch (error) {
8749
- console.error(chalk26.red(`Error: ${error}`));
10376
+ console.error(chalk27.red(`Error: ${error}`));
8750
10377
  process.exit(1);
8751
10378
  }
8752
10379
  });
@@ -8759,7 +10386,7 @@ collabCmd.command("bloc").description("Add a BLoC to an existing collaborative f
8759
10386
  const { createCollaborativeBloc: createCollaborativeBloc2 } = await import("./generator-CACGWIBO.mjs");
8760
10387
  await createCollaborativeBloc2({ featurePath: options.feature, blocName: name });
8761
10388
  } catch (error) {
8762
- console.error(chalk26.red(`Error: ${error}`));
10389
+ console.error(chalk27.red(`Error: ${error}`));
8763
10390
  process.exit(1);
8764
10391
  }
8765
10392
  });
@@ -8773,26 +10400,26 @@ var docsCmd = program.command("docs").description("Project documentation tools")
8773
10400
  docsCmd.command("serve").description("Start Docusaurus dev server").option("-d, --dir <path>", "project root directory", process.cwd()).action(async (options) => {
8774
10401
  const bookDir = detectBookDir(options.dir);
8775
10402
  if (!bookDir) {
8776
- console.error(chalk26.red("No Docusaurus book/ directory found. Run from a monorepo root."));
10403
+ console.error(chalk27.red("No Docusaurus book/ directory found. Run from a monorepo root."));
8777
10404
  process.exit(1);
8778
10405
  }
8779
- console.log(chalk26.cyan(`Serving docs from ${bookDir}`));
10406
+ console.log(chalk27.cyan(`Serving docs from ${bookDir}`));
8780
10407
  await serveBook(bookDir);
8781
10408
  });
8782
10409
  docsCmd.command("commands").description("Show Makefile & melos commands reference").option("-d, --dir <path>", "project root directory", process.cwd()).action(async (options) => {
8783
10410
  const commands = discoverCommands(options.dir);
8784
10411
  if (commands.length === 0) {
8785
- console.log(chalk26.yellow("No commands found. Run from a monorepo root."));
10412
+ console.log(chalk27.yellow("No commands found. Run from a monorepo root."));
8786
10413
  return;
8787
10414
  }
8788
- console.log(chalk26.green(`Found ${commands.length} command(s)
10415
+ console.log(chalk27.green(`Found ${commands.length} command(s)
8789
10416
  `));
8790
10417
  displayCommands(commands);
8791
10418
  });
8792
10419
  docsCmd.command("architecture").description("Display monorepo architecture tree").option("-d, --dir <path>", "project root directory", process.cwd()).action(async (options) => {
8793
10420
  const info = discoverArchitecture(options.dir);
8794
10421
  if (!info) {
8795
- console.log(chalk26.yellow("No monorepo detected. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."));
10422
+ console.log(chalk27.yellow("No monorepo detected. Run from a Melos repo (root pubspec with workspace:/melos:, or legacy melos.yaml)."));
8796
10423
  return;
8797
10424
  }
8798
10425
  displayArchitecture(info);