@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/CHANGELOG.md +48 -0
- package/package.json +1 -1
- package/plugins/code-tour.ts +397 -0
- package/plugins/lib/fresh.d.ts +22 -3
- package/plugins/pkg.ts +374 -79
- package/plugins/schemas/package.schema.json +146 -1
- package/plugins/schemas/tour.schema.json +103 -0
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 '
|
|
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 '
|
|
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
|
-
|
|
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
|
|
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,
|
|
780
|
+
return await installFromMonorepo(parsed, packageName, version);
|
|
674
781
|
} else {
|
|
675
782
|
// Standard git installation: clone directly
|
|
676
|
-
return await installFromRepo(parsed.repoUrl, packageName,
|
|
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
|
|
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}`, `${
|
|
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(
|
|
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(
|
|
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
|
|
722
|
-
await editor.spawnProcess("rm", ["-rf",
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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,
|
|
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(
|
|
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",
|
|
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(
|
|
976
|
+
await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
|
|
809
977
|
|
|
810
|
-
|
|
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}
|
|
816
|
-
} else if (manifest
|
|
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}
|
|
819
|
-
} else if (manifest
|
|
820
|
-
await loadLanguagePack(
|
|
821
|
-
editor.setStatus(`Installed language pack ${packageName}
|
|
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}
|
|
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.
|
|
834
|
-
* 3.
|
|
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
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
editor.setStatus(`Failed to
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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(
|
|
1094
|
+
await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
|
|
906
1095
|
|
|
907
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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");
|