Function bodies 191 total
env.CalculatePushDiff function · go · L18-L45 (28 LOC)internal/env/diff.go
func CalculatePushDiff(local, vault map[string]string) *PushDiff {
diff := &PushDiff{}
// Check local secrets against vault
for key, localVal := range local {
if vaultVal, exists := vault[key]; exists {
if localVal != vaultVal {
diff.Changed = append(diff.Changed, key)
}
} else {
diff.Added = append(diff.Added, key)
}
}
// Find vault-only secrets (will be removed)
for key := range vault {
if _, exists := local[key]; !exists {
diff.Removed = append(diff.Removed, key)
}
}
// Sort for deterministic output
sort.Strings(diff.Added)
sort.Strings(diff.Changed)
sort.Strings(diff.Removed)
return diff
}env.CalculatePullDiff function · go · L61-L91 (31 LOC)internal/env/diff.go
func CalculatePullDiff(local, vault map[string]string) *PullDiff {
diff := &PullDiff{}
// Check vault secrets against local
for key, vaultVal := range vault {
if localVal, exists := local[key]; exists {
if localVal != vaultVal {
diff.Changed = append(diff.Changed, key)
} else {
diff.Unchanged = append(diff.Unchanged, key)
}
} else {
diff.Added = append(diff.Added, key)
}
}
// Find local-only secrets
for key := range local {
if _, exists := vault[key]; !exists {
diff.LocalOnly = append(diff.LocalOnly, key)
}
}
// Sort for deterministic output
sort.Strings(diff.Added)
sort.Strings(diff.Changed)
sort.Strings(diff.LocalOnly)
sort.Strings(diff.Unchanged)
return diff
}env.Discover function · go · L17-L41 (25 LOC)internal/env/discover.go
func Discover() []Candidate {
entries, err := os.ReadDir(".")
if err != nil {
return nil
}
// Template files to exclude (not real secrets)
excludeFiles := map[string]bool{
".env.example": true, // Template files
".env.sample": true,
".env.template": true,
}
var candidates []Candidate
for _, entry := range entries {
name := entry.Name()
if strings.HasPrefix(name, ".env") && !excludeFiles[name] && !entry.IsDir() {
candidates = append(candidates, Candidate{
File: name,
Env: DeriveEnvFromFile(name),
})
}
}
return candidates
}env.DeriveEnvFromFile function · go · L48-L57 (10 LOC)internal/env/discover.go
func DeriveEnvFromFile(file string) string {
base := filepath.Base(file)
if base == ".env" {
return "development"
}
if strings.HasPrefix(base, ".env.") {
return strings.TrimPrefix(base, ".env.")
}
return "development"
}env.Parse function · go · L11-L38 (28 LOC)internal/env/parser.go
func Parse(content string) map[string]string {
result := make(map[string]string)
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
idx := strings.Index(line, "=")
if idx == -1 {
continue
}
key := strings.TrimSpace(line[:idx])
value := line[idx+1:]
// Remove surrounding quotes
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
if key != "" {
result[key] = value
}
}
return result
}env.CountLines function · go · L41-L50 (10 LOC)internal/env/parser.go
func CountLines(content string) int {
count := 0
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
count++
}
}
return count
}env.Merge function · go · L54-L79 (26 LOC)internal/env/parser.go
func Merge(vaultContent string, local, vault map[string]string) string {
// Start with vault content
result := strings.TrimRight(vaultContent, "\n")
// Find local-only secrets and collect keys for sorting
var localOnlyKeys []string
for key := range local {
if _, exists := vault[key]; !exists {
localOnlyKeys = append(localOnlyKeys, key)
}
}
if len(localOnlyKeys) > 0 {
// Sort keys for deterministic output
sort.Strings(localOnlyKeys)
result += "\n\n# Local variables (not in vault)\n"
for _, key := range localOnlyKeys {
result += key + "=" + local[key] + "\n"
}
} else {
result += "\n"
}
return result
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
git.IsGitRepository function · go · L20-L25 (6 LOC)internal/git/repo.go
func IsGitRepository() bool {
cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
cmd.Stderr = nil
cmd.Stdout = nil
return cmd.Run() == nil
}git.DetectRepo function · go · L28-L42 (15 LOC)internal/git/repo.go
func DetectRepo() (string, error) {
if !IsGitRepository() {
return "", fmt.Errorf("not in a git repository")
}
cmd := exec.Command("git", "remote", "get-url", "origin")
cmd.Stderr = nil
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("no remote origin configured")
}
remoteURL := strings.TrimSpace(string(output))
return ParseGitHubURL(remoteURL)
}git.ParseGitHubURL function · go · L45-L57 (13 LOC)internal/git/repo.go
func ParseGitHubURL(url string) (string, error) {
// Try SSH format
if matches := sshRegex.FindStringSubmatch(url); matches != nil {
return fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
}
// Try HTTPS format
if matches := httpsRegex.FindStringSubmatch(url); matches != nil {
return fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
}
return "", fmt.Errorf("not a GitHub URL: %s", url)
}git.GetGitRoot function · go · L60-L68 (9 LOC)internal/git/repo.go
func GetGitRoot() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
cmd.Stderr = nil
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}git.CheckEnvGitignore function · go · L71-L99 (29 LOC)internal/git/repo.go
func CheckEnvGitignore() bool {
gitRoot, err := GetGitRoot()
if err != nil {
return true // Not a git repo, don't warn
}
gitignorePath := filepath.Join(gitRoot, ".gitignore")
content, err := os.ReadFile(gitignorePath)
if err != nil {
return false // No .gitignore file
}
lines := strings.Split(string(content), "\n")
patterns := []string{".env", ".env*", ".env.*", "*.env"}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
for _, pattern := range patterns {
if line == pattern {
return true
}
}
}
return false
}git.AddEnvToGitignore function · go · L102-L120 (19 LOC)internal/git/repo.go
func AddEnvToGitignore() error {
gitRoot, err := GetGitRoot()
if err != nil {
return err
}
gitignorePath := filepath.Join(gitRoot, ".gitignore")
// Read existing content
content, _ := os.ReadFile(gitignorePath)
newContent := string(content)
if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") {
newContent += "\n"
}
newContent += ".env*\n"
return os.WriteFile(gitignorePath, []byte(newContent), 0644)
}git.DetectMonorepo function · go · L130-L165 (36 LOC)internal/git/repo.go
func DetectMonorepo() MonorepoInfo {
gitRoot, err := GetGitRoot()
if err != nil {
return MonorepoInfo{IsMonorepo: false}
}
// Check for monorepo tool config files (in order of popularity)
monorepoIndicators := []struct {
file string
tool string
}{
{"turbo.json", "Turborepo"},
{"nx.json", "Nx"},
{"pnpm-workspace.yaml", "pnpm workspaces"},
{"lerna.json", "Lerna"},
{"rush.json", "Rush"},
}
for _, indicator := range monorepoIndicators {
if _, err := os.Stat(filepath.Join(gitRoot, indicator.file)); err == nil {
return MonorepoInfo{IsMonorepo: true, Tool: indicator.tool}
}
}
// Check package.json for workspaces field (npm/yarn workspaces)
packageJSONPath := filepath.Join(gitRoot, "package.json")
if content, err := os.ReadFile(packageJSONPath); err == nil {
contentStr := string(content)
// Simple check for "workspaces" field in package.json
if strings.Contains(contentStr, `"workspaces"`) {
return MonorepoInfo{IsMonorepo: true, Tool: "npm/yarn worinjector.RunCommand function · go · L15-L67 (53 LOC)internal/injector/injector.go
func RunCommand(command string, args []string, secrets map[string]string) error {
// Prepare the command
cmd := exec.Command(command, args...)
// Connect standard input/output
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Build the environment
// Start with current environment
currentEnv := os.Environ()
newEnv := make([]string, 0, len(currentEnv)+len(secrets))
newEnv = append(newEnv, currentEnv...)
// Append secrets
for k, v := range secrets {
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v))
}
cmd.Env = newEnv
// Handle signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, signals...)
// Start the command
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start command: %w", err)
}
// Forward signals to the child process
go func() {
for sig := range sigs {
if cmd.Process != nil {
_ = cmd.Process.Signal(sig)
}
}
}()
// Wait for the command to finish
err := cmd.Wait()
// Handle exit code
All rows scored by the Repobility analyzer (https://repobility.com)
ui.Confirm function · go · L92-L104 (13 LOC)internal/ui/ui.go
func Confirm(message string, defaultValue bool) (bool, error) {
result := defaultValue
err := huh.NewConfirm().
Title(message).
Value(&result).
Affirmative("Yes").
Negative("No").
Run()
if err != nil {
return defaultValue, err
}
return result, nil
}ui.Select function · go · L107-L120 (14 LOC)internal/ui/ui.go
func Select(message string, options []string) (string, error) {
var result string
opts := make([]huh.Option[string], len(options))
for i, opt := range options {
opts[i] = huh.NewOption(opt, opt)
}
err := huh.NewSelect[string]().
Title(message).
Options(opts...).
Value(&result).
Run()
return result, err
}ui.Password function · go · L123-L131 (9 LOC)internal/ui/ui.go
func Password(message string) (string, error) {
var result string
err := huh.NewInput().
Title(message).
EchoMode(huh.EchoModePassword).
Value(&result).
Run()
return result, err
}ui.Spin function · go · L134-L146 (13 LOC)internal/ui/ui.go
func Spin(message string, fn func() error) error {
var err error
spinErr := spinner.New().
Title(message).
Action(func() {
err = fn()
}).
Run()
if spinErr != nil {
return spinErr
}
return err
}ui.IsInteractive function · go · L149-L160 (12 LOC)internal/ui/ui.go
func IsInteractive() bool {
// Check CI environment
if ci := os.Getenv("CI"); ci == "true" || ci == "1" {
return false
}
// Check if stdin is a terminal
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}version.getCacheFilePath function · go · L18-L24 (7 LOC)internal/version/cache.go
func getCacheFilePath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "keyway", "update-check.json"), nil
}version.LoadCache function · go · L27-L44 (18 LOC)internal/version/cache.go
func LoadCache() (*CacheData, error) {
path, err := getCacheFilePath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cache CacheData
if err := json.Unmarshal(data, &cache); err != nil {
return nil, err
}
return &cache, nil
}version.SaveCache function · go · L47-L64 (18 LOC)internal/version/cache.go
func SaveCache(cache *CacheData) error {
path, err := getCacheFilePath()
if err != nil {
return err
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
data, err := json.MarshalIndent(cache, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}Repobility · code-quality intelligence platform · https://repobility.com
version.DetectInstallMethod function · go · L21-L57 (37 LOC)internal/version/detect.go
func DetectInstallMethod() InstallMethod {
execPath, err := os.Executable()
if err != nil {
return InstallMethodBinary
}
// Resolve symlinks to get the real path
realPath, err := filepath.EvalSymlinks(execPath)
if err != nil {
realPath = execPath
}
pathLower := strings.ToLower(realPath)
// Check for npx (temporary cache)
if strings.Contains(pathLower, "_npx") ||
strings.Contains(pathLower, "npx-") {
return InstallMethodNPX
}
// Check for npm global installation
if strings.Contains(pathLower, "node_modules") ||
strings.Contains(pathLower, "@keywaysh") {
return InstallMethodNPM
}
// Check for Homebrew installation (macOS and Linux)
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
if strings.Contains(pathLower, "/cellar/") ||
strings.Contains(pathLower, "/homebrew/") ||
strings.Contains(pathLower, "/linuxbrew/") {
return InstallMethodHomebrew
}
}
return InstallMethodBinary
}version.FetchLatestVersion function · go · L23-L54 (32 LOC)internal/version/github.go
func FetchLatestVersion(ctx context.Context) (string, error) {
// Skip update checks for self-hosted instances
if config.IsCustomAPIURL() {
return "", fmt.Errorf("update checks disabled for self-hosted instances")
}
req, err := http.NewRequestWithContext(ctx, "GET", defaultGitHubReleasesURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "keyway-cli")
client := &http.Client{Timeout: CheckTimeout}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub API returned %d", resp.StatusCode)
}
var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return "", err
}
return release.TagName, nil
}version.CheckForUpdate function · go · L30-L73 (44 LOC)internal/version/version.go
func CheckForUpdate(ctx context.Context, currentVersion string) *UpdateInfo {
if IsUpdateCheckDisabled() {
return nil
}
// Skip update check for self-hosted instances
if config.IsCustomAPIURL() {
return nil
}
// Skip check for dev builds
if currentVersion == "dev" || currentVersion == "" {
return nil
}
// Detect install method once
method := DetectInstallMethod()
// Skip check for npx (always fetches latest)
if method == InstallMethodNPX {
return nil
}
// Check cache first
cached, err := LoadCache()
if err == nil && cached != nil && time.Since(cached.LastCheck) < CacheDuration {
return buildUpdateInfo(currentVersion, cached.LatestVersion, cached.InstallMethod)
}
// Fetch from GitHub
latest, err := FetchLatestVersion(ctx)
if err != nil {
return nil // Silent failure
}
// Save to cache (ignore errors)
_ = SaveCache(&CacheData{
LastCheck: time.Now(),
LatestVersion: latest,
InstallMethod: method,
})
return buildUpdateInfo(currentVersioversion.buildUpdateInfo function · go · L75-L87 (13 LOC)internal/version/version.go
func buildUpdateInfo(current, latest string, method InstallMethod) *UpdateInfo {
if !IsNewerVersion(latest, current) {
return nil
}
return &UpdateInfo{
Available: true,
CurrentVersion: current,
LatestVersion: latest,
InstallMethod: method,
UpdateCommand: GetUpdateCommand(method),
}
}version.IsNewerVersion function · go · L109-L138 (30 LOC)internal/version/version.go
func IsNewerVersion(latest, current string) bool {
latestParts := parseVersion(latest)
currentParts := parseVersion(current)
if len(latestParts) == 0 || len(currentParts) == 0 {
return false
}
// Compare major, minor, patch
for i := 0; i < 3; i++ {
latestPart := 0
currentPart := 0
if i < len(latestParts) {
latestPart = latestParts[i]
}
if i < len(currentParts) {
currentPart = currentParts[i]
}
if latestPart > currentPart {
return true
}
if latestPart < currentPart {
return false
}
}
return false
}version.parseVersion function · go · L141-L162 (22 LOC)internal/version/version.go
func parseVersion(v string) []int {
// Strip 'v' prefix
v = strings.TrimPrefix(v, "v")
// Handle dirty/dev suffixes
if idx := strings.IndexAny(v, "-+"); idx != -1 {
v = v[:idx]
}
parts := strings.Split(v, ".")
result := make([]int, 0, 3)
for _, part := range parts {
n, err := strconv.Atoi(part)
if err != nil {
break
}
result = append(result, n)
}
return result
}getBinaryPath function · javascript · L27-L32 (6 LOC)npm/scripts/install.js
function getBinaryPath() {
const binDir = path.join(__dirname, "..", "bin");
// Use keyway-bin to avoid conflict with the wrapper script
const binaryName = process.platform === "win32" ? `${BINARY_NAME}-bin.exe` : `${BINARY_NAME}-bin`;
return path.join(binDir, binaryName);
}fetch function · javascript · L34-L60 (27 LOC)npm/scripts/install.js
function fetch(url) {
return new Promise((resolve, reject) => {
const request = https.get(url, (response) => {
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
// Follow redirect
fetch(response.headers.location).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode}`));
return;
}
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => resolve(Buffer.concat(chunks)));
response.on("error", reject);
});
request.on("error", reject);
request.setTimeout(60000, () => {
request.destroy();
reject(new Error("Request timeout"));
});
});
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
downloadChecksum function · javascript · L62-L71 (10 LOC)npm/scripts/install.js
async function downloadChecksum() {
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
try {
const data = await fetch(url);
return data.toString();
} catch (error) {
console.warn("Warning: Could not download checksums file");
return null;
}
}verifyChecksum function · javascript · L73-L90 (18 LOC)npm/scripts/install.js
function verifyChecksum(buffer, checksums, filename) {
if (!checksums) return true;
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
const expectedLine = checksums.split("\n").find((line) => line.includes(filename));
if (!expectedLine) {
console.warn(`Warning: No checksum found for ${filename}`);
return true;
}
const expectedHash = expectedLine.split(/\s+/)[0];
if (hash !== expectedHash) {
throw new Error(`Checksum mismatch for ${filename}\nExpected: ${expectedHash}\nGot: ${hash}`);
}
return true;
}extractTarGz function · javascript · L92-L101 (10 LOC)npm/scripts/install.js
async function extractTarGz(buffer, destDir) {
const tmpFile = path.join(os.tmpdir(), `keyway-${Date.now()}.tar.gz`);
fs.writeFileSync(tmpFile, buffer);
try {
execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" });
} finally {
fs.unlinkSync(tmpFile);
}
}extractZip function · javascript · L103-L120 (18 LOC)npm/scripts/install.js
async function extractZip(buffer, destDir) {
const tmpFile = path.join(os.tmpdir(), `keyway-${Date.now()}.zip`);
fs.writeFileSync(tmpFile, buffer);
try {
// Try powershell on Windows
if (process.platform === "win32") {
execSync(
`powershell -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${destDir}' -Force"`,
{ stdio: "pipe" }
);
} else {
execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" });
}
} finally {
fs.unlinkSync(tmpFile);
}
}install function · javascript · L122-L209 (88 LOC)npm/scripts/install.js
async function install() {
const platformKey = getPlatformKey();
const target = PLATFORMS[platformKey];
if (!target) {
console.error(`\nUnsupported platform: ${platformKey}`);
console.error(`Supported platforms: ${Object.keys(PLATFORMS).join(", ")}`);
console.error(`\nYou can install manually from: https://github.com/${REPO}/releases`);
process.exit(1);
}
const binDir = path.join(__dirname, "..", "bin");
const binaryPath = getBinaryPath();
// Check if binary already exists and is correct version
if (fs.existsSync(binaryPath)) {
try {
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf8" });
if (result.stdout && result.stdout.includes(VERSION)) {
console.log(`keyway v${VERSION} already installed`);
return;
}
} catch (e) {
// Binary exists but couldn't run, reinstall
}
}
const isWindows = process.platform === "win32";
const ext = isWindows ? "zip" : "tar.gz";
const filename = getPlatform function · javascript · L26-L32 (7 LOC)npm/scripts/postinstall.js
function getPlatform() {
const platform = PLATFORM_MAP[process.platform];
if (!platform) {
throw new Error(`Unsupported platform: ${process.platform}`);
}
return platform;
}getArch function · javascript · L34-L40 (7 LOC)npm/scripts/postinstall.js
function getArch() {
const arch = ARCH_MAP[process.arch];
if (!arch) {
throw new Error(`Unsupported architecture: ${process.arch}`);
}
return arch;
}httpsGet function · javascript · L47-L65 (19 LOC)npm/scripts/postinstall.js
function httpsGet(url) {
return new Promise((resolve, reject) => {
https
.get(url, { headers: { "User-Agent": "keyway-npm-installer" } }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return httpsGet(res.headers.location).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
return;
}
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve(Buffer.concat(chunks)));
res.on("error", reject);
})
.on("error", reject);
});
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
downloadAndExtract function · javascript · L67-L121 (55 LOC)npm/scripts/postinstall.js
async function downloadAndExtract(url, destPath) {
console.log(`Downloading ${url}`);
const data = await httpsGet(url);
// Create bin directory if it doesn't exist
if (!fs.existsSync(BIN_DIR)) {
fs.mkdirSync(BIN_DIR, { recursive: true });
}
if (url.endsWith(".tar.gz")) {
// Extract tar.gz
const tmpDir = path.join(__dirname, "..", "tmp");
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
const tarPath = path.join(tmpDir, "keyway.tar.gz");
fs.writeFileSync(tarPath, data);
// Use tar command to extract
execSync(`tar -xzf "${tarPath}" -C "${tmpDir}"`, { stdio: "pipe" });
// Find and move binary
const extractedBinary = path.join(tmpDir, "keyway");
if (fs.existsSync(extractedBinary)) {
fs.copyFileSync(extractedBinary, destPath);
fs.chmodSync(destPath, 0o755);
}
// Cleanup
fs.rmSync(tmpDir, { recursive: true, force: true });
} else if (url.endsWith(".zip")) {
// For Wmain function · javascript · L123-L155 (33 LOC)npm/scripts/postinstall.js
async function main() {
try {
const platform = getPlatform();
const arch = getArch();
const version = getVersion();
// Skip download for version 0.0.0 (development)
if (version === "0.0.0") {
console.log("Development version detected, skipping binary download");
// Create a placeholder script
const placeholder = `#!/bin/sh
echo "Keyway CLI not installed. Run 'make build' in the cli directory."
exit 1
`;
fs.writeFileSync(path.join(BIN_DIR, "keyway"), placeholder);
fs.chmodSync(path.join(BIN_DIR, "keyway"), 0o755);
return;
}
const ext = platform === "windows" ? "zip" : "tar.gz";
const filename = `keyway_${version}_${platform}_${arch}.${ext}`;
const url = `https://github.com/${REPO}/releases/download/v${version}/${filename}`;
const destPath = path.join(BIN_DIR, BINARY_NAME);
await downloadAndExtract(url, destPath);
console.log(`Keyway CLI v${version} installed successfully!`);
} catch (error) {
‹ prevpage 4 / 4