Skip to content

Commit 1036d57

Browse files
committed
feat: Add Delete All Apps option to Danger Zone
- Add deleteAllApps IPC method and handler for deleting all apps - Update settings page with Delete All Apps button and confirmation dialog - Add loading spinners to danger zone buttons during operations - Fix TypeScript compilation errors for terminal output - Update django settings to use secure SECRET_KEY generation - Enhance backend framework support and full-stack app creation
1 parent 0c9a82a commit 1036d57

File tree

8 files changed

+221
-18
lines changed

8 files changed

+221
-18
lines changed

scaffold-backend/django/config/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
BASE_DIR = Path(__file__).resolve().parent.parent
55

6-
SECRET_KEY = os.environ.get('SECRET_KEY', 'CHANGE_THIS_SECRET_KEY_IN_PRODUCTION')
6+
from django.core.management.utils import get_random_secret_key
7+
SECRET_KEY = os.environ.get('SECRET_KEY', get_random_secret_key())
78

89
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
910

src/ipc/handlers/app_handlers.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,54 @@ export function registerAppHandlers() {
13961396
},
13971397
);
13981398

1399+
ipcMain.handle("delete-all-apps", async (): Promise<void> => {
1400+
logger.log("start: deleting all apps and their files.");
1401+
// Stop all running apps first
1402+
logger.log("stopping all running apps...");
1403+
const runningAppIds = Array.from(runningApps.keys());
1404+
for (const appId of runningAppIds) {
1405+
try {
1406+
const appInfo = runningApps.get(appId)!;
1407+
await stopAppByInfo(appId, appInfo);
1408+
} catch (error) {
1409+
logger.error(`Error stopping app ${appId} during delete all:`, error);
1410+
// Continue with deletion even if stopping fails
1411+
}
1412+
}
1413+
logger.log("all running apps stopped.");
1414+
1415+
// Get all apps
1416+
const allApps = await db.query.apps.findMany();
1417+
1418+
// Delete all apps from database
1419+
logger.log("deleting all apps from database...");
1420+
try {
1421+
await db.delete(apps);
1422+
// Note: Associated chats will cascade delete
1423+
} catch (error: any) {
1424+
logger.error("Error deleting all apps from database:", error);
1425+
throw new Error(`Failed to delete apps from database: ${error.message}`);
1426+
}
1427+
logger.log("all apps deleted from database.");
1428+
1429+
// Delete all app files
1430+
logger.log("deleting all app files...");
1431+
for (const app of allApps) {
1432+
const appPath = getDyadAppPath(app.path);
1433+
if (fs.existsSync(appPath)) {
1434+
try {
1435+
await fsPromises.rm(appPath, { recursive: true, force: true });
1436+
logger.log(`Deleted app files for ${app.name} at ${appPath}`);
1437+
} catch (error: any) {
1438+
logger.warn(`Error deleting app files for ${app.name}:`, error);
1439+
// Continue with other apps even if one fails
1440+
}
1441+
}
1442+
}
1443+
logger.log("all app files deleted.");
1444+
logger.log("delete all apps complete.");
1445+
});
1446+
13991447
ipcMain.handle(
14001448
"rename-app",
14011449
async (

src/ipc/handlers/createFromTemplate.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,21 +1172,47 @@ export async function setupBackendFramework(backendPath: string, framework: stri
11721172
logger.info(`Setting up ${framework} framework in ${backendPath}`);
11731173

11741174
try {
1175-
switch (framework) {
1176-
case 'django':
1177-
await setupDjango(backendPath);
1178-
break;
1179-
case 'fastapi':
1180-
await setupFastAPI(backendPath);
1181-
break;
1182-
case 'flask':
1183-
await setupFlask(backendPath);
1184-
break;
1185-
case 'nodejs':
1186-
await setupNodeJS(backendPath);
1187-
break;
1188-
default:
1189-
logger.warn(`Unknown backend framework: ${framework}`);
1175+
// Check if scaffold-backend exists for this framework
1176+
const scaffoldPath = path.join("/Volumes/Farhan/Desktop/AliFullstack", "scaffold-backend", framework);
1177+
1178+
if (fs.existsSync(scaffoldPath)) {
1179+
logger.info(`Found scaffold for ${framework} at ${scaffoldPath}, copying to ${backendPath}`);
1180+
1181+
// Copy the scaffold-backend directory to backendPath
1182+
await fs.copy(scaffoldPath, backendPath, {
1183+
overwrite: true,
1184+
filter: (src, dest) => {
1185+
// Exclude .DS_Store and other unwanted files
1186+
const relativePath = path.relative(scaffoldPath, src);
1187+
const shouldExclude = relativePath === '.DS_Store' || relativePath.includes('.git');
1188+
if (shouldExclude) {
1189+
logger.debug(`Excluding ${src} from copy`);
1190+
}
1191+
return !shouldExclude;
1192+
}
1193+
});
1194+
1195+
logger.info(`Successfully copied ${framework} scaffold from ${scaffoldPath} to ${backendPath}`);
1196+
} else {
1197+
logger.warn(`Scaffold not found for ${framework} at ${scaffoldPath}, falling back to programmatic setup`);
1198+
1199+
// Fallback to programmatic setup if scaffold doesn't exist
1200+
switch (framework) {
1201+
case 'django':
1202+
await setupDjango(backendPath);
1203+
break;
1204+
case 'fastapi':
1205+
await setupFastAPI(backendPath);
1206+
break;
1207+
case 'flask':
1208+
await setupFlask(backendPath);
1209+
break;
1210+
case 'nodejs':
1211+
await setupNodeJS(backendPath);
1212+
break;
1213+
default:
1214+
logger.warn(`Unknown backend framework: ${framework}`);
1215+
}
11901216
}
11911217

11921218
// Install dependencies after setting up the framework

src/ipc/ipc_client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,11 @@ export class IpcClient {
604604
await this.ipcRenderer.invoke("delete-app", { appId });
605605
}
606606

607+
// Delete all apps and all their files
608+
public async deleteAllApps(): Promise<void> {
609+
await this.ipcRenderer.invoke("delete-all-apps");
610+
}
611+
607612
// Rename an app (update name and path)
608613
public async renameApp({
609614
appId,

src/ipc/processors/response_processor.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getDyadExecuteSqlTags,
2929
getDyadRunBackendTerminalCmdTags,
3030
getDyadRunFrontendTerminalCmdTags,
31+
getDyadRunTerminalCmdTags,
3132
} from "../utils/dyad_tag_parser";
3233
import { runShellCommand } from "../utils/runShellCommand";
3334
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
@@ -125,6 +126,7 @@ export async function processFullResponseActions(
125126
: [];
126127
const dyadRunBackendTerminalCmdTags = getDyadRunBackendTerminalCmdTags(fullResponse);
127128
const dyadRunFrontendTerminalCmdTags = getDyadRunFrontendTerminalCmdTags(fullResponse);
129+
const dyadRunTerminalCmdTags = getDyadRunTerminalCmdTags(fullResponse);
128130

129131
const message = await db.query.messages.findFirst({
130132
where: and(
@@ -262,6 +264,47 @@ export async function processFullResponseActions(
262264
logger.log(`Executed ${dyadRunFrontendTerminalCmdTags.length} frontend terminal commands`);
263265
}
264266

267+
// Handle general terminal command tags
268+
if (dyadRunTerminalCmdTags.length > 0) {
269+
for (const cmdTag of dyadRunTerminalCmdTags) {
270+
try {
271+
const cwd = cmdTag.cwd ? path.join(appPath, cmdTag.cwd) : appPath;
272+
273+
logger.log(`Executing general terminal command: ${cmdTag.command} in ${cwd}`);
274+
275+
const result = await runShellCommand(`cd "${cwd}" && ${cmdTag.command}`);
276+
277+
if (result === null) {
278+
errors.push({
279+
message: `Terminal command failed: ${cmdTag.description || cmdTag.command}`,
280+
error: `Command execution failed in ${cwd}`,
281+
});
282+
// Add error to backend terminal
283+
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${cmdTag.description || cmdTag.command}`, "error");
284+
} else {
285+
logger.log(`Terminal command succeeded: ${cmdTag.description || cmdTag.command}`);
286+
287+
// Add command and result to backend terminal
288+
addTerminalOutput(chatWithApp.app.id, "backend", `$ ${cmdTag.command}`, "command");
289+
290+
if (result.trim()) {
291+
addTerminalOutput(chatWithApp.app.id, "backend", result, "output");
292+
}
293+
294+
addTerminalOutput(chatWithApp.app.id, "backend", `✅ ${cmdTag.description || cmdTag.command} completed successfully`, "success");
295+
}
296+
} catch (error) {
297+
errors.push({
298+
message: `Terminal command failed: ${cmdTag.description || cmdTag.command}`,
299+
error: error,
300+
});
301+
// Add error to backend terminal
302+
addTerminalOutput(chatWithApp.app.id, "backend", `❌ Error: ${error}`, "error");
303+
}
304+
}
305+
logger.log(`Executed ${dyadRunTerminalCmdTags.length} general terminal commands`);
306+
}
307+
265308
// Handle add dependency tags
266309
if (dyadAddDependencyPackages.length > 0) {
267310
try {

src/ipc/utils/dyad_tag_parser.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,32 @@ export function getDyadRunFrontendTerminalCmdTags(fullResponse: string): {
195195

196196
return commands;
197197
}
198+
199+
export function getDyadRunTerminalCmdTags(fullResponse: string): {
200+
command: string;
201+
cwd?: string;
202+
description?: string;
203+
}[] {
204+
const dyadRunTerminalCmdRegex =
205+
/<run_terminal_cmd([^>]*)>([\s\S]*?)<\/run_terminal_cmd>/g;
206+
const cwdRegex = /cwd="([^"]+)"/;
207+
const descriptionRegex = /description="([^"]+)"/;
208+
209+
let match;
210+
const commands: { command: string; cwd?: string; description?: string }[] = [];
211+
212+
while ((match = dyadRunTerminalCmdRegex.exec(fullResponse)) !== null) {
213+
const attributesString = match[1];
214+
const command = match[2].trim();
215+
216+
const cwdMatch = cwdRegex.exec(attributesString);
217+
const descriptionMatch = descriptionRegex.exec(attributesString);
218+
219+
const cwd = cwdMatch?.[1];
220+
const description = descriptionMatch?.[1];
221+
222+
commands.push({ command, cwd, description });
223+
}
224+
225+
return commands;
226+
}

src/pages/settings.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ThinkingBudgetSelector } from "@/components/ThinkingBudgetSelector";
1111
import { useSettings } from "@/hooks/useSettings";
1212
import { useAppVersion } from "@/hooks/useAppVersion";
1313
import { Button } from "@/components/ui/button";
14-
import { ArrowLeft } from "lucide-react";
14+
import { ArrowLeft, Loader2 } from "lucide-react";
1515
import { useRouter } from "@tanstack/react-router";
1616
import { GitHubIntegration } from "@/components/GitHubIntegration";
1717
import { VercelIntegration } from "@/components/VercelIntegration";
@@ -28,6 +28,8 @@ import { RuntimeModeSelector } from "@/components/RuntimeModeSelector";
2828
export default function SettingsPage() {
2929
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
3030
const [isResetting, setIsResetting] = useState(false);
31+
const [isDeleteAllAppsDialogOpen, setIsDeleteAllAppsDialogOpen] = useState(false);
32+
const [isDeletingAllApps, setIsDeletingAllApps] = useState(false);
3133
const appVersion = useAppVersion();
3234
const { settings, updateSettings } = useSettings();
3335
const router = useRouter();
@@ -49,6 +51,23 @@ export default function SettingsPage() {
4951
}
5052
};
5153

54+
const handleDeleteAllApps = async () => {
55+
setIsDeletingAllApps(true);
56+
try {
57+
const ipcClient = IpcClient.getInstance();
58+
await ipcClient.deleteAllApps();
59+
showSuccess("Successfully deleted all apps.");
60+
} catch (error) {
61+
console.error("Error deleting all apps:", error);
62+
showError(
63+
error instanceof Error ? error.message : "An unknown error occurred",
64+
);
65+
} finally {
66+
setIsDeletingAllApps(false);
67+
setIsDeleteAllAppsDialogOpen(false);
68+
}
69+
};
70+
5271
return (
5372
<div className="min-h-screen px-8 py-4">
5473
<div className="max-w-5xl mx-auto">
@@ -169,6 +188,26 @@ export default function SettingsPage() {
169188
</h2>
170189

171190
<div className="space-y-4">
191+
<div className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4">
192+
<div>
193+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
194+
Delete All Apps
195+
</h3>
196+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
197+
This will delete all your apps and their files. This action
198+
cannot be undone.
199+
</p>
200+
</div>
201+
<button
202+
onClick={() => setIsDeleteAllAppsDialogOpen(true)}
203+
disabled={isDeletingAllApps}
204+
className="rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
205+
>
206+
{isDeletingAllApps && <Loader2 className="h-4 w-4 animate-spin" />}
207+
{isDeletingAllApps ? "Deleting..." : "Delete All Apps"}
208+
</button>
209+
</div>
210+
172211
<div className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4">
173212
<div>
174213
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
@@ -182,8 +221,9 @@ export default function SettingsPage() {
182221
<button
183222
onClick={() => setIsResetDialogOpen(true)}
184223
disabled={isResetting}
185-
className="rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
224+
className="rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
186225
>
226+
{isResetting && <Loader2 className="h-4 w-4 animate-spin" />}
187227
{isResetting ? "Resetting..." : "Reset Everything"}
188228
</button>
189229
</div>
@@ -192,6 +232,16 @@ export default function SettingsPage() {
192232
</div>
193233
</div>
194234

235+
<ConfirmationDialog
236+
isOpen={isDeleteAllAppsDialogOpen}
237+
title="Delete All Apps"
238+
message="Are you sure you want to delete all apps? This will delete all your apps and their files. This action cannot be undone."
239+
confirmText="Delete All Apps"
240+
cancelText="Cancel"
241+
onConfirm={handleDeleteAllApps}
242+
onCancel={() => setIsDeleteAllAppsDialogOpen(false)}
243+
/>
244+
195245
<ConfirmationDialog
196246
isOpen={isResetDialogOpen}
197247
title="Reset Everything"

src/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const validInvokeChannels = [
4242
"checkout-version",
4343
"get-current-branch",
4444
"delete-app",
45+
"delete-all-apps",
4546
"rename-app",
4647
"get-user-settings",
4748
"set-user-settings",

0 commit comments

Comments
 (0)