@vibe-interviewing/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/chunk-CI3BD2WQ.js +141 -0
- package/dist/chunk-CI3BD2WQ.js.map +1 -0
- package/dist/index.d.ts +19 -364
- package/dist/index.js +172 -105
- package/dist/index.js.map +1 -1
- package/dist/network/index.d.ts +81 -0
- package/dist/network/index.js +309 -0
- package/dist/network/index.js.map +1 -0
- package/dist/session-code-CfhXelpW.d.ts +519 -0
- package/package.json +6 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scenario/types.ts","../src/scenario/loader.ts","../src/errors.ts","../src/scenario/validator.ts","../src/scenario/registry.ts","../src/scenario/importer.ts","../src/launcher/claude-code.ts","../src/session/recorder.ts","../src/launcher/detector.ts","../src/session/manager.ts","../src/session/store.ts","../src/session/types.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst AIRulesSchema = z.object({\n /** Role description for the AI assistant */\n role: z.string(),\n /** Behavioral rules (e.g., \"don't reveal the answer\") */\n rules: z.array(z.string()),\n /** Knowledge about the bug/solution (hidden from candidate) */\n knowledge: z.string(),\n})\n\nconst EvaluationSchema = z.object({\n /** Evaluation criteria for the interviewer */\n criteria: z.array(z.string()),\n /** Description of the expected fix */\n expected_fix: z.string().optional(),\n})\n\n/** A file modification to inject the bug */\nconst PatchSchema = z.object({\n /** Path to the file relative to repo root */\n file: z.string(),\n /** The original text to find */\n find: z.string(),\n /** The replacement text (with the bug) */\n replace: z.string(),\n})\n\n/** Full scenario configuration schema */\nexport const ScenarioConfigSchema = z.object({\n /** Scenario display name */\n name: z.string(),\n /** One-line description */\n description: z.string(),\n /** Difficulty level */\n difficulty: z.enum(['easy', 'medium', 'hard']),\n /** Estimated time (e.g., \"30-45m\") */\n estimated_time: z.string(),\n /** Searchable tags */\n tags: z.array(z.string()).default([]),\n\n /** GitHub repo URL or owner/repo shorthand */\n repo: z.string(),\n /** Commit SHA to pin the clone to (ensures reproducibility) */\n commit: z.string(),\n /** Shell commands to run after cloning (e.g., [\"npm install\"]) */\n setup: z.array(z.string()).default([]),\n\n /** Find-and-replace patches to inject the bug after cloning */\n patch: z.array(PatchSchema).default([]),\n\n /** Briefing shown to the candidate (written like a team lead message) */\n briefing: z.string(),\n /** AI behavioral rules (injected via system prompt, hidden from candidate) */\n ai_rules: AIRulesSchema,\n /** Interviewer reference — what the fix looks like */\n solution: z.string(),\n\n /** Evaluation rubric */\n evaluation: EvaluationSchema.optional(),\n /** License of the original project */\n license: z.string().optional(),\n})\n\nexport type ScenarioConfig = z.infer<typeof ScenarioConfigSchema>\nexport type AIRules = z.infer<typeof AIRulesSchema>\nexport type Evaluation = z.infer<typeof EvaluationSchema>\n\n/** Metadata about a discovered scenario */\nexport interface ScenarioInfo {\n /** Scenario name */\n name: string\n /** Parsed config */\n config: ScenarioConfig\n /** Whether this is a built-in scenario */\n builtIn: boolean\n}\n","import { readFile } from 'node:fs/promises'\nimport { parse as parseYaml } from 'yaml'\nimport { ScenarioNotFoundError, ScenarioValidationError } from '../errors.js'\nimport { ScenarioConfigSchema, type ScenarioConfig } from './types.js'\nimport { existsSync } from 'node:fs'\n\n/**\n * Load and parse a scenario config from a YAML file.\n *\n * @param configPath - Absolute path to the scenario.yaml file\n * @returns The parsed and validated scenario config\n */\nexport async function loadScenarioConfig(configPath: string): Promise<ScenarioConfig> {\n if (!existsSync(configPath)) {\n throw new ScenarioNotFoundError(configPath)\n }\n\n const raw = await readFile(configPath, 'utf-8')\n const parsed: unknown = parseYaml(raw)\n\n const result = ScenarioConfigSchema.safeParse(parsed)\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`)\n throw new ScenarioValidationError('validation failed', issues)\n }\n\n return result.data\n}\n\n/**\n * Generate a system prompt string from a scenario's ai_rules.\n *\n * This prompt is injected into the AI tool via --append-system-prompt\n * and is hidden from the candidate.\n *\n * @param config - The scenario config containing ai_rules\n * @returns The formatted system prompt\n */\nexport function generateSystemPrompt(config: ScenarioConfig): string {\n const lines: string[] = []\n\n lines.push(`# Interview Scenario: ${config.name}`)\n lines.push('')\n lines.push('## Your Role')\n lines.push(config.ai_rules.role.trim())\n lines.push('')\n lines.push('## Rules')\n for (const rule of config.ai_rules.rules) {\n lines.push(`- ${rule}`)\n }\n lines.push('')\n lines.push('## Knowledge (DO NOT share directly with the candidate)')\n lines.push(config.ai_rules.knowledge.trim())\n\n return lines.join('\\n')\n}\n","/** Base error class for all vibe-interviewing errors */\nexport class VibeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly hint?: string,\n ) {\n super(message)\n this.name = 'VibeError'\n }\n}\n\nexport class ScenarioNotFoundError extends VibeError {\n constructor(name: string) {\n super(\n `Scenario not found: ${name}`,\n 'SCENARIO_NOT_FOUND',\n 'Run `vibe-interviewing list` to see available scenarios',\n )\n }\n}\n\nexport class ScenarioValidationError extends VibeError {\n constructor(\n message: string,\n public readonly issues: string[],\n ) {\n super(`Invalid scenario config: ${message}`, 'SCENARIO_VALIDATION_ERROR', issues.join('\\n'))\n }\n}\n\nexport class AIToolNotFoundError extends VibeError {\n static readonly installHints: Record<string, string> = {\n 'claude-code': 'Install Claude Code: npm install -g @anthropic-ai/claude-code',\n }\n\n constructor(tool: string) {\n super(\n `${tool} is not installed`,\n 'AI_TOOL_NOT_FOUND',\n AIToolNotFoundError.installHints[tool] ?? `Install ${tool} and try again`,\n )\n }\n}\n\nexport class SessionNotFoundError extends VibeError {\n constructor(id: string) {\n super(\n `Session not found: ${id}`,\n 'SESSION_NOT_FOUND',\n 'Run `vibe-interviewing list` to see active sessions',\n )\n }\n}\n\nexport class GitCloneError extends VibeError {\n constructor(repo: string, reason?: string) {\n super(\n `Failed to clone repository: ${repo}${reason ? ` — ${reason}` : ''}`,\n 'GIT_CLONE_FAILED',\n 'Check the repo URL and your network connection',\n )\n }\n}\n\nexport class SetupError extends VibeError {\n constructor(command: string, reason?: string) {\n super(\n `Setup command failed: ${command}${reason ? ` — ${reason}` : ''}`,\n 'SETUP_FAILED',\n 'Check the scenario setup commands and try again',\n )\n }\n}\n","import { ScenarioValidationError } from '../errors.js'\nimport type { ScenarioConfig } from './types.js'\n\n/** Result of validating a scenario configuration */\nexport interface ValidationResult {\n /** Whether the scenario is valid (no errors) */\n valid: boolean\n /** Non-fatal issues that should be addressed */\n warnings: string[]\n /** Fatal issues that prevent the scenario from running */\n errors: string[]\n}\n\n/**\n * Validate a scenario config for completeness and correctness.\n *\n * @param config - The scenario config to validate\n * @returns Validation result with errors and warnings\n */\nexport async function validateScenario(config: ScenarioConfig): Promise<ValidationResult> {\n const warnings: string[] = []\n const errors: string[] = []\n\n // Check briefing is not empty\n if (!config.briefing.trim()) {\n errors.push('Briefing cannot be empty')\n }\n\n // Check AI rules\n if (!config.ai_rules.role.trim()) {\n errors.push('ai_rules.role cannot be empty')\n }\n\n // Check repo is not empty\n if (!config.repo.trim()) {\n errors.push('repo cannot be empty')\n }\n\n // Check commit is pinned\n if (!config.commit.trim()) {\n errors.push('commit cannot be empty — pin to a specific commit SHA for reproducibility')\n }\n\n // Warnings for non-critical missing content\n if (config.ai_rules.rules.length === 0) {\n warnings.push('ai_rules.rules is empty — the AI will have no behavioral constraints')\n }\n\n if (!config.solution.trim()) {\n warnings.push('solution is empty — interviewers will have no solution reference')\n }\n\n if (!config.evaluation) {\n warnings.push('No evaluation criteria defined')\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n }\n}\n\n/**\n * Validate a scenario config and throw if invalid.\n *\n * @param config - The scenario config to validate\n * @returns Validation result (only returned if valid)\n * @throws ScenarioValidationError if the config has errors\n */\nexport async function validateScenarioOrThrow(config: ScenarioConfig): Promise<ValidationResult> {\n const result = await validateScenario(config)\n if (!result.valid) {\n throw new ScenarioValidationError('scenario validation failed', result.errors)\n }\n return result\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\nimport { loadScenarioConfig } from './loader.js'\nimport type { ScenarioInfo } from './types.js'\n\n/** Shape of a single entry in registry.yaml */\ninterface RegistryEntry {\n name: string\n repo: string\n commit: string\n description: string\n difficulty: 'easy' | 'medium' | 'hard'\n estimated_time: string\n}\n\n/** Shape of the registry.yaml file */\ninterface RegistryFile {\n scenarios: RegistryEntry[]\n}\n\n/**\n * Get the path to the scenarios package directory.\n *\n * Resolves via the @vibe-interviewing/scenarios package, which works both\n * in the monorepo (workspace link) and when installed from npm.\n */\nasync function getScenariosPackagePath(): Promise<string> {\n try {\n const { getScenariosDir } = await import('@vibe-interviewing/scenarios')\n return getScenariosDir()\n } catch {\n // Fallback: try relative to process.cwd() (monorepo root)\n const fromCwd = join(process.cwd(), 'packages', 'scenarios')\n if (existsSync(fromCwd)) {\n return fromCwd\n }\n throw new Error('Could not locate @vibe-interviewing/scenarios package')\n }\n}\n\n/**\n * Discover built-in scenarios from the registry.yaml file in the scenarios package.\n *\n * Each entry in the registry points to a scenario directory containing a full\n * scenario.yaml. The registry provides quick lookup metadata, while the full\n * config is loaded from the scenario's own config file.\n *\n * @returns Array of discovered scenario info objects\n */\nexport async function discoverBuiltInScenarios(): Promise<ScenarioInfo[]> {\n const scenariosPath = await getScenariosPackagePath()\n const registryPath = join(scenariosPath, 'registry.yaml')\n\n if (!existsSync(registryPath)) {\n return []\n }\n\n const raw = await readFile(registryPath, 'utf-8')\n const registry = parseYaml(raw) as RegistryFile\n\n if (!registry.scenarios || !Array.isArray(registry.scenarios)) {\n return []\n }\n\n const scenarios: ScenarioInfo[] = []\n\n for (const entry of registry.scenarios) {\n // Load the full config from the scenario's own directory\n const scenarioConfigPath = join(scenariosPath, entry.name, 'scenario.yaml')\n\n if (existsSync(scenarioConfigPath)) {\n const config = await loadScenarioConfig(scenarioConfigPath)\n scenarios.push({\n name: entry.name,\n config,\n builtIn: true,\n })\n }\n }\n\n return scenarios\n}\n\n/**\n * Discover all available scenarios.\n *\n * Currently returns only built-in scenarios from the registry.\n * Local scenario support can be added later.\n *\n * @returns Array of all discovered scenario info objects\n */\nexport async function discoverAllScenarios(): Promise<ScenarioInfo[]> {\n return discoverBuiltInScenarios()\n}\n","import { mkdtemp } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { GitCloneError } from '../errors.js'\n\n/**\n * Import a repository by cloning it to a local directory.\n *\n * For commit SHAs, uses a shallow fetch to avoid downloading full history.\n *\n * @param repoUrl - GitHub URL, SSH URL, or owner/repo shorthand\n * @param targetPath - Directory to clone into (defaults to a temp directory)\n * @param ref - Optional branch, tag, or commit to checkout\n * @returns The path to the cloned directory\n */\nexport async function importRepo(repoUrl: string, targetPath?: string, ref?: string): Promise<string> {\n const { simpleGit } = await import('simple-git')\n\n // Normalize shorthand (owner/repo -> https://github.com/owner/repo)\n const url = normalizeRepoUrl(repoUrl)\n\n // Clone to target path or temp directory\n const dest = targetPath ?? (await mkdtemp(join(tmpdir(), 'vibe-import-')))\n\n const git = simpleGit()\n\n try {\n if (ref && /^[0-9a-f]{7,40}$/i.test(ref)) {\n // Commit SHA — init + shallow fetch to avoid downloading full history\n await git.init([dest])\n const repoGit = simpleGit(dest)\n await repoGit.addRemote('origin', url)\n await repoGit.fetch(['origin', ref, '--depth', '1'])\n await repoGit.checkout(['FETCH_HEAD'])\n } else if (ref) {\n // Branch or tag — shallow clone\n await git.clone(url, dest, ['--depth', '1', '--branch', ref])\n } else {\n await git.clone(url, dest, ['--depth', '1'])\n }\n } catch (err) {\n throw new GitCloneError(url, err instanceof Error ? err.message : String(err))\n }\n\n return dest\n}\n\n/** Normalize various repo URL formats to full HTTPS URLs */\nfunction normalizeRepoUrl(input: string): string {\n // Already a full URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return input\n }\n\n // Git SSH format\n if (input.startsWith('git@')) {\n return input.replace('git@github.com:', 'https://github.com/').replace(/\\.git$/, '')\n }\n\n // Shorthand: owner/repo\n if (/^[a-zA-Z0-9_-]+\\/[a-zA-Z0-9._-]+$/.test(input)) {\n return `https://github.com/${input}`\n }\n\n return input\n}\n","import { spawn } from 'node:child_process'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { readFile } from 'node:fs/promises'\nimport type { AIToolLauncher, LaunchConfig, LaunchedProcess } from './types.js'\nimport { SessionRecorder } from '../session/recorder.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** Launcher for Anthropic's Claude Code CLI */\nexport class ClaudeCodeLauncher implements AIToolLauncher {\n readonly name = 'claude-code'\n readonly displayName = 'Claude Code'\n\n /** Check if the claude CLI is installed */\n async isInstalled(): Promise<boolean> {\n try {\n await execFileAsync('claude', ['--version'])\n return true\n } catch {\n return false\n }\n }\n\n /** Get the installed Claude Code version */\n async getVersion(): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync('claude', ['--version'])\n return stdout.trim()\n } catch {\n return null\n }\n }\n\n /** Launch Claude Code in the given working directory with the provided config */\n async launch(workdir: string, config: LaunchConfig): Promise<LaunchedProcess> {\n const args: string[] = []\n\n // Inject hidden system prompt\n const systemPrompt = await readFile(config.systemPromptPath, 'utf-8')\n args.push('--append-system-prompt', systemPrompt)\n\n // Set permission mode\n args.push('--permission-mode', config.permissionMode ?? 'default')\n\n // Set session name\n args.push('--name', `Interview: ${config.scenarioName}`)\n\n // Set model if specified\n if (config.model) {\n args.push('--model', config.model)\n }\n\n // Disallow tools for fairness\n if (config.disallowedTools && config.disallowedTools.length > 0) {\n args.push('--disallowedTools', ...config.disallowedTools)\n }\n\n // When recording, pipe stdout/stderr through the recorder\n const useRecording = config.recording === true\n const recorder = useRecording ? new SessionRecorder() : undefined\n\n // Spawn claude process\n const proc = spawn('claude', args, {\n cwd: workdir,\n stdio: useRecording ? ['inherit', 'pipe', 'pipe'] : 'inherit',\n env: { ...process.env },\n })\n\n // Forward piped output to the terminal and record it\n if (useRecording && recorder) {\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stdout', text)\n process.stdout.write(chunk)\n })\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stderr', text)\n process.stderr.write(chunk)\n })\n }\n\n return {\n wait: () =>\n new Promise((resolve) => {\n proc.on('exit', (code) => resolve({ exitCode: code ?? 0 }))\n }),\n kill: async () => {\n proc.kill('SIGTERM')\n },\n recorder,\n }\n }\n}\n","import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\n\n/** Event types that can be captured during a session */\nexport type SessionEventType = 'stdout' | 'stderr' | 'command' | 'note'\n\n/** A single timestamped event captured during a session */\nexport interface SessionEvent {\n /** Milliseconds since recording started */\n timestamp: number\n /** The kind of event */\n type: SessionEventType\n /** The captured data */\n data: string\n}\n\n/** Serialized recording format */\nexport interface RecordingData {\n /** Session ID this recording belongs to */\n sessionId: string\n /** ISO string of when recording started */\n startedAt: string\n /** All captured events */\n events: SessionEvent[]\n}\n\nconst RECORDINGS_DIR = join(homedir(), '.vibe-interviewing', 'recordings')\n\nasync function ensureRecordingsDir(): Promise<void> {\n if (!existsSync(RECORDINGS_DIR)) {\n await mkdir(RECORDINGS_DIR, { recursive: true })\n }\n}\n\n/**\n * Records timestamped events during an interview session.\n *\n * Captures stdout, stderr, commands, and notes with millisecond timestamps\n * relative to when the recorder was created.\n */\nexport class SessionRecorder {\n private readonly events: SessionEvent[] = []\n private readonly startTime: number\n private readonly startedAt: string\n\n constructor() {\n this.startTime = Date.now()\n this.startedAt = new Date().toISOString()\n }\n\n /** Record a timestamped event */\n record(type: SessionEventType, data: string): void {\n this.events.push({\n timestamp: Date.now() - this.startTime,\n type,\n data,\n })\n }\n\n /** Get all recorded events */\n getEvents(): ReadonlyArray<SessionEvent> {\n return this.events\n }\n\n /** Serialize the recording to a JSON-compatible object */\n toJSON(sessionId: string): RecordingData {\n return {\n sessionId,\n startedAt: this.startedAt,\n events: [...this.events],\n }\n }\n\n /** Create a SessionRecorder pre-populated with events from serialized data */\n static fromJSON(data: RecordingData): SessionRecorder {\n const recorder = new SessionRecorder()\n // Override the startedAt via Object.defineProperty since it's readonly\n Object.defineProperty(recorder, 'startedAt', { value: data.startedAt })\n for (const event of data.events) {\n recorder.events.push({ ...event })\n }\n return recorder\n }\n\n /** Save the recording to disk */\n async save(sessionId: string): Promise<void> {\n await ensureRecordingsDir()\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const data = this.toJSON(sessionId)\n await writeFile(filePath, JSON.stringify(data, null, 2))\n }\n\n /** Load a recording from disk */\n static async load(sessionId: string): Promise<SessionRecorder> {\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const raw = await readFile(filePath, 'utf-8')\n const data = JSON.parse(raw) as RecordingData\n return SessionRecorder.fromJSON(data)\n }\n\n /** List all available recording session IDs */\n static async list(): Promise<string[]> {\n await ensureRecordingsDir()\n const files = await readdir(RECORDINGS_DIR)\n return files\n .filter((f) => f.endsWith('.json'))\n .map((f) => f.replace(/\\.json$/, ''))\n .sort()\n }\n}\n","import { ClaudeCodeLauncher } from './claude-code.js'\nimport type { AIToolLauncher } from './types.js'\n\n/** All supported AI tool launchers */\nconst launchers: AIToolLauncher[] = [new ClaudeCodeLauncher()]\n\n/** Information about a detected AI coding tool */\nexport interface DetectedTool {\n /** The launcher instance */\n launcher: AIToolLauncher\n /** Installed version string, or null if unknown */\n version: string | null\n}\n\n/** Detect which AI coding tools are installed on the system */\nexport async function detectInstalledTools(): Promise<DetectedTool[]> {\n const results: DetectedTool[] = []\n\n for (const launcher of launchers) {\n const installed = await launcher.isInstalled()\n if (installed) {\n const version = await launcher.getVersion()\n results.push({ launcher, version })\n }\n }\n\n return results\n}\n\n/** Get a launcher by its internal name */\nexport function getLauncher(name: string): AIToolLauncher | undefined {\n return launchers.find((l) => l.name === name)\n}\n\n/** Get all registered launchers */\nexport function getAllLaunchers(): AIToolLauncher[] {\n return [...launchers]\n}\n","import { execSync } from 'node:child_process'\nimport { readFile, writeFile, mkdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { randomBytes } from 'node:crypto'\nimport type { AIToolLauncher, LaunchConfig } from '../launcher/types.js'\nimport type { ScenarioConfig } from '../scenario/types.js'\nimport { importRepo } from '../scenario/importer.js'\nimport { generateSystemPrompt } from '../scenario/loader.js'\nimport { SetupError } from '../errors.js'\nimport { saveSession, deleteSession } from './store.js'\nimport { toStoredSession } from './types.js'\nimport type { Session } from './types.js'\n\nexport type { Session }\n\n/** Callback for reporting session progress */\nexport type ProgressCallback = (stage: string) => void\n\n/** Manages the lifecycle of an interview session */\nexport class SessionManager {\n constructor(private launcher: AIToolLauncher) {}\n\n /**\n * Create a new interview session.\n *\n * Flow:\n * 1. Clone the repo at a pinned commit\n * 2. Apply bug patches (find/replace in source files)\n * 3. Wipe git history so the candidate can't diff to find the bug\n * 4. Remove scenario.yaml from workspace (interviewer-only)\n * 5. Write BRIEFING.md and system prompt\n * 6. Run setup commands (npm install, etc.)\n */\n async createSession(\n config: ScenarioConfig,\n workdir?: string,\n onProgress?: ProgressCallback,\n ): Promise<{ session: Session; config: ScenarioConfig }> {\n const id = randomBytes(4).toString('hex')\n const sessionDir = workdir ?? join(homedir(), 'vibe-sessions', `${config.name}-${id}`)\n\n const session: Session = {\n id,\n scenarioName: config.name,\n workdir: sessionDir,\n systemPromptPath: '',\n status: 'cloning',\n createdAt: new Date().toISOString(),\n }\n\n // 1. Clone the repo at the pinned commit\n onProgress?.('Cloning repository...')\n await importRepo(config.repo, sessionDir, config.commit)\n\n // 2. Apply bug patches\n onProgress?.('Injecting scenario...')\n for (const p of config.patch) {\n const filePath = join(sessionDir, p.file)\n const content = await readFile(filePath, 'utf-8')\n const patched = content.replace(p.find, p.replace)\n if (patched === content) {\n throw new SetupError(\n `patch ${p.file}`,\n `Could not find text to replace. The upstream code may have changed.`,\n )\n }\n await writeFile(filePath, patched)\n }\n\n // 3. Wipe git history so candidate can't see the injected changes\n onProgress?.('Preparing workspace...')\n await rm(join(sessionDir, '.git'), { recursive: true, force: true })\n execSync('git init && git add -A && git commit -m \"initial\"', {\n cwd: sessionDir,\n stdio: 'ignore',\n })\n\n // 4. Remove scenario.yaml from workspace (interviewer-only data)\n await rm(join(sessionDir, 'scenario.yaml'), { force: true })\n\n // 5. Write BRIEFING.md\n await writeFile(join(sessionDir, 'BRIEFING.md'), `# Interview Briefing\\n\\n${config.briefing}`)\n\n // Write system prompt OUTSIDE the workspace\n const promptDir = join(homedir(), '.vibe-interviewing', 'prompts')\n await mkdir(promptDir, { recursive: true })\n const systemPromptPath = join(promptDir, `${id}.md`)\n await writeFile(systemPromptPath, generateSystemPrompt(config))\n session.systemPromptPath = systemPromptPath\n\n // 6. Run setup commands\n session.status = 'setting-up'\n for (const cmd of config.setup) {\n onProgress?.(`Running: ${cmd}`)\n try {\n execSync(cmd, { cwd: sessionDir, stdio: 'pipe', timeout: 300000 })\n } catch (err) {\n throw new SetupError(cmd, err instanceof Error ? err.message : String(err))\n }\n }\n\n session.status = 'running'\n await saveSession(toStoredSession(session))\n return { session, config }\n }\n\n /** Launch the AI coding tool for an active session */\n async launchAITool(\n session: Session,\n _config: ScenarioConfig,\n launchConfig: Partial<LaunchConfig> = {},\n ): Promise<{ exitCode: number }> {\n const fullConfig: LaunchConfig = {\n scenarioName: session.scenarioName,\n systemPromptPath: session.systemPromptPath,\n ...launchConfig,\n }\n\n session.aiTool = this.launcher.name\n session.startedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n const proc = await this.launcher.launch(session.workdir, fullConfig)\n const result = await proc.wait()\n\n session.status = 'complete'\n session.completedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n return result\n }\n\n /** Destroy a session by removing its stored data */\n async destroySession(session: Session): Promise<void> {\n await deleteSession(session.id)\n }\n\n /** Get elapsed time since the AI tool was launched, formatted as a human-readable string */\n getElapsedTime(session: Session): string | null {\n if (!session.startedAt) return null\n\n const elapsed = Date.now() - new Date(session.startedAt).getTime()\n const minutes = Math.floor(elapsed / 60000)\n const seconds = Math.floor((elapsed % 60000) / 1000)\n\n if (minutes === 0) return `${seconds}s`\n return `${minutes}m ${seconds}s`\n }\n}\n","import { readFile, writeFile, readdir, unlink, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\nimport type { StoredSession } from './types.js'\n\nconst SESSIONS_DIR = join(homedir(), '.vibe-interviewing', 'sessions')\n\nasync function ensureSessionsDir(): Promise<void> {\n if (!existsSync(SESSIONS_DIR)) {\n await mkdir(SESSIONS_DIR, { recursive: true })\n }\n}\n\n/** Save a session to disk */\nexport async function saveSession(session: StoredSession): Promise<void> {\n await ensureSessionsDir()\n const filePath = join(SESSIONS_DIR, `${session.id}.json`)\n await writeFile(filePath, JSON.stringify(session, null, 2))\n}\n\n/** Load a session from disk */\nexport async function loadSession(id: string): Promise<StoredSession | null> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (!existsSync(filePath)) return null\n\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as StoredSession\n}\n\n/** Delete a session from disk */\nexport async function deleteSession(id: string): Promise<void> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (existsSync(filePath)) {\n await unlink(filePath)\n }\n}\n\n/** List all stored sessions */\nexport async function listSessions(): Promise<StoredSession[]> {\n await ensureSessionsDir()\n const files = await readdir(SESSIONS_DIR)\n const sessions: StoredSession[] = []\n\n for (const file of files) {\n if (!file.endsWith('.json')) continue\n try {\n const raw = await readFile(join(SESSIONS_DIR, file), 'utf-8')\n sessions.push(JSON.parse(raw) as StoredSession)\n } catch {\n // Skip corrupted files\n }\n }\n\n return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())\n}\n\n/** List only active (non-complete) sessions */\nexport async function listActiveSessions(): Promise<StoredSession[]> {\n const all = await listSessions()\n return all.filter((s) => s.status !== 'complete')\n}\n","/** Status of an interview session */\nexport type SessionStatus = 'cloning' | 'setting-up' | 'running' | 'complete'\n\n/** A live interview session */\nexport interface Session {\n /** Unique session identifier */\n id: string\n /** Name of the scenario being run */\n scenarioName: string\n /** Local working directory for the candidate */\n workdir: string\n /** Path to the system prompt file (outside workspace) */\n systemPromptPath: string\n /** Current session status */\n status: SessionStatus\n /** ISO timestamp of session creation */\n createdAt: string\n /** ISO timestamp of when the AI tool was launched */\n startedAt?: string\n /** ISO timestamp of session completion */\n completedAt?: string\n /** Name of the AI tool used */\n aiTool?: string\n}\n\n/** Serializable session data for persistence */\nexport interface StoredSession {\n /** Unique session identifier */\n id: string\n /** Name of the scenario being run */\n scenarioName: string\n /** Current session status */\n status: SessionStatus\n /** Local working directory for the candidate */\n workdir: string\n /** Path to the system prompt file */\n systemPromptPath: string\n /** Name of the AI tool used */\n aiTool?: string\n /** ISO timestamp of session creation */\n createdAt: string\n /** ISO timestamp of when the AI tool was launched */\n startedAt?: string\n /** ISO timestamp of session completion */\n completedAt?: string\n}\n\n/** Convert a Session to a StoredSession for persistence */\nexport function toStoredSession(session: Session): StoredSession {\n return {\n id: session.id,\n scenarioName: session.scenarioName,\n status: session.status,\n workdir: session.workdir,\n systemPromptPath: session.systemPromptPath,\n aiTool: session.aiTool,\n createdAt: session.createdAt,\n startedAt: session.startedAt,\n completedAt: session.completedAt,\n }\n}\n"],"mappings":"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","names":["readFile","existsSync","parseYaml","existsSync","readFile","parseYaml","join","join","readFile","readFile","join","existsSync","readFile","readFile","writeFile","mkdir","join","homedir","readFile","writeFile","readdir","mkdir","join","homedir","existsSync","join","homedir","readFile","writeFile","mkdir"]}
|
|
1
|
+
{"version":3,"sources":["../src/scenario/types.ts","../src/scenario/loader.ts","../src/scenario/validator.ts","../src/scenario/registry.ts","../src/scenario/importer.ts","../src/launcher/claude-code.ts","../src/session/recorder.ts","../src/launcher/detector.ts","../src/session/manager.ts","../src/session/store.ts","../src/session/types.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst AIRulesSchema = z.object({\n /** Role description for the AI assistant */\n role: z.string(),\n /** Behavioral rules (e.g., \"don't reveal the answer\") */\n rules: z.array(z.string()),\n /** Knowledge about the bug/solution (hidden from candidate) */\n knowledge: z.string(),\n})\n\nconst EvaluationSchema = z.object({\n /** Evaluation criteria for the interviewer */\n criteria: z.array(z.string()),\n /** Description of the expected fix */\n expected_fix: z.string().optional(),\n})\n\n/** A signal to watch for during the interview, with green and red flag indicators */\nconst KeySignalSchema = z.object({\n /** What behavior or skill this signal measures */\n signal: z.string(),\n /** What a strong candidate does (green flag) */\n positive: z.string(),\n /** What a weak candidate does (red flag) */\n negative: z.string(),\n})\n\n/** Structured guide for the interviewer — shown during hosting, never to the candidate */\nconst InterviewerGuideSchema = z.object({\n /** High-level summary of what this scenario evaluates and why */\n overview: z.string(),\n /** Specific behaviors to watch for, with green/red flag indicators */\n key_signals: z.array(KeySignalSchema).default([]),\n /** Common mistakes candidates make */\n common_pitfalls: z.array(z.string()).default([]),\n /** Questions to ask the candidate after the session */\n debrief_questions: z.array(z.string()).default([]),\n})\n\n/** A file modification to inject the bug */\nconst PatchSchema = z.object({\n /** Path to the file relative to repo root */\n file: z.string(),\n /** The original text to find */\n find: z.string(),\n /** The replacement text (with the bug) */\n replace: z.string(),\n})\n\n/** Scenario type — determines validation rules and system prompt context */\nconst ScenarioTypeSchema = z.enum(['debug', 'feature', 'refactor']).default('debug')\n\n/** Full scenario configuration schema */\nexport const ScenarioConfigSchema = z.object({\n /** Scenario display name */\n name: z.string(),\n /** One-line description (candidate-visible — describe symptoms/task, never the root cause or solution) */\n description: z.string(),\n /** Scenario type: debug (find a bug), feature (build something), refactor (improve code) */\n type: ScenarioTypeSchema,\n /** Difficulty level */\n difficulty: z.enum(['easy', 'medium', 'hard']),\n /** Estimated time (e.g., \"30-45m\") */\n estimated_time: z.string(),\n /** Searchable tags */\n tags: z.array(z.string()).default([]),\n\n /** GitHub repo URL or owner/repo shorthand */\n repo: z.string(),\n /** Commit SHA to pin the clone to (ensures reproducibility) */\n commit: z.string(),\n /** Shell commands to run after cloning (e.g., [\"npm install\"]) */\n setup: z.array(z.string()).default([]),\n\n /** Find-and-replace patches to inject the bug after cloning */\n patch: z.array(PatchSchema).default([]),\n /** Files or directories to delete after cloning (globs relative to repo root) */\n delete_files: z.array(z.string()).default([]),\n\n /** Briefing shown to the candidate (written like a team lead message) */\n briefing: z.string(),\n /** AI behavioral rules (injected via system prompt, hidden from candidate) */\n ai_rules: AIRulesSchema,\n /** Interviewer reference — what the fix/implementation looks like */\n solution: z.string().optional(),\n /** Acceptance criteria for feature scenarios (concrete, testable requirements) */\n acceptance_criteria: z.array(z.string()).optional(),\n\n /** Evaluation rubric */\n evaluation: EvaluationSchema.optional(),\n /** Structured interviewer guide — what to watch for, common pitfalls, debrief questions */\n interviewer_guide: InterviewerGuideSchema.optional(),\n /** License of the original project */\n license: z.string().optional(),\n})\n\nexport type ScenarioConfig = z.infer<typeof ScenarioConfigSchema>\nexport type ScenarioType = z.infer<typeof ScenarioTypeSchema>\nexport type AIRules = z.infer<typeof AIRulesSchema>\nexport type Evaluation = z.infer<typeof EvaluationSchema>\nexport type InterviewerGuide = z.infer<typeof InterviewerGuideSchema>\nexport type KeySignal = z.infer<typeof KeySignalSchema>\n\n/** Metadata about a discovered scenario */\nexport interface ScenarioInfo {\n /** Scenario name */\n name: string\n /** Parsed config */\n config: ScenarioConfig\n /** Whether this is a built-in scenario */\n builtIn: boolean\n}\n","import { readFile } from 'node:fs/promises'\nimport { parse as parseYaml } from 'yaml'\nimport { ScenarioNotFoundError, ScenarioValidationError, ScenarioFetchError } from '../errors.js'\nimport { ScenarioConfigSchema, type ScenarioConfig } from './types.js'\nimport { existsSync } from 'node:fs'\n\nconst MAX_RESPONSE_BYTES = 1_048_576 // 1 MB\nconst FETCH_TIMEOUT_MS = 15_000\n\n/**\n * Check whether a string looks like a URL (http:// or https://).\n *\n * @param input - The string to test\n * @returns true if the string starts with http:// or https://\n */\nexport function isUrl(input: string): boolean {\n return input.startsWith('http://') || input.startsWith('https://')\n}\n\n/**\n * Convert a GitHub blob URL to a raw content URL.\n *\n * @example\n * toGitHubRawUrl('https://github.com/owner/repo/blob/main/scenario.yaml')\n * // => 'https://raw.githubusercontent.com/owner/repo/main/scenario.yaml'\n */\nfunction toGitHubRawUrl(url: string): string {\n const match = url.match(/^https?:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/blob\\/(.+)$/)\n if (match) {\n return `https://raw.githubusercontent.com/${match[1]}/${match[2]}`\n }\n return url\n}\n\n/**\n * Fetch scenario YAML from a URL.\n *\n * @param url - The URL to fetch (GitHub blob URLs are auto-converted to raw)\n * @returns The YAML text\n * @throws ScenarioFetchError on network failure, timeout, or oversized response\n */\nasync function fetchScenarioYaml(url: string): Promise<string> {\n const rawUrl = toGitHubRawUrl(url)\n\n let response: Response\n try {\n response = await fetch(rawUrl, {\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n headers: { Accept: 'text/plain, application/x-yaml, */*' },\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : 'unknown error'\n throw new ScenarioFetchError(url, message)\n }\n\n if (!response.ok) {\n throw new ScenarioFetchError(url, `HTTP ${response.status} ${response.statusText}`)\n }\n\n const contentLength = response.headers.get('content-length')\n if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) {\n throw new ScenarioFetchError(url, 'response too large (>1 MB)')\n }\n\n const text = await response.text()\n if (text.length > MAX_RESPONSE_BYTES) {\n throw new ScenarioFetchError(url, 'response too large (>1 MB)')\n }\n\n return text\n}\n\n/**\n * Parse and validate scenario YAML text into a ScenarioConfig.\n *\n * @param raw - Raw YAML text\n * @param source - Source identifier (file path or URL) for error messages\n * @returns The parsed and validated scenario config\n */\nfunction parseScenarioYaml(raw: string, source: string): ScenarioConfig {\n const parsed: unknown = parseYaml(raw)\n\n const result = ScenarioConfigSchema.safeParse(parsed)\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`)\n throw new ScenarioValidationError(`validation failed (${source})`, issues)\n }\n\n return result.data\n}\n\n/**\n * Load and parse a scenario config from a YAML file or URL.\n *\n * Accepts either a local file path or an HTTP(S) URL. GitHub blob URLs\n * are automatically converted to raw content URLs.\n *\n * @param pathOrUrl - Absolute path to a scenario.yaml file, or a URL\n * @returns The parsed and validated scenario config\n */\nexport async function loadScenarioConfig(pathOrUrl: string): Promise<ScenarioConfig> {\n if (isUrl(pathOrUrl)) {\n const raw = await fetchScenarioYaml(pathOrUrl)\n return parseScenarioYaml(raw, pathOrUrl)\n }\n\n if (!existsSync(pathOrUrl)) {\n throw new ScenarioNotFoundError(pathOrUrl)\n }\n\n const raw = await readFile(pathOrUrl, 'utf-8')\n return parseScenarioYaml(raw, pathOrUrl)\n}\n\n/**\n * Generate a system prompt string from a scenario's ai_rules.\n *\n * This prompt is injected into the AI tool via --append-system-prompt\n * and is hidden from the candidate.\n *\n * @param config - The scenario config containing ai_rules\n * @returns The formatted system prompt\n */\nexport function generateSystemPrompt(config: ScenarioConfig): string {\n const lines: string[] = []\n\n lines.push(`# Interview Scenario: ${config.name}`)\n lines.push('')\n\n // Type-specific context\n const typeDescriptions: Record<string, string> = {\n debug:\n 'The candidate is debugging a bug in this codebase. Guide them through the debugging process without revealing the answer.',\n feature:\n 'The candidate is building a new feature. Help them understand the requirements, plan their approach, and implement it. Offer architectural guidance but let them drive the implementation.',\n refactor:\n 'The candidate is improving existing code. Help them identify issues and plan improvements. Encourage them to explain their reasoning for changes.',\n }\n lines.push('## Scenario Type')\n lines.push(typeDescriptions[config.type] ?? typeDescriptions['debug']!)\n lines.push('')\n\n lines.push('## Your Role')\n lines.push(config.ai_rules.role.trim())\n lines.push('')\n lines.push('## Rules')\n for (const rule of config.ai_rules.rules) {\n lines.push(`- ${rule}`)\n }\n lines.push('')\n\n const knowledgeHeaders: Record<string, string> = {\n debug: 'Knowledge (DO NOT share directly with the candidate)',\n feature: 'Implementation Context (DO NOT share directly with the candidate)',\n refactor: 'Improvement Context (DO NOT share directly with the candidate)',\n }\n lines.push(`## ${knowledgeHeaders[config.type] ?? knowledgeHeaders['debug']!}`)\n lines.push(config.ai_rules.knowledge.trim())\n\n return lines.join('\\n')\n}\n","import { ScenarioValidationError } from '../errors.js'\nimport type { ScenarioConfig } from './types.js'\n\n/** Result of validating a scenario configuration */\nexport interface ValidationResult {\n /** Whether the scenario is valid (no errors) */\n valid: boolean\n /** Non-fatal issues that should be addressed */\n warnings: string[]\n /** Fatal issues that prevent the scenario from running */\n errors: string[]\n}\n\n/**\n * Validate a scenario config for completeness and correctness.\n *\n * @param config - The scenario config to validate\n * @returns Validation result with errors and warnings\n */\nexport function validateScenario(config: ScenarioConfig): ValidationResult {\n const warnings: string[] = []\n const errors: string[] = []\n\n // Check briefing is not empty\n if (!config.briefing.trim()) {\n errors.push('Briefing cannot be empty')\n }\n\n // Check AI rules\n if (!config.ai_rules.role.trim()) {\n errors.push('ai_rules.role cannot be empty')\n }\n\n // Check repo is not empty\n if (!config.repo.trim()) {\n errors.push('repo cannot be empty')\n }\n\n // Check commit is a valid SHA\n if (!config.commit.trim()) {\n errors.push('commit cannot be empty — pin to a specific commit SHA for reproducibility')\n } else if (!/^[0-9a-f]{7,40}$/i.test(config.commit.trim())) {\n errors.push(\n 'commit must be a hex SHA (7-40 characters) — branch/tag names are not allowed for reproducibility',\n )\n }\n\n // Warnings for non-critical missing content\n if (config.ai_rules.rules.length === 0) {\n warnings.push('ai_rules.rules is empty — the AI will have no behavioral constraints')\n }\n\n // Type-specific validation\n if (config.type === 'debug') {\n if (config.patch.length === 0) {\n warnings.push('debug scenario has no patches — a bug must be injected via patch')\n }\n if (!config.solution?.trim()) {\n warnings.push('solution is empty — interviewers will have no solution reference')\n }\n }\n\n if (config.type === 'feature') {\n const hasCriteria =\n (config.acceptance_criteria && config.acceptance_criteria.length > 0) ||\n (config.evaluation && config.evaluation.criteria && config.evaluation.criteria.length > 0)\n if (!hasCriteria) {\n warnings.push(\n 'feature scenario has no acceptance_criteria or evaluation.criteria — candidates need a definition of done',\n )\n }\n }\n\n if (!config.evaluation) {\n warnings.push('No evaluation criteria defined')\n }\n\n if (!config.interviewer_guide) {\n warnings.push(\n 'No interviewer_guide defined — interviewers will have limited context during the session',\n )\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n }\n}\n\n/**\n * Validate a scenario config and throw if invalid.\n *\n * @param config - The scenario config to validate\n * @returns Validation result (only returned if valid)\n * @throws ScenarioValidationError if the config has errors\n */\nexport function validateScenarioOrThrow(config: ScenarioConfig): ValidationResult {\n const result = validateScenario(config)\n if (!result.valid) {\n throw new ScenarioValidationError('scenario validation failed', result.errors)\n }\n return result\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\nimport { loadScenarioConfig } from './loader.js'\nimport type { ScenarioInfo } from './types.js'\n\n/** Shape of a single entry in registry.yaml */\ninterface RegistryEntry {\n name: string\n repo: string\n commit: string\n description: string\n difficulty: 'easy' | 'medium' | 'hard'\n estimated_time: string\n}\n\n/** Shape of the registry.yaml file */\ninterface RegistryFile {\n scenarios: RegistryEntry[]\n}\n\n/**\n * Get the path to the scenarios package directory.\n *\n * Resolves via the @vibe-interviewing/scenarios package, which works both\n * in the monorepo (workspace link) and when installed from npm.\n */\nasync function getScenariosPackagePath(): Promise<string> {\n try {\n const { getScenariosDir } = await import('@vibe-interviewing/scenarios')\n return getScenariosDir()\n } catch {\n // Fallback: try relative to process.cwd() (monorepo root)\n const fromCwd = join(process.cwd(), 'packages', 'scenarios')\n if (existsSync(fromCwd)) {\n return fromCwd\n }\n throw new Error('Could not locate @vibe-interviewing/scenarios package')\n }\n}\n\n/**\n * Discover built-in scenarios from the registry.yaml file in the scenarios package.\n *\n * Each entry in the registry points to a scenario directory containing a full\n * scenario.yaml. The registry provides quick lookup metadata, while the full\n * config is loaded from the scenario's own config file.\n *\n * @returns Array of discovered scenario info objects\n */\nexport async function discoverBuiltInScenarios(): Promise<ScenarioInfo[]> {\n const scenariosPath = await getScenariosPackagePath()\n const registryPath = join(scenariosPath, 'registry.yaml')\n\n if (!existsSync(registryPath)) {\n return []\n }\n\n const raw = await readFile(registryPath, 'utf-8')\n const registry = parseYaml(raw) as RegistryFile\n\n if (!registry.scenarios || !Array.isArray(registry.scenarios)) {\n return []\n }\n\n const scenarios: ScenarioInfo[] = []\n\n for (const entry of registry.scenarios) {\n // Load the full config from the scenario's own directory\n const scenarioConfigPath = join(scenariosPath, entry.name, 'scenario.yaml')\n\n if (existsSync(scenarioConfigPath)) {\n const config = await loadScenarioConfig(scenarioConfigPath)\n scenarios.push({\n name: entry.name,\n config,\n builtIn: true,\n })\n } else {\n console.warn(\n `Warning: scenario \"${entry.name}\" listed in registry but ${scenarioConfigPath} not found`,\n )\n }\n }\n\n return scenarios\n}\n\n/**\n * Discover all available scenarios.\n *\n * Currently returns only built-in scenarios from the registry.\n * Local scenario support can be added later.\n *\n * @returns Array of all discovered scenario info objects\n */\nexport async function discoverAllScenarios(): Promise<ScenarioInfo[]> {\n return discoverBuiltInScenarios()\n}\n","import { mkdtemp } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { GitCloneError } from '../errors.js'\n\n/**\n * Import a repository by cloning it to a local directory.\n *\n * For commit SHAs, uses a shallow fetch to avoid downloading full history.\n *\n * @param repoUrl - GitHub URL, SSH URL, or owner/repo shorthand\n * @param targetPath - Directory to clone into (defaults to a temp directory)\n * @param ref - Optional branch, tag, or commit to checkout\n * @returns The path to the cloned directory\n */\nexport async function importRepo(\n repoUrl: string,\n targetPath?: string,\n ref?: string,\n): Promise<string> {\n const { simpleGit } = await import('simple-git')\n\n // Normalize shorthand (owner/repo -> https://github.com/owner/repo)\n const url = normalizeRepoUrl(repoUrl)\n\n // Clone to target path or temp directory\n const dest = targetPath ?? (await mkdtemp(join(tmpdir(), 'vibe-import-')))\n\n const git = simpleGit()\n\n try {\n if (ref && /^[0-9a-f]{7,40}$/i.test(ref)) {\n // Commit SHA — init + shallow fetch to avoid downloading full history\n await git.init([dest])\n const repoGit = simpleGit(dest)\n await repoGit.addRemote('origin', url)\n await repoGit.fetch(['origin', ref, '--depth', '1'])\n await repoGit.checkout(['FETCH_HEAD'])\n } else if (ref) {\n // Branch or tag — shallow clone\n await git.clone(url, dest, ['--depth', '1', '--branch', ref])\n } else {\n await git.clone(url, dest, ['--depth', '1'])\n }\n } catch (err) {\n throw new GitCloneError(url, err instanceof Error ? err.message : String(err))\n }\n\n return dest\n}\n\n/** Normalize various repo URL formats to full HTTPS GitHub URLs.\n * Currently only handles GitHub SSH URLs — other hosts pass through as-is. */\nfunction normalizeRepoUrl(input: string): string {\n // Already a full URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return input\n }\n\n // Git SSH format\n if (input.startsWith('git@')) {\n return input.replace('git@github.com:', 'https://github.com/').replace(/\\.git$/, '')\n }\n\n // Shorthand: owner/repo\n if (/^[a-zA-Z0-9_-]+\\/[a-zA-Z0-9._-]+$/.test(input)) {\n return `https://github.com/${input}`\n }\n\n return input\n}\n","import { spawn } from 'node:child_process'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { readFile } from 'node:fs/promises'\nimport type { AIToolLauncher, LaunchConfig, LaunchedProcess } from './types.js'\nimport { SessionRecorder } from '../session/recorder.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** Launcher for Anthropic's Claude Code CLI */\nexport class ClaudeCodeLauncher implements AIToolLauncher {\n readonly name = 'claude-code'\n readonly displayName = 'Claude Code'\n\n /** Check if the claude CLI is installed */\n async isInstalled(): Promise<boolean> {\n try {\n await execFileAsync('claude', ['--version'])\n return true\n } catch {\n return false\n }\n }\n\n /** Get the installed Claude Code version */\n async getVersion(): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync('claude', ['--version'])\n return stdout.trim()\n } catch {\n return null\n }\n }\n\n /** Launch Claude Code in the given working directory with the provided config */\n async launch(workdir: string, config: LaunchConfig): Promise<LaunchedProcess> {\n const args: string[] = []\n\n // Inject hidden system prompt\n const systemPrompt = await readFile(config.systemPromptPath, 'utf-8')\n args.push('--append-system-prompt', systemPrompt)\n\n // Set permission mode\n args.push('--permission-mode', config.permissionMode ?? 'default')\n\n // Set session name\n args.push('--name', `Interview: ${config.scenarioName}`)\n\n // Set model if specified\n if (config.model) {\n args.push('--model', config.model)\n }\n\n // Disallow tools for fairness\n if (config.disallowedTools && config.disallowedTools.length > 0) {\n args.push('--disallowedTools', ...config.disallowedTools)\n }\n\n // When recording, pipe stdout/stderr through the recorder\n const useRecording = config.recording === true\n const recorder = useRecording ? new SessionRecorder() : undefined\n\n // Spawn claude process\n const proc = spawn('claude', args, {\n cwd: workdir,\n stdio: useRecording ? ['inherit', 'pipe', 'pipe'] : 'inherit',\n env: { ...process.env },\n })\n\n // Forward piped output to the terminal and record it\n if (useRecording && recorder) {\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stdout', text)\n process.stdout.write(chunk)\n })\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stderr', text)\n process.stderr.write(chunk)\n })\n }\n\n return {\n wait: () =>\n new Promise((resolve) => {\n proc.on('exit', (code) => resolve({ exitCode: code ?? 0 }))\n }),\n kill: async () => {\n proc.kill('SIGTERM')\n },\n recorder,\n }\n }\n}\n","import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\n\n/** Event types that can be captured during a session */\nexport type SessionEventType = 'stdout' | 'stderr' | 'command' | 'note'\n\n/** A single timestamped event captured during a session */\nexport interface SessionEvent {\n /** Milliseconds since recording started */\n timestamp: number\n /** The kind of event */\n type: SessionEventType\n /** The captured data */\n data: string\n}\n\n/** Serialized recording format */\nexport interface RecordingData {\n /** Session ID this recording belongs to */\n sessionId: string\n /** ISO string of when recording started */\n startedAt: string\n /** All captured events */\n events: SessionEvent[]\n}\n\nconst RECORDINGS_DIR = join(homedir(), '.vibe-interviewing', 'recordings')\n\nasync function ensureRecordingsDir(): Promise<void> {\n if (!existsSync(RECORDINGS_DIR)) {\n await mkdir(RECORDINGS_DIR, { recursive: true })\n }\n}\n\n/**\n * Records timestamped events during an interview session.\n *\n * Captures stdout, stderr, commands, and notes with millisecond timestamps\n * relative to when the recorder was created.\n */\nexport class SessionRecorder {\n private readonly events: SessionEvent[] = []\n private readonly startTime: number\n private readonly startedAt: string\n\n constructor() {\n this.startTime = Date.now()\n this.startedAt = new Date().toISOString()\n }\n\n /** Record a timestamped event */\n record(type: SessionEventType, data: string): void {\n this.events.push({\n timestamp: Date.now() - this.startTime,\n type,\n data,\n })\n }\n\n /** Get all recorded events */\n getEvents(): ReadonlyArray<SessionEvent> {\n return this.events\n }\n\n /** Serialize the recording to a JSON-compatible object */\n toJSON(sessionId: string): RecordingData {\n return {\n sessionId,\n startedAt: this.startedAt,\n events: [...this.events],\n }\n }\n\n /** Create a SessionRecorder pre-populated with events from serialized data */\n static fromJSON(data: RecordingData): SessionRecorder {\n const recorder = new SessionRecorder()\n // Override startedAt and startTime to preserve original timing\n Object.defineProperty(recorder, 'startedAt', { value: data.startedAt })\n Object.defineProperty(recorder, 'startTime', {\n value: new Date(data.startedAt).getTime(),\n })\n for (const event of data.events) {\n recorder.events.push({ ...event })\n }\n return recorder\n }\n\n /** Save the recording to disk */\n async save(sessionId: string): Promise<void> {\n await ensureRecordingsDir()\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const data = this.toJSON(sessionId)\n await writeFile(filePath, JSON.stringify(data, null, 2))\n }\n\n /** Load a recording from disk */\n static async load(sessionId: string): Promise<SessionRecorder> {\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const raw = await readFile(filePath, 'utf-8')\n const data = JSON.parse(raw) as RecordingData\n return SessionRecorder.fromJSON(data)\n }\n\n /** List all available recording session IDs */\n static async list(): Promise<string[]> {\n await ensureRecordingsDir()\n const files = await readdir(RECORDINGS_DIR)\n return files\n .filter((f) => f.endsWith('.json'))\n .map((f) => f.replace(/\\.json$/, ''))\n .sort()\n }\n}\n","import { ClaudeCodeLauncher } from './claude-code.js'\nimport type { AIToolLauncher } from './types.js'\n\n/** All supported AI tool launchers */\nconst launchers: AIToolLauncher[] = [new ClaudeCodeLauncher()]\n\n/** Information about a detected AI coding tool */\nexport interface DetectedTool {\n /** The launcher instance */\n launcher: AIToolLauncher\n /** Installed version string, or null if unknown */\n version: string | null\n}\n\n/** Detect which AI coding tools are installed on the system */\nexport async function detectInstalledTools(): Promise<DetectedTool[]> {\n const results: DetectedTool[] = []\n\n for (const launcher of launchers) {\n const installed = await launcher.isInstalled()\n if (installed) {\n const version = await launcher.getVersion()\n results.push({ launcher, version })\n }\n }\n\n return results\n}\n\n/** Get a launcher by its internal name */\nexport function getLauncher(name: string): AIToolLauncher | undefined {\n return launchers.find((l) => l.name === name)\n}\n\n/** Get all registered launchers */\nexport function getAllLaunchers(): AIToolLauncher[] {\n return [...launchers]\n}\n","import { execSync } from 'node:child_process'\nimport { readFile, writeFile, mkdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { randomBytes } from 'node:crypto'\nimport type { AIToolLauncher, LaunchConfig } from '../launcher/types.js'\nimport type { ScenarioConfig } from '../scenario/types.js'\nimport { importRepo } from '../scenario/importer.js'\nimport { generateSystemPrompt } from '../scenario/loader.js'\nimport { SetupError } from '../errors.js'\nimport { saveSession, deleteSession } from './store.js'\nimport { toStoredSession } from './types.js'\nimport type { Session } from './types.js'\n\nexport type { Session }\n\n/** Callback for reporting session progress */\nexport type ProgressCallback = (stage: string) => void\n\n/** Manages the lifecycle of an interview session */\nexport class SessionManager {\n constructor(private launcher: AIToolLauncher) {}\n\n /**\n * Create a new interview session.\n *\n * Flow:\n * 1. Clone the repo at a pinned commit\n * 2. Apply bug patches (find/replace in source files)\n * 3. Delete files excluded by the scenario (e.g., tests that reveal the bug)\n * 4. Wipe git history so the candidate can't diff to find the bug\n * 5. Remove scenario.yaml from workspace (interviewer-only)\n * 6. Write BRIEFING.md and system prompt\n * 7. Run setup commands (npm install, etc.)\n */\n async createSession(\n config: ScenarioConfig,\n workdir?: string,\n onProgress?: ProgressCallback,\n options?: { skipSetup?: boolean },\n ): Promise<{ session: Session; config: ScenarioConfig }> {\n const id = randomBytes(4).toString('hex')\n const sessionDir = workdir ?? join(homedir(), 'vibe-sessions', `${config.name}-${id}`)\n\n const session: Session = {\n id,\n scenarioName: config.name,\n workdir: sessionDir,\n systemPromptPath: '',\n status: 'cloning',\n createdAt: new Date().toISOString(),\n }\n\n // 1. Clone the repo at the pinned commit\n onProgress?.('Cloning repository...')\n await importRepo(config.repo, sessionDir, config.commit)\n\n // 2. Apply bug patches\n onProgress?.('Injecting scenario...')\n for (const p of config.patch) {\n const filePath = join(sessionDir, p.file)\n const content = await readFile(filePath, 'utf-8')\n const patched = content.replaceAll(p.find, p.replace)\n if (patched === content) {\n throw new SetupError(\n `patch ${p.file}`,\n `Could not find text to replace. The upstream code may have changed.`,\n )\n }\n await writeFile(filePath, patched)\n }\n\n // 3. Delete files/directories specified by the scenario\n if (config.delete_files.length > 0) {\n onProgress?.('Removing excluded files...')\n for (const target of config.delete_files) {\n await rm(join(sessionDir, target), { recursive: true, force: true })\n }\n }\n\n // 4. Wipe git history so candidate can't see the injected changes\n onProgress?.('Preparing workspace...')\n await rm(join(sessionDir, '.git'), { recursive: true, force: true })\n execSync(\n 'git init && git add -A && git -c user.name=vibe -c user.email=vibe@local commit -m \"initial\"',\n { cwd: sessionDir, stdio: 'ignore' },\n )\n\n // 5. Remove scenario.yaml from workspace (interviewer-only data)\n await rm(join(sessionDir, 'scenario.yaml'), { force: true })\n\n // 6. Write BRIEFING.md\n await writeFile(join(sessionDir, 'BRIEFING.md'), `# Interview Briefing\\n\\n${config.briefing}`)\n\n // Write system prompt OUTSIDE the workspace\n const promptDir = join(homedir(), '.vibe-interviewing', 'prompts')\n await mkdir(promptDir, { recursive: true })\n const systemPromptPath = join(promptDir, `${id}.md`)\n await writeFile(systemPromptPath, generateSystemPrompt(config))\n session.systemPromptPath = systemPromptPath\n\n // 7. Run setup commands (skip when hosting — candidate runs setup after download)\n if (!options?.skipSetup) {\n session.status = 'setting-up'\n for (const cmd of config.setup) {\n onProgress?.(`Running: ${cmd}`)\n try {\n execSync(cmd, { cwd: sessionDir, stdio: 'pipe', timeout: 300000 })\n } catch (err) {\n throw new SetupError(cmd, err instanceof Error ? err.message : String(err))\n }\n }\n }\n\n session.status = 'running'\n await saveSession(toStoredSession(session))\n return { session, config }\n }\n\n /** Launch the AI coding tool for an active session */\n async launchAITool(\n session: Session,\n _config: ScenarioConfig,\n launchConfig: Partial<LaunchConfig> = {},\n ): Promise<{ exitCode: number }> {\n const fullConfig: LaunchConfig = {\n scenarioName: session.scenarioName,\n systemPromptPath: session.systemPromptPath,\n ...launchConfig,\n }\n\n session.aiTool = this.launcher.name\n session.startedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n const proc = await this.launcher.launch(session.workdir, fullConfig)\n const result = await proc.wait()\n\n session.status = 'complete'\n session.completedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n return result\n }\n\n /** Destroy a session by removing its stored data */\n async destroySession(session: Session): Promise<void> {\n await deleteSession(session.id)\n }\n\n /** Get elapsed time since the AI tool was launched, formatted as a human-readable string */\n getElapsedTime(session: Session): string | null {\n if (!session.startedAt) return null\n\n const elapsed = Date.now() - new Date(session.startedAt).getTime()\n const minutes = Math.floor(elapsed / 60000)\n const seconds = Math.floor((elapsed % 60000) / 1000)\n\n if (minutes === 0) return `${seconds}s`\n return `${minutes}m ${seconds}s`\n }\n}\n","import { readFile, writeFile, readdir, unlink, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\nimport type { StoredSession } from './types.js'\n\nconst SESSIONS_DIR = join(homedir(), '.vibe-interviewing', 'sessions')\n\nasync function ensureSessionsDir(): Promise<void> {\n if (!existsSync(SESSIONS_DIR)) {\n await mkdir(SESSIONS_DIR, { recursive: true })\n }\n}\n\n/** Save a session to disk */\nexport async function saveSession(session: StoredSession): Promise<void> {\n await ensureSessionsDir()\n const filePath = join(SESSIONS_DIR, `${session.id}.json`)\n await writeFile(filePath, JSON.stringify(session, null, 2))\n}\n\n/** Load a session from disk */\nexport async function loadSession(id: string): Promise<StoredSession | null> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (!existsSync(filePath)) return null\n\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as StoredSession\n}\n\n/** Delete a session from disk */\nexport async function deleteSession(id: string): Promise<void> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (existsSync(filePath)) {\n await unlink(filePath)\n }\n}\n\n/** List all stored sessions */\nexport async function listSessions(): Promise<StoredSession[]> {\n await ensureSessionsDir()\n const files = await readdir(SESSIONS_DIR)\n const sessions: StoredSession[] = []\n\n for (const file of files) {\n if (!file.endsWith('.json')) continue\n try {\n const raw = await readFile(join(SESSIONS_DIR, file), 'utf-8')\n sessions.push(JSON.parse(raw) as StoredSession)\n } catch {\n // Skip corrupted files\n }\n }\n\n return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())\n}\n\n/** List only active (non-complete) sessions */\nexport async function listActiveSessions(): Promise<StoredSession[]> {\n const all = await listSessions()\n return all.filter((s) => s.status !== 'complete')\n}\n","/** Status of an interview session */\nexport type SessionStatus = 'cloning' | 'setting-up' | 'running' | 'complete'\n\n/** Interview session — used both in-memory and for persistence */\nexport interface Session {\n /** Unique session identifier */\n id: string\n /** Name of the scenario being run */\n scenarioName: string\n /** Local working directory for the candidate */\n workdir: string\n /** Path to the system prompt file (outside workspace) */\n systemPromptPath: string\n /** Current session status */\n status: SessionStatus\n /** ISO timestamp of session creation */\n createdAt: string\n /** ISO timestamp of when the AI tool was launched */\n startedAt?: string\n /** ISO timestamp of session completion */\n completedAt?: string\n /** Name of the AI tool used */\n aiTool?: string\n}\n\n/**\n * Serializable session data for persistence.\n * Identical to Session — kept as an alias for API clarity at persistence boundaries.\n */\nexport type StoredSession = Session\n\n/** Convert a Session to a StoredSession for persistence */\nexport function toStoredSession(session: Session): StoredSession {\n return { ...session }\n}\n"],"mappings":"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","names":["raw","readFile","existsSync","parseYaml","existsSync","readFile","parseYaml","join","join","readFile","readFile","join","existsSync","readFile","readFile","writeFile","mkdir","join","homedir","readFile","writeFile","readdir","mkdir","join","homedir","existsSync","join","homedir","readFile","writeFile","mkdir"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { l as Session, S as ScenarioConfig, V as VibeError, P as ProgressCallback } from '../session-code-CfhXelpW.js';
|
|
2
|
+
export { f as InvalidSessionCodeError, s as decodeSessionCode, t as encodeSessionCode, u as isCloudSessionCode } from '../session-code-CfhXelpW.js';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
/** Metadata served to candidates */
|
|
7
|
+
interface SessionMetadata {
|
|
8
|
+
scenarioName: string;
|
|
9
|
+
type: string;
|
|
10
|
+
difficulty: string;
|
|
11
|
+
estimatedTime: string;
|
|
12
|
+
briefing: string;
|
|
13
|
+
setupCommands: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Events emitted by the session server */
|
|
16
|
+
interface SessionServerEvents {
|
|
17
|
+
'candidate-connected': [];
|
|
18
|
+
'download-complete': [];
|
|
19
|
+
}
|
|
20
|
+
/** HTTP server that serves a prepared workspace to a remote candidate */
|
|
21
|
+
declare class SessionServer extends EventEmitter<SessionServerEvents> {
|
|
22
|
+
private server;
|
|
23
|
+
/** Start serving the session workspace */
|
|
24
|
+
start(session: Session, config: ScenarioConfig, port?: number): Promise<{
|
|
25
|
+
code: string;
|
|
26
|
+
port: number;
|
|
27
|
+
host: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** Stop the server and clean up */
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Result of downloading a session from a host */
|
|
34
|
+
interface DownloadedSession {
|
|
35
|
+
/** Path to the extracted workspace */
|
|
36
|
+
workdir: string;
|
|
37
|
+
/** Path to the system prompt file */
|
|
38
|
+
systemPromptPath: string;
|
|
39
|
+
/** Session metadata from the host */
|
|
40
|
+
metadata: SessionMetadata;
|
|
41
|
+
/** Generated session ID */
|
|
42
|
+
id: string;
|
|
43
|
+
}
|
|
44
|
+
/** Error for network/connection failures */
|
|
45
|
+
declare class NetworkError extends VibeError {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Download a session from a host.
|
|
50
|
+
*
|
|
51
|
+
* Fetches metadata, system prompt, and workspace tarball, then
|
|
52
|
+
* extracts everything to a local directory.
|
|
53
|
+
*/
|
|
54
|
+
declare function downloadSession(host: string, port: number, targetDir?: string, onProgress?: ProgressCallback): Promise<DownloadedSession>;
|
|
55
|
+
|
|
56
|
+
/** Default Cloudflare Worker URL */
|
|
57
|
+
declare const DEFAULT_WORKER_URL = "https://api.vibe-interviewing.iar.dev";
|
|
58
|
+
/** Get the configured worker URL (env override or default) */
|
|
59
|
+
declare function getWorkerUrl(): string;
|
|
60
|
+
/** Error for cloud upload/download failures */
|
|
61
|
+
declare class CloudError extends VibeError {
|
|
62
|
+
constructor(message: string);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Upload a session to the cloud relay.
|
|
66
|
+
*
|
|
67
|
+
* Sends metadata, system prompt, and workspace tarball to the Cloudflare Worker.
|
|
68
|
+
* Returns a session code the candidate can use to download.
|
|
69
|
+
*/
|
|
70
|
+
declare function uploadSession(workerUrl: string, metadata: SessionMetadata, systemPrompt: string, tarballPath: string, onProgress?: ProgressCallback): Promise<{
|
|
71
|
+
code: string;
|
|
72
|
+
expiresAt: string;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Download a session from the cloud relay.
|
|
76
|
+
*
|
|
77
|
+
* Fetches metadata, system prompt, and workspace tarball from the Cloudflare Worker.
|
|
78
|
+
*/
|
|
79
|
+
declare function downloadSessionFromCloud(workerUrl: string, code: string, targetDir?: string, onProgress?: ProgressCallback): Promise<DownloadedSession>;
|
|
80
|
+
|
|
81
|
+
export { CloudError, DEFAULT_WORKER_URL, type DownloadedSession, NetworkError, type SessionMetadata, SessionServer, type SessionServerEvents, downloadSession, downloadSessionFromCloud, getWorkerUrl, uploadSession };
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidSessionCodeError,
|
|
3
|
+
VibeError,
|
|
4
|
+
decodeSessionCode,
|
|
5
|
+
encodeSessionCode,
|
|
6
|
+
isCloudSessionCode
|
|
7
|
+
} from "../chunk-CI3BD2WQ.js";
|
|
8
|
+
|
|
9
|
+
// src/network/server.ts
|
|
10
|
+
import { createServer } from "http";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { networkInterfaces } from "os";
|
|
15
|
+
import { EventEmitter } from "events";
|
|
16
|
+
var SessionServer = class extends EventEmitter {
|
|
17
|
+
server = null;
|
|
18
|
+
/** Start serving the session workspace */
|
|
19
|
+
async start(session, config, port) {
|
|
20
|
+
const tarball = join(session.workdir, "..", `${session.id}.tar.gz`);
|
|
21
|
+
execSync(`tar czf "${tarball}" -C "${session.workdir}" .`, { stdio: "pipe" });
|
|
22
|
+
const systemPrompt = await readFile(session.systemPromptPath, "utf-8");
|
|
23
|
+
const metadata = {
|
|
24
|
+
scenarioName: config.name,
|
|
25
|
+
type: config.type,
|
|
26
|
+
difficulty: config.difficulty,
|
|
27
|
+
estimatedTime: config.estimated_time,
|
|
28
|
+
briefing: config.briefing,
|
|
29
|
+
setupCommands: config.setup
|
|
30
|
+
};
|
|
31
|
+
this.server = createServer((req, res) => {
|
|
32
|
+
if (req.method !== "GET") {
|
|
33
|
+
res.writeHead(405);
|
|
34
|
+
res.end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
switch (req.url) {
|
|
38
|
+
case "/metadata":
|
|
39
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify(metadata));
|
|
41
|
+
break;
|
|
42
|
+
case "/system-prompt":
|
|
43
|
+
this.emit("candidate-connected");
|
|
44
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
45
|
+
res.end(systemPrompt);
|
|
46
|
+
break;
|
|
47
|
+
case "/workspace":
|
|
48
|
+
readFile(tarball).then((data) => {
|
|
49
|
+
res.writeHead(200, {
|
|
50
|
+
"Content-Type": "application/gzip",
|
|
51
|
+
"Content-Length": data.length
|
|
52
|
+
});
|
|
53
|
+
res.end(data);
|
|
54
|
+
this.emit("download-complete");
|
|
55
|
+
}).catch(() => {
|
|
56
|
+
res.writeHead(500);
|
|
57
|
+
res.end("Failed to read workspace tarball");
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
res.writeHead(404);
|
|
62
|
+
res.end();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const listenPort = port ?? 0;
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
this.server.listen(listenPort, () => {
|
|
68
|
+
const addr = this.server.address();
|
|
69
|
+
if (!addr || typeof addr === "string") {
|
|
70
|
+
reject(new Error("Failed to get server address"));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const host = getLanIp();
|
|
74
|
+
const code = encodeSessionCode(host, addr.port);
|
|
75
|
+
resolve({ code, port: addr.port, host });
|
|
76
|
+
});
|
|
77
|
+
this.server.on("error", reject);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Stop the server and clean up */
|
|
81
|
+
async stop() {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
if (this.server) {
|
|
84
|
+
this.server.close(() => resolve());
|
|
85
|
+
} else {
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function getLanIp() {
|
|
92
|
+
const interfaces = networkInterfaces();
|
|
93
|
+
for (const entries of Object.values(interfaces)) {
|
|
94
|
+
if (!entries) continue;
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (entry.family === "IPv4" && !entry.internal) {
|
|
97
|
+
return entry.address;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return "127.0.0.1";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/network/client.ts
|
|
105
|
+
import { get } from "http";
|
|
106
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
107
|
+
import { join as join2 } from "path";
|
|
108
|
+
import { homedir } from "os";
|
|
109
|
+
import { execSync as execSync2 } from "child_process";
|
|
110
|
+
import { randomBytes } from "crypto";
|
|
111
|
+
var NetworkError = class extends VibeError {
|
|
112
|
+
constructor(message) {
|
|
113
|
+
super(message, "NETWORK_ERROR", "Check the session code and ensure the host is running");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function fetchJson(host, port, path) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const req = get({ hostname: host, port, path, timeout: 1e4 }, (res) => {
|
|
119
|
+
if (res.statusCode !== 200) {
|
|
120
|
+
reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let data = "";
|
|
124
|
+
res.on("data", (chunk) => {
|
|
125
|
+
data += chunk.toString();
|
|
126
|
+
});
|
|
127
|
+
res.on("end", () => {
|
|
128
|
+
try {
|
|
129
|
+
resolve(JSON.parse(data));
|
|
130
|
+
} catch {
|
|
131
|
+
reject(new NetworkError(`Invalid response from server for ${path}`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
req.on("error", (err) => reject(new NetworkError(`Connection failed: ${err.message}`)));
|
|
136
|
+
req.on("timeout", () => {
|
|
137
|
+
req.destroy();
|
|
138
|
+
reject(new NetworkError("Connection timed out"));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function fetchText(host, port, path) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const req = get({ hostname: host, port, path, timeout: 1e4 }, (res) => {
|
|
145
|
+
if (res.statusCode !== 200) {
|
|
146
|
+
reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let data = "";
|
|
150
|
+
res.on("data", (chunk) => {
|
|
151
|
+
data += chunk.toString();
|
|
152
|
+
});
|
|
153
|
+
res.on("end", () => resolve(data));
|
|
154
|
+
});
|
|
155
|
+
req.on("error", (err) => reject(new NetworkError(`Connection failed: ${err.message}`)));
|
|
156
|
+
req.on("timeout", () => {
|
|
157
|
+
req.destroy();
|
|
158
|
+
reject(new NetworkError("Connection timed out"));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function fetchBinary(host, port, path) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const req = get({ hostname: host, port, path, timeout: 6e4 }, (res) => {
|
|
165
|
+
if (res.statusCode !== 200) {
|
|
166
|
+
reject(new NetworkError(`Server returned ${res.statusCode} for ${path}`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const chunks = [];
|
|
170
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
171
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
172
|
+
});
|
|
173
|
+
req.on("error", (err) => reject(new NetworkError(`Download failed: ${err.message}`)));
|
|
174
|
+
req.on("timeout", () => {
|
|
175
|
+
req.destroy();
|
|
176
|
+
reject(new NetworkError("Download timed out"));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function downloadSession(host, port, targetDir, onProgress) {
|
|
181
|
+
const id = randomBytes(4).toString("hex");
|
|
182
|
+
onProgress?.("Connecting to host...");
|
|
183
|
+
const metadata = await fetchJson(host, port, "/metadata");
|
|
184
|
+
onProgress?.("Downloading scenario...");
|
|
185
|
+
const systemPrompt = await fetchText(host, port, "/system-prompt");
|
|
186
|
+
onProgress?.("Downloading workspace...");
|
|
187
|
+
const tarball = await fetchBinary(host, port, "/workspace");
|
|
188
|
+
onProgress?.("Extracting workspace...");
|
|
189
|
+
const workdir = targetDir ?? join2(homedir(), "vibe-sessions", `${metadata.scenarioName}-${id}`);
|
|
190
|
+
await mkdir(workdir, { recursive: true });
|
|
191
|
+
const tarballPath = join2(workdir, "..", `${id}-download.tar.gz`);
|
|
192
|
+
await writeFile(tarballPath, tarball);
|
|
193
|
+
execSync2(`tar xzf "${tarballPath}" -C "${workdir}"`, { stdio: "pipe" });
|
|
194
|
+
const promptDir = join2(homedir(), ".vibe-interviewing", "prompts");
|
|
195
|
+
await mkdir(promptDir, { recursive: true });
|
|
196
|
+
const systemPromptPath = join2(promptDir, `${id}.md`);
|
|
197
|
+
await writeFile(systemPromptPath, systemPrompt);
|
|
198
|
+
return { workdir, systemPromptPath, metadata, id };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/network/cloud-client.ts
|
|
202
|
+
import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
203
|
+
import { join as join3 } from "path";
|
|
204
|
+
import { homedir as homedir2 } from "os";
|
|
205
|
+
import { execSync as execSync3 } from "child_process";
|
|
206
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
207
|
+
var DEFAULT_WORKER_URL = "https://api.vibe-interviewing.iar.dev";
|
|
208
|
+
function getWorkerUrl() {
|
|
209
|
+
return process.env["VIBE_WORKER_URL"] || DEFAULT_WORKER_URL;
|
|
210
|
+
}
|
|
211
|
+
var CloudError = class extends VibeError {
|
|
212
|
+
constructor(message) {
|
|
213
|
+
super(message, "CLOUD_ERROR", "Check your network connection and try again");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
async function uploadSession(workerUrl, metadata, systemPrompt, tarballPath, onProgress) {
|
|
217
|
+
onProgress?.("Reading workspace tarball...");
|
|
218
|
+
const tarballData = await readFile2(tarballPath);
|
|
219
|
+
onProgress?.("Uploading to cloud...");
|
|
220
|
+
const formData = new FormData();
|
|
221
|
+
formData.set("metadata", JSON.stringify(metadata));
|
|
222
|
+
formData.set("systemPrompt", systemPrompt);
|
|
223
|
+
formData.set(
|
|
224
|
+
"workspace",
|
|
225
|
+
new Blob([tarballData], { type: "application/gzip" }),
|
|
226
|
+
"workspace.tar.gz"
|
|
227
|
+
);
|
|
228
|
+
let response;
|
|
229
|
+
try {
|
|
230
|
+
response = await fetch(`${workerUrl}/sessions`, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: formData
|
|
233
|
+
});
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new CloudError(
|
|
236
|
+
`Failed to connect to cloud relay at ${workerUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const body = await response.text().catch(() => "unknown error");
|
|
241
|
+
throw new CloudError(`Cloud upload failed (${response.status}): ${body}`);
|
|
242
|
+
}
|
|
243
|
+
const result = await response.json();
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
async function downloadSessionFromCloud(workerUrl, code, targetDir, onProgress) {
|
|
247
|
+
const id = randomBytes2(4).toString("hex");
|
|
248
|
+
let rawCode = code.trim().toUpperCase();
|
|
249
|
+
if (rawCode.startsWith("VIBE-")) {
|
|
250
|
+
rawCode = rawCode.slice(5);
|
|
251
|
+
}
|
|
252
|
+
rawCode = rawCode.toLowerCase();
|
|
253
|
+
onProgress?.("Connecting to cloud relay...");
|
|
254
|
+
const metadataRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/metadata`);
|
|
255
|
+
const metadata = await metadataRes.json();
|
|
256
|
+
onProgress?.("Downloading scenario...");
|
|
257
|
+
const promptRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/system-prompt`);
|
|
258
|
+
const systemPrompt = await promptRes.text();
|
|
259
|
+
onProgress?.("Downloading workspace...");
|
|
260
|
+
const workspaceRes = await cloudFetch(`${workerUrl}/sessions/${rawCode}/workspace`);
|
|
261
|
+
const tarball = Buffer.from(await workspaceRes.arrayBuffer());
|
|
262
|
+
onProgress?.("Extracting workspace...");
|
|
263
|
+
const workdir = targetDir ?? join3(homedir2(), "vibe-sessions", `${metadata.scenarioName}-${id}`);
|
|
264
|
+
await mkdir2(workdir, { recursive: true });
|
|
265
|
+
const tarballPath = join3(workdir, "..", `${id}-download.tar.gz`);
|
|
266
|
+
await writeFile2(tarballPath, tarball);
|
|
267
|
+
execSync3(`tar xzf "${tarballPath}" -C "${workdir}"`, { stdio: "pipe" });
|
|
268
|
+
const promptDir = join3(homedir2(), ".vibe-interviewing", "prompts");
|
|
269
|
+
await mkdir2(promptDir, { recursive: true });
|
|
270
|
+
const systemPromptPath = join3(promptDir, `${id}.md`);
|
|
271
|
+
await writeFile2(systemPromptPath, systemPrompt);
|
|
272
|
+
return { workdir, systemPromptPath, metadata, id };
|
|
273
|
+
}
|
|
274
|
+
async function cloudFetch(url) {
|
|
275
|
+
let response;
|
|
276
|
+
try {
|
|
277
|
+
response = await fetch(url, { signal: AbortSignal.timeout(6e4) });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
280
|
+
throw new CloudError("Cloud download timed out");
|
|
281
|
+
}
|
|
282
|
+
throw new CloudError(
|
|
283
|
+
`Failed to connect to cloud relay: ${err instanceof Error ? err.message : String(err)}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (response.status === 404) {
|
|
287
|
+
throw new CloudError("Session not found or expired");
|
|
288
|
+
}
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
const body = await response.text().catch(() => "unknown error");
|
|
291
|
+
throw new CloudError(`Cloud request failed (${response.status}): ${body}`);
|
|
292
|
+
}
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
export {
|
|
296
|
+
CloudError,
|
|
297
|
+
DEFAULT_WORKER_URL,
|
|
298
|
+
InvalidSessionCodeError,
|
|
299
|
+
NetworkError,
|
|
300
|
+
SessionServer,
|
|
301
|
+
decodeSessionCode,
|
|
302
|
+
downloadSession,
|
|
303
|
+
downloadSessionFromCloud,
|
|
304
|
+
encodeSessionCode,
|
|
305
|
+
getWorkerUrl,
|
|
306
|
+
isCloudSessionCode,
|
|
307
|
+
uploadSession
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=index.js.map
|