@fresh-editor/fresh-editor 0.1.98 → 0.1.99

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/plugins/pkg.ts CHANGED
@@ -40,6 +40,7 @@ const CONFIG_DIR = editor.getConfigDir();
40
40
  const PACKAGES_DIR = editor.pathJoin(CONFIG_DIR, "plugins", "packages");
41
41
  const THEMES_PACKAGES_DIR = editor.pathJoin(CONFIG_DIR, "themes", "packages");
42
42
  const LANGUAGES_PACKAGES_DIR = editor.pathJoin(CONFIG_DIR, "languages", "packages");
43
+ const BUNDLES_PACKAGES_DIR = editor.pathJoin(CONFIG_DIR, "bundles", "packages");
43
44
  const INDEX_DIR = editor.pathJoin(PACKAGES_DIR, ".index");
44
45
  const CACHE_DIR = editor.pathJoin(PACKAGES_DIR, ".cache");
45
46
  const LOCKFILE_PATH = editor.pathJoin(CONFIG_DIR, "fresh.lock");
@@ -58,11 +59,60 @@ const DEFAULT_REGISTRY = "https://github.com/sinelaw/fresh-plugins-registry";
58
59
  // - docs/internal/package-index-template/schemas/package.schema.json
59
60
  // - crates/fresh-editor/plugins/schemas/package.schema.json
60
61
 
62
+ // Bundle language definition (used in fresh.languages[])
63
+ interface BundleLanguage {
64
+ /** Language identifier (e.g., 'elixir', 'heex') */
65
+ id: string;
66
+ /** Grammar configuration */
67
+ grammar?: {
68
+ file: string;
69
+ extensions?: string[];
70
+ firstLine?: string;
71
+ };
72
+ /** Language configuration */
73
+ language?: {
74
+ commentPrefix?: string;
75
+ blockCommentStart?: string;
76
+ blockCommentEnd?: string;
77
+ useTabs?: boolean;
78
+ tabSize?: number;
79
+ autoIndent?: boolean;
80
+ showWhitespaceTabs?: boolean;
81
+ formatter?: {
82
+ command: string;
83
+ args?: string[];
84
+ };
85
+ };
86
+ /** LSP server configuration */
87
+ lsp?: {
88
+ command: string;
89
+ args?: string[];
90
+ autoStart?: boolean;
91
+ initializationOptions?: Record<string, unknown>;
92
+ };
93
+ }
94
+
95
+ // Bundle plugin definition (used in fresh.plugins[])
96
+ interface BundlePlugin {
97
+ /** Plugin entry point file relative to package */
98
+ entry: string;
99
+ }
100
+
101
+ // Bundle theme definition (used in fresh.themes[])
102
+ interface BundleTheme {
103
+ /** Theme JSON file path relative to package */
104
+ file: string;
105
+ /** Display name for the theme */
106
+ name: string;
107
+ /** Theme variant (dark or light) */
108
+ variant?: "dark" | "light";
109
+ }
110
+
61
111
  interface PackageManifest {
62
112
  name: string;
63
113
  version: string;
64
114
  description: string;
65
- type: "plugin" | "theme" | "theme-pack" | "language";
115
+ type: "plugin" | "theme" | "theme-pack" | "language" | "bundle";
66
116
  author?: string;
67
117
  license?: string;
68
118
  repository?: string;
@@ -104,6 +154,12 @@ interface PackageManifest {
104
154
  autoStart?: boolean;
105
155
  initializationOptions?: Record<string, unknown>;
106
156
  };
157
+
158
+ // Bundle fields
159
+ /** Languages included in this bundle */
160
+ languages?: BundleLanguage[];
161
+ /** Plugins included in this bundle */
162
+ plugins?: BundlePlugin[];
107
163
  };
108
164
  keywords?: string[];
109
165
  }
@@ -130,7 +186,7 @@ interface RegistryData {
130
186
  interface InstalledPackage {
131
187
  name: string;
132
188
  path: string;
133
- type: "plugin" | "theme" | "language";
189
+ type: "plugin" | "theme" | "language" | "bundle";
134
190
  source: string;
135
191
  version: string;
136
192
  commit?: string;
@@ -476,9 +532,10 @@ function isRegistrySynced(): boolean {
476
532
  /**
477
533
  * Get list of installed packages
478
534
  */
479
- function getInstalledPackages(type: "plugin" | "theme" | "language"): InstalledPackage[] {
535
+ function getInstalledPackages(type: "plugin" | "theme" | "language" | "bundle"): InstalledPackage[] {
480
536
  const packagesDir = type === "plugin" ? PACKAGES_DIR
481
537
  : type === "theme" ? THEMES_PACKAGES_DIR
538
+ : type === "bundle" ? BUNDLES_PACKAGES_DIR
482
539
  : LANGUAGES_PACKAGES_DIR;
483
540
  const packages: InstalledPackage[] = [];
484
541
 
@@ -573,14 +630,14 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
573
630
  if (!manifest.type) {
574
631
  return {
575
632
  valid: false,
576
- error: "Invalid package.json - missing 'type' field (should be 'plugin', 'theme', or 'language')"
633
+ error: "Invalid package.json - missing 'type' field (should be 'plugin', 'theme', 'language', or 'bundle')"
577
634
  };
578
635
  }
579
636
 
580
- if (manifest.type !== "plugin" && manifest.type !== "theme" && manifest.type !== "language") {
637
+ if (manifest.type !== "plugin" && manifest.type !== "theme" && manifest.type !== "language" && manifest.type !== "bundle") {
581
638
  return {
582
639
  valid: false,
583
- error: `Invalid package.json - 'type' must be 'plugin', 'theme', or 'language', got '${manifest.type}'`
640
+ error: `Invalid package.json - 'type' must be 'plugin', 'theme', 'language', or 'bundle', got '${manifest.type}'`
584
641
  };
585
642
  }
586
643
 
@@ -628,6 +685,67 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
628
685
  return { valid: true, manifest };
629
686
  }
630
687
 
688
+ // For bundles, validate at least one language, plugin, or theme is defined
689
+ if (manifest.type === "bundle") {
690
+ const hasLanguages = manifest.fresh?.languages && manifest.fresh.languages.length > 0;
691
+ const hasPlugins = manifest.fresh?.plugins && manifest.fresh.plugins.length > 0;
692
+ const hasThemes = manifest.fresh?.themes && manifest.fresh.themes.length > 0;
693
+
694
+ if (!hasLanguages && !hasPlugins && !hasThemes) {
695
+ return {
696
+ valid: false,
697
+ error: "Bundle package must define at least one language, plugin, or theme"
698
+ };
699
+ }
700
+
701
+ // Validate each language entry
702
+ if (manifest.fresh?.languages) {
703
+ for (const lang of manifest.fresh.languages) {
704
+ if (!lang.id) {
705
+ return {
706
+ valid: false,
707
+ error: "Bundle language entry missing required 'id' field"
708
+ };
709
+ }
710
+ // Validate grammar file exists if specified
711
+ if (lang.grammar?.file) {
712
+ const grammarPath = editor.pathJoin(packageDir, lang.grammar.file);
713
+ if (!editor.fileExists(grammarPath)) {
714
+ return {
715
+ valid: false,
716
+ error: `Grammar file not found for language '${lang.id}': ${lang.grammar.file}`
717
+ };
718
+ }
719
+ }
720
+ }
721
+ }
722
+
723
+ // Validate each plugin entry
724
+ if (manifest.fresh?.plugins) {
725
+ for (const plugin of manifest.fresh.plugins) {
726
+ if (!plugin.entry) {
727
+ return {
728
+ valid: false,
729
+ error: "Bundle plugin entry missing required 'entry' field"
730
+ };
731
+ }
732
+ const entryPath = editor.pathJoin(packageDir, plugin.entry);
733
+ if (!editor.fileExists(entryPath)) {
734
+ // Try .js as fallback
735
+ const jsEntryPath = entryPath.replace(/\.ts$/, ".js");
736
+ if (!editor.fileExists(jsEntryPath)) {
737
+ return {
738
+ valid: false,
739
+ error: `Plugin entry file not found: ${plugin.entry}`
740
+ };
741
+ }
742
+ }
743
+ }
744
+ }
745
+
746
+ return { valid: true, manifest };
747
+ }
748
+
631
749
  // Themes don't need entry file validation
632
750
  return { valid: true, manifest };
633
751
  }
@@ -646,52 +764,43 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
646
764
  async function installPackage(
647
765
  url: string,
648
766
  name?: string,
649
- type: "plugin" | "theme" | "language" = "plugin",
767
+ _type?: "plugin" | "theme" | "language" | "bundle", // Ignored - type is auto-detected from manifest
650
768
  version?: string
651
769
  ): Promise<boolean> {
652
770
  const parsed = parsePackageUrl(url);
653
771
  const packageName = name || parsed.name;
654
- const packagesDir = type === "plugin" ? PACKAGES_DIR
655
- : type === "theme" ? THEMES_PACKAGES_DIR
656
- : LANGUAGES_PACKAGES_DIR;
657
- const targetDir = editor.pathJoin(packagesDir, packageName);
658
-
659
- if (editor.fileExists(targetDir)) {
660
- editor.setStatus(`Package '${packageName}' is already installed`);
661
- return false;
662
- }
663
-
664
- await ensureDir(packagesDir);
665
772
 
666
773
  editor.setStatus(`Installing ${packageName}...`);
667
774
 
668
775
  if (parsed.isLocal) {
669
776
  // Local path installation: copy directly
670
- return await installFromLocalPath(parsed, packageName, targetDir);
777
+ return await installFromLocalPath(parsed, packageName);
671
778
  } else if (parsed.subpath) {
672
779
  // Remote monorepo installation: clone to temp, copy subdirectory
673
- return await installFromMonorepo(parsed, packageName, targetDir, version);
780
+ return await installFromMonorepo(parsed, packageName, version);
674
781
  } else {
675
782
  // Standard git installation: clone directly
676
- return await installFromRepo(parsed.repoUrl, packageName, targetDir, version);
783
+ return await installFromRepo(parsed.repoUrl, packageName, version);
677
784
  }
678
785
  }
679
786
 
680
787
  /**
681
788
  * Install from a standard git repository (no subpath)
789
+ * Clones to temp first to detect type, then moves to correct location.
682
790
  */
683
791
  async function installFromRepo(
684
792
  repoUrl: string,
685
793
  packageName: string,
686
- targetDir: string,
687
794
  version?: string
688
795
  ): Promise<boolean> {
689
- // Clone the repository
796
+ // Clone to temp directory first to detect package type
797
+ const tempDir = `/tmp/fresh-pkg-clone-${hashString(repoUrl)}-${Date.now()}`;
798
+
690
799
  const cloneArgs = ["clone"];
691
800
  if (!version || version === "latest") {
692
801
  cloneArgs.push("--depth", "1");
693
802
  }
694
- cloneArgs.push(`${repoUrl}`, `${targetDir}`);
803
+ cloneArgs.push(`${repoUrl}`, `${tempDir}`);
695
804
 
696
805
  const result = await gitCommand(cloneArgs);
697
806
 
@@ -707,34 +816,66 @@ async function installFromRepo(
707
816
 
708
817
  // Checkout specific version if requested
709
818
  if (version && version !== "latest") {
710
- const checkoutResult = await checkoutVersion(targetDir, version);
819
+ const checkoutResult = await checkoutVersion(tempDir, version);
711
820
  if (!checkoutResult) {
712
821
  editor.setStatus(`Installed ${packageName} but failed to checkout version ${version}`);
713
822
  }
714
823
  }
715
824
 
716
825
  // Validate package structure
717
- const validation = validatePackage(targetDir, packageName);
826
+ const validation = validatePackage(tempDir, packageName);
718
827
  if (!validation.valid) {
719
828
  editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
720
829
  editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
721
- // Clean up the invalid package
722
- await editor.spawnProcess("rm", ["-rf", targetDir]);
830
+ // Clean up
831
+ await editor.spawnProcess("rm", ["-rf", tempDir]);
723
832
  return false;
724
833
  }
725
834
 
726
835
  const manifest = validation.manifest;
727
836
 
728
- // Dynamically load plugins, reload themes, or load language packs
837
+ // Use manifest name as the authoritative package name
838
+ if (manifest?.name) packageName = manifest.name;
839
+
840
+ // Determine correct target directory based on actual package type
841
+ const actualType = manifest?.type || "plugin";
842
+ const correctPackagesDir = actualType === "plugin" ? PACKAGES_DIR
843
+ : actualType === "theme" ? THEMES_PACKAGES_DIR
844
+ : actualType === "bundle" ? BUNDLES_PACKAGES_DIR
845
+ : LANGUAGES_PACKAGES_DIR;
846
+ const correctTargetDir = editor.pathJoin(correctPackagesDir, packageName);
847
+
848
+ // Check if already installed in correct location
849
+ if (editor.fileExists(correctTargetDir)) {
850
+ editor.setStatus(`Package '${packageName}' is already installed`);
851
+ await editor.spawnProcess("rm", ["-rf", tempDir]);
852
+ return false;
853
+ }
854
+
855
+ // Ensure correct directory exists and move from temp
856
+ await ensureDir(correctPackagesDir);
857
+ const moveResult = await editor.spawnProcess("mv", [tempDir, correctTargetDir]);
858
+ if (moveResult.exit_code !== 0) {
859
+ editor.setStatus(`Failed to install ${packageName}: ${moveResult.stderr}`);
860
+ await editor.spawnProcess("rm", ["-rf", tempDir]);
861
+ return false;
862
+ }
863
+
864
+ // Dynamically load plugins, reload themes, load language packs, or load bundles
729
865
  if (manifest?.type === "plugin" && validation.entryPath) {
730
- await editor.loadPlugin(validation.entryPath);
866
+ // Update entry path to new location
867
+ const newEntryPath = validation.entryPath.replace(tempDir, correctTargetDir);
868
+ await editor.loadPlugin(newEntryPath);
731
869
  editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
732
870
  } else if (manifest?.type === "theme") {
733
871
  editor.reloadThemes();
734
872
  editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
735
873
  } else if (manifest?.type === "language") {
736
- await loadLanguagePack(targetDir, manifest);
874
+ await loadLanguagePack(correctTargetDir, manifest);
737
875
  editor.setStatus(`Installed language pack ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
876
+ } else if (manifest?.type === "bundle") {
877
+ await loadBundle(correctTargetDir, manifest);
878
+ editor.setStatus(`Installed bundle ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
738
879
  } else {
739
880
  editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
740
881
  }
@@ -748,11 +889,11 @@ async function installFromRepo(
748
889
  * - If subpath is specified: copy that subdirectory
749
890
  * - Otherwise: copy the entire directory
750
891
  * - Store the source path for reference
892
+ * - Auto-detect package type from manifest and install to correct directory
751
893
  */
752
894
  async function installFromLocalPath(
753
895
  parsed: ParsedPackageUrl,
754
- packageName: string,
755
- targetDir: string
896
+ packageName: string
756
897
  ): Promise<boolean> {
757
898
  // Resolve the full source path
758
899
  let sourcePath = parsed.repoUrl;
@@ -781,21 +922,48 @@ async function installFromLocalPath(
781
922
  return false;
782
923
  }
783
924
 
784
- // Copy the directory to target
925
+ // Read manifest FIRST to determine actual package type and name
926
+ const manifest = readJsonFile<PackageManifest>(manifestPath);
927
+ if (!manifest) {
928
+ editor.setStatus(`Invalid package.json in ${sourcePath}`);
929
+ return false;
930
+ }
931
+
932
+ // Use manifest name as the authoritative package name
933
+ packageName = manifest.name;
934
+
935
+ // Determine correct target directory based on actual package type
936
+ const actualType = manifest.type || "plugin";
937
+ const correctPackagesDir = actualType === "plugin" ? PACKAGES_DIR
938
+ : actualType === "theme" ? THEMES_PACKAGES_DIR
939
+ : actualType === "bundle" ? BUNDLES_PACKAGES_DIR
940
+ : LANGUAGES_PACKAGES_DIR;
941
+ const correctTargetDir = editor.pathJoin(correctPackagesDir, packageName);
942
+
943
+ // Check if already installed in correct location
944
+ if (editor.fileExists(correctTargetDir)) {
945
+ editor.setStatus(`Package '${packageName}' is already installed`);
946
+ return false;
947
+ }
948
+
949
+ // Ensure correct directory exists
950
+ await ensureDir(correctPackagesDir);
951
+
952
+ // Copy the directory to correct target
785
953
  editor.setStatus(`Copying from ${sourcePath}...`);
786
- const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, targetDir]);
954
+ const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, correctTargetDir]);
787
955
  if (copyResult.exit_code !== 0) {
788
956
  editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
789
957
  return false;
790
958
  }
791
959
 
792
960
  // Validate package structure
793
- const validation = validatePackage(targetDir, packageName);
961
+ const validation = validatePackage(correctTargetDir, packageName);
794
962
  if (!validation.valid) {
795
963
  editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
796
964
  editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
797
965
  // Clean up the invalid package
798
- await editor.spawnProcess("rm", ["-rf", targetDir]);
966
+ await editor.spawnProcess("rm", ["-rf", correctTargetDir]);
799
967
  return false;
800
968
  }
801
969
 
@@ -805,22 +973,23 @@ async function installFromLocalPath(
805
973
  original_url: parsed.subpath ? `${parsed.repoUrl}#${parsed.subpath}` : parsed.repoUrl,
806
974
  installed_at: new Date().toISOString()
807
975
  };
808
- await writeJsonFile(editor.pathJoin(targetDir, ".fresh-source.json"), sourceInfo);
976
+ await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
809
977
 
810
- const manifest = validation.manifest;
811
-
812
- // Dynamically load plugins, reload themes, or load language packs
813
- if (manifest?.type === "plugin" && validation.entryPath) {
978
+ // Dynamically load plugins, reload themes, load language packs, or load bundles
979
+ if (manifest.type === "plugin" && validation.entryPath) {
814
980
  await editor.loadPlugin(validation.entryPath);
815
- editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
816
- } else if (manifest?.type === "theme") {
981
+ editor.setStatus(`Installed and activated ${packageName} v${manifest.version || "unknown"}`);
982
+ } else if (manifest.type === "theme") {
817
983
  editor.reloadThemes();
818
- editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
819
- } else if (manifest?.type === "language") {
820
- await loadLanguagePack(targetDir, manifest);
821
- editor.setStatus(`Installed language pack ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
984
+ editor.setStatus(`Installed theme ${packageName} v${manifest.version || "unknown"}`);
985
+ } else if (manifest.type === "language") {
986
+ await loadLanguagePack(correctTargetDir, manifest);
987
+ editor.setStatus(`Installed language pack ${packageName} v${manifest.version || "unknown"}`);
988
+ } else if (manifest.type === "bundle") {
989
+ await loadBundle(correctTargetDir, manifest);
990
+ editor.setStatus(`Installed bundle ${packageName} v${manifest.version || "unknown"}`);
822
991
  } else {
823
- editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
992
+ editor.setStatus(`Installed ${packageName} v${manifest.version || "unknown"}`);
824
993
  }
825
994
  return true;
826
995
  }
@@ -830,14 +999,13 @@ async function installFromLocalPath(
830
999
  *
831
1000
  * Strategy:
832
1001
  * 1. Clone the repo to a temp directory
833
- * 2. Copy the subdirectory to the target location
834
- * 3. Initialize a new git repo in the target (for updates)
1002
+ * 2. Detect package type from manifest
1003
+ * 3. Copy the subdirectory to the correct target location
835
1004
  * 4. Store the original URL for reference
836
1005
  */
837
1006
  async function installFromMonorepo(
838
1007
  parsed: ParsedPackageUrl,
839
1008
  packageName: string,
840
- targetDir: string,
841
1009
  version?: string
842
1010
  ): Promise<boolean> {
843
1011
  const tempDir = `/tmp/fresh-pkg-${hashString(parsed.repoUrl)}-${Date.now()}`;
@@ -875,26 +1043,47 @@ async function installFromMonorepo(
875
1043
  return false;
876
1044
  }
877
1045
 
878
- // Copy subdirectory to target
879
- editor.setStatus(`Installing ${packageName} from ${parsed.subpath}...`);
880
- const copyResult = await editor.spawnProcess("cp", ["-r", subpathDir, targetDir]);
881
- if (copyResult.exit_code !== 0) {
882
- editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
1046
+ // Validate package structure (validates against subpath dir)
1047
+ const validation = validatePackage(subpathDir, packageName);
1048
+ if (!validation.valid) {
1049
+ editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
1050
+ editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
883
1051
  await editor.spawnProcess("rm", ["-rf", tempDir]);
884
1052
  return false;
885
1053
  }
886
1054
 
887
- // Validate package structure
888
- const validation = validatePackage(targetDir, packageName);
889
- if (!validation.valid) {
890
- editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
891
- editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
892
- // Clean up the invalid package
893
- await editor.spawnProcess("rm", ["-rf", targetDir]);
1055
+ const manifest = validation.manifest;
1056
+
1057
+ // Use manifest name as the authoritative package name
1058
+ if (manifest?.name) packageName = manifest.name;
1059
+
1060
+ // Determine correct target directory based on actual package type
1061
+ const actualType = manifest?.type || "plugin";
1062
+ const correctPackagesDir = actualType === "plugin" ? PACKAGES_DIR
1063
+ : actualType === "theme" ? THEMES_PACKAGES_DIR
1064
+ : actualType === "bundle" ? BUNDLES_PACKAGES_DIR
1065
+ : LANGUAGES_PACKAGES_DIR;
1066
+ const correctTargetDir = editor.pathJoin(correctPackagesDir, packageName);
1067
+
1068
+ // Check if already installed
1069
+ if (editor.fileExists(correctTargetDir)) {
1070
+ editor.setStatus(`Package '${packageName}' is already installed`);
1071
+ await editor.spawnProcess("rm", ["-rf", tempDir]);
1072
+ return false;
1073
+ }
1074
+
1075
+ // Ensure correct directory exists
1076
+ await ensureDir(correctPackagesDir);
1077
+
1078
+ // Copy subdirectory to correct target
1079
+ editor.setStatus(`Installing ${packageName} from ${parsed.subpath}...`);
1080
+ const copyResult = await editor.spawnProcess("cp", ["-r", subpathDir, correctTargetDir]);
1081
+ if (copyResult.exit_code !== 0) {
1082
+ editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
1083
+ await editor.spawnProcess("rm", ["-rf", tempDir]);
894
1084
  return false;
895
1085
  }
896
1086
 
897
- // Initialize git in target for future updates
898
1087
  // Store the original monorepo URL in a .fresh-source file
899
1088
  const sourceInfo = {
900
1089
  repository: parsed.repoUrl,
@@ -902,20 +1091,23 @@ async function installFromMonorepo(
902
1091
  installed_from: `${parsed.repoUrl}#${parsed.subpath}`,
903
1092
  installed_at: new Date().toISOString()
904
1093
  };
905
- await writeJsonFile(editor.pathJoin(targetDir, ".fresh-source.json"), sourceInfo);
1094
+ await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
906
1095
 
907
- const manifest = validation.manifest;
908
-
909
- // Dynamically load plugins, reload themes, or load language packs
1096
+ // Dynamically load plugins, reload themes, load language packs, or load bundles
910
1097
  if (manifest?.type === "plugin" && validation.entryPath) {
911
- await editor.loadPlugin(validation.entryPath);
1098
+ // Update entry path to new location
1099
+ const newEntryPath = validation.entryPath.replace(subpathDir, correctTargetDir);
1100
+ await editor.loadPlugin(newEntryPath);
912
1101
  editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
913
1102
  } else if (manifest?.type === "theme") {
914
1103
  editor.reloadThemes();
915
1104
  editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
916
1105
  } else if (manifest?.type === "language") {
917
- await loadLanguagePack(targetDir, manifest);
1106
+ await loadLanguagePack(correctTargetDir, manifest);
918
1107
  editor.setStatus(`Installed language pack ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
1108
+ } else if (manifest?.type === "bundle") {
1109
+ await loadBundle(correctTargetDir, manifest);
1110
+ editor.setStatus(`Installed bundle ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
919
1111
  } else {
920
1112
  editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
921
1113
  }
@@ -972,6 +1164,90 @@ async function loadLanguagePack(packageDir: string, manifest: PackageManifest):
972
1164
  editor.reloadGrammars();
973
1165
  }
974
1166
 
1167
+ /**
1168
+ * Load a bundle package (register all languages and load all plugins)
1169
+ */
1170
+ async function loadBundle(packageDir: string, manifest: PackageManifest): Promise<void> {
1171
+ const bundleName = manifest.name;
1172
+ editor.debug(`[pkg] Loading bundle: ${bundleName}`);
1173
+
1174
+ // Load all languages from the bundle
1175
+ if (manifest.fresh?.languages) {
1176
+ for (const lang of manifest.fresh.languages) {
1177
+ const langId = lang.id;
1178
+ editor.debug(`[pkg] Loading bundle language: ${langId}`);
1179
+
1180
+ // Register grammar if present
1181
+ if (lang.grammar) {
1182
+ const grammarPath = editor.pathJoin(packageDir, lang.grammar.file);
1183
+ const extensions = lang.grammar.extensions || [];
1184
+ editor.registerGrammar(langId, grammarPath, extensions);
1185
+ }
1186
+
1187
+ // Register language config if present
1188
+ if (lang.language) {
1189
+ const langConfig = lang.language;
1190
+ editor.registerLanguageConfig(langId, {
1191
+ commentPrefix: langConfig.commentPrefix ?? null,
1192
+ blockCommentStart: langConfig.blockCommentStart ?? null,
1193
+ blockCommentEnd: langConfig.blockCommentEnd ?? null,
1194
+ useTabs: langConfig.useTabs ?? null,
1195
+ tabSize: langConfig.tabSize ?? null,
1196
+ autoIndent: langConfig.autoIndent ?? null,
1197
+ showWhitespaceTabs: langConfig.showWhitespaceTabs ?? null,
1198
+ formatter: langConfig.formatter ? {
1199
+ command: langConfig.formatter.command,
1200
+ args: langConfig.formatter.args ?? [],
1201
+ } : null,
1202
+ });
1203
+ }
1204
+
1205
+ // Register LSP server if present
1206
+ if (lang.lsp) {
1207
+ const lsp = lang.lsp;
1208
+ editor.registerLspServer(langId, {
1209
+ command: lsp.command,
1210
+ args: lsp.args ?? [],
1211
+ autoStart: lsp.autoStart ?? null,
1212
+ initializationOptions: lsp.initializationOptions ?? null,
1213
+ });
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ // Load all plugins from the bundle
1219
+ if (manifest.fresh?.plugins) {
1220
+ for (const plugin of manifest.fresh.plugins) {
1221
+ let entryPath = editor.pathJoin(packageDir, plugin.entry);
1222
+
1223
+ // Try .js fallback if .ts doesn't exist
1224
+ if (!editor.fileExists(entryPath) && entryPath.endsWith(".ts")) {
1225
+ const jsPath = entryPath.replace(/\.ts$/, ".js");
1226
+ if (editor.fileExists(jsPath)) {
1227
+ entryPath = jsPath;
1228
+ }
1229
+ }
1230
+
1231
+ if (editor.fileExists(entryPath)) {
1232
+ editor.debug(`[pkg] Loading bundle plugin: ${plugin.entry}`);
1233
+ await editor.loadPlugin(entryPath);
1234
+ } else {
1235
+ editor.warn(`[pkg] Bundle plugin not found: ${plugin.entry}`);
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // Reload themes if bundle contains any (uses same format as theme-packs)
1241
+ if (manifest.fresh?.themes && manifest.fresh.themes.length > 0) {
1242
+ editor.debug(`[pkg] Bundle contains ${manifest.fresh.themes.length} theme(s), reloading themes`);
1243
+ editor.reloadThemes();
1244
+ }
1245
+
1246
+ // Apply grammar changes
1247
+ editor.reloadGrammars();
1248
+ editor.debug(`[pkg] Bundle loaded: ${bundleName}`);
1249
+ }
1250
+
975
1251
  /**
976
1252
  * Checkout a specific version in a package directory
977
1253
  */
@@ -1246,7 +1522,7 @@ interface PackageListItem {
1246
1522
  stars?: number;
1247
1523
  downloads?: number;
1248
1524
  keywords?: string[];
1249
- packageType: "plugin" | "theme" | "language";
1525
+ packageType: "plugin" | "theme" | "language" | "bundle";
1250
1526
  // For installed packages
1251
1527
  installedPackage?: InstalledPackage;
1252
1528
  // For available packages
@@ -1255,7 +1531,7 @@ interface PackageListItem {
1255
1531
 
1256
1532
  // Focus target types for Tab navigation
1257
1533
  type FocusTarget =
1258
- | { type: "filter"; index: number } // 0=All, 1=Installed, 2=Plugins, 3=Themes, 4=Languages
1534
+ | { type: "filter"; index: number } // 0=All, 1=Installed, 2=Plugins, 3=Themes, 4=Languages, 5=Bundles
1259
1535
  | { type: "sync" }
1260
1536
  | { type: "search" }
1261
1537
  | { type: "list" } // Package list (use arrows to navigate)
@@ -1266,7 +1542,7 @@ interface PkgManagerState {
1266
1542
  bufferId: number | null;
1267
1543
  splitId: number | null;
1268
1544
  sourceBufferId: number | null;
1269
- filter: "all" | "installed" | "plugins" | "themes" | "languages";
1545
+ filter: "all" | "installed" | "plugins" | "themes" | "languages" | "bundles";
1270
1546
  searchQuery: string;
1271
1547
  items: PackageListItem[];
1272
1548
  selectedIndex: number;
@@ -1397,9 +1673,10 @@ function buildPackageList(): PackageListItem[] {
1397
1673
  const installedPlugins = getInstalledPackages("plugin");
1398
1674
  const installedThemes = getInstalledPackages("theme");
1399
1675
  const installedLanguages = getInstalledPackages("language");
1676
+ const installedBundles = getInstalledPackages("bundle");
1400
1677
  const installedMap = new Map<string, InstalledPackage>();
1401
1678
 
1402
- for (const pkg of [...installedPlugins, ...installedThemes, ...installedLanguages]) {
1679
+ for (const pkg of [...installedPlugins, ...installedThemes, ...installedLanguages, ...installedBundles]) {
1403
1680
  installedMap.set(pkg.name, pkg);
1404
1681
  items.push({
1405
1682
  type: "installed",
@@ -1513,6 +1790,9 @@ function getFilteredItems(): PackageListItem[] {
1513
1790
  case "languages":
1514
1791
  items = items.filter(i => i.packageType === "language");
1515
1792
  break;
1793
+ case "bundles":
1794
+ items = items.filter(i => i.packageType === "bundle");
1795
+ break;
1516
1796
  }
1517
1797
 
1518
1798
  // Apply search (case insensitive)
@@ -1639,6 +1919,7 @@ function buildListViewEntries(): TextPropertyEntry[] {
1639
1919
  { id: "plugins", label: "Plugins" },
1640
1920
  { id: "themes", label: "Themes" },
1641
1921
  { id: "languages", label: "Languages" },
1922
+ { id: "bundles", label: "Bundles" },
1642
1923
  ];
1643
1924
 
1644
1925
  // Build filter buttons with position tracking
@@ -1720,7 +2001,7 @@ function buildListViewEntries(): TextPropertyEntry[] {
1720
2001
  const isSelected = idx === pkgState.selectedIndex;
1721
2002
  const listFocused = pkgState.focus.type === "list";
1722
2003
  const prefix = isSelected && listFocused ? "▸" : " ";
1723
- const typeTag = item.packageType === "theme" ? "T" : item.packageType === "language" ? "L" : "P";
2004
+ const typeTag = item.packageType === "theme" ? "T" : item.packageType === "language" ? "L" : item.packageType === "bundle" ? "B" : "P";
1724
2005
  const name = item.name.length > 22 ? item.name.slice(0, 21) + "…" : item.name;
1725
2006
  const line = `${prefix} ${name.padEnd(22)} [${typeTag}]`;
1726
2007
  leftLines.push({ text: line, type: "package-row", selected: isSelected, installed: false });
@@ -2126,6 +2407,7 @@ function getFocusOrder(): FocusTarget[] {
2126
2407
  { type: "filter", index: 2 }, // Plugins
2127
2408
  { type: "filter", index: 3 }, // Themes
2128
2409
  { type: "filter", index: 4 }, // Languages
2410
+ { type: "filter", index: 5 }, // Bundles
2129
2411
  { type: "sync" },
2130
2412
  { type: "list" },
2131
2413
  ];
@@ -2209,7 +2491,7 @@ globalThis.pkg_activate = async function(): Promise<void> {
2209
2491
 
2210
2492
  // Handle filter button activation
2211
2493
  if (focus.type === "filter") {
2212
- const filters = ["all", "installed", "plugins", "themes", "languages"] as const;
2494
+ const filters = ["all", "installed", "plugins", "themes", "languages", "bundles"] as const;
2213
2495
  pkgState.filter = filters[focus.index];
2214
2496
  pkgState.selectedIndex = 0;
2215
2497
  pkgState.items = buildPackageList();
@@ -2617,10 +2899,11 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
2617
2899
  // are available via the package manager UI and don't need global command palette entries.
2618
2900
 
2619
2901
  // =============================================================================
2620
- // Startup: Load installed language packs
2902
+ // Startup: Load installed language packs and bundles
2621
2903
  // =============================================================================
2622
2904
 
2623
- (async function loadInstalledLanguagePacks() {
2905
+ (async function loadInstalledPackages() {
2906
+ // Load language packs
2624
2907
  const languages = getInstalledPackages("language");
2625
2908
  for (const pkg of languages) {
2626
2909
  if (pkg.manifest) {
@@ -2631,6 +2914,18 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
2631
2914
  if (languages.length > 0) {
2632
2915
  editor.debug(`[pkg] Loaded ${languages.length} language pack(s)`);
2633
2916
  }
2917
+
2918
+ // Load bundles
2919
+ const bundles = getInstalledPackages("bundle");
2920
+ for (const pkg of bundles) {
2921
+ if (pkg.manifest) {
2922
+ editor.debug(`[pkg] Loading bundle: ${pkg.name}`);
2923
+ await loadBundle(pkg.path, pkg.manifest);
2924
+ }
2925
+ }
2926
+ if (bundles.length > 0) {
2927
+ editor.debug(`[pkg] Loaded ${bundles.length} bundle(s)`);
2928
+ }
2634
2929
  })();
2635
2930
 
2636
2931
  editor.debug("Package Manager plugin loaded");