feat(ui): Improved project approval settings

This commit is contained in:
Armin Ronacher
2026-06-09 13:25:54 +02:00
parent 66335d3a49
commit 5cb4f597f7
25 changed files with 767 additions and 374 deletions

View File

@@ -1,6 +1,7 @@
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
import chalk from "chalk";
import { selectConfig } from "./cli/config-selector.ts";
import { createProjectTrustContext } from "./cli/project-trust.ts";
import {
APP_NAME,
detectInstallMethod,
@@ -12,7 +13,10 @@ import {
type SelfUpdateCommand,
VERSION,
} from "./config.ts";
import type { ExtensionFactory } from "./core/extensions/types.ts";
import { DefaultPackageManager } from "./core/package-manager.ts";
import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
import { DefaultResourceLoader } from "./core/resource-loader.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
@@ -425,22 +429,82 @@ function parseProjectTrustOverride(args: readonly string[]): boolean | undefined
return trustOverride;
}
function resolveProjectTrusted(cwd: string, agentDir: string, trustOverride: boolean | undefined): boolean {
if (trustOverride !== undefined) {
return trustOverride;
}
return !hasProjectTrustInputs(cwd) || new ProjectTrustStore(agentDir).get(cwd) === true;
export interface PackageCommandRuntimeOptions {
extensionFactories?: ExtensionFactory[];
}
export async function handleConfigCommand(args: string[]): Promise<boolean> {
interface CommandSettingsResult {
settingsManager: SettingsManager;
projectTrustWarnings: string[];
}
function getCommandAppMode(): AppMode {
return process.stdin.isTTY && process.stdout.isTTY ? "interactive" : "print";
}
function reportProjectTrustWarnings(warnings: readonly string[]): void {
for (const warning of warnings) {
console.error(chalk.yellow(`Warning: ${warning}`));
}
}
async function createCommandSettingsManager(options: {
cwd: string;
agentDir: string;
projectTrustOverride?: boolean;
extensionFactories?: ExtensionFactory[];
}): Promise<CommandSettingsResult> {
const settingsManager = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });
const projectTrustWarnings: string[] = [];
const appMode = getCommandAppMode();
const extensionsResult =
options.projectTrustOverride === undefined && hasProjectTrustInputs(options.cwd)
? await new DefaultResourceLoader({
cwd: options.cwd,
agentDir: options.agentDir,
settingsManager,
extensionFactories: options.extensionFactories,
}).loadProjectTrustExtensions()
: undefined;
for (const error of extensionsResult?.errors ?? []) {
projectTrustWarnings.push(`Failed to load extension "${error.path}": ${error.error}`);
}
const projectTrusted = await resolveProjectTrusted({
cwd: options.cwd,
trustStore: new ProjectTrustStore(options.agentDir),
trustOverride: options.projectTrustOverride,
defaultProjectTrust: settingsManager.getDefaultProjectTrust(),
extensionsResult,
projectTrustContext: createProjectTrustContext({
cwd: options.cwd,
mode: appMode,
settingsManager,
hasUI: appMode === "interactive",
}),
onExtensionError: (message) => projectTrustWarnings.push(message),
});
settingsManager.setProjectTrusted(projectTrusted);
return { settingsManager, projectTrustWarnings };
}
export async function handleConfigCommand(
args: string[],
runtimeOptions: PackageCommandRuntimeOptions = {},
): Promise<boolean> {
if (args[0] !== "config") {
return false;
}
const cwd = process.cwd();
const agentDir = getAgentDir();
const projectTrusted = parseProjectTrustOverride(args) ?? true;
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
const { settingsManager, projectTrustWarnings } = await createCommandSettingsManager({
cwd,
agentDir,
projectTrustOverride: parseProjectTrustOverride(args),
extensionFactories: runtimeOptions.extensionFactories,
});
reportProjectTrustWarnings(projectTrustWarnings);
reportSettingsErrors(settingsManager, "config command");
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
const resolvedPaths = await packageManager.resolve();
@@ -455,7 +519,10 @@ export async function handleConfigCommand(args: string[]): Promise<boolean> {
process.exit(0);
}
export async function handlePackageCommand(args: string[]): Promise<boolean> {
export async function handlePackageCommand(
args: string[],
runtimeOptions: PackageCommandRuntimeOptions = {},
): Promise<boolean> {
const options = parsePackageCommand(args);
if (!options) {
return false;
@@ -505,13 +572,18 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
const cwd = process.cwd();
const agentDir = getAgentDir();
const writesProjectPackageConfig = (options.command === "install" || options.command === "remove") && options.local;
const projectTrusted = resolveProjectTrusted(cwd, agentDir, options.projectTrustOverride);
if (!projectTrusted && writesProjectPackageConfig) {
const { settingsManager, projectTrustWarnings } = await createCommandSettingsManager({
cwd,
agentDir,
projectTrustOverride: options.projectTrustOverride,
extensionFactories: runtimeOptions.extensionFactories,
});
reportProjectTrustWarnings(projectTrustWarnings);
if (!settingsManager.isProjectTrusted() && writesProjectPackageConfig) {
console.error(chalk.red("Project is not trusted. Use --approve to modify local package config."));
process.exitCode = 1;
return true;
}
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
reportSettingsErrors(settingsManager, "package command");
const selfUpdateNpmCommand = settingsManager.getGlobalSettings().npmCommand;