@jay-framework/jay-stack-cli 0.15.5 → 0.15.6

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 (36) hide show
  1. package/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
  2. package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
  3. package/agent-kit-template/designer/jay-html-styling.md +97 -0
  4. package/agent-kit-template/designer/jay-html-syntax.md +44 -0
  5. package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
  6. package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
  7. package/agent-kit-template/developer/cli-commands.md +228 -0
  8. package/agent-kit-template/developer/component-data.md +109 -0
  9. package/agent-kit-template/developer/component-refs.md +117 -0
  10. package/agent-kit-template/developer/component-state.md +140 -0
  11. package/agent-kit-template/developer/configuration.md +76 -0
  12. package/agent-kit-template/developer/page-components.md +103 -0
  13. package/agent-kit-template/developer/page-contracts.md +114 -0
  14. package/agent-kit-template/developer/project-structure.md +242 -0
  15. package/agent-kit-template/developer/render-results.md +112 -0
  16. package/agent-kit-template/developer/routing.md +161 -0
  17. package/agent-kit-template/developer/seo-guide.md +93 -0
  18. package/agent-kit-template/plugin/INSTRUCTIONS.md +40 -0
  19. package/agent-kit-template/plugin/actions-guide.md +125 -0
  20. package/agent-kit-template/plugin/component-context.md +103 -0
  21. package/agent-kit-template/plugin/component-data.md +109 -0
  22. package/agent-kit-template/plugin/component-refs.md +117 -0
  23. package/agent-kit-template/plugin/component-state.md +140 -0
  24. package/agent-kit-template/plugin/component-structure.md +174 -0
  25. package/agent-kit-template/plugin/contracts-guide.md +193 -0
  26. package/agent-kit-template/plugin/plugin-structure.md +194 -0
  27. package/agent-kit-template/plugin/render-results.md +112 -0
  28. package/agent-kit-template/plugin/seo-guide.md +93 -0
  29. package/agent-kit-template/plugin/services-guide.md +116 -0
  30. package/agent-kit-template/plugin/validation.md +101 -0
  31. package/dist/index.js +678 -60
  32. package/package.json +10 -10
  33. /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
  34. /package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +0 -0
  35. /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
  36. /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { listContracts, materializeContracts } from "@jay-framework/stack-server
14
14
  import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
15
15
  import { Command } from "commander";
16
16
  import chalk from "chalk";
17
+ import { createRequire } from "module";
17
18
  import { glob } from "glob";
18
19
  import { parse } from "node-html-parser";
19
20
  const DEFAULT_CONFIG = {
@@ -244,7 +245,7 @@ function getCommonStyles(node) {
244
245
  transformStyles.push(`rotate(${node.rotation}deg)`);
245
246
  }
246
247
  if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
247
- const visibleEffects = node.effects.filter((e) => e.visible !== false).reverse();
248
+ const visibleEffects = node.effects.filter((e2) => e2.visible !== false).reverse();
248
249
  const filterFunctions = [];
249
250
  const boxShadows = [];
250
251
  for (const effect of visibleEffects) {
@@ -410,7 +411,7 @@ function getStrokeStyles(node) {
410
411
  if (!node.strokes || node.strokes.length === 0) {
411
412
  return "";
412
413
  }
413
- const visibleStrokes = node.strokes.filter((s) => s.visible !== false);
414
+ const visibleStrokes = node.strokes.filter((s2) => s2.visible !== false);
414
415
  if (visibleStrokes.length === 0) {
415
416
  return "";
416
417
  }
@@ -1588,9 +1589,9 @@ function convertContractToProtocol(contract) {
1588
1589
  }
1589
1590
  async function isPageDirectory(dirPath) {
1590
1591
  const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1591
- const hasPageHtml = entries.some((e) => e.name === PAGE_FILENAME);
1592
- const hasPageContract = entries.some((e) => e.name === PAGE_CONTRACT_FILENAME);
1593
- const hasPageConfig = entries.some((e) => e.name === PAGE_CONFIG_FILENAME);
1592
+ const hasPageHtml = entries.some((e2) => e2.name === PAGE_FILENAME);
1593
+ const hasPageContract = entries.some((e2) => e2.name === PAGE_CONTRACT_FILENAME);
1594
+ const hasPageConfig = entries.some((e2) => e2.name === PAGE_CONFIG_FILENAME);
1594
1595
  const isPage = hasPageHtml || hasPageContract || hasPageConfig;
1595
1596
  return { isPage, hasPageHtml, hasPageContract, hasPageConfig };
1596
1597
  }
@@ -2155,7 +2156,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
2155
2156
  }
2156
2157
  return {
2157
2158
  type: "publish",
2158
- success: status.every((s) => s.success),
2159
+ success: status.every((s2) => s2.success),
2159
2160
  status
2160
2161
  };
2161
2162
  };
@@ -2511,6 +2512,280 @@ async function startDevServer(options = {}) {
2511
2512
  process.on("SIGTERM", shutdown);
2512
2513
  process.on("SIGINT", shutdown);
2513
2514
  }
2515
+ const s = createRequire(import.meta.url), e = s("typescript"), u = e;
2516
+ new Proxy(e, {
2517
+ get(t, r) {
2518
+ return t[r];
2519
+ }
2520
+ });
2521
+ const FRAMEWORK_PROP_TYPES = /* @__PURE__ */ new Set(["PageProps", "RequestQuery"]);
2522
+ function checkComponentPropsAndParams(sourceCode, contract, contractName, contractPath, sourcePath) {
2523
+ const errors = [];
2524
+ const warnings = [];
2525
+ const sourceFile = u.createSourceFile(
2526
+ sourcePath,
2527
+ sourceCode,
2528
+ u.ScriptTarget.Latest,
2529
+ true,
2530
+ u.ScriptKind.TS
2531
+ );
2532
+ const localInterfaces = collectLocalInterfaces(sourceFile);
2533
+ const contractImportedTypes = collectContractImportedTypes(sourceFile);
2534
+ const builderInfo = analyzeBuilderChains(sourceFile);
2535
+ for (const propsTypeName of builderInfo.propsTypeNames) {
2536
+ checkPropsConsistency(
2537
+ propsTypeName,
2538
+ localInterfaces,
2539
+ contractImportedTypes,
2540
+ contract,
2541
+ contractName,
2542
+ contractPath,
2543
+ sourcePath,
2544
+ errors,
2545
+ warnings
2546
+ );
2547
+ }
2548
+ if (builderInfo.hasLoadParams) {
2549
+ checkParamsConsistency(
2550
+ builderInfo.paramsTypeNames,
2551
+ localInterfaces,
2552
+ contractImportedTypes,
2553
+ contract,
2554
+ contractName,
2555
+ contractPath,
2556
+ sourcePath,
2557
+ errors,
2558
+ warnings
2559
+ );
2560
+ }
2561
+ return { errors, warnings };
2562
+ }
2563
+ function collectLocalInterfaces(sourceFile) {
2564
+ const interfaces = /* @__PURE__ */ new Map();
2565
+ for (const statement of sourceFile.statements) {
2566
+ if (u.isInterfaceDeclaration(statement)) {
2567
+ const name = statement.name.text;
2568
+ const properties = [];
2569
+ for (const member of statement.members) {
2570
+ if (u.isPropertySignature(member) && member.name) {
2571
+ if (u.isIdentifier(member.name)) {
2572
+ properties.push(member.name.text);
2573
+ } else if (u.isStringLiteral(member.name)) {
2574
+ properties.push(member.name.text);
2575
+ }
2576
+ }
2577
+ }
2578
+ const extendsTypes = [];
2579
+ if (statement.heritageClauses) {
2580
+ for (const clause of statement.heritageClauses) {
2581
+ for (const type of clause.types) {
2582
+ if (u.isIdentifier(type.expression)) {
2583
+ extendsTypes.push(type.expression.text);
2584
+ }
2585
+ }
2586
+ }
2587
+ }
2588
+ interfaces.set(name, { name, properties, extendsTypes });
2589
+ }
2590
+ if (u.isTypeAliasDeclaration(statement)) {
2591
+ const name = statement.name.text;
2592
+ if (u.isTypeLiteralNode(statement.type)) {
2593
+ const properties = [];
2594
+ for (const member of statement.type.members) {
2595
+ if (u.isPropertySignature(member) && member.name) {
2596
+ if (u.isIdentifier(member.name)) {
2597
+ properties.push(member.name.text);
2598
+ }
2599
+ }
2600
+ }
2601
+ interfaces.set(name, { name, properties, extendsTypes: [] });
2602
+ }
2603
+ }
2604
+ }
2605
+ return interfaces;
2606
+ }
2607
+ function collectContractImportedTypes(sourceFile) {
2608
+ const contractTypes = /* @__PURE__ */ new Set();
2609
+ for (const statement of sourceFile.statements) {
2610
+ if (!u.isImportDeclaration(statement))
2611
+ continue;
2612
+ const moduleSpecifier = statement.moduleSpecifier;
2613
+ if (!u.isStringLiteral(moduleSpecifier))
2614
+ continue;
2615
+ const modulePath = moduleSpecifier.text;
2616
+ if (!modulePath.includes(".jay-contract"))
2617
+ continue;
2618
+ const importClause = statement.importClause;
2619
+ if (!importClause)
2620
+ continue;
2621
+ const namedBindings = importClause.namedBindings;
2622
+ if (namedBindings && u.isNamedImports(namedBindings)) {
2623
+ for (const element of namedBindings.elements) {
2624
+ contractTypes.add(element.name.text);
2625
+ }
2626
+ }
2627
+ }
2628
+ return contractTypes;
2629
+ }
2630
+ function analyzeBuilderChains(sourceFile) {
2631
+ const result = {
2632
+ propsTypeNames: [],
2633
+ hasLoadParams: false,
2634
+ paramsTypeNames: []
2635
+ };
2636
+ visitNode(sourceFile, result);
2637
+ return result;
2638
+ }
2639
+ function visitNode(node, result) {
2640
+ if (u.isCallExpression(node)) {
2641
+ const expr = node.expression;
2642
+ if (u.isPropertyAccessExpression(expr) && expr.name.text === "withProps") {
2643
+ if (node.typeArguments && node.typeArguments.length > 0) {
2644
+ const typeNames = extractTypeNames(node.typeArguments[0]);
2645
+ result.propsTypeNames.push(...typeNames);
2646
+ }
2647
+ }
2648
+ if (u.isPropertyAccessExpression(expr) && expr.name.text === "withLoadParams") {
2649
+ result.hasLoadParams = true;
2650
+ if (node.typeArguments && node.typeArguments.length > 0) {
2651
+ const typeNames = extractTypeNames(node.typeArguments[0]);
2652
+ result.paramsTypeNames.push(...typeNames);
2653
+ }
2654
+ if (node.arguments.length > 0) {
2655
+ const arg = node.arguments[0];
2656
+ if (u.isIdentifier(arg))
2657
+ ;
2658
+ }
2659
+ }
2660
+ }
2661
+ u.forEachChild(node, (child) => visitNode(child, result));
2662
+ }
2663
+ function extractTypeNames(typeNode) {
2664
+ if (u.isTypeReferenceNode(typeNode)) {
2665
+ if (u.isIdentifier(typeNode.typeName)) {
2666
+ return [typeNode.typeName.text];
2667
+ }
2668
+ }
2669
+ if (u.isIntersectionTypeNode(typeNode)) {
2670
+ const names = [];
2671
+ for (const member of typeNode.types) {
2672
+ names.push(...extractTypeNames(member));
2673
+ }
2674
+ return names;
2675
+ }
2676
+ return [];
2677
+ }
2678
+ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
2679
+ if (FRAMEWORK_PROP_TYPES.has(propsTypeName))
2680
+ return;
2681
+ if (contractImportedTypes.has(propsTypeName))
2682
+ return;
2683
+ const prefix = `[${contractName}]`;
2684
+ const iface = localInterfaces.get(propsTypeName);
2685
+ if (!iface) {
2686
+ if (!contract.props || contract.props.length === 0) {
2687
+ errors.push({
2688
+ type: "contract-invalid",
2689
+ message: `${prefix} component uses .withProps<${propsTypeName}>() but the contract does not declare any props`,
2690
+ location: contractPath,
2691
+ suggestion: `Add a props section to the contract`
2692
+ });
2693
+ }
2694
+ return;
2695
+ }
2696
+ const ownProperties = iface.properties;
2697
+ if (ownProperties.length === 0)
2698
+ return;
2699
+ if (!contract.props || contract.props.length === 0) {
2700
+ errors.push({
2701
+ type: "contract-invalid",
2702
+ message: `${prefix} component uses .withProps<${propsTypeName}>() with properties [${ownProperties.join(", ")}] but the contract does not declare any props`,
2703
+ location: contractPath,
2704
+ suggestion: `Add to contract: props:
2705
+ ` + ownProperties.map((p) => ` - name: ${p}
2706
+ type: string`).join("\n")
2707
+ });
2708
+ return;
2709
+ }
2710
+ const contractPropNames = new Set(contract.props.map((p) => p.name));
2711
+ for (const prop of ownProperties) {
2712
+ if (!contractPropNames.has(prop)) {
2713
+ errors.push({
2714
+ type: "contract-invalid",
2715
+ message: `${prefix} component prop "${prop}" (from ${propsTypeName}) is not declared in the contract`,
2716
+ location: contractPath,
2717
+ suggestion: `Add to contract props: - name: ${prop}`
2718
+ });
2719
+ }
2720
+ }
2721
+ for (const contractProp of contract.props) {
2722
+ if (!ownProperties.includes(contractProp.name)) {
2723
+ warnings.push({
2724
+ type: "contract-invalid",
2725
+ message: `${prefix} contract declares prop "${contractProp.name}" but the component's ${propsTypeName} interface does not include it`,
2726
+ location: sourcePath,
2727
+ suggestion: `Add "${contractProp.name}" to the ${propsTypeName} interface, or remove it from the contract`
2728
+ });
2729
+ }
2730
+ }
2731
+ }
2732
+ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
2733
+ const prefix = `[${contractName}]`;
2734
+ if (!contract.params || contract.params.length === 0) {
2735
+ errors.push({
2736
+ type: "contract-invalid",
2737
+ message: `${prefix} component uses .withLoadParams() but the contract does not declare any params`,
2738
+ location: contractPath,
2739
+ suggestion: `Add a params section to the contract (e.g., params: { slug: string })`
2740
+ });
2741
+ for (const typeName of paramsTypeNames) {
2742
+ if (FRAMEWORK_PROP_TYPES.has(typeName))
2743
+ continue;
2744
+ if (contractImportedTypes.has(typeName))
2745
+ continue;
2746
+ const iface = localInterfaces.get(typeName);
2747
+ if (iface) {
2748
+ const ownProps = iface.properties;
2749
+ if (ownProps.length > 0) {
2750
+ errors[errors.length - 1].suggestion = `Add to contract: params:
2751
+ ` + ownProps.map((p) => ` ${p}: string`).join("\n");
2752
+ }
2753
+ }
2754
+ }
2755
+ return;
2756
+ }
2757
+ for (const typeName of paramsTypeNames) {
2758
+ if (FRAMEWORK_PROP_TYPES.has(typeName))
2759
+ continue;
2760
+ if (contractImportedTypes.has(typeName))
2761
+ continue;
2762
+ const iface = localInterfaces.get(typeName);
2763
+ if (!iface)
2764
+ continue;
2765
+ const ownProperties = iface.properties;
2766
+ const contractParamNames = new Set(contract.params.map((p) => p.name));
2767
+ for (const prop of ownProperties) {
2768
+ if (!contractParamNames.has(prop)) {
2769
+ errors.push({
2770
+ type: "contract-invalid",
2771
+ message: `${prefix} component param "${prop}" (from ${typeName}) is not declared in the contract`,
2772
+ location: contractPath,
2773
+ suggestion: `Add to contract params: ${prop}: string`
2774
+ });
2775
+ }
2776
+ }
2777
+ for (const contractParam of contract.params) {
2778
+ if (!ownProperties.includes(contractParam.name)) {
2779
+ warnings.push({
2780
+ type: "contract-invalid",
2781
+ message: `${prefix} contract declares param "${contractParam.name}" but the component's ${typeName} interface does not include it`,
2782
+ location: sourcePath,
2783
+ suggestion: `Add "${contractParam.name}" to the ${typeName} interface, or remove it from the contract`
2784
+ });
2785
+ }
2786
+ }
2787
+ }
2788
+ }
2514
2789
  async function validatePlugin(options = {}) {
2515
2790
  const pluginPath = options.pluginPath || process.cwd();
2516
2791
  if (options.local) {
@@ -2612,6 +2887,36 @@ async function validateLocalPlugins(projectPath, options) {
2612
2887
  typesGenerated: allResults.reduce((sum, r) => sum + (r.typesGenerated || 0), 0)
2613
2888
  };
2614
2889
  }
2890
+ function validateDocFile(docPath, label, context, result) {
2891
+ const resolvedPath = path.join(context.pluginPath, docPath);
2892
+ if (!fs.existsSync(resolvedPath)) {
2893
+ result.errors.push({
2894
+ type: "file-missing",
2895
+ message: `Doc file for ${label} not found: ${docPath}`,
2896
+ location: "plugin.yaml",
2897
+ suggestion: `Create the documentation file at ${resolvedPath}`
2898
+ });
2899
+ return;
2900
+ }
2901
+ if (context.isNpmPackage) {
2902
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
2903
+ try {
2904
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
2905
+ if (packageJson.exports) {
2906
+ const exportKey = "./" + docPath.replace(/^\.\//, "");
2907
+ if (!packageJson.exports[exportKey]) {
2908
+ result.errors.push({
2909
+ type: "export-mismatch",
2910
+ message: `Doc file for ${label} is not exported in package.json: ${docPath}`,
2911
+ location: packageJsonPath,
2912
+ suggestion: `Add "${exportKey}": "${docPath}" to the exports field`
2913
+ });
2914
+ }
2915
+ }
2916
+ } catch {
2917
+ }
2918
+ }
2919
+ }
2615
2920
  async function validateSchema(context, result) {
2616
2921
  const { manifest } = context;
2617
2922
  if (!manifest.name) {
@@ -2702,46 +3007,111 @@ async function validateSchema(context, result) {
2702
3007
  suggestion: 'Add either "contracts" or "dynamic_contracts" to expose functionality'
2703
3008
  });
2704
3009
  }
2705
- }
2706
- async function validateContract(contract, index, context, generateTypes, result) {
2707
- result.contractsChecked = (result.contractsChecked || 0) + 1;
2708
- let contractPath;
2709
- if (context.isNpmPackage) {
2710
- const contractSpec = contract.contract;
2711
- const possiblePaths = [
2712
- path.join(context.pluginPath, "dist", contractSpec),
2713
- path.join(context.pluginPath, "lib", contractSpec),
2714
- path.join(context.pluginPath, contractSpec)
2715
- ];
2716
- let found = false;
2717
- for (const possiblePath of possiblePaths) {
2718
- if (fs.existsSync(possiblePath)) {
2719
- contractPath = possiblePath;
2720
- found = true;
2721
- break;
2722
- }
2723
- }
2724
- if (!found) {
3010
+ if (manifest.services) {
3011
+ if (!Array.isArray(manifest.services)) {
2725
3012
  result.errors.push({
2726
- type: "file-missing",
2727
- message: `Contract file not found: ${contractSpec}`,
2728
- location: `plugin.yaml contracts[${index}]`,
2729
- suggestion: `Ensure the contract file exists (looked in dist/, lib/, and root)`
3013
+ type: "schema",
3014
+ message: 'Field "services" must be an array',
3015
+ location: "plugin.yaml"
3016
+ });
3017
+ } else {
3018
+ manifest.services.forEach((service, index) => {
3019
+ if (!service.name) {
3020
+ result.errors.push({
3021
+ type: "schema",
3022
+ message: `Service at index ${index} is missing "name" field`,
3023
+ location: "plugin.yaml"
3024
+ });
3025
+ }
3026
+ if (!service.marker) {
3027
+ result.errors.push({
3028
+ type: "schema",
3029
+ message: `Service "${service.name || index}" is missing "marker" field`,
3030
+ location: "plugin.yaml",
3031
+ suggestion: "Specify the exported service marker constant name"
3032
+ });
3033
+ }
3034
+ if (service.doc) {
3035
+ validateDocFile(service.doc, `service "${service.name}"`, context, result);
3036
+ }
2730
3037
  });
2731
- return;
2732
3038
  }
2733
- } else {
2734
- contractPath = path.join(context.pluginPath, contract.contract);
2735
- if (!fs.existsSync(contractPath)) {
3039
+ }
3040
+ if (manifest.contexts) {
3041
+ if (!Array.isArray(manifest.contexts)) {
2736
3042
  result.errors.push({
2737
- type: "file-missing",
2738
- message: `Contract file not found: ${contract.contract}`,
2739
- location: `plugin.yaml contracts[${index}]`,
2740
- suggestion: `Create the contract file at ${contractPath}`
3043
+ type: "schema",
3044
+ message: 'Field "contexts" must be an array',
3045
+ location: "plugin.yaml"
3046
+ });
3047
+ } else {
3048
+ manifest.contexts.forEach((ctx, index) => {
3049
+ if (!ctx.name) {
3050
+ result.errors.push({
3051
+ type: "schema",
3052
+ message: `Context at index ${index} is missing "name" field`,
3053
+ location: "plugin.yaml"
3054
+ });
3055
+ }
3056
+ if (!ctx.marker) {
3057
+ result.errors.push({
3058
+ type: "schema",
3059
+ message: `Context "${ctx.name || index}" is missing "marker" field`,
3060
+ location: "plugin.yaml",
3061
+ suggestion: "Specify the exported context marker constant name"
3062
+ });
3063
+ }
3064
+ if (ctx.doc) {
3065
+ validateDocFile(ctx.doc, `context "${ctx.name}"`, context, result);
3066
+ }
2741
3067
  });
2742
- return;
2743
3068
  }
2744
3069
  }
3070
+ }
3071
+ function resolveContractFile(contractSpec, context) {
3072
+ if (context.isNpmPackage) {
3073
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
3074
+ if (fs.existsSync(packageJsonPath)) {
3075
+ try {
3076
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
3077
+ if (packageJson.exports) {
3078
+ const exportKey = "./" + contractSpec;
3079
+ const exportValue = packageJson.exports[exportKey];
3080
+ if (exportValue) {
3081
+ const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
3082
+ if (resolvedPath) {
3083
+ const fullPath = path.join(context.pluginPath, resolvedPath);
3084
+ if (fs.existsSync(fullPath))
3085
+ return fullPath;
3086
+ }
3087
+ }
3088
+ }
3089
+ } catch {
3090
+ }
3091
+ }
3092
+ for (const dir of ["dist", "lib", ""]) {
3093
+ const candidate = path.join(context.pluginPath, dir, contractSpec);
3094
+ if (fs.existsSync(candidate))
3095
+ return candidate;
3096
+ }
3097
+ return void 0;
3098
+ } else {
3099
+ const candidate = path.join(context.pluginPath, contractSpec);
3100
+ return fs.existsSync(candidate) ? candidate : void 0;
3101
+ }
3102
+ }
3103
+ async function validateContract(contract, index, context, generateTypes, result) {
3104
+ result.contractsChecked = (result.contractsChecked || 0) + 1;
3105
+ const contractPath = resolveContractFile(contract.contract, context);
3106
+ if (!contractPath) {
3107
+ result.errors.push({
3108
+ type: "file-missing",
3109
+ message: `Contract file not found: ${contract.contract}`,
3110
+ location: `plugin.yaml contracts[${index}]`,
3111
+ suggestion: context.isNpmPackage ? `Ensure the contract is exported in package.json and the file exists` : `Create the contract file at ${path.join(context.pluginPath, contract.contract)}`
3112
+ });
3113
+ return;
3114
+ }
2745
3115
  try {
2746
3116
  const contractContent = await fs.promises.readFile(contractPath, "utf-8");
2747
3117
  const parsedContract = YAML.parse(contractContent);
@@ -2792,6 +3162,7 @@ async function validateComponent(contract, index, context, result) {
2792
3162
  location: `plugin.yaml contracts[${index}]`,
2793
3163
  suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
2794
3164
  });
3165
+ return;
2795
3166
  }
2796
3167
  if (contract.component.includes("/") || contract.component.includes(".")) {
2797
3168
  result.warnings.push({
@@ -2801,6 +3172,143 @@ async function validateComponent(contract, index, context, result) {
2801
3172
  suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
2802
3173
  });
2803
3174
  }
3175
+ await checkComponentContractConsistency(contract, context, result);
3176
+ }
3177
+ function hasExportModifier(node) {
3178
+ return node.modifiers?.some((m) => m.kind === u.SyntaxKind.ExportKeyword) ?? false;
3179
+ }
3180
+ function resolveModulePath(basePath) {
3181
+ for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
3182
+ const candidate = basePath + ext;
3183
+ if (fs.existsSync(candidate))
3184
+ return candidate;
3185
+ }
3186
+ return void 0;
3187
+ }
3188
+ function resolveComponentSourcePath(componentName, context) {
3189
+ const modulePath = context.manifest.module || "index";
3190
+ const entryBase = path.join(context.pluginPath, modulePath);
3191
+ const entryFile = resolveModulePath(entryBase);
3192
+ const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
3193
+ const sourceEntry = entryFile || libEntryFile;
3194
+ if (!sourceEntry)
3195
+ return void 0;
3196
+ if (!sourceEntry.endsWith(".ts"))
3197
+ return void 0;
3198
+ let sourceCode;
3199
+ try {
3200
+ sourceCode = fs.readFileSync(sourceEntry, "utf-8");
3201
+ } catch {
3202
+ return void 0;
3203
+ }
3204
+ const sourceFile = u.createSourceFile(
3205
+ sourceEntry,
3206
+ sourceCode,
3207
+ u.ScriptTarget.Latest,
3208
+ true,
3209
+ u.ScriptKind.TS
3210
+ );
3211
+ const starReexportModules = [];
3212
+ for (const statement of sourceFile.statements) {
3213
+ if (!u.isExportDeclaration(statement))
3214
+ continue;
3215
+ if (!statement.moduleSpecifier)
3216
+ continue;
3217
+ if (!u.isStringLiteral(statement.moduleSpecifier))
3218
+ continue;
3219
+ const moduleSpec = statement.moduleSpecifier.text;
3220
+ const exportClause = statement.exportClause;
3221
+ if (!exportClause) {
3222
+ starReexportModules.push(moduleSpec);
3223
+ continue;
3224
+ }
3225
+ if (u.isNamedExports(exportClause)) {
3226
+ for (const element of exportClause.elements) {
3227
+ const exportedName = element.name.text;
3228
+ if (exportedName === componentName) {
3229
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
3230
+ return resolveModulePath(resolvedBase);
3231
+ }
3232
+ }
3233
+ }
3234
+ }
3235
+ for (const moduleSpec of starReexportModules) {
3236
+ if (!moduleSpec.startsWith("."))
3237
+ continue;
3238
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
3239
+ const resolvedPath = resolveModulePath(resolvedBase);
3240
+ if (!resolvedPath || !resolvedPath.endsWith(".ts"))
3241
+ continue;
3242
+ try {
3243
+ const modSource = fs.readFileSync(resolvedPath, "utf-8");
3244
+ const modFile = u.createSourceFile(
3245
+ resolvedPath,
3246
+ modSource,
3247
+ u.ScriptTarget.Latest,
3248
+ true,
3249
+ u.ScriptKind.TS
3250
+ );
3251
+ for (const stmt of modFile.statements) {
3252
+ if (u.isVariableStatement(stmt) && hasExportModifier(stmt)) {
3253
+ for (const decl of stmt.declarationList.declarations) {
3254
+ if (u.isIdentifier(decl.name) && decl.name.text === componentName) {
3255
+ return resolvedPath;
3256
+ }
3257
+ }
3258
+ }
3259
+ if (u.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name?.text === componentName) {
3260
+ return resolvedPath;
3261
+ }
3262
+ }
3263
+ } catch {
3264
+ continue;
3265
+ }
3266
+ }
3267
+ return sourceEntry;
3268
+ }
3269
+ function resolveContractPath(contract, context) {
3270
+ return resolveContractFile(contract.contract, context);
3271
+ }
3272
+ async function checkComponentContractConsistency(contract, context, result) {
3273
+ const componentName = contract.component;
3274
+ if (!componentName)
3275
+ return;
3276
+ const sourcePath = resolveComponentSourcePath(componentName, context);
3277
+ if (!sourcePath)
3278
+ return;
3279
+ if (!sourcePath.endsWith(".ts"))
3280
+ return;
3281
+ const contractPath = resolveContractPath(contract, context);
3282
+ if (!contractPath)
3283
+ return;
3284
+ let contractContent;
3285
+ try {
3286
+ contractContent = await fs.promises.readFile(contractPath, "utf-8");
3287
+ } catch {
3288
+ return;
3289
+ }
3290
+ const parsed = parseContract(contractContent, path.basename(contractPath));
3291
+ if (parsed.validations.length > 0)
3292
+ return;
3293
+ let sourceCode;
3294
+ try {
3295
+ sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
3296
+ } catch {
3297
+ return;
3298
+ }
3299
+ const contractName = contract.contract.replace(/\.jay-contract$/, "");
3300
+ const checkResult = checkComponentPropsAndParams(
3301
+ sourceCode,
3302
+ {
3303
+ props: parsed.val?.props,
3304
+ params: parsed.val?.params
3305
+ },
3306
+ contractName,
3307
+ contractPath,
3308
+ sourcePath
3309
+ );
3310
+ result.errors.push(...checkResult.errors);
3311
+ result.warnings.push(...checkResult.warnings);
2804
3312
  }
2805
3313
  async function validatePackageJson(context, result) {
2806
3314
  const packageJsonPath = path.join(context.pluginPath, "package.json");
@@ -3327,6 +3835,101 @@ function checkRouteParams(parsedFile, filePath, pagesBase, jayHtmlContent) {
3327
3835
  }
3328
3836
  return warnings;
3329
3837
  }
3838
+ function checkRouteToContractParams(parsedFile, filePath, pagesBase) {
3839
+ const routeParams = extractRouteParams(filePath, pagesBase);
3840
+ if (routeParams.size === 0)
3841
+ return [];
3842
+ const hasAnyContract = !!parsedFile.contract || parsedFile.headlessImports.some((imp) => !!imp.contract);
3843
+ if (!hasAnyContract)
3844
+ return [];
3845
+ const declaredParams = /* @__PURE__ */ new Set();
3846
+ if (parsedFile.contract?.params) {
3847
+ for (const p of parsedFile.contract.params) {
3848
+ declaredParams.add(p.name);
3849
+ }
3850
+ }
3851
+ for (const imp of parsedFile.headlessImports) {
3852
+ if (imp.contract?.params) {
3853
+ for (const p of imp.contract.params) {
3854
+ declaredParams.add(p.name);
3855
+ }
3856
+ }
3857
+ }
3858
+ const warnings = [];
3859
+ for (const routeParam of routeParams) {
3860
+ if (!declaredParams.has(routeParam)) {
3861
+ warnings.push(
3862
+ `Route provides param "${routeParam}" but no contract on this page declares it. Add params: { ${routeParam}: string } to the appropriate contract.`
3863
+ );
3864
+ }
3865
+ }
3866
+ return warnings;
3867
+ }
3868
+ const HEADLESS_SKIP_ATTRS = /* @__PURE__ */ new Set([
3869
+ "foreach",
3870
+ "if",
3871
+ "ref",
3872
+ "trackby",
3873
+ "slowforeach",
3874
+ "jayindex",
3875
+ "jaytrackby",
3876
+ "when-resolved",
3877
+ "when-loading",
3878
+ "when-rejected",
3879
+ "accessor",
3880
+ "props",
3881
+ "key",
3882
+ "jay-coordinate-base",
3883
+ "jay-scope"
3884
+ ]);
3885
+ function checkHeadlessInstanceProps(jayHtml, file) {
3886
+ const imports = jayHtml.headlessImports;
3887
+ const warnings = [];
3888
+ function walkElement(element) {
3889
+ const tagName = element.rawTagName?.toLowerCase();
3890
+ if (tagName?.startsWith("jay:")) {
3891
+ const contractName = tagName.substring(4);
3892
+ const imp = imports.find((i) => i.contractName === contractName && i.contract);
3893
+ if (imp?.contract) {
3894
+ const contract = imp.contract;
3895
+ const attrs = element.attributes ?? {};
3896
+ const passedProps = /* @__PURE__ */ new Set();
3897
+ for (const attrName of Object.keys(attrs)) {
3898
+ if (!HEADLESS_SKIP_ATTRS.has(attrName.toLowerCase())) {
3899
+ passedProps.add(attrName);
3900
+ }
3901
+ }
3902
+ if (passedProps.size > 0) {
3903
+ const contractPropNames = new Set((contract.props || []).map((p) => p.name));
3904
+ for (const prop of passedProps) {
3905
+ if (!contractPropNames.has(prop)) {
3906
+ imp.key ? `${imp.key} (${contractName})` : contractName;
3907
+ warnings.push(
3908
+ `<jay:${contractName}> passes attribute "${prop}" but the "${contract.name}" contract does not declare it as a prop. Add to ${contractName}.jay-contract: props: [{ name: ${prop}, type: string }]`
3909
+ );
3910
+ }
3911
+ }
3912
+ }
3913
+ if (contract.props) {
3914
+ for (const contractProp of contract.props) {
3915
+ if (contractProp.required && !passedProps.has(contractProp.name)) {
3916
+ warnings.push(
3917
+ `<jay:${contractName}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
3918
+ );
3919
+ }
3920
+ }
3921
+ }
3922
+ }
3923
+ }
3924
+ for (const child of element.childNodes ?? []) {
3925
+ if (child.nodeType === 1) {
3926
+ walkElement(child);
3927
+ }
3928
+ }
3929
+ }
3930
+ walkElement(jayHtml.body);
3931
+ return warnings;
3932
+ }
3330
3933
  async function validateJayFiles(options = {}) {
3331
3934
  const config = loadConfig();
3332
3935
  const resolvedConfig = getConfigWithDefaults(config);
@@ -3404,10 +4007,22 @@ async function validateJayFiles(options = {}) {
3404
4007
  for (const msg of routeParamWarnings) {
3405
4008
  warnings.push({ file: relativePath, message: msg });
3406
4009
  }
4010
+ const routeToContractWarnings = checkRouteToContractParams(
4011
+ parsedFile.val,
4012
+ jayFile,
4013
+ scanDir
4014
+ );
4015
+ for (const msg of routeToContractWarnings) {
4016
+ warnings.push({ file: relativePath, message: msg });
4017
+ }
3407
4018
  const refTypeErrors = checkRefElementTypes(parsedFile.val, relativePath);
3408
4019
  for (const msg of refTypeErrors) {
3409
4020
  errors.push({ file: relativePath, message: msg, stage: "generate" });
3410
4021
  }
4022
+ const headlessPropWarnings = checkHeadlessInstanceProps(parsedFile.val, relativePath);
4023
+ for (const msg of headlessPropWarnings) {
4024
+ warnings.push({ file: relativePath, message: msg });
4025
+ }
3411
4026
  const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
3412
4027
  if (fileCoverage) {
3413
4028
  coverage.push(fileCoverage);
@@ -3878,32 +4493,32 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
3878
4493
  process.exit(1);
3879
4494
  }
3880
4495
  });
3881
- async function ensureAgentKitDocs(projectRoot, force) {
4496
+ const ALL_ROLES = ["designer", "developer", "plugin"];
4497
+ async function ensureAgentKitDocs(projectRoot, _force, mode) {
3882
4498
  const path2 = await import("node:path");
3883
4499
  const fs2 = await import("node:fs/promises");
3884
4500
  const { fileURLToPath } = await import("node:url");
3885
4501
  const agentKitDir = path2.join(projectRoot, "agent-kit");
3886
- await fs2.mkdir(agentKitDir, { recursive: true });
3887
4502
  const thisDir = path2.dirname(fileURLToPath(import.meta.url));
3888
4503
  const templateDir = path2.resolve(thisDir, "..", "agent-kit-template");
3889
- let files;
3890
- try {
3891
- files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
3892
- } catch {
3893
- getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
3894
- return;
3895
- }
3896
- for (const filename of files) {
3897
- const destPath = path2.join(agentKitDir, filename);
3898
- if (!force) {
3899
- try {
3900
- await fs2.access(destPath);
3901
- continue;
3902
- } catch {
3903
- }
4504
+ const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
4505
+ for (const role of roles) {
4506
+ const roleTemplateDir = path2.join(templateDir, role);
4507
+ const roleOutputDir = path2.join(agentKitDir, role);
4508
+ let files;
4509
+ try {
4510
+ files = (await fs2.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
4511
+ } catch {
4512
+ continue;
4513
+ }
4514
+ await fs2.mkdir(roleOutputDir, { recursive: true });
4515
+ for (const filename of files) {
4516
+ await fs2.copyFile(
4517
+ path2.join(roleTemplateDir, filename),
4518
+ path2.join(roleOutputDir, filename)
4519
+ );
4520
+ getLogger().info(chalk.gray(` Created agent-kit/${role}/${filename}`));
3904
4521
  }
3905
- await fs2.copyFile(path2.join(templateDir, filename), destPath);
3906
- getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
3907
4522
  }
3908
4523
  }
3909
4524
  async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
@@ -4025,7 +4640,10 @@ program.command("setup [plugin]").description(
4025
4640
  });
4026
4641
  program.command("agent-kit").description(
4027
4642
  "Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
4028
- ).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option("-v, --verbose", "Show detailed output").action(async (options) => {
4643
+ ).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option(
4644
+ "-m, --mode <role>",
4645
+ "Generate guides for a specific role: designer, developer, or plugin (default: all)"
4646
+ ).option("-v, --verbose", "Show detailed output").action(async (options) => {
4029
4647
  const projectRoot = process.cwd();
4030
4648
  const { initErrors, viteServer } = await runMaterialize(
4031
4649
  projectRoot,
@@ -4036,7 +4654,7 @@ program.command("agent-kit").description(
4036
4654
  );
4037
4655
  try {
4038
4656
  if (!options.list) {
4039
- await ensureAgentKitDocs(projectRoot, options.force);
4657
+ await ensureAgentKitDocs(projectRoot, options.force, options.mode);
4040
4658
  if (options.references !== false) {
4041
4659
  await generatePluginReferences(projectRoot, options, initErrors, viteServer);
4042
4660
  }