@fresh-editor/fresh-editor 0.1.97 → 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 +76 -0
- package/package.json +1 -1
- package/plugins/code-tour.ts +397 -0
- package/plugins/lib/fresh.d.ts +26 -3
- package/plugins/pkg.ts +487 -72
- 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;
|
|
@@ -155,12 +211,14 @@ interface Lockfile {
|
|
|
155
211
|
// =============================================================================
|
|
156
212
|
|
|
157
213
|
interface ParsedPackageUrl {
|
|
158
|
-
/** The base git repository URL (without fragment) */
|
|
214
|
+
/** The base git repository URL or local path (without fragment) */
|
|
159
215
|
repoUrl: string;
|
|
160
|
-
/** Optional path within the repository (from fragment) */
|
|
216
|
+
/** Optional path within the repository/directory (from fragment) */
|
|
161
217
|
subpath: string | null;
|
|
162
218
|
/** Extracted package name */
|
|
163
219
|
name: string;
|
|
220
|
+
/** Whether this is a local file path (not a remote URL) */
|
|
221
|
+
isLocal: boolean;
|
|
164
222
|
}
|
|
165
223
|
|
|
166
224
|
// =============================================================================
|
|
@@ -206,6 +264,29 @@ async function gitCommand(args: string[]): Promise<{ exit_code: number; stdout:
|
|
|
206
264
|
return result;
|
|
207
265
|
}
|
|
208
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Check if a string is a local file path (not a URL).
|
|
269
|
+
*/
|
|
270
|
+
function isLocalPath(str: string): boolean {
|
|
271
|
+
// Absolute paths start with /
|
|
272
|
+
if (str.startsWith("/")) return true;
|
|
273
|
+
// Windows absolute paths (C:\, D:\, etc.)
|
|
274
|
+
if (/^[A-Za-z]:[\\\/]/.test(str)) return true;
|
|
275
|
+
// Relative paths starting with . or ..
|
|
276
|
+
if (str.startsWith("./") || str.startsWith("../")) return true;
|
|
277
|
+
// Home directory expansion
|
|
278
|
+
if (str.startsWith("~/")) return true;
|
|
279
|
+
// Not a URL scheme (http://, https://, git://, ssh://, file://)
|
|
280
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(str)) {
|
|
281
|
+
// If it doesn't look like a URL and doesn't contain @, it's probably a path
|
|
282
|
+
// (git@github.com:user/repo is a git URL)
|
|
283
|
+
if (!str.includes("@") || str.startsWith("/")) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
209
290
|
/**
|
|
210
291
|
* Parse a package URL that may contain a subpath fragment.
|
|
211
292
|
*
|
|
@@ -213,6 +294,8 @@ async function gitCommand(args: string[]): Promise<{ exit_code: number; stdout:
|
|
|
213
294
|
* - `https://github.com/user/repo` - standard repo
|
|
214
295
|
* - `https://github.com/user/repo#path/to/plugin` - monorepo with subpath
|
|
215
296
|
* - `https://github.com/user/repo.git#packages/my-plugin` - with .git suffix
|
|
297
|
+
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
298
|
+
* - `/path/to/local/package` - direct local package path
|
|
216
299
|
*
|
|
217
300
|
* The fragment (after #) specifies a subdirectory within the repo.
|
|
218
301
|
*/
|
|
@@ -234,19 +317,22 @@ function parsePackageUrl(url: string): ParsedPackageUrl {
|
|
|
234
317
|
repoUrl = url;
|
|
235
318
|
}
|
|
236
319
|
|
|
320
|
+
// Determine if this is a local path
|
|
321
|
+
const isLocal = isLocalPath(repoUrl);
|
|
322
|
+
|
|
237
323
|
// Extract package name
|
|
238
324
|
let name: string;
|
|
239
325
|
if (subpath) {
|
|
240
|
-
// For monorepo, use the last component of the subpath
|
|
326
|
+
// For monorepo/directory, use the last component of the subpath
|
|
241
327
|
const parts = subpath.split("/");
|
|
242
328
|
name = parts[parts.length - 1].replace(/^fresh-/, "");
|
|
243
329
|
} else {
|
|
244
|
-
// For regular repo, use
|
|
330
|
+
// For regular repo/path, use the last component
|
|
245
331
|
const match = repoUrl.match(/\/([^\/]+?)(\.git)?$/);
|
|
246
332
|
name = match ? match[1].replace(/^fresh-/, "") : "unknown";
|
|
247
333
|
}
|
|
248
334
|
|
|
249
|
-
return { repoUrl, subpath, name };
|
|
335
|
+
return { repoUrl, subpath, name, isLocal };
|
|
250
336
|
}
|
|
251
337
|
|
|
252
338
|
/**
|
|
@@ -446,9 +532,10 @@ function isRegistrySynced(): boolean {
|
|
|
446
532
|
/**
|
|
447
533
|
* Get list of installed packages
|
|
448
534
|
*/
|
|
449
|
-
function getInstalledPackages(type: "plugin" | "theme" | "language"): InstalledPackage[] {
|
|
535
|
+
function getInstalledPackages(type: "plugin" | "theme" | "language" | "bundle"): InstalledPackage[] {
|
|
450
536
|
const packagesDir = type === "plugin" ? PACKAGES_DIR
|
|
451
537
|
: type === "theme" ? THEMES_PACKAGES_DIR
|
|
538
|
+
: type === "bundle" ? BUNDLES_PACKAGES_DIR
|
|
452
539
|
: LANGUAGES_PACKAGES_DIR;
|
|
453
540
|
const packages: InstalledPackage[] = [];
|
|
454
541
|
|
|
@@ -543,14 +630,14 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
|
|
|
543
630
|
if (!manifest.type) {
|
|
544
631
|
return {
|
|
545
632
|
valid: false,
|
|
546
|
-
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')"
|
|
547
634
|
};
|
|
548
635
|
}
|
|
549
636
|
|
|
550
|
-
if (manifest.type !== "plugin" && manifest.type !== "theme" && manifest.type !== "language") {
|
|
637
|
+
if (manifest.type !== "plugin" && manifest.type !== "theme" && manifest.type !== "language" && manifest.type !== "bundle") {
|
|
551
638
|
return {
|
|
552
639
|
valid: false,
|
|
553
|
-
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}'`
|
|
554
641
|
};
|
|
555
642
|
}
|
|
556
643
|
|
|
@@ -598,64 +685,122 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
|
|
|
598
685
|
return { valid: true, manifest };
|
|
599
686
|
}
|
|
600
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
|
+
|
|
601
749
|
// Themes don't need entry file validation
|
|
602
750
|
return { valid: true, manifest };
|
|
603
751
|
}
|
|
604
752
|
|
|
605
753
|
/**
|
|
606
|
-
* Install a package from git URL.
|
|
754
|
+
* Install a package from git URL or local path.
|
|
607
755
|
*
|
|
608
|
-
* Supports
|
|
609
|
-
* - `https://github.com/user/repo
|
|
756
|
+
* Supports:
|
|
757
|
+
* - `https://github.com/user/repo` - standard git repo
|
|
758
|
+
* - `https://github.com/user/repo#packages/my-plugin` - monorepo with subpath
|
|
759
|
+
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
760
|
+
* - `/path/to/local/package` - direct local package path
|
|
610
761
|
*
|
|
611
|
-
* For subpath packages, clones to temp directory and copies the subdirectory.
|
|
762
|
+
* For subpath packages, clones/copies to temp directory and copies the subdirectory.
|
|
612
763
|
*/
|
|
613
764
|
async function installPackage(
|
|
614
765
|
url: string,
|
|
615
766
|
name?: string,
|
|
616
|
-
|
|
767
|
+
_type?: "plugin" | "theme" | "language" | "bundle", // Ignored - type is auto-detected from manifest
|
|
617
768
|
version?: string
|
|
618
769
|
): Promise<boolean> {
|
|
619
770
|
const parsed = parsePackageUrl(url);
|
|
620
771
|
const packageName = name || parsed.name;
|
|
621
|
-
const packagesDir = type === "plugin" ? PACKAGES_DIR
|
|
622
|
-
: type === "theme" ? THEMES_PACKAGES_DIR
|
|
623
|
-
: LANGUAGES_PACKAGES_DIR;
|
|
624
|
-
const targetDir = editor.pathJoin(packagesDir, packageName);
|
|
625
|
-
|
|
626
|
-
if (editor.fileExists(targetDir)) {
|
|
627
|
-
editor.setStatus(`Package '${packageName}' is already installed`);
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
await ensureDir(packagesDir);
|
|
632
772
|
|
|
633
773
|
editor.setStatus(`Installing ${packageName}...`);
|
|
634
774
|
|
|
635
|
-
if (parsed.
|
|
636
|
-
//
|
|
637
|
-
return await
|
|
775
|
+
if (parsed.isLocal) {
|
|
776
|
+
// Local path installation: copy directly
|
|
777
|
+
return await installFromLocalPath(parsed, packageName);
|
|
778
|
+
} else if (parsed.subpath) {
|
|
779
|
+
// Remote monorepo installation: clone to temp, copy subdirectory
|
|
780
|
+
return await installFromMonorepo(parsed, packageName, version);
|
|
638
781
|
} else {
|
|
639
|
-
// Standard installation: clone directly
|
|
640
|
-
return await installFromRepo(parsed.repoUrl, packageName,
|
|
782
|
+
// Standard git installation: clone directly
|
|
783
|
+
return await installFromRepo(parsed.repoUrl, packageName, version);
|
|
641
784
|
}
|
|
642
785
|
}
|
|
643
786
|
|
|
644
787
|
/**
|
|
645
788
|
* Install from a standard git repository (no subpath)
|
|
789
|
+
* Clones to temp first to detect type, then moves to correct location.
|
|
646
790
|
*/
|
|
647
791
|
async function installFromRepo(
|
|
648
792
|
repoUrl: string,
|
|
649
793
|
packageName: string,
|
|
650
|
-
targetDir: string,
|
|
651
794
|
version?: string
|
|
652
795
|
): Promise<boolean> {
|
|
653
|
-
// Clone
|
|
796
|
+
// Clone to temp directory first to detect package type
|
|
797
|
+
const tempDir = `/tmp/fresh-pkg-clone-${hashString(repoUrl)}-${Date.now()}`;
|
|
798
|
+
|
|
654
799
|
const cloneArgs = ["clone"];
|
|
655
800
|
if (!version || version === "latest") {
|
|
656
801
|
cloneArgs.push("--depth", "1");
|
|
657
802
|
}
|
|
658
|
-
cloneArgs.push(`${repoUrl}`, `${
|
|
803
|
+
cloneArgs.push(`${repoUrl}`, `${tempDir}`);
|
|
659
804
|
|
|
660
805
|
const result = await gitCommand(cloneArgs);
|
|
661
806
|
|
|
@@ -671,53 +816,196 @@ async function installFromRepo(
|
|
|
671
816
|
|
|
672
817
|
// Checkout specific version if requested
|
|
673
818
|
if (version && version !== "latest") {
|
|
674
|
-
const checkoutResult = await checkoutVersion(
|
|
819
|
+
const checkoutResult = await checkoutVersion(tempDir, version);
|
|
675
820
|
if (!checkoutResult) {
|
|
676
821
|
editor.setStatus(`Installed ${packageName} but failed to checkout version ${version}`);
|
|
677
822
|
}
|
|
678
823
|
}
|
|
679
824
|
|
|
680
825
|
// Validate package structure
|
|
681
|
-
const validation = validatePackage(
|
|
826
|
+
const validation = validatePackage(tempDir, packageName);
|
|
682
827
|
if (!validation.valid) {
|
|
683
828
|
editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
|
|
684
829
|
editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
|
|
685
|
-
// Clean up
|
|
686
|
-
await editor.spawnProcess("rm", ["-rf",
|
|
830
|
+
// Clean up
|
|
831
|
+
await editor.spawnProcess("rm", ["-rf", tempDir]);
|
|
687
832
|
return false;
|
|
688
833
|
}
|
|
689
834
|
|
|
690
835
|
const manifest = validation.manifest;
|
|
691
836
|
|
|
692
|
-
//
|
|
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
|
|
693
865
|
if (manifest?.type === "plugin" && validation.entryPath) {
|
|
694
|
-
|
|
866
|
+
// Update entry path to new location
|
|
867
|
+
const newEntryPath = validation.entryPath.replace(tempDir, correctTargetDir);
|
|
868
|
+
await editor.loadPlugin(newEntryPath);
|
|
695
869
|
editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
696
870
|
} else if (manifest?.type === "theme") {
|
|
697
871
|
editor.reloadThemes();
|
|
698
872
|
editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
699
873
|
} else if (manifest?.type === "language") {
|
|
700
|
-
await loadLanguagePack(
|
|
874
|
+
await loadLanguagePack(correctTargetDir, manifest);
|
|
701
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}` : ""}`);
|
|
702
879
|
} else {
|
|
703
880
|
editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
704
881
|
}
|
|
705
882
|
return true;
|
|
706
883
|
}
|
|
707
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Install from a local file path.
|
|
887
|
+
*
|
|
888
|
+
* Strategy:
|
|
889
|
+
* - If subpath is specified: copy that subdirectory
|
|
890
|
+
* - Otherwise: copy the entire directory
|
|
891
|
+
* - Store the source path for reference
|
|
892
|
+
* - Auto-detect package type from manifest and install to correct directory
|
|
893
|
+
*/
|
|
894
|
+
async function installFromLocalPath(
|
|
895
|
+
parsed: ParsedPackageUrl,
|
|
896
|
+
packageName: string
|
|
897
|
+
): Promise<boolean> {
|
|
898
|
+
// Resolve the full source path
|
|
899
|
+
let sourcePath = parsed.repoUrl;
|
|
900
|
+
|
|
901
|
+
// Handle home directory expansion
|
|
902
|
+
if (sourcePath.startsWith("~/")) {
|
|
903
|
+
const home = editor.getEnv("HOME") || editor.getEnv("USERPROFILE") || "";
|
|
904
|
+
sourcePath = editor.pathJoin(home, sourcePath.slice(2));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// If there's a subpath, append it
|
|
908
|
+
if (parsed.subpath) {
|
|
909
|
+
sourcePath = editor.pathJoin(sourcePath, parsed.subpath);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Check if source exists
|
|
913
|
+
if (!editor.fileExists(sourcePath)) {
|
|
914
|
+
editor.setStatus(`Local path not found: ${sourcePath}`);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Check if it's a directory (by checking for package.json)
|
|
919
|
+
const manifestPath = editor.pathJoin(sourcePath, "package.json");
|
|
920
|
+
if (!editor.fileExists(manifestPath)) {
|
|
921
|
+
editor.setStatus(`Not a valid package (no package.json): ${sourcePath}`);
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
|
|
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
|
|
953
|
+
editor.setStatus(`Copying from ${sourcePath}...`);
|
|
954
|
+
const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, correctTargetDir]);
|
|
955
|
+
if (copyResult.exit_code !== 0) {
|
|
956
|
+
editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Validate package structure
|
|
961
|
+
const validation = validatePackage(correctTargetDir, packageName);
|
|
962
|
+
if (!validation.valid) {
|
|
963
|
+
editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
|
|
964
|
+
editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
|
|
965
|
+
// Clean up the invalid package
|
|
966
|
+
await editor.spawnProcess("rm", ["-rf", correctTargetDir]);
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Store the source path for reference
|
|
971
|
+
const sourceInfo = {
|
|
972
|
+
local_path: sourcePath,
|
|
973
|
+
original_url: parsed.subpath ? `${parsed.repoUrl}#${parsed.subpath}` : parsed.repoUrl,
|
|
974
|
+
installed_at: new Date().toISOString()
|
|
975
|
+
};
|
|
976
|
+
await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
|
|
977
|
+
|
|
978
|
+
// Dynamically load plugins, reload themes, load language packs, or load bundles
|
|
979
|
+
if (manifest.type === "plugin" && validation.entryPath) {
|
|
980
|
+
await editor.loadPlugin(validation.entryPath);
|
|
981
|
+
editor.setStatus(`Installed and activated ${packageName} v${manifest.version || "unknown"}`);
|
|
982
|
+
} else if (manifest.type === "theme") {
|
|
983
|
+
editor.reloadThemes();
|
|
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"}`);
|
|
991
|
+
} else {
|
|
992
|
+
editor.setStatus(`Installed ${packageName} v${manifest.version || "unknown"}`);
|
|
993
|
+
}
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
|
|
708
997
|
/**
|
|
709
998
|
* Install from a monorepo (URL with subpath fragment)
|
|
710
999
|
*
|
|
711
1000
|
* Strategy:
|
|
712
1001
|
* 1. Clone the repo to a temp directory
|
|
713
|
-
* 2.
|
|
714
|
-
* 3.
|
|
1002
|
+
* 2. Detect package type from manifest
|
|
1003
|
+
* 3. Copy the subdirectory to the correct target location
|
|
715
1004
|
* 4. Store the original URL for reference
|
|
716
1005
|
*/
|
|
717
1006
|
async function installFromMonorepo(
|
|
718
1007
|
parsed: ParsedPackageUrl,
|
|
719
1008
|
packageName: string,
|
|
720
|
-
targetDir: string,
|
|
721
1009
|
version?: string
|
|
722
1010
|
): Promise<boolean> {
|
|
723
1011
|
const tempDir = `/tmp/fresh-pkg-${hashString(parsed.repoUrl)}-${Date.now()}`;
|
|
@@ -755,26 +1043,47 @@ async function installFromMonorepo(
|
|
|
755
1043
|
return false;
|
|
756
1044
|
}
|
|
757
1045
|
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
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}`);
|
|
763
1051
|
await editor.spawnProcess("rm", ["-rf", tempDir]);
|
|
764
1052
|
return false;
|
|
765
1053
|
}
|
|
766
1054
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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]);
|
|
774
1084
|
return false;
|
|
775
1085
|
}
|
|
776
1086
|
|
|
777
|
-
// Initialize git in target for future updates
|
|
778
1087
|
// Store the original monorepo URL in a .fresh-source file
|
|
779
1088
|
const sourceInfo = {
|
|
780
1089
|
repository: parsed.repoUrl,
|
|
@@ -782,20 +1091,23 @@ async function installFromMonorepo(
|
|
|
782
1091
|
installed_from: `${parsed.repoUrl}#${parsed.subpath}`,
|
|
783
1092
|
installed_at: new Date().toISOString()
|
|
784
1093
|
};
|
|
785
|
-
await writeJsonFile(editor.pathJoin(
|
|
786
|
-
|
|
787
|
-
const manifest = validation.manifest;
|
|
1094
|
+
await writeJsonFile(editor.pathJoin(correctTargetDir, ".fresh-source.json"), sourceInfo);
|
|
788
1095
|
|
|
789
|
-
// Dynamically load plugins, reload themes,
|
|
1096
|
+
// Dynamically load plugins, reload themes, load language packs, or load bundles
|
|
790
1097
|
if (manifest?.type === "plugin" && validation.entryPath) {
|
|
791
|
-
|
|
1098
|
+
// Update entry path to new location
|
|
1099
|
+
const newEntryPath = validation.entryPath.replace(subpathDir, correctTargetDir);
|
|
1100
|
+
await editor.loadPlugin(newEntryPath);
|
|
792
1101
|
editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
793
1102
|
} else if (manifest?.type === "theme") {
|
|
794
1103
|
editor.reloadThemes();
|
|
795
1104
|
editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
796
1105
|
} else if (manifest?.type === "language") {
|
|
797
|
-
await loadLanguagePack(
|
|
1106
|
+
await loadLanguagePack(correctTargetDir, manifest);
|
|
798
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}` : ""}`);
|
|
799
1111
|
} else {
|
|
800
1112
|
editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
801
1113
|
}
|
|
@@ -852,6 +1164,90 @@ async function loadLanguagePack(packageDir: string, manifest: PackageManifest):
|
|
|
852
1164
|
editor.reloadGrammars();
|
|
853
1165
|
}
|
|
854
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
|
+
|
|
855
1251
|
/**
|
|
856
1252
|
* Checkout a specific version in a package directory
|
|
857
1253
|
*/
|
|
@@ -1126,7 +1522,7 @@ interface PackageListItem {
|
|
|
1126
1522
|
stars?: number;
|
|
1127
1523
|
downloads?: number;
|
|
1128
1524
|
keywords?: string[];
|
|
1129
|
-
packageType: "plugin" | "theme" | "language";
|
|
1525
|
+
packageType: "plugin" | "theme" | "language" | "bundle";
|
|
1130
1526
|
// For installed packages
|
|
1131
1527
|
installedPackage?: InstalledPackage;
|
|
1132
1528
|
// For available packages
|
|
@@ -1135,7 +1531,7 @@ interface PackageListItem {
|
|
|
1135
1531
|
|
|
1136
1532
|
// Focus target types for Tab navigation
|
|
1137
1533
|
type FocusTarget =
|
|
1138
|
-
| { 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
|
|
1139
1535
|
| { type: "sync" }
|
|
1140
1536
|
| { type: "search" }
|
|
1141
1537
|
| { type: "list" } // Package list (use arrows to navigate)
|
|
@@ -1146,7 +1542,7 @@ interface PkgManagerState {
|
|
|
1146
1542
|
bufferId: number | null;
|
|
1147
1543
|
splitId: number | null;
|
|
1148
1544
|
sourceBufferId: number | null;
|
|
1149
|
-
filter: "all" | "installed" | "plugins" | "themes" | "languages";
|
|
1545
|
+
filter: "all" | "installed" | "plugins" | "themes" | "languages" | "bundles";
|
|
1150
1546
|
searchQuery: string;
|
|
1151
1547
|
items: PackageListItem[];
|
|
1152
1548
|
selectedIndex: number;
|
|
@@ -1277,9 +1673,10 @@ function buildPackageList(): PackageListItem[] {
|
|
|
1277
1673
|
const installedPlugins = getInstalledPackages("plugin");
|
|
1278
1674
|
const installedThemes = getInstalledPackages("theme");
|
|
1279
1675
|
const installedLanguages = getInstalledPackages("language");
|
|
1676
|
+
const installedBundles = getInstalledPackages("bundle");
|
|
1280
1677
|
const installedMap = new Map<string, InstalledPackage>();
|
|
1281
1678
|
|
|
1282
|
-
for (const pkg of [...installedPlugins, ...installedThemes, ...installedLanguages]) {
|
|
1679
|
+
for (const pkg of [...installedPlugins, ...installedThemes, ...installedLanguages, ...installedBundles]) {
|
|
1283
1680
|
installedMap.set(pkg.name, pkg);
|
|
1284
1681
|
items.push({
|
|
1285
1682
|
type: "installed",
|
|
@@ -1393,6 +1790,9 @@ function getFilteredItems(): PackageListItem[] {
|
|
|
1393
1790
|
case "languages":
|
|
1394
1791
|
items = items.filter(i => i.packageType === "language");
|
|
1395
1792
|
break;
|
|
1793
|
+
case "bundles":
|
|
1794
|
+
items = items.filter(i => i.packageType === "bundle");
|
|
1795
|
+
break;
|
|
1396
1796
|
}
|
|
1397
1797
|
|
|
1398
1798
|
// Apply search (case insensitive)
|
|
@@ -1519,6 +1919,7 @@ function buildListViewEntries(): TextPropertyEntry[] {
|
|
|
1519
1919
|
{ id: "plugins", label: "Plugins" },
|
|
1520
1920
|
{ id: "themes", label: "Themes" },
|
|
1521
1921
|
{ id: "languages", label: "Languages" },
|
|
1922
|
+
{ id: "bundles", label: "Bundles" },
|
|
1522
1923
|
];
|
|
1523
1924
|
|
|
1524
1925
|
// Build filter buttons with position tracking
|
|
@@ -1600,7 +2001,7 @@ function buildListViewEntries(): TextPropertyEntry[] {
|
|
|
1600
2001
|
const isSelected = idx === pkgState.selectedIndex;
|
|
1601
2002
|
const listFocused = pkgState.focus.type === "list";
|
|
1602
2003
|
const prefix = isSelected && listFocused ? "▸" : " ";
|
|
1603
|
-
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";
|
|
1604
2005
|
const name = item.name.length > 22 ? item.name.slice(0, 21) + "…" : item.name;
|
|
1605
2006
|
const line = `${prefix} ${name.padEnd(22)} [${typeTag}]`;
|
|
1606
2007
|
leftLines.push({ text: line, type: "package-row", selected: isSelected, installed: false });
|
|
@@ -2006,6 +2407,7 @@ function getFocusOrder(): FocusTarget[] {
|
|
|
2006
2407
|
{ type: "filter", index: 2 }, // Plugins
|
|
2007
2408
|
{ type: "filter", index: 3 }, // Themes
|
|
2008
2409
|
{ type: "filter", index: 4 }, // Languages
|
|
2410
|
+
{ type: "filter", index: 5 }, // Bundles
|
|
2009
2411
|
{ type: "sync" },
|
|
2010
2412
|
{ type: "list" },
|
|
2011
2413
|
];
|
|
@@ -2089,7 +2491,7 @@ globalThis.pkg_activate = async function(): Promise<void> {
|
|
|
2089
2491
|
|
|
2090
2492
|
// Handle filter button activation
|
|
2091
2493
|
if (focus.type === "filter") {
|
|
2092
|
-
const filters = ["all", "installed", "plugins", "themes", "languages"] as const;
|
|
2494
|
+
const filters = ["all", "installed", "plugins", "themes", "languages", "bundles"] as const;
|
|
2093
2495
|
pkgState.filter = filters[focus.index];
|
|
2094
2496
|
pkgState.selectedIndex = 0;
|
|
2095
2497
|
pkgState.items = buildPackageList();
|
|
@@ -2497,10 +2899,11 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
|
|
|
2497
2899
|
// are available via the package manager UI and don't need global command palette entries.
|
|
2498
2900
|
|
|
2499
2901
|
// =============================================================================
|
|
2500
|
-
// Startup: Load installed language packs
|
|
2902
|
+
// Startup: Load installed language packs and bundles
|
|
2501
2903
|
// =============================================================================
|
|
2502
2904
|
|
|
2503
|
-
(async function
|
|
2905
|
+
(async function loadInstalledPackages() {
|
|
2906
|
+
// Load language packs
|
|
2504
2907
|
const languages = getInstalledPackages("language");
|
|
2505
2908
|
for (const pkg of languages) {
|
|
2506
2909
|
if (pkg.manifest) {
|
|
@@ -2511,6 +2914,18 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
|
|
|
2511
2914
|
if (languages.length > 0) {
|
|
2512
2915
|
editor.debug(`[pkg] Loaded ${languages.length} language pack(s)`);
|
|
2513
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
|
+
}
|
|
2514
2929
|
})();
|
|
2515
2930
|
|
|
2516
2931
|
editor.debug("Package Manager plugin loaded");
|