@symbiosis-lab/moss-api 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.d.mts +409 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +754 -1
- package/dist/index.mjs.map +1 -1
- package/dist/testing/index.d.mts +33 -0
- package/dist/testing/index.d.mts.map +1 -1
- package/dist/testing/index.mjs +72 -0
- package/dist/testing/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -541,6 +541,570 @@ async function executeBinary(options) {
|
|
|
541
541
|
};
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/utils/platform.ts
|
|
546
|
+
/**
|
|
547
|
+
* Platform detection utilities for Moss plugins
|
|
548
|
+
*
|
|
549
|
+
* Detects the current operating system and architecture to enable
|
|
550
|
+
* platform-specific binary downloads and operations.
|
|
551
|
+
*/
|
|
552
|
+
let cachedPlatform = null;
|
|
553
|
+
/**
|
|
554
|
+
* Detect the current platform (OS and architecture)
|
|
555
|
+
*
|
|
556
|
+
* Uses system commands to detect the platform:
|
|
557
|
+
* - On macOS/Linux: `uname -s` for OS, `uname -m` for architecture
|
|
558
|
+
* - On Windows: Falls back to environment variables and defaults
|
|
559
|
+
*
|
|
560
|
+
* Results are cached after the first call.
|
|
561
|
+
*
|
|
562
|
+
* @returns Platform information including OS, architecture, and combined key
|
|
563
|
+
* @throws Error if platform detection fails or platform is unsupported
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```typescript
|
|
567
|
+
* const platform = await getPlatformInfo();
|
|
568
|
+
* console.log(platform.platformKey); // "darwin-arm64"
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
async function getPlatformInfo() {
|
|
572
|
+
if (cachedPlatform) return cachedPlatform;
|
|
573
|
+
const os = await detectOS();
|
|
574
|
+
const arch = await detectArch(os);
|
|
575
|
+
const platformKey = `${os}-${arch}`;
|
|
576
|
+
const supportedPlatforms = [
|
|
577
|
+
"darwin-arm64",
|
|
578
|
+
"darwin-x64",
|
|
579
|
+
"linux-x64",
|
|
580
|
+
"windows-x64"
|
|
581
|
+
];
|
|
582
|
+
if (!supportedPlatforms.includes(platformKey)) throw new Error(`Unsupported platform: ${platformKey}. Supported platforms: ${supportedPlatforms.join(", ")}`);
|
|
583
|
+
cachedPlatform = {
|
|
584
|
+
os,
|
|
585
|
+
arch,
|
|
586
|
+
platformKey
|
|
587
|
+
};
|
|
588
|
+
return cachedPlatform;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Clear the cached platform info
|
|
592
|
+
*
|
|
593
|
+
* Useful for testing or when platform detection needs to be re-run.
|
|
594
|
+
*
|
|
595
|
+
* @internal
|
|
596
|
+
*/
|
|
597
|
+
function clearPlatformCache() {
|
|
598
|
+
cachedPlatform = null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Detect the operating system
|
|
602
|
+
*/
|
|
603
|
+
async function detectOS() {
|
|
604
|
+
try {
|
|
605
|
+
const result = await executeBinary({
|
|
606
|
+
binaryPath: "uname",
|
|
607
|
+
args: ["-s"],
|
|
608
|
+
timeoutMs: 5e3
|
|
609
|
+
});
|
|
610
|
+
if (result.success) {
|
|
611
|
+
const osName = result.stdout.trim().toLowerCase();
|
|
612
|
+
if (osName === "darwin") return "darwin";
|
|
613
|
+
if (osName === "linux") return "linux";
|
|
614
|
+
}
|
|
615
|
+
} catch {}
|
|
616
|
+
try {
|
|
617
|
+
const result = await executeBinary({
|
|
618
|
+
binaryPath: "cmd",
|
|
619
|
+
args: ["/c", "ver"],
|
|
620
|
+
timeoutMs: 5e3
|
|
621
|
+
});
|
|
622
|
+
if (result.success && result.stdout.toLowerCase().includes("windows")) return "windows";
|
|
623
|
+
} catch {}
|
|
624
|
+
throw new Error("Unable to detect operating system. Supported systems: macOS (Darwin), Linux, Windows");
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Detect the CPU architecture
|
|
628
|
+
*/
|
|
629
|
+
async function detectArch(os) {
|
|
630
|
+
if (os === "windows") try {
|
|
631
|
+
const result = await executeBinary({
|
|
632
|
+
binaryPath: "cmd",
|
|
633
|
+
args: [
|
|
634
|
+
"/c",
|
|
635
|
+
"echo",
|
|
636
|
+
"%PROCESSOR_ARCHITECTURE%"
|
|
637
|
+
],
|
|
638
|
+
timeoutMs: 5e3
|
|
639
|
+
});
|
|
640
|
+
if (result.success) {
|
|
641
|
+
if (result.stdout.trim().toLowerCase() === "arm64") return "arm64";
|
|
642
|
+
return "x64";
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
return "x64";
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const result = await executeBinary({
|
|
649
|
+
binaryPath: "uname",
|
|
650
|
+
args: ["-m"],
|
|
651
|
+
timeoutMs: 5e3
|
|
652
|
+
});
|
|
653
|
+
if (result.success) {
|
|
654
|
+
const machine = result.stdout.trim().toLowerCase();
|
|
655
|
+
if (machine === "arm64" || machine === "aarch64") return "arm64";
|
|
656
|
+
if (machine === "x86_64" || machine === "amd64") return "x64";
|
|
657
|
+
if (machine.includes("arm")) return "arm64";
|
|
658
|
+
return "x64";
|
|
659
|
+
}
|
|
660
|
+
} catch {}
|
|
661
|
+
return "x64";
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/utils/archive.ts
|
|
666
|
+
/**
|
|
667
|
+
* Archive extraction utilities for Moss plugins
|
|
668
|
+
*
|
|
669
|
+
* Provides functions to extract .tar.gz and .zip archives using
|
|
670
|
+
* system commands (tar, unzip, PowerShell).
|
|
671
|
+
*/
|
|
672
|
+
/**
|
|
673
|
+
* Extract an archive to a destination directory
|
|
674
|
+
*
|
|
675
|
+
* Uses system commands for extraction:
|
|
676
|
+
* - .tar.gz: `tar -xzf` (macOS/Linux)
|
|
677
|
+
* - .zip: `unzip` (macOS/Linux) or PowerShell `Expand-Archive` (Windows)
|
|
678
|
+
*
|
|
679
|
+
* @param options - Extraction options
|
|
680
|
+
* @returns Extraction result
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* ```typescript
|
|
684
|
+
* const result = await extractArchive({
|
|
685
|
+
* archivePath: "/path/to/hugo.tar.gz",
|
|
686
|
+
* destDir: "/path/to/extract/",
|
|
687
|
+
* });
|
|
688
|
+
*
|
|
689
|
+
* if (!result.success) {
|
|
690
|
+
* console.error(`Extraction failed: ${result.error}`);
|
|
691
|
+
* }
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
async function extractArchive(options) {
|
|
695
|
+
const { archivePath, destDir, timeoutMs = 6e4 } = options;
|
|
696
|
+
const format = options.format ?? detectFormat(archivePath);
|
|
697
|
+
if (!format) return {
|
|
698
|
+
success: false,
|
|
699
|
+
error: `Unable to detect archive format for: ${archivePath}. Supported formats: .tar.gz, .zip`
|
|
700
|
+
};
|
|
701
|
+
const platform = await getPlatformInfo();
|
|
702
|
+
try {
|
|
703
|
+
if (format === "tar.gz") return await extractTarGz(archivePath, destDir, timeoutMs);
|
|
704
|
+
else if (platform.os === "windows") return await extractZipWindows(archivePath, destDir, timeoutMs);
|
|
705
|
+
else return await extractZipUnix(archivePath, destDir, timeoutMs);
|
|
706
|
+
} catch (error$1) {
|
|
707
|
+
return {
|
|
708
|
+
success: false,
|
|
709
|
+
error: error$1 instanceof Error ? error$1.message : String(error$1)
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Make a file executable (Unix only)
|
|
715
|
+
*
|
|
716
|
+
* Runs `chmod +x` on the specified file. No-op on Windows.
|
|
717
|
+
*
|
|
718
|
+
* @param filePath - Absolute path to the file
|
|
719
|
+
* @returns Whether the operation succeeded
|
|
720
|
+
*
|
|
721
|
+
* @example
|
|
722
|
+
* ```typescript
|
|
723
|
+
* await makeExecutable("/path/to/binary");
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
async function makeExecutable(filePath) {
|
|
727
|
+
if ((await getPlatformInfo()).os === "windows") return true;
|
|
728
|
+
try {
|
|
729
|
+
return (await executeBinary({
|
|
730
|
+
binaryPath: "chmod",
|
|
731
|
+
args: ["+x", filePath],
|
|
732
|
+
timeoutMs: 5e3
|
|
733
|
+
})).success;
|
|
734
|
+
} catch {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Detect archive format from file extension
|
|
740
|
+
*/
|
|
741
|
+
function detectFormat(archivePath) {
|
|
742
|
+
const lowerPath = archivePath.toLowerCase();
|
|
743
|
+
if (lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz")) return "tar.gz";
|
|
744
|
+
if (lowerPath.endsWith(".zip")) return "zip";
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Extract .tar.gz using tar command
|
|
749
|
+
*/
|
|
750
|
+
async function extractTarGz(archivePath, destDir, timeoutMs) {
|
|
751
|
+
const result = await executeBinary({
|
|
752
|
+
binaryPath: "tar",
|
|
753
|
+
args: [
|
|
754
|
+
"-xzf",
|
|
755
|
+
archivePath,
|
|
756
|
+
"-C",
|
|
757
|
+
destDir
|
|
758
|
+
],
|
|
759
|
+
timeoutMs
|
|
760
|
+
});
|
|
761
|
+
if (!result.success) return {
|
|
762
|
+
success: false,
|
|
763
|
+
error: result.stderr || `tar extraction failed with exit code ${result.exitCode}`
|
|
764
|
+
};
|
|
765
|
+
return { success: true };
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Extract .zip using unzip command (macOS/Linux)
|
|
769
|
+
*/
|
|
770
|
+
async function extractZipUnix(archivePath, destDir, timeoutMs) {
|
|
771
|
+
const result = await executeBinary({
|
|
772
|
+
binaryPath: "unzip",
|
|
773
|
+
args: [
|
|
774
|
+
"-o",
|
|
775
|
+
"-q",
|
|
776
|
+
archivePath,
|
|
777
|
+
"-d",
|
|
778
|
+
destDir
|
|
779
|
+
],
|
|
780
|
+
timeoutMs
|
|
781
|
+
});
|
|
782
|
+
if (!result.success) return {
|
|
783
|
+
success: false,
|
|
784
|
+
error: result.stderr || `unzip extraction failed with exit code ${result.exitCode}`
|
|
785
|
+
};
|
|
786
|
+
return { success: true };
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Extract .zip using PowerShell (Windows)
|
|
790
|
+
*/
|
|
791
|
+
async function extractZipWindows(archivePath, destDir, timeoutMs) {
|
|
792
|
+
const result = await executeBinary({
|
|
793
|
+
binaryPath: "powershell",
|
|
794
|
+
args: [
|
|
795
|
+
"-NoProfile",
|
|
796
|
+
"-NonInteractive",
|
|
797
|
+
"-Command",
|
|
798
|
+
`Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`
|
|
799
|
+
],
|
|
800
|
+
timeoutMs
|
|
801
|
+
});
|
|
802
|
+
if (!result.success) return {
|
|
803
|
+
success: false,
|
|
804
|
+
error: result.stderr || `PowerShell extraction failed with exit code ${result.exitCode}`
|
|
805
|
+
};
|
|
806
|
+
return { success: true };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/utils/binary-resolver.ts
|
|
811
|
+
/**
|
|
812
|
+
* Binary resolver for Moss plugins
|
|
813
|
+
*
|
|
814
|
+
* Provides auto-detection and download of external CLI tools like Hugo.
|
|
815
|
+
* Implements a 4-step resolution flow:
|
|
816
|
+
* 1. Check configured path (from plugin config)
|
|
817
|
+
* 2. Check system PATH
|
|
818
|
+
* 3. Check plugin bin directory
|
|
819
|
+
* 4. Download from GitHub releases (if enabled)
|
|
820
|
+
*/
|
|
821
|
+
/**
|
|
822
|
+
* Error thrown during binary resolution
|
|
823
|
+
*/
|
|
824
|
+
var BinaryResolutionError = class extends Error {
|
|
825
|
+
constructor(message, phase, cause) {
|
|
826
|
+
super(message);
|
|
827
|
+
this.phase = phase;
|
|
828
|
+
this.cause = cause;
|
|
829
|
+
this.name = "BinaryResolutionError";
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
/**
|
|
833
|
+
* Resolve a binary, downloading if necessary
|
|
834
|
+
*
|
|
835
|
+
* Resolution order:
|
|
836
|
+
* 1. Configured path (from plugin config, e.g., hugo_path)
|
|
837
|
+
* 2. System PATH (just the binary name)
|
|
838
|
+
* 3. Plugin bin directory (.moss/plugins/{plugin}/bin/{name})
|
|
839
|
+
* 4. Download from GitHub releases (if autoDownload is true)
|
|
840
|
+
*
|
|
841
|
+
* @param config - Binary configuration
|
|
842
|
+
* @param options - Resolution options
|
|
843
|
+
* @returns Resolution result with path and source
|
|
844
|
+
* @throws BinaryResolutionError if binary cannot be resolved
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* ```typescript
|
|
848
|
+
* const hugo = await resolveBinary(HUGO_CONFIG, {
|
|
849
|
+
* configuredPath: context.config.hugo_path,
|
|
850
|
+
* onProgress: (phase, msg) => reportProgress(phase, 0, 1, msg),
|
|
851
|
+
* });
|
|
852
|
+
*
|
|
853
|
+
* await executeBinary({
|
|
854
|
+
* binaryPath: hugo.path,
|
|
855
|
+
* args: ["--version"],
|
|
856
|
+
* });
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
async function resolveBinary(config, options = {}) {
|
|
860
|
+
const { configuredPath, autoDownload = true, onProgress } = options;
|
|
861
|
+
const progress = (phase, message) => {
|
|
862
|
+
onProgress?.(phase, message);
|
|
863
|
+
};
|
|
864
|
+
if (configuredPath) {
|
|
865
|
+
progress("detection", `Checking configured path: ${configuredPath}`);
|
|
866
|
+
const result = await checkBinary(configuredPath, config);
|
|
867
|
+
if (result) return {
|
|
868
|
+
path: configuredPath,
|
|
869
|
+
version: result.version,
|
|
870
|
+
source: "config"
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
progress("detection", `Checking system PATH for ${config.name}`);
|
|
874
|
+
const pathResult = await checkBinary(config.name, config);
|
|
875
|
+
if (pathResult) return {
|
|
876
|
+
path: config.name,
|
|
877
|
+
version: pathResult.version,
|
|
878
|
+
source: "path"
|
|
879
|
+
};
|
|
880
|
+
const pluginBinPath = await getPluginBinPath(config);
|
|
881
|
+
progress("detection", `Checking plugin storage: ${pluginBinPath}`);
|
|
882
|
+
if (await binaryExistsInPluginStorage(config)) {
|
|
883
|
+
const storedResult = await checkBinary(pluginBinPath, config);
|
|
884
|
+
if (storedResult) return {
|
|
885
|
+
path: pluginBinPath,
|
|
886
|
+
version: storedResult.version,
|
|
887
|
+
source: "plugin-storage"
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (!autoDownload) throw new BinaryResolutionError(`${config.name} not found. Please install it manually or set the path in plugin configuration.\n\nInstallation options:\n- Install via package manager (brew, apt, etc.)\n- Download from the official website\n- Set ${config.name}_path in .moss/config.toml`, "detection");
|
|
891
|
+
progress("download", `Downloading ${config.name}...`);
|
|
892
|
+
const downloadedPath = await downloadBinary(config, progress);
|
|
893
|
+
const downloadedResult = await checkBinary(downloadedPath, config);
|
|
894
|
+
if (!downloadedResult) throw new BinaryResolutionError(`Downloaded ${config.name} binary failed verification. The binary may be corrupted or incompatible with your system.`, "validation");
|
|
895
|
+
return {
|
|
896
|
+
path: downloadedPath,
|
|
897
|
+
version: downloadedResult.version,
|
|
898
|
+
source: "downloaded"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Check if a binary exists and optionally extract its version
|
|
903
|
+
*/
|
|
904
|
+
async function checkBinary(binaryPath, config) {
|
|
905
|
+
try {
|
|
906
|
+
const [cmd, ...args] = parseCommand(config.versionCommand ?? `${config.name} version`, binaryPath, config.name);
|
|
907
|
+
const result = await executeBinary({
|
|
908
|
+
binaryPath: cmd,
|
|
909
|
+
args,
|
|
910
|
+
timeoutMs: 1e4
|
|
911
|
+
});
|
|
912
|
+
if (!result.success) return null;
|
|
913
|
+
let version;
|
|
914
|
+
if (config.versionPattern) {
|
|
915
|
+
const match = (result.stdout + result.stderr).match(config.versionPattern);
|
|
916
|
+
if (match && match[1]) version = match[1];
|
|
917
|
+
}
|
|
918
|
+
return { version };
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Parse a version command, replacing {name} with the binary path
|
|
925
|
+
*/
|
|
926
|
+
function parseCommand(template, binaryPath, name) {
|
|
927
|
+
const resolved = template.replace(/{name}/g, binaryPath);
|
|
928
|
+
if (resolved.startsWith(binaryPath)) return [binaryPath, ...resolved.slice(binaryPath.length).trim().split(/\s+/).filter(Boolean)];
|
|
929
|
+
return resolved.split(/\s+/).filter(Boolean);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get the full path to the binary in plugin storage
|
|
933
|
+
*/
|
|
934
|
+
async function getPluginBinPath(config) {
|
|
935
|
+
const ctx = getInternalContext();
|
|
936
|
+
const binaryName = getBinaryFilename(config, (await getPlatformInfo()).os === "windows");
|
|
937
|
+
return `${ctx.moss_dir}/plugins/${ctx.plugin_name}/bin/${binaryName}`;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get the binary filename (with .exe on Windows)
|
|
941
|
+
*/
|
|
942
|
+
function getBinaryFilename(config, isWindows) {
|
|
943
|
+
const baseName = config.binaryName ?? config.name;
|
|
944
|
+
return isWindows ? `${baseName}.exe` : baseName;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Check if binary exists in plugin storage
|
|
948
|
+
*/
|
|
949
|
+
async function binaryExistsInPluginStorage(config) {
|
|
950
|
+
return pluginFileExists(`bin/${getBinaryFilename(config, (await getPlatformInfo()).os === "windows")}`);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Download and extract binary from GitHub releases
|
|
954
|
+
*/
|
|
955
|
+
async function downloadBinary(config, progress) {
|
|
956
|
+
const platform = await getPlatformInfo();
|
|
957
|
+
const source = config.sources[platform.platformKey];
|
|
958
|
+
if (!source) throw new BinaryResolutionError(`No download source configured for platform: ${platform.platformKey}`, "download");
|
|
959
|
+
let version;
|
|
960
|
+
let downloadUrl;
|
|
961
|
+
if (source.github) {
|
|
962
|
+
progress("download", "Fetching latest release info from GitHub...");
|
|
963
|
+
version = (await getLatestRelease(source.github.owner, source.github.repo)).version;
|
|
964
|
+
const assetName = resolveAssetPattern(source.github.assetPattern, version, platform);
|
|
965
|
+
downloadUrl = `https://github.com/${source.github.owner}/${source.github.repo}/releases/download/v${version}/${assetName}`;
|
|
966
|
+
} else if (source.directUrl) throw new BinaryResolutionError("Direct URL downloads not yet implemented. Please use GitHub source.", "download");
|
|
967
|
+
else throw new BinaryResolutionError(`No download source configured for ${config.name}`, "download");
|
|
968
|
+
progress("download", `Downloading ${config.name} v${version}...`);
|
|
969
|
+
const ctx = getInternalContext();
|
|
970
|
+
const archiveFilename = downloadUrl.split("/").pop() ?? "archive";
|
|
971
|
+
const archivePath = `${ctx.moss_dir}/plugins/${ctx.plugin_name}/.tmp/${archiveFilename}`;
|
|
972
|
+
await downloadToPluginStorage(downloadUrl, `.tmp/${archiveFilename}`);
|
|
973
|
+
progress("extraction", "Extracting archive...");
|
|
974
|
+
const binDir = `${ctx.moss_dir}/plugins/${ctx.plugin_name}/bin`;
|
|
975
|
+
await writePluginFile("bin/.gitkeep", "");
|
|
976
|
+
const extractResult = await extractArchive({
|
|
977
|
+
archivePath,
|
|
978
|
+
destDir: binDir
|
|
979
|
+
});
|
|
980
|
+
if (!extractResult.success) throw new BinaryResolutionError(`Failed to extract archive: ${extractResult.error}`, "extraction");
|
|
981
|
+
const binaryPath = await getPluginBinPath(config);
|
|
982
|
+
await makeExecutable(binaryPath);
|
|
983
|
+
progress("complete", `${config.name} v${version} installed successfully`);
|
|
984
|
+
await cacheReleaseInfo(config.name, version);
|
|
985
|
+
return binaryPath;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Fetch latest release info from GitHub API
|
|
989
|
+
*/
|
|
990
|
+
async function getLatestRelease(owner, repo) {
|
|
991
|
+
const cacheKey = `${owner}/${repo}`;
|
|
992
|
+
try {
|
|
993
|
+
const response = await fetchUrl(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { timeoutMs: 1e4 });
|
|
994
|
+
if (!response.ok) {
|
|
995
|
+
if (response.status === 403 || response.status === 429) {
|
|
996
|
+
const cached = await getCachedRelease(cacheKey);
|
|
997
|
+
if (cached) return cached;
|
|
998
|
+
throw new BinaryResolutionError("GitHub API rate limit exceeded. Please try again later or install the binary manually.", "download");
|
|
999
|
+
}
|
|
1000
|
+
throw new BinaryResolutionError(`Failed to fetch release info: HTTP ${response.status}`, "download");
|
|
1001
|
+
}
|
|
1002
|
+
const tag = JSON.parse(response.text()).tag_name;
|
|
1003
|
+
return {
|
|
1004
|
+
version: tag.replace(/^v/, ""),
|
|
1005
|
+
tag
|
|
1006
|
+
};
|
|
1007
|
+
} catch (error$1) {
|
|
1008
|
+
if (error$1 instanceof BinaryResolutionError) throw error$1;
|
|
1009
|
+
const cached = await getCachedRelease(cacheKey);
|
|
1010
|
+
if (cached) return cached;
|
|
1011
|
+
throw new BinaryResolutionError(`Failed to fetch release info: ${error$1 instanceof Error ? error$1.message : String(error$1)}`, "download", error$1 instanceof Error ? error$1 : void 0);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Resolve asset pattern with actual values
|
|
1016
|
+
*/
|
|
1017
|
+
function resolveAssetPattern(pattern, version, platform) {
|
|
1018
|
+
return pattern.replace(/{version}/g, version).replace(/{os}/g, platform.os).replace(/{arch}/g, {
|
|
1019
|
+
x64: "amd64",
|
|
1020
|
+
arm64: "arm64"
|
|
1021
|
+
}[platform.arch] ?? platform.arch);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Download a file to plugin storage
|
|
1025
|
+
*
|
|
1026
|
+
* Uses curl (macOS/Linux) or PowerShell (Windows) to download the file
|
|
1027
|
+
* directly to the target path, avoiding memory limitations of base64 encoding.
|
|
1028
|
+
*/
|
|
1029
|
+
async function downloadToPluginStorage(url, relativePath) {
|
|
1030
|
+
const ctx = getInternalContext();
|
|
1031
|
+
const platform = await getPlatformInfo();
|
|
1032
|
+
const targetPath = `${ctx.moss_dir}/plugins/${ctx.plugin_name}/${relativePath}`;
|
|
1033
|
+
const parentDir = targetPath.substring(0, targetPath.lastIndexOf("/"));
|
|
1034
|
+
if (platform.os === "windows") {
|
|
1035
|
+
await executeBinary({
|
|
1036
|
+
binaryPath: "powershell",
|
|
1037
|
+
args: [
|
|
1038
|
+
"-NoProfile",
|
|
1039
|
+
"-NonInteractive",
|
|
1040
|
+
"-Command",
|
|
1041
|
+
`New-Item -ItemType Directory -Force -Path '${parentDir}'`
|
|
1042
|
+
],
|
|
1043
|
+
timeoutMs: 5e3
|
|
1044
|
+
});
|
|
1045
|
+
const result = await executeBinary({
|
|
1046
|
+
binaryPath: "powershell",
|
|
1047
|
+
args: [
|
|
1048
|
+
"-NoProfile",
|
|
1049
|
+
"-NonInteractive",
|
|
1050
|
+
"-Command",
|
|
1051
|
+
`Invoke-WebRequest -Uri '${url}' -OutFile '${targetPath}'`
|
|
1052
|
+
],
|
|
1053
|
+
timeoutMs: 3e5
|
|
1054
|
+
});
|
|
1055
|
+
if (!result.success) throw new BinaryResolutionError(`Download failed: ${result.stderr || result.stdout}`, "download");
|
|
1056
|
+
} else {
|
|
1057
|
+
await executeBinary({
|
|
1058
|
+
binaryPath: "mkdir",
|
|
1059
|
+
args: ["-p", parentDir],
|
|
1060
|
+
timeoutMs: 5e3
|
|
1061
|
+
});
|
|
1062
|
+
const result = await executeBinary({
|
|
1063
|
+
binaryPath: "curl",
|
|
1064
|
+
args: [
|
|
1065
|
+
"-fsSL",
|
|
1066
|
+
"--create-dirs",
|
|
1067
|
+
"-o",
|
|
1068
|
+
targetPath,
|
|
1069
|
+
url
|
|
1070
|
+
],
|
|
1071
|
+
timeoutMs: 3e5
|
|
1072
|
+
});
|
|
1073
|
+
if (!result.success) throw new BinaryResolutionError(`Download failed: ${result.stderr || `curl exited with code ${result.exitCode}`}`, "download");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Cache release info for fallback
|
|
1078
|
+
*/
|
|
1079
|
+
async function cacheReleaseInfo(name, version) {
|
|
1080
|
+
try {
|
|
1081
|
+
const cached = {
|
|
1082
|
+
version,
|
|
1083
|
+
tag: `v${version}`,
|
|
1084
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1085
|
+
};
|
|
1086
|
+
await writePluginFile(`cache/${name}-release.json`, JSON.stringify(cached, null, 2));
|
|
1087
|
+
} catch {}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Get cached release info
|
|
1091
|
+
*/
|
|
1092
|
+
async function getCachedRelease(cacheKey) {
|
|
1093
|
+
try {
|
|
1094
|
+
const name = cacheKey.split("/")[1];
|
|
1095
|
+
if (!name) return null;
|
|
1096
|
+
if (!await pluginFileExists(`cache/${name}-release.json`)) return null;
|
|
1097
|
+
const content = await readPluginFile(`cache/${name}-release.json`);
|
|
1098
|
+
const cached = JSON.parse(content);
|
|
1099
|
+
return {
|
|
1100
|
+
version: cached.version,
|
|
1101
|
+
tag: cached.tag
|
|
1102
|
+
};
|
|
1103
|
+
} catch {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
544
1108
|
//#endregion
|
|
545
1109
|
//#region src/utils/cookies.ts
|
|
546
1110
|
/**
|
|
@@ -608,5 +1172,194 @@ async function setPluginCookie(cookies) {
|
|
|
608
1172
|
}
|
|
609
1173
|
|
|
610
1174
|
//#endregion
|
|
611
|
-
|
|
1175
|
+
//#region src/utils/window.ts
|
|
1176
|
+
/**
|
|
1177
|
+
* Window utilities for plugins
|
|
1178
|
+
* Enables plugins to show custom dialogs and UI elements
|
|
1179
|
+
*/
|
|
1180
|
+
/**
|
|
1181
|
+
* Show a plugin dialog and wait for user response
|
|
1182
|
+
*
|
|
1183
|
+
* The dialog can be an embedded HTML page (via data: URL) that communicates
|
|
1184
|
+
* back to the plugin via the submitDialogResult function.
|
|
1185
|
+
*
|
|
1186
|
+
* @param options - Dialog configuration
|
|
1187
|
+
* @returns Dialog result with submitted value or cancellation
|
|
1188
|
+
*
|
|
1189
|
+
* @example
|
|
1190
|
+
* ```typescript
|
|
1191
|
+
* const result = await showPluginDialog({
|
|
1192
|
+
* url: createMyDialogUrl(),
|
|
1193
|
+
* title: "Create Repository",
|
|
1194
|
+
* width: 400,
|
|
1195
|
+
* height: 300,
|
|
1196
|
+
* });
|
|
1197
|
+
*
|
|
1198
|
+
* if (result.type === "submitted") {
|
|
1199
|
+
* console.log("User submitted:", result.value);
|
|
1200
|
+
* } else {
|
|
1201
|
+
* console.log("User cancelled");
|
|
1202
|
+
* }
|
|
1203
|
+
* ```
|
|
1204
|
+
*/
|
|
1205
|
+
async function showPluginDialog(options) {
|
|
1206
|
+
return await getTauriCore().invoke("show_plugin_dialog", {
|
|
1207
|
+
url: options.url,
|
|
1208
|
+
title: options.title,
|
|
1209
|
+
width: options.width ?? 500,
|
|
1210
|
+
height: options.height ?? 400,
|
|
1211
|
+
timeoutMs: options.timeoutMs ?? 3e5
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Submit a result from within a plugin dialog
|
|
1216
|
+
*
|
|
1217
|
+
* This is called from inside the dialog HTML to send data back to the plugin.
|
|
1218
|
+
* The dialog will be closed automatically after submission.
|
|
1219
|
+
*
|
|
1220
|
+
* @param dialogId - The dialog ID (provided in the dialog's query string)
|
|
1221
|
+
* @param value - The value to submit
|
|
1222
|
+
* @returns Whether the submission was successful
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```typescript
|
|
1226
|
+
* // Inside dialog HTML:
|
|
1227
|
+
* const dialogId = new URLSearchParams(location.search).get('dialogId');
|
|
1228
|
+
* await submitDialogResult(dialogId, { repoName: 'my-repo' });
|
|
1229
|
+
* ```
|
|
1230
|
+
*/
|
|
1231
|
+
async function submitDialogResult(dialogId, value) {
|
|
1232
|
+
return getTauriCore().invoke("submit_dialog_result", {
|
|
1233
|
+
dialogId,
|
|
1234
|
+
result: {
|
|
1235
|
+
type: "submitted",
|
|
1236
|
+
value
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Cancel a plugin dialog
|
|
1242
|
+
*
|
|
1243
|
+
* This is called from inside the dialog HTML to cancel without submitting.
|
|
1244
|
+
* The dialog will be closed automatically.
|
|
1245
|
+
*
|
|
1246
|
+
* @param dialogId - The dialog ID (provided in the dialog's query string)
|
|
1247
|
+
*
|
|
1248
|
+
* @example
|
|
1249
|
+
* ```typescript
|
|
1250
|
+
* // Inside dialog HTML:
|
|
1251
|
+
* const dialogId = new URLSearchParams(location.search).get('dialogId');
|
|
1252
|
+
* await cancelDialog(dialogId);
|
|
1253
|
+
* ```
|
|
1254
|
+
*/
|
|
1255
|
+
async function cancelDialog(dialogId) {
|
|
1256
|
+
return getTauriCore().invoke("submit_dialog_result", {
|
|
1257
|
+
dialogId,
|
|
1258
|
+
result: { type: "cancelled" }
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/utils/events.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Get Tauri event API
|
|
1266
|
+
* @internal
|
|
1267
|
+
*/
|
|
1268
|
+
function getTauriEvent() {
|
|
1269
|
+
const w = window;
|
|
1270
|
+
if (!w.__TAURI__?.event) throw new Error("Tauri event API not available");
|
|
1271
|
+
return w.__TAURI__.event;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Check if Tauri event API is available
|
|
1275
|
+
*/
|
|
1276
|
+
function isEventApiAvailable() {
|
|
1277
|
+
return !!window.__TAURI__?.event;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Emit an event to other parts of the application
|
|
1281
|
+
*
|
|
1282
|
+
* @param event - Event name (e.g., "repo-created", "dialog-result")
|
|
1283
|
+
* @param payload - Data to send with the event
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* ```typescript
|
|
1287
|
+
* // From dialog:
|
|
1288
|
+
* await emitEvent("repo-name-validated", { name: "my-repo", available: true });
|
|
1289
|
+
*
|
|
1290
|
+
* // From plugin:
|
|
1291
|
+
* await emitEvent("deployment-started", { url: "https://github.com/..." });
|
|
1292
|
+
* ```
|
|
1293
|
+
*/
|
|
1294
|
+
async function emitEvent(event, payload) {
|
|
1295
|
+
await getTauriEvent().emit(event, payload);
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Listen for events from other parts of the application
|
|
1299
|
+
*
|
|
1300
|
+
* @param event - Event name to listen for
|
|
1301
|
+
* @param handler - Function to call when event is received
|
|
1302
|
+
* @returns Cleanup function to stop listening
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* ```typescript
|
|
1306
|
+
* const unlisten = await onEvent<{ name: string; available: boolean }>(
|
|
1307
|
+
* "repo-name-validated",
|
|
1308
|
+
* (data) => {
|
|
1309
|
+
* console.log(`Repo ${data.name} is ${data.available ? "available" : "taken"}`);
|
|
1310
|
+
* }
|
|
1311
|
+
* );
|
|
1312
|
+
*
|
|
1313
|
+
* // Later, to stop listening:
|
|
1314
|
+
* unlisten();
|
|
1315
|
+
* ```
|
|
1316
|
+
*/
|
|
1317
|
+
async function onEvent(event, handler) {
|
|
1318
|
+
return await getTauriEvent().listen(event, (e) => {
|
|
1319
|
+
handler(e.payload);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Wait for a single event occurrence
|
|
1324
|
+
*
|
|
1325
|
+
* @param event - Event name to wait for
|
|
1326
|
+
* @param timeoutMs - Maximum time to wait (default: 30000ms)
|
|
1327
|
+
* @returns Promise that resolves with the event payload
|
|
1328
|
+
* @throws Error if timeout is reached
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* ```typescript
|
|
1332
|
+
* try {
|
|
1333
|
+
* const result = await waitForEvent<{ confirmed: boolean }>("user-confirmed", 10000);
|
|
1334
|
+
* if (result.confirmed) {
|
|
1335
|
+
* // proceed
|
|
1336
|
+
* }
|
|
1337
|
+
* } catch (e) {
|
|
1338
|
+
* console.log("User did not respond in time");
|
|
1339
|
+
* }
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
async function waitForEvent(event, timeoutMs = 3e4) {
|
|
1343
|
+
return new Promise((resolve, reject) => {
|
|
1344
|
+
let unlisten = null;
|
|
1345
|
+
let timeoutId;
|
|
1346
|
+
const cleanup = () => {
|
|
1347
|
+
if (unlisten) unlisten();
|
|
1348
|
+
clearTimeout(timeoutId);
|
|
1349
|
+
};
|
|
1350
|
+
timeoutId = setTimeout(() => {
|
|
1351
|
+
cleanup();
|
|
1352
|
+
reject(/* @__PURE__ */ new Error(`Timeout waiting for event: ${event}`));
|
|
1353
|
+
}, timeoutMs);
|
|
1354
|
+
onEvent(event, (payload) => {
|
|
1355
|
+
cleanup();
|
|
1356
|
+
resolve(payload);
|
|
1357
|
+
}).then((unlistenFn) => {
|
|
1358
|
+
unlisten = unlistenFn;
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
//#endregion
|
|
1364
|
+
export { BinaryResolutionError, cancelDialog, clearPlatformCache, closeBrowser, downloadAsset, emitEvent, error, executeBinary, extractArchive, fetchUrl, fileExists, getMessageContext, getPlatformInfo, getPluginCookie, getTauriCore, isEventApiAvailable, isTauriAvailable, listFiles, listPluginFiles, log, makeExecutable, onEvent, openBrowser, pluginFileExists, readFile, readPluginFile, reportComplete, reportError, reportProgress, resolveBinary, sendMessage, setMessageContext, setPluginCookie, showPluginDialog, submitDialogResult, waitForEvent, warn, writeFile, writePluginFile };
|
|
612
1365
|
//# sourceMappingURL=index.mjs.map
|