Skip to content

Commit 0e79b53

Browse files
committed
feat: Implement backend framework options for Hub
- Add support for backend frameworks (Django, FastAPI, Flask, Node.js) in Hub - Enhance tag parsing to handle write_to_file and search_replace tags - Update response processor to process nested dyad-write tags - Add autonomous development orchestrator for app creation flow - Update AI_RULES.md files with proper tag format instructions - Fix TypeScript compilation errors in app handlers - Integrate backend framework scaffolding with app creation process
1 parent 6f61b51 commit 0e79b53

File tree

8 files changed

+974
-10
lines changed

8 files changed

+974
-10
lines changed

scaffold-backend/django/AI_RULES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
- Follow Django best practices and conventions.
55
- Always put source code in the appropriate Django app folders.
66

7+
## File Operations
8+
- Use `<write_to_file>` tags to create new files with their complete content
9+
- Use `<search_replace>` tags to modify existing files
10+
- Use `<run_terminal_cmd>` tags to execute Django management commands (migrations, startapp, etc.)
11+
- Always provide complete file content, not partial updates
12+
713
## Project Structure
814
- `config/`: Main Django project directory containing settings
915
- `apps/`: Django applications directory

scaffold-backend/fastapi/AI_RULES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
- Follow FastAPI best practices and async/await patterns.
55
- Always put source code in appropriate modules and packages.
66

7+
## File Operations
8+
- Use `<write_to_file>` tags to create new files with their complete content
9+
- Use `<search_replace>` tags to modify existing files
10+
- Use `<run_terminal_cmd>` tags to execute FastAPI development commands
11+
- Always provide complete file content, not partial updates
12+
713
## Project Structure
814
- `main.py`: Main FastAPI application entry point
915
- `requirements.txt`: Python dependencies

scaffold-backend/flask/AI_RULES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
- Follow Flask best practices and patterns.
55
- Always put source code in appropriate modules and packages.
66

7+
## File Operations
8+
- Use `<write_to_file>` tags to create new files with their complete content
9+
- Use `<search_replace>` tags to modify existing files
10+
- Use `<run_terminal_cmd>` tags to execute Flask development commands
11+
- Always provide complete file content, not partial updates
12+
713
## Project Structure
814
- `run.py`: Main Flask application entry point
915
- `requirements.txt`: Python dependencies

scaffold-backend/nodejs/AI_RULES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
- Follow Node.js best practices and Express.js patterns.
55
- Always put source code in appropriate modules and directories.
66

7+
## File Operations
8+
- Use `<write_to_file>` tags to create new files with their complete content
9+
- Use `<search_replace>` tags to modify existing files
10+
- Use `<run_terminal_cmd>` tags to execute Node.js development commands
11+
- Always provide complete file content, not partial updates
12+
713
## Project Structure
814
- `server.js`: Main Express application entry point
915
- `package.json`: Node.js dependencies and scripts

src/ipc/handlers/app_handlers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { getVercelTeamSlug } from "../utils/vercel_utils";
5353
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
5454
import { AppSearchResult } from "@/lib/schemas";
5555
import { CreateMissingFolderParams } from "../ipc_types";
56+
import { developmentOrchestrator } from "../utils/development_orchestrator";
5657

5758
const DEFAULT_COMMAND =
5859
"(node -e \"try { const pkg = require('./package.json'); if (pkg.dependencies && pkg.dependencies['@SFARPak/react-vite-component-tagger']) { delete pkg.dependencies['@SFARPak/react-vite-component-tagger']; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); } if (pkg.devDependencies && pkg.devDependencies['@SFARPak/react-vite-component-tagger']) { delete pkg.devDependencies['@SFARPak/react-vite-component-tagger']; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); } } catch(e) {}; try { const fs = require('fs'); if (fs.existsSync('./vite.config.ts')) { let config = fs.readFileSync('./vite.config.ts', 'utf8'); config = config.replace(/import.*@SFARPak\\/react-vite-component-tagger.*;\\s*/g, ''); config = config.replace(/dyadComponentTagger[^}]*},?\\s*/g, ''); config = config.replace(/applyComponentTagger[^}]*},?\\s*/g, ''); fs.writeFileSync('./vite.config.ts', config); } } catch(e) {}\" && pnpm install && pnpm run dev --port 32100) || (node -e \"try { const pkg = require('./package.json'); if (pkg.dependencies && pkg.dependencies['@SFARPak/react-vite-component-tagger']) { delete pkg.dependencies['@SFARPak/react-vite-component-tagger']; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); } if (pkg.devDependencies && pkg.devDependencies['@SFARPak/react-vite-component-tagger']) { delete pkg.devDependencies['@SFARPak/react-vite-component-tagger']; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); } } catch(e) {}; try { const fs = require('fs'); if (fs.existsSync('./vite.config.ts')) { let config = fs.readFileSync('./vite.config.ts', 'utf8'); config = config.replace(/import.*@SFARPak\\/react-vite-component-tagger.*;\\s*/g, ''); config = config.replace(/dyadComponentTagger[^}]*},?\\s*/g, ''); config = config.replace(/applyComponentTagger[^}]*},?\\s*/g, ''); fs.writeFileSync('./vite.config.ts', config); } } catch(e) {}\" && npm install --legacy-peer-deps && npm run dev -- --port 32100)";
@@ -666,6 +667,23 @@ export function registerAppHandlers() {
666667
logger.warn(`App ${app.id} created without Git repository`);
667668
}
668669

670+
// Start autonomous development process
671+
try {
672+
logger.info(`Starting autonomous development for app ${app.id}`);
673+
const requirements: string[] = []; // Requirements will be gathered during development
674+
developmentOrchestrator.startAutonomousDevelopment(
675+
app.id,
676+
"react", // default frontend framework
677+
params.selectedBackendFramework || undefined,
678+
requirements
679+
);
680+
logger.info(`Autonomous development started for app ${app.id}`);
681+
} catch (devError) {
682+
logger.error(`Failed to start autonomous development for app ${app.id}:`, devError);
683+
// Don't fail app creation if autonomous development fails to start
684+
logger.warn(`App ${app.id} created but autonomous development failed to start`);
685+
}
686+
669687
return { app, chatId: chat.id };
670688
},
671689
);

src/ipc/processors/response_processor.ts

Lines changed: 149 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
getDyadRunBackendTerminalCmdTags,
3030
getDyadRunFrontendTerminalCmdTags,
3131
getDyadRunTerminalCmdTags,
32+
getWriteToFileTags,
33+
getSearchReplaceTags,
3234
} from "../utils/dyad_tag_parser";
3335
import { runShellCommand } from "../utils/runShellCommand";
3436
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
@@ -118,6 +120,8 @@ export async function processFullResponseActions(
118120
try {
119121
// Extract all tags
120122
const dyadWriteTags = getDyadWriteTags(fullResponse);
123+
const writeToFileTags = getWriteToFileTags(fullResponse);
124+
const searchReplaceTags = getSearchReplaceTags(fullResponse);
121125
const dyadRenameTags = getDyadRenameTags(fullResponse);
122126
const dyadDeletePaths = getDyadDeleteTags(fullResponse);
123127
const dyadAddDependencyPackages = getDyadAddDependencyTags(fullResponse);
@@ -456,12 +460,108 @@ export async function processFullResponseActions(
456460
}
457461
}
458462

459-
// Process all file writes
463+
// Process all file writes (dyad-write tags)
460464
for (const tag of dyadWriteTags) {
461465
const filePath = tag.path;
462466
let content: string | Buffer = tag.content;
463467
const fullFilePath = safeJoin(appPath, filePath);
464468

469+
// Check if this is a search_replace operation
470+
if (typeof content === "string" && content.startsWith("SEARCH_REPLACE:")) {
471+
// Handle search_replace operation
472+
const parts = content.split(":");
473+
if (parts.length >= 3) {
474+
const oldString = parts[1];
475+
const newString = parts.slice(2).join(":");
476+
477+
if (fs.existsSync(fullFilePath)) {
478+
try {
479+
let fileContent = fs.readFileSync(fullFilePath, 'utf8');
480+
481+
if (fileContent.includes(oldString)) {
482+
fileContent = fileContent.replace(oldString, newString);
483+
fs.writeFileSync(fullFilePath, fileContent);
484+
logger.log(`Successfully applied search_replace to file: ${fullFilePath}`);
485+
writtenFiles.push(filePath);
486+
} else {
487+
logger.warn(`Old string not found in file for search_replace: ${fullFilePath}`);
488+
warnings.push({
489+
message: `Search string not found in file: ${filePath}`,
490+
error: null,
491+
});
492+
}
493+
} catch (error) {
494+
logger.error(`Failed to apply search_replace to file: ${fullFilePath}`, error);
495+
errors.push({
496+
message: `Failed to apply search_replace to file: ${filePath}`,
497+
error: error,
498+
});
499+
}
500+
} else {
501+
logger.warn(`File not found for search_replace: ${fullFilePath}`);
502+
warnings.push({
503+
message: `File not found for search_replace: ${filePath}`,
504+
error: null,
505+
});
506+
}
507+
}
508+
} else {
509+
// Handle regular file write
510+
// Check if content (stripped of whitespace) exactly matches a file ID and replace with actual file content
511+
if (fileUploadsMap) {
512+
const trimmedContent = tag.content.trim();
513+
const fileInfo = fileUploadsMap.get(trimmedContent);
514+
if (fileInfo) {
515+
try {
516+
const fileContent = await readFile(fileInfo.filePath);
517+
content = fileContent;
518+
logger.log(
519+
`Replaced file ID ${trimmedContent} with content from ${fileInfo.originalName}`,
520+
);
521+
} catch (error) {
522+
logger.error(
523+
`Failed to read uploaded file ${fileInfo.originalName}:`,
524+
error,
525+
);
526+
errors.push({
527+
message: `Failed to read uploaded file: ${fileInfo.originalName}`,
528+
error: error,
529+
});
530+
}
531+
}
532+
}
533+
534+
// Ensure directory exists
535+
const dirPath = path.dirname(fullFilePath);
536+
fs.mkdirSync(dirPath, { recursive: true });
537+
538+
// Write file content
539+
fs.writeFileSync(fullFilePath, content);
540+
logger.log(`Successfully wrote file: ${fullFilePath}`);
541+
writtenFiles.push(filePath);
542+
if (isServerFunction(filePath) && typeof content === "string") {
543+
try {
544+
await deploySupabaseFunctions({
545+
supabaseProjectId: chatWithApp.app.supabaseProjectId!,
546+
functionName: path.basename(path.dirname(filePath)),
547+
content: content,
548+
});
549+
} catch (error) {
550+
errors.push({
551+
message: `Failed to deploy Supabase function: ${filePath}`,
552+
error: error,
553+
});
554+
}
555+
}
556+
}
557+
}
558+
559+
// Process write_to_file tags
560+
for (const tag of writeToFileTags) {
561+
const filePath = tag.path;
562+
let content: string | Buffer = tag.content;
563+
const fullFilePath = safeJoin(appPath, filePath);
564+
465565
// Check if content (stripped of whitespace) exactly matches a file ID and replace with actual file content
466566
if (fileUploadsMap) {
467567
const trimmedContent = tag.content.trim();
@@ -492,7 +592,7 @@ export async function processFullResponseActions(
492592

493593
// Write file content
494594
fs.writeFileSync(fullFilePath, content);
495-
logger.log(`Successfully wrote file: ${fullFilePath}`);
595+
logger.log(`Successfully wrote file via write_to_file tag: ${fullFilePath}`);
496596
writtenFiles.push(filePath);
497597
if (isServerFunction(filePath) && typeof content === "string") {
498598
try {
@@ -510,12 +610,55 @@ export async function processFullResponseActions(
510610
}
511611
}
512612

613+
// Process search_replace tags
614+
for (const tag of searchReplaceTags) {
615+
const filePath = tag.file;
616+
const fullFilePath = safeJoin(appPath, filePath);
617+
618+
if (fs.existsSync(fullFilePath)) {
619+
try {
620+
let fileContent = fs.readFileSync(fullFilePath, 'utf8');
621+
622+
// Replace old_string with new_string
623+
const oldString = tag.old_string.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
624+
const newString = tag.new_string.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
625+
626+
if (fileContent.includes(oldString)) {
627+
fileContent = fileContent.replace(oldString, newString);
628+
fs.writeFileSync(fullFilePath, fileContent);
629+
logger.log(`Successfully applied search_replace to file: ${fullFilePath}`);
630+
writtenFiles.push(filePath);
631+
} else {
632+
logger.warn(`Old string not found in file for search_replace: ${fullFilePath}`);
633+
warnings.push({
634+
message: `Search string not found in file: ${filePath}`,
635+
error: null,
636+
});
637+
}
638+
} catch (error) {
639+
logger.error(`Failed to apply search_replace to file: ${fullFilePath}`, error);
640+
errors.push({
641+
message: `Failed to apply search_replace to file: ${filePath}`,
642+
error: error,
643+
});
644+
}
645+
} else {
646+
logger.warn(`File not found for search_replace: ${fullFilePath}`);
647+
warnings.push({
648+
message: `File not found for search_replace: ${filePath}`,
649+
error: null,
650+
});
651+
}
652+
}
653+
513654
// If we have any file changes, commit them all at once
514655
hasChanges =
515656
writtenFiles.length > 0 ||
516657
renamedFiles.length > 0 ||
517658
deletedFiles.length > 0 ||
518-
dyadAddDependencyPackages.length > 0;
659+
dyadAddDependencyPackages.length > 0 ||
660+
writeToFileTags.length > 0 ||
661+
searchReplaceTags.length > 0;
519662

520663
let uncommittedFiles: string[] = [];
521664
let extraFilesError: string | undefined;
@@ -545,13 +688,13 @@ export async function processFullResponseActions(
545688
if (dyadExecuteSqlQueries.length > 0)
546689
changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
547690

548-
let message = chatSummary
691+
const commitMessage = chatSummary
549692
? `[alifullstack] ${chatSummary} - ${changes.join(", ")}`
550693
: `[alifullstack] ${changes.join(", ")}`;
551694
// Use chat summary, if provided, or default for commit message
552695
let commitHash = await gitCommit({
553696
path: appPath,
554-
message,
697+
message: commitMessage,
555698
});
556699
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
557700

@@ -571,7 +714,7 @@ export async function processFullResponseActions(
571714
try {
572715
commitHash = await gitCommit({
573716
path: appPath,
574-
message: message + " + extra files edited outside of AliFullStack",
717+
message: commitMessage + " + extra files edited outside of AliFullStack",
575718
amend: true,
576719
});
577720
logger.log(

0 commit comments

Comments
 (0)