feat(backend): support container runtime paths
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import type { AppConfig } from "../config.js";
|
||||
import { RpcClient } from "../rpc/rpc-client.js";
|
||||
|
||||
@@ -11,7 +10,11 @@ export interface PiModel {
|
||||
}
|
||||
|
||||
interface Opts {
|
||||
spawnFn?: () => ChildProcessWithoutNullStreams;
|
||||
spawnFn?: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env: NodeJS.ProcessEnv },
|
||||
) => ChildProcessWithoutNullStreams;
|
||||
ttlMs?: number;
|
||||
nowMs?: () => number;
|
||||
}
|
||||
@@ -24,22 +27,16 @@ interface Opts {
|
||||
export function createPiModelLister(cfg: AppConfig, opts: Opts = {}): () => Promise<PiModel[]> {
|
||||
const ttlMs = opts.ttlMs ?? 60_000;
|
||||
const now = opts.nowMs ?? (() => Date.now());
|
||||
const spawnFn =
|
||||
opts.spawnFn ??
|
||||
(() => {
|
||||
const harnessVenvBin = join(cfg.harnessDir, ".venv", "bin");
|
||||
return nodeSpawn(cfg.piBin, ["--mode", "rpc"], {
|
||||
cwd: cfg.harnessDir,
|
||||
env: { ...process.env, PATH: `${harnessVenvBin}:${process.env.PATH ?? ""}` },
|
||||
}) as ChildProcessWithoutNullStreams;
|
||||
});
|
||||
const spawnFn = opts.spawnFn ?? nodeSpawn;
|
||||
|
||||
let cache: { at: number; models: PiModel[] } | null = null;
|
||||
|
||||
return async function listModels(): Promise<PiModel[]> {
|
||||
if (cache && now() - cache.at < ttlMs) return cache.models;
|
||||
|
||||
const child = spawnFn();
|
||||
const env: NodeJS.ProcessEnv = { ...process.env };
|
||||
if (cfg.dataRoot !== undefined) env.THT_DATA_ROOT = cfg.dataRoot;
|
||||
const child = spawnFn(cfg.piBin, ["--mode", "rpc"], { cwd: cfg.harnessDir, env });
|
||||
child.stderr.resume();
|
||||
const rpc = new RpcClient(child);
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import type { AppConfig } from "../config.js";
|
||||
import { RpcClient } from "../rpc/rpc-client.js";
|
||||
import { SessionBridge } from "../bridge/session-bridge.js";
|
||||
@@ -11,8 +10,12 @@ export interface SessionRuntime {
|
||||
child: ChildProcessWithoutNullStreams;
|
||||
}
|
||||
|
||||
/** Injected test double signature: produce a child process, no args needed. */
|
||||
type SpawnFn = () => ChildProcessWithoutNullStreams;
|
||||
/** Injectable child-process boundary; callbacks may ignore arguments in simpler tests. */
|
||||
type SpawnFn = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env: NodeJS.ProcessEnv },
|
||||
) => ChildProcessWithoutNullStreams;
|
||||
|
||||
export class PiProcessManager {
|
||||
private runtimes = new Map<string, SessionRuntime>();
|
||||
@@ -20,30 +23,29 @@ export class PiProcessManager {
|
||||
|
||||
constructor(private cfg: AppConfig, opts?: { spawnFn?: SpawnFn }) {
|
||||
if (opts?.spawnFn) {
|
||||
this.spawnFn = () => opts.spawnFn!();
|
||||
this.spawnFn = (sessionId: string, author: string) => this.spawnPi(opts.spawnFn!, sessionId, author);
|
||||
} else {
|
||||
this.spawnFn = (sessionId: string, author: string) => {
|
||||
const harnessVenvBin = join(cfg.harnessDir, ".venv", "bin");
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
THT_SESSION: sessionId,
|
||||
THT_AUTHOR: author,
|
||||
PATH: `${harnessVenvBin}:${process.env.PATH ?? ""}`,
|
||||
};
|
||||
// pi 0.73 (the @mariozechner rebrand) removed the `--approve` flag: rpc mode is
|
||||
// headless and runs tools without an approval gate, so passing it makes pi exit
|
||||
// with "Unknown option: --approve". Args are intentionally just `--mode rpc`.
|
||||
const child = nodeSpawn(cfg.piBin, ["--mode", "rpc"], {
|
||||
cwd: cfg.harnessDir,
|
||||
env,
|
||||
});
|
||||
// Drain stderr so the child's stderr buffer never blocks the process.
|
||||
child.stderr.resume();
|
||||
return child;
|
||||
};
|
||||
this.spawnFn = (sessionId: string, author: string) => this.spawnPi(nodeSpawn, sessionId, author);
|
||||
}
|
||||
}
|
||||
|
||||
private spawnPi(spawnFn: SpawnFn, sessionId: string, author: string): ChildProcessWithoutNullStreams {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
THT_SESSION: sessionId,
|
||||
THT_AUTHOR: author,
|
||||
};
|
||||
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
|
||||
// pi 0.73 removed `--approve`: rpc mode is headless and its argv is intentionally minimal.
|
||||
const child = spawnFn(this.cfg.piBin, ["--mode", "rpc"], {
|
||||
cwd: this.cfg.harnessDir,
|
||||
env,
|
||||
});
|
||||
// Drain stderr so the child's stderr buffer never blocks the process.
|
||||
child.stderr.resume?.();
|
||||
return child;
|
||||
}
|
||||
|
||||
count(): number { return this.runtimes.size; }
|
||||
|
||||
get(id: string): SessionRuntime | undefined { return this.runtimes.get(id); }
|
||||
|
||||
Reference in New Issue
Block a user