@getcoherent/cli 0.6.23 → 0.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-SKQRPBPF.js → chunk-H644LLXJ.js} +124 -11
- package/dist/{chunk-FAV3UHFM.js → chunk-IYLHC4RC.js} +28 -1
- package/dist/index.js +25 -11
- package/dist/{plan-generator-5I6BPEVL.js → plan-generator-BHDEJGMY.js} +1 -1
- package/dist/{quality-validator-OX25OLIR.js → quality-validator-VDQXXDAV.js} +1 -1
- package/dist/{reuse-validator-HC4LZEKF.js → reuse-validator-XR2ZEYC4.js} +2 -1
- package/package.json +2 -2
|
@@ -679,6 +679,10 @@ function replaceRawColors(classes, colorMap) {
|
|
|
679
679
|
async function autoFixCode(code, context) {
|
|
680
680
|
const fixes = [];
|
|
681
681
|
let fixed = code;
|
|
682
|
+
if (!fixed.includes("\n") && fixed.includes("\\n")) {
|
|
683
|
+
fixed = fixed.replace(/\\n/g, "\n");
|
|
684
|
+
fixes.push("unescaped literal \\n to real newlines");
|
|
685
|
+
}
|
|
682
686
|
const beforeQuoteFix = fixed;
|
|
683
687
|
fixed = fixed.replace(/\\'(\s*[}\],])/g, "'$1");
|
|
684
688
|
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
@@ -703,17 +707,9 @@ async function autoFixCode(code, context) {
|
|
|
703
707
|
};
|
|
704
708
|
fixed = fixed.split("\n").map((line) => {
|
|
705
709
|
let l = line;
|
|
706
|
-
l = l.replace(/<
|
|
707
|
-
l = l.replace(/>
|
|
708
|
-
l = l.replace(/&
|
|
709
|
-
l = l.replace(
|
|
710
|
-
/([\w)\]])\s*<\s*([\w(])/g,
|
|
711
|
-
(m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} < ${p2}`
|
|
712
|
-
);
|
|
713
|
-
l = l.replace(
|
|
714
|
-
/([\w)\]])\s*>\s*([\w(])/g,
|
|
715
|
-
(m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} > ${p2}`
|
|
716
|
-
);
|
|
710
|
+
l = l.replace(/</g, (m, offset) => isInsideAttrValue(line, offset) ? m : "<");
|
|
711
|
+
l = l.replace(/>/g, (m, offset) => isInsideAttrValue(line, offset) ? m : ">");
|
|
712
|
+
l = l.replace(/&/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "&");
|
|
717
713
|
return l;
|
|
718
714
|
}).join("\n");
|
|
719
715
|
if (fixed !== beforeEntityFix) {
|
|
@@ -807,6 +803,123 @@ ${fixed}`;
|
|
|
807
803
|
}
|
|
808
804
|
fixes.push("<button> \u2192 <Button> (with import)");
|
|
809
805
|
}
|
|
806
|
+
if (/<input\b[^>]*(?:\/>|>)/i.test(fixed) && !fixed.includes('type="hidden"')) {
|
|
807
|
+
const inputLines = fixed.split("\n");
|
|
808
|
+
let hasReplacedInput = false;
|
|
809
|
+
for (let i = 0; i < inputLines.length; i++) {
|
|
810
|
+
if (!/<input\b/i.test(inputLines[i])) continue;
|
|
811
|
+
if (inputLines[i].includes('type="hidden"') || inputLines[i].includes("type='hidden'")) continue;
|
|
812
|
+
inputLines[i] = inputLines[i].replace(/<input\b/gi, "<Input");
|
|
813
|
+
hasReplacedInput = true;
|
|
814
|
+
}
|
|
815
|
+
if (hasReplacedInput) {
|
|
816
|
+
fixed = inputLines.join("\n");
|
|
817
|
+
const hasInputImport = /import\s.*\bInput\b.*from\s+['"]@\/components\/ui\/input['"]/.test(fixed);
|
|
818
|
+
if (!hasInputImport) {
|
|
819
|
+
const lastImportIdx = fixed.lastIndexOf("\nimport ");
|
|
820
|
+
if (lastImportIdx !== -1) {
|
|
821
|
+
const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
|
|
822
|
+
fixed = fixed.slice(0, lineEnd + 1) + "import { Input } from '@/components/ui/input'\n" + fixed.slice(lineEnd + 1);
|
|
823
|
+
} else {
|
|
824
|
+
const hasUseClient2 = /^['"]use client['"]/.test(fixed.trim());
|
|
825
|
+
const insertAfter2 = hasUseClient2 ? fixed.indexOf("\n") + 1 : 0;
|
|
826
|
+
fixed = fixed.slice(0, insertAfter2) + "import { Input } from '@/components/ui/input'\n" + fixed.slice(insertAfter2);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
fixes.push("<input> \u2192 <Input> (with import)");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const compositeComponents = {
|
|
833
|
+
select: [
|
|
834
|
+
"Select",
|
|
835
|
+
"SelectContent",
|
|
836
|
+
"SelectItem",
|
|
837
|
+
"SelectTrigger",
|
|
838
|
+
"SelectValue",
|
|
839
|
+
"SelectGroup",
|
|
840
|
+
"SelectLabel",
|
|
841
|
+
"SelectSeparator",
|
|
842
|
+
"SelectScrollUpButton",
|
|
843
|
+
"SelectScrollDownButton"
|
|
844
|
+
],
|
|
845
|
+
dialog: [
|
|
846
|
+
"Dialog",
|
|
847
|
+
"DialogContent",
|
|
848
|
+
"DialogDescription",
|
|
849
|
+
"DialogFooter",
|
|
850
|
+
"DialogHeader",
|
|
851
|
+
"DialogTitle",
|
|
852
|
+
"DialogTrigger",
|
|
853
|
+
"DialogClose",
|
|
854
|
+
"DialogOverlay",
|
|
855
|
+
"DialogPortal"
|
|
856
|
+
],
|
|
857
|
+
dropdown_menu: [
|
|
858
|
+
"DropdownMenu",
|
|
859
|
+
"DropdownMenuContent",
|
|
860
|
+
"DropdownMenuItem",
|
|
861
|
+
"DropdownMenuLabel",
|
|
862
|
+
"DropdownMenuSeparator",
|
|
863
|
+
"DropdownMenuTrigger",
|
|
864
|
+
"DropdownMenuCheckboxItem",
|
|
865
|
+
"DropdownMenuGroup",
|
|
866
|
+
"DropdownMenuRadioGroup",
|
|
867
|
+
"DropdownMenuRadioItem",
|
|
868
|
+
"DropdownMenuShortcut",
|
|
869
|
+
"DropdownMenuSub",
|
|
870
|
+
"DropdownMenuSubContent",
|
|
871
|
+
"DropdownMenuSubTrigger"
|
|
872
|
+
],
|
|
873
|
+
table: ["Table", "TableBody", "TableCaption", "TableCell", "TableFooter", "TableHead", "TableHeader", "TableRow"],
|
|
874
|
+
tabs: ["Tabs", "TabsContent", "TabsList", "TabsTrigger"],
|
|
875
|
+
card: ["Card", "CardContent", "CardDescription", "CardFooter", "CardHeader", "CardTitle"],
|
|
876
|
+
alert_dialog: [
|
|
877
|
+
"AlertDialog",
|
|
878
|
+
"AlertDialogAction",
|
|
879
|
+
"AlertDialogCancel",
|
|
880
|
+
"AlertDialogContent",
|
|
881
|
+
"AlertDialogDescription",
|
|
882
|
+
"AlertDialogFooter",
|
|
883
|
+
"AlertDialogHeader",
|
|
884
|
+
"AlertDialogTitle",
|
|
885
|
+
"AlertDialogTrigger"
|
|
886
|
+
],
|
|
887
|
+
popover: ["Popover", "PopoverContent", "PopoverTrigger"],
|
|
888
|
+
command: [
|
|
889
|
+
"Command",
|
|
890
|
+
"CommandDialog",
|
|
891
|
+
"CommandEmpty",
|
|
892
|
+
"CommandGroup",
|
|
893
|
+
"CommandInput",
|
|
894
|
+
"CommandItem",
|
|
895
|
+
"CommandList",
|
|
896
|
+
"CommandSeparator",
|
|
897
|
+
"CommandShortcut"
|
|
898
|
+
],
|
|
899
|
+
form: ["Form", "FormControl", "FormDescription", "FormField", "FormItem", "FormLabel", "FormMessage"]
|
|
900
|
+
};
|
|
901
|
+
const beforeSubImportFix = fixed;
|
|
902
|
+
for (const [uiName, allExports] of Object.entries(compositeComponents)) {
|
|
903
|
+
const importPath = `@/components/ui/${uiName.replace(/_/g, "-")}`;
|
|
904
|
+
const importRe = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${importPath.replace(/[-/]/g, "\\$&")}['"]`);
|
|
905
|
+
const importMatch = fixed.match(importRe);
|
|
906
|
+
if (!importMatch) continue;
|
|
907
|
+
const imported = new Set(
|
|
908
|
+
importMatch[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
909
|
+
);
|
|
910
|
+
const usedInCode = allExports.filter((e) => {
|
|
911
|
+
if (imported.has(e)) return false;
|
|
912
|
+
return new RegExp(`<${e}[\\s/>]`).test(fixed) || new RegExp(`</${e}>`).test(fixed);
|
|
913
|
+
});
|
|
914
|
+
if (usedInCode.length > 0) {
|
|
915
|
+
const merged = [...imported, ...usedInCode];
|
|
916
|
+
const newImport = `import { ${merged.join(", ")} } from '${importPath}'`;
|
|
917
|
+
fixed = fixed.replace(importRe, newImport);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (fixed !== beforeSubImportFix) {
|
|
921
|
+
fixes.push("added missing sub-imports for composite components");
|
|
922
|
+
}
|
|
810
923
|
const colorMap = {
|
|
811
924
|
"bg-zinc-950": "bg-background",
|
|
812
925
|
"bg-zinc-900": "bg-background",
|
|
@@ -236,6 +236,29 @@ function loadPlan(projectRoot) {
|
|
|
236
236
|
function toKebabCase(name) {
|
|
237
237
|
return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
238
238
|
}
|
|
239
|
+
function extractPropsInterface(code, componentName) {
|
|
240
|
+
const interfaceRe = new RegExp(`interface\\s+${componentName}Props\\s*\\{([^}]+)\\}`, "s");
|
|
241
|
+
const match = code.match(interfaceRe);
|
|
242
|
+
if (match) {
|
|
243
|
+
return match[1].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//")).join("; ");
|
|
244
|
+
}
|
|
245
|
+
const typeRe = new RegExp(`type\\s+${componentName}Props\\s*=\\s*\\{([^}]+)\\}`, "s");
|
|
246
|
+
const typeMatch = code.match(typeRe);
|
|
247
|
+
if (typeMatch) {
|
|
248
|
+
return typeMatch[1].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//")).join("; ");
|
|
249
|
+
}
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
function extractUsageExample(code, componentName) {
|
|
253
|
+
const funcMatch = code.match(new RegExp(`export function ${componentName}\\s*\\(\\{([^}]+)\\}`, "s"));
|
|
254
|
+
if (!funcMatch) return void 0;
|
|
255
|
+
const props = funcMatch[1].split(",").map((p) => p.split(":")[0].trim()).filter(Boolean);
|
|
256
|
+
const example = props.map((p) => {
|
|
257
|
+
if (p.startsWith("...")) return `${p.slice(3)}={{}}`;
|
|
258
|
+
return `${p}={...}`;
|
|
259
|
+
}).join(" ");
|
|
260
|
+
return `<${componentName} ${example} />`;
|
|
261
|
+
}
|
|
239
262
|
async function generateSharedComponentsFromPlan(plan, styleContext, projectRoot, aiProvider) {
|
|
240
263
|
if (plan.sharedComponents.length === 0) return [];
|
|
241
264
|
const componentSpecs = plan.sharedComponents.map(
|
|
@@ -297,6 +320,8 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
|
|
|
297
320
|
}
|
|
298
321
|
for (const comp of results) {
|
|
299
322
|
const planned = plan.sharedComponents.find((c) => c.name === comp.name);
|
|
323
|
+
const propsInterface = extractPropsInterface(comp.code, comp.name);
|
|
324
|
+
const usageExample = extractUsageExample(comp.code, comp.name);
|
|
300
325
|
await generateSharedComponent(projectRoot, {
|
|
301
326
|
name: comp.name,
|
|
302
327
|
type: planned?.type ?? "section",
|
|
@@ -304,7 +329,9 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
|
|
|
304
329
|
description: planned?.description,
|
|
305
330
|
usedIn: planned?.usedBy ?? [],
|
|
306
331
|
source: "generated",
|
|
307
|
-
overwrite: true
|
|
332
|
+
overwrite: true,
|
|
333
|
+
propsInterface,
|
|
334
|
+
usageExample
|
|
308
335
|
});
|
|
309
336
|
}
|
|
310
337
|
return results;
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
formatIssues,
|
|
8
8
|
validatePageQuality,
|
|
9
9
|
verifyIncrementalEdit
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-H644LLXJ.js";
|
|
11
11
|
import {
|
|
12
12
|
generateArchitecturePlan,
|
|
13
13
|
getPageGroup,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
routeToKey,
|
|
17
17
|
savePlan,
|
|
18
18
|
updateArchitecturePlan
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-IYLHC4RC.js";
|
|
20
20
|
import {
|
|
21
21
|
CORE_CONSTRAINTS,
|
|
22
22
|
DESIGN_QUALITY,
|
|
@@ -6315,7 +6315,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6315
6315
|
if (plan && plan.sharedComponents.length > 0) {
|
|
6316
6316
|
spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
|
|
6317
6317
|
try {
|
|
6318
|
-
const { generateSharedComponentsFromPlan } = await import("./plan-generator-
|
|
6318
|
+
const { generateSharedComponentsFromPlan } = await import("./plan-generator-BHDEJGMY.js");
|
|
6319
6319
|
const generated = await generateSharedComponentsFromPlan(
|
|
6320
6320
|
plan,
|
|
6321
6321
|
styleContext,
|
|
@@ -6775,7 +6775,7 @@ async function regeneratePage(pageId, config2, projectRoot) {
|
|
|
6775
6775
|
const code = await generator.generate(page, appType);
|
|
6776
6776
|
const route = page.route || "/";
|
|
6777
6777
|
const isAuth = isAuthRoute(route) || isAuthRoute(page.name || page.id || "");
|
|
6778
|
-
const { loadPlan: loadPlanForPath } = await import("./plan-generator-
|
|
6778
|
+
const { loadPlan: loadPlanForPath } = await import("./plan-generator-BHDEJGMY.js");
|
|
6779
6779
|
const planForPath = loadPlanForPath(projectRoot);
|
|
6780
6780
|
const filePath = routeToFsPath(projectRoot, route, planForPath || isAuth);
|
|
6781
6781
|
await mkdir3(dirname5(filePath), { recursive: true });
|
|
@@ -7091,7 +7091,17 @@ function extractImportsFrom(code, fromPath) {
|
|
|
7091
7091
|
return [...new Set(results)];
|
|
7092
7092
|
}
|
|
7093
7093
|
function printPostGenerationReport(opts) {
|
|
7094
|
-
const {
|
|
7094
|
+
const {
|
|
7095
|
+
action,
|
|
7096
|
+
pageTitle,
|
|
7097
|
+
filePath,
|
|
7098
|
+
code,
|
|
7099
|
+
route,
|
|
7100
|
+
postFixes = [],
|
|
7101
|
+
layoutShared = [],
|
|
7102
|
+
allShared = [],
|
|
7103
|
+
groupLayout
|
|
7104
|
+
} = opts;
|
|
7095
7105
|
const uiComponents = extractImportsFrom(code, "@/components/ui");
|
|
7096
7106
|
const sharedImportNames = extractImportsFrom(code, "@/components/shared/");
|
|
7097
7107
|
const inCodeShared = allShared.filter((s) => sharedImportNames.some((n) => n === s.name));
|
|
@@ -7108,8 +7118,10 @@ function printPostGenerationReport(opts) {
|
|
|
7108
7118
|
console.log(chalk10.dim(` Shared: ${inCodeShared.map((s) => `${s.id} (${s.name})`).join(", ")}`));
|
|
7109
7119
|
}
|
|
7110
7120
|
const isAuthPage = route && (/^\/(login|signin|signup|register|forgot-password|reset-password)\b/.test(route) || filePath.includes("(auth)"));
|
|
7111
|
-
|
|
7112
|
-
|
|
7121
|
+
const isSidebarPage = groupLayout === "sidebar" || filePath.includes("(app)");
|
|
7122
|
+
const filteredLayout = isAuthPage ? [] : isSidebarPage ? layoutShared.filter((l) => /sidebar/i.test(l.name)) : layoutShared;
|
|
7123
|
+
if (filteredLayout.length > 0) {
|
|
7124
|
+
console.log(chalk10.dim(` Layout: ${filteredLayout.map((l) => `${l.id} (${l.name})`).join(", ")} via layout.tsx`));
|
|
7113
7125
|
}
|
|
7114
7126
|
if (iconCount > 0) {
|
|
7115
7127
|
console.log(chalk10.dim(` Icons: ${iconCount} from lucide-react`));
|
|
@@ -8534,7 +8546,7 @@ async function chatCommand(message, options) {
|
|
|
8534
8546
|
spinner.start(`Creating shared component: ${componentName}...`);
|
|
8535
8547
|
const { createAIProvider: createAIProvider2 } = await import("./ai-provider-CGSIYFZT.js");
|
|
8536
8548
|
const { generateSharedComponent: generateSharedComponent7 } = await import("@getcoherent/core");
|
|
8537
|
-
const { autoFixCode: autoFixCode2 } = await import("./quality-validator-
|
|
8549
|
+
const { autoFixCode: autoFixCode2 } = await import("./quality-validator-VDQXXDAV.js");
|
|
8538
8550
|
const { extractPropsInterface, extractDependencies } = await import("./component-extractor-VYJLT5NR.js");
|
|
8539
8551
|
const aiProvider = await createAIProvider2(provider ?? "auto");
|
|
8540
8552
|
const prompt = `Generate a React component called "${componentName}". Description: ${message}.
|
|
@@ -9001,9 +9013,10 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
9001
9013
|
}
|
|
9002
9014
|
}
|
|
9003
9015
|
try {
|
|
9004
|
-
const { validateReuse } = await import("./reuse-validator-
|
|
9016
|
+
const { validateReuse } = await import("./reuse-validator-XR2ZEYC4.js");
|
|
9005
9017
|
const { inferPageTypeFromRoute: inferPageTypeFromRoute2 } = await import("./design-constraints-EIP2XM7T.js");
|
|
9006
9018
|
const manifest2 = await loadManifest8(projectRoot);
|
|
9019
|
+
const reuseplan = projectRoot ? loadPlan(projectRoot) : null;
|
|
9007
9020
|
if (manifest2.shared.length > 0) {
|
|
9008
9021
|
for (const request of normalizedRequests) {
|
|
9009
9022
|
if (request.type !== "add-page") continue;
|
|
@@ -9012,7 +9025,8 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
9012
9025
|
if (!pageCode) continue;
|
|
9013
9026
|
const route = changes.route || "";
|
|
9014
9027
|
const pageType = inferPageTypeFromRoute2(route);
|
|
9015
|
-
const
|
|
9028
|
+
const planned = reuseplan ? new Set(reuseplan.sharedComponents.filter((c) => c.usedBy.includes(route)).map((c) => c.name)) : void 0;
|
|
9029
|
+
const warnings = validateReuse(manifest2, pageCode, pageType, void 0, planned);
|
|
9016
9030
|
for (const w of warnings) {
|
|
9017
9031
|
console.log(chalk13.yellow(` \u26A0 ${w.message}`));
|
|
9018
9032
|
}
|
|
@@ -11400,7 +11414,7 @@ async function checkCommand(opts = {}) {
|
|
|
11400
11414
|
}
|
|
11401
11415
|
if (!skipShared) {
|
|
11402
11416
|
try {
|
|
11403
|
-
const { validateReuse } = await import("./reuse-validator-
|
|
11417
|
+
const { validateReuse } = await import("./reuse-validator-XR2ZEYC4.js");
|
|
11404
11418
|
const { inferPageTypeFromRoute: inferPageTypeFromRoute2 } = await import("./design-constraints-EIP2XM7T.js");
|
|
11405
11419
|
const manifest = await loadManifest11(projectRoot);
|
|
11406
11420
|
const appDir = resolve14(projectRoot, "app");
|
|
@@ -6,11 +6,12 @@ var RELEVANT_TYPES = {
|
|
|
6
6
|
auth: /* @__PURE__ */ new Set(["form", "feedback"]),
|
|
7
7
|
marketing: /* @__PURE__ */ new Set(["section", "layout"])
|
|
8
8
|
};
|
|
9
|
-
function validateReuse(manifest, generatedCode, pageType, newComponents) {
|
|
9
|
+
function validateReuse(manifest, generatedCode, pageType, newComponents, plannedComponentNames) {
|
|
10
10
|
const warnings = [];
|
|
11
11
|
const relevantTypes = RELEVANT_TYPES[pageType] || RELEVANT_TYPES.app;
|
|
12
12
|
for (const comp of manifest.shared) {
|
|
13
13
|
if (!relevantTypes.has(comp.type)) continue;
|
|
14
|
+
if (plannedComponentNames && !plannedComponentNames.has(comp.name)) continue;
|
|
14
15
|
const isImported = generatedCode.includes(`from '@/components/shared/`) && (generatedCode.includes(`{ ${comp.name} }`) || generatedCode.includes(`{ ${comp.name},`));
|
|
15
16
|
if (!isImported) {
|
|
16
17
|
warnings.push({
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.25",
|
|
7
7
|
"description": "CLI interface for Coherent Design Method",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"ora": "^7.0.1",
|
|
44
44
|
"prompts": "^2.4.2",
|
|
45
45
|
"zod": "^3.22.4",
|
|
46
|
-
"@getcoherent/core": "0.6.
|
|
46
|
+
"@getcoherent/core": "0.6.25"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^20.11.0",
|