mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 01:46:23 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5921c86fc | ||
|
|
827e9a7160 |
@@ -72,6 +72,30 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("uninstall", {
|
||||
description: "Uninstall OpenCode and remove its user files",
|
||||
params: {
|
||||
keepConfig: Flag.boolean("keep-config").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Keep configuration files"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
keepData: Flag.boolean("keep-data").pipe(
|
||||
Flag.withAlias("d"),
|
||||
Flag.withDescription("Keep session data and snapshots"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
dryRun: Flag.boolean("dry-run").pipe(
|
||||
Flag.withDescription("Show what would be removed without removing it"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
force: Flag.boolean("force").pipe(
|
||||
Flag.withAlias("f"),
|
||||
Flag.withDescription("Skip confirmation prompts"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("acp", { description: "Start an Agent Client Protocol server" }),
|
||||
Spec.make("api", {
|
||||
description: "Make a request to the running server",
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { confirm, intro, log, outro, spinner } from "@clack/prompts"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Installation } from "../../services/installation"
|
||||
import { ServerConnection } from "../../services/server-connection"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import { handlePromptErrors, prompt, requireInteractive } from "../../ui/prompt"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.uninstall,
|
||||
Effect.fn("cli.uninstall")(function* (input) {
|
||||
intro("Uninstall OpenCode")
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const installation = yield* Installation.make()
|
||||
const method = yield* installation.method()
|
||||
log.info(`Installation method: ${method ?? "unknown"}`)
|
||||
if (!method && !input.dryRun)
|
||||
return yield* Effect.fail(
|
||||
new Error(
|
||||
"Could not detect the installation method. Uninstall OpenCode with your package manager or remove its binary manually.",
|
||||
),
|
||||
)
|
||||
const removal =
|
||||
method && method !== "curl" && installation.installedPackage
|
||||
? { method, command: Installation.uninstallCommand(method, installation.installedPackage) }
|
||||
: undefined
|
||||
const shell = method === "curl" ? yield* installation.shellChanges() : []
|
||||
const v1Installed = (!input.keepConfig || !input.keepData) && (yield* installation.v1Installed())
|
||||
const interactive = !input.force && !input.dryRun
|
||||
if (interactive)
|
||||
yield* requireInteractive("Pass --force to uninstall without an interactive terminal, or --dry-run to preview.")
|
||||
const removeConfig = yield* confirmRemoval({
|
||||
keep: input.keepConfig,
|
||||
interactive,
|
||||
message: v1Installed
|
||||
? "Delete global OpenCode config? OpenCode v1 also uses it."
|
||||
: "Delete global OpenCode config?",
|
||||
})
|
||||
const removeData = yield* confirmRemoval({
|
||||
keep: input.keepData,
|
||||
interactive,
|
||||
message: v1Installed ? "Delete all OpenCode data? OpenCode v1 also uses it." : "Delete all OpenCode data?",
|
||||
})
|
||||
const directories = [
|
||||
{ path: global.data, label: "Data", keep: !removeData },
|
||||
{ path: global.cache, label: "Cache", keep: false },
|
||||
{ path: global.config, label: "Config", keep: !removeConfig },
|
||||
{ path: global.state, label: "State", keep: false },
|
||||
]
|
||||
|
||||
log.message("Uninstall plan:")
|
||||
log.info("Stop the local background service")
|
||||
yield* Effect.forEach(
|
||||
directories,
|
||||
(directory) =>
|
||||
fs
|
||||
.exists(directory.path)
|
||||
.pipe(
|
||||
Effect.flatMap((exists) =>
|
||||
exists
|
||||
? Effect.sync(() =>
|
||||
log.info(`${directory.keep ? "Keep" : "Remove"} ${directory.label}: ${directory.path}`),
|
||||
)
|
||||
: Effect.void,
|
||||
),
|
||||
),
|
||||
{ discard: true },
|
||||
)
|
||||
shell.forEach((change) => log.info(`Remove installer PATH entry: ${change.path}`))
|
||||
if (removal) log.info(`Package: ${removal.command.join(" ")}`)
|
||||
if (method === "curl") log.info(`Binary (manual removal): ${process.execPath}`)
|
||||
if (v1Installed && (removeConfig || removeData))
|
||||
log.warn("OpenCode v1 is also installed and uses the files marked for removal.")
|
||||
if (input.dryRun) {
|
||||
outro("Dry run - no changes made")
|
||||
return undefined
|
||||
}
|
||||
if (!input.force) {
|
||||
if (!(yield* prompt(() => confirm({ message: "Proceed with uninstalling OpenCode?", initialValue: false })))) {
|
||||
outro("Cancelled")
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const progress = spinner()
|
||||
yield* Effect.gen(function* () {
|
||||
progress.start("Stopping background service...")
|
||||
const options = yield* ServiceConfig.options()
|
||||
yield* ServerConnection.shutdownPersistentPty(options).pipe(Effect.ignore)
|
||||
yield* Service.stop(options)
|
||||
progress.stop("Background service stopped")
|
||||
|
||||
if (removal) {
|
||||
progress.start(`Running ${removal.command.join(" ")}...`)
|
||||
yield* installation.uninstall(removal.method)
|
||||
progress.stop("Package removed")
|
||||
}
|
||||
yield* Effect.forEach(
|
||||
shell,
|
||||
(change) =>
|
||||
Effect.gen(function* () {
|
||||
progress.start(`Cleaning ${change.path}...`)
|
||||
yield* fs.writeFileString(change.path, change.content)
|
||||
progress.stop(`Cleaned ${change.path}`)
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* Effect.forEach(
|
||||
directories.filter((directory) => !directory.keep),
|
||||
(directory) =>
|
||||
Effect.gen(function* () {
|
||||
progress.start(`Removing ${directory.label}...`)
|
||||
yield* fs.remove(directory.path, { recursive: true, force: true })
|
||||
progress.stop(`Removed ${directory.label}`)
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
}).pipe(Effect.tapCause(() => Effect.sync(() => progress.stop("Uninstall failed", 1))))
|
||||
|
||||
if (method === "curl") {
|
||||
log.message("To finish removing the binary, run:")
|
||||
log.info(Installation.binaryRemovalCommand())
|
||||
}
|
||||
outro("Done")
|
||||
return undefined
|
||||
}, handlePromptErrors),
|
||||
)
|
||||
|
||||
function confirmRemoval(input: { readonly keep: boolean; readonly interactive: boolean; readonly message: string }) {
|
||||
if (input.keep) return Effect.succeed(false)
|
||||
if (!input.interactive) return Effect.succeed(true)
|
||||
return prompt(() => confirm({ message: input.message, initialValue: true }))
|
||||
}
|
||||
@@ -6,6 +6,16 @@ import { Global } from "@opencode-ai/util/global"
|
||||
import { Updater } from "../services/updater"
|
||||
import { Config } from "../config"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
|
||||
type Requirements =
|
||||
| FileSystem.FileSystem
|
||||
| Global.Service
|
||||
| Npm.Service
|
||||
| AppProcess.Service
|
||||
| Updater.Service
|
||||
| Config.Service
|
||||
| Scope.Scope
|
||||
|
||||
export type Input<Value> =
|
||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||
@@ -14,29 +24,11 @@ export type Input<Value> =
|
||||
? Input
|
||||
: never
|
||||
|
||||
type RuntimeHandler = (
|
||||
input: unknown,
|
||||
) => Effect.Effect<
|
||||
void,
|
||||
unknown,
|
||||
FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope
|
||||
>
|
||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Requirements>
|
||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||
default: (
|
||||
input: Input<Node>,
|
||||
) => Effect.Effect<
|
||||
void,
|
||||
any,
|
||||
FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope
|
||||
>
|
||||
default: (input: Input<Node>) => Effect.Effect<void, any, Requirements>
|
||||
}>
|
||||
type ProvidedCommand = Command.Command<
|
||||
string,
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
FileSystem.FileSystem | Global.Service | Npm.Service | Updater.Service | Config.Service | Scope.Scope
|
||||
>
|
||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Requirements>
|
||||
|
||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||
? Loader<Node>
|
||||
@@ -67,12 +59,14 @@ export function handlers<const Root extends Spec.Any>(root: Root, handlers: Hand
|
||||
function add(node: Spec.Any, value: RuntimeHandlers) {
|
||||
if (typeof value === "function") {
|
||||
result.push({ spec: node.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
|
||||
for (const alias of node.aliases) result.push({ spec: alias.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
|
||||
for (const alias of node.aliases)
|
||||
result.push({ spec: alias.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
|
||||
return
|
||||
}
|
||||
if (value.$) {
|
||||
result.push({ spec: node.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
|
||||
for (const alias of node.aliases) result.push({ spec: alias.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
|
||||
for (const alias of node.aliases)
|
||||
result.push({ spec: alias.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
|
||||
}
|
||||
for (const [name, child] of Object.entries(node.commands)) add(child, value[name] as RuntimeHandlers)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CpuProfile } from "./cpu-profile"
|
||||
const Handlers = Runtime.handlers(Commands, {
|
||||
$: () => import("./commands/handlers/default"),
|
||||
upgrade: () => import("./commands/handlers/upgrade"),
|
||||
uninstall: () => import("./commands/handlers/uninstall"),
|
||||
acp: () => import("./commands/handlers/acp"),
|
||||
api: () => import("./commands/handlers/api"),
|
||||
auth: {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
export * as Installation from "./installation"
|
||||
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Duration, Effect, FileSystem, Schema } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import path from "node:path"
|
||||
|
||||
export const methods = ["curl", "npm", "pnpm", "bun", "yarn"] as const
|
||||
export type Method = (typeof methods)[number]
|
||||
|
||||
export type ShellChange = {
|
||||
readonly path: string
|
||||
readonly content: string
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly installedPackage: string | undefined
|
||||
readonly method: () => Effect.Effect<Method | undefined>
|
||||
readonly v1Installed: () => Effect.Effect<boolean>
|
||||
readonly uninstall: (method: Exclude<Method, "curl">) => Effect.Effect<void, Error>
|
||||
readonly shellChanges: () => Effect.Effect<ReadonlyArray<ShellChange>>
|
||||
}
|
||||
|
||||
const Manifest = Schema.fromJsonString(
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
bin: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}),
|
||||
)
|
||||
|
||||
export const make = Effect.fnUntraced(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const appProcess = yield* AppProcess.Service
|
||||
const installedPackage = yield* Effect.gen(function* () {
|
||||
const executable = yield* fs.realPath(process.execPath)
|
||||
const directory = path.dirname(path.dirname(executable))
|
||||
const manifest = yield* fs
|
||||
.readFileString(path.join(directory, "package.json"))
|
||||
.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Manifest)))
|
||||
return Object.values(manifest.bin ?? {}).some((bin) => path.resolve(directory, bin) === executable)
|
||||
? manifest.name
|
||||
: undefined
|
||||
}).pipe(Effect.orElseSucceed(() => undefined))
|
||||
|
||||
const run = Effect.fnUntraced(
|
||||
function* (command: ReadonlyArray<string>, timeout: Duration.Input = "10 seconds") {
|
||||
const result = yield* appProcess.run(ChildProcess.make(command[0], command.slice(1)), {
|
||||
timeout,
|
||||
maxOutputBytes: 100_000,
|
||||
maxErrorBytes: 100_000,
|
||||
})
|
||||
return {
|
||||
code: result.exitCode,
|
||||
stdout: result.stdout.toString("utf8"),
|
||||
stderr: result.stderr.toString("utf8"),
|
||||
}
|
||||
},
|
||||
Effect.catch((error) =>
|
||||
Effect.succeed({
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const method = Effect.fn("cli.installation.method")(function* () {
|
||||
const binary = path.join(
|
||||
global.home,
|
||||
".opencode",
|
||||
"bin",
|
||||
process.platform === "win32" ? "opencode2.exe" : "opencode2",
|
||||
)
|
||||
if (path.resolve(process.execPath) === path.resolve(binary)) return "curl"
|
||||
if (!installedPackage) return undefined
|
||||
|
||||
const checks: ReadonlyArray<{ method: Method; command: string[] }> = [
|
||||
{ method: "npm", command: ["npm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
||||
{ method: "yarn", command: ["yarn", "global", "list"] },
|
||||
]
|
||||
const results = yield* Effect.forEach(
|
||||
checks,
|
||||
(check) => run(check.command).pipe(Effect.map((result) => ({ check, result }))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return results.find((result) => result.result.stdout.includes(installedPackage))?.check.method
|
||||
})
|
||||
|
||||
const v1Installed = Effect.fn("cli.installation.v1-installed")(
|
||||
function* () {
|
||||
const curlBinary = path.join(
|
||||
global.home,
|
||||
".opencode",
|
||||
"bin",
|
||||
process.platform === "win32" ? "opencode.exe" : "opencode",
|
||||
)
|
||||
if (yield* fs.exists(curlBinary)) return true
|
||||
// Package-manager installs resolve through PATH (including Bun, nvm, pnpm, Yarn, and Homebrew).
|
||||
yield* appProcess.run(ChildProcess.make("opencode", ["--version"]), {
|
||||
timeout: "5 seconds",
|
||||
maxOutputBytes: 10_000,
|
||||
maxErrorBytes: 10_000,
|
||||
})
|
||||
return true
|
||||
},
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
)
|
||||
|
||||
const uninstall = Effect.fn("cli.installation.uninstall")(function* (method: Exclude<Method, "curl">) {
|
||||
if (!installedPackage) return yield* Effect.fail(new Error("Could not identify the installed OpenCode package"))
|
||||
const result = yield* run(uninstallCommand(method, installedPackage), "5 minutes")
|
||||
if (result.code !== 0)
|
||||
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to uninstall with ${method}`))
|
||||
return undefined
|
||||
})
|
||||
|
||||
const shellChanges = Effect.fn("cli.installation.shell-changes")(function* () {
|
||||
const bin = path.join(global.home, ".opencode", "bin")
|
||||
const files = yield* fs.readDirectory(bin).pipe(Effect.orElseSucceed(() => []))
|
||||
// V1 and other installed binaries still need this shared PATH entry.
|
||||
if (files.some((name) => name !== path.basename(process.execPath))) return []
|
||||
const shell = path.basename(process.env.SHELL || "bash")
|
||||
const entry = shell === "fish" ? `fish_add_path ${bin}` : `export PATH=${bin}:$PATH`
|
||||
const changes = yield* Effect.forEach(shellConfigFiles(shell, global.home), (file) =>
|
||||
fs.readFileString(file).pipe(
|
||||
Effect.flatMap((content) => {
|
||||
const updated = removeShellEntry(content, entry)
|
||||
return Effect.succeed(updated === undefined ? undefined : { path: file, content: updated })
|
||||
}),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
),
|
||||
)
|
||||
return changes.filter((change) => change !== undefined)
|
||||
})
|
||||
|
||||
return { installedPackage, method, v1Installed, uninstall, shellChanges } satisfies Interface
|
||||
})
|
||||
|
||||
export function uninstallCommand(method: Exclude<Method, "curl">, name: string): [string, ...string[]] {
|
||||
const commands: Record<Exclude<Method, "curl">, [string, ...string[]]> = {
|
||||
npm: ["npm", "uninstall", "--global", name],
|
||||
pnpm: ["pnpm", "remove", "--global", name],
|
||||
bun: ["bun", "remove", "--global", name],
|
||||
yarn: ["yarn", "global", "remove", name],
|
||||
}
|
||||
return commands[method]
|
||||
}
|
||||
|
||||
export function binaryRemovalCommand(executable = process.execPath) {
|
||||
return process.platform === "win32"
|
||||
? `Remove-Item -LiteralPath '${executable.replaceAll("'", "''")}'`
|
||||
: `rm -- '${executable.replaceAll("'", "'\\''")}'`
|
||||
}
|
||||
|
||||
function shellConfigFiles(shell: string, home: string) {
|
||||
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config")
|
||||
const zsh = process.env.ZDOTDIR || home
|
||||
const candidates: Record<string, string[]> = {
|
||||
fish: [path.join(home, ".config", "fish", "config.fish")],
|
||||
zsh: [
|
||||
path.join(zsh, ".zshrc"),
|
||||
path.join(zsh, ".zshenv"),
|
||||
path.join(xdg, "zsh", ".zshrc"),
|
||||
path.join(xdg, "zsh", ".zshenv"),
|
||||
],
|
||||
bash: [
|
||||
path.join(home, ".bashrc"),
|
||||
path.join(home, ".bash_profile"),
|
||||
path.join(home, ".profile"),
|
||||
path.join(xdg, "bash", ".bashrc"),
|
||||
path.join(xdg, "bash", ".bash_profile"),
|
||||
],
|
||||
ash: [path.join(home, ".ashrc"), path.join(home, ".profile")],
|
||||
sh: [path.join(home, ".ashrc"), path.join(home, ".profile")],
|
||||
}
|
||||
return [...new Set(candidates[shell] ?? [])]
|
||||
}
|
||||
|
||||
function removeShellEntry(content: string, entry: string) {
|
||||
const newline = content.includes("\r\n") ? "\r\n" : "\n"
|
||||
const lines = content.split(/\r?\n/)
|
||||
const marker = lines.findIndex((line, index) => line === "# opencode" && lines[index + 1] === entry)
|
||||
if (marker === -1) return undefined
|
||||
const start = marker > 0 && lines[marker - 1] === "" ? marker - 1 : marker
|
||||
return [...lines.slice(0, start), ...lines.slice(marker + 2)].join(newline)
|
||||
}
|
||||
@@ -6,9 +6,10 @@ import { ChildProcess } from "effect/unstable/process"
|
||||
import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
import { action, parseReleaseVersion, type Policy } from "./updater-action"
|
||||
import { Installation } from "./installation"
|
||||
|
||||
export const methods = ["curl", "npm", "pnpm", "bun", "yarn"] as const
|
||||
export type Method = (typeof methods)[number]
|
||||
export const methods = Installation.methods
|
||||
export type Method = Installation.Method
|
||||
export type RunResult = { readonly type: "available" | "installed"; readonly version: string }
|
||||
export type CheckResult = RunResult | { readonly type: "unavailable"; readonly message: string }
|
||||
|
||||
@@ -56,17 +57,10 @@ const make = Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const appProcess = yield* AppProcess.Service
|
||||
const installation = yield* Installation.make()
|
||||
const installedVersion = yield* Ref.make(OPENCODE_VERSION)
|
||||
const channel = OPENCODE_CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||
const installedPackage = yield* Effect.gen(function* () {
|
||||
const executable = yield* fs.realPath(process.execPath)
|
||||
const directory = path.dirname(path.dirname(executable))
|
||||
const manifest: { name: string; bin?: Record<string, string> } = yield* fs
|
||||
.readFileString(path.join(directory, "package.json"))
|
||||
.pipe(Effect.flatMap((text) => Effect.try(() => JSON.parse(text))))
|
||||
if (Object.values(manifest.bin ?? {}).some((bin) => path.resolve(directory, bin) === executable))
|
||||
return manifest.name
|
||||
}).pipe(Effect.orElseSucceed(() => undefined))
|
||||
const installedPackage = installation.installedPackage
|
||||
|
||||
const readPolicy = Effect.fnUntraced(function* () {
|
||||
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
|
||||
@@ -95,29 +89,7 @@ const make = Effect.gen(function* () {
|
||||
)
|
||||
})
|
||||
|
||||
const method = Effect.fnUntraced(function* () {
|
||||
const binary = path.join(
|
||||
global.home,
|
||||
".opencode",
|
||||
"bin",
|
||||
process.platform === "win32" ? "opencode2.exe" : "opencode2",
|
||||
)
|
||||
if (path.resolve(process.execPath) === path.resolve(binary)) return "curl"
|
||||
if (!installedPackage) return
|
||||
|
||||
const checks: ReadonlyArray<{ method: Method; command: string[] }> = [
|
||||
{ method: "npm", command: ["npm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
||||
{ method: "yarn", command: ["yarn", "global", "list"] },
|
||||
]
|
||||
const results = yield* Effect.forEach(
|
||||
checks,
|
||||
(check) => exec(check.command).pipe(Effect.map((result) => ({ check, result }))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return results.find((result) => result.result.stdout.includes(installedPackage))?.check.method
|
||||
})
|
||||
const method = installation.method
|
||||
|
||||
const release = Effect.fnUntraced(function* () {
|
||||
const response = yield* Effect.tryPromise({
|
||||
|
||||
Reference in New Issue
Block a user