schrod-cli
Version:
AI-driven application framework CLI that displays hierarchical Schrödinger project structure for systematic development workflow
306 lines (254 loc) • 9.62 kB
text/typescript
// Schröd service for managing nodes and configuration
import * as path from 'path';
import { SchrodNode, SchrodConfig, SchrodLevel, SchrodDomain, SchrodNodeType, SchrodNodeStatus } from '../types';
import { FileSystemService } from './fileSystemService';
import { DependencyService } from './dependencyService';
import { ExecutionService } from './executionService';
export class SchrodService {
private workspaceRoot: string = '';
private schrodRoot: string = '';
private nodes: Map<string, SchrodNode> = new Map();
private config: SchrodConfig | null = null;
constructor(
private fileSystemService: FileSystemService,
private dependencyService: DependencyService,
private executionService: ExecutionService
) {}
async initialize(workspaceRoot: string): Promise<void> {
this.workspaceRoot = workspaceRoot;
this.schrodRoot = path.join(workspaceRoot, '.schrod');
// Load config
await this.loadConfig();
// Scan for Schröd nodes
await this.scanSchrodNodes();
}
async initializeProject(projectName: string): Promise<void> {
if (!this.workspaceRoot) {
throw new Error('No workspace root found');
}
// Create .schrod directory
await this.fileSystemService.createDirectory(this.schrodRoot);
// Create default config
const config: SchrodConfig = {
version: '1.0.0',
projectName,
defaultAI: 'claude-sonnet-4',
aiOverrides: {
'**/*@Schröd.ui/**': 'claude-haiku',
'**/*@Schröd.logic/**': 'claude-sonnet-4',
'**/test*': 'gpt-4'
},
outputDir: 'src',
created: new Date().toISOString(),
lastModified: new Date().toISOString()
};
await this.fileSystemService.writeJson(
path.join(this.schrodRoot, 'config.json'),
config
);
this.config = config;
// Create initial app node
const appNodePath = path.join(this.schrodRoot, `${projectName}öd`);
await this.fileSystemService.createDirectory(appNodePath);
await this.fileSystemService.writeFile(
path.join(appNodePath, 'idea.md'),
`# ${projectName}\n\n## Overview\n\nDescribe your application idea here.\n`
);
await this.scanSchrodNodes();
}
async createAppSpec(idea: string): Promise<void> {
const appNode = this.getAppNode();
if (!appNode) {
throw new Error('No app node found. Initialize project first.');
}
const appPath = path.join(this.schrodRoot, appNode.name);
await this.fileSystemService.writeFile(
path.join(appPath, 'idea.md'),
`# ${this.config?.projectName}\n\n## Overview\n\n${idea}\n`
);
await this.fileSystemService.writeFile(
path.join(appPath, 'requirements.md'),
`# Requirements\n\n## Functional Requirements\n\n- TODO: Add functional requirements\n\n## Non-Functional Requirements\n\n- TODO: Add non-functional requirements\n`
);
}
async decomposeToUILogic(): Promise<void> {
const appNode = this.getAppNode();
if (!appNode) {
throw new Error('No app node found');
}
// Create UI domain
const uiPath = path.join(this.schrodRoot, 'Schröd.ui');
await this.fileSystemService.createDirectory(uiPath);
await this.fileSystemService.writeFile(
path.join(uiPath, 'architecture.md'),
'# UI Architecture\n\n## Overview\n\nDescribe the UI architecture here.\n'
);
// Create Logic domain
const logicPath = path.join(this.schrodRoot, 'Schröd.logic');
await this.fileSystemService.createDirectory(logicPath);
await this.fileSystemService.writeFile(
path.join(logicPath, 'architecture.md'),
'# Logic Architecture\n\n## Overview\n\nDescribe the business logic architecture here.\n'
);
// Create Test domain
const testPath = path.join(this.schrodRoot, 'Schröd.test');
await this.fileSystemService.createDirectory(testPath);
await this.fileSystemService.writeFile(
path.join(testPath, 'architecture.md'),
'# Test Architecture\n\n## Overview\n\nDescribe the testing strategy here.\n'
);
await this.scanSchrodNodes();
}
async addFeature(featureName: string, domain: SchrodDomain): Promise<void> {
const domainPath = path.join(this.schrodRoot, `Schröd.${domain}`);
const featurePath = path.join(domainPath, `${featureName}öd.${domain}`);
await this.fileSystemService.createDirectory(featurePath);
await this.fileSystemService.writeFile(
path.join(featurePath, 'design.md'),
`# ${featureName} Design\n\n## Overview\n\nDescribe the ${featureName} feature design here.\n`
);
await this.scanSchrodNodes();
}
async createTickets(nodePath: string): Promise<void> {
const node = this.nodes.get(nodePath);
if (!node || node.level !== SchrodLevel.Feature) {
throw new Error('Can only create tickets for feature-level nodes');
}
// This is a placeholder - in real implementation, this would use AI
// to analyze the feature and create appropriate tickets
const ticketName = 'implementation';
const ticketPath = path.join(
this.schrodRoot,
nodePath,
`${ticketName}öd.ticket`
);
await this.fileSystemService.createDirectory(ticketPath);
await this.fileSystemService.writeFile(
path.join(ticketPath, 'spec.md'),
`# ${ticketName} Specification\n\n## Implementation Details\n\nTODO: Add implementation details\n`
);
await this.fileSystemService.writeJson(
path.join(ticketPath, 'status.json'),
{
status: SchrodNodeStatus.Pending,
dependencies: [],
outputs: [],
checkpoints: []
}
);
await this.scanSchrodNodes();
}
async runNode(nodePath: string): Promise<void> {
const node = this.nodes.get(nodePath);
if (!node) {
throw new Error(`Node not found: ${nodePath}`);
}
await this.executionService.execute(node, this.config!);
await this.scanSchrodNodes();
}
async getNodeStatus(nodePath: string): Promise<SchrodNodeStatus> {
const node = this.nodes.get(nodePath);
return node?.status || SchrodNodeStatus.Pending;
}
getNodes(): Map<string, SchrodNode> {
return new Map(this.nodes);
}
getConfig(): SchrodConfig | null {
return this.config;
}
private async loadConfig(): Promise<void> {
const configPath = path.join(this.schrodRoot, 'config.json');
if (await this.fileSystemService.exists(configPath)) {
this.config = await this.fileSystemService.readJson<SchrodConfig>(configPath);
}
}
private async scanSchrodNodes(): Promise<void> {
this.nodes.clear();
if (!await this.fileSystemService.exists(this.schrodRoot)) {
return;
}
await this.scanDirectory(this.schrodRoot, '');
this.dependencyService.buildGraph(this.nodes);
}
private async scanDirectory(dirPath: string, parentPath: string): Promise<void> {
const entries = await this.fileSystemService.readDirectory(dirPath);
for (const entry of entries) {
if (entry.includes('@Schröd')) {
const fullPath = path.join(dirPath, entry);
const relativePath = path.relative(this.schrodRoot, fullPath);
const node = this.parseSchrodNode(entry, relativePath, parentPath);
if (node) {
this.nodes.set(relativePath, node);
// Load status for tickets
if (node.type === SchrodNodeType.Ticket) {
const statusPath = path.join(fullPath, 'status.json');
if (await this.fileSystemService.exists(statusPath)) {
const status = await this.fileSystemService.readJson<any>(statusPath);
node.status = status.status || SchrodNodeStatus.Pending;
}
}
// Recursively scan subdirectories
if (await this.fileSystemService.isDirectory(fullPath)) {
await this.scanDirectory(fullPath, relativePath);
}
}
}
}
}
private parseSchrodNode(name: string, path: string, parent: string): SchrodNode | null {
const match = name.match(/^(.+)öd(?:\.(\w+))?$/);
if (!match) {
return null;
}
const [, , domainOrType] = match;
const level = this.getNodeLevel(path);
const domain = this.getNodeDomain(domainOrType);
const type = this.getNodeType(level, domainOrType);
return {
path,
name,
level,
domain,
type,
status: SchrodNodeStatus.Pending,
parent: parent || undefined,
children: [],
dependencies: []
};
}
private getNodeLevel(path: string): SchrodLevel {
const depth = path.split('/').length;
return Math.min(depth, 4) as SchrodLevel;
}
private getNodeDomain(domainOrType?: string): SchrodDomain | null {
if (!domainOrType) {
return null;
}
if (Object.values(SchrodDomain).includes(domainOrType as SchrodDomain)) {
return domainOrType as SchrodDomain;
}
return null;
}
private getNodeType(level: SchrodLevel, domainOrType?: string): SchrodNodeType {
if (domainOrType === 'ticket') {
return SchrodNodeType.Ticket;
}
switch (level) {
case SchrodLevel.App:
return SchrodNodeType.App;
case SchrodLevel.Architecture:
return SchrodNodeType.Domain;
case SchrodLevel.Feature:
return SchrodNodeType.Feature;
case SchrodLevel.Ticket:
return SchrodNodeType.Ticket;
default:
return SchrodNodeType.Feature;
}
}
private getAppNode(): SchrodNode | undefined {
return Array.from(this.nodes.values()).find(
node => node.type === SchrodNodeType.App
);
}
}