wlmaker 1.2.8 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +829 -34
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
- import chalk5 from "chalk";
6
+ import chalk6 from "chalk";
7
7
 
8
8
  // src/core/create-bloc.ts
9
9
  import * as fs3 from "fs";
@@ -106,17 +106,17 @@ function hasBuildRunner(projectRoot) {
106
106
  return /build_runner/.test(pubspec);
107
107
  }
108
108
  function runBuildRunner(projectRoot) {
109
- return new Promise((resolve3) => {
109
+ return new Promise((resolve4) => {
110
110
  const child = spawn(
111
111
  "dart",
112
112
  ["run", "build_runner", "build", "--delete-conflicting-outputs"],
113
113
  { cwd: projectRoot, stdio: "inherit" }
114
114
  );
115
115
  child.on("close", (code) => {
116
- resolve3();
116
+ resolve4();
117
117
  });
118
118
  child.on("error", () => {
119
- resolve3();
119
+ resolve4();
120
120
  });
121
121
  });
122
122
  }
@@ -956,18 +956,453 @@ async function createUseCase(name, tierInput, options) {
956
956
  }
957
957
 
958
958
  // src/interactive.ts
959
- import * as fs8 from "fs";
959
+ import * as fs10 from "fs";
960
960
  import * as os from "os";
961
- import * as path8 from "path";
961
+ import * as path9 from "path";
962
962
  import * as clack from "@clack/prompts";
963
+ import chalk5 from "chalk";
964
+
965
+ // src/core/create-endpoint.ts
966
+ import * as fs9 from "fs";
967
+ import * as path8 from "path";
963
968
  import chalk4 from "chalk";
969
+ import { pascalCase as pascalCase4, camelCase } from "change-case";
970
+
971
+ // src/core/json-to-dart.ts
972
+ function jsonToFields(jsonStr) {
973
+ const parsed = JSON.parse(jsonStr);
974
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
975
+ throw new Error("Expected a JSON object at the top level");
976
+ }
977
+ return objectToFields(parsed);
978
+ }
979
+ function objectToFields(obj) {
980
+ const fields = [];
981
+ for (const [key, value] of Object.entries(obj)) {
982
+ fields.push({
983
+ name: key,
984
+ dartType: dartTypeOf(value, key),
985
+ isNullable: value === null
986
+ });
987
+ }
988
+ return fields;
989
+ }
990
+ function dartTypeOf(value, nameHint) {
991
+ if (value === null) return "dynamic";
992
+ if (typeof value === "string") return "String";
993
+ if (typeof value === "boolean") return "bool";
994
+ if (typeof value === "number") {
995
+ return Number.isInteger(value) ? "int" : "double";
996
+ }
997
+ if (Array.isArray(value)) {
998
+ if (value.length === 0) return "List<dynamic>";
999
+ const elementType = dartTypeOf(value[0], nameHint);
1000
+ return `List<${elementType}>`;
1001
+ }
1002
+ if (typeof value === "object") {
1003
+ return "Map<String, dynamic>";
1004
+ }
1005
+ return "dynamic";
1006
+ }
1007
+ function fieldsToConstructorParams(fields, indent = " ") {
1008
+ return fields.map((f) => {
1009
+ const nullable = f.isNullable ? "?" : "";
1010
+ return `${indent}required ${f.dartType}${nullable} ${f.name},`;
1011
+ }).join("\n");
1012
+ }
1013
+ function fieldsToFromJson(fields, modelName) {
1014
+ const entries = fields.map((f) => {
1015
+ if (f.dartType.startsWith("List<")) {
1016
+ return ` ${f.name}: (${_jsonFieldName(f)} as List<dynamic>).map((e) => e as ${_extractGenericType(f.dartType)}).toList(),`;
1017
+ }
1018
+ return ` ${f.name}: ${_jsonFieldName(f)} as ${f.dartType}${f.isNullable ? "?" : ""},`;
1019
+ }).join("\n");
1020
+ return `factory ${modelName}.fromJson(Map<String, dynamic> json) => ${modelName}(
1021
+ ${entries}
1022
+ );`;
1023
+ }
1024
+ function fieldsToJson(fields, modelName) {
1025
+ const entries = fields.map((f) => ` '${f.name}': ${f.name},`).join("\n");
1026
+ return `Map<String, dynamic> toJson() => {
1027
+ ${entries}
1028
+ };`;
1029
+ }
1030
+ function _jsonFieldName(f) {
1031
+ return `json['${f.name}']`;
1032
+ }
1033
+ function _extractGenericType(listType) {
1034
+ const match = listType.match(/^List<(.+)>$/);
1035
+ return match ? match[1] : "dynamic";
1036
+ }
1037
+
1038
+ // src/core/endpoint-templates.ts
1039
+ function entityTemplate(name, pascal, fields) {
1040
+ const params = fieldsToConstructorParams(fields);
1041
+ const props = fields.map((f) => {
1042
+ const nullable = f.isNullable ? "?" : "";
1043
+ return ` final ${f.dartType}${nullable} ${f.name};`;
1044
+ }).join("\n");
1045
+ return `class ${pascal}Entity {
1046
+ ${props}
1047
+
1048
+ const ${pascal}Entity({
1049
+ ${params}
1050
+ });
1051
+ }
1052
+ `;
1053
+ }
1054
+ function modelTemplate(name, pascal, fields) {
1055
+ const params = fieldsToConstructorParams(fields);
1056
+ const props = fields.map((f) => {
1057
+ const nullable = f.isNullable ? "?" : "";
1058
+ return ` final ${f.dartType}${nullable} ${f.name};`;
1059
+ }).join("\n");
1060
+ const fromJson = fieldsToFromJson(fields, `${pascal}Model`);
1061
+ const toJson = fieldsToJson(fields, `${pascal}Model`);
1062
+ return `import 'package:json_annotation/json_annotation.dart';
1063
+ import '../../domain/entities/${name}/${name}_entity.dart';
1064
+
1065
+ part '${name}_model.g.dart';
1066
+
1067
+ @JsonSerializable()
1068
+ class ${pascal}Model extends ${pascal}Entity {
1069
+ const ${pascal}Model({
1070
+ ${params}
1071
+ }) : super(
1072
+ ${fields.map((f) => ` ${f.name}: ${f.name},`).join("\n")}
1073
+ );
1074
+
1075
+ ${fromJson}
1076
+
1077
+ ${toJson}
1078
+
1079
+ factory ${pascal}Model.fromJson(Map<String, dynamic> json) =>
1080
+ _$${pascal}ModelFromJson(json);
1081
+
1082
+ @override
1083
+ Map<String, dynamic> toJson() => _$${pascal}ModelToJson(this);
1084
+ }
1085
+ `;
1086
+ }
1087
+ function requestModelTemplate(name, pascal, fields) {
1088
+ const params = fieldsToConstructorParams(fields);
1089
+ const props = fields.map((f) => {
1090
+ const nullable = f.isNullable ? "?" : "";
1091
+ return ` final ${f.dartType}${nullable} ${f.name};`;
1092
+ }).join("\n");
1093
+ return `import 'package:json_annotation/json_annotation.dart';
1094
+
1095
+ part '${name}_request_model.g.dart';
1096
+
1097
+ @JsonSerializable()
1098
+ class ${pascal}RequestModel {
1099
+ ${props}
1100
+
1101
+ ${pascal}RequestModel({
1102
+ ${params}
1103
+ });
1104
+
1105
+ factory ${pascal}RequestModel.fromJson(Map<String, dynamic> json) =>
1106
+ _$${pascal}RequestModelFromJson(json);
1107
+
1108
+ Map<String, dynamic> toJson() => _$${pascal}RequestModelToJson(this);
1109
+ }
1110
+ `;
1111
+ }
1112
+ function useCaseTemplate2(name, pascal, method, params, returnType, repositoryInterface) {
1113
+ const hasParams = params.length > 0;
1114
+ const paramsClass = hasParams ? `
1115
+ class Params {
1116
+ ${params.map((p) => ` final ${p.type} ${p.name};`).join("\n")}
1117
+ const Params({${params.map((p) => `required this.${p.name}`).join(", ")}});
1118
+ }
1119
+ ` : "";
1120
+ const callParams = hasParams ? "Params params" : "";
1121
+ const callReturn = `Future<${returnType}>`;
1122
+ const args = hasParams ? params.map((p) => `params.${p.name}`).join(", ") : "";
1123
+ return `import 'package:dartz/dartz.dart';
1124
+ import '../../repositories/${repositoryInterface}.dart';
1125
+
1126
+ class ${pascal}UseCase {
1127
+ final ${repositoryInterface} _repository;
1128
+
1129
+ ${pascal}UseCase(this._repository);
1130
+ ${paramsClass}
1131
+ ${callReturn} call(${callParams}) async {
1132
+ return await _repository.${method}(${args});
1133
+ }
1134
+ }
1135
+ `;
1136
+ }
1137
+ function retrofitMethod(methodName, path10, httpMethod, params, returnType) {
1138
+ const httpAnnotation = `@${httpMethod.toUpperCase()}('${path10}')`;
1139
+ const paramList = params.map((p) => {
1140
+ if (p.isPath) return `@Path('${p.name}') ${p.type} ${p.name}`;
1141
+ if (p.isBody) return `@Body() ${p.type} ${p.name}`;
1142
+ if (p.isQuery) return `@Query('${p.name}') ${p.type} ${p.name}`;
1143
+ return `${p.type} ${p.name}`;
1144
+ }).join(", ");
1145
+ return `${httpAnnotation}
1146
+ Future<${returnType}> ${methodName}(${paramList});`;
1147
+ }
1148
+ function datasourceMethod(methodName, returnType, params) {
1149
+ const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
1150
+ return `Future<${returnType}> ${methodName}(${paramList}) async {
1151
+ // TODO: implement ${methodName}
1152
+ throw UnimplementedError();
1153
+ }`;
1154
+ }
1155
+ function repositoryInterfaceMethod(methodName, returnType, params) {
1156
+ const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
1157
+ return `Future<${returnType}> ${methodName}(${paramList});`;
1158
+ }
1159
+ function repositoryImplMethod(methodName, returnType, params, datasourceName) {
1160
+ const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
1161
+ const args = params.map((p) => p.name).join(", ");
1162
+ return `@override
1163
+ Future<${returnType}> ${methodName}(${paramList}) async {
1164
+ return await ${datasourceName}.${methodName}(${args});
1165
+ }`;
1166
+ }
1167
+
1168
+ // src/core/dart-injector.ts
1169
+ import * as fs8 from "fs";
1170
+ function injectMethod(filePath, className, methodCode) {
1171
+ const content = fs8.readFileSync(filePath, "utf8");
1172
+ const classRegex = new RegExp(`class\\s+${className}\\s*[^{]*\\{`);
1173
+ const classMatch = content.match(classRegex);
1174
+ if (!classMatch) {
1175
+ throw new Error(`Class "${className}" not found in ${filePath}`);
1176
+ }
1177
+ const classStart = content.indexOf(classMatch[0]);
1178
+ let braceCount = 0;
1179
+ let classEnd = -1;
1180
+ let foundOpen = false;
1181
+ for (let i = classStart; i < content.length; i++) {
1182
+ if (content[i] === "{") {
1183
+ braceCount++;
1184
+ foundOpen = true;
1185
+ } else if (content[i] === "}") {
1186
+ braceCount--;
1187
+ if (foundOpen && braceCount === 0) {
1188
+ classEnd = i;
1189
+ break;
1190
+ }
1191
+ }
1192
+ }
1193
+ if (classEnd === -1) {
1194
+ throw new Error(`Could not find closing brace for class "${className}" in ${filePath}`);
1195
+ }
1196
+ const methodSignature = methodCode.trim().split("\n")[0].trim();
1197
+ if (content.includes(methodSignature)) {
1198
+ return;
1199
+ }
1200
+ const indentedMethod = methodCode.split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n");
1201
+ const newContent = content.slice(0, classEnd) + "\n" + indentedMethod + "\n" + content.slice(classEnd);
1202
+ fs8.writeFileSync(filePath, newContent);
1203
+ }
1204
+ function injectImport(filePath, importLine) {
1205
+ const content = fs8.readFileSync(filePath, "utf8");
1206
+ if (content.includes(importLine.trim())) {
1207
+ return;
1208
+ }
1209
+ const importRegex = /^import\s+[^;]+;/gm;
1210
+ const imports = [...content.matchAll(importRegex)];
1211
+ if (imports.length > 0) {
1212
+ const lastImport = imports[imports.length - 1];
1213
+ const insertPos = lastImport.index + lastImport[0].length;
1214
+ const newContent = content.slice(0, insertPos) + "\n" + importLine + content.slice(insertPos);
1215
+ fs8.writeFileSync(filePath, newContent);
1216
+ } else {
1217
+ fs8.writeFileSync(filePath, importLine + "\n\n" + content);
1218
+ }
1219
+ }
1220
+ function injectExport(filePath, exportLine) {
1221
+ let content = "";
1222
+ if (fs8.existsSync(filePath)) {
1223
+ content = fs8.readFileSync(filePath, "utf8");
1224
+ }
1225
+ if (content.includes(exportLine)) {
1226
+ return;
1227
+ }
1228
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
1229
+ lines.push(exportLine);
1230
+ lines.sort();
1231
+ fs8.writeFileSync(filePath, lines.join("\n") + "\n");
1232
+ }
1233
+
1234
+ // src/core/create-endpoint.ts
1235
+ async function createEndpoint(options) {
1236
+ const lib = path8.join(options.projectRoot, "lib");
1237
+ const pascal = pascalCase4(options.responseModelName);
1238
+ const useCasePascal = pascalCase4(options.useCaseName);
1239
+ const useCaseSnake = options.useCaseName;
1240
+ const hasRequestBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod) && options.requestFields;
1241
+ const pathParams = extractPathParams(options.endpointPath);
1242
+ const methodParams = buildMethodParams(options, pathParams, hasRequestBody);
1243
+ const methodParamsForSignature = methodParams.map((p) => ({ name: p.name, type: p.type }));
1244
+ const returnType = `${pascal}Entity`;
1245
+ const repositoryInterfaceSnake = path8.basename(options.repositoryInterfaceFile, ".dart");
1246
+ const spinner2 = (msg) => console.log(chalk4.cyan(` \u2192 ${msg}`));
1247
+ if (options.responseFields && options.responseFields.length > 0) {
1248
+ spinner2("Generating entity");
1249
+ const entityDir = path8.join(lib, "domain", "entities", options.feature);
1250
+ fs9.mkdirSync(entityDir, { recursive: true });
1251
+ fs9.writeFileSync(
1252
+ path8.join(entityDir, `${options.responseModelName}_entity.dart`),
1253
+ entityTemplate(options.responseModelName, pascal, options.responseFields)
1254
+ );
1255
+ spinner2("Generating model");
1256
+ const modelDir = path8.join(lib, "data", "models", options.feature);
1257
+ fs9.mkdirSync(modelDir, { recursive: true });
1258
+ fs9.writeFileSync(
1259
+ path8.join(modelDir, `${options.responseModelName}_model.dart`),
1260
+ modelTemplate(options.responseModelName, pascal, options.responseFields)
1261
+ );
1262
+ }
1263
+ if (hasRequestBody && options.requestFields && options.requestModelName) {
1264
+ spinner2("Generating request model");
1265
+ const reqPascal = pascalCase4(options.requestModelName);
1266
+ const reqModelDir = path8.join(lib, "data", "models", options.feature);
1267
+ fs9.mkdirSync(reqModelDir, { recursive: true });
1268
+ fs9.writeFileSync(
1269
+ path8.join(reqModelDir, `${options.requestModelName}_request_model.dart`),
1270
+ requestModelTemplate(options.requestModelName, reqPascal, options.requestFields)
1271
+ );
1272
+ }
1273
+ spinner2("Injecting Retrofit method");
1274
+ const bffPath = path8.resolve(options.bffApiFile);
1275
+ if (fs9.existsSync(bffPath)) {
1276
+ const retrofitParams = buildRetrofitParams(options, pathParams, hasRequestBody);
1277
+ const retrofitReturnType = `${pascal}Model`;
1278
+ const method = retrofitMethod(
1279
+ camelCase(options.useCaseName),
1280
+ options.endpointPath,
1281
+ options.httpMethod,
1282
+ retrofitParams,
1283
+ retrofitReturnType
1284
+ );
1285
+ injectMethod(bffPath, extractClassName(bffPath), method);
1286
+ const modelImport = `import 'package:${options.feature}/data/models/${options.responseModelName}/${options.responseModelName}_model.dart';`;
1287
+ injectImport(bffPath, modelImport);
1288
+ } else {
1289
+ console.log(chalk4.yellow(` \u26A0 BFF API file not found: ${bffPath}`));
1290
+ }
1291
+ spinner2("Injecting datasource method");
1292
+ const dsPath = path8.resolve(options.datasourceFile);
1293
+ if (fs9.existsSync(dsPath)) {
1294
+ const dsMethod = datasourceMethod(
1295
+ camelCase(options.useCaseName),
1296
+ returnType,
1297
+ methodParamsForSignature
1298
+ );
1299
+ injectMethod(dsPath, options.datasourceClassName, dsMethod);
1300
+ } else {
1301
+ console.log(chalk4.yellow(` \u26A0 Datasource file not found: ${dsPath}`));
1302
+ }
1303
+ spinner2("Injecting repository interface method");
1304
+ const repoIfacePath = path8.resolve(options.repositoryInterfaceFile);
1305
+ if (fs9.existsSync(repoIfacePath)) {
1306
+ const ifaceMethod = repositoryInterfaceMethod(
1307
+ camelCase(options.useCaseName),
1308
+ returnType,
1309
+ methodParamsForSignature
1310
+ );
1311
+ injectMethod(repoIfacePath, options.repositoryInterfaceName, ifaceMethod);
1312
+ const entityImport = `import 'package:${options.feature}/domain/entities/${options.responseModelName}/${options.responseModelName}_entity.dart';`;
1313
+ injectImport(repoIfacePath, entityImport);
1314
+ } else {
1315
+ console.log(chalk4.yellow(` \u26A0 Repository interface not found: ${repoIfacePath}`));
1316
+ }
1317
+ spinner2("Injectating repository implementation method");
1318
+ const repoImplPath = path8.resolve(options.repositoryImplFile);
1319
+ if (fs9.existsSync(repoImplPath)) {
1320
+ const dsVarName = camelCase(options.datasourceClassName);
1321
+ const implMethod = repositoryImplMethod(
1322
+ camelCase(options.useCaseName),
1323
+ returnType,
1324
+ methodParamsForSignature,
1325
+ dsVarName
1326
+ );
1327
+ injectMethod(repoImplPath, options.repositoryImplClassName, implMethod);
1328
+ } else {
1329
+ console.log(chalk4.yellow(` \u26A0 Repository implementation not found: ${repoImplPath}`));
1330
+ }
1331
+ spinner2("Generating UseCase");
1332
+ const useCaseDir = path8.join(lib, "domain", "usecases", options.feature);
1333
+ fs9.mkdirSync(useCaseDir, { recursive: true });
1334
+ fs9.writeFileSync(
1335
+ path8.join(useCaseDir, `${useCaseSnake}_usecase.dart`),
1336
+ useCaseTemplate2(
1337
+ useCaseSnake,
1338
+ useCasePascal,
1339
+ camelCase(options.useCaseName),
1340
+ methodParamsForSignature,
1341
+ returnType,
1342
+ repositoryInterfaceName
1343
+ )
1344
+ );
1345
+ spinner2("Updating barrel file");
1346
+ const barrelPath = path8.join(useCaseDir, "usecases.dart");
1347
+ const exportLine = `export '${useCaseSnake}_usecase.dart';`;
1348
+ injectExport(barrelPath, exportLine);
1349
+ if (options.runBuildRunner) {
1350
+ const projectRoot = findPubspecDir(options.projectRoot);
1351
+ if (projectRoot && hasBuildRunner(projectRoot)) {
1352
+ spinner2("Running build_runner...");
1353
+ await runBuildRunner(projectRoot);
1354
+ console.log(chalk4.green(" \u2713 build_runner completed."));
1355
+ } else {
1356
+ console.log(chalk4.yellow(" \u26A0 Skipping build_runner (not found)."));
1357
+ }
1358
+ }
1359
+ console.log(chalk4.green(`
1360
+ \u2713 Endpoint "${options.useCaseName}" generated successfully.`));
1361
+ }
1362
+ function extractPathParams(endpointPath) {
1363
+ const regex = /\{(\w+)\}/g;
1364
+ const params = [];
1365
+ let match;
1366
+ while ((match = regex.exec(endpointPath)) !== null) {
1367
+ params.push({ name: match[1], type: "String" });
1368
+ }
1369
+ return params;
1370
+ }
1371
+ function buildMethodParams(options, pathParams, hasRequestBody) {
1372
+ const params = [...pathParams];
1373
+ if (hasRequestBody && options.requestModelName) {
1374
+ const reqPascal = pascalCase4(options.requestModelName);
1375
+ params.push({ name: "body", type: `${reqPascal}RequestModel` });
1376
+ }
1377
+ return params;
1378
+ }
1379
+ function buildRetrofitParams(options, pathParams, hasRequestBody) {
1380
+ const params = [];
1381
+ for (const pp of pathParams) {
1382
+ params.push({ ...pp, isPath: true });
1383
+ }
1384
+ if (hasRequestBody && options.requestModelName) {
1385
+ const reqPascal = pascalCase4(options.requestModelName);
1386
+ params.push({ name: "body", type: `${reqPascal}RequestModel`, isBody: true });
1387
+ }
1388
+ return params;
1389
+ }
1390
+ function extractClassName(filePath) {
1391
+ const content = fs9.readFileSync(filePath, "utf8");
1392
+ const match = content.match(/class\s+(\w+)\s+/);
1393
+ if (!match) throw new Error(`No class found in ${filePath}`);
1394
+ return match[1];
1395
+ }
1396
+
1397
+ // src/interactive.ts
1398
+ import { pascalCase as pascalCase5 } from "change-case";
964
1399
  var SNAKE_CASE_REGEX4 = /^[a-z][a-z0-9_]*$/;
965
1400
  async function resolveProject() {
966
1401
  const s = clack.spinner();
967
1402
  s.start("Analyzing current directory...");
968
1403
  const cwdProject = analyzeProject(process.cwd());
969
1404
  if (cwdProject && (cwdProject.hasFreezed || cwdProject.hasBloc)) {
970
- s.stop(`Found ${chalk4.green(cwdProject.projectName)}`);
1405
+ s.stop(`Found ${chalk5.green(cwdProject.projectName)}`);
971
1406
  return cwdProject;
972
1407
  }
973
1408
  s.message("Looking for Melos monorepo...");
@@ -980,8 +1415,8 @@ async function resolveProject() {
980
1415
  }
981
1416
  }
982
1417
  s.message("Scanning for Flutter projects...");
983
- const homeDev = path8.join(os.homedir(), "Development");
984
- if (fs8.existsSync(homeDev)) {
1418
+ const homeDev = path9.join(os.homedir(), "Development");
1419
+ if (fs10.existsSync(homeDev)) {
985
1420
  const projects = discoverProjects(homeDev, 2);
986
1421
  if (projects.length > 0) {
987
1422
  s.stop(`Found ${projects.length} Flutter project(s)`);
@@ -989,12 +1424,12 @@ async function resolveProject() {
989
1424
  }
990
1425
  }
991
1426
  s.stop("No Flutter projects found");
992
- clack.outro(chalk4.red("Could not find any Flutter project with freezed or flutter_bloc."));
1427
+ clack.outro(chalk5.red("Could not find any Flutter project with freezed or flutter_bloc."));
993
1428
  return null;
994
1429
  }
995
1430
  async function selectPackage(projects) {
996
1431
  if (projects.length === 1) {
997
- clack.log.info(`Using ${chalk4.green(projects[0].projectName)}`);
1432
+ clack.log.info(`Using ${chalk5.green(projects[0].projectName)}`);
998
1433
  return projects[0];
999
1434
  }
1000
1435
  const selected = await clack.select({
@@ -1002,7 +1437,7 @@ async function selectPackage(projects) {
1002
1437
  options: projects.map((p) => ({
1003
1438
  value: p,
1004
1439
  label: p.projectName,
1005
- hint: path8.relative(os.homedir(), p.projectRoot)
1440
+ hint: path9.relative(os.homedir(), p.projectRoot)
1006
1441
  }))
1007
1442
  });
1008
1443
  if (clack.isCancel(selected)) {
@@ -1050,13 +1485,13 @@ async function blocFlow(project) {
1050
1485
  clack.cancel("Cancelled");
1051
1486
  return;
1052
1487
  }
1053
- targetDir = path8.resolve(customPath);
1488
+ targetDir = path9.resolve(customPath);
1054
1489
  } else {
1055
- targetDir = path8.join(project.projectRoot, "lib", "features", feature);
1490
+ targetDir = path9.join(project.projectRoot, "lib", "features", feature);
1056
1491
  }
1057
- } else if (fs8.existsSync(path8.join(project.projectRoot, "lib", "bloc"))) {
1058
- targetDir = path8.join(project.projectRoot, "lib", "bloc");
1059
- clack.log.info(`Target: ${chalk4.cyan("lib/bloc/")}`);
1492
+ } else if (fs10.existsSync(path9.join(project.projectRoot, "lib", "bloc"))) {
1493
+ targetDir = path9.join(project.projectRoot, "lib", "bloc");
1494
+ clack.log.info(`Target: ${chalk5.cyan("lib/bloc/")}`);
1060
1495
  } else {
1061
1496
  clack.note(
1062
1497
  "No lib/features/ directory found. Provide a target path manually.",
@@ -1073,7 +1508,7 @@ async function blocFlow(project) {
1073
1508
  clack.cancel("Cancelled");
1074
1509
  return;
1075
1510
  }
1076
- targetDir = path8.resolve(customPath);
1511
+ targetDir = path9.resolve(customPath);
1077
1512
  }
1078
1513
  const defaultRun = project.hasBuildRunner;
1079
1514
  const runBuildRunner2 = await clack.confirm({
@@ -1092,10 +1527,10 @@ async function blocFlow(project) {
1092
1527
  buildRunner: runBuildRunner2
1093
1528
  });
1094
1529
  genSpinner.stop("BLoC generated");
1095
- clack.outro(chalk4.green("Done!"));
1530
+ clack.outro(chalk5.green("Done!"));
1096
1531
  } catch (error) {
1097
1532
  genSpinner.stop("Failed");
1098
- clack.outro(chalk4.red(`Error: ${error}`));
1533
+ clack.outro(chalk5.red(`Error: ${error}`));
1099
1534
  }
1100
1535
  }
1101
1536
  async function widgetFlow() {
@@ -1103,14 +1538,14 @@ async function widgetFlow() {
1103
1538
  const ds = detectDesignSystem(projectRoot);
1104
1539
  if (!ds) {
1105
1540
  clack.outro(
1106
- chalk4.red(
1541
+ chalk5.red(
1107
1542
  "No design system detected. Run this command from a project with wl_design_system/ directory."
1108
1543
  )
1109
1544
  );
1110
1545
  return;
1111
1546
  }
1112
1547
  clack.log.info(
1113
- `Design system: ${chalk4.cyan(path8.relative(projectRoot, ds.componentsDir))}`
1548
+ `Design system: ${chalk5.cyan(path9.relative(projectRoot, ds.componentsDir))}`
1114
1549
  );
1115
1550
  const name = await clack.text({
1116
1551
  message: "Widget name (snake_case, without wl_ prefix)",
@@ -1175,17 +1610,17 @@ async function widgetFlow() {
1175
1610
  genSpinner.stop(`Use-case skipped: ${e}`);
1176
1611
  }
1177
1612
  }
1178
- clack.outro(chalk4.green("Done!"));
1613
+ clack.outro(chalk5.green("Done!"));
1179
1614
  } catch (error) {
1180
1615
  genSpinner.stop("Failed");
1181
- clack.outro(chalk4.red(`Error: ${error}`));
1616
+ clack.outro(chalk5.red(`Error: ${error}`));
1182
1617
  }
1183
1618
  }
1184
1619
  async function useCaseFlow(project) {
1185
1620
  const ds = detectDesignSystem(project.projectRoot);
1186
1621
  if (!ds) {
1187
1622
  clack.outro(
1188
- chalk4.red(
1623
+ chalk5.red(
1189
1624
  "No design system detected. Ensure wl_design_system/ directory exists."
1190
1625
  )
1191
1626
  );
@@ -1193,7 +1628,7 @@ async function useCaseFlow(project) {
1193
1628
  }
1194
1629
  if (!ds.widgetbookDir) {
1195
1630
  clack.outro(
1196
- chalk4.red(
1631
+ chalk5.red(
1197
1632
  "No widgetbook package detected. Ensure apps/widgetbook/ exists."
1198
1633
  )
1199
1634
  );
@@ -1242,14 +1677,358 @@ async function useCaseFlow(project) {
1242
1677
  buildRunner: runBuildRunner2
1243
1678
  });
1244
1679
  genSpinner.stop("Use-case generated");
1245
- clack.outro(chalk4.green("Done!"));
1680
+ clack.outro(chalk5.green("Done!"));
1246
1681
  } catch (error) {
1247
1682
  genSpinner.stop("Failed");
1248
- clack.outro(chalk4.red(`Error: ${error}`));
1683
+ clack.outro(chalk5.red(`Error: ${error}`));
1684
+ }
1685
+ }
1686
+ async function endpointFlow(project) {
1687
+ const lib = path9.join(project.projectRoot, "lib");
1688
+ const httpMethod = await clack.select({
1689
+ message: "HTTP Method",
1690
+ options: [
1691
+ { value: "GET", label: "GET" },
1692
+ { value: "POST", label: "POST" },
1693
+ { value: "PUT", label: "PUT" },
1694
+ { value: "PATCH", label: "PATCH" },
1695
+ { value: "DELETE", label: "DELETE" }
1696
+ ]
1697
+ });
1698
+ if (clack.isCancel(httpMethod)) {
1699
+ clack.cancel("Cancelled");
1700
+ return;
1701
+ }
1702
+ const endpointPath = await clack.text({
1703
+ message: "Endpoint path (e.g. /cp/{zipCode})",
1704
+ placeholder: "/api/users/{id}",
1705
+ validate: (v) => {
1706
+ if (!v.trim()) return "Path is required";
1707
+ }
1708
+ });
1709
+ if (clack.isCancel(endpointPath)) {
1710
+ clack.cancel("Cancelled");
1711
+ return;
1712
+ }
1713
+ const bffDir = path9.join(lib, "data", "api", "bff");
1714
+ let bffApiFile = "";
1715
+ if (fs10.existsSync(bffDir)) {
1716
+ const dartFiles = fs10.readdirSync(bffDir).filter((f) => f.endsWith(".dart")).sort();
1717
+ if (dartFiles.length > 0) {
1718
+ const selected = await clack.select({
1719
+ message: "Select BFF API file",
1720
+ options: [
1721
+ ...dartFiles.map((f) => ({
1722
+ value: path9.join(bffDir, f),
1723
+ label: f
1724
+ })),
1725
+ { value: "__custom__", label: "Custom path..." }
1726
+ ]
1727
+ });
1728
+ if (clack.isCancel(selected)) {
1729
+ clack.cancel("Cancelled");
1730
+ return;
1731
+ }
1732
+ if (selected === "__custom__") {
1733
+ const customPath = await clack.text({
1734
+ message: "BFF API file path",
1735
+ placeholder: "lib/data/api/bff/my_api.dart",
1736
+ validate: (v) => {
1737
+ if (!v.trim()) return "Path is required";
1738
+ }
1739
+ });
1740
+ if (clack.isCancel(customPath)) {
1741
+ clack.cancel("Cancelled");
1742
+ return;
1743
+ }
1744
+ bffApiFile = path9.resolve(customPath);
1745
+ } else {
1746
+ bffApiFile = selected;
1747
+ }
1748
+ } else {
1749
+ const customPath = await clack.text({
1750
+ message: "BFF API file path (no .dart files found in bff/)",
1751
+ placeholder: "lib/data/api/bff/my_api.dart",
1752
+ validate: (v) => {
1753
+ if (!v.trim()) return "Path is required";
1754
+ }
1755
+ });
1756
+ if (clack.isCancel(customPath)) {
1757
+ clack.cancel("Cancelled");
1758
+ return;
1759
+ }
1760
+ bffApiFile = path9.resolve(customPath);
1761
+ }
1762
+ } else {
1763
+ const customPath = await clack.text({
1764
+ message: "BFF API file path (bff/ dir not found)",
1765
+ placeholder: "lib/data/api/bff/my_api.dart",
1766
+ validate: (v) => {
1767
+ if (!v.trim()) return "Path is required";
1768
+ }
1769
+ });
1770
+ if (clack.isCancel(customPath)) {
1771
+ clack.cancel("Cancelled");
1772
+ return;
1773
+ }
1774
+ bffApiFile = path9.resolve(customPath);
1775
+ }
1776
+ const responseChoice = await clack.select({
1777
+ message: "Response model",
1778
+ options: [
1779
+ { value: "existing", label: "Use existing model", hint: "Reference an already created model" },
1780
+ { value: "json", label: "Paste JSON", hint: "Generate entity + model from JSON" }
1781
+ ]
1782
+ });
1783
+ if (clack.isCancel(responseChoice)) {
1784
+ clack.cancel("Cancelled");
1785
+ return;
1786
+ }
1787
+ let responseModelName = "";
1788
+ let responseFields;
1789
+ if (responseChoice === "json") {
1790
+ const modelName = await clack.text({
1791
+ message: "Response model name (snake_case)",
1792
+ placeholder: "e.g. user_profile",
1793
+ validate: (v) => {
1794
+ if (!v.trim()) return "Name is required";
1795
+ if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1796
+ }
1797
+ });
1798
+ if (clack.isCancel(modelName)) {
1799
+ clack.cancel("Cancelled");
1800
+ return;
1801
+ }
1802
+ responseModelName = modelName;
1803
+ const jsonStr = await clack.text({
1804
+ message: "Paste the JSON response",
1805
+ placeholder: '{ "id": 1, "name": "John" }',
1806
+ validate: (v) => {
1807
+ if (!v.trim()) return "JSON is required";
1808
+ try {
1809
+ JSON.parse(v);
1810
+ } catch {
1811
+ return "Invalid JSON";
1812
+ }
1813
+ }
1814
+ });
1815
+ if (clack.isCancel(jsonStr)) {
1816
+ clack.cancel("Cancelled");
1817
+ return;
1818
+ }
1819
+ responseFields = jsonToFields(jsonStr);
1820
+ } else {
1821
+ const modelName = await clack.text({
1822
+ message: "Existing response model name (snake_case)",
1823
+ placeholder: "e.g. user_profile",
1824
+ validate: (v) => {
1825
+ if (!v.trim()) return "Name is required";
1826
+ }
1827
+ });
1828
+ if (clack.isCancel(modelName)) {
1829
+ clack.cancel("Cancelled");
1830
+ return;
1831
+ }
1832
+ responseModelName = modelName;
1833
+ }
1834
+ let requestModelName;
1835
+ let requestFields;
1836
+ const needsBody = ["POST", "PUT", "PATCH"].includes(httpMethod);
1837
+ if (needsBody) {
1838
+ const requestChoice = await clack.select({
1839
+ message: "Request body",
1840
+ options: [
1841
+ { value: "json", label: "Paste JSON", hint: "Generate request model from JSON" },
1842
+ { value: "existing", label: "Use existing model", hint: "Reference an already created model" },
1843
+ { value: "none", label: "No body", hint: "Just path/query params" }
1844
+ ]
1845
+ });
1846
+ if (clack.isCancel(requestChoice)) {
1847
+ clack.cancel("Cancelled");
1848
+ return;
1849
+ }
1850
+ if (requestChoice === "json") {
1851
+ const reqName = await clack.text({
1852
+ message: "Request model name (snake_case)",
1853
+ placeholder: `e.g. create_${responseModelName}`,
1854
+ validate: (v) => {
1855
+ if (!v.trim()) return "Name is required";
1856
+ if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1857
+ }
1858
+ });
1859
+ if (clack.isCancel(reqName)) {
1860
+ clack.cancel("Cancelled");
1861
+ return;
1862
+ }
1863
+ requestModelName = reqName;
1864
+ const reqJson = await clack.text({
1865
+ message: "Paste the request JSON body",
1866
+ placeholder: '{ "name": "John", "email": "john@example.com" }',
1867
+ validate: (v) => {
1868
+ if (!v.trim()) return "JSON is required";
1869
+ try {
1870
+ JSON.parse(v);
1871
+ } catch {
1872
+ return "Invalid JSON";
1873
+ }
1874
+ }
1875
+ });
1876
+ if (clack.isCancel(reqJson)) {
1877
+ clack.cancel("Cancelled");
1878
+ return;
1879
+ }
1880
+ requestFields = jsonToFields(reqJson);
1881
+ } else if (requestChoice === "existing") {
1882
+ const reqName = await clack.text({
1883
+ message: "Existing request model name (snake_case)",
1884
+ placeholder: `e.g. create_${responseModelName}`,
1885
+ validate: (v) => {
1886
+ if (!v.trim()) return "Name is required";
1887
+ }
1888
+ });
1889
+ if (clack.isCancel(reqName)) {
1890
+ clack.cancel("Cancelled");
1891
+ return;
1892
+ }
1893
+ requestModelName = reqName;
1894
+ }
1895
+ }
1896
+ const pathSegments = endpointPath.replace(/^\//, "").split("/").filter((s) => !s.startsWith("{"));
1897
+ const inferredName = pathSegments.length > 0 ? "get_" + pathSegments.join("_") : "get_data";
1898
+ const useCaseName = await clack.text({
1899
+ message: "UseCase name (snake_case)",
1900
+ placeholder: inferredName,
1901
+ initialValue: inferredName,
1902
+ validate: (v) => {
1903
+ if (!v.trim()) return "Name is required";
1904
+ if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1905
+ }
1906
+ });
1907
+ if (clack.isCancel(useCaseName)) {
1908
+ clack.cancel("Cancelled");
1909
+ return;
1910
+ }
1911
+ const datasourceFile = await clack.text({
1912
+ message: "Datasource file path",
1913
+ placeholder: `lib/data/datasources/${responseModelName}_datasource.dart`,
1914
+ validate: (v) => {
1915
+ if (!v.trim()) return "Path is required";
1916
+ }
1917
+ });
1918
+ if (clack.isCancel(datasourceFile)) {
1919
+ clack.cancel("Cancelled");
1920
+ return;
1921
+ }
1922
+ const datasourceClassName = await clack.text({
1923
+ message: "Datasource class name",
1924
+ placeholder: `${pascalCase5(responseModelName)}Datasource`,
1925
+ initialValue: `${pascalCase5(responseModelName)}Datasource`,
1926
+ validate: (v) => {
1927
+ if (!v.trim()) return "Class name is required";
1928
+ }
1929
+ });
1930
+ if (clack.isCancel(datasourceClassName)) {
1931
+ clack.cancel("Cancelled");
1932
+ return;
1933
+ }
1934
+ const repositoryInterfaceFile = await clack.text({
1935
+ message: "Repository interface file path",
1936
+ placeholder: `lib/domain/repositories/${responseModelName}_repository.dart`,
1937
+ validate: (v) => {
1938
+ if (!v.trim()) return "Path is required";
1939
+ }
1940
+ });
1941
+ if (clack.isCancel(repositoryInterfaceFile)) {
1942
+ clack.cancel("Cancelled");
1943
+ return;
1944
+ }
1945
+ const repositoryInterfaceName2 = await clack.text({
1946
+ message: "Repository interface name",
1947
+ placeholder: `${pascalCase5(responseModelName)}Repository`,
1948
+ initialValue: `${pascalCase5(responseModelName)}Repository`,
1949
+ validate: (v) => {
1950
+ if (!v.trim()) return "Name is required";
1951
+ }
1952
+ });
1953
+ if (clack.isCancel(repositoryInterfaceName2)) {
1954
+ clack.cancel("Cancelled");
1955
+ return;
1956
+ }
1957
+ const repositoryImplFile = await clack.text({
1958
+ message: "Repository implementation file path",
1959
+ placeholder: `lib/data/repositories/${responseModelName}_repository_impl.dart`,
1960
+ validate: (v) => {
1961
+ if (!v.trim()) return "Path is required";
1962
+ }
1963
+ });
1964
+ if (clack.isCancel(repositoryImplFile)) {
1965
+ clack.cancel("Cancelled");
1966
+ return;
1967
+ }
1968
+ const repositoryImplClassName = await clack.text({
1969
+ message: "Repository implementation class name",
1970
+ placeholder: `${pascalCase5(responseModelName)}RepositoryImpl`,
1971
+ initialValue: `${pascalCase5(responseModelName)}RepositoryImpl`,
1972
+ validate: (v) => {
1973
+ if (!v.trim()) return "Name is required";
1974
+ }
1975
+ });
1976
+ if (clack.isCancel(repositoryImplClassName)) {
1977
+ clack.cancel("Cancelled");
1978
+ return;
1979
+ }
1980
+ const feature = await clack.text({
1981
+ message: "Feature name (snake_case, for directory grouping)",
1982
+ placeholder: responseModelName,
1983
+ initialValue: responseModelName,
1984
+ validate: (v) => {
1985
+ if (!v.trim()) return "Feature name is required";
1986
+ if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1987
+ }
1988
+ });
1989
+ if (clack.isCancel(feature)) {
1990
+ clack.cancel("Cancelled");
1991
+ return;
1992
+ }
1993
+ const runBuildRunner2 = await clack.confirm({
1994
+ message: "Run build_runner after generation?",
1995
+ initialValue: project.hasBuildRunner
1996
+ });
1997
+ if (clack.isCancel(runBuildRunner2)) {
1998
+ clack.cancel("Cancelled");
1999
+ return;
2000
+ }
2001
+ const genSpinner = clack.spinner();
2002
+ genSpinner.start("Generating endpoint stack...");
2003
+ try {
2004
+ await createEndpoint({
2005
+ projectRoot: project.projectRoot,
2006
+ httpMethod,
2007
+ endpointPath,
2008
+ bffApiFile,
2009
+ responseModelName,
2010
+ responseFields,
2011
+ requestModelName,
2012
+ requestFields,
2013
+ useCaseName,
2014
+ datasourceFile,
2015
+ datasourceClassName,
2016
+ repositoryInterfaceFile,
2017
+ repositoryInterfaceName: repositoryInterfaceName2,
2018
+ repositoryImplFile,
2019
+ repositoryImplClassName,
2020
+ feature,
2021
+ runBuildRunner: runBuildRunner2
2022
+ });
2023
+ genSpinner.stop("Endpoint generated");
2024
+ clack.outro(chalk5.green("Done!"));
2025
+ } catch (error) {
2026
+ genSpinner.stop("Failed");
2027
+ clack.outro(chalk5.red(`Error: ${error}`));
1249
2028
  }
1250
2029
  }
1251
2030
  async function interactiveMode() {
1252
- clack.intro(chalk4.bgCyan(chalk4.black(" wlmaker ")));
2031
+ clack.intro(chalk5.bgCyan(chalk5.black(" wlmaker ")));
1253
2032
  const createType = await clack.select({
1254
2033
  message: "What do you want to create?",
1255
2034
  options: [
@@ -1259,6 +2038,11 @@ async function interactiveMode() {
1259
2038
  value: "usecase",
1260
2039
  label: "Widgetbook Use-Case",
1261
2040
  hint: "Component showcase"
2041
+ },
2042
+ {
2043
+ value: "endpoint",
2044
+ label: "Endpoint",
2045
+ hint: "BFF Clean Architecture stack"
1262
2046
  }
1263
2047
  ]
1264
2048
  });
@@ -1283,6 +2067,12 @@ async function interactiveMode() {
1283
2067
  await useCaseFlow(project);
1284
2068
  break;
1285
2069
  }
2070
+ case "endpoint": {
2071
+ const project = await resolveProject();
2072
+ if (!project) return;
2073
+ await endpointFlow(project);
2074
+ break;
2075
+ }
1286
2076
  }
1287
2077
  }
1288
2078
 
@@ -1302,7 +2092,7 @@ program.command("bloc").description("Create a new BLoC with Freezed sealed class
1302
2092
  try {
1303
2093
  await createBloc(name, options);
1304
2094
  } catch (error) {
1305
- console.error(chalk5.red(`Error: ${error}`));
2095
+ console.error(chalk6.red(`Error: ${error}`));
1306
2096
  process.exit(1);
1307
2097
  }
1308
2098
  }
@@ -1319,12 +2109,12 @@ program.command("widget").description("Create a new widget in the design system"
1319
2109
  projectRoot: options.dir,
1320
2110
  buildRunner: false
1321
2111
  });
1322
- console.log(chalk5.green("Widgetbook use-case created"));
2112
+ console.log(chalk6.green("Widgetbook use-case created"));
1323
2113
  } catch {
1324
- console.log(chalk5.yellow("Use-case skipped (may already exist)"));
2114
+ console.log(chalk6.yellow("Use-case skipped (may already exist)"));
1325
2115
  }
1326
2116
  } catch (error) {
1327
- console.error(chalk5.red(`Error: ${error}`));
2117
+ console.error(chalk6.red(`Error: ${error}`));
1328
2118
  process.exit(1);
1329
2119
  }
1330
2120
  }
@@ -1337,11 +2127,16 @@ program.command("usecase").description("Create a Widgetbook use-case for an exis
1337
2127
  buildRunner: options.buildRunner
1338
2128
  });
1339
2129
  } catch (error) {
1340
- console.error(chalk5.red(`Error: ${error}`));
2130
+ console.error(chalk6.red(`Error: ${error}`));
1341
2131
  process.exit(1);
1342
2132
  }
1343
2133
  }
1344
2134
  );
2135
+ program.command("endpoint").description("Generate Clean Architecture stack for a BFF endpoint").action(async () => {
2136
+ const project = await resolveProject();
2137
+ if (!project) return;
2138
+ await endpointFlow(project);
2139
+ });
1345
2140
  program.action(async () => {
1346
2141
  await interactiveMode();
1347
2142
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wlmaker",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Create Flutter BLoCs with Freezed sealed classes from the terminal",
5
5
  "keywords": [
6
6
  "flutter",