@kosdev-code/kos-codegen-core 3.0.20 → 3.0.21

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 (53) hide show
  1. package/index.d.ts +2 -2
  2. package/index.d.ts.map +1 -1
  3. package/index.js +1266 -379
  4. package/index.js.map +1 -1
  5. package/index.mjs +1267 -380
  6. package/index.mjs.map +1 -1
  7. package/lib/generators/add-container-support/generate-add-container-support.d.ts +14 -0
  8. package/lib/generators/add-container-support/generate-add-container-support.d.ts.map +1 -1
  9. package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts +15 -0
  10. package/lib/generators/add-parent-aware/generate-add-parent-aware.d.ts.map +1 -0
  11. package/lib/generators/add-parent-aware/index.d.ts +2 -0
  12. package/lib/generators/add-parent-aware/index.d.ts.map +1 -0
  13. package/lib/generators/augment/find-model-declaration.d.ts +11 -0
  14. package/lib/generators/augment/find-model-declaration.d.ts.map +1 -0
  15. package/lib/generators/augment/model-mocks.d.ts +47 -0
  16. package/lib/generators/augment/model-mocks.d.ts.map +1 -0
  17. package/lib/generators/augment/resolve-child-model.d.ts +16 -0
  18. package/lib/generators/augment/resolve-child-model.d.ts.map +1 -0
  19. package/lib/generators/augment/resolve-model-file.d.ts.map +1 -1
  20. package/lib/generators/augment/ts-toolkit.d.ts +26 -1
  21. package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
  22. package/lib/generators/generate-container-model.d.ts +7 -0
  23. package/lib/generators/generate-container-model.d.ts.map +1 -1
  24. package/lib/generators/generate-model-project.d.ts +36 -0
  25. package/lib/generators/generate-model-project.d.ts.map +1 -0
  26. package/lib/generators/generate-view-model.d.ts +35 -0
  27. package/lib/generators/generate-view-model.d.ts.map +1 -0
  28. package/lib/generators/index.d.ts +21 -16
  29. package/lib/generators/index.d.ts.map +1 -1
  30. package/lib/generators/member-mutators/add-child.d.ts +10 -2
  31. package/lib/generators/member-mutators/add-child.d.ts.map +1 -1
  32. package/lib/generators/member-mutators/add-service-request.d.ts +71 -0
  33. package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
  34. package/lib/generators/member-mutators/index.d.ts +1 -1
  35. package/lib/generators/member-mutators/index.d.ts.map +1 -1
  36. package/lib/generators/registration/upsert-chain-entry.d.ts +13 -0
  37. package/lib/generators/registration/upsert-chain-entry.d.ts.map +1 -0
  38. package/lib/generators/service-catalog.d.ts +12 -0
  39. package/lib/generators/service-catalog.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/templates/kos-container-model/model/types/index.d.ts.template +2 -2
  42. package/templates/kos-model-project/project/.eslintrc.json.template +33 -0
  43. package/templates/kos-model-project/project/.kos.json.template +14 -0
  44. package/templates/kos-model-project/project/README.md.template +7 -0
  45. package/templates/kos-model-project/project/package.json.template +9 -0
  46. package/templates/kos-model-project/project/project.json.template +35 -0
  47. package/templates/kos-model-project/project/src/index.ts.template +1 -0
  48. package/templates/kos-model-project/project/src/lib/__projectName__.ts.template +3 -0
  49. package/templates/kos-model-project/project/tsconfig.json.template +20 -0
  50. package/templates/kos-model-project/project/tsconfig.lib.json.template +10 -0
  51. package/templates/kos-model-project/project/vite.config.ts.template +47 -0
  52. package/templates/kos-view-model/__nameDashCase__-view-model.ts.template +30 -0
  53. package/templates/kos-view-model/index.ts.template +1 -0
package/index.js CHANGED
@@ -496,6 +496,84 @@ function generateSplashProject(codegenFs, templateDir, options) {
496
496
  normalized
497
497
  );
498
498
  }
499
+ const MODEL_PROJECT_SUFFIX = "-models";
500
+ const DEFAULT_MODEL_LIBS_DIR = "libs";
501
+ const JSONC_ESLINT_PARSER_VERSION = "^2.1.0";
502
+ function resolveModelProjectLayout(codegenFs, options) {
503
+ const base = dashCase(options.name);
504
+ const projectName = base.endsWith(MODEL_PROJECT_SUFFIX) ? base : `${base}${MODEL_PROJECT_SUFFIX}`;
505
+ const libsDir = (options.libsDir || DEFAULT_MODEL_LIBS_DIR).replace(
506
+ /\\/g,
507
+ "/"
508
+ );
509
+ const scope = codegenFs ? readNpmScope(codegenFs) : void 0;
510
+ return {
511
+ projectName,
512
+ projectRoot: `${libsDir}/${projectName}`.replace(/^\/+/, ""),
513
+ importPath: scope ? `@${scope}/${projectName}` : projectName
514
+ };
515
+ }
516
+ function readNpmScope(codegenFs) {
517
+ if (!codegenFs.exists("package.json")) {
518
+ return void 0;
519
+ }
520
+ const name = readJson(codegenFs, "package.json").name;
521
+ if (!name?.startsWith("@")) {
522
+ return void 0;
523
+ }
524
+ return name.split("/")[0].slice(1);
525
+ }
526
+ function offsetFromRoot(projectRoot) {
527
+ return projectRoot.split("/").filter(Boolean).map(() => "../").join("");
528
+ }
529
+ function generateModelProject(codegenFs, templateDir, options) {
530
+ const layout = resolveModelProjectLayout(codegenFs, options);
531
+ generateFilesFromTemplates(
532
+ codegenFs,
533
+ path__namespace.join(templateDir, "project"),
534
+ layout.projectRoot,
535
+ {
536
+ ...layout,
537
+ projectNameCamelCase: camelCase(layout.projectName),
538
+ offsetFromRoot: offsetFromRoot(layout.projectRoot),
539
+ template: ""
540
+ }
541
+ );
542
+ registerTsconfigPath(codegenFs, layout);
543
+ ensureJsoncEslintParser(codegenFs);
544
+ return layout;
545
+ }
546
+ function registerTsconfigPath(codegenFs, layout) {
547
+ if (!codegenFs.exists("tsconfig.base.json")) {
548
+ return;
549
+ }
550
+ updateJson(codegenFs, "tsconfig.base.json", (json) => {
551
+ json.compilerOptions = json.compilerOptions ?? {};
552
+ json.compilerOptions.paths = json.compilerOptions.paths ?? {};
553
+ json.compilerOptions.paths[layout.importPath] = [
554
+ `${layout.projectRoot}/src/index.ts`
555
+ ];
556
+ return json;
557
+ });
558
+ }
559
+ function ensureJsoncEslintParser(codegenFs) {
560
+ if (!codegenFs.exists("package.json")) {
561
+ return;
562
+ }
563
+ const pkg = readJson(codegenFs, "package.json");
564
+ const devDependencies = pkg.devDependencies ?? {};
565
+ if ("jsonc-eslint-parser" in devDependencies) {
566
+ return;
567
+ }
568
+ devDependencies["jsonc-eslint-parser"] = JSONC_ESLINT_PARSER_VERSION;
569
+ pkg.devDependencies = Object.fromEntries(
570
+ // Codepoint order, matching what Nx's own dependency helper produces.
571
+ Object.entries(devDependencies).sort(
572
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
573
+ )
574
+ );
575
+ writeJson(codegenFs, "package.json", pkg);
576
+ }
499
577
  function generateInit(codegenFs, options) {
500
578
  const logger = getCodegenLogger();
501
579
  const { appProject, modelProject, registrationProject } = options;
@@ -947,6 +1025,58 @@ function normalizeOptions(codegenFs, options, projects) {
947
1025
  template: ""
948
1026
  };
949
1027
  }
1028
+ const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1029
+ import { resolve } from "path";
1030
+ import { readFileSync, writeFileSync } from "fs";
1031
+ import prettier from "prettier";
1032
+
1033
+ // KOS artifact versioning: stamps the project's .kos.json "version" field
1034
+ // (which kabtool bakes into the KAB). Never touches package.json.
1035
+ // Driven by tag-based releases:
1036
+ // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1037
+
1038
+ const { readCachedProjectGraph } = devkit;
1039
+ const [, , name, versionArg] = process.argv;
1040
+
1041
+ // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1042
+ // treat that (or a missing arg) as "report current version, change nothing".
1043
+ const version =
1044
+ versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1045
+
1046
+ if (!name) {
1047
+ console.error("usage: update-release-version.mjs <project> <version>");
1048
+ process.exit(1);
1049
+ }
1050
+
1051
+ const graph = readCachedProjectGraph();
1052
+ const project = graph.nodes[name];
1053
+ if (!project) {
1054
+ console.error("Unknown project: " + name);
1055
+ process.exit(1);
1056
+ }
1057
+
1058
+ const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1059
+ let kosJson;
1060
+ try {
1061
+ kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1062
+ } catch {
1063
+ console.error("Missing or invalid .kos.json: " + kosJsonPath);
1064
+ process.exit(1);
1065
+ }
1066
+
1067
+ if (!version) {
1068
+ console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1069
+ process.exit(0);
1070
+ }
1071
+
1072
+ const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1073
+ const output = await prettier.format(
1074
+ JSON.stringify({ ...kosJson, version }, null, 2),
1075
+ { ...prettierOptions, parser: "json" }
1076
+ );
1077
+ writeFileSync(kosJsonPath, output);
1078
+ console.log(name + ": version -> " + version);
1079
+ `;
950
1080
  function transformSourceFile(codegenFs, filePath, mutate) {
951
1081
  const content = codegenFs.read(filePath);
952
1082
  if (content === null) {
@@ -1088,7 +1218,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1088
1218
  }
1089
1219
  literal.addPropertyAssignment({
1090
1220
  name: spec.key,
1091
- initializer: spec.initializer
1221
+ initializer: spec.initializer,
1222
+ leadingTrivia: spec.leadingTrivia
1092
1223
  });
1093
1224
  return { key: spec.key, created: true };
1094
1225
  }
@@ -1106,7 +1237,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1106
1237
  }
1107
1238
  literal.addPropertyAssignment({
1108
1239
  name: spec.key,
1109
- initializer: spec.initializer
1240
+ initializer: spec.initializer,
1241
+ leadingTrivia: spec.leadingTrivia
1110
1242
  });
1111
1243
  return { key: spec.key, created: true };
1112
1244
  }
@@ -1115,7 +1247,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
1115
1247
  sourceFile.addTypeAlias({
1116
1248
  name: spec.name,
1117
1249
  type: spec.type,
1118
- isExported: true
1250
+ isExported: true,
1251
+ docs: spec.docs ? [spec.docs] : void 0
1119
1252
  });
1120
1253
  return true;
1121
1254
  }
@@ -1139,12 +1272,47 @@ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1139
1272
  sourceFile.addExportDeclaration({ moduleSpecifier });
1140
1273
  return true;
1141
1274
  }
1275
+ function ensureNamedExports(sourceFile, moduleSpecifier, names, isTypeOnly = false) {
1276
+ const existing = sourceFile.getExportDeclarations().find(
1277
+ (d) => d.getModuleSpecifierValue() === moduleSpecifier && d.isTypeOnly() === isTypeOnly
1278
+ );
1279
+ if (!existing) {
1280
+ sourceFile.addExportDeclaration({
1281
+ moduleSpecifier,
1282
+ isTypeOnly,
1283
+ namedExports: names.map((name) => {
1284
+ return { name };
1285
+ })
1286
+ });
1287
+ return true;
1288
+ }
1289
+ const present = new Set(existing.getNamedExports().map((e) => e.getName()));
1290
+ const missing = names.filter((name) => !present.has(name));
1291
+ if (missing.length === 0) return false;
1292
+ existing.addNamedExports(
1293
+ missing.map((name) => {
1294
+ return { name };
1295
+ })
1296
+ );
1297
+ return true;
1298
+ }
1299
+ function ensureExportedInterface(sourceFile, name, properties = [], extendsTypes = []) {
1300
+ if (sourceFile.getInterface(name)) return false;
1301
+ sourceFile.addInterface({
1302
+ name,
1303
+ isExported: true,
1304
+ extends: extendsTypes,
1305
+ properties
1306
+ });
1307
+ return true;
1308
+ }
1142
1309
  function addDecoratedMethod(cls, spec) {
1143
1310
  if (cls.getMethod(spec.name)) return false;
1144
1311
  cls.addMethod({
1145
1312
  name: spec.name,
1146
1313
  isAsync: spec.isAsync,
1147
1314
  returnType: spec.returnType,
1315
+ docs: spec.docs ? [spec.docs] : void 0,
1148
1316
  parameters: spec.parameters?.map((p) => {
1149
1317
  return { name: p.name, type: p.type, hasQuestionToken: p.optional };
1150
1318
  }),
@@ -1152,6 +1320,7 @@ function addDecoratedMethod(cls, spec) {
1152
1320
  decorators: [
1153
1321
  {
1154
1322
  name: spec.decoratorName,
1323
+ typeArguments: spec.decoratorTypeArgs,
1155
1324
  arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1156
1325
  }
1157
1326
  ]
@@ -1234,17 +1403,28 @@ function listServiceCatalog(codegenFs, query, projects) {
1234
1403
  app,
1235
1404
  version,
1236
1405
  serviceModulePath: posix,
1237
- operations: readOperations(codegenFs, openapiPath)
1406
+ operations: readOperations(codegenFs, openapiPath) ?? []
1238
1407
  });
1239
1408
  }
1240
1409
  return entries.sort(
1241
1410
  (a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
1242
1411
  );
1243
1412
  }
1413
+ function readServiceModuleOperations(codegenFs, serviceModuleFile) {
1414
+ const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
1415
+ const posix = moduleFile.split(path__namespace.sep).join("/");
1416
+ const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
1417
+ for (const candidate of [openapiPath, posix]) {
1418
+ if (!codegenFs.exists(candidate)) continue;
1419
+ const operations = readOperations(codegenFs, candidate);
1420
+ if (operations) return operations;
1421
+ }
1422
+ return null;
1423
+ }
1244
1424
  function readOperations(codegenFs, openapiPath) {
1245
1425
  return readSourceFile(codegenFs, openapiPath, (sf) => {
1246
1426
  const paths = sf.getInterface("paths");
1247
- if (!paths) return [];
1427
+ if (!paths) return null;
1248
1428
  const operations = [];
1249
1429
  for (const pathProp of paths.getProperties()) {
1250
1430
  const servicePath = unquote(pathProp.getName());
@@ -1278,58 +1458,6 @@ function readSummary(methodProp) {
1278
1458
  function unquote(name) {
1279
1459
  return name.replace(/^["']|["']$/g, "");
1280
1460
  }
1281
- const UPDATE_RELEASE_VERSION_SCRIPT = `import devkit from "@nx/devkit";
1282
- import { resolve } from "path";
1283
- import { readFileSync, writeFileSync } from "fs";
1284
- import prettier from "prettier";
1285
-
1286
- // KOS artifact versioning: stamps the project's .kos.json "version" field
1287
- // (which kabtool bakes into the KAB). Never touches package.json.
1288
- // Driven by tag-based releases:
1289
- // nx run-many --target=version --args=--ver=$KOSBUILD_VERSION
1290
-
1291
- const { readCachedProjectGraph } = devkit;
1292
- const [, , name, versionArg] = process.argv;
1293
-
1294
- // "{args.ver}" arrives literally when the target runs without --args=--ver=<v>;
1295
- // treat that (or a missing arg) as "report current version, change nothing".
1296
- const version =
1297
- versionArg && !versionArg.startsWith("{args") ? versionArg : undefined;
1298
-
1299
- if (!name) {
1300
- console.error("usage: update-release-version.mjs <project> <version>");
1301
- process.exit(1);
1302
- }
1303
-
1304
- const graph = readCachedProjectGraph();
1305
- const project = graph.nodes[name];
1306
- if (!project) {
1307
- console.error("Unknown project: " + name);
1308
- process.exit(1);
1309
- }
1310
-
1311
- const kosJsonPath = resolve(process.cwd(), project.data.root, ".kos.json");
1312
- let kosJson;
1313
- try {
1314
- kosJson = JSON.parse(readFileSync(kosJsonPath, "utf8"));
1315
- } catch {
1316
- console.error("Missing or invalid .kos.json: " + kosJsonPath);
1317
- process.exit(1);
1318
- }
1319
-
1320
- if (!version) {
1321
- console.log(name + ": " + kosJson.version + " (no --ver given; unchanged)");
1322
- process.exit(0);
1323
- }
1324
-
1325
- const prettierOptions = await prettier.resolveConfig(kosJsonPath);
1326
- const output = await prettier.format(
1327
- JSON.stringify({ ...kosJson, version }, null, 2),
1328
- { ...prettierOptions, parser: "json" }
1329
- );
1330
- writeFileSync(kosJsonPath, output);
1331
- console.log(name + ": version -> " + version);
1332
- `;
1333
1461
  function appendBarrelExport(codegenFs, indexPath, exportPath) {
1334
1462
  const exportLine = `export * from '${exportPath}'`;
1335
1463
  const content = codegenFs.read(indexPath) ?? "";
@@ -1376,10 +1504,57 @@ function updateModelIndex(codegenFs, indexPath, modelPath) {
1376
1504
  const newContents = printer.printFile(updatedSourceFile);
1377
1505
  codegenFs.write(indexPath, newContents);
1378
1506
  }
1379
- function generateHook(codegenFs, templateDir, options, cwd, projects) {
1380
- if (!options.appProject) {
1381
- throw new Error("No app project specified");
1507
+ function generateCompanionModel(codegenFs, templateDir, options, projects) {
1508
+ const logger = getCodegenLogger();
1509
+ const normalized = normalizeAllValues({
1510
+ companionModelName: options.companionModelName,
1511
+ modelName: options.modelName
1512
+ });
1513
+ const companionChildKosConfig = getKosProjectConfiguration(
1514
+ codegenFs,
1515
+ options.companionModelProject,
1516
+ projects
1517
+ );
1518
+ const parentProject = findProjectByName(
1519
+ codegenFs.root,
1520
+ options.modelProject,
1521
+ projects
1522
+ );
1523
+ const childProject = findProjectByName(
1524
+ codegenFs.root,
1525
+ options.companionModelProject,
1526
+ projects
1527
+ );
1528
+ const projectRoot = childProject?.sourceRoot;
1529
+ if (!projectRoot) {
1530
+ logger.warn(`Companion child project source root not found`);
1531
+ return;
1532
+ }
1533
+ let importPath = "";
1534
+ if (parentProject) {
1535
+ const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
1536
+ try {
1537
+ const pkgJson = readJson(codegenFs, pkgJsonPath);
1538
+ importPath = pkgJson.name || "";
1539
+ } catch {
1540
+ importPath = "";
1541
+ }
1382
1542
  }
1543
+ const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1544
+ const filePath = path__namespace.join(
1545
+ projectRoot,
1546
+ modelLocation,
1547
+ normalized.companionModelNameDashCase
1548
+ );
1549
+ logger.info(`Generating companion model in ${filePath}`);
1550
+ generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1551
+ ...options,
1552
+ ...normalized,
1553
+ importPath
1554
+ });
1555
+ }
1556
+ function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1557
+ const logger = getCodegenLogger();
1383
1558
  const currentProject = getProject(codegenFs, cwd);
1384
1559
  const modelProjectName = options.modelProject || currentProject?.name;
1385
1560
  if (!modelProjectName) {
@@ -1387,56 +1562,109 @@ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1387
1562
  "No model project found. Please specify a model project with --modelProject."
1388
1563
  );
1389
1564
  }
1390
- const modelName = options.name || getCurrentDirectoryName(cwd);
1565
+ const modelName = options.modelName || getCurrentDirectoryName(cwd);
1391
1566
  if (!modelName) {
1392
1567
  throw new Error(
1393
1568
  "No model name found. Please specify a model name with --name."
1394
1569
  );
1395
1570
  }
1396
- const kosModelConfig = getKosModelConfiguration(
1397
- codegenFs,
1398
- modelProjectName,
1399
- modelName,
1400
- projects
1401
- );
1402
- options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1403
- options.name = modelName;
1404
- options.modelProject = modelProjectName;
1571
+ options.modelName = modelName;
1572
+ options.name = `${modelName}-container`;
1405
1573
  const normalized = normalizeOptions(codegenFs, options, projects);
1406
- const appProject = findProjectByName(
1574
+ const projectConfig = findProjectByName(
1407
1575
  codegenFs.root,
1408
- normalized.appProject,
1576
+ normalized.modelProject,
1409
1577
  projects
1410
1578
  );
1411
- if (!appProject) {
1412
- throw new Error(`App project '${normalized.appProject}' not found`);
1579
+ if (!projectConfig) {
1580
+ throw new Error(`Model project '${normalized.modelProject}' not found`);
1413
1581
  }
1582
+ addKosModelConfiguration({
1583
+ codegenFs,
1584
+ modelName: normalized.nameDashCase,
1585
+ projectName: projectConfig.name,
1586
+ projectRoot: projectConfig.root,
1587
+ singleton: !!options.singleton,
1588
+ container: true,
1589
+ // The container's exported registration bean (`export const <ProperCase>`).
1590
+ factory: normalized.nameProperCase
1591
+ });
1414
1592
  const kosConfig = getKosProjectConfiguration(
1415
1593
  codegenFs,
1416
- appProject.name,
1594
+ projectConfig.name,
1417
1595
  projects
1418
1596
  );
1419
- const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1420
- options.appDirectory = options.appDirectory || componentLocation;
1421
- const projectRoot = appProject.sourceRoot;
1597
+ const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1598
+ const internal = !!kosConfig?.generator?.internal;
1599
+ options.modelDirectory = options.modelDirectory || modelLocation;
1600
+ const projectRoot = projectConfig.sourceRoot;
1422
1601
  if (projectRoot) {
1423
- generateFilesFromTemplates(
1424
- codegenFs,
1425
- templateDir,
1426
- path__namespace.join(
1427
- projectRoot,
1428
- options.appDirectory,
1429
- "hooks",
1430
- normalized.nameDashCase
1431
- ),
1432
- normalized
1602
+ logger.info(
1603
+ `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1433
1604
  );
1434
- appendBarrelExport(
1435
- codegenFs,
1436
- path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1437
- `./${normalized.nameDashCase}`
1605
+ const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1606
+ const modelFolder = path__namespace.join(
1607
+ projectRoot,
1608
+ options.modelDirectory || "",
1609
+ modelNameDashCase
1610
+ );
1611
+ if (options.existingModel) {
1612
+ addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, {
1613
+ ...normalized,
1614
+ internal
1615
+ });
1616
+ } else {
1617
+ generateFilesFromTemplates(
1618
+ codegenFs,
1619
+ path__namespace.join(templateDir, "model"),
1620
+ modelFolder,
1621
+ { ...normalized, internal }
1622
+ );
1623
+ }
1624
+ const modelIndex = path__namespace.join(projectRoot, "index.ts");
1625
+ const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1626
+ updateModelIndex(codegenFs, modelIndex, modelPath);
1627
+ }
1628
+ }
1629
+ function addContainerToExistingModelFolder(codegenFs, templateDir, modelFolder, substitutions) {
1630
+ const containerFileName = `${substitutions.nameDashCase}-model.ts`;
1631
+ const containerTemplate = path__namespace.join(
1632
+ templateDir,
1633
+ "model",
1634
+ "__nameDashCase__-model.ts.template"
1635
+ );
1636
+ const rendered = ejs__namespace.render(
1637
+ fs__namespace.readFileSync(containerTemplate, "utf-8"),
1638
+ substitutions,
1639
+ { filename: containerTemplate }
1640
+ );
1641
+ codegenFs.write(path__namespace.join(modelFolder, containerFileName), rendered);
1642
+ const typesPath = path__namespace.join(modelFolder, "types", "index.d.ts");
1643
+ if (codegenFs.read(typesPath) === null) {
1644
+ codegenFs.write(typesPath, "");
1645
+ }
1646
+ transformSourceFile(codegenFs, typesPath, (sourceFile) => {
1647
+ ensureExportedInterface(
1648
+ sourceFile,
1649
+ `${substitutions.nameProperCase}Options`
1438
1650
  );
1651
+ });
1652
+ const barrelPath = path__namespace.join(modelFolder, "index.ts");
1653
+ if (codegenFs.read(barrelPath) === null) {
1654
+ codegenFs.write(barrelPath, "");
1439
1655
  }
1656
+ transformSourceFile(codegenFs, barrelPath, (sourceFile) => {
1657
+ const moduleSpecifier = `./${containerFileName.replace(/\.ts$/, "")}`;
1658
+ ensureNamedExports(sourceFile, moduleSpecifier, [
1659
+ substitutions.nameProperCase
1660
+ ]);
1661
+ ensureNamedExports(
1662
+ sourceFile,
1663
+ moduleSpecifier,
1664
+ [`${substitutions.nameProperCase}Model`],
1665
+ true
1666
+ );
1667
+ });
1440
1668
  }
1441
1669
  function generateContext(codegenFs, templateDir, options, cwd, projects) {
1442
1670
  if (!options.appProject) {
@@ -1490,8 +1718,10 @@ function generateContext(codegenFs, templateDir, options, cwd, projects) {
1490
1718
  );
1491
1719
  }
1492
1720
  }
1493
- function generateContainerModel(codegenFs, templateDir, options, cwd, projects) {
1494
- const logger = getCodegenLogger();
1721
+ function generateHook(codegenFs, templateDir, options, cwd, projects) {
1722
+ if (!options.appProject) {
1723
+ throw new Error("No app project specified");
1724
+ }
1495
1725
  const currentProject = getProject(codegenFs, cwd);
1496
1726
  const modelProjectName = options.modelProject || currentProject?.name;
1497
1727
  if (!modelProjectName) {
@@ -1499,106 +1729,56 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1499
1729
  "No model project found. Please specify a model project with --modelProject."
1500
1730
  );
1501
1731
  }
1502
- const modelName = options.modelName || getCurrentDirectoryName(cwd);
1732
+ const modelName = options.name || getCurrentDirectoryName(cwd);
1503
1733
  if (!modelName) {
1504
1734
  throw new Error(
1505
1735
  "No model name found. Please specify a model name with --name."
1506
1736
  );
1507
1737
  }
1508
- options.modelName = modelName;
1509
- options.name = `${modelName}-container`;
1738
+ const kosModelConfig = getKosModelConfiguration(
1739
+ codegenFs,
1740
+ modelProjectName,
1741
+ modelName,
1742
+ projects
1743
+ );
1744
+ options.singleton = kosModelConfig ? !!kosModelConfig.singleton : false;
1745
+ options.name = modelName;
1746
+ options.modelProject = modelProjectName;
1510
1747
  const normalized = normalizeOptions(codegenFs, options, projects);
1511
- const projectConfig = findProjectByName(
1748
+ const appProject = findProjectByName(
1512
1749
  codegenFs.root,
1513
- normalized.modelProject,
1750
+ normalized.appProject,
1514
1751
  projects
1515
1752
  );
1516
- if (!projectConfig) {
1517
- throw new Error(`Model project '${normalized.modelProject}' not found`);
1753
+ if (!appProject) {
1754
+ throw new Error(`App project '${normalized.appProject}' not found`);
1518
1755
  }
1519
- addKosModelConfiguration({
1520
- codegenFs,
1521
- modelName: normalized.nameDashCase,
1522
- projectName: projectConfig.name,
1523
- projectRoot: projectConfig.root,
1524
- singleton: !!options.singleton,
1525
- container: true,
1526
- // The container's exported registration bean (`export const <ProperCase>`).
1527
- factory: normalized.nameProperCase
1528
- });
1529
1756
  const kosConfig = getKosProjectConfiguration(
1530
1757
  codegenFs,
1531
- projectConfig.name,
1758
+ appProject.name,
1532
1759
  projects
1533
1760
  );
1534
- const modelLocation = kosConfig?.generator?.defaults?.model?.folder || "";
1535
- const internal = !!kosConfig?.generator?.internal;
1536
- options.modelDirectory = options.modelDirectory || modelLocation;
1537
- const projectRoot = projectConfig.sourceRoot;
1761
+ const componentLocation = kosConfig?.generator?.defaults?.component?.folder || "";
1762
+ options.appDirectory = options.appDirectory || componentLocation;
1763
+ const projectRoot = appProject.sourceRoot;
1538
1764
  if (projectRoot) {
1539
- logger.info(
1540
- `Generating container model ${normalized.nameDashCase} in ${projectRoot}`
1541
- );
1542
- const modelNameDashCase = normalized.modelNameDashCase || normalized.nameDashCase;
1543
1765
  generateFilesFromTemplates(
1544
1766
  codegenFs,
1545
- path__namespace.join(templateDir, "model"),
1546
- path__namespace.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1547
- { ...normalized, internal }
1767
+ templateDir,
1768
+ path__namespace.join(
1769
+ projectRoot,
1770
+ options.appDirectory,
1771
+ "hooks",
1772
+ normalized.nameDashCase
1773
+ ),
1774
+ normalized
1775
+ );
1776
+ appendBarrelExport(
1777
+ codegenFs,
1778
+ path__namespace.join(projectRoot, options.appDirectory, "hooks", "index.ts"),
1779
+ `./${normalized.nameDashCase}`
1548
1780
  );
1549
- const modelIndex = path__namespace.join(projectRoot, "index.ts");
1550
- const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1551
- updateModelIndex(codegenFs, modelIndex, modelPath);
1552
- }
1553
- }
1554
- function generateCompanionModel(codegenFs, templateDir, options, projects) {
1555
- const logger = getCodegenLogger();
1556
- const normalized = normalizeAllValues({
1557
- companionModelName: options.companionModelName,
1558
- modelName: options.modelName
1559
- });
1560
- const companionChildKosConfig = getKosProjectConfiguration(
1561
- codegenFs,
1562
- options.companionModelProject,
1563
- projects
1564
- );
1565
- const parentProject = findProjectByName(
1566
- codegenFs.root,
1567
- options.modelProject,
1568
- projects
1569
- );
1570
- const childProject = findProjectByName(
1571
- codegenFs.root,
1572
- options.companionModelProject,
1573
- projects
1574
- );
1575
- const projectRoot = childProject?.sourceRoot;
1576
- if (!projectRoot) {
1577
- logger.warn(`Companion child project source root not found`);
1578
- return;
1579
- }
1580
- let importPath = "";
1581
- if (parentProject) {
1582
- const pkgJsonPath = path__namespace.join(parentProject.root, "package.json");
1583
- try {
1584
- const pkgJson = readJson(codegenFs, pkgJsonPath);
1585
- importPath = pkgJson.name || "";
1586
- } catch {
1587
- importPath = "";
1588
- }
1589
1781
  }
1590
- const modelLocation = companionChildKosConfig?.generator?.defaults?.model?.folder || "";
1591
- const filePath = path__namespace.join(
1592
- projectRoot,
1593
- modelLocation,
1594
- normalized.companionModelNameDashCase
1595
- );
1596
- logger.info(`Generating companion model in ${filePath}`);
1597
- generateFilesFromTemplates(codegenFs, templateDir, filePath, {
1598
- ...options,
1599
- ...normalized,
1600
- importPath
1601
- });
1602
1782
  }
1603
1783
  function generateModel(params) {
1604
1784
  const {
@@ -1697,6 +1877,55 @@ function generateModel(params) {
1697
1877
  );
1698
1878
  }
1699
1879
  }
1880
+ const DECLARATION_MARKER = "@kosModel";
1881
+ function findModelDeclarationFile(codegenFs, searchRoot, typeIds) {
1882
+ const wanted = new Set(typeIds.filter(Boolean));
1883
+ if (wanted.size === 0) return null;
1884
+ const matches = [];
1885
+ for (const filePath of codegenFs.listFiles(searchRoot)) {
1886
+ if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts")) continue;
1887
+ const content = codegenFs.read(filePath);
1888
+ if (!content || !content.includes(DECLARATION_MARKER)) continue;
1889
+ const declared = readDeclaredModelType(codegenFs, filePath);
1890
+ if (declared && wanted.has(declared)) matches.push(filePath);
1891
+ }
1892
+ if (matches.length === 0) return null;
1893
+ if (matches.length > 1) {
1894
+ throw new Error(
1895
+ `Model type '${[...wanted].join("' / '")}' is declared in more than one file:
1896
+ ` + matches.sort().map((c) => ` - ${c}`).join("\n") + `
1897
+ Pass modelPath to pick one.`
1898
+ );
1899
+ }
1900
+ return matches[0];
1901
+ }
1902
+ function readDeclaredModelType(codegenFs, filePath) {
1903
+ try {
1904
+ return readSourceFile(codegenFs, filePath, (sf) => {
1905
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel"));
1906
+ const arg = cls?.getDecorator("kosModel")?.getArguments()[0];
1907
+ if (!arg) return void 0;
1908
+ const inner = arg.getKindName() === "ObjectLiteralExpression" ? modelTypeIdProperty(arg.getText()) : arg.getText();
1909
+ if (!inner) return void 0;
1910
+ return resolveToStringLiteral(inner.trim(), sf.getFullText());
1911
+ });
1912
+ } catch {
1913
+ return void 0;
1914
+ }
1915
+ }
1916
+ function modelTypeIdProperty(objectText) {
1917
+ const m = objectText.match(/\bmodelTypeId\s*:\s*([^,}]+)/);
1918
+ return m ? m[1] : void 0;
1919
+ }
1920
+ function resolveToStringLiteral(expression, fileText) {
1921
+ const literal = expression.match(/^["'`](.*)["'`]$/);
1922
+ if (literal) return literal[1];
1923
+ if (!/^[A-Za-z_$][\w$]*$/.test(expression)) return void 0;
1924
+ const declared = fileText.match(
1925
+ new RegExp(`\\b(?:const|let|var)\\s+${expression}\\s*(?::[^=]+)?=\\s*["'\`]([^"'\`]+)["'\`]`)
1926
+ );
1927
+ return declared ? declared[1] : void 0;
1928
+ }
1700
1929
  function resolveModelFilePath(codegenFs, query, projects) {
1701
1930
  const kosConfig = getKosProjectConfiguration(
1702
1931
  codegenFs,
@@ -1731,14 +1960,23 @@ function resolveModelFilePath(codegenFs, query, projects) {
1731
1960
  if (codegenFs.exists(modelFilePath)) {
1732
1961
  return { modelFilePath, internal, sourceRoot };
1733
1962
  }
1963
+ const searchRoot = path__namespace.join(sourceRoot, modelLocation);
1734
1964
  const discovered = findModelFileByName(
1735
1965
  codegenFs,
1736
- path__namespace.join(sourceRoot, modelLocation),
1966
+ searchRoot,
1737
1967
  modelNameDashCase
1738
1968
  );
1739
1969
  if (discovered) {
1740
1970
  return { modelFilePath: discovered, internal, sourceRoot };
1741
1971
  }
1972
+ const declaredType = kosConfig?.models?.[query.modelName]?.type;
1973
+ const byDeclaration = findModelDeclarationFile(codegenFs, searchRoot, [
1974
+ query.modelName,
1975
+ ...declaredType ? [declaredType] : []
1976
+ ]);
1977
+ if (byDeclaration) {
1978
+ return { modelFilePath: byDeclaration, internal, sourceRoot };
1979
+ }
1742
1980
  return { modelFilePath, internal, sourceRoot };
1743
1981
  }
1744
1982
  function findModelFileByName(codegenFs, searchRoot, modelNameDashCase) {
@@ -1754,6 +1992,150 @@ Pass modelPath to pick one.`
1754
1992
  }
1755
1993
  return candidates[0];
1756
1994
  }
1995
+ function resolveChildModelType(codegenFs, query, projects) {
1996
+ const childProject = query.childModelProject || query.modelProject;
1997
+ const childType = `${properCase(query.childModel)}Model`;
1998
+ if (childProject !== query.modelProject) {
1999
+ const project = findProjectByName(codegenFs.root, childProject, projects);
2000
+ const pkgJson = project ? readJson(
2001
+ codegenFs,
2002
+ path__namespace.join(project.root, "package.json")
2003
+ ) : void 0;
2004
+ return { childType, childTypeModule: pkgJson?.name };
2005
+ }
2006
+ const { modelFilePath: childFilePath } = resolveModelFilePath(
2007
+ codegenFs,
2008
+ { modelName: query.childModel, modelProject: childProject },
2009
+ projects
2010
+ );
2011
+ const relative = path__namespace.relative(path__namespace.dirname(query.modelFilePath), childFilePath).replace(/\.ts$/, "");
2012
+ return {
2013
+ childType,
2014
+ childTypeModule: relative.startsWith(".") ? relative : `./${relative}`
2015
+ };
2016
+ }
2017
+ function generateViewModel(codegenFs, templateDir, options, cwd, projects) {
2018
+ const logger = getCodegenLogger();
2019
+ if (!options.name) {
2020
+ throw new Error(
2021
+ "No ViewModel name found. Please specify a name with --name."
2022
+ );
2023
+ }
2024
+ const currentProject = getProject(codegenFs, cwd);
2025
+ const modelProjectName = options.modelProject || currentProject?.name;
2026
+ if (!modelProjectName) {
2027
+ throw new Error(
2028
+ "No model project found. Please specify a model project with --project."
2029
+ );
2030
+ }
2031
+ const normalized = normalizeOptions(
2032
+ codegenFs,
2033
+ { ...options, modelProject: modelProjectName },
2034
+ projects
2035
+ );
2036
+ const projectConfig = findProjectByName(
2037
+ codegenFs.root,
2038
+ modelProjectName,
2039
+ projects
2040
+ );
2041
+ if (!projectConfig) {
2042
+ throw new Error(`Model project '${modelProjectName}' not found`);
2043
+ }
2044
+ const kosConfig = getKosProjectConfiguration(
2045
+ codegenFs,
2046
+ projectConfig.name,
2047
+ projects
2048
+ );
2049
+ const modelDirectory = options.modelDirectory || kosConfig?.generator?.defaults?.model?.folder || "";
2050
+ const internal = !!kosConfig?.generator?.internal;
2051
+ const projectRoot = projectConfig.sourceRoot || path__namespace.join(projectConfig.root, "src");
2052
+ const viewModelFolder = path__namespace.join(
2053
+ projectRoot,
2054
+ modelDirectory,
2055
+ normalized.nameDashCase
2056
+ );
2057
+ const viewModelFilePath = path__namespace.join(
2058
+ viewModelFolder,
2059
+ `${normalized.nameDashCase}-view-model.ts`
2060
+ );
2061
+ if (codegenFs.exists(viewModelFilePath)) {
2062
+ logger.info(`ViewModel already exists: ${viewModelFilePath}`);
2063
+ return { viewModelFilePath, created: false };
2064
+ }
2065
+ const resolved = (options.models || []).map(
2066
+ (source) => resolveSourceModel(
2067
+ codegenFs,
2068
+ source,
2069
+ viewModelFilePath,
2070
+ modelProjectName,
2071
+ projects
2072
+ )
2073
+ );
2074
+ logger.info(
2075
+ `Generating ViewModel ${normalized.nameDashCase} in ${projectRoot}`
2076
+ );
2077
+ const constructorParams = resolved.map(({ name, type }) => ({ name, type }));
2078
+ generateFilesFromTemplates(codegenFs, templateDir, viewModelFolder, {
2079
+ ...normalized,
2080
+ internal,
2081
+ typeId: options.typeId || normalized.nameDashCase,
2082
+ devToolsEnabled: !!options.devToolsEnabled,
2083
+ constructorParams,
2084
+ constructorArgs: constructorParams.map((param) => param.name).join(", "),
2085
+ modelImports: groupImports(resolved)
2086
+ });
2087
+ updateModelIndex(
2088
+ codegenFs,
2089
+ path__namespace.join(projectRoot, "index.ts"),
2090
+ modelDirectory ? `${modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase
2091
+ );
2092
+ return { viewModelFilePath, created: true };
2093
+ }
2094
+ function resolveSourceModel(codegenFs, source, viewModelFilePath, modelProject, projects) {
2095
+ const sourceProject = source.project || modelProject;
2096
+ if (sourceProject === modelProject) {
2097
+ const { modelFilePath } = resolveModelFilePath(
2098
+ codegenFs,
2099
+ { modelName: source.model, modelProject: sourceProject },
2100
+ projects
2101
+ );
2102
+ if (!codegenFs.exists(modelFilePath)) {
2103
+ throw new Error(
2104
+ `Model '${source.model}' not found in project '${sourceProject}' (looked for ${modelFilePath})`
2105
+ );
2106
+ }
2107
+ } else if (!findProjectByName(codegenFs.root, sourceProject, projects)) {
2108
+ throw new Error(`Model project '${sourceProject}' not found`);
2109
+ }
2110
+ const { childType, childTypeModule } = resolveChildModelType(
2111
+ codegenFs,
2112
+ {
2113
+ modelFilePath: viewModelFilePath,
2114
+ modelProject,
2115
+ childModel: source.model,
2116
+ childModelProject: sourceProject
2117
+ },
2118
+ projects
2119
+ );
2120
+ return {
2121
+ name: camelCase(source.model),
2122
+ type: childType,
2123
+ module: childTypeModule
2124
+ };
2125
+ }
2126
+ function groupImports(resolved) {
2127
+ const byModule = /* @__PURE__ */ new Map();
2128
+ for (const { type, module: module2 } of resolved) {
2129
+ if (!module2) continue;
2130
+ const types = byModule.get(module2);
2131
+ if (!types) {
2132
+ byModule.set(module2, [type]);
2133
+ } else if (!types.includes(type)) {
2134
+ types.push(type);
2135
+ }
2136
+ }
2137
+ return [...byModule].map(([module2, types]) => ({ module: module2, types }));
2138
+ }
1757
2139
  function modelBaseName(modelFilePath, modelName) {
1758
2140
  const base = path__namespace.basename(modelFilePath);
1759
2141
  const match = base.match(/^(.*)-model\.ts$/);
@@ -1766,7 +2148,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
1766
2148
  `${modelBase}-services.ts`
1767
2149
  );
1768
2150
  }
1769
- function header(modelBase) {
2151
+ function header$1(modelBase) {
1770
2152
  return `/**
1771
2153
  * Service layer for the ${modelBase} model: the endpoints it calls and the
1772
2154
  * types derived from them. Standalone service functions for callers outside a
@@ -1777,7 +2159,7 @@ function header(modelBase) {
1777
2159
  function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1778
2160
  const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1779
2161
  if (!codegenFs.exists(servicesFilePath)) {
1780
- codegenFs.write(servicesFilePath, header(modelBase));
2162
+ codegenFs.write(servicesFilePath, header$1(modelBase));
1781
2163
  }
1782
2164
  const barrelPath = path__namespace.join(path__namespace.dirname(servicesFilePath), "index.ts");
1783
2165
  if (!codegenFs.exists(barrelPath)) {
@@ -2262,11 +2644,25 @@ function buildDecoratorArgs(options) {
2262
2644
  }
2263
2645
  function addContainerSupportToModel(codegenFs, options, projects) {
2264
2646
  const logger = getCodegenLogger();
2265
- const childType = options.childType?.trim() || "IKosDataModel";
2266
2647
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2267
2648
  if (!codegenFs.exists(modelFilePath)) {
2268
2649
  throw new Error(`Model file not found: ${modelFilePath}`);
2269
2650
  }
2651
+ const resolvedChild = options.childModel ? resolveChildModelType(
2652
+ codegenFs,
2653
+ {
2654
+ modelFilePath,
2655
+ modelProject: options.modelProject,
2656
+ childModel: options.childModel,
2657
+ childModelProject: options.childModelProject
2658
+ },
2659
+ projects
2660
+ ) : {
2661
+ childType: options.childType,
2662
+ childTypeModule: options.childTypeModule
2663
+ };
2664
+ const childType = resolvedChild.childType?.trim() || "IKosDataModel";
2665
+ const childTypeModule = resolvedChild.childTypeModule;
2270
2666
  logger.info(
2271
2667
  `Adding container support (<${childType}>) to model: ${options.modelName}`
2272
2668
  );
@@ -2278,6 +2674,10 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2278
2674
  ]);
2279
2675
  if (childType === "IKosDataModel") {
2280
2676
  ensureNamedImport(sf, sdk, [{ name: "IKosDataModel", isTypeOnly: true }]);
2677
+ } else if (childTypeModule) {
2678
+ ensureNamedImport(sf, childTypeModule, [
2679
+ { name: childType, isTypeOnly: true }
2680
+ ]);
2281
2681
  }
2282
2682
  const cls = getModelClass(sf);
2283
2683
  const className = cls.getName();
@@ -2307,39 +2707,173 @@ function addContainerSupportToModel(codegenFs, options, projects) {
2307
2707
  });
2308
2708
  return { modelFilePath };
2309
2709
  }
2310
- function addModelEffectToModel(codegenFs, options, projects) {
2710
+ function addParentAwareToModel(codegenFs, options, projects) {
2311
2711
  const logger = getCodegenLogger();
2312
2712
  const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2313
2713
  if (!codegenFs.exists(modelFilePath)) {
2314
2714
  throw new Error(`Model file not found: ${modelFilePath}`);
2315
2715
  }
2316
- logger.info(
2317
- `Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
2318
- );
2716
+ logger.info(`Adding parent awareness to model: ${options.modelName}`);
2717
+ let optionsTypeName;
2319
2718
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2320
2719
  const sdk = resolveSdkModuleSpecifier(sf);
2321
- ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
2720
+ ensureNamedImport(sf, sdk, [{ name: "kosParentAware" }]);
2322
2721
  const cls = getModelClass(sf);
2323
- const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
2324
- addDecoratedMethod(cls, {
2325
- name: options.methodName,
2326
- decoratorName: "kosModelEffect",
2327
- decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
2328
- isAsync: true,
2329
- returnType: "Promise<void>",
2330
- statements: "// TODO: react to the tracked dependencies"
2722
+ addClassDecorator(cls, "kosParentAware", {
2723
+ argsText: options.parentId ? `{ parentId: ${JSON.stringify(options.parentId)} }` : ""
2331
2724
  });
2725
+ const ctor = cls.getConstructors()[0];
2726
+ optionsTypeName = ctor?.getParameters()[1]?.getTypeNode()?.getText()?.replace(/<.*$/, "");
2332
2727
  });
2333
- return { modelFilePath };
2728
+ const optionsFilePath = optionsTypeName ? extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) : void 0;
2729
+ return { modelFilePath, optionsFilePath };
2334
2730
  }
2335
- const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
2336
- function addDependencyToModel(codegenFs, options, projects) {
2337
- const logger = getCodegenLogger();
2338
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2339
- if (!codegenFs.exists(modelFilePath)) {
2340
- throw new Error(`Model file not found: ${modelFilePath}`);
2341
- }
2342
- logger.info(
2731
+ function extendOptionsInterface(codegenFs, modelFilePath, optionsTypeName, logger) {
2732
+ const typesPath = path__namespace.join(
2733
+ path__namespace.dirname(modelFilePath),
2734
+ "types",
2735
+ "index.d.ts"
2736
+ );
2737
+ const target = codegenFs.exists(typesPath) ? typesPath : modelFilePath;
2738
+ let extended = false;
2739
+ transformSourceFile(codegenFs, target, (sf) => {
2740
+ const iface = sf.getInterface(optionsTypeName);
2741
+ if (!iface) return;
2742
+ const already = iface.getExtends().some((clause) => clause.getText().includes("KosParentAware"));
2743
+ if (already) {
2744
+ extended = true;
2745
+ return;
2746
+ }
2747
+ ensureNamedImport(sf, "@kosdev-code/kos-ui-sdk", [
2748
+ { name: "KosParentAware", isTypeOnly: true }
2749
+ ]);
2750
+ iface.addExtends("KosParentAware");
2751
+ extended = true;
2752
+ });
2753
+ if (!extended) {
2754
+ logger.warn(
2755
+ `Could not find interface ${optionsTypeName}; add "extends KosParentAware" to it by hand.`
2756
+ );
2757
+ return void 0;
2758
+ }
2759
+ return target;
2760
+ }
2761
+ function firstDecoratorArg(decoratorText) {
2762
+ const open = decoratorText.indexOf("(");
2763
+ if (open === -1) return void 0;
2764
+ const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
2765
+ return inner || void 0;
2766
+ }
2767
+ function collectDecorated(cls, decoratorName) {
2768
+ const members = [];
2769
+ const visit = (name, decoratorTextOf, typeText) => {
2770
+ const text = decoratorTextOf();
2771
+ if (text === void 0) return;
2772
+ members.push({
2773
+ name,
2774
+ arg: firstDecoratorArg(text),
2775
+ type: typeText || void 0
2776
+ });
2777
+ };
2778
+ for (const m of cls.getMethods()) {
2779
+ const dec = m.getDecorator(decoratorName);
2780
+ if (dec) {
2781
+ visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
2782
+ }
2783
+ }
2784
+ for (const p of cls.getProperties()) {
2785
+ const dec = p.getDecorator(decoratorName);
2786
+ if (dec) {
2787
+ visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
2788
+ }
2789
+ }
2790
+ return members;
2791
+ }
2792
+ function describeModel(codegenFs, options, projects) {
2793
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2794
+ const content = codegenFs.read(modelFilePath);
2795
+ if (content === null) {
2796
+ throw new Error(`Model file not found: ${modelFilePath}`);
2797
+ }
2798
+ const project = new tsMorph.Project({ useInMemoryFileSystem: true });
2799
+ const sf = project.createSourceFile(modelFilePath, content, {
2800
+ overwrite: true
2801
+ });
2802
+ let modelType;
2803
+ const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
2804
+ if (modelTypeDecl) {
2805
+ const init = modelTypeDecl.getInitializer()?.getText();
2806
+ if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
2807
+ }
2808
+ const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
2809
+ if (!cls) {
2810
+ return {
2811
+ modelFilePath,
2812
+ modelType,
2813
+ classDecorators: [],
2814
+ singleton: false,
2815
+ isCompanion: false,
2816
+ children: [],
2817
+ dependencies: [],
2818
+ topicHandlers: [],
2819
+ configProperties: [],
2820
+ serviceRequests: [],
2821
+ effects: [],
2822
+ futures: []
2823
+ };
2824
+ }
2825
+ const classDecorators = cls.getDecorators().map((d) => d.getName());
2826
+ const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
2827
+ const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
2828
+ return {
2829
+ modelFilePath,
2830
+ modelType,
2831
+ className: cls.getName(),
2832
+ classDecorators,
2833
+ singleton,
2834
+ isCompanion: classDecorators.includes("kosCompanion"),
2835
+ children: collectDecorated(cls, "kosChild"),
2836
+ dependencies: collectDecorated(cls, "kosDependency"),
2837
+ topicHandlers: collectDecorated(cls, "kosTopicHandler"),
2838
+ configProperties: collectDecorated(cls, "kosConfigProperty"),
2839
+ serviceRequests: collectDecorated(cls, "kosServiceRequest"),
2840
+ effects: collectDecorated(cls, "kosModelEffect"),
2841
+ futures: collectDecorated(cls, "kosFuture")
2842
+ };
2843
+ }
2844
+ function addModelEffectToModel(codegenFs, options, projects) {
2845
+ const logger = getCodegenLogger();
2846
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2847
+ if (!codegenFs.exists(modelFilePath)) {
2848
+ throw new Error(`Model file not found: ${modelFilePath}`);
2849
+ }
2850
+ logger.info(
2851
+ `Adding @kosModelEffect "${options.methodName}" to ${options.modelName}`
2852
+ );
2853
+ transformSourceFile(codegenFs, modelFilePath, (sf) => {
2854
+ const sdk = resolveSdkModuleSpecifier(sf);
2855
+ ensureNamedImport(sf, sdk, [{ name: "kosModelEffect" }]);
2856
+ const cls = getModelClass(sf);
2857
+ const modelType = (cls.getName() ?? "").replace(/Impl$/, "");
2858
+ addDecoratedMethod(cls, {
2859
+ name: options.methodName,
2860
+ decoratorName: "kosModelEffect",
2861
+ decoratorArgsText: `{ dependencies: (model: ${modelType}) => [] }`,
2862
+ isAsync: true,
2863
+ returnType: "Promise<void>",
2864
+ statements: "// TODO: react to the tracked dependencies"
2865
+ });
2866
+ });
2867
+ return { modelFilePath };
2868
+ }
2869
+ const FRAMEWORK_TYPES$1 = /* @__PURE__ */ new Set(["IKosDataModel", "IKosIdentifiable"]);
2870
+ function addDependencyToModel(codegenFs, options, projects) {
2871
+ const logger = getCodegenLogger();
2872
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2873
+ if (!codegenFs.exists(modelFilePath)) {
2874
+ throw new Error(`Model file not found: ${modelFilePath}`);
2875
+ }
2876
+ logger.info(
2343
2877
  `Adding @kosDependency "${options.propertyName}" to ${options.modelName}`
2344
2878
  );
2345
2879
  const argsParts = [`modelType: ${options.modelTypeRef}`];
@@ -2382,7 +2916,18 @@ function addChildToModel(codegenFs, options, projects) {
2382
2916
  logger.info(
2383
2917
  `Adding @kosChild "${options.propertyName}" (${shape}) to ${options.modelName}`
2384
2918
  );
2385
- const childType = options.childType?.trim();
2919
+ const resolvedChild = options.childModel ? resolveChildModelType(
2920
+ codegenFs,
2921
+ {
2922
+ modelFilePath,
2923
+ modelProject: options.modelProject,
2924
+ childModel: options.childModel,
2925
+ childModelProject: options.childModelProject
2926
+ },
2927
+ projects
2928
+ ) : { childType: options.childType, childTypeModule: options.childPackage };
2929
+ const childType = resolvedChild.childType?.trim();
2930
+ const childTypeModule = resolvedChild.childTypeModule;
2386
2931
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2387
2932
  const sdk = resolveSdkModuleSpecifier(sf);
2388
2933
  const sdkImports = [
@@ -2390,8 +2935,8 @@ function addChildToModel(codegenFs, options, projects) {
2390
2935
  ];
2391
2936
  if (shape === "container") sdkImports.push({ name: "KosModelContainer" });
2392
2937
  ensureNamedImport(sf, sdk, sdkImports);
2393
- if (options.childPackage && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2394
- ensureNamedImport(sf, options.childPackage, [
2938
+ if (childTypeModule && childType && /^[A-Za-z_$][\w$]*$/.test(childType) && !FRAMEWORK_TYPES.has(childType)) {
2939
+ ensureNamedImport(sf, childTypeModule, [
2395
2940
  { name: childType, isTypeOnly: true }
2396
2941
  ]);
2397
2942
  }
@@ -2537,6 +3082,154 @@ function addComputedToModel(codegenFs, options, projects) {
2537
3082
  });
2538
3083
  return { modelFilePath };
2539
3084
  }
3085
+ function mocksFilePathFor(modelFilePath, modelBase) {
3086
+ return path__namespace.join(
3087
+ path__namespace.dirname(modelFilePath),
3088
+ "mocks",
3089
+ `${modelBase}-mocks.ts`
3090
+ );
3091
+ }
3092
+ function mockRegisterFunctionName(modelBase) {
3093
+ return `register${pascalCase(modelBase)}Mocks`;
3094
+ }
3095
+ function toMockRoutePattern(servicePath) {
3096
+ return servicePath.replace(/\{([^}]+)\}/g, ":$1");
3097
+ }
3098
+ function mockRouteCall(method, pattern, body) {
3099
+ const shorthand = {
3100
+ get: "get",
3101
+ post: "post",
3102
+ put: "put",
3103
+ delete: "del"
3104
+ };
3105
+ const fn = shorthand[method];
3106
+ return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
3107
+ pattern
3108
+ )}, ${body});`;
3109
+ }
3110
+ function header(modelBase, registerFn) {
3111
+ return `/**
3112
+ * KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
3113
+ * this project's generated OpenAPI types do not declare.
3114
+ *
3115
+ * NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
3116
+ * gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
3117
+ * dev-only entry module, or behind your own switch). Registering a route enables
3118
+ * KosMock, and a mock that shipped would shadow the real endpoint once it exists.
3119
+ *
3120
+ * Unmatched requests still pass through to the real device (KosMock's hybrid
3121
+ * default), so these routes shadow only the paths named below. Every mocked
3122
+ * response carries the \`kos-mocked: true\` wire header, so mock-fed data is
3123
+ * identifiable in devtools.
3124
+ *
3125
+ * DELETE THIS FILE once every endpoint below is in the generated types.
3126
+ */
3127
+ `;
3128
+ }
3129
+ function ensureMockRoute(codegenFs, options) {
3130
+ const mocksFilePath = mocksFilePathFor(
3131
+ options.modelFilePath,
3132
+ options.modelBase
3133
+ );
3134
+ const registerFunction = mockRegisterFunctionName(options.modelBase);
3135
+ const routePattern = toMockRoutePattern(options.servicePath);
3136
+ const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
3137
+ const samplePlaceholder = !options.sampleText;
3138
+ if (!codegenFs.exists(mocksFilePath)) {
3139
+ codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
3140
+ }
3141
+ const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
3142
+ let routeCreated = false;
3143
+ transformSourceFile(codegenFs, mocksFilePath, (sf) => {
3144
+ ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
3145
+ ensureNamedImport(sf, "../services", [
3146
+ { name: options.rawAlias, isTypeOnly: true }
3147
+ ]);
3148
+ ensureSample(sf, {
3149
+ name: sampleName,
3150
+ type: options.rawAlias,
3151
+ servicePath: options.servicePath,
3152
+ method: options.method,
3153
+ sampleText: options.sampleText
3154
+ });
3155
+ const statement = mockRouteCall(
3156
+ options.method,
3157
+ routePattern,
3158
+ `{ data: ${sampleName} }`
3159
+ );
3160
+ routeCreated = ensureRegisteredRoute(sf, {
3161
+ registerFunction,
3162
+ modelBase: options.modelBase,
3163
+ statement,
3164
+ // Matching on the pattern literal alone would collide across methods.
3165
+ marker: `${options.method.toUpperCase()} ${routePattern}`,
3166
+ matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
3167
+ });
3168
+ });
3169
+ return {
3170
+ mocksFilePath,
3171
+ registerFunction,
3172
+ routePattern,
3173
+ sampleName,
3174
+ routeCreated,
3175
+ samplePlaceholder
3176
+ };
3177
+ }
3178
+ function shorthandOrMethod(method) {
3179
+ const shorthand = {
3180
+ get: "KosMock.get(",
3181
+ post: "KosMock.post(",
3182
+ put: "KosMock.put(",
3183
+ delete: "KosMock.del("
3184
+ };
3185
+ return shorthand[method] ?? `"${method.toUpperCase()}"`;
3186
+ }
3187
+ function ensureSample(sf, spec) {
3188
+ if (sf.getVariableDeclaration(spec.name)) return;
3189
+ const lines = [
3190
+ `Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
3191
+ "UNWRAPPED `data` payload, i.e. exactly what the transform receives."
3192
+ ];
3193
+ if (!spec.sampleText) {
3194
+ lines.push(
3195
+ "",
3196
+ "TODO: replace the placeholder with something the backend would actually",
3197
+ "serve. Until then the route answers with nothing and the transform is",
3198
+ "never exercised."
3199
+ );
3200
+ }
3201
+ const docs = lines.join("\n");
3202
+ sf.addVariableStatement({
3203
+ isExported: true,
3204
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
3205
+ docs: [docs],
3206
+ declarations: [
3207
+ {
3208
+ name: spec.name,
3209
+ type: spec.type,
3210
+ initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
3211
+ }
3212
+ ]
3213
+ });
3214
+ }
3215
+ function ensureRegisteredRoute(sf, spec) {
3216
+ let fn = sf.getFunction(spec.registerFunction);
3217
+ if (!fn) {
3218
+ fn = sf.addFunction({
3219
+ name: spec.registerFunction,
3220
+ isExported: true,
3221
+ returnType: "void",
3222
+ docs: [
3223
+ `Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
3224
+ ]
3225
+ });
3226
+ }
3227
+ const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
3228
+ if (already) return false;
3229
+ fn.addStatements(`// ${spec.marker}
3230
+ ${spec.statement}`);
3231
+ return true;
3232
+ }
2540
3233
  const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
2541
3234
  function findServiceModules(codegenFs, sourceRoot) {
2542
3235
  return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path__namespace.sep).join("/")));
@@ -2561,10 +3254,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
2561
3254
  `${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
2562
3255
  );
2563
3256
  }
3257
+ function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
3258
+ const file = `${serviceModuleFile}.ts`;
3259
+ if (!codegenFs.exists(file)) return;
3260
+ const declared = readSourceFile(
3261
+ codegenFs,
3262
+ file,
3263
+ (sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
3264
+ );
3265
+ if (declared) return;
3266
+ throw new Error(
3267
+ `${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
3268
+ );
3269
+ }
2564
3270
  function sameEndpoint(existing, candidate) {
2565
3271
  const args = (text) => {
2566
3272
  const match = text.match(
2567
- /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
3273
+ /^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2568
3274
  );
2569
3275
  return match ? [match[2], match[4].toLowerCase()] : null;
2570
3276
  };
@@ -2572,6 +3278,104 @@ function sameEndpoint(existing, candidate) {
2572
3278
  const b = args(candidate.trim());
2573
3279
  return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2574
3280
  }
3281
+ function editDistance(a, b) {
3282
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
3283
+ for (let i = 1; i <= a.length; i++) {
3284
+ const current = [i];
3285
+ for (let j = 1; j <= b.length; j++) {
3286
+ current[j] = Math.min(
3287
+ previous[j] + 1,
3288
+ current[j - 1] + 1,
3289
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
3290
+ );
3291
+ }
3292
+ previous = current;
3293
+ }
3294
+ return previous[b.length];
3295
+ }
3296
+ const NEAREST_SHOWN = 5;
3297
+ function provisionalEntryComment(servicePath, method, catalogName) {
3298
+ return `/**
3299
+ * PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
3300
+ * generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
3301
+ * does not exist, so nothing about this endpoint is checked against a spec and
3302
+ * its response cannot be derived from one. Do not read this as a real endpoint.
3303
+ *
3304
+ * WHEN THE ENDPOINT LANDS:
3305
+ * 1. regenerate this project's API types (\`kosui api:generate\`)
3306
+ * 2. delete \` as ApiPath\` from the entry below
3307
+ * 3. point the endpoint's \`…Raw\` alias at
3308
+ * \`EndpointResponse<typeof ${catalogName}.…>\`
3309
+ * 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
3310
+ * 5. delete the model's \`mocks/\` module
3311
+ * 6. KEEP the transform — it is real code and survives all of the above
3312
+ */
3313
+ `;
3314
+ }
3315
+ function provisionalMethodDocs(servicePath, method) {
3316
+ return [
3317
+ `PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
3318
+ "generated OpenAPI types, so this request is served by a KosMock route, not",
3319
+ "by the device. The feature it backs is NOT complete: report the missing",
3320
+ "endpoint rather than treating this as finished.",
3321
+ "",
3322
+ "See the endpoint's entry in `./services` for what to undo when it lands."
3323
+ ].join("\n");
3324
+ }
3325
+ function rawTypeDocs(servicePath, method, rawType) {
3326
+ const shape = rawType ? [
3327
+ "Stated by hand and unverified: there is no spec entry to check it",
3328
+ "against."
3329
+ ] : [
3330
+ "TODO: state the shape the backend is expected to serve. `unknown`",
3331
+ "compiles, but leaves the response boundary undescribed and the",
3332
+ "transform with nothing to narrow."
3333
+ ];
3334
+ return [
3335
+ `Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
3336
+ "`data` payload, since the client strips the `{status, data}` envelope",
3337
+ "before the transform runs.",
3338
+ "",
3339
+ ...shape,
3340
+ "",
3341
+ "Replace with `EndpointResponse<…>` once the endpoint is in the generated",
3342
+ "types."
3343
+ ].join("\n");
3344
+ }
3345
+ function removalSteps(spec) {
3346
+ return [
3347
+ `Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
3348
+ `In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
3349
+ `In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
3350
+ `In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
3351
+ spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
3352
+ `KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
3353
+ ];
3354
+ }
3355
+ function untypedEndpointError(spec) {
3356
+ const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
3357
+ const pathTypedForOtherMethods = methodsForPath.length > 0;
3358
+ const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
3359
+ (a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
3360
+ ).slice(0, NEAREST_SHOWN);
3361
+ const waysForward = [
3362
+ `The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
3363
+ `The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
3364
+ `What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
3365
+ ];
3366
+ const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
3367
+ const error = new Error(message);
3368
+ error.details = {
3369
+ servicePath: spec.servicePath,
3370
+ method: spec.method,
3371
+ pathTypedForOtherMethods,
3372
+ methodsForPath,
3373
+ otherEndpointsInThisApi: nearest,
3374
+ warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
3375
+ waysForward
3376
+ };
3377
+ return error;
3378
+ }
2575
3379
  function addServiceRequestToModel(codegenFs, options, projects) {
2576
3380
  const logger = getCodegenLogger();
2577
3381
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2633,34 +3437,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2633
3437
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2634
3438
  const lifecycle = options.lifecycle || "LOAD";
2635
3439
  const catalogName = `${pascalCase(modelBase)}Endpoints`;
3440
+ const mockMode = options.mock ?? "auto";
3441
+ const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
3442
+ const pathValidated = operations !== null;
3443
+ const operationTyped = operations === null || operations.some(
3444
+ (op) => op.path === options.servicePath && op.method === method
3445
+ );
3446
+ if (!operationTyped && mockMode === "never") {
3447
+ throw untypedEndpointError({
3448
+ servicePath: options.servicePath,
3449
+ method,
3450
+ modelProject: options.modelProject,
3451
+ operations: operations ?? []
3452
+ });
3453
+ }
3454
+ const provisional = !operationTyped;
3455
+ const writeMock = provisional || mockMode === "always";
3456
+ if (provisional) {
3457
+ assertServiceModuleAcceptsProvisional(
3458
+ codegenFs,
3459
+ serviceModuleFile,
3460
+ options.modelProject
3461
+ );
3462
+ }
2636
3463
  logger.info(
2637
- `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
3464
+ `Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2638
3465
  );
3466
+ if (provisional) {
3467
+ logger.warn(
3468
+ `${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
3469
+ );
3470
+ }
2639
3471
  ensureServicesModule(codegenFs, modelFilePath, modelBase);
2640
3472
  let endpointKey = options.methodName;
2641
3473
  let endpointCreated = true;
2642
3474
  let dataAlias = "";
2643
3475
  let mapperName = "";
2644
3476
  let ctxAlias = "";
3477
+ let rawAlias = "";
2645
3478
  transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2646
3479
  ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
3480
+ if (provisional) {
3481
+ ensureNamedImport(sf, serviceImportFromServices, [
3482
+ { name: "ApiPath", isTypeOnly: true }
3483
+ ]);
3484
+ }
3485
+ const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
2647
3486
  const entry = ensureConstCatalogEntry(sf, {
2648
3487
  catalogName,
2649
3488
  key: options.methodName,
2650
- initializer: `endpoint(${JSON.stringify(
2651
- options.servicePath
2652
- )}, ${JSON.stringify(method)})`,
2653
- equals: sameEndpoint
3489
+ initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
3490
+ equals: sameEndpoint,
3491
+ leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
2654
3492
  });
2655
3493
  endpointKey = entry.key;
2656
3494
  endpointCreated = entry.created;
2657
3495
  dataAlias = `${pascalCase(endpointKey)}Data`;
2658
3496
  mapperName = `to${pascalCase(endpointKey)}Data`;
2659
3497
  ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2660
- const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2661
- ensureNamedImport(sf, serviceImportFromServices, [
2662
- { name: "EndpointResponse", isTypeOnly: true }
2663
- ]);
3498
+ rawAlias = `${pascalCase(endpointKey)}Raw`;
3499
+ let rawType;
3500
+ if (provisional) {
3501
+ ensureExportedTypeAlias(sf, {
3502
+ name: rawAlias,
3503
+ type: options.rawType || "unknown",
3504
+ docs: rawTypeDocs(options.servicePath, method, options.rawType)
3505
+ });
3506
+ rawType = rawAlias;
3507
+ } else {
3508
+ rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
3509
+ ensureNamedImport(sf, serviceImportFromServices, [
3510
+ { name: "EndpointResponse", isTypeOnly: true }
3511
+ ]);
3512
+ if (writeMock) {
3513
+ ensureExportedTypeAlias(sf, {
3514
+ name: rawAlias,
3515
+ type: rawType,
3516
+ docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
3517
+ });
3518
+ }
3519
+ }
2664
3520
  ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2665
3521
  ensureExportedMapper(sf, {
2666
3522
  name: mapperName,
@@ -2681,20 +3537,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2681
3537
  const className = getModelClass(sf).getName();
2682
3538
  if (!className) throw new Error("Model class has no name.");
2683
3539
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2684
- ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
3540
+ const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
3541
+ const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
3542
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
2685
3543
  if (mode === "lifecycle") {
2686
3544
  ensureNamedImport(sf, "./services", [
2687
3545
  { name: catalogName },
2688
3546
  { name: mapperName },
2689
- { name: dataAlias, isTypeOnly: true }
3547
+ { name: dataAlias, isTypeOnly: true },
3548
+ ...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
2690
3549
  ]);
2691
3550
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2692
3551
  { name: "DependencyLifecycle" }
2693
3552
  ]);
2694
3553
  addDecoratedMethod(getModelClass(sf), {
2695
3554
  name: options.methodName,
2696
- decoratorName: "serviceRequest",
3555
+ decoratorName,
3556
+ decoratorTypeArgs,
2697
3557
  decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
3558
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2698
3559
  // The manager invokes phase handlers error-first, passing (null, data)
2699
3560
  // on success. The `error` half is only ever reached because
2700
3561
  // `serviceRequest` supplies an errorHandler — the bare decorator
@@ -2711,7 +3572,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2711
3572
  ensureNamedImport(sf, "./services", [
2712
3573
  { name: catalogName },
2713
3574
  { name: mapperName },
2714
- { name: ctxAlias, isTypeOnly: true }
3575
+ { name: ctxAlias, isTypeOnly: true },
3576
+ ...provisional ? [
3577
+ { name: rawAlias, isTypeOnly: true },
3578
+ { name: dataAlias, isTypeOnly: true }
3579
+ ] : []
2715
3580
  ]);
2716
3581
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2717
3582
  { name: "executeServiceRequest" },
@@ -2726,8 +3591,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2726
3591
  addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
2727
3592
  addDecoratedMethod(getModelClass(sf), {
2728
3593
  name: options.methodName,
2729
- decoratorName: "serviceRequest",
3594
+ decoratorName,
3595
+ decoratorTypeArgs,
2730
3596
  decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
3597
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2731
3598
  // Optional: the framework appends the context, callers never pass it.
2732
3599
  parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2733
3600
  isAsync: true,
@@ -2739,172 +3606,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2739
3606
  ].join("\n")
2740
3607
  });
2741
3608
  });
3609
+ const mock = writeMock ? ensureMockRoute(codegenFs, {
3610
+ modelFilePath,
3611
+ modelBase,
3612
+ sdkModuleSpecifier: readSourceFile(
3613
+ codegenFs,
3614
+ modelFilePath,
3615
+ resolveSdkModuleSpecifier
3616
+ ),
3617
+ rawAlias,
3618
+ servicePath: options.servicePath,
3619
+ method,
3620
+ endpointKey,
3621
+ sampleText: options.sample
3622
+ }) : void 0;
2742
3623
  return {
2743
3624
  modelFilePath,
2744
3625
  servicesFilePath,
2745
3626
  serviceModule: serviceImportFromModel,
2746
3627
  mode,
2747
3628
  endpointKey,
2748
- endpointCreated
2749
- };
2750
- }
2751
- function modelElementType(typeText) {
2752
- let m;
2753
- if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
2754
- if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
2755
- if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
2756
- return m[1];
2757
- return null;
2758
- }
2759
- function validateModel(codegenFs, options, projects) {
2760
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2761
- const content = codegenFs.read(modelFilePath);
2762
- if (content === null) {
2763
- throw new Error(`Model file not found: ${modelFilePath}`);
2764
- }
2765
- const project = new tsMorph.Project({ useInMemoryFileSystem: true });
2766
- const sf = project.createSourceFile(modelFilePath, content, {
2767
- overwrite: true
2768
- });
2769
- const findings = [];
2770
- const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
2771
- if (!hasKosModel) {
2772
- findings.push({
2773
- level: "error",
2774
- rule: "missing-kosModel",
2775
- message: "No @kosModel decorator found — this is not a KOS model."
2776
- });
2777
- }
2778
- const imports = sf.getImportDeclarations();
2779
- const mobxImport = imports.find(
2780
- (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
2781
- );
2782
- if (mobxImport) {
2783
- findings.push({
2784
- level: "error",
2785
- rule: "mobx-import",
2786
- message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
2787
- });
2788
- }
2789
- const barrelServiceRequest = imports.find(
2790
- (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
2791
- );
2792
- if (barrelServiceRequest) {
2793
- findings.push({
2794
- level: "warning",
2795
- rule: "untyped-service-request",
2796
- message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
2797
- });
2798
- }
2799
- for (const cls of sf.getClasses()) {
2800
- for (const prop of cls.getProperties()) {
2801
- const name = prop.getName();
2802
- const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
2803
- const initText = prop.getInitializer()?.getText() ?? "";
2804
- const hasChild = !!prop.getDecorator("kosChild");
2805
- const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
2806
- if (isModelContainer && !hasChild) {
2807
- findings.push({
2808
- level: "warning",
2809
- rule: "container-missing-kosChild",
2810
- message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
2811
- });
2812
- continue;
2813
- }
2814
- const elem = modelElementType(typeText);
2815
- if (elem && !isModelContainer) {
2816
- findings.push({
2817
- level: "warning",
2818
- rule: "raw-model-collection",
2819
- message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
2820
- });
2821
- }
2822
- }
2823
- }
2824
- const hasError = findings.some((f) => f.level === "error");
2825
- return { modelFilePath, ok: !hasError, findings };
2826
- }
2827
- function firstDecoratorArg(decoratorText) {
2828
- const open = decoratorText.indexOf("(");
2829
- if (open === -1) return void 0;
2830
- const inner = decoratorText.slice(open + 1, decoratorText.lastIndexOf(")")).replace(/\s+/g, " ").trim();
2831
- return inner || void 0;
2832
- }
2833
- function collectDecorated(cls, decoratorName) {
2834
- const members = [];
2835
- const visit = (name, decoratorTextOf, typeText) => {
2836
- const text = decoratorTextOf();
2837
- if (text === void 0) return;
2838
- members.push({
2839
- name,
2840
- arg: firstDecoratorArg(text),
2841
- type: typeText || void 0
2842
- });
2843
- };
2844
- for (const m of cls.getMethods()) {
2845
- const dec = m.getDecorator(decoratorName);
2846
- if (dec) {
2847
- visit(m.getName(), () => dec.getText(), m.getReturnTypeNode()?.getText());
2848
- }
2849
- }
2850
- for (const p of cls.getProperties()) {
2851
- const dec = p.getDecorator(decoratorName);
2852
- if (dec) {
2853
- visit(p.getName(), () => dec.getText(), p.getTypeNode()?.getText());
2854
- }
2855
- }
2856
- return members;
2857
- }
2858
- function describeModel(codegenFs, options, projects) {
2859
- const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
2860
- const content = codegenFs.read(modelFilePath);
2861
- if (content === null) {
2862
- throw new Error(`Model file not found: ${modelFilePath}`);
2863
- }
2864
- const project = new tsMorph.Project({ useInMemoryFileSystem: true });
2865
- const sf = project.createSourceFile(modelFilePath, content, {
2866
- overwrite: true
2867
- });
2868
- let modelType;
2869
- const modelTypeDecl = sf.getVariableDeclaration("MODEL_TYPE");
2870
- if (modelTypeDecl) {
2871
- const init = modelTypeDecl.getInitializer()?.getText();
2872
- if (init) modelType = init.replace(/^["'`]|["'`]$/g, "");
2873
- }
2874
- const cls = sf.getClasses().find((c) => c.getDecorator("kosModel")) ?? sf.getClasses().find((c) => c.isExported()) ?? sf.getClasses()[0];
2875
- if (!cls) {
2876
- return {
2877
- modelFilePath,
2878
- modelType,
2879
- classDecorators: [],
2880
- singleton: false,
2881
- isCompanion: false,
2882
- children: [],
2883
- dependencies: [],
2884
- topicHandlers: [],
2885
- configProperties: [],
2886
- serviceRequests: [],
2887
- effects: [],
2888
- futures: []
2889
- };
2890
- }
2891
- const classDecorators = cls.getDecorators().map((d) => d.getName());
2892
- const kosModelArg = cls.getDecorator("kosModel") ? firstDecoratorArg(cls.getDecorator("kosModel").getText()) : void 0;
2893
- const singleton = /\bsingleton\s*:\s*true\b/.test(kosModelArg ?? "");
2894
- return {
2895
- modelFilePath,
2896
- modelType,
2897
- className: cls.getName(),
2898
- classDecorators,
2899
- singleton,
2900
- isCompanion: classDecorators.includes("kosCompanion"),
2901
- children: collectDecorated(cls, "kosChild"),
2902
- dependencies: collectDecorated(cls, "kosDependency"),
2903
- topicHandlers: collectDecorated(cls, "kosTopicHandler"),
2904
- configProperties: collectDecorated(cls, "kosConfigProperty"),
2905
- serviceRequests: collectDecorated(cls, "kosServiceRequest"),
2906
- effects: collectDecorated(cls, "kosModelEffect"),
2907
- futures: collectDecorated(cls, "kosFuture")
3629
+ endpointCreated,
3630
+ pathValidated,
3631
+ provisional,
3632
+ mocksFilePath: mock?.mocksFilePath,
3633
+ mockRegisterFunction: mock?.registerFunction,
3634
+ mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
3635
+ mockSamplePlaceholder: mock?.samplePlaceholder,
3636
+ removalSteps: provisional ? removalSteps({
3637
+ modelProject: options.modelProject,
3638
+ servicePath: options.servicePath,
3639
+ method,
3640
+ catalogName,
3641
+ endpointKey,
3642
+ servicesFilePath,
3643
+ mocksFilePath: mock?.mocksFilePath,
3644
+ mapperName
3645
+ }) : void 0
2908
3646
  };
2909
3647
  }
2910
3648
  const DEFAULT_SDK_PACKAGE = "@kosdev-code/kos-ui-sdk";
@@ -3090,6 +3828,82 @@ function lookupSdkType(codegenFs, options, projects) {
3090
3828
  )}). Check the name, or it may be internal / not part of the public surface.`
3091
3829
  };
3092
3830
  }
3831
+ function modelElementType(typeText) {
3832
+ let m;
3833
+ if (m = typeText.match(/^([A-Za-z_]\w*Model)\s*\[\]$/)) return m[1];
3834
+ if (m = typeText.match(/^Array<\s*([A-Za-z_]\w*Model)\s*>$/)) return m[1];
3835
+ if (m = typeText.match(/^(?:Map|Set)<[^>]*?\b([A-Za-z_]\w*Model)\b[^>]*>$/))
3836
+ return m[1];
3837
+ return null;
3838
+ }
3839
+ function validateModel(codegenFs, options, projects) {
3840
+ const { modelFilePath } = resolveModelFilePath(codegenFs, options, projects);
3841
+ const content = codegenFs.read(modelFilePath);
3842
+ if (content === null) {
3843
+ throw new Error(`Model file not found: ${modelFilePath}`);
3844
+ }
3845
+ const project = new tsMorph.Project({ useInMemoryFileSystem: true });
3846
+ const sf = project.createSourceFile(modelFilePath, content, {
3847
+ overwrite: true
3848
+ });
3849
+ const findings = [];
3850
+ const hasKosModel = sf.getClasses().some((c) => c.getDecorator("kosModel"));
3851
+ if (!hasKosModel) {
3852
+ findings.push({
3853
+ level: "error",
3854
+ rule: "missing-kosModel",
3855
+ message: "No @kosModel decorator found — this is not a KOS model."
3856
+ });
3857
+ }
3858
+ const imports = sf.getImportDeclarations();
3859
+ const mobxImport = imports.find(
3860
+ (d) => /^mobx(-react-lite)?$/.test(d.getModuleSpecifierValue())
3861
+ );
3862
+ if (mobxImport) {
3863
+ findings.push({
3864
+ level: "error",
3865
+ rule: "mobx-import",
3866
+ message: "Imports mobx directly. Reactivity is internal to KOS — remove the mobx import and use KOS reactivity / container models."
3867
+ });
3868
+ }
3869
+ const barrelServiceRequest = imports.find(
3870
+ (d) => d.getModuleSpecifierValue() === "@kosdev-code/kos-ui-sdk" && d.getNamedImports().some((n) => n.getName() === "kosServiceRequest")
3871
+ );
3872
+ if (barrelServiceRequest) {
3873
+ findings.push({
3874
+ level: "warning",
3875
+ rule: "untyped-service-request",
3876
+ message: "kosServiceRequest imported from the SDK barrel. Use the typed decorator from the api:generate'd service module so paths are validated against the OpenAPI types."
3877
+ });
3878
+ }
3879
+ for (const cls of sf.getClasses()) {
3880
+ for (const prop of cls.getProperties()) {
3881
+ const name = prop.getName();
3882
+ const typeText = (prop.getTypeNode()?.getText() ?? "").replace(/\s+/g, " ").trim();
3883
+ const initText = prop.getInitializer()?.getText() ?? "";
3884
+ const hasChild = !!prop.getDecorator("kosChild");
3885
+ const isModelContainer = /\bI?KosModelContainer\s*</.test(typeText) || /new\s+KosModelContainer\b/.test(initText);
3886
+ if (isModelContainer && !hasChild) {
3887
+ findings.push({
3888
+ level: "warning",
3889
+ rule: "container-missing-kosChild",
3890
+ message: `Property '${name}' is a model container but is not marked @kosChild — add @kosChild so its models join the model graph and lifecycle.`
3891
+ });
3892
+ continue;
3893
+ }
3894
+ const elem = modelElementType(typeText);
3895
+ if (elem && !isModelContainer) {
3896
+ findings.push({
3897
+ level: "warning",
3898
+ rule: "raw-model-collection",
3899
+ message: `Property '${name}' is a raw ${typeText} of models — prefer a KosModelContainer<${elem}> (indexing, sorting, lifecycle, delta handling) marked @kosChild.`
3900
+ });
3901
+ }
3902
+ }
3903
+ }
3904
+ const hasError = findings.some((f) => f.level === "error");
3905
+ return { modelFilePath, ok: !hasError, findings };
3906
+ }
3093
3907
  const PLUGIN_TYPES = {
3094
3908
  CUI: "cui",
3095
3909
  UTILITY: "utility",
@@ -3644,8 +4458,75 @@ function updatePluginConfiguration(codegenFs, projectConfig, options) {
3644
4458
  );
3645
4459
  }
3646
4460
  }
4461
+ function parse(content) {
4462
+ const singleQuoted = /^\s*import\b[^"']*'/m.test(content);
4463
+ const project = new tsMorph.Project({
4464
+ useInMemoryFileSystem: true,
4465
+ manipulationSettings: {
4466
+ indentationText: tsMorph.IndentationText.TwoSpaces,
4467
+ quoteKind: singleQuoted ? tsMorph.QuoteKind.Single : tsMorph.QuoteKind.Double
4468
+ }
4469
+ });
4470
+ return project.createSourceFile("registration-chain.ts", content, {
4471
+ overwrite: true
4472
+ });
4473
+ }
4474
+ function chainCalls(sf, name) {
4475
+ return sf.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).filter((call) => {
4476
+ const callee = call.getExpression();
4477
+ return callee.getKind() === tsMorph.SyntaxKind.PropertyAccessExpression && callee.asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getName() === name;
4478
+ });
4479
+ }
4480
+ function indentOfCall(sf, call) {
4481
+ const text = sf.getFullText();
4482
+ const nameNode = call.getExpression().asKindOrThrow(tsMorph.SyntaxKind.PropertyAccessExpression).getNameNode();
4483
+ const lineStart = text.lastIndexOf("\n", nameNode.getStart()) + 1;
4484
+ return text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
4485
+ }
4486
+ function countChainEntries(content) {
4487
+ return chainCalls(parse(content), "model").length;
4488
+ }
4489
+ function upsertChainEntry(content, bean, importSpec) {
4490
+ const sf = parse(content);
4491
+ const modelCalls = chainCalls(sf, "model");
4492
+ const registered = modelCalls.some((call) => {
4493
+ const [arg, ...rest] = call.getArguments();
4494
+ return rest.length === 0 && arg?.getText() === bean;
4495
+ });
4496
+ if (registered) return { changed: false, note: "already registered" };
4497
+ let anchor;
4498
+ let indent;
4499
+ if (modelCalls.length > 0) {
4500
+ anchor = modelCalls.reduce(
4501
+ (last, call) => call.getEnd() > last.getEnd() ? call : last
4502
+ );
4503
+ indent = indentOfCall(sf, anchor);
4504
+ } else {
4505
+ const starts = chainCalls(sf, "models").filter(
4506
+ (call) => call.getArguments().length === 0
4507
+ );
4508
+ anchor = starts[starts.length - 1];
4509
+ if (!anchor) {
4510
+ return {
4511
+ changed: false,
4512
+ error: "no .models() chain found in the registration file"
4513
+ };
4514
+ }
4515
+ const text = sf.getFullText();
4516
+ const lineStart = text.lastIndexOf("\n", anchor.getStart()) + 1;
4517
+ indent = (text.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "") + " ";
4518
+ }
4519
+ anchor.replaceWithText(`${anchor.getText()}
4520
+ ${indent}.model(${bean})`);
4521
+ const imported = sf.getImportDeclarations().some(
4522
+ (decl) => decl.getNamedImports().some((named) => named.getName() === bean)
4523
+ );
4524
+ if (!imported) ensureNamedImport(sf, importSpec, [{ name: bean }]);
4525
+ return { changed: true, content: sf.getFullText(), importAdded: !imported };
4526
+ }
3647
4527
  exports.BasePluginHandler = BasePluginHandler;
3648
4528
  exports.CONTRIBUTION_TYPE_MAP = CONTRIBUTION_TYPE_MAP;
4529
+ exports.DEFAULT_MODEL_LIBS_DIR = DEFAULT_MODEL_LIBS_DIR;
3649
4530
  exports.DirectFileSystem = DirectFileSystem;
3650
4531
  exports.KAB_OUTPUT_DIR = KAB_OUTPUT_DIR;
3651
4532
  exports.KAB_OUTPUT_PATH = KAB_OUTPUT_PATH;
@@ -3667,6 +4548,7 @@ exports.addFutureToModel = addFutureToModel;
3667
4548
  exports.addJavaArtifactToManifests = addJavaArtifactToManifests;
3668
4549
  exports.addKosModelConfiguration = addKosModelConfiguration;
3669
4550
  exports.addModelEffectToModel = addModelEffectToModel;
4551
+ exports.addParentAwareToModel = addParentAwareToModel;
3670
4552
  exports.addPropertyToModel = addPropertyToModel;
3671
4553
  exports.addServiceRequestToModel = addServiceRequestToModel;
3672
4554
  exports.addTopicHandlerToModel = addTopicHandlerToModel;
@@ -3675,6 +4557,7 @@ exports.buildKabTargets = buildKabTargets;
3675
4557
  exports.buildSbomTarget = buildSbomTarget;
3676
4558
  exports.camelCase = camelCase;
3677
4559
  exports.constantCase = constantCase;
4560
+ exports.countChainEntries = countChainEntries;
3678
4561
  exports.dashCase = dashCase;
3679
4562
  exports.describeModel = describeModel;
3680
4563
  exports.discoverJavaArtifacts = discoverJavaArtifacts;
@@ -3693,8 +4576,10 @@ exports.generateFilesFromTemplates = generateFilesFromTemplates;
3693
4576
  exports.generateHook = generateHook;
3694
4577
  exports.generateInit = generateInit;
3695
4578
  exports.generateModel = generateModel;
4579
+ exports.generateModelProject = generateModelProject;
3696
4580
  exports.generatePolyglotWorkspace = generatePolyglotWorkspace;
3697
4581
  exports.generateSplashProject = generateSplashProject;
4582
+ exports.generateViewModel = generateViewModel;
3698
4583
  exports.getCodegenLogger = getCodegenLogger;
3699
4584
  exports.getCurrentDirectoryName = getCurrentDirectoryName;
3700
4585
  exports.getKosModelConfigProp = getKosModelConfigProp;
@@ -3712,10 +4597,12 @@ exports.readJson = readJson;
3712
4597
  exports.readNxJson = readNxJson;
3713
4598
  exports.resolveKabPath = resolveKabPath;
3714
4599
  exports.resolveModelFilePath = resolveModelFilePath;
4600
+ exports.resolveModelProjectLayout = resolveModelProjectLayout;
3715
4601
  exports.setCodegenLogger = setCodegenLogger;
3716
4602
  exports.syncCiManifests = syncCiManifests;
3717
4603
  exports.updateJson = updateJson;
3718
4604
  exports.updateModelIndex = updateModelIndex;
4605
+ exports.upsertChainEntry = upsertChainEntry;
3719
4606
  exports.validateModel = validateModel;
3720
4607
  exports.writeJson = writeJson;
3721
4608
  //# sourceMappingURL=index.js.map