Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
/**
|
|
* Cassette Key Generation
|
|
*
|
|
* Builds stable SHA256-based identifiers for cassettes.
|
|
* Two spans are separate concerns:
|
|
* - hashWorktreeFiles: fingerprints the worktree state at spawn time (for execute mode drift)
|
|
* - buildCassetteKey: hashes all key components into a 32-char hex filename
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import type { CassetteKey } from './types.js';
|
|
|
|
/**
|
|
* Recursively hash all non-hidden files in a directory.
|
|
*
|
|
* Hidden entries (starting with '.') are skipped — this excludes .git, .cw, etc.
|
|
* Entries are processed in sorted order for determinism across platforms.
|
|
*
|
|
* Returns the first 16 hex chars of the SHA256, or 'empty' if the directory
|
|
* is absent or contains no readable files.
|
|
*/
|
|
export function hashWorktreeFiles(dir: string): string {
|
|
const hash = createHash('sha256');
|
|
let hasContent = false;
|
|
|
|
function walkDir(currentDir: string): void {
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
if (entry.name.startsWith('.')) continue;
|
|
|
|
const fullPath = join(currentDir, entry.name);
|
|
const relPath = fullPath.slice(dir.length);
|
|
|
|
if (entry.isDirectory()) {
|
|
hash.update(`d:${relPath}\n`);
|
|
walkDir(fullPath);
|
|
} else if (entry.isFile()) {
|
|
try {
|
|
const content = readFileSync(fullPath);
|
|
hash.update(`f:${relPath}:${content.length}\n`);
|
|
hash.update(content);
|
|
hasContent = true;
|
|
} catch {
|
|
// skip unreadable files
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walkDir(dir);
|
|
return hasContent ? hash.digest('hex').slice(0, 16) : 'empty';
|
|
}
|
|
|
|
/**
|
|
* Compute a stable 32-char hex identifier for a cassette key.
|
|
*
|
|
* modelArgs are sorted before hashing so insertion order differences
|
|
* between providers don't produce different cassettes.
|
|
*/
|
|
export function buildCassetteKey(key: CassetteKey): string {
|
|
const canonical = JSON.stringify({
|
|
normalizedPrompt: key.normalizedPrompt,
|
|
providerName: key.providerName,
|
|
modelArgs: [...key.modelArgs].sort(),
|
|
worktreeHash: key.worktreeHash,
|
|
});
|
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 32);
|
|
}
|