Archived
feat: generalize agent runtime config
This commit is contained in:
+40
-1
@@ -15,7 +15,7 @@ import type { SessionProxyDaemon } from "./sessiond/sessionProxyRoutes.js";
|
||||
import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js";
|
||||
import { machineScopedPluginId } from "../shared/machinePluginIds.js";
|
||||
import { MAX_IMAGE_PREVIEW_BYTES } from "../shared/workspaceFiles.js";
|
||||
import type { PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
|
||||
import type { PiWebConfigResponse, PiWebConfigValues, PiWebStatusResponse } from "../shared/apiTypes.js";
|
||||
import type { Project, Workspace } from "./types.js";
|
||||
|
||||
let app: FastifyInstance;
|
||||
@@ -556,6 +556,35 @@ describe("buildApp", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the latest configured agent dir for PI WEB status after config writes", async () => {
|
||||
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
|
||||
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
||||
try {
|
||||
const initialAgentDir = join(tempDir, "initial-agent");
|
||||
const updatedAgentDir = join(tempDir, "updated-agent");
|
||||
piWebConfig = { agent: { command: "pi", dir: initialAgentDir } };
|
||||
await mkdir(initialAgentDir, { recursive: true });
|
||||
await installConfiguredPiWebPackage(updatedAgentDir);
|
||||
|
||||
const initialStatus = await app.inject({ method: "GET", url: "/api/pi-web/status" });
|
||||
expect(initialStatus.statusCode).toBe(200);
|
||||
|
||||
const updateResponse = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/config",
|
||||
payload: { config: { agent: { command: "pi", dir: updatedAgentDir } } },
|
||||
});
|
||||
expect(updateResponse.statusCode).toBe(200);
|
||||
|
||||
const refreshedStatus = await app.inject({ method: "GET", url: "/api/pi-web/status" });
|
||||
|
||||
expect(refreshedStatus.statusCode).toBe(200);
|
||||
expect(refreshedStatus.json<PiWebStatusResponse>().components.web.installation).toMatchObject({ kind: "pi-package", source: process.cwd(), scope: "user" });
|
||||
} finally {
|
||||
restoreEnv("PI_WEB_SKIP_VERSION_CHECK", originalSkipVersionCheck);
|
||||
}
|
||||
});
|
||||
|
||||
it("serves supported workspace images as previews", async () => {
|
||||
const addResponse = await app.inject({
|
||||
method: "POST",
|
||||
@@ -920,6 +949,16 @@ function fakeSessionDaemon(): SessionProxyDaemon {
|
||||
};
|
||||
}
|
||||
|
||||
async function installConfiguredPiWebPackage(agentDir: string): Promise<void> {
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ packages: [process.cwd()] }, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) Reflect.deleteProperty(process.env, key);
|
||||
else process.env[key] = value;
|
||||
}
|
||||
|
||||
function fakeRemoteClient(overrides: Partial<MachineClient>): MachineClient {
|
||||
return {
|
||||
request: () => Promise.resolve({ statusCode: 200, headers: {}, body: Readable.from([]) }),
|
||||
|
||||
+35
-7
@@ -20,9 +20,9 @@ import { registerTerminalProxyRoutes } from "./terminalProxyRoutes.js";
|
||||
import { registerWorkspaceDeletionRoutes } from "./workspaces/workspaceDeletionRoutes.js";
|
||||
import { createFilePiWebConfigService, registerConfigRoutes, type PiWebConfigService } from "./configRoutes.js";
|
||||
import { PiWebPluginService } from "./piWebPluginService.js";
|
||||
import { createPiWebStatusCache } from "./piWebStatusCache.js";
|
||||
import { createPiWebStatusCache, type PiWebStatusCache } from "./piWebStatusCache.js";
|
||||
import { getPiWebRuntime, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
|
||||
import { effectiveAgentConfig, effectivePiWebConfig } from "../config.js";
|
||||
import { effectiveAgentConfig, type EffectivePiWebAgentConfig } from "../config.js";
|
||||
import { MachineService } from "./machines/machineService.js";
|
||||
import { registerMachineRoutes } from "./machines/machineRoutes.js";
|
||||
import { registerMachineProxyRoutes } from "./machines/machineProxyRoutes.js";
|
||||
@@ -116,17 +116,42 @@ function registerLocalFileSuggestionRoutes(app: FastifyInstance, projects: Proje
|
||||
});
|
||||
}
|
||||
|
||||
async function readEffectiveConfig(config: Pick<PiWebConfigService, "read">) {
|
||||
return (await config.read()).effectiveConfig;
|
||||
}
|
||||
|
||||
async function readEffectiveAgentConfig(config: Pick<PiWebConfigService, "read">): Promise<EffectivePiWebAgentConfig> {
|
||||
return effectiveAgentConfig(process.env, await readEffectiveConfig(config));
|
||||
}
|
||||
|
||||
function invalidatePiWebStatusOnWrite(config: PiWebConfigService, statusCache: Pick<PiWebStatusCache, "invalidate">): PiWebConfigService {
|
||||
return {
|
||||
read: () => config.read(),
|
||||
write: async (nextConfig) => {
|
||||
const response = await config.write(nextConfig);
|
||||
statusCache.invalidate();
|
||||
return response;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInstance> {
|
||||
const app = Fastify({ logger: deps.logger ?? true, ...(deps.bodyLimit === undefined ? {} : { bodyLimit: deps.bodyLimit }) });
|
||||
await app.register(fastifyWebsocket);
|
||||
|
||||
const projects = deps.projects ?? new ProjectService(new ProjectStore());
|
||||
const workspaces = deps.workspaces ?? new WorkspaceService();
|
||||
const agent = effectiveAgentConfig(process.env, effectivePiWebConfig().config);
|
||||
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({ agentDir: agent.dir });
|
||||
const configService = deps.config ?? createFilePiWebConfigService();
|
||||
const readConfig = () => readEffectiveConfig(configService);
|
||||
const readAgentConfig = () => readEffectiveAgentConfig(configService);
|
||||
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({
|
||||
configProvider: readConfig,
|
||||
});
|
||||
const sessionDaemon = deps.sessionDaemon ?? new SessionDaemonClient();
|
||||
const piWebStatusCache = createPiWebStatusCache(() => getPiWebStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir }), {
|
||||
const piWebStatusCache = createPiWebStatusCache(async () => {
|
||||
const agent = await readAgentConfig();
|
||||
return getPiWebStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir });
|
||||
}, {
|
||||
onError: (error) => { app.log.warn({ err: error }, "failed to refresh PI WEB status cache"); },
|
||||
});
|
||||
const machines = deps.machines ?? new MachineService(undefined, {
|
||||
@@ -144,10 +169,13 @@ export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInsta
|
||||
});
|
||||
|
||||
app.get("/api/pi-web/status", async () => piWebStatusCache.get());
|
||||
app.get("/api/pi-web/version", async () => getPiWebVersionStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir }));
|
||||
app.get("/api/pi-web/version", async () => {
|
||||
const agent = await readAgentConfig();
|
||||
return getPiWebVersionStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir });
|
||||
});
|
||||
app.get("/api/pi-web/runtime", async () => getPiWebRuntime(sessionDaemon));
|
||||
app.get("/api/plugins", async () => piWebPlugins.plugins());
|
||||
registerConfigRoutes(app, configService);
|
||||
registerConfigRoutes(app, invalidatePiWebStatusOnWrite(configService, piWebStatusCache));
|
||||
|
||||
registerMachineRoutes(app, machines);
|
||||
registerMachinePluginProxyRoutes(app, machines);
|
||||
|
||||
@@ -37,11 +37,11 @@ describe("config routes", () => {
|
||||
const response = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/config",
|
||||
payload: { config: { host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "omp", dir: "~/.omp/agent" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads\\manual" }, maxUploadBytes: 1234 } },
|
||||
payload: { config: { host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "agent-lab", dir: "~/agent-profiles/lab" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads\\manual" }, maxUploadBytes: 1234 } },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(savedConfig).toEqual({ host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "omp", dir: "~/.omp/agent" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads/manual" }, maxUploadBytes: 1234 });
|
||||
expect(savedConfig).toEqual({ host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "agent-lab", dir: "~/agent-profiles/lab" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads/manual" }, maxUploadBytes: 1234 });
|
||||
expect(response.json<PiWebConfigResponse>().config).toEqual(savedConfig);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { effectiveAgentConfig, effectivePiWebConfig, hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
|
||||
import { hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, resolveEffectivePiWebConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
|
||||
import type { PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
|
||||
import { isPiWebPluginId } from "../shared/pluginIds.js";
|
||||
|
||||
@@ -20,14 +20,14 @@ export function createFilePiWebConfigService(options: LoadOptions = {}): PiWebCo
|
||||
|
||||
export function currentPiWebConfigResponse(options: LoadOptions = {}): PiWebConfigResponse {
|
||||
const loaded = loadPiWebConfig(options);
|
||||
const effective = effectivePiWebConfig(options);
|
||||
const effective = resolveEffectivePiWebConfig(loaded, options);
|
||||
const env = options.env ?? process.env;
|
||||
return {
|
||||
path: loaded.path,
|
||||
exists: loaded.exists,
|
||||
config: loaded.config,
|
||||
effectiveConfig: effective.config,
|
||||
envOverrides: piWebConfigEnvOverrides(env, loaded.config),
|
||||
envOverrides: piWebConfigEnvOverrides(env),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,8 +167,7 @@ function parsePluginsRequest(value: unknown): NonNullable<PiWebConfig["plugins"]
|
||||
}));
|
||||
}
|
||||
|
||||
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv, config: PiWebConfig = {}): PiWebConfigEnvOverrides {
|
||||
const agent = effectiveAgentConfig(env, config);
|
||||
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv): PiWebConfigEnvOverrides {
|
||||
return {
|
||||
host: isEnvSet(env["PI_WEB_HOST"]),
|
||||
port: isEnvSet(env["PI_WEB_PORT"]) || isEnvSet(env["PORT"]),
|
||||
@@ -176,8 +175,8 @@ function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv, config: PiWebConfig = {
|
||||
spawnSessions: isEnvSet(env["PI_WEB_SPAWN_SESSIONS"]),
|
||||
subsessions: isEnvSet(env["PI_WEB_SUBSESSIONS"]),
|
||||
agentCommand: isEnvSet(env["PI_WEB_AGENT_COMMAND"]),
|
||||
agentDir: hasAgentDirEnvOverride(env, agent.command),
|
||||
agentSessionDir: hasAgentSessionDirEnvOverride(env, agent.command),
|
||||
agentDir: hasAgentDirEnvOverride(env),
|
||||
agentSessionDir: hasAgentSessionDirEnvOverride(env),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,27 @@ describe("PiWebPluginService", () => {
|
||||
expect(manifest.plugins[0]?.module).toMatch(/^\/pi-web-plugins\/review\/dist\/review\.js\?v=\d+$/u);
|
||||
});
|
||||
|
||||
it("uses the current config provider for Pi package plugin discovery", async () => {
|
||||
const packageDir = join(tempDir, "pkg");
|
||||
const initialAgentDir = join(tempDir, "initial-agent");
|
||||
const updatedAgentDir = join(tempDir, "updated-agent");
|
||||
let currentConfig = { agent: { dir: initialAgentDir } };
|
||||
await writePlugin(packageDir, {
|
||||
packageJson: { piWeb: { plugins: [{ id: "agent-package", module: "dist/plugin.js" }] } },
|
||||
files: { "dist/plugin.js": "export default {};" },
|
||||
});
|
||||
await mkdir(initialAgentDir, { recursive: true });
|
||||
await mkdir(updatedAgentDir, { recursive: true });
|
||||
await writeFile(join(updatedAgentDir, "settings.json"), `${JSON.stringify({ packages: [packageDir] }, null, 2)}\n`, "utf8");
|
||||
const service = new PiWebPluginService({ roots: [], cwd: tempDir, configProvider: () => currentConfig });
|
||||
|
||||
await expect(service.manifest()).resolves.toEqual({ plugins: [] });
|
||||
|
||||
currentConfig = { agent: { dir: updatedAgentDir } };
|
||||
|
||||
await expect(service.manifest()).resolves.toMatchObject({ plugins: [{ id: "agent-package", source: packageDir, scope: "user" }] });
|
||||
});
|
||||
|
||||
it("discovers source checkout plugin packages without symlinks", async () => {
|
||||
await mkdir(join(tempDir, "src", "server"), { recursive: true });
|
||||
await writeFile(join(tempDir, "src", "server", "index.ts"), "export {};\n");
|
||||
|
||||
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
||||
import { dirname, join, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js";
|
||||
import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { effectiveAgentConfig, loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js";
|
||||
import type { PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope } from "../shared/apiTypes.js";
|
||||
import { isPiWebPluginId } from "../shared/pluginIds.js";
|
||||
|
||||
@@ -46,8 +46,9 @@ interface PiWebPluginServiceOptions {
|
||||
roots?: LocalPluginRoot[];
|
||||
cwd?: string;
|
||||
agentDir?: string;
|
||||
agentDirProvider?: () => string | Promise<string>;
|
||||
packageProvider?: PiPackageProvider | false;
|
||||
configProvider?: () => PiWebConfig;
|
||||
configProvider?: () => PiWebConfig | Promise<PiWebConfig>;
|
||||
}
|
||||
|
||||
interface LocalPluginRoot {
|
||||
@@ -71,11 +72,12 @@ type ArraylessPluginRecord = Omit<PluginRecord, "source" | "scope">;
|
||||
export class DefaultPiPackageProvider implements PiPackageProvider {
|
||||
private readonly packageManager: DefaultPackageManager;
|
||||
|
||||
constructor(cwd = process.cwd(), agentDir = getAgentDir()) {
|
||||
constructor(cwd = process.cwd(), agentDir?: string) {
|
||||
const resolvedAgentDir = agentDir ?? defaultAgentDirForCwd(cwd);
|
||||
this.packageManager = new DefaultPackageManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager: SettingsManager.create(cwd, agentDir),
|
||||
agentDir: resolvedAgentDir,
|
||||
settingsManager: SettingsManager.create(cwd, resolvedAgentDir),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,16 +90,32 @@ export class DefaultPiPackageProvider implements PiPackageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
function defaultAgentDirForCwd(cwd: string): string {
|
||||
return effectiveAgentConfig(process.env, loadPiWebConfig({ cwd }).config, cwd).dir;
|
||||
}
|
||||
|
||||
export class PiWebPluginService {
|
||||
private readonly cwd: string;
|
||||
private readonly roots: LocalPluginRoot[];
|
||||
private readonly packageProvider: PiPackageProvider | undefined;
|
||||
private readonly configProvider: () => PiWebConfig;
|
||||
private readonly agentDir: string | undefined;
|
||||
private readonly agentDirProvider: (() => string | Promise<string>) | undefined;
|
||||
private readonly packageProviderForAgentDir: ((agentDir: string) => PiPackageProvider) | undefined;
|
||||
private readonly configProvider: () => PiWebConfig | Promise<PiWebConfig>;
|
||||
|
||||
constructor(options: PiWebPluginServiceOptions = {}) {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const agentDir = options.agentDir ?? getAgentDir();
|
||||
this.cwd = cwd;
|
||||
this.roots = options.roots ?? defaultPluginRoots(cwd);
|
||||
this.packageProvider = options.packageProvider === false ? undefined : options.packageProvider ?? new DefaultPiPackageProvider(cwd, agentDir);
|
||||
this.agentDir = options.agentDir;
|
||||
this.agentDirProvider = options.agentDirProvider;
|
||||
const packageProvider = options.packageProvider;
|
||||
if (packageProvider === false) {
|
||||
this.packageProviderForAgentDir = undefined;
|
||||
} else if (packageProvider !== undefined) {
|
||||
this.packageProviderForAgentDir = () => packageProvider;
|
||||
} else {
|
||||
this.packageProviderForAgentDir = (agentDir) => new DefaultPiPackageProvider(cwd, agentDir);
|
||||
}
|
||||
this.configProvider = options.configProvider ?? (() => loadPiWebConfig({ cwd }).config);
|
||||
}
|
||||
|
||||
@@ -110,7 +128,8 @@ export class PiWebPluginService {
|
||||
}
|
||||
|
||||
async plugins(): Promise<PiWebPluginsResponse> {
|
||||
const [plugins, config] = await Promise.all([this.discoverPlugins(), Promise.resolve(this.configProvider())]);
|
||||
const config = await this.configProvider();
|
||||
const plugins = await this.discoverPlugins(config);
|
||||
return { plugins: plugins.map((plugin) => this.pluginInfo(plugin, config)) };
|
||||
}
|
||||
|
||||
@@ -143,15 +162,28 @@ export class PiWebPluginService {
|
||||
};
|
||||
}
|
||||
|
||||
private async discoverPlugins(): Promise<PluginRecord[]> {
|
||||
private async discoverPlugins(config?: PiWebConfig): Promise<PluginRecord[]> {
|
||||
const records = new Map<string, PluginRecord>();
|
||||
for (const plugin of await this.discoverLocalPlugins()) addUnique(records, plugin);
|
||||
if (this.packageProvider !== undefined) {
|
||||
for (const plugin of await this.discoverPiPackagePlugins(this.packageProvider)) addUnique(records, plugin);
|
||||
const packageProvider = await this.packageProvider(config);
|
||||
if (packageProvider !== undefined) {
|
||||
for (const plugin of await this.discoverPiPackagePlugins(packageProvider)) addUnique(records, plugin);
|
||||
}
|
||||
return [...records.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
private async packageProvider(config?: PiWebConfig): Promise<PiPackageProvider | undefined> {
|
||||
if (this.packageProviderForAgentDir === undefined) return undefined;
|
||||
return this.packageProviderForAgentDir(await this.currentAgentDir(config));
|
||||
}
|
||||
|
||||
private async currentAgentDir(config?: PiWebConfig): Promise<string> {
|
||||
if (this.agentDirProvider !== undefined) return await this.agentDirProvider();
|
||||
if (this.agentDir !== undefined) return this.agentDir;
|
||||
const currentConfig = config ?? await this.configProvider();
|
||||
return effectiveAgentConfig(process.env, currentConfig, this.cwd).dir;
|
||||
}
|
||||
|
||||
private async discoverLocalPlugins(): Promise<PluginRecord[]> {
|
||||
const plugins: PluginRecord[] = [];
|
||||
for (const root of this.roots) plugins.push(...await discoverLocalRoot(root));
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("PI WEB status", () => {
|
||||
capabilities: [],
|
||||
});
|
||||
|
||||
const status = await getPiWebVersionStatus(daemon, { agentCommand: "omp", agentDir });
|
||||
const status = await getPiWebVersionStatus(daemon, { agentCommand: "alt-agent", agentDir });
|
||||
|
||||
expect(status.components.sessiond.installation).toMatchObject({ kind: "pi-package", source: process.cwd(), scope: "user" });
|
||||
} finally {
|
||||
@@ -85,12 +85,12 @@ describe("PI WEB status", () => {
|
||||
{ kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" },
|
||||
"pi-web restart",
|
||||
{
|
||||
agentCommand: "/tmp/agent's/omp",
|
||||
hasCommand: (command) => Promise.resolve(command === "/tmp/agent's/omp"),
|
||||
agentCommand: "/tmp/agent's/alt-agent",
|
||||
hasCommand: (command) => Promise.resolve(command === "/tmp/agent's/alt-agent"),
|
||||
},
|
||||
);
|
||||
|
||||
expect(updateCommand).toBe("'/tmp/agent'\\''s/omp' update 'npm:@jmfederico/pi-web' && pi-web restart");
|
||||
expect(updateCommand).toBe("'/tmp/agent'\\''s/alt-agent' update 'npm:@jmfederico/pi-web' && pi-web restart");
|
||||
});
|
||||
|
||||
it("suggests native systemd commands for local development services", async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface PiWebStatusCacheOptions {
|
||||
export interface PiWebStatusCache {
|
||||
get(): Promise<PiWebStatusResponse>;
|
||||
refresh(): Promise<PiWebStatusResponse>;
|
||||
invalidate(): void;
|
||||
}
|
||||
|
||||
export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>, options: PiWebStatusCacheOptions = {}): PiWebStatusCache {
|
||||
@@ -42,5 +43,8 @@ export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>,
|
||||
return refresh();
|
||||
},
|
||||
refresh,
|
||||
invalidate(): void {
|
||||
cached = undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ describe("AuthService", () => {
|
||||
const agentDir = await tempAgentDir();
|
||||
const auth = new AuthService({ agentDir });
|
||||
|
||||
auth.saveApiKey("anthropic", "sk-omp");
|
||||
auth.saveApiKey("anthropic", "sk-test");
|
||||
|
||||
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-omp");
|
||||
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-test");
|
||||
auth.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,12 +60,12 @@ describe("SessionDirResolver", () => {
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
|
||||
it("uses OMP sessionDir environment overrides before settings", async () => {
|
||||
const envDir = join(tempDir, "omp-env-sessions");
|
||||
it("uses PI WEB sessionDir environment overrides before settings", async () => {
|
||||
const envDir = join(tempDir, "pi-web-env-sessions");
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8");
|
||||
|
||||
const resolver = new SessionDirResolver({ agentDir, env: { OMP_CODING_AGENT_SESSION_DIR: envDir }, sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"] });
|
||||
const resolver = new SessionDirResolver({ agentDir, env: { PI_WEB_AGENT_SESSION_DIR: envDir } });
|
||||
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
@@ -92,14 +92,13 @@ describe("Pi session manager gateway", () => {
|
||||
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "default-session", cwd }), expect.objectContaining({ id: "env-session", cwd })]));
|
||||
});
|
||||
|
||||
it("includes command-specific env session directories in global listing", async () => {
|
||||
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR"]) {
|
||||
it("includes generic env session directories in global listing", async () => {
|
||||
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"]) {
|
||||
const envSessionDir = join(tempDir, `${envKey.toLowerCase()}-sessions`);
|
||||
await writeSessionFile(envSessionDir, `${envKey.toLowerCase()}-session`, cwd);
|
||||
const gateway = createPiSessionManagerGateway({
|
||||
agentDir,
|
||||
env: { [envKey]: envSessionDir },
|
||||
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
|
||||
});
|
||||
|
||||
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
|
||||
|
||||
@@ -2,12 +2,11 @@ import type { Dirent } from "node:fs";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { getAgentDir, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { agentSessionDirEnvKeys, effectiveAgentConfig } from "../../config.js";
|
||||
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
||||
import type { PiSessionListEntry, PiSessionManager, PiSessionManagerGateway } from "./piSessionService.js";
|
||||
|
||||
export const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
||||
|
||||
type SessionDirSource = "env" | "settings" | "pi-default";
|
||||
|
||||
export interface SessionDirResolution {
|
||||
@@ -28,9 +27,9 @@ export class SessionDirResolver {
|
||||
private readonly sessionDirEnvKeys: readonly string[];
|
||||
|
||||
constructor(options: SessionDirResolverOptions = {}) {
|
||||
this.agentDir = options.agentDir ?? getAgentDir();
|
||||
this.agentDir = options.agentDir ?? effectiveAgentConfig().dir;
|
||||
this.env = options.env ?? process.env;
|
||||
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? [PI_SESSION_DIR_ENV];
|
||||
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? agentSessionDirEnvKeys();
|
||||
}
|
||||
|
||||
defaultSessionsRoot(): string {
|
||||
@@ -131,11 +130,11 @@ function uniqueSessionsByPath(sessions: readonly PiSessionListEntry[]): PiSessio
|
||||
return [...byPath.values()].sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
}
|
||||
|
||||
export function defaultPiSessionsRoot(agentDir = getAgentDir()): string {
|
||||
export function defaultPiSessionsRoot(agentDir = effectiveAgentConfig().dir): string {
|
||||
return join(agentDir, "sessions");
|
||||
}
|
||||
|
||||
export function defaultPiSessionDir(cwd: string, agentDir = getAgentDir()): string {
|
||||
export function defaultPiSessionDir(cwd: string, agentDir = effectiveAgentConfig().dir): string {
|
||||
return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
createAgentSessionServices,
|
||||
createEditToolDefinition,
|
||||
defineTool,
|
||||
getAgentDir,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
@@ -25,6 +24,7 @@ import { createModelRegistryForAgentDir, type AuthChange } from "./authService.j
|
||||
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
|
||||
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
||||
import { effectiveAgentConfig } from "../../config.js";
|
||||
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
||||
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
||||
import type { SavedPromptAttachment } from "../../shared/apiTypes.js";
|
||||
@@ -338,7 +338,7 @@ export class PiSessionService implements SessionRouteService {
|
||||
|
||||
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) {
|
||||
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
||||
this.agentDir = deps.agentDir ?? getAgentDir();
|
||||
this.agentDir = deps.agentDir ?? effectiveAgentConfig().dir;
|
||||
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
|
||||
this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir);
|
||||
this.spawnTargets = deps.spawnTargets;
|
||||
|
||||
Reference in New Issue
Block a user