|
|
@@ -1,3 +1,4 @@
|
|
|
+import * as crypto from 'node:crypto';
|
|
|
import * as fs from 'node:fs';
|
|
|
import * as os from 'node:os';
|
|
|
import * as path from 'node:path';
|
|
|
@@ -18,8 +19,24 @@ function dataDir(): string {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-export function getTuiStatePath(): string {
|
|
|
- return path.join(dataDir(), 'opencode', 'storage', STATE_DIR, STATE_FILE);
|
|
|
+// ponytail: per-project scope prevents /model overrides from leaking across projects
|
|
|
+function projectScope(projectDir: string): string {
|
|
|
+ return crypto
|
|
|
+ .createHash('sha256')
|
|
|
+ .update(path.resolve(projectDir))
|
|
|
+ .digest('hex')
|
|
|
+ .slice(0, 12);
|
|
|
+}
|
|
|
+
|
|
|
+export function getTuiStatePath(projectDir: string): string {
|
|
|
+ return path.join(
|
|
|
+ dataDir(),
|
|
|
+ 'opencode',
|
|
|
+ 'storage',
|
|
|
+ STATE_DIR,
|
|
|
+ projectScope(projectDir),
|
|
|
+ STATE_FILE,
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
function emptySnapshot(): TuiSnapshot {
|
|
|
@@ -44,25 +61,29 @@ function parseSnapshot(value: string): TuiSnapshot {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-export function readTuiSnapshot(): TuiSnapshot {
|
|
|
+export function readTuiSnapshot(projectDir: string): TuiSnapshot {
|
|
|
try {
|
|
|
- return parseSnapshot(fs.readFileSync(getTuiStatePath(), 'utf8'));
|
|
|
+ return parseSnapshot(fs.readFileSync(getTuiStatePath(projectDir), 'utf8'));
|
|
|
} catch {
|
|
|
return emptySnapshot();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export async function readTuiSnapshotAsync(): Promise<TuiSnapshot> {
|
|
|
+export async function readTuiSnapshotAsync(
|
|
|
+ projectDir: string,
|
|
|
+): Promise<TuiSnapshot> {
|
|
|
try {
|
|
|
- return parseSnapshot(await fs.promises.readFile(getTuiStatePath(), 'utf8'));
|
|
|
+ return parseSnapshot(
|
|
|
+ await fs.promises.readFile(getTuiStatePath(projectDir), 'utf8'),
|
|
|
+ );
|
|
|
} catch {
|
|
|
return emptySnapshot();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function writeTuiSnapshot(snapshot: TuiSnapshot): void {
|
|
|
+function writeTuiSnapshot(snapshot: TuiSnapshot, projectDir: string): void {
|
|
|
try {
|
|
|
- const filePath = getTuiStatePath();
|
|
|
+ const filePath = getTuiStatePath(projectDir);
|
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
|
fs.writeFileSync(filePath, `${JSON.stringify(snapshot)}\n`);
|
|
|
} catch {
|
|
|
@@ -70,29 +91,38 @@ function writeTuiSnapshot(snapshot: TuiSnapshot): void {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function updateSnapshot(mutator: (snapshot: TuiSnapshot) => void): void {
|
|
|
- const snapshot = readTuiSnapshot();
|
|
|
+function updateSnapshot(
|
|
|
+ projectDir: string,
|
|
|
+ mutator: (snapshot: TuiSnapshot) => void,
|
|
|
+): void {
|
|
|
+ const snapshot = readTuiSnapshot(projectDir);
|
|
|
mutator(snapshot);
|
|
|
snapshot.updatedAt = Date.now();
|
|
|
- writeTuiSnapshot(snapshot);
|
|
|
+ writeTuiSnapshot(snapshot, projectDir);
|
|
|
}
|
|
|
|
|
|
-export function recordTuiAgentModels(input: {
|
|
|
- agentModels: Record<string, string>;
|
|
|
- agentVariants?: Record<string, string>;
|
|
|
-}): void {
|
|
|
- updateSnapshot((snapshot) => {
|
|
|
+export function recordTuiAgentModels(
|
|
|
+ input: {
|
|
|
+ agentModels: Record<string, string>;
|
|
|
+ agentVariants?: Record<string, string>;
|
|
|
+ },
|
|
|
+ projectDir: string,
|
|
|
+): void {
|
|
|
+ updateSnapshot(projectDir, (snapshot) => {
|
|
|
snapshot.agentModels = { ...input.agentModels };
|
|
|
snapshot.agentVariants = { ...(input.agentVariants ?? {}) };
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-export function recordTuiAgentModel(input: {
|
|
|
- agentName: string;
|
|
|
- model: string;
|
|
|
- variant?: string | null;
|
|
|
-}): void {
|
|
|
- updateSnapshot((snapshot) => {
|
|
|
+export function recordTuiAgentModel(
|
|
|
+ input: {
|
|
|
+ agentName: string;
|
|
|
+ model: string;
|
|
|
+ variant?: string | null;
|
|
|
+ },
|
|
|
+ projectDir: string,
|
|
|
+): void {
|
|
|
+ updateSnapshot(projectDir, (snapshot) => {
|
|
|
snapshot.agentModels[input.agentName] = input.model;
|
|
|
if (input.variant !== undefined) {
|
|
|
if (input.variant === null) {
|