@jay-framework/jay-stack-cli 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import express from "express";
3
- import { mkDevServer } from "@jay-framework/dev-server";
3
+ import { mkDevServer, createViteForCli } from "@jay-framework/dev-server";
4
4
  import { createEditorServer } from "@jay-framework/editor-server";
5
5
  import getPort from "get-port";
6
6
  import path from "path";
7
- import fs from "fs";
7
+ import fs, { promises } from "fs";
8
8
  import YAML from "yaml";
9
+ import { getLogger, createDevLogger, setDevLogger } from "@jay-framework/logger";
9
10
  import { parse } from "node-html-parser";
10
11
  import { createRequire } from "module";
11
- import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, ContractTagType } from "@jay-framework/compiler-jay-html";
12
- import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, JayAtomicType, JayEnumType, loadPluginManifest } from "@jay-framework/compiler-shared";
12
+ import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, ContractTagType, generateElementFile } from "@jay-framework/compiler-jay-html";
13
+ import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
14
+ import { listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
15
+ import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
13
16
  import { Command } from "commander";
14
17
  import chalk from "chalk";
18
+ import { glob } from "glob";
15
19
  const DEFAULT_CONFIG = {
16
20
  devServer: {
17
21
  portRange: [3e3, 3100],
@@ -43,7 +47,7 @@ function loadConfig() {
43
47
  }
44
48
  };
45
49
  } catch (error) {
46
- console.warn("Failed to parse .jay YAML config file, using defaults:", error);
50
+ getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
47
51
  return DEFAULT_CONFIG;
48
52
  }
49
53
  }
@@ -81,7 +85,7 @@ function updateConfig(updates) {
81
85
  const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
82
86
  fs.writeFileSync(configPath, yamlContent);
83
87
  } catch (error) {
84
- console.warn("Failed to update .jay config file:", error);
88
+ getLogger().warn(`Failed to update .jay config file: ${error}`);
85
89
  }
86
90
  }
87
91
  const PAGE_FILENAME = `page${JAY_EXTENSION}`;
@@ -157,7 +161,7 @@ async function scanPageDirectories(pagesBasePath, onPageFound) {
157
161
  }
158
162
  }
159
163
  } catch (error) {
160
- console.warn(`Failed to scan directory ${dirPath}:`, error);
164
+ getLogger().warn(`Failed to scan directory ${dirPath}: ${error}`);
161
165
  }
162
166
  }
163
167
  await scanDirectory(pagesBasePath);
@@ -167,9 +171,8 @@ async function parseContractFile(contractFilePath) {
167
171
  const contractYaml = await fs.promises.readFile(contractFilePath, "utf-8");
168
172
  const parsedContract = parseContract(contractYaml, contractFilePath);
169
173
  if (parsedContract.validations.length > 0) {
170
- console.warn(
171
- `Contract validation errors in ${contractFilePath}:`,
172
- parsedContract.validations
174
+ getLogger().warn(
175
+ `Contract validation errors in ${contractFilePath}: ${parsedContract.validations.join(", ")}`
173
176
  );
174
177
  }
175
178
  if (parsedContract.val) {
@@ -183,7 +186,7 @@ async function parseContractFile(contractFilePath) {
183
186
  };
184
187
  }
185
188
  } catch (error) {
186
- console.warn(`Failed to parse contract file ${contractFilePath}:`, error);
189
+ getLogger().warn(`Failed to parse contract file ${contractFilePath}: ${error}`);
187
190
  }
188
191
  return null;
189
192
  }
@@ -209,11 +212,11 @@ async function resolveLinkedTags(tags, baseDir) {
209
212
  }
210
213
  resolvedTags.push(resolvedTag);
211
214
  } else {
212
- console.warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
215
+ getLogger().warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
213
216
  resolvedTags.push(convertContractTagToProtocol(tag));
214
217
  }
215
218
  } catch (error) {
216
- console.warn(`Error resolving linked contract ${tag.link}:`, error);
219
+ getLogger().warn(`Error resolving linked contract ${tag.link}: ${error}`);
217
220
  resolvedTags.push(convertContractTagToProtocol(tag));
218
221
  }
219
222
  } else if (tag.tags) {
@@ -234,9 +237,8 @@ function resolveAppContractPath(appModule, contractFileName, projectRootPath) {
234
237
  const resolvedPath = require2.resolve(modulePath);
235
238
  return resolvedPath;
236
239
  } catch (error) {
237
- console.warn(
238
- `Failed to resolve contract: ${appModule}/${contractFileName}`,
239
- error instanceof Error ? error.message : error
240
+ getLogger().warn(
241
+ `Failed to resolve contract: ${appModule}/${contractFileName} - ${error instanceof Error ? error.message : error}`
240
242
  );
241
243
  return null;
242
244
  }
@@ -315,12 +317,12 @@ async function scanInstalledAppContracts(configBasePath, projectRootPath) {
315
317
  installedAppContracts[appName] = appContracts;
316
318
  }
317
319
  } catch (error) {
318
- console.warn(`Failed to parse app config ${appConfigPath}:`, error);
320
+ getLogger().warn(`Failed to parse app config ${appConfigPath}: ${error}`);
319
321
  }
320
322
  }
321
323
  }
322
324
  } catch (error) {
323
- console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
325
+ getLogger().warn(`Failed to scan installed apps directory ${installedAppsPath}: ${error}`);
324
326
  }
325
327
  return installedAppContracts;
326
328
  }
@@ -419,7 +421,7 @@ async function scanProjectComponents(componentsBasePath) {
419
421
  }
420
422
  }
421
423
  } catch (error) {
422
- console.warn(`Failed to scan components directory ${componentsBasePath}:`, error);
424
+ getLogger().warn(`Failed to scan components directory ${componentsBasePath}: ${error}`);
423
425
  }
424
426
  return components;
425
427
  }
@@ -447,12 +449,12 @@ async function scanInstalledApps(configBasePath) {
447
449
  });
448
450
  }
449
451
  } catch (error) {
450
- console.warn(`Failed to parse app config ${appConfigPath}:`, error);
452
+ getLogger().warn(`Failed to parse app config ${appConfigPath}: ${error}`);
451
453
  }
452
454
  }
453
455
  }
454
456
  } catch (error) {
455
- console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
457
+ getLogger().warn(`Failed to scan installed apps directory ${installedAppsPath}: ${error}`);
456
458
  }
457
459
  return installedApps;
458
460
  }
@@ -465,7 +467,7 @@ async function getProjectName(configBasePath) {
465
467
  return projectConfig.name || "Unnamed Project";
466
468
  }
467
469
  } catch (error) {
468
- console.warn(`Failed to read project config ${projectConfigPath}:`, error);
470
+ getLogger().warn(`Failed to read project config ${projectConfigPath}: ${error}`);
469
471
  }
470
472
  return "Unnamed Project";
471
473
  }
@@ -492,12 +494,14 @@ async function scanPlugins(projectRootPath) {
492
494
  }
493
495
  });
494
496
  } catch (error) {
495
- console.warn(`Failed to parse plugin.yaml for ${dir.name}:`, error);
497
+ getLogger().warn(`Failed to parse plugin.yaml for ${dir.name}: ${error}`);
496
498
  }
497
499
  }
498
500
  }
499
501
  } catch (error) {
500
- console.warn(`Failed to scan local plugins directory ${localPluginsPath}:`, error);
502
+ getLogger().warn(
503
+ `Failed to scan local plugins directory ${localPluginsPath}: ${error}`
504
+ );
501
505
  }
502
506
  }
503
507
  const nodeModulesPath = path.join(projectRootPath, "node_modules");
@@ -548,16 +552,15 @@ async function scanPlugins(projectRootPath) {
548
552
  }
549
553
  });
550
554
  } catch (error) {
551
- console.warn(
552
- `Failed to parse plugin.yaml for package ${pkgPath}:`,
553
- error
555
+ getLogger().warn(
556
+ `Failed to parse plugin.yaml for package ${pkgPath}: ${error}`
554
557
  );
555
558
  }
556
559
  }
557
560
  }
558
561
  }
559
562
  } catch (error) {
560
- console.warn(`Failed to scan node_modules for plugins:`, error);
563
+ getLogger().warn(`Failed to scan node_modules for plugins: ${error}`);
561
564
  }
562
565
  }
563
566
  return plugins;
@@ -585,7 +588,7 @@ async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath
585
588
  contractSchema = parsedContract;
586
589
  }
587
590
  } catch (error) {
588
- console.warn(`Failed to parse contract file ${contractPath}:`, error);
591
+ getLogger().warn(`Failed to parse contract file ${contractPath}: ${error}`);
589
592
  }
590
593
  }
591
594
  if (hasPageHtml) {
@@ -597,7 +600,7 @@ async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath
597
600
  installedAppContracts
598
601
  );
599
602
  } catch (error) {
600
- console.warn(`Failed to read page file ${pageFilePath}:`, error);
603
+ getLogger().warn(`Failed to read page file ${pageFilePath}: ${error}`);
601
604
  }
602
605
  } else if (hasPageConfig) {
603
606
  try {
@@ -698,7 +701,7 @@ async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath
698
701
  }
699
702
  }
700
703
  } catch (error) {
701
- console.warn(`Failed to parse page config ${pageConfigPath}:`, error);
704
+ getLogger().warn(`Failed to parse page config ${pageConfigPath}: ${error}`);
702
705
  }
703
706
  }
704
707
  pages.push({
@@ -731,7 +734,7 @@ async function handlePagePublish(resolvedConfig, page) {
731
734
  if (page.contract) {
732
735
  contractPath = path.join(dirname, `page${JAY_CONTRACT_EXTENSION}`);
733
736
  await fs.promises.writeFile(contractPath, page.contract, "utf-8");
734
- console.log(`📄 Published page contract: ${contractPath}`);
737
+ getLogger().info(`📄 Published page contract: ${contractPath}`);
735
738
  }
736
739
  const createdJayHtml = {
737
740
  jayHtml: page.jayHtml,
@@ -739,7 +742,7 @@ async function handlePagePublish(resolvedConfig, page) {
739
742
  dirname,
740
743
  fullPath
741
744
  };
742
- console.log(`📝 Published page: ${fullPath}`);
745
+ getLogger().info(`📝 Published page: ${fullPath}`);
743
746
  return [
744
747
  {
745
748
  success: true,
@@ -749,7 +752,7 @@ async function handlePagePublish(resolvedConfig, page) {
749
752
  createdJayHtml
750
753
  ];
751
754
  } catch (error) {
752
- console.error(`Failed to publish page ${page.route}:`, error);
755
+ getLogger().error(`Failed to publish page ${page.route}: ${error}`);
753
756
  return [
754
757
  {
755
758
  success: false,
@@ -777,7 +780,7 @@ async function handleComponentPublish(resolvedConfig, component) {
777
780
  dirname,
778
781
  fullPath
779
782
  };
780
- console.log(`🧩 Published component: ${fullPath}`);
783
+ getLogger().info(`🧩 Published component: ${fullPath}`);
781
784
  return [
782
785
  {
783
786
  success: true,
@@ -787,7 +790,7 @@ async function handleComponentPublish(resolvedConfig, component) {
787
790
  createdJayHtml
788
791
  ];
789
792
  } catch (error) {
790
- console.error(`Failed to publish component ${component.name}:`, error);
793
+ getLogger().error(`Failed to publish component ${component.name}: ${error}`);
791
794
  return [
792
795
  {
793
796
  success: false,
@@ -831,7 +834,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
831
834
  );
832
835
  const definitionFile = generateElementDefinitionFile(parsedJayHtml);
833
836
  if (definitionFile.validations.length > 0)
834
- console.log(
837
+ getLogger().warn(
835
838
  `failed to generate .d.ts for ${fullPath} with validation errors: ${definitionFile.validations.join("\n")}`
836
839
  );
837
840
  else
@@ -850,14 +853,14 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
850
853
  const filename = `${params.imageId}.png`;
851
854
  const imagePath = path.join(imagesDir, filename);
852
855
  await fs.promises.writeFile(imagePath, Buffer.from(params.imageData, "base64"));
853
- console.log(`🖼️ Saved image: ${imagePath}`);
856
+ getLogger().info(`🖼️ Saved image: ${imagePath}`);
854
857
  return {
855
858
  type: "saveImage",
856
859
  success: true,
857
860
  imageUrl: `/images/${filename}`
858
861
  };
859
862
  } catch (error) {
860
- console.error("Failed to save image:", error);
863
+ getLogger().error(`Failed to save image: ${error}`);
861
864
  return {
862
865
  type: "saveImage",
863
866
  success: false,
@@ -881,7 +884,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
881
884
  imageUrl: exists ? `/images/${filename}` : void 0
882
885
  };
883
886
  } catch (error) {
884
- console.error("Failed to check image:", error);
887
+ getLogger().error(`Failed to check image: ${error}`);
885
888
  return {
886
889
  type: "hasImage",
887
890
  success: false,
@@ -902,18 +905,18 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
902
905
  configBasePath,
903
906
  projectRootPath
904
907
  );
905
- console.log(`📋 Retrieved project info: ${info.name}`);
906
- console.log(` Pages: ${info.pages.length}`);
907
- console.log(` Components: ${info.components.length}`);
908
- console.log(` Installed Apps: ${info.installedApps.length}`);
909
- console.log(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
908
+ getLogger().info(`📋 Retrieved project info: ${info.name}`);
909
+ getLogger().info(` Pages: ${info.pages.length}`);
910
+ getLogger().info(` Components: ${info.components.length}`);
911
+ getLogger().info(` Installed Apps: ${info.installedApps.length}`);
912
+ getLogger().info(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
910
913
  return {
911
914
  type: "getProjectInfo",
912
915
  success: true,
913
916
  info
914
917
  };
915
918
  } catch (error) {
916
- console.error("Failed to get project info:", error);
919
+ getLogger().error(`Failed to get project info: ${error}`);
917
920
  return {
918
921
  type: "getProjectInfo",
919
922
  success: false,
@@ -968,16 +971,16 @@ async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
968
971
  );
969
972
  const definitionFile = generateElementDefinitionFile(parsedJayHtml);
970
973
  if (definitionFile.validations.length > 0) {
971
- console.log(
974
+ getLogger().warn(
972
975
  `failed to generate .d.ts for ${jayHtmlPath} with validation errors: ${definitionFile.validations.join("\n")}`
973
976
  );
974
977
  } else {
975
978
  const definitionFilePath2 = jayHtmlPath + ".d.ts";
976
979
  await fs.promises.writeFile(definitionFilePath2, definitionFile.val, "utf-8");
977
- console.log(`📦 Generated definition file: ${definitionFilePath2}`);
980
+ getLogger().info(`📦 Generated definition file: ${definitionFilePath2}`);
978
981
  }
979
982
  } catch (error) {
980
- console.error(`Failed to generate definition file for ${jayHtmlPath}:`, error);
983
+ getLogger().error(`Failed to generate definition file for ${jayHtmlPath}: ${error}`);
981
984
  }
982
985
  }
983
986
  }
@@ -994,11 +997,12 @@ async function startDevServer(options = {}) {
994
997
  };
995
998
  const app = express();
996
999
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
1000
+ const log = getLogger();
997
1001
  const editorServer = createEditorServer({
998
1002
  portRange: resolvedConfig.editorServer.portRange,
999
1003
  editorId: resolvedConfig.editorServer.editorId,
1000
1004
  onEditorId: (editorId2) => {
1001
- console.log(`Editor connected with ID: ${editorId2}`);
1005
+ log.info(`Editor connected with ID: ${editorId2}`);
1002
1006
  updateConfig({
1003
1007
  editorServer: {
1004
1008
  editorId: editorId2
@@ -1021,35 +1025,69 @@ async function startDevServer(options = {}) {
1021
1025
  projectRootFolder: process.cwd(),
1022
1026
  publicBaseUrlPath: "/",
1023
1027
  dontCacheSlowly: false,
1024
- jayRollupConfig: jayOptions
1028
+ jayRollupConfig: jayOptions,
1029
+ logLevel: options.logLevel
1025
1030
  });
1026
1031
  app.use(server);
1027
1032
  const publicPath = path.resolve(resolvedConfig.devServer.publicFolder);
1028
1033
  if (fs.existsSync(publicPath)) {
1029
1034
  app.use(express.static(publicPath));
1030
1035
  } else {
1031
- console.log(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
1036
+ log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
1032
1037
  }
1033
1038
  routes.forEach((route) => {
1034
1039
  app.get(route.path, route.handler);
1035
1040
  });
1036
1041
  generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
1037
1042
  const expressServer = app.listen(devServerPort, () => {
1038
- console.log(`🚀 Jay Stack dev server started successfully!`);
1039
- console.log(`📱 Dev Server: http://localhost:${devServerPort}`);
1040
- console.log(`🎨 Editor Server: http://localhost:${editorPort} (ID: ${editorId})`);
1041
- console.log(`📁 Pages directory: ${resolvedConfig.devServer.pagesBase}`);
1043
+ log.important(`🚀 Jay Stack dev server started successfully!`);
1044
+ log.important(`📱 Dev Server: http://localhost:${devServerPort}`);
1045
+ log.important(`🎨 Editor Server: http://localhost:${editorPort} (ID: ${editorId})`);
1046
+ log.important(`📁 Pages directory: ${resolvedConfig.devServer.pagesBase}`);
1042
1047
  if (fs.existsSync(publicPath)) {
1043
- console.log(`📁 Public folder: ${resolvedConfig.devServer.publicFolder}`);
1048
+ log.important(`📁 Public folder: ${resolvedConfig.devServer.publicFolder}`);
1049
+ }
1050
+ if (options.testMode) {
1051
+ log.important(`🧪 Test Mode: enabled`);
1052
+ log.important(` Health: http://localhost:${devServerPort}/_jay/health`);
1053
+ log.important(
1054
+ ` Shutdown: curl -X POST http://localhost:${devServerPort}/_jay/shutdown`
1055
+ );
1056
+ if (options.timeout) {
1057
+ log.important(` Timeout: ${options.timeout}s`);
1058
+ }
1044
1059
  }
1045
1060
  });
1046
1061
  const shutdown = async () => {
1047
- console.log("\n🛑 Shutting down servers...");
1062
+ log.important("\n🛑 Shutting down servers...");
1048
1063
  await editorServer.stop();
1049
1064
  expressServer.closeAllConnections();
1050
1065
  await new Promise((resolve) => expressServer.close(resolve));
1051
1066
  process.exit(0);
1052
1067
  };
1068
+ if (options.testMode) {
1069
+ app.get("/_jay/health", (_req, res) => {
1070
+ res.json({
1071
+ status: "ready",
1072
+ port: devServerPort,
1073
+ editorPort,
1074
+ uptime: process.uptime()
1075
+ });
1076
+ });
1077
+ app.post("/_jay/shutdown", async (_req, res) => {
1078
+ res.json({ status: "shutting_down" });
1079
+ setTimeout(async () => {
1080
+ await shutdown();
1081
+ }, 100);
1082
+ });
1083
+ if (options.timeout) {
1084
+ setTimeout(async () => {
1085
+ log.important(`
1086
+ ⏰ Timeout (${options.timeout}s) reached, shutting down...`);
1087
+ await shutdown();
1088
+ }, options.timeout * 1e3);
1089
+ }
1090
+ }
1053
1091
  process.on("SIGTERM", shutdown);
1054
1092
  process.on("SIGINT", shutdown);
1055
1093
  }
@@ -1207,29 +1245,33 @@ async function validateSchema(context, result) {
1207
1245
  }
1208
1246
  }
1209
1247
  if (manifest.dynamic_contracts) {
1210
- if (!manifest.dynamic_contracts.component) {
1211
- result.errors.push({
1212
- type: "schema",
1213
- message: 'dynamic_contracts is missing "component" field',
1214
- location: "plugin.yaml",
1215
- suggestion: "Specify path to shared component for dynamic contracts"
1216
- });
1217
- }
1218
- if (!manifest.dynamic_contracts.generator) {
1219
- result.errors.push({
1220
- type: "schema",
1221
- message: 'dynamic_contracts is missing "generator" field',
1222
- location: "plugin.yaml",
1223
- suggestion: "Specify path to generator file"
1224
- });
1225
- }
1226
- if (!manifest.dynamic_contracts.prefix) {
1227
- result.errors.push({
1228
- type: "schema",
1229
- message: 'dynamic_contracts is missing "prefix" field',
1230
- location: "plugin.yaml",
1231
- suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
1232
- });
1248
+ const dynamicConfigs = Array.isArray(manifest.dynamic_contracts) ? manifest.dynamic_contracts : [manifest.dynamic_contracts];
1249
+ for (const config of dynamicConfigs) {
1250
+ const prefix = config.prefix || "(unknown)";
1251
+ if (!config.component) {
1252
+ result.errors.push({
1253
+ type: "schema",
1254
+ message: `dynamic_contracts[${prefix}] is missing "component" field`,
1255
+ location: "plugin.yaml",
1256
+ suggestion: "Specify path to shared component for dynamic contracts"
1257
+ });
1258
+ }
1259
+ if (!config.generator) {
1260
+ result.errors.push({
1261
+ type: "schema",
1262
+ message: `dynamic_contracts[${prefix}] is missing "generator" field`,
1263
+ location: "plugin.yaml",
1264
+ suggestion: "Specify path to generator file or export name"
1265
+ });
1266
+ }
1267
+ if (!config.prefix) {
1268
+ result.errors.push({
1269
+ type: "schema",
1270
+ message: 'dynamic_contracts entry is missing "prefix" field',
1271
+ location: "plugin.yaml",
1272
+ suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
1273
+ });
1274
+ }
1233
1275
  }
1234
1276
  }
1235
1277
  if (!manifest.contracts && !manifest.dynamic_contracts) {
@@ -1420,54 +1462,549 @@ async function validateDynamicContracts(context, result) {
1420
1462
  const { dynamic_contracts } = context.manifest;
1421
1463
  if (!dynamic_contracts)
1422
1464
  return;
1423
- if (dynamic_contracts.generator) {
1424
- const generatorPath = path.join(context.pluginPath, dynamic_contracts.generator);
1425
- const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
1426
- let found = false;
1427
- for (const ext of possibleExtensions) {
1428
- if (fs.existsSync(generatorPath + ext)) {
1429
- found = true;
1430
- break;
1465
+ const dynamicConfigs = Array.isArray(dynamic_contracts) ? dynamic_contracts : [dynamic_contracts];
1466
+ for (const config of dynamicConfigs) {
1467
+ const prefix = config.prefix || "(unknown)";
1468
+ if (config.generator) {
1469
+ const isFilePath = config.generator.startsWith("./") || config.generator.startsWith("/") || config.generator.includes(".ts") || config.generator.includes(".js");
1470
+ if (isFilePath) {
1471
+ const generatorPath = path.join(context.pluginPath, config.generator);
1472
+ const possibleExtensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
1473
+ let found = false;
1474
+ for (const ext of possibleExtensions) {
1475
+ if (fs.existsSync(generatorPath + ext)) {
1476
+ found = true;
1477
+ break;
1478
+ }
1479
+ }
1480
+ if (!found && !context.isNpmPackage) {
1481
+ result.errors.push({
1482
+ type: "file-missing",
1483
+ message: `Generator file not found for ${prefix}: ${config.generator}`,
1484
+ location: "plugin.yaml dynamic_contracts",
1485
+ suggestion: `Create generator file at ${generatorPath}.ts`
1486
+ });
1487
+ }
1431
1488
  }
1432
1489
  }
1433
- if (!found && !context.isNpmPackage) {
1434
- result.errors.push({
1435
- type: "file-missing",
1436
- message: `Generator file not found: ${dynamic_contracts.generator}`,
1437
- location: "plugin.yaml dynamic_contracts",
1438
- suggestion: `Create generator file at ${generatorPath}.ts`
1439
- });
1490
+ if (config.component) {
1491
+ const isFilePath = config.component.startsWith("./") || config.component.startsWith("/") || config.component.includes(".ts") || config.component.includes(".js");
1492
+ if (isFilePath) {
1493
+ const componentPath = path.join(context.pluginPath, config.component);
1494
+ const possibleExtensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
1495
+ let found = false;
1496
+ for (const ext of possibleExtensions) {
1497
+ if (fs.existsSync(componentPath + ext)) {
1498
+ found = true;
1499
+ break;
1500
+ }
1501
+ }
1502
+ if (!found && !context.isNpmPackage) {
1503
+ result.errors.push({
1504
+ type: "file-missing",
1505
+ message: `Dynamic contracts component not found for ${prefix}: ${config.component}`,
1506
+ location: "plugin.yaml dynamic_contracts",
1507
+ suggestion: `Create component file at ${componentPath}.ts`
1508
+ });
1509
+ }
1510
+ }
1440
1511
  }
1441
1512
  }
1442
- if (dynamic_contracts.component) {
1443
- const componentPath = path.join(context.pluginPath, dynamic_contracts.component);
1444
- const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
1445
- let found = false;
1446
- for (const ext of possibleExtensions) {
1447
- if (fs.existsSync(componentPath + ext)) {
1448
- found = true;
1449
- break;
1513
+ }
1514
+ async function findJayFiles(dir) {
1515
+ return await glob(`${dir}/**/*${JAY_EXTENSION}`);
1516
+ }
1517
+ async function findContractFiles(dir) {
1518
+ return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
1519
+ }
1520
+ async function validateJayFiles(options = {}) {
1521
+ const config = loadConfig();
1522
+ const resolvedConfig = getConfigWithDefaults(config);
1523
+ const projectRoot = process.cwd();
1524
+ const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
1525
+ const errors = [];
1526
+ const warnings = [];
1527
+ const jayHtmlFiles = await findJayFiles(scanDir);
1528
+ const contractFiles = await findContractFiles(scanDir);
1529
+ if (options.verbose) {
1530
+ getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
1531
+ getLogger().info(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
1532
+ getLogger().info(chalk.gray(`Found ${contractFiles.length} .jay-contract files
1533
+ `));
1534
+ }
1535
+ for (const contractFile of contractFiles) {
1536
+ const relativePath = path.relative(projectRoot, contractFile);
1537
+ try {
1538
+ const content = await promises.readFile(contractFile, "utf-8");
1539
+ const result = parseContract(content, path.basename(contractFile));
1540
+ if (result.validations.length > 0) {
1541
+ for (const validation of result.validations) {
1542
+ errors.push({
1543
+ file: relativePath,
1544
+ message: validation,
1545
+ stage: "parse"
1546
+ });
1547
+ }
1548
+ if (options.verbose) {
1549
+ getLogger().info(chalk.red(`❌ ${relativePath}`));
1550
+ }
1551
+ } else if (options.verbose) {
1552
+ getLogger().info(chalk.green(`✓ ${relativePath}`));
1553
+ }
1554
+ } catch (error) {
1555
+ errors.push({
1556
+ file: relativePath,
1557
+ message: error.message,
1558
+ stage: "parse"
1559
+ });
1560
+ if (options.verbose) {
1561
+ getLogger().info(chalk.red(`❌ ${relativePath}`));
1450
1562
  }
1451
1563
  }
1452
- if (!found && !context.isNpmPackage) {
1453
- result.errors.push({
1454
- type: "file-missing",
1455
- message: `Dynamic contracts component not found: ${dynamic_contracts.component}`,
1456
- location: "plugin.yaml dynamic_contracts",
1457
- suggestion: `Create component file at ${componentPath}.ts`
1564
+ }
1565
+ for (const jayFile of jayHtmlFiles) {
1566
+ const relativePath = path.relative(projectRoot, jayFile);
1567
+ const filename = path.basename(jayFile.replace(JAY_EXTENSION, ""));
1568
+ const dirname = path.dirname(jayFile);
1569
+ try {
1570
+ const content = await promises.readFile(jayFile, "utf-8");
1571
+ const parsedFile = await parseJayFile(
1572
+ content,
1573
+ filename,
1574
+ dirname,
1575
+ {},
1576
+ JAY_IMPORT_RESOLVER,
1577
+ projectRoot
1578
+ );
1579
+ if (parsedFile.validations.length > 0) {
1580
+ for (const validation of parsedFile.validations) {
1581
+ errors.push({
1582
+ file: relativePath,
1583
+ message: validation,
1584
+ stage: "parse"
1585
+ });
1586
+ }
1587
+ if (options.verbose) {
1588
+ getLogger().info(chalk.red(`❌ ${relativePath}`));
1589
+ }
1590
+ continue;
1591
+ }
1592
+ const generatedFile = generateElementFile(
1593
+ parsedFile.val,
1594
+ RuntimeMode.MainTrusted,
1595
+ GenerateTarget.jay
1596
+ );
1597
+ if (generatedFile.validations.length > 0) {
1598
+ for (const validation of generatedFile.validations) {
1599
+ errors.push({
1600
+ file: relativePath,
1601
+ message: validation,
1602
+ stage: "generate"
1603
+ });
1604
+ }
1605
+ if (options.verbose) {
1606
+ getLogger().info(chalk.red(`❌ ${relativePath}`));
1607
+ }
1608
+ } else if (options.verbose) {
1609
+ getLogger().info(chalk.green(`✓ ${relativePath}`));
1610
+ }
1611
+ } catch (error) {
1612
+ errors.push({
1613
+ file: relativePath,
1614
+ message: error.message,
1615
+ stage: "parse"
1458
1616
  });
1617
+ if (options.verbose) {
1618
+ getLogger().info(chalk.red(`❌ ${relativePath}`));
1619
+ }
1620
+ }
1621
+ }
1622
+ return {
1623
+ valid: errors.length === 0,
1624
+ jayHtmlFilesScanned: jayHtmlFiles.length,
1625
+ contractFilesScanned: contractFiles.length,
1626
+ errors,
1627
+ warnings
1628
+ };
1629
+ }
1630
+ function printJayValidationResult(result, options) {
1631
+ const logger = getLogger();
1632
+ if (options.json) {
1633
+ logger.important(JSON.stringify(result, null, 2));
1634
+ return;
1635
+ }
1636
+ logger.important("");
1637
+ if (result.valid) {
1638
+ logger.important(chalk.green("✅ Jay Stack validation successful!\n"));
1639
+ logger.important(
1640
+ `Scanned ${result.jayHtmlFilesScanned} .jay-html files, ${result.contractFilesScanned} .jay-contract files`
1641
+ );
1642
+ logger.important("No errors found.");
1643
+ } else {
1644
+ logger.important(chalk.red("❌ Jay Stack validation failed\n"));
1645
+ logger.important("Errors:");
1646
+ for (const error of result.errors) {
1647
+ logger.important(chalk.red(` ❌ ${error.file}`));
1648
+ logger.important(chalk.gray(` ${error.message}`));
1649
+ logger.important("");
1650
+ }
1651
+ const validFiles = result.jayHtmlFilesScanned + result.contractFilesScanned - result.errors.length;
1652
+ logger.important(
1653
+ chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
1654
+ );
1655
+ }
1656
+ }
1657
+ async function initializeServicesForCli(projectRoot, viteServer) {
1658
+ const path2 = await import("node:path");
1659
+ const fs2 = await import("node:fs");
1660
+ const {
1661
+ runInitCallbacks,
1662
+ getServiceRegistry,
1663
+ discoverPluginsWithInit,
1664
+ sortPluginsByDependencies,
1665
+ executePluginServerInits
1666
+ } = await import("@jay-framework/stack-server-runtime");
1667
+ try {
1668
+ const discoveredPlugins = await discoverPluginsWithInit({
1669
+ projectRoot,
1670
+ verbose: false
1671
+ });
1672
+ const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
1673
+ try {
1674
+ await executePluginServerInits(pluginsWithInit, viteServer, false);
1675
+ } catch (error) {
1676
+ getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
1677
+ }
1678
+ const initPathTs = path2.join(projectRoot, "src", "init.ts");
1679
+ const initPathJs = path2.join(projectRoot, "src", "init.js");
1680
+ let initModule;
1681
+ if (fs2.existsSync(initPathTs) && viteServer) {
1682
+ initModule = await viteServer.ssrLoadModule(initPathTs);
1683
+ } else if (fs2.existsSync(initPathJs)) {
1684
+ initModule = await import(initPathJs);
1685
+ }
1686
+ if (initModule?.init?._serverInit) {
1687
+ await initModule.init._serverInit();
1688
+ }
1689
+ await runInitCallbacks();
1690
+ } catch (error) {
1691
+ getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
1692
+ getLogger().warn(chalk.gray(" Static contracts will still be listed."));
1693
+ }
1694
+ return getServiceRegistry();
1695
+ }
1696
+ async function runAction(actionRef, options, projectRoot, initializeServices) {
1697
+ let viteServer;
1698
+ try {
1699
+ const slashIndex = actionRef.indexOf("/");
1700
+ if (slashIndex === -1) {
1701
+ getLogger().error(
1702
+ chalk.red("❌ Invalid action reference. Use format: <plugin>/<action>")
1703
+ );
1704
+ process.exit(1);
1705
+ }
1706
+ const actionExport = actionRef.substring(slashIndex + 1);
1707
+ const input = options.input ? JSON.parse(options.input) : {};
1708
+ if (options.verbose) {
1709
+ getLogger().info("Starting Vite for TypeScript support...");
1710
+ }
1711
+ viteServer = await createViteForCli({ projectRoot });
1712
+ await initializeServices(projectRoot, viteServer);
1713
+ const { discoverAndRegisterActions, discoverAllPluginActions, ActionRegistry } = await import("@jay-framework/stack-server-runtime");
1714
+ const registry = new ActionRegistry();
1715
+ await discoverAndRegisterActions({
1716
+ projectRoot,
1717
+ registry,
1718
+ verbose: options.verbose,
1719
+ viteServer
1720
+ });
1721
+ await discoverAllPluginActions({
1722
+ projectRoot,
1723
+ registry,
1724
+ verbose: options.verbose,
1725
+ viteServer
1726
+ });
1727
+ const allNames = registry.getNames();
1728
+ const matchedName = allNames.find((name) => name === actionExport) || allNames.find((name) => name.endsWith("." + actionExport)) || allNames.find((name) => name === actionRef);
1729
+ if (!matchedName) {
1730
+ getLogger().error(chalk.red(`❌ Action "${actionExport}" not found.`));
1731
+ if (allNames.length > 0) {
1732
+ getLogger().error(` Available actions: ${allNames.join(", ")}`);
1733
+ } else {
1734
+ getLogger().error(" No actions registered. Does the plugin have actions?");
1735
+ }
1736
+ process.exit(1);
1737
+ }
1738
+ if (options.verbose) {
1739
+ getLogger().info(`Executing action: ${matchedName}`);
1740
+ getLogger().info(`Input: ${JSON.stringify(input)}`);
1741
+ }
1742
+ const result = await registry.execute(matchedName, input);
1743
+ if (result.success) {
1744
+ if (options.yaml) {
1745
+ getLogger().important(YAML.stringify(result.data));
1746
+ } else {
1747
+ getLogger().important(JSON.stringify(result.data, null, 2));
1748
+ }
1749
+ } else {
1750
+ getLogger().error(
1751
+ chalk.red(`❌ Action failed: [${result.error.code}] ${result.error.message}`)
1752
+ );
1753
+ process.exit(1);
1754
+ }
1755
+ } catch (error) {
1756
+ getLogger().error(chalk.red("❌ Failed to run action:") + " " + error.message);
1757
+ if (options.verbose) {
1758
+ getLogger().error(error.stack);
1759
+ }
1760
+ process.exit(1);
1761
+ } finally {
1762
+ if (viteServer) {
1763
+ await viteServer.close();
1764
+ }
1765
+ }
1766
+ }
1767
+ async function runParams(contractRef, options, projectRoot, initializeServices) {
1768
+ let viteServer;
1769
+ try {
1770
+ const slashIndex = contractRef.indexOf("/");
1771
+ if (slashIndex === -1) {
1772
+ getLogger().error(
1773
+ chalk.red("❌ Invalid contract reference. Use format: <plugin>/<contract>")
1774
+ );
1775
+ process.exit(1);
1776
+ }
1777
+ const pluginName = contractRef.substring(0, slashIndex);
1778
+ const contractName = contractRef.substring(slashIndex + 1);
1779
+ if (options.verbose) {
1780
+ getLogger().info("Starting Vite for TypeScript support...");
1781
+ }
1782
+ viteServer = await createViteForCli({ projectRoot });
1783
+ await initializeServices(projectRoot, viteServer);
1784
+ const { resolvePluginComponent } = await import("@jay-framework/compiler-shared");
1785
+ const resolution = resolvePluginComponent(projectRoot, pluginName, contractName);
1786
+ if (resolution.validations.length > 0 || !resolution.val) {
1787
+ getLogger().error(
1788
+ chalk.red(`❌ Could not resolve plugin "${pluginName}" contract "${contractName}"`)
1789
+ );
1790
+ for (const msg of resolution.validations) {
1791
+ getLogger().error(` ${msg}`);
1792
+ }
1793
+ process.exit(1);
1794
+ }
1795
+ const componentPath = resolution.val.componentPath;
1796
+ const componentName = resolution.val.componentName;
1797
+ if (options.verbose) {
1798
+ getLogger().info(`Loading component "${componentName}" from ${componentPath}`);
1799
+ }
1800
+ const module = await viteServer.ssrLoadModule(componentPath);
1801
+ const component = module[componentName];
1802
+ if (!component || !component.loadParams) {
1803
+ getLogger().error(
1804
+ chalk.red(`❌ Component "${componentName}" does not have loadParams.`)
1805
+ );
1806
+ getLogger().error(
1807
+ " Only components with withLoadParams() expose discoverable URL params."
1808
+ );
1809
+ process.exit(1);
1810
+ }
1811
+ const { resolveServices } = await import("@jay-framework/stack-server-runtime");
1812
+ const resolvedServices = resolveServices(component.services || []);
1813
+ const allParams = [];
1814
+ const paramsGenerator = component.loadParams(resolvedServices);
1815
+ for await (const batch of paramsGenerator) {
1816
+ allParams.push(...batch);
1817
+ }
1818
+ if (options.yaml) {
1819
+ getLogger().important(YAML.stringify(allParams));
1820
+ } else {
1821
+ getLogger().important(JSON.stringify(allParams, null, 2));
1822
+ }
1823
+ if (!options.yaml) {
1824
+ getLogger().important(
1825
+ chalk.green(`
1826
+ ✅ Found ${allParams.length} param combination(s)`)
1827
+ );
1828
+ }
1829
+ } catch (error) {
1830
+ getLogger().error(chalk.red("❌ Failed to discover params:") + " " + error.message);
1831
+ if (options.verbose) {
1832
+ getLogger().error(error.stack);
1833
+ }
1834
+ process.exit(1);
1835
+ } finally {
1836
+ if (viteServer) {
1837
+ await viteServer.close();
1838
+ }
1839
+ }
1840
+ }
1841
+ async function runSetup(pluginFilter, options, projectRoot, initializeServices) {
1842
+ let viteServer;
1843
+ try {
1844
+ const logger = getLogger();
1845
+ const path2 = await import("node:path");
1846
+ const jayConfig = loadConfig();
1847
+ const configDir = path2.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
1848
+ logger.important(chalk.bold("\n🔧 Setting up plugins...\n"));
1849
+ if (options.verbose) {
1850
+ logger.info("Starting Vite for TypeScript support...");
1851
+ }
1852
+ viteServer = await createViteForCli({ projectRoot });
1853
+ const { discoverPluginsWithSetup, executePluginSetup } = await import("@jay-framework/stack-server-runtime");
1854
+ const pluginsWithSetup = await discoverPluginsWithSetup({
1855
+ projectRoot,
1856
+ verbose: options.verbose,
1857
+ pluginFilter
1858
+ });
1859
+ if (pluginsWithSetup.length === 0) {
1860
+ if (pluginFilter) {
1861
+ logger.important(
1862
+ chalk.yellow(
1863
+ `⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`
1864
+ )
1865
+ );
1866
+ } else {
1867
+ logger.important(chalk.gray("No plugins with setup handlers found."));
1868
+ }
1869
+ return;
1870
+ }
1871
+ if (options.verbose) {
1872
+ logger.info(
1873
+ `Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
1874
+ );
1875
+ }
1876
+ let initError;
1877
+ try {
1878
+ await initializeServices(projectRoot, viteServer);
1879
+ } catch (error) {
1880
+ initError = error;
1881
+ if (options.verbose) {
1882
+ logger.info(chalk.yellow(`⚠️ Service init error: ${error.message}`));
1883
+ }
1884
+ }
1885
+ let configured = 0;
1886
+ let needsConfig = 0;
1887
+ let errors = 0;
1888
+ for (const plugin of pluginsWithSetup) {
1889
+ logger.important(chalk.bold(`📦 ${plugin.name}`));
1890
+ if (plugin.setupDescription && options.verbose) {
1891
+ logger.important(chalk.gray(` ${plugin.setupDescription}`));
1892
+ }
1893
+ try {
1894
+ const result = await executePluginSetup(plugin, {
1895
+ projectRoot,
1896
+ configDir,
1897
+ force: options.force ?? false,
1898
+ initError,
1899
+ viteServer,
1900
+ verbose: options.verbose
1901
+ });
1902
+ switch (result.status) {
1903
+ case "configured":
1904
+ configured++;
1905
+ logger.important(chalk.green(" ✅ Services verified"));
1906
+ if (result.configCreated?.length) {
1907
+ for (const cfg of result.configCreated) {
1908
+ logger.important(chalk.green(` ✅ Created ${cfg}`));
1909
+ }
1910
+ }
1911
+ if (result.message) {
1912
+ logger.important(chalk.gray(` ${result.message}`));
1913
+ }
1914
+ break;
1915
+ case "needs-config":
1916
+ needsConfig++;
1917
+ if (result.configCreated?.length) {
1918
+ for (const cfg of result.configCreated) {
1919
+ logger.important(chalk.yellow(` ⚠️ Config template created: ${cfg}`));
1920
+ }
1921
+ }
1922
+ if (result.message) {
1923
+ logger.important(chalk.yellow(` → ${result.message}`));
1924
+ } else {
1925
+ logger.important(
1926
+ chalk.yellow(
1927
+ ` → Fill in credentials and re-run: jay-stack setup ${plugin.name}`
1928
+ )
1929
+ );
1930
+ }
1931
+ break;
1932
+ case "error":
1933
+ errors++;
1934
+ logger.important(
1935
+ chalk.red(` ❌ ${result.message || "Setup failed"}`)
1936
+ );
1937
+ break;
1938
+ }
1939
+ } catch (error) {
1940
+ errors++;
1941
+ logger.important(chalk.red(` ❌ Setup failed: ${error.message}`));
1942
+ if (options.verbose) {
1943
+ logger.error(error.stack);
1944
+ }
1945
+ }
1946
+ logger.important("");
1947
+ }
1948
+ const parts = [];
1949
+ if (configured > 0)
1950
+ parts.push(`${configured} configured`);
1951
+ if (needsConfig > 0)
1952
+ parts.push(`${needsConfig} needs config`);
1953
+ if (errors > 0)
1954
+ parts.push(`${errors} error(s)`);
1955
+ logger.important(`Setup complete: ${parts.join(", ")}`);
1956
+ if (errors > 0) {
1957
+ process.exit(1);
1958
+ }
1959
+ } catch (error) {
1960
+ getLogger().error(chalk.red("❌ Setup failed:") + " " + error.message);
1961
+ if (options.verbose) {
1962
+ getLogger().error(error.stack);
1963
+ }
1964
+ process.exit(1);
1965
+ } finally {
1966
+ if (viteServer) {
1967
+ await viteServer.close();
1459
1968
  }
1460
1969
  }
1461
1970
  }
1462
1971
  const program = new Command();
1463
1972
  program.name("jay-stack").description("Jay Stack CLI - Development server and plugin validation").version("0.9.0");
1464
- program.command("dev [path]").description("Start the Jay Stack development server").action(async (path2, options) => {
1973
+ program.command("dev [path]").description("Start the Jay Stack development server").option("-v, --verbose", "Enable verbose logging output").option("-q, --quiet", "Suppress all non-error output").option("--test-mode", "Enable test endpoints (/_jay/health, /_jay/shutdown)").option("--timeout <seconds>", "Auto-shutdown after N seconds (implies --test-mode)", parseInt).action(async (path2, options) => {
1465
1974
  try {
1975
+ const logLevel = options.quiet ? "silent" : options.verbose ? "verbose" : "info";
1976
+ setDevLogger(createDevLogger(logLevel));
1977
+ const testMode = options.testMode || options.timeout !== void 0;
1466
1978
  await startDevServer({
1467
- projectPath: path2 || process.cwd()
1979
+ projectPath: path2 || process.cwd(),
1980
+ testMode,
1981
+ timeout: options.timeout,
1982
+ logLevel
1468
1983
  });
1469
1984
  } catch (error) {
1470
- console.error(chalk.red("Error starting dev server:"), error.message);
1985
+ getLogger().error(chalk.red("Error starting dev server:") + " " + error.message);
1986
+ process.exit(1);
1987
+ }
1988
+ });
1989
+ program.command("validate [path]").description("Validate all .jay-html and .jay-contract files in the project").option("-v, --verbose", "Show per-file validation status").option("--json", "Output results as JSON").action(async (scanPath, options) => {
1990
+ try {
1991
+ const result = await validateJayFiles({
1992
+ path: scanPath,
1993
+ verbose: options.verbose,
1994
+ json: options.json
1995
+ });
1996
+ printJayValidationResult(result, options);
1997
+ if (!result.valid) {
1998
+ process.exit(1);
1999
+ }
2000
+ } catch (error) {
2001
+ if (options.json) {
2002
+ getLogger().important(
2003
+ JSON.stringify({ valid: false, error: error.message }, null, 2)
2004
+ );
2005
+ } else {
2006
+ getLogger().error(chalk.red("Validation error:") + " " + error.message);
2007
+ }
1471
2008
  process.exit(1);
1472
2009
  }
1473
2010
  });
@@ -1485,64 +2022,238 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
1485
2022
  process.exit(1);
1486
2023
  }
1487
2024
  } catch (error) {
1488
- console.error(chalk.red("Validation error:"), error.message);
2025
+ getLogger().error(chalk.red("Validation error:") + " " + error.message);
1489
2026
  process.exit(1);
1490
2027
  }
1491
2028
  });
2029
+ async function ensureAgentKitDocs(projectRoot, force) {
2030
+ const path2 = await import("node:path");
2031
+ const fs2 = await import("node:fs/promises");
2032
+ const { fileURLToPath } = await import("node:url");
2033
+ const agentKitDir = path2.join(projectRoot, "agent-kit");
2034
+ await fs2.mkdir(agentKitDir, { recursive: true });
2035
+ const thisDir = path2.dirname(fileURLToPath(import.meta.url));
2036
+ const templateDir = path2.resolve(thisDir, "..", "agent-kit-template");
2037
+ let files;
2038
+ try {
2039
+ files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
2040
+ } catch {
2041
+ getLogger().warn(
2042
+ chalk.yellow(" Agent-kit template folder not found: " + templateDir)
2043
+ );
2044
+ return;
2045
+ }
2046
+ for (const filename of files) {
2047
+ const destPath = path2.join(agentKitDir, filename);
2048
+ if (!force) {
2049
+ try {
2050
+ await fs2.access(destPath);
2051
+ continue;
2052
+ } catch {
2053
+ }
2054
+ }
2055
+ await fs2.copyFile(path2.join(templateDir, filename), destPath);
2056
+ getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
2057
+ }
2058
+ }
2059
+ async function generatePluginReferences(projectRoot, options) {
2060
+ const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
2061
+ const plugins = await discoverPluginsWithReferences({
2062
+ projectRoot,
2063
+ verbose: options.verbose,
2064
+ pluginFilter: options.plugin
2065
+ });
2066
+ if (plugins.length === 0)
2067
+ return;
2068
+ const logger = getLogger();
2069
+ logger.important("");
2070
+ logger.important(chalk.bold("📚 Generating plugin references..."));
2071
+ for (const plugin of plugins) {
2072
+ try {
2073
+ const result = await executePluginReferences(plugin, {
2074
+ projectRoot,
2075
+ force: options.force ?? false,
2076
+ verbose: options.verbose
2077
+ });
2078
+ if (result.referencesCreated.length > 0) {
2079
+ logger.important(chalk.green(` ✅ ${plugin.name}:`));
2080
+ for (const ref of result.referencesCreated) {
2081
+ logger.important(chalk.gray(` ${ref}`));
2082
+ }
2083
+ if (result.message) {
2084
+ logger.important(chalk.gray(` ${result.message}`));
2085
+ }
2086
+ }
2087
+ } catch (error) {
2088
+ logger.warn(
2089
+ chalk.yellow(` ⚠️ ${plugin.name}: references skipped — ${error.message}`)
2090
+ );
2091
+ }
2092
+ }
2093
+ }
2094
+ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2095
+ const path2 = await import("node:path");
2096
+ const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
2097
+ let viteServer;
2098
+ try {
2099
+ if (options.list) {
2100
+ const index = await listContracts({
2101
+ projectRoot,
2102
+ dynamicOnly: options.dynamicOnly,
2103
+ pluginFilter: options.plugin
2104
+ });
2105
+ if (options.yaml) {
2106
+ getLogger().important(YAML.stringify(index));
2107
+ } else {
2108
+ printContractList(index);
2109
+ }
2110
+ return;
2111
+ }
2112
+ if (options.verbose) {
2113
+ getLogger().info("Starting Vite for TypeScript support...");
2114
+ }
2115
+ viteServer = await createViteForCli({ projectRoot });
2116
+ const services = await initializeServicesForCli(projectRoot, viteServer);
2117
+ const result = await materializeContracts(
2118
+ {
2119
+ projectRoot,
2120
+ outputDir,
2121
+ force: options.force,
2122
+ dynamicOnly: options.dynamicOnly,
2123
+ pluginFilter: options.plugin,
2124
+ verbose: options.verbose,
2125
+ viteServer
2126
+ },
2127
+ services
2128
+ );
2129
+ if (options.yaml) {
2130
+ getLogger().important(YAML.stringify(result.index));
2131
+ } else {
2132
+ getLogger().important(
2133
+ chalk.green(`
2134
+ ✅ Materialized ${result.index.contracts.length} contracts`)
2135
+ );
2136
+ getLogger().important(` Static: ${result.staticCount}`);
2137
+ getLogger().important(` Dynamic: ${result.dynamicCount}`);
2138
+ getLogger().important(` Output: ${result.outputDir}`);
2139
+ }
2140
+ } catch (error) {
2141
+ getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
2142
+ if (options.verbose) {
2143
+ getLogger().error(error.stack);
2144
+ }
2145
+ process.exit(1);
2146
+ } finally {
2147
+ if (viteServer) {
2148
+ await viteServer.close();
2149
+ }
2150
+ }
2151
+ }
2152
+ program.command("setup [plugin]").description(
2153
+ "Run plugin setup: create config templates, validate credentials, generate reference data"
2154
+ ).option("--force", "Force re-run (overwrite config templates and regenerate references)").option("-v, --verbose", "Show detailed output").action(async (plugin, options) => {
2155
+ await runSetup(plugin, options, process.cwd(), initializeServicesForCli);
2156
+ });
2157
+ program.command("agent-kit").description(
2158
+ "Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
2159
+ ).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) => {
2160
+ const projectRoot = process.cwd();
2161
+ await runMaterialize(projectRoot, options, "agent-kit/materialized-contracts");
2162
+ if (!options.list) {
2163
+ await ensureAgentKitDocs(projectRoot, options.force);
2164
+ if (options.references !== false) {
2165
+ await generatePluginReferences(projectRoot, options);
2166
+ }
2167
+ }
2168
+ });
2169
+ program.command("action <plugin/action>").description(
2170
+ `Run a plugin action (e.g., jay-stack action wix-stores/searchProducts --input '{"query":""}')`
2171
+ ).option("--input <json>", "JSON input for the action (default: {})").option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (actionRef, options) => {
2172
+ await runAction(actionRef, options, process.cwd(), initializeServicesForCli);
2173
+ });
2174
+ program.command("params <plugin/contract>").description(
2175
+ "Discover load param values for a contract (e.g., jay-stack params wix-stores/product-page)"
2176
+ ).option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
2177
+ await runParams(contractRef, options, process.cwd(), initializeServicesForCli);
2178
+ });
1492
2179
  program.parse(process.argv);
1493
2180
  if (!process.argv.slice(2).length) {
1494
2181
  program.outputHelp();
1495
2182
  }
1496
2183
  function printValidationResult(result, verbose) {
2184
+ const logger = getLogger();
1497
2185
  if (result.valid && result.warnings.length === 0) {
1498
- console.log(chalk.green("✅ Plugin validation successful!\n"));
2186
+ logger.important(chalk.green("✅ Plugin validation successful!\n"));
1499
2187
  if (verbose) {
1500
- console.log("Plugin:", result.pluginName);
1501
- console.log(" ✅ plugin.yaml valid");
1502
- console.log(` ✅ ${result.contractsChecked} contracts validated`);
2188
+ logger.important("Plugin: " + result.pluginName);
2189
+ logger.important(" ✅ plugin.yaml valid");
2190
+ logger.important(` ✅ ${result.contractsChecked} contracts validated`);
1503
2191
  if (result.typesGenerated) {
1504
- console.log(` ✅ ${result.typesGenerated} type definitions generated`);
2192
+ logger.important(` ✅ ${result.typesGenerated} type definitions generated`);
1505
2193
  }
1506
- console.log(` ✅ ${result.componentsChecked} components validated`);
2194
+ logger.important(` ✅ ${result.componentsChecked} components validated`);
1507
2195
  if (result.packageJsonChecked) {
1508
- console.log(" ✅ package.json valid");
2196
+ logger.important(" ✅ package.json valid");
1509
2197
  }
1510
- console.log("\nNo errors found.");
2198
+ logger.important("\nNo errors found.");
1511
2199
  }
1512
2200
  } else if (result.valid && result.warnings.length > 0) {
1513
- console.log(chalk.yellow("⚠️ Plugin validation passed with warnings\n"));
1514
- console.log("Warnings:");
2201
+ logger.important(chalk.yellow("⚠️ Plugin validation passed with warnings\n"));
2202
+ logger.important("Warnings:");
1515
2203
  result.warnings.forEach((warning) => {
1516
- console.log(chalk.yellow(` ⚠️ ${warning.message}`));
2204
+ logger.important(chalk.yellow(` ⚠️ ${warning.message}`));
1517
2205
  if (warning.location) {
1518
- console.log(chalk.gray(` Location: ${warning.location}`));
2206
+ logger.important(chalk.gray(` Location: ${warning.location}`));
1519
2207
  }
1520
2208
  if (warning.suggestion) {
1521
- console.log(chalk.gray(` → ${warning.suggestion}`));
2209
+ logger.important(chalk.gray(` → ${warning.suggestion}`));
1522
2210
  }
1523
- console.log();
2211
+ logger.important("");
1524
2212
  });
1525
- console.log(chalk.gray("Use --strict to treat warnings as errors."));
2213
+ logger.important(chalk.gray("Use --strict to treat warnings as errors."));
1526
2214
  } else {
1527
- console.log(chalk.red("❌ Plugin validation failed\n"));
1528
- console.log("Errors:");
2215
+ logger.important(chalk.red("❌ Plugin validation failed\n"));
2216
+ logger.important("Errors:");
1529
2217
  result.errors.forEach((error) => {
1530
- console.log(chalk.red(` ❌ ${error.message}`));
2218
+ logger.important(chalk.red(` ❌ ${error.message}`));
1531
2219
  if (error.location) {
1532
- console.log(chalk.gray(` Location: ${error.location}`));
2220
+ logger.important(chalk.gray(` Location: ${error.location}`));
1533
2221
  }
1534
2222
  if (error.suggestion) {
1535
- console.log(chalk.gray(` → ${error.suggestion}`));
2223
+ logger.important(chalk.gray(` → ${error.suggestion}`));
1536
2224
  }
1537
- console.log();
2225
+ logger.important("");
1538
2226
  });
1539
- console.log(chalk.red(`${result.errors.length} errors found.`));
2227
+ logger.important(chalk.red(`${result.errors.length} errors found.`));
2228
+ }
2229
+ }
2230
+ function printContractList(index) {
2231
+ const logger = getLogger();
2232
+ logger.important("\nAvailable Contracts:\n");
2233
+ const byPlugin = /* @__PURE__ */ new Map();
2234
+ for (const contract of index.contracts) {
2235
+ const existing = byPlugin.get(contract.plugin) || [];
2236
+ existing.push(contract);
2237
+ byPlugin.set(contract.plugin, existing);
2238
+ }
2239
+ for (const [plugin, contracts] of byPlugin) {
2240
+ logger.important(chalk.bold(`📦 ${plugin}`));
2241
+ for (const contract of contracts) {
2242
+ const typeIcon = contract.type === "static" ? "📄" : "⚡";
2243
+ logger.important(` ${typeIcon} ${contract.name}`);
2244
+ }
2245
+ logger.important("");
2246
+ }
2247
+ if (index.contracts.length === 0) {
2248
+ logger.important(chalk.gray("No contracts found."));
1540
2249
  }
1541
2250
  }
1542
2251
  export {
1543
2252
  createEditorHandlers,
1544
2253
  getConfigWithDefaults,
2254
+ listContracts2 as listContracts,
1545
2255
  loadConfig,
2256
+ materializeContracts2 as materializeContracts,
1546
2257
  startDevServer,
1547
2258
  updateConfig
1548
2259
  };