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.
- package/README.md +87 -3
- package/dist/cli.mjs +1814 -187
- 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
|
|
50
|
+
import * as fs30 from "fs";
|
|
51
51
|
import { Command } from "commander";
|
|
52
|
-
import
|
|
53
|
-
import { z as
|
|
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,
|
|
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
|
|
503
|
+
const propArgs = properties.map((p) => {
|
|
502
504
|
const camel = propNameToCamel(p.name);
|
|
503
505
|
return ` ${camel}: ${camel},`;
|
|
504
506
|
}).join("\n");
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
507
|
+
function factoryCall(factoryName) {
|
|
508
|
+
if (isTemplate && properties.length > 0) {
|
|
509
|
+
return `${className}.${factoryName}(
|
|
508
510
|
i18n: Wl${pascal2}I18n(
|
|
509
|
-
${
|
|
511
|
+
${propArgs}
|
|
510
512
|
),
|
|
511
513
|
)`;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
566
|
+
path: '${useCasePath}',
|
|
528
567
|
)
|
|
529
568
|
Widget useCase${className}(BuildContext context) {
|
|
530
|
-
${
|
|
531
|
-
|
|
532
|
-
return ListView(
|
|
569
|
+
${knobBlock} return ListView(
|
|
533
570
|
padding: const EdgeInsets.all(16),
|
|
534
571
|
children: [
|
|
535
|
-
|
|
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.
|
|
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
|
|
913
|
-
import
|
|
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
|
|
1221
|
-
clack3.log.warn(` \u2022 ${
|
|
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: {
|
|
1311
|
-
dropProps: [
|
|
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: {
|
|
1317
|
-
dropProps: [
|
|
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 =
|
|
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 {
|
|
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
|
|
1434
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
1534
|
-
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
|
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)
|
|
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
|
|
1847
|
-
const iconData =
|
|
1848
|
-
const todoLine =
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
-
|
|
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(
|
|
2144
|
-
|
|
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 (
|
|
2556
|
+
if (working.mode === "skeleton") {
|
|
2156
2557
|
const skeletonPath = path8.join(outDir, `${fileName}_skeleton.dart`);
|
|
2157
2558
|
if (fs8.existsSync(skeletonPath)) {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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 (
|
|
2583
|
+
} else if (fs8.existsSync(targetFile)) {
|
|
2584
|
+
if (!skipPrompts) {
|
|
2180
2585
|
const overwrite = await clack4.confirm({
|
|
2181
|
-
message: `El componente "${
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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(`${
|
|
2254
|
-
if (ds.widgetbookDir &&
|
|
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:
|
|
2261
|
-
|
|
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(
|
|
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
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
2564
|
-
const httpAnnotation = `@${httpMethod.toUpperCase()}('${
|
|
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 =
|
|
2633
|
-
const useCasePascal =
|
|
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 = `${
|
|
3077
|
+
const datasourceClassName = `${pascalCase7(domain)}RestDataSource`;
|
|
2639
3078
|
const repositoryInterfaceFile = path10.join(lib, "domain", "repositories", `${domain}_repository.dart`);
|
|
2640
|
-
const repositoryInterfaceName = `${
|
|
3079
|
+
const repositoryInterfaceName = `${pascalCase7(domain)}Repository`;
|
|
2641
3080
|
const repositoryImplFile = path10.join(lib, "data", "repositories", `${domain}_repository_data.dart`);
|
|
2642
|
-
const repositoryImplClassName = `${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
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
|
|
4682
|
+
import { pascalCase as pascalCase8 } from "change-case";
|
|
4211
4683
|
function pascal(name) {
|
|
4212
|
-
return
|
|
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
|
|
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 =
|
|
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${
|
|
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 = `${
|
|
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 = `${
|
|
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 = `${
|
|
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 =
|
|
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(
|
|
8459
|
-
return
|
|
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
|
-
|
|
8562
|
-
const createType = await
|
|
8563
|
-
message: "What do you want to
|
|
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 (
|
|
8578
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
10228
|
+
console.log(chalk27.green("Widgetbook use-case created"));
|
|
8663
10229
|
} catch {
|
|
8664
|
-
console.log(
|
|
10230
|
+
console.log(chalk27.yellow("Use-case skipped (may already exist)"));
|
|
8665
10231
|
}
|
|
8666
10232
|
} catch (error) {
|
|
8667
|
-
if (error instanceof
|
|
8668
|
-
console.error(
|
|
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
|
-
|
|
10237
|
+
chalk27.yellow(` \u2022 ${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
8672
10238
|
);
|
|
8673
10239
|
}
|
|
8674
10240
|
} else {
|
|
8675
|
-
console.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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
10412
|
+
console.log(chalk27.yellow("No commands found. Run from a monorepo root."));
|
|
8786
10413
|
return;
|
|
8787
10414
|
}
|
|
8788
|
-
console.log(
|
|
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(
|
|
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);
|