@svton/cli 1.1.0 → 1.2.1

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
@@ -40,8 +40,8 @@ var import_commander = require("commander");
40
40
  var import_inquirer = __toESM(require("inquirer"));
41
41
  var import_chalk2 = __toESM(require("chalk"));
42
42
  var import_ora = __toESM(require("ora"));
43
- var import_fs_extra4 = __toESM(require("fs-extra"));
44
- var import_path3 = __toESM(require("path"));
43
+ var import_fs_extra5 = __toESM(require("fs-extra"));
44
+ var import_path4 = __toESM(require("path"));
45
45
  var import_validate_npm_package_name = __toESM(require("validate-npm-package-name"));
46
46
 
47
47
  // src/utils/template.ts
@@ -681,6 +681,225 @@ async function initGit(projectName) {
681
681
  }
682
682
  }
683
683
 
684
+ // src/utils/features.ts
685
+ var import_fs_extra4 = __toESM(require("fs-extra"));
686
+ var import_path3 = __toESM(require("path"));
687
+ async function loadFeaturesConfig() {
688
+ const configPath = import_path3.default.join(__dirname, "../features.json");
689
+ if (!import_fs_extra4.default.existsSync(configPath)) {
690
+ const devPath = import_path3.default.join(__dirname, "../../features.json");
691
+ if (import_fs_extra4.default.existsSync(devPath)) {
692
+ return await import_fs_extra4.default.readJSON(devPath);
693
+ }
694
+ }
695
+ return await import_fs_extra4.default.readJSON(configPath);
696
+ }
697
+ function getFeatureChoices(config) {
698
+ return Object.entries(config.features).map(([key, feature]) => ({
699
+ name: `${feature.name} - ${feature.description}`,
700
+ value: key,
701
+ checked: false
702
+ }));
703
+ }
704
+ function collectDependencies(features, config) {
705
+ const dependencies = {};
706
+ for (const featureKey of features) {
707
+ const feature = config.features[featureKey];
708
+ if (feature) {
709
+ Object.assign(dependencies, feature.packages.dependencies);
710
+ }
711
+ }
712
+ return dependencies;
713
+ }
714
+ function collectEnvVars(features, config) {
715
+ const envVars = [];
716
+ const seen = /* @__PURE__ */ new Set();
717
+ for (const featureKey of features) {
718
+ const feature = config.features[featureKey];
719
+ if (feature) {
720
+ for (const envVar of feature.envVars) {
721
+ if (!seen.has(envVar.key)) {
722
+ envVars.push(envVar);
723
+ seen.add(envVar.key);
724
+ }
725
+ }
726
+ }
727
+ }
728
+ return envVars;
729
+ }
730
+ async function generateEnvExample(features, config, targetPath) {
731
+ const envVars = collectEnvVars(features, config);
732
+ const content = [
733
+ "# Environment Variables",
734
+ "# Copy this file to .env and fill in the values",
735
+ "",
736
+ ...envVars.map((envVar) => {
737
+ const lines = [];
738
+ if (envVar.description) {
739
+ lines.push(`# ${envVar.description}`);
740
+ }
741
+ lines.push(`${envVar.key}=${envVar.default}`);
742
+ lines.push("");
743
+ return lines.join("\n");
744
+ })
745
+ ].join("\n");
746
+ await import_fs_extra4.default.writeFile(import_path3.default.join(targetPath, ".env.example"), content);
747
+ logger.info("Generated .env.example");
748
+ }
749
+ async function copyConfigFiles(features, config, templatePath, targetPath) {
750
+ for (const featureKey of features) {
751
+ const feature = config.features[featureKey];
752
+ if (feature && feature.configFiles) {
753
+ for (const configFile of feature.configFiles) {
754
+ const sourcePath = import_path3.default.join(templatePath, configFile.template);
755
+ const destPath = import_path3.default.join(targetPath, configFile.path);
756
+ await import_fs_extra4.default.ensureDir(import_path3.default.dirname(destPath));
757
+ await import_fs_extra4.default.copy(sourcePath, destPath);
758
+ logger.info(`Copied config: ${configFile.path}`);
759
+ }
760
+ }
761
+ }
762
+ }
763
+ async function copyExampleFiles(features, config, templatePath, targetPath) {
764
+ for (const featureKey of features) {
765
+ const feature = config.features[featureKey];
766
+ if (feature && feature.exampleFiles) {
767
+ const sourcePath = import_path3.default.join(templatePath, feature.exampleFiles.source);
768
+ const destPath = import_path3.default.join(targetPath, feature.exampleFiles.target);
769
+ if (await import_fs_extra4.default.pathExists(sourcePath)) {
770
+ await import_fs_extra4.default.ensureDir(import_path3.default.dirname(destPath));
771
+ await import_fs_extra4.default.copy(sourcePath, destPath);
772
+ logger.info(`Copied examples: ${feature.exampleFiles.target}`);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ async function copySkillFiles(features, config, templatePath, targetPath) {
778
+ const skillsDir = import_path3.default.join(targetPath, ".kiro/skills");
779
+ await import_fs_extra4.default.ensureDir(skillsDir);
780
+ const baseSkillSource = import_path3.default.join(templatePath, "skills/base.skill.md");
781
+ const baseSkillDest = import_path3.default.join(skillsDir, "project-capabilities.md");
782
+ if (await import_fs_extra4.default.pathExists(baseSkillSource)) {
783
+ await import_fs_extra4.default.copy(baseSkillSource, baseSkillDest);
784
+ logger.info("Copied base skill file");
785
+ }
786
+ for (const featureKey of features) {
787
+ const feature = config.features[featureKey];
788
+ if (feature && feature.skillFile) {
789
+ const sourcePath = import_path3.default.join(templatePath, feature.skillFile.template);
790
+ const destPath = import_path3.default.join(targetPath, feature.skillFile.target);
791
+ if (await import_fs_extra4.default.pathExists(sourcePath)) {
792
+ await import_fs_extra4.default.ensureDir(import_path3.default.dirname(destPath));
793
+ await import_fs_extra4.default.copy(sourcePath, destPath);
794
+ logger.info(`Copied skill: ${feature.skillFile.target}`);
795
+ }
796
+ }
797
+ }
798
+ await generateCapabilitiesIndex(features, config, targetPath);
799
+ }
800
+ async function generateCapabilitiesIndex(features, config, targetPath) {
801
+ const featuresList = features.map((featureKey) => {
802
+ const feature = config.features[featureKey];
803
+ if (!feature) return "";
804
+ const packages = Object.keys(feature.packages.dependencies).join(", ");
805
+ return `### ${feature.name}
806
+
807
+ ${feature.description}
808
+
809
+ - \u{1F4E6} \u5305\uFF1A${packages}
810
+ - \u{1F4DD} \u793A\u4F8B\u4EE3\u7801\uFF1A\`${feature.exampleFiles.target}\`
811
+ - \u{1F4DA} \u8BE6\u7EC6\u6587\u6863\uFF1A\u67E5\u770B \`.kiro/skills/${featureKey}.md\`
812
+ `;
813
+ }).join("\n");
814
+ const content = `# \u9879\u76EE\u80FD\u529B\u7D22\u5F15
815
+
816
+ \u672C\u9879\u76EE\u57FA\u4E8E Svton \u6846\u67B6\u521B\u5EFA\uFF0C\u5DF2\u96C6\u6210\u4EE5\u4E0B\u529F\u80FD\u6A21\u5757\uFF1A
817
+
818
+ ## \u5DF2\u542F\u7528\u7684\u529F\u80FD
819
+
820
+ ${featuresList}
821
+
822
+ ## \u4F7F\u7528\u5EFA\u8BAE
823
+
824
+ \u5F53\u4F60\u9700\u8981\u4F7F\u7528\u67D0\u4E2A\u529F\u80FD\u65F6\uFF0C\u53EF\u4EE5\uFF1A
825
+
826
+ 1. \u67E5\u770B\u5BF9\u5E94\u7684 skill \u6587\u6863\u4E86\u89E3 API \u548C\u6700\u4F73\u5B9E\u8DF5
827
+ 2. \u53C2\u8003 \`src/examples/\` \u76EE\u5F55\u4E0B\u7684\u793A\u4F8B\u4EE3\u7801
828
+ 3. \u67E5\u770B\u5B98\u65B9\u6587\u6863\u83B7\u53D6\u66F4\u591A\u4FE1\u606F
829
+
830
+ ## \u6587\u6863\u8D44\u6E90
831
+
832
+ - Svton \u5B98\u65B9\u6587\u6863\uFF1Ahttps://751848178.github.io/svton
833
+ - GitHub\uFF1Ahttps://github.com/751848178/svton
834
+ `;
835
+ const indexPath = import_path3.default.join(targetPath, ".kiro/skills/project-capabilities.md");
836
+ await import_fs_extra4.default.writeFile(indexPath, content);
837
+ logger.info("Generated capabilities index");
838
+ }
839
+ async function updatePackageJson(features, config, targetPath) {
840
+ const packageJsonPath = import_path3.default.join(targetPath, "package.json");
841
+ const packageJson = await import_fs_extra4.default.readJSON(packageJsonPath);
842
+ const dependencies = collectDependencies(features, config);
843
+ packageJson.dependencies = {
844
+ ...packageJson.dependencies,
845
+ ...dependencies
846
+ };
847
+ await import_fs_extra4.default.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
848
+ logger.info("Updated package.json with feature dependencies");
849
+ }
850
+ function generateModuleImports(features, config) {
851
+ const imports = [];
852
+ for (const featureKey of features) {
853
+ const feature = config.features[featureKey];
854
+ if (feature && feature.moduleImports) {
855
+ for (const moduleImport of feature.moduleImports) {
856
+ imports.push(`import { ${moduleImport.import} } from '${moduleImport.from}';`);
857
+ }
858
+ }
859
+ }
860
+ return imports.join("\n");
861
+ }
862
+ function generateModuleRegistrations(features, config) {
863
+ const registrations = [];
864
+ for (const featureKey of features) {
865
+ const feature = config.features[featureKey];
866
+ if (feature && feature.moduleRegistration) {
867
+ const { module: moduleName, config: moduleConfig } = feature.moduleRegistration;
868
+ registrations.push(` ${moduleName}.${feature.moduleRegistration.type}({
869
+ useFactory: (configService: ConfigService) => ${moduleConfig},
870
+ inject: [ConfigService],
871
+ }),`);
872
+ }
873
+ }
874
+ return registrations.join("\n");
875
+ }
876
+ async function updateAppModule(features, config, targetPath) {
877
+ const appModulePath = import_path3.default.join(targetPath, "src/app.module.ts");
878
+ if (!await import_fs_extra4.default.pathExists(appModulePath)) {
879
+ logger.warn("app.module.ts not found, skipping module injection");
880
+ return;
881
+ }
882
+ let content = await import_fs_extra4.default.readFile(appModulePath, "utf-8");
883
+ const imports = generateModuleImports(features, config);
884
+ const registrations = generateModuleRegistrations(features, config);
885
+ const importsMatch = content.match(/imports:\s*\[([\s\S]*?)\]/);
886
+ if (importsMatch) {
887
+ const existingImports = importsMatch[1];
888
+ const newImports = `${existingImports}
889
+ ${registrations}`;
890
+ content = content.replace(
891
+ /imports:\s*\[([\s\S]*?)\]/,
892
+ `imports: [${newImports}
893
+ ]`
894
+ );
895
+ }
896
+ const lastImportIndex = content.lastIndexOf("import ");
897
+ const lastImportEnd = content.indexOf("\n", lastImportIndex);
898
+ content = content.slice(0, lastImportEnd + 1) + imports + "\n" + content.slice(lastImportEnd + 1);
899
+ await import_fs_extra4.default.writeFile(appModulePath, content);
900
+ logger.info("Updated app.module.ts with feature modules");
901
+ }
902
+
684
903
  // src/commands/create.ts
685
904
  async function createProject(projectName, options = {}) {
686
905
  try {
@@ -692,18 +911,21 @@ async function createProject(projectName, options = {}) {
692
911
  }
693
912
  process.exit(1);
694
913
  }
695
- const projectPath = import_path3.default.resolve(process.cwd(), projectName);
696
- if (await import_fs_extra4.default.pathExists(projectPath)) {
914
+ const projectPath = import_path4.default.resolve(process.cwd(), projectName);
915
+ if (await import_fs_extra5.default.pathExists(projectPath)) {
697
916
  logger.error(`Directory ${projectName} already exists!`);
698
917
  process.exit(1);
699
918
  }
700
919
  logger.info(import_chalk2.default.blue("\u{1F680} Welcome to Svton App Generator!"));
701
920
  logger.info("");
921
+ const featuresConfig = await loadFeaturesConfig();
702
922
  let answers;
703
923
  if (options.yes) {
704
924
  answers = {
705
925
  org: options.org || projectName,
706
926
  template: options.template || "full-stack",
927
+ features: [],
928
+ // 默认不选择额外功能
707
929
  packageManager: options.packageManager || "pnpm",
708
930
  installDeps: !options.skipInstall,
709
931
  initGit: !options.skipGit
@@ -732,6 +954,13 @@ async function createProject(projectName, options = {}) {
732
954
  ],
733
955
  default: options.template || "full-stack"
734
956
  },
957
+ {
958
+ type: "checkbox",
959
+ name: "features",
960
+ message: "Select features to include (use space to select, enter to confirm):",
961
+ choices: getFeatureChoices(featuresConfig),
962
+ when: (answers2) => answers2.template === "backend-only" || answers2.template === "full-stack"
963
+ },
735
964
  {
736
965
  type: "list",
737
966
  name: "packageManager",
@@ -757,6 +986,7 @@ async function createProject(projectName, options = {}) {
757
986
  projectName,
758
987
  orgName: answers.org.startsWith("@") ? answers.org : `@${answers.org}`,
759
988
  template: answers.template,
989
+ features: answers.features || [],
760
990
  packageManager: answers.packageManager,
761
991
  installDeps: answers.installDeps,
762
992
  initGit: answers.initGit,
@@ -767,6 +997,9 @@ async function createProject(projectName, options = {}) {
767
997
  logger.info(` Project Name: ${import_chalk2.default.white(config.projectName)}`);
768
998
  logger.info(` Organization: ${import_chalk2.default.white(config.orgName)}`);
769
999
  logger.info(` Template: ${import_chalk2.default.white(config.template)}`);
1000
+ if (config.features.length > 0) {
1001
+ logger.info(` Features: ${import_chalk2.default.white(config.features.join(", "))}`);
1002
+ }
770
1003
  logger.info(` Package Manager: ${import_chalk2.default.white(config.packageManager)}`);
771
1004
  logger.info(` Install Dependencies: ${import_chalk2.default.white(config.installDeps ? "Yes" : "No")}`);
772
1005
  logger.info(` Initialize Git: ${import_chalk2.default.white(config.initGit ? "Yes" : "No")}`);
@@ -811,10 +1044,23 @@ async function createProject(projectName, options = {}) {
811
1044
  async function createProjectFromTemplate(config) {
812
1045
  const spinner = (0, import_ora.default)("Creating project...").start();
813
1046
  try {
814
- await import_fs_extra4.default.ensureDir(config.projectPath);
1047
+ await import_fs_extra5.default.ensureDir(config.projectPath);
815
1048
  process.chdir(config.projectPath);
816
1049
  spinner.text = "Generating project files...";
817
1050
  await generateFromTemplate(config);
1051
+ if (config.features.length > 0) {
1052
+ spinner.text = "Integrating selected features...";
1053
+ const featuresConfig = await loadFeaturesConfig();
1054
+ const templatePath = import_path4.default.join(__dirname, "../../../templates");
1055
+ await updatePackageJson(config.features, featuresConfig, config.projectPath);
1056
+ await copyConfigFiles(config.features, featuresConfig, templatePath, config.projectPath);
1057
+ await copyExampleFiles(config.features, featuresConfig, templatePath, config.projectPath);
1058
+ await copySkillFiles(config.features, featuresConfig, templatePath, config.projectPath);
1059
+ await generateEnvExample(config.features, featuresConfig, config.projectPath);
1060
+ if (config.template === "backend-only" || config.template === "full-stack") {
1061
+ await updateAppModule(config.features, featuresConfig, config.projectPath);
1062
+ }
1063
+ }
818
1064
  if (config.installDeps) {
819
1065
  spinner.text = "Installing dependencies...";
820
1066
  await installDependencies(config.packageManager);
@@ -831,7 +1077,7 @@ async function createProjectFromTemplate(config) {
831
1077
  }
832
1078
 
833
1079
  // package.json
834
- var version = "1.1.0";
1080
+ var version = "1.2.1";
835
1081
 
836
1082
  // src/index.ts
837
1083
  async function cli() {
package/dist/index.mjs CHANGED
@@ -13,8 +13,8 @@ import { Command } from "commander";
13
13
  import inquirer from "inquirer";
14
14
  import chalk2 from "chalk";
15
15
  import ora from "ora";
16
- import fs4 from "fs-extra";
17
- import path3 from "path";
16
+ import fs5 from "fs-extra";
17
+ import path4 from "path";
18
18
  import validateNpmPackageName from "validate-npm-package-name";
19
19
 
20
20
  // src/utils/template.ts
@@ -654,6 +654,225 @@ async function initGit(projectName) {
654
654
  }
655
655
  }
656
656
 
657
+ // src/utils/features.ts
658
+ import fs4 from "fs-extra";
659
+ import path3 from "path";
660
+ async function loadFeaturesConfig() {
661
+ const configPath = path3.join(__dirname, "../features.json");
662
+ if (!fs4.existsSync(configPath)) {
663
+ const devPath = path3.join(__dirname, "../../features.json");
664
+ if (fs4.existsSync(devPath)) {
665
+ return await fs4.readJSON(devPath);
666
+ }
667
+ }
668
+ return await fs4.readJSON(configPath);
669
+ }
670
+ function getFeatureChoices(config) {
671
+ return Object.entries(config.features).map(([key, feature]) => ({
672
+ name: `${feature.name} - ${feature.description}`,
673
+ value: key,
674
+ checked: false
675
+ }));
676
+ }
677
+ function collectDependencies(features, config) {
678
+ const dependencies = {};
679
+ for (const featureKey of features) {
680
+ const feature = config.features[featureKey];
681
+ if (feature) {
682
+ Object.assign(dependencies, feature.packages.dependencies);
683
+ }
684
+ }
685
+ return dependencies;
686
+ }
687
+ function collectEnvVars(features, config) {
688
+ const envVars = [];
689
+ const seen = /* @__PURE__ */ new Set();
690
+ for (const featureKey of features) {
691
+ const feature = config.features[featureKey];
692
+ if (feature) {
693
+ for (const envVar of feature.envVars) {
694
+ if (!seen.has(envVar.key)) {
695
+ envVars.push(envVar);
696
+ seen.add(envVar.key);
697
+ }
698
+ }
699
+ }
700
+ }
701
+ return envVars;
702
+ }
703
+ async function generateEnvExample(features, config, targetPath) {
704
+ const envVars = collectEnvVars(features, config);
705
+ const content = [
706
+ "# Environment Variables",
707
+ "# Copy this file to .env and fill in the values",
708
+ "",
709
+ ...envVars.map((envVar) => {
710
+ const lines = [];
711
+ if (envVar.description) {
712
+ lines.push(`# ${envVar.description}`);
713
+ }
714
+ lines.push(`${envVar.key}=${envVar.default}`);
715
+ lines.push("");
716
+ return lines.join("\n");
717
+ })
718
+ ].join("\n");
719
+ await fs4.writeFile(path3.join(targetPath, ".env.example"), content);
720
+ logger.info("Generated .env.example");
721
+ }
722
+ async function copyConfigFiles(features, config, templatePath, targetPath) {
723
+ for (const featureKey of features) {
724
+ const feature = config.features[featureKey];
725
+ if (feature && feature.configFiles) {
726
+ for (const configFile of feature.configFiles) {
727
+ const sourcePath = path3.join(templatePath, configFile.template);
728
+ const destPath = path3.join(targetPath, configFile.path);
729
+ await fs4.ensureDir(path3.dirname(destPath));
730
+ await fs4.copy(sourcePath, destPath);
731
+ logger.info(`Copied config: ${configFile.path}`);
732
+ }
733
+ }
734
+ }
735
+ }
736
+ async function copyExampleFiles(features, config, templatePath, targetPath) {
737
+ for (const featureKey of features) {
738
+ const feature = config.features[featureKey];
739
+ if (feature && feature.exampleFiles) {
740
+ const sourcePath = path3.join(templatePath, feature.exampleFiles.source);
741
+ const destPath = path3.join(targetPath, feature.exampleFiles.target);
742
+ if (await fs4.pathExists(sourcePath)) {
743
+ await fs4.ensureDir(path3.dirname(destPath));
744
+ await fs4.copy(sourcePath, destPath);
745
+ logger.info(`Copied examples: ${feature.exampleFiles.target}`);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ async function copySkillFiles(features, config, templatePath, targetPath) {
751
+ const skillsDir = path3.join(targetPath, ".kiro/skills");
752
+ await fs4.ensureDir(skillsDir);
753
+ const baseSkillSource = path3.join(templatePath, "skills/base.skill.md");
754
+ const baseSkillDest = path3.join(skillsDir, "project-capabilities.md");
755
+ if (await fs4.pathExists(baseSkillSource)) {
756
+ await fs4.copy(baseSkillSource, baseSkillDest);
757
+ logger.info("Copied base skill file");
758
+ }
759
+ for (const featureKey of features) {
760
+ const feature = config.features[featureKey];
761
+ if (feature && feature.skillFile) {
762
+ const sourcePath = path3.join(templatePath, feature.skillFile.template);
763
+ const destPath = path3.join(targetPath, feature.skillFile.target);
764
+ if (await fs4.pathExists(sourcePath)) {
765
+ await fs4.ensureDir(path3.dirname(destPath));
766
+ await fs4.copy(sourcePath, destPath);
767
+ logger.info(`Copied skill: ${feature.skillFile.target}`);
768
+ }
769
+ }
770
+ }
771
+ await generateCapabilitiesIndex(features, config, targetPath);
772
+ }
773
+ async function generateCapabilitiesIndex(features, config, targetPath) {
774
+ const featuresList = features.map((featureKey) => {
775
+ const feature = config.features[featureKey];
776
+ if (!feature) return "";
777
+ const packages = Object.keys(feature.packages.dependencies).join(", ");
778
+ return `### ${feature.name}
779
+
780
+ ${feature.description}
781
+
782
+ - \u{1F4E6} \u5305\uFF1A${packages}
783
+ - \u{1F4DD} \u793A\u4F8B\u4EE3\u7801\uFF1A\`${feature.exampleFiles.target}\`
784
+ - \u{1F4DA} \u8BE6\u7EC6\u6587\u6863\uFF1A\u67E5\u770B \`.kiro/skills/${featureKey}.md\`
785
+ `;
786
+ }).join("\n");
787
+ const content = `# \u9879\u76EE\u80FD\u529B\u7D22\u5F15
788
+
789
+ \u672C\u9879\u76EE\u57FA\u4E8E Svton \u6846\u67B6\u521B\u5EFA\uFF0C\u5DF2\u96C6\u6210\u4EE5\u4E0B\u529F\u80FD\u6A21\u5757\uFF1A
790
+
791
+ ## \u5DF2\u542F\u7528\u7684\u529F\u80FD
792
+
793
+ ${featuresList}
794
+
795
+ ## \u4F7F\u7528\u5EFA\u8BAE
796
+
797
+ \u5F53\u4F60\u9700\u8981\u4F7F\u7528\u67D0\u4E2A\u529F\u80FD\u65F6\uFF0C\u53EF\u4EE5\uFF1A
798
+
799
+ 1. \u67E5\u770B\u5BF9\u5E94\u7684 skill \u6587\u6863\u4E86\u89E3 API \u548C\u6700\u4F73\u5B9E\u8DF5
800
+ 2. \u53C2\u8003 \`src/examples/\` \u76EE\u5F55\u4E0B\u7684\u793A\u4F8B\u4EE3\u7801
801
+ 3. \u67E5\u770B\u5B98\u65B9\u6587\u6863\u83B7\u53D6\u66F4\u591A\u4FE1\u606F
802
+
803
+ ## \u6587\u6863\u8D44\u6E90
804
+
805
+ - Svton \u5B98\u65B9\u6587\u6863\uFF1Ahttps://751848178.github.io/svton
806
+ - GitHub\uFF1Ahttps://github.com/751848178/svton
807
+ `;
808
+ const indexPath = path3.join(targetPath, ".kiro/skills/project-capabilities.md");
809
+ await fs4.writeFile(indexPath, content);
810
+ logger.info("Generated capabilities index");
811
+ }
812
+ async function updatePackageJson(features, config, targetPath) {
813
+ const packageJsonPath = path3.join(targetPath, "package.json");
814
+ const packageJson = await fs4.readJSON(packageJsonPath);
815
+ const dependencies = collectDependencies(features, config);
816
+ packageJson.dependencies = {
817
+ ...packageJson.dependencies,
818
+ ...dependencies
819
+ };
820
+ await fs4.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
821
+ logger.info("Updated package.json with feature dependencies");
822
+ }
823
+ function generateModuleImports(features, config) {
824
+ const imports = [];
825
+ for (const featureKey of features) {
826
+ const feature = config.features[featureKey];
827
+ if (feature && feature.moduleImports) {
828
+ for (const moduleImport of feature.moduleImports) {
829
+ imports.push(`import { ${moduleImport.import} } from '${moduleImport.from}';`);
830
+ }
831
+ }
832
+ }
833
+ return imports.join("\n");
834
+ }
835
+ function generateModuleRegistrations(features, config) {
836
+ const registrations = [];
837
+ for (const featureKey of features) {
838
+ const feature = config.features[featureKey];
839
+ if (feature && feature.moduleRegistration) {
840
+ const { module: moduleName, config: moduleConfig } = feature.moduleRegistration;
841
+ registrations.push(` ${moduleName}.${feature.moduleRegistration.type}({
842
+ useFactory: (configService: ConfigService) => ${moduleConfig},
843
+ inject: [ConfigService],
844
+ }),`);
845
+ }
846
+ }
847
+ return registrations.join("\n");
848
+ }
849
+ async function updateAppModule(features, config, targetPath) {
850
+ const appModulePath = path3.join(targetPath, "src/app.module.ts");
851
+ if (!await fs4.pathExists(appModulePath)) {
852
+ logger.warn("app.module.ts not found, skipping module injection");
853
+ return;
854
+ }
855
+ let content = await fs4.readFile(appModulePath, "utf-8");
856
+ const imports = generateModuleImports(features, config);
857
+ const registrations = generateModuleRegistrations(features, config);
858
+ const importsMatch = content.match(/imports:\s*\[([\s\S]*?)\]/);
859
+ if (importsMatch) {
860
+ const existingImports = importsMatch[1];
861
+ const newImports = `${existingImports}
862
+ ${registrations}`;
863
+ content = content.replace(
864
+ /imports:\s*\[([\s\S]*?)\]/,
865
+ `imports: [${newImports}
866
+ ]`
867
+ );
868
+ }
869
+ const lastImportIndex = content.lastIndexOf("import ");
870
+ const lastImportEnd = content.indexOf("\n", lastImportIndex);
871
+ content = content.slice(0, lastImportEnd + 1) + imports + "\n" + content.slice(lastImportEnd + 1);
872
+ await fs4.writeFile(appModulePath, content);
873
+ logger.info("Updated app.module.ts with feature modules");
874
+ }
875
+
657
876
  // src/commands/create.ts
658
877
  async function createProject(projectName, options = {}) {
659
878
  try {
@@ -665,18 +884,21 @@ async function createProject(projectName, options = {}) {
665
884
  }
666
885
  process.exit(1);
667
886
  }
668
- const projectPath = path3.resolve(process.cwd(), projectName);
669
- if (await fs4.pathExists(projectPath)) {
887
+ const projectPath = path4.resolve(process.cwd(), projectName);
888
+ if (await fs5.pathExists(projectPath)) {
670
889
  logger.error(`Directory ${projectName} already exists!`);
671
890
  process.exit(1);
672
891
  }
673
892
  logger.info(chalk2.blue("\u{1F680} Welcome to Svton App Generator!"));
674
893
  logger.info("");
894
+ const featuresConfig = await loadFeaturesConfig();
675
895
  let answers;
676
896
  if (options.yes) {
677
897
  answers = {
678
898
  org: options.org || projectName,
679
899
  template: options.template || "full-stack",
900
+ features: [],
901
+ // 默认不选择额外功能
680
902
  packageManager: options.packageManager || "pnpm",
681
903
  installDeps: !options.skipInstall,
682
904
  initGit: !options.skipGit
@@ -705,6 +927,13 @@ async function createProject(projectName, options = {}) {
705
927
  ],
706
928
  default: options.template || "full-stack"
707
929
  },
930
+ {
931
+ type: "checkbox",
932
+ name: "features",
933
+ message: "Select features to include (use space to select, enter to confirm):",
934
+ choices: getFeatureChoices(featuresConfig),
935
+ when: (answers2) => answers2.template === "backend-only" || answers2.template === "full-stack"
936
+ },
708
937
  {
709
938
  type: "list",
710
939
  name: "packageManager",
@@ -730,6 +959,7 @@ async function createProject(projectName, options = {}) {
730
959
  projectName,
731
960
  orgName: answers.org.startsWith("@") ? answers.org : `@${answers.org}`,
732
961
  template: answers.template,
962
+ features: answers.features || [],
733
963
  packageManager: answers.packageManager,
734
964
  installDeps: answers.installDeps,
735
965
  initGit: answers.initGit,
@@ -740,6 +970,9 @@ async function createProject(projectName, options = {}) {
740
970
  logger.info(` Project Name: ${chalk2.white(config.projectName)}`);
741
971
  logger.info(` Organization: ${chalk2.white(config.orgName)}`);
742
972
  logger.info(` Template: ${chalk2.white(config.template)}`);
973
+ if (config.features.length > 0) {
974
+ logger.info(` Features: ${chalk2.white(config.features.join(", "))}`);
975
+ }
743
976
  logger.info(` Package Manager: ${chalk2.white(config.packageManager)}`);
744
977
  logger.info(` Install Dependencies: ${chalk2.white(config.installDeps ? "Yes" : "No")}`);
745
978
  logger.info(` Initialize Git: ${chalk2.white(config.initGit ? "Yes" : "No")}`);
@@ -784,10 +1017,23 @@ async function createProject(projectName, options = {}) {
784
1017
  async function createProjectFromTemplate(config) {
785
1018
  const spinner = ora("Creating project...").start();
786
1019
  try {
787
- await fs4.ensureDir(config.projectPath);
1020
+ await fs5.ensureDir(config.projectPath);
788
1021
  process.chdir(config.projectPath);
789
1022
  spinner.text = "Generating project files...";
790
1023
  await generateFromTemplate(config);
1024
+ if (config.features.length > 0) {
1025
+ spinner.text = "Integrating selected features...";
1026
+ const featuresConfig = await loadFeaturesConfig();
1027
+ const templatePath = path4.join(__dirname, "../../../templates");
1028
+ await updatePackageJson(config.features, featuresConfig, config.projectPath);
1029
+ await copyConfigFiles(config.features, featuresConfig, templatePath, config.projectPath);
1030
+ await copyExampleFiles(config.features, featuresConfig, templatePath, config.projectPath);
1031
+ await copySkillFiles(config.features, featuresConfig, templatePath, config.projectPath);
1032
+ await generateEnvExample(config.features, featuresConfig, config.projectPath);
1033
+ if (config.template === "backend-only" || config.template === "full-stack") {
1034
+ await updateAppModule(config.features, featuresConfig, config.projectPath);
1035
+ }
1036
+ }
791
1037
  if (config.installDeps) {
792
1038
  spinner.text = "Installing dependencies...";
793
1039
  await installDependencies(config.packageManager);
@@ -804,7 +1050,7 @@ async function createProjectFromTemplate(config) {
804
1050
  }
805
1051
 
806
1052
  // package.json
807
- var version = "1.1.0";
1053
+ var version = "1.2.1";
808
1054
 
809
1055
  // src/index.ts
810
1056
  async function cli() {
package/features.json ADDED
@@ -0,0 +1,339 @@
1
+ {
2
+ "features": {
3
+ "cache": {
4
+ "name": "缓存",
5
+ "description": "基于 Redis 的声明式缓存",
6
+ "category": "backend",
7
+ "packages": {
8
+ "dependencies": {
9
+ "@svton/nestjs-cache": "latest",
10
+ "@svton/nestjs-redis": "latest"
11
+ }
12
+ },
13
+ "envVars": [
14
+ { "key": "REDIS_HOST", "default": "localhost", "description": "Redis 主机地址" },
15
+ { "key": "REDIS_PORT", "default": "6379", "description": "Redis 端口" },
16
+ { "key": "REDIS_PASSWORD", "default": "", "description": "Redis 密码(可选)" }
17
+ ],
18
+ "configFiles": [
19
+ {
20
+ "path": "src/config/cache.config.ts",
21
+ "template": "configs/cache.config.ts"
22
+ }
23
+ ],
24
+ "moduleImports": [
25
+ {
26
+ "from": "@svton/nestjs-cache",
27
+ "import": "CacheModule"
28
+ }
29
+ ],
30
+ "moduleRegistration": {
31
+ "type": "forRootAsync",
32
+ "module": "CacheModule",
33
+ "config": "useCacheConfig(configService)"
34
+ },
35
+ "exampleFiles": {
36
+ "source": "examples/cache",
37
+ "target": "src/examples/cache",
38
+ "description": "缓存装饰器使用示例"
39
+ },
40
+ "skillFile": {
41
+ "template": "skills/cache.skill.md",
42
+ "target": ".kiro/skills/cache.md"
43
+ }
44
+ },
45
+ "queue": {
46
+ "name": "消息队列",
47
+ "description": "基于 BullMQ 的异步任务处理",
48
+ "category": "backend",
49
+ "packages": {
50
+ "dependencies": {
51
+ "@svton/nestjs-queue": "latest",
52
+ "@svton/nestjs-redis": "latest"
53
+ }
54
+ },
55
+ "envVars": [
56
+ { "key": "REDIS_HOST", "default": "localhost", "description": "Redis 主机地址" },
57
+ { "key": "REDIS_PORT", "default": "6379", "description": "Redis 端口" }
58
+ ],
59
+ "configFiles": [
60
+ {
61
+ "path": "src/config/queue.config.ts",
62
+ "template": "configs/queue.config.ts"
63
+ }
64
+ ],
65
+ "moduleImports": [
66
+ {
67
+ "from": "@svton/nestjs-queue",
68
+ "import": "QueueModule"
69
+ }
70
+ ],
71
+ "moduleRegistration": {
72
+ "type": "forRootAsync",
73
+ "module": "QueueModule",
74
+ "config": "useQueueConfig(configService)"
75
+ },
76
+ "exampleFiles": {
77
+ "source": "examples/queue",
78
+ "target": "src/examples/queue",
79
+ "description": "队列任务处理示例"
80
+ },
81
+ "skillFile": {
82
+ "template": "skills/queue.skill.md",
83
+ "target": ".kiro/skills/queue.md"
84
+ }
85
+ },
86
+ "payment": {
87
+ "name": "支付",
88
+ "description": "微信支付 + 支付宝",
89
+ "category": "backend",
90
+ "packages": {
91
+ "dependencies": {
92
+ "@svton/nestjs-payment": "latest"
93
+ }
94
+ },
95
+ "envVars": [
96
+ { "key": "WECHAT_MCH_ID", "default": "", "description": "微信商户号" },
97
+ { "key": "WECHAT_PRIVATE_KEY", "default": "", "description": "微信商户 API 私钥路径" },
98
+ { "key": "WECHAT_SERIAL_NO", "default": "", "description": "微信商户 API 证书序列号" },
99
+ { "key": "WECHAT_API_V3_KEY", "default": "", "description": "微信 APIv3 密钥" },
100
+ { "key": "WECHAT_APP_ID", "default": "", "description": "微信关联的 AppID" },
101
+ { "key": "ALIPAY_APP_ID", "default": "", "description": "支付宝应用 ID" },
102
+ { "key": "ALIPAY_PRIVATE_KEY", "default": "", "description": "支付宝应用私钥路径" },
103
+ { "key": "ALIPAY_PUBLIC_KEY", "default": "", "description": "支付宝公钥路径" }
104
+ ],
105
+ "configFiles": [
106
+ {
107
+ "path": "src/config/payment.config.ts",
108
+ "template": "configs/payment.config.ts"
109
+ }
110
+ ],
111
+ "moduleImports": [
112
+ {
113
+ "from": "@svton/nestjs-payment",
114
+ "import": "PaymentModule"
115
+ }
116
+ ],
117
+ "moduleRegistration": {
118
+ "type": "forRootAsync",
119
+ "module": "PaymentModule",
120
+ "config": "usePaymentConfig(configService)"
121
+ },
122
+ "exampleFiles": {
123
+ "source": "examples/payment",
124
+ "target": "src/examples/payment",
125
+ "description": "支付功能示例(微信/支付宝)"
126
+ },
127
+ "skillFile": {
128
+ "template": "skills/payment.skill.md",
129
+ "target": ".kiro/skills/payment.md"
130
+ }
131
+ },
132
+ "oauth": {
133
+ "name": "OAuth 登录",
134
+ "description": "微信登录(开放平台/公众号/小程序)",
135
+ "category": "backend",
136
+ "packages": {
137
+ "dependencies": {
138
+ "@svton/nestjs-oauth": "latest"
139
+ }
140
+ },
141
+ "envVars": [
142
+ { "key": "WECHAT_OPEN_APP_ID", "default": "", "description": "微信开放平台 AppID" },
143
+ { "key": "WECHAT_OPEN_APP_SECRET", "default": "", "description": "微信开放平台 AppSecret" },
144
+ { "key": "WECHAT_MINI_APP_ID", "default": "", "description": "微信小程序 AppID" },
145
+ { "key": "WECHAT_MINI_APP_SECRET", "default": "", "description": "微信小程序 AppSecret" }
146
+ ],
147
+ "configFiles": [
148
+ {
149
+ "path": "src/config/oauth.config.ts",
150
+ "template": "configs/oauth.config.ts"
151
+ }
152
+ ],
153
+ "moduleImports": [
154
+ {
155
+ "from": "@svton/nestjs-oauth",
156
+ "import": "OAuthModule"
157
+ }
158
+ ],
159
+ "moduleRegistration": {
160
+ "type": "forRootAsync",
161
+ "module": "OAuthModule",
162
+ "config": "useOAuthConfig(configService)"
163
+ },
164
+ "exampleFiles": {
165
+ "source": "examples/oauth",
166
+ "target": "src/examples/oauth",
167
+ "description": "OAuth 登录示例"
168
+ },
169
+ "skillFile": {
170
+ "template": "skills/oauth.skill.md",
171
+ "target": ".kiro/skills/oauth.md"
172
+ }
173
+ },
174
+ "sms": {
175
+ "name": "短信",
176
+ "description": "阿里云/腾讯云短信发送",
177
+ "category": "backend",
178
+ "packages": {
179
+ "dependencies": {
180
+ "@svton/nestjs-sms": "latest"
181
+ }
182
+ },
183
+ "envVars": [
184
+ { "key": "SMS_PROVIDER", "default": "aliyun", "description": "短信服务商 (aliyun/tencent)" },
185
+ { "key": "SMS_ACCESS_KEY_ID", "default": "", "description": "AccessKey ID" },
186
+ { "key": "SMS_ACCESS_KEY_SECRET", "default": "", "description": "AccessKey Secret" },
187
+ { "key": "SMS_SIGN_NAME", "default": "", "description": "短信签名" }
188
+ ],
189
+ "configFiles": [
190
+ {
191
+ "path": "src/config/sms.config.ts",
192
+ "template": "configs/sms.config.ts"
193
+ }
194
+ ],
195
+ "moduleImports": [
196
+ {
197
+ "from": "@svton/nestjs-sms",
198
+ "import": "SmsModule"
199
+ }
200
+ ],
201
+ "moduleRegistration": {
202
+ "type": "forRootAsync",
203
+ "module": "SmsModule",
204
+ "config": "useSmsConfig(configService)"
205
+ },
206
+ "exampleFiles": {
207
+ "source": "examples/sms",
208
+ "target": "src/examples/sms",
209
+ "description": "短信发送示例"
210
+ },
211
+ "skillFile": {
212
+ "template": "skills/sms.skill.md",
213
+ "target": ".kiro/skills/sms.md"
214
+ }
215
+ },
216
+ "storage": {
217
+ "name": "对象存储",
218
+ "description": "七牛云/阿里云 OSS",
219
+ "category": "backend",
220
+ "packages": {
221
+ "dependencies": {
222
+ "@svton/nestjs-object-storage": "latest",
223
+ "@svton/nestjs-object-storage-qiniu-kodo": "latest"
224
+ }
225
+ },
226
+ "envVars": [
227
+ { "key": "STORAGE_PROVIDER", "default": "qiniu", "description": "存储服务商 (qiniu/aliyun)" },
228
+ { "key": "QINIU_ACCESS_KEY", "default": "", "description": "七牛云 AccessKey" },
229
+ { "key": "QINIU_SECRET_KEY", "default": "", "description": "七牛云 SecretKey" },
230
+ { "key": "QINIU_BUCKET", "default": "", "description": "七牛云存储空间名称" },
231
+ { "key": "QINIU_DOMAIN", "default": "", "description": "七牛云 CDN 域名" }
232
+ ],
233
+ "configFiles": [
234
+ {
235
+ "path": "src/config/storage.config.ts",
236
+ "template": "configs/storage.config.ts"
237
+ }
238
+ ],
239
+ "moduleImports": [
240
+ {
241
+ "from": "@svton/nestjs-object-storage",
242
+ "import": "ObjectStorageModule"
243
+ }
244
+ ],
245
+ "moduleRegistration": {
246
+ "type": "forRootAsync",
247
+ "module": "ObjectStorageModule",
248
+ "config": "useStorageConfig(configService)"
249
+ },
250
+ "exampleFiles": {
251
+ "source": "examples/storage",
252
+ "target": "src/examples/storage",
253
+ "description": "文件上传示例"
254
+ },
255
+ "skillFile": {
256
+ "template": "skills/storage.skill.md",
257
+ "target": ".kiro/skills/storage.md"
258
+ }
259
+ },
260
+ "rateLimit": {
261
+ "name": "限流",
262
+ "description": "接口访问频率限制",
263
+ "category": "backend",
264
+ "packages": {
265
+ "dependencies": {
266
+ "@svton/nestjs-rate-limit": "latest",
267
+ "@svton/nestjs-redis": "latest"
268
+ }
269
+ },
270
+ "envVars": [
271
+ { "key": "REDIS_HOST", "default": "localhost", "description": "Redis 主机地址" },
272
+ { "key": "REDIS_PORT", "default": "6379", "description": "Redis 端口" }
273
+ ],
274
+ "configFiles": [
275
+ {
276
+ "path": "src/config/rate-limit.config.ts",
277
+ "template": "configs/rate-limit.config.ts"
278
+ }
279
+ ],
280
+ "moduleImports": [
281
+ {
282
+ "from": "@svton/nestjs-rate-limit",
283
+ "import": "RateLimitModule"
284
+ }
285
+ ],
286
+ "moduleRegistration": {
287
+ "type": "forRootAsync",
288
+ "module": "RateLimitModule",
289
+ "config": "useRateLimitConfig(configService)"
290
+ },
291
+ "exampleFiles": {
292
+ "source": "examples/rate-limit",
293
+ "target": "src/examples/rate-limit",
294
+ "description": "限流使用示例"
295
+ },
296
+ "skillFile": {
297
+ "template": "skills/rate-limit.skill.md",
298
+ "target": ".kiro/skills/rate-limit.md"
299
+ }
300
+ },
301
+ "authz": {
302
+ "name": "权限控制",
303
+ "description": "RBAC 权限管理",
304
+ "category": "backend",
305
+ "packages": {
306
+ "dependencies": {
307
+ "@svton/nestjs-authz": "latest"
308
+ }
309
+ },
310
+ "envVars": [],
311
+ "configFiles": [
312
+ {
313
+ "path": "src/config/authz.config.ts",
314
+ "template": "configs/authz.config.ts"
315
+ }
316
+ ],
317
+ "moduleImports": [
318
+ {
319
+ "from": "@svton/nestjs-authz",
320
+ "import": "AuthzModule"
321
+ }
322
+ ],
323
+ "moduleRegistration": {
324
+ "type": "forRootAsync",
325
+ "module": "AuthzModule",
326
+ "config": "useAuthzConfig(configService)"
327
+ },
328
+ "exampleFiles": {
329
+ "source": "examples/authz",
330
+ "target": "src/examples/authz",
331
+ "description": "权限控制示例"
332
+ },
333
+ "skillFile": {
334
+ "template": "skills/authz.skill.md",
335
+ "target": ".kiro/skills/authz.md"
336
+ }
337
+ }
338
+ }
339
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svton/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Svton CLI - Create full-stack applications with NestJS, Next.js, and Taro",
5
5
  "keywords": [
6
6
  "cli",
@@ -30,6 +30,7 @@
30
30
  "files": [
31
31
  "dist",
32
32
  "bin",
33
+ "features.json",
33
34
  "README.md",
34
35
  "LICENSE"
35
36
  ],