Implement Phase 1: Foundation

- Set up TypeScript project with strict mode
- Configure tsup build system (ESM output)
- Configure vitest for testing (36 tests passing)
- Configure ESLint with flat config for TypeScript
- Implement Commander.js CLI with all 4 commands:
  - init: Configure API keys with validation
  - new: Create new Ralph Method project (stub)
  - validate: Validate PROMPT.md files
  - research: Research topics via Perplexity (stub)
- Implement config management:
  - Keys stored in ~/.ralph-generator/config.json
  - File permissions set to 600
  - Environment variables override file config
- Implement logging utility with verbosity levels
- Implement atomic file writes utility

All Phase 1 acceptance criteria met.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-01-10 12:08:24 +00:00
parent 688cfe57ed
commit 606f27d6bb
22 changed files with 5262 additions and 6 deletions

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { join } from 'path';
import { mkdir, rm, writeFile as fsWriteFile, readFile } from 'fs/promises';
import { tmpdir } from 'os';
import { fileExists, readFileContent, writeFileAtomic, readJsonFile, writeJsonFile, ensureDir } from '../utils/files.js';
describe('File Utilities', () => {
const testDir = join(tmpdir(), `ralph-files-test-${Date.now()}`);
beforeEach(async () => {
await mkdir(testDir, { recursive: true });
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
});
describe('ensureDir', () => {
it('creates directory if it does not exist', async () => {
const newDir = join(testDir, 'new', 'nested', 'dir');
await ensureDir(newDir);
expect(await fileExists(newDir)).toBe(true);
});
it('does not throw if directory exists', async () => {
await expect(ensureDir(testDir)).resolves.not.toThrow();
});
});
describe('fileExists', () => {
it('returns true for existing file', async () => {
const filePath = join(testDir, 'test.txt');
await fsWriteFile(filePath, 'test');
expect(await fileExists(filePath)).toBe(true);
});
it('returns false for non-existing file', async () => {
expect(await fileExists(join(testDir, 'nonexistent.txt'))).toBe(false);
});
});
describe('readFileContent', () => {
it('reads file content', async () => {
const filePath = join(testDir, 'read-test.txt');
await fsWriteFile(filePath, 'hello world');
const content = await readFileContent(filePath);
expect(content).toBe('hello world');
});
});
describe('writeFileAtomic', () => {
it('writes file atomically', async () => {
const filePath = join(testDir, 'atomic.txt');
await writeFileAtomic(filePath, 'atomic content');
const content = await readFile(filePath, 'utf-8');
expect(content).toBe('atomic content');
});
it('creates parent directories', async () => {
const filePath = join(testDir, 'new', 'deep', 'file.txt');
await writeFileAtomic(filePath, 'nested');
expect(await fileExists(filePath)).toBe(true);
});
});
describe('readJsonFile', () => {
it('reads and parses JSON file', async () => {
const filePath = join(testDir, 'data.json');
await fsWriteFile(filePath, JSON.stringify({ name: 'test', value: 42 }));
const data = await readJsonFile<{ name: string; value: number }>(filePath);
expect(data.name).toBe('test');
expect(data.value).toBe(42);
});
});
describe('writeJsonFile', () => {
it('writes JSON file with formatting', async () => {
const filePath = join(testDir, 'output.json');
await writeJsonFile(filePath, { key: 'value' });
const content = await readFile(filePath, 'utf-8');
expect(content).toContain('"key"');
expect(content).toContain('"value"');
// Check it's formatted (has newlines)
expect(content).toContain('\n');
});
});
});