diff options
Diffstat (limited to 'packages/system-prompt/src/resolver.ts')
| -rw-r--r-- | packages/system-prompt/src/resolver.ts | 236 |
1 files changed, 118 insertions, 118 deletions
diff --git a/packages/system-prompt/src/resolver.ts b/packages/system-prompt/src/resolver.ts index e864554..8b397d2 100644 --- a/packages/system-prompt/src/resolver.ts +++ b/packages/system-prompt/src/resolver.ts @@ -17,9 +17,9 @@ import { isAbsolute, resolve as resolvePath } from "node:path"; /** Result of a spawned command (used for git). */ export interface GitSpawnResult { - readonly stdout: string; - readonly stderr: string; - readonly exitCode: number | null; + readonly stdout: string; + readonly stderr: string; + readonly exitCode: number | null; } /** @@ -27,73 +27,73 @@ export interface GitSpawnResult { * resolver (e.g. git not installed, bad cwd). */ export type GitSpawn = ( - command: readonly string[], - opts: { readonly cwd: string }, + command: readonly string[], + opts: { readonly cwd: string }, ) => Promise<GitSpawnResult>; /** Filesystem adapter — the read effects the resolver needs. */ export interface ResolverFs { - readonly readText: (path: string) => Promise<string>; - readonly exists: (path: string) => Promise<boolean>; + readonly readText: (path: string) => Promise<string>; + readonly exists: (path: string) => Promise<boolean>; } /** Injected effects + optional overridable clocks for deterministic tests. */ export interface ResolverAdapters { - /** Run a command (git) and capture stdout. */ - readonly spawn: GitSpawn; - /** File read effects. */ - readonly fs: ResolverFs; - /** Override the current time (defaults to `new Date()`). */ - readonly now?: () => Date; - /** - * Override `process.platform` (defaults to the real platform). Async so a - * remote adapter can run `uname -s` over SSH. - */ - readonly platform?: () => Promise<string>; - /** - * Override the hostname (defaults to `os.hostname()`). Async so a remote - * adapter can run `hostname` over SSH. - */ - readonly hostname?: () => Promise<string>; + /** Run a command (git) and capture stdout. */ + readonly spawn: GitSpawn; + /** File read effects. */ + readonly fs: ResolverFs; + /** Override the current time (defaults to `new Date()`). */ + readonly now?: () => Date; + /** + * Override `process.platform` (defaults to the real platform). Async so a + * remote adapter can run `uname -s` over SSH. + */ + readonly platform?: () => Promise<string>; + /** + * Override the hostname (defaults to `os.hostname()`). Async so a remote + * adapter can run `hostname` over SSH. + */ + readonly hostname?: () => Promise<string>; } /** Per-construction context forwarded by the session-orchestrator. */ export interface ResolverContext { - readonly model?: string; - readonly conversationId?: string; - readonly workspaceId?: string; + readonly model?: string; + readonly conversationId?: string; + readonly workspaceId?: string; } export interface ResolveOptions { - readonly context?: ResolverContext; - /** Variable keys referenced by the template (drives dynamic `file:` reads). */ - readonly referencedKeys?: readonly string[]; + readonly context?: ResolverContext; + /** Variable keys referenced by the template (drives dynamic `file:` reads). */ + readonly referencedKeys?: readonly string[]; } /** Run a git subcommand in `cwd`; return raw stdout on success, else null. */ async function runGit( - args: readonly string[], - cwd: string, - spawn: GitSpawn, + args: readonly string[], + cwd: string, + spawn: GitSpawn, ): Promise<string | null> { - try { - const res = await spawn(["git", ...args], { cwd }); - if (res.exitCode !== 0) return null; - return res.stdout; - } catch { - return null; - } + try { + const res = await spawn(["git", ...args], { cwd }); + if (res.exitCode !== 0) return null; + return res.stdout; + } catch { + return null; + } } /** Read a file (relative to cwd, or absolute). Missing/error → null. */ async function readFile(filePath: string, cwd: string, fs: ResolverFs): Promise<string | null> { - const abs = isAbsolute(filePath) ? filePath : resolvePath(cwd, filePath); - try { - if (!(await fs.exists(abs))) return null; - return await fs.readText(abs); - } catch { - return null; - } + const abs = isAbsolute(filePath) ? filePath : resolvePath(cwd, filePath); + try { + if (!(await fs.exists(abs))) return null; + return await fs.readText(abs); + } catch { + return null; + } } /** @@ -109,38 +109,38 @@ async function readFile(filePath: string, cwd: string, fs: ResolverFs): Promise< * to the base platform string). The `platform` override is honored for tests. */ async function resolveOs(platform: string, fs: ResolverFs): Promise<string> { - if (platform !== "linux") return platform; - - let distro: string | null = null; - const osRelease = await readFile("/etc/os-release", "/", fs); - if (osRelease !== null) { - const pretty = osRelease.match(/^PRETTY_NAME="(.+)"/m); - if (pretty?.[1] !== undefined) { - distro = pretty[1]; - } else { - const name = osRelease.match(/^NAME="(.+)"/m); - const version = osRelease.match(/^VERSION_ID="(.+)"/m); - if (name?.[1] !== undefined) { - distro = version?.[1] !== undefined ? `${name[1]} ${version[1]}` : name[1]; - } - } - } - - let isWsl = false; - const wslInterop = await readFile("/proc/sys/fs/binfmt_misc/WSLInterop", "/", fs); - if (wslInterop !== null) { - isWsl = true; - } else { - const procVersion = await readFile("/proc/version", "/", fs); - if (procVersion !== null && /microsoft/i.test(procVersion)) { - isWsl = true; - } - } - - if (distro !== null) { - return isWsl ? `${distro} (WSL)` : distro; - } - return isWsl ? "Linux (WSL)" : "linux"; + if (platform !== "linux") return platform; + + let distro: string | null = null; + const osRelease = await readFile("/etc/os-release", "/", fs); + if (osRelease !== null) { + const pretty = osRelease.match(/^PRETTY_NAME="(.+)"/m); + if (pretty?.[1] !== undefined) { + distro = pretty[1]; + } else { + const name = osRelease.match(/^NAME="(.+)"/m); + const version = osRelease.match(/^VERSION_ID="(.+)"/m); + if (name?.[1] !== undefined) { + distro = version?.[1] !== undefined ? `${name[1]} ${version[1]}` : name[1]; + } + } + } + + let isWsl = false; + const wslInterop = await readFile("/proc/sys/fs/binfmt_misc/WSLInterop", "/", fs); + if (wslInterop !== null) { + isWsl = true; + } else { + const procVersion = await readFile("/proc/version", "/", fs); + if (procVersion !== null && /microsoft/i.test(procVersion)) { + isWsl = true; + } + } + + if (distro !== null) { + return isWsl ? `${distro} (WSL)` : distro; + } + return isWsl ? "Linux (WSL)" : "linux"; } /** @@ -151,45 +151,45 @@ async function resolveOs(platform: string, fs: ResolverFs): Promise<string> { * the template). Unknown types are intentionally left out of the map. */ export async function resolveVariables( - cwd: string, - adapters: ResolverAdapters, - options?: ResolveOptions, + cwd: string, + adapters: ResolverAdapters, + options?: ResolveOptions, ): Promise<Map<string, string | null>> { - const ctx = options?.context; - const referencedKeys = options?.referencedKeys; - const now = adapters.now?.() ?? new Date(); - const vars = new Map<string, string | null>(); - - // ── system:* ──────────────────────────────────────────────────────────── - vars.set("system:time", now.toISOString()); - vars.set("system:date", now.toISOString().slice(0, 10)); - const platform = (await adapters.platform?.()) ?? process.platform; - vars.set("system:os", await resolveOs(platform, adapters.fs)); - vars.set("system:hostname", (await adapters.hostname?.()) ?? osHostname()); - - // ── prompt:* ──────────────────────────────────────────────────────────── - vars.set("prompt:cwd", cwd); - vars.set("prompt:model", ctx?.model ?? null); - vars.set("prompt:conversation_id", ctx?.conversationId ?? null); - vars.set("prompt:workspace_id", ctx?.workspaceId ?? null); - - // ── git:* ──────────────────────────────────────────────────────────────── - // branch is a single value — trim fully; status keeps its leading status - // indicators, dropping only the trailing newline (trimEnd). - const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd, adapters.spawn); - vars.set("git:branch", branch === null ? null : branch.trim()); - const status = await runGit(["status", "--short"], cwd, adapters.spawn); - vars.set("git:status", status === null ? null : status.trimEnd()); - - // ── file:<path> (dynamic — only those referenced by the template) ──────── - if (referencedKeys !== undefined) { - for (const key of referencedKeys) { - if (key.startsWith("file:")) { - const filePath = key.slice("file:".length); - vars.set(key, await readFile(filePath, cwd, adapters.fs)); - } - } - } - - return vars; + const ctx = options?.context; + const referencedKeys = options?.referencedKeys; + const now = adapters.now?.() ?? new Date(); + const vars = new Map<string, string | null>(); + + // ── system:* ──────────────────────────────────────────────────────────── + vars.set("system:time", now.toISOString()); + vars.set("system:date", now.toISOString().slice(0, 10)); + const platform = (await adapters.platform?.()) ?? process.platform; + vars.set("system:os", await resolveOs(platform, adapters.fs)); + vars.set("system:hostname", (await adapters.hostname?.()) ?? osHostname()); + + // ── prompt:* ──────────────────────────────────────────────────────────── + vars.set("prompt:cwd", cwd); + vars.set("prompt:model", ctx?.model ?? null); + vars.set("prompt:conversation_id", ctx?.conversationId ?? null); + vars.set("prompt:workspace_id", ctx?.workspaceId ?? null); + + // ── git:* ──────────────────────────────────────────────────────────────── + // branch is a single value — trim fully; status keeps its leading status + // indicators, dropping only the trailing newline (trimEnd). + const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd, adapters.spawn); + vars.set("git:branch", branch === null ? null : branch.trim()); + const status = await runGit(["status", "--short"], cwd, adapters.spawn); + vars.set("git:status", status === null ? null : status.trimEnd()); + + // ── file:<path> (dynamic — only those referenced by the template) ──────── + if (referencedKeys !== undefined) { + for (const key of referencedKeys) { + if (key.startsWith("file:")) { + const filePath = key.slice("file:".length); + vars.set(key, await readFile(filePath, cwd, adapters.fs)); + } + } + } + + return vars; } |
