diff --git a/extension/src/operations.ts b/extension/src/operations.ts index 57f7ecf..1e0f5e9 100644 --- a/extension/src/operations.ts +++ b/extension/src/operations.ts @@ -197,7 +197,8 @@ function handleMissingPackageAlert( }, ): Effect.Effect { const { context, code, uv, config } = deps; - const installPackages = (venvPath: string, packages: ReadonlyArray) => + + const installPackages = (venv: string, packages: ReadonlyArray) => code.window.useInfallible((api) => api.withProgress( { @@ -211,7 +212,20 @@ function handleMissingPackageAlert( progress.report({ message: `Installing ${packages.join(", ")}...`, }); - yield* uv.pipInstall(packages, { venv: venvPath }); + yield* Effect.logDebug("Attempting `uv add`.").pipe( + Effect.annotateLogs({ packages, directory: venv }), + ); + yield* uv.add(packages, { directory: venv }).pipe( + Effect.catchTag( + "MissingPyProjectError", + Effect.fnUntraced(function* () { + yield* Effect.logWarning( + "Failed to `uv add`, attempting `uv pip install`.", + ); + yield* uv.pipInstall(packages, { venv }); + }), + ), + ); progress.report({ message: `Successfully installed ${packages.join(", ")}`, }); @@ -249,21 +263,21 @@ function handleMissingPackageAlert( const venv = findVenvPath(context.controller.env.path); if (Option.isNone(venv)) { - // no venv so can't do anything + yield* Effect.logWarning("Could not find venv.Skipping install."); return; } - const choice = yield* code.window - .useInfallible((api) => + const choice = Option.fromNullable( + yield* code.window.useInfallible((api) => api.showInformationMessage( operation.packages.length === 1 - ? `Missing package: ${operation.packages[0]}. Install with \`uv pip\`?` - : `Missing packages: ${operation.packages.join(", ")}. Install with \`uv pip\`?`, + ? `Missing package: ${operation.packages[0]}. Install with uv?` + : `Missing packages: ${operation.packages.join(", ")}. Install with uv?`, "Install All", "Customize...", ), - ) - .pipe(Effect.map(Option.fromNullable)); + ), + ); if (Option.isNone(choice)) { // dismissed @@ -275,7 +289,9 @@ function handleMissingPackageAlert( Effect.annotateLogs("packages", operation.packages), ); yield* installPackages(venv.value, operation.packages); - } else if (choice.value === "Customize...") { + } + + if (choice.value === "Customize...") { const response = yield* code.window .useInfallible((api) => api.showInputBox({ @@ -294,10 +310,9 @@ function handleMissingPackageAlert( yield* Effect.logInfo("Install packages").pipe( Effect.annotateLogs("packages", newPackages), ); + yield* installPackages(venv.value, newPackages); } - - // Cancel - do nothing }).pipe(Effect.catchAllCause(Effect.logError)); } diff --git a/extension/src/services/Uv.ts b/extension/src/services/Uv.ts index e577965..26dd8e5 100644 --- a/extension/src/services/Uv.ts +++ b/extension/src/services/Uv.ts @@ -16,19 +16,49 @@ class UvError extends Data.TaggedError("UvError")<{ stderr: string; }> {} +class MissingPyProjectError extends Data.TaggedError("MissingPyProjectError")<{ + directory: string; + cause: UvError; +}> {} + export class Uv extends Effect.Service()("Uv", { dependencies: [NodeContext.layer, LoggerLive], scoped: Effect.gen(function* () { const executor = yield* CommandExecutor.CommandExecutor; const uv = createUv(executor); return { + add( + packages: ReadonlyArray, + options: { + readonly directory: string; + }, + ) { + const { directory } = options; + return uv({ + args: ["add", "--directory", directory, ...packages], + }).pipe( + Effect.catchTag("UvError", (cause) => + Effect.fail( + cause.stderr.includes( + "error: No `pyproject.toml` found in current directory or any parent directory", + ) + ? new MissingPyProjectError({ directory, cause }) + : cause, + ), + ), + ); + }, pipInstall( packages: ReadonlyArray, - options: { readonly venv: string }, + options: { + readonly venv: string; + }, ) { return uv({ args: ["pip", "install", ...packages], - venv: options.venv, + env: { + VIRTUAL_ENV: options.venv, + }, }); }, }; @@ -38,10 +68,10 @@ export class Uv extends Effect.Service()("Uv", { function createUv(executor: CommandExecutor.CommandExecutor) { return Effect.fn("uv")(function* (options: { readonly args: ReadonlyArray; - readonly venv: string; + readonly env?: Record; }) { const command = Command.make("uv", ...options.args).pipe( - Command.env({ NO_COLOR: "1", VIRTUAL_ENV: options.venv }), + Command.env({ NO_COLOR: "1", ...options.env }), ); yield* Effect.logDebug("Running command").pipe( Effect.annotateLogs({ command }),