Files
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
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
2026-03-03 11:22:53 +01:00

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);
}