Merge remote-tracking branch 'origin/main' into pr-36-generic-agent-config

# Conflicts:
#	docs/config.html
#	docs/config.md
#	src/cli.test.ts
#	src/cli.ts
#	src/client/src/components/settings/SettingsSessiondPanel.ts
#	src/client/src/components/settings/settingsConfigDraft.test.ts
#	src/client/src/components/settings/settingsConfigDraft.ts
#	src/server/app.test.ts
#	src/server/app.ts
#	src/server/configRoutes.test.ts
#	src/server/configRoutes.ts
#	src/server/piWebPluginService.test.ts
#	src/server/piWebPluginService.ts
#	src/server/piWebStatus.test.ts
#	src/server/piWebStatus.ts
#	src/server/piWebStatusCache.ts
#	src/server/sessions/authService.test.ts
#	src/server/sessions/piSessionService.ts
#	src/server/sessions/sessionRoutes.test.ts
This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 20:30:08 +02:00
289 changed files with 29164 additions and 6419 deletions
+50 -17
View File
@@ -70,23 +70,25 @@ interface PiWebPluginEntry {
type ArraylessPluginRecord = Omit<PluginRecord, "source" | "scope">;
export class DefaultPiPackageProvider implements PiPackageProvider {
private readonly packageManager: DefaultPackageManager;
constructor(cwd = process.cwd(), agentDir?: string) {
const resolvedAgentDir = agentDir ?? defaultAgentDirForCwd(cwd);
this.packageManager = new DefaultPackageManager({
cwd,
agentDir: resolvedAgentDir,
settingsManager: SettingsManager.create(cwd, resolvedAgentDir),
});
}
constructor(
private readonly cwd = process.cwd(),
private readonly agentDir = defaultAgentDirForCwd(cwd),
) {}
listPackages(): ConfiguredPiPackage[] {
return this.packageManager.listConfiguredPackages();
return this.createPackageManager().listConfiguredPackages();
}
getInstalledPath(source: string, scope: "user" | "project"): string | undefined {
return this.packageManager.getInstalledPath(source, scope);
return this.createPackageManager().getInstalledPath(source, scope);
}
private createPackageManager(): DefaultPackageManager {
return new DefaultPackageManager({
cwd: this.cwd,
agentDir: this.agentDir,
settingsManager: SettingsManager.create(this.cwd, this.agentDir),
});
}
}
@@ -154,7 +156,7 @@ export class PiWebPluginService {
private pluginInfo(plugin: PluginRecord, config: PiWebConfig): PiWebPluginInfo {
return {
id: plugin.id,
module: `/pi-web-plugins/${encodeURIComponent(plugin.id)}/${plugin.entryFile}?v=${encodeURIComponent(plugin.version)}`,
module: `/pi-web-plugins/${encodeURIComponent(plugin.id)}/${plugin.entryFile}?${pluginModuleQuery(plugin)}`,
source: plugin.source,
scope: plugin.scope,
machineSpecific: plugin.machineSpecific,
@@ -219,6 +221,35 @@ function bundledPluginRoot(packageRoot: string): string {
return join(packageRoot, "dist", "pi-web-plugins");
}
function pluginModuleQuery(plugin: PluginRecord): string {
const params = new URLSearchParams({ v: plugin.version });
const dockerMode = plugin.id === "updates" ? dockerModeFromEnv() : undefined;
if (dockerMode !== undefined) params.set("piWebDockerMode", dockerMode);
return params.toString();
}
function dockerModeFromEnv(): "runtime" | "dev" | undefined {
if (!isTruthyEnv("PI_WEB_DOCKER_RUNTIME")) return undefined;
const mode = process.env["PI_WEB_DOCKER_MODE"];
if (mode === "runtime" || mode === "dev") return mode;
if (firstNonEmptyEnv("PI_WEB_DOCKER_DEV_REPO_ROOT") !== undefined) return "dev";
if (firstNonEmptyEnv("PI_WEB_DOCKER_INSTALL_DIR") !== undefined) return "runtime";
return undefined;
}
function firstNonEmptyEnv(...keys: string[]): string | undefined {
for (const key of keys) {
const value = process.env[key];
if (value !== undefined && value !== "") return value;
}
return undefined;
}
function isTruthyEnv(key: string): boolean {
const value = process.env[key];
return value !== undefined && value !== "" && value !== "0" && value.toLowerCase() !== "false";
}
function sourceCheckoutPluginRoots(cwd: string): LocalPluginRoot[] {
const pluginsRoot = join(cwd, "plugins");
if (!existsSync(join(cwd, "src", "server", "index.ts")) || !existsSync(pluginsRoot)) return [];
@@ -338,10 +369,12 @@ function isWithin(root: string, candidate: string): boolean {
}
function contentTypeFor(path: string): string {
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
if (path.endsWith(".json")) return "application/json; charset=utf-8";
if (path.endsWith(".css")) return "text/css; charset=utf-8";
if (path.endsWith(".html")) return "text/html; charset=utf-8";
const lowerPath = path.toLowerCase();
if (lowerPath.endsWith(".js")) return "application/javascript; charset=utf-8";
if (lowerPath.endsWith(".json")) return "application/json; charset=utf-8";
if (lowerPath.endsWith(".css")) return "text/css; charset=utf-8";
if (lowerPath.endsWith(".html")) return "text/html; charset=utf-8";
if (lowerPath.endsWith(".svg")) return "image/svg+xml";
return "application/octet-stream";
}