feat: allow custom install and start commands (#892)
# Description Gives the ability to define an `install` and `startup` command when importing a project, so we can work on a project locally without any issue. # Preview <img width="2256" height="1422" alt="image" src="https://github.com/user-attachments/assets/2132b1cb-5f71-4b88-84db-8ecc81cf1f66" /> --------- Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
@@ -27,6 +27,12 @@ import {
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
|
||||
interface ImportAppDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -39,6 +45,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
const [customAppName, setCustomAppName] = useState<string>("");
|
||||
const [nameExists, setNameExists] = useState<boolean>(false);
|
||||
const [isCheckingName, setIsCheckingName] = useState<boolean>(false);
|
||||
const [installCommand, setInstallCommand] = useState("pnpm install");
|
||||
const [startCommand, setStartCommand] = useState("pnpm dev");
|
||||
const navigate = useNavigate();
|
||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||
const { refreshApps } = useLoadApps();
|
||||
@@ -89,6 +97,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
return IpcClient.getInstance().importApp({
|
||||
path: selectedPath,
|
||||
appName: customAppName,
|
||||
installCommand: installCommand || undefined,
|
||||
startCommand: startCommand || undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: async (result) => {
|
||||
@@ -128,6 +138,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
setHasAiRules(null);
|
||||
setCustomAppName("");
|
||||
setNameExists(false);
|
||||
setInstallCommand("pnpm install");
|
||||
setStartCommand("pnpm dev");
|
||||
};
|
||||
|
||||
const handleAppNameChange = async (
|
||||
@@ -140,6 +152,10 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const hasInstallCommand = installCommand.trim().length > 0;
|
||||
const hasStartCommand = startCommand.trim().length > 0;
|
||||
const commandsValid = hasInstallCommand === hasStartCommand;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
@@ -221,6 +237,41 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="advanced-options">
|
||||
<AccordionTrigger className="text-sm hover:no-underline">
|
||||
Advanced options
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm ml-2 mb-2">
|
||||
Install command
|
||||
</Label>
|
||||
<Input
|
||||
value={installCommand}
|
||||
onChange={(e) => setInstallCommand(e.target.value)}
|
||||
placeholder="pnpm install"
|
||||
disabled={importAppMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm ml-2 mb-2">Start command</Label>
|
||||
<Input
|
||||
value={startCommand}
|
||||
onChange={(e) => setStartCommand(e.target.value)}
|
||||
placeholder="pnpm dev"
|
||||
disabled={importAppMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
{!commandsValid && (
|
||||
<p className="text-sm text-red-500">
|
||||
Both commands are required when customizing.
|
||||
</p>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{hasAiRules === false && (
|
||||
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
|
||||
<TooltipProvider>
|
||||
@@ -264,7 +315,10 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={
|
||||
!selectedPath || importAppMutation.isPending || nameExists
|
||||
!selectedPath ||
|
||||
importAppMutation.isPending ||
|
||||
nameExists ||
|
||||
!commandsValid
|
||||
}
|
||||
className="min-w-[80px]"
|
||||
>
|
||||
|
||||
@@ -23,6 +23,8 @@ export const apps = sqliteTable("apps", {
|
||||
vercelProjectName: text("vercel_project_name"),
|
||||
vercelTeamId: text("vercel_team_id"),
|
||||
vercelDeploymentUrl: text("vercel_deployment_url"),
|
||||
installCommand: text("install_command"),
|
||||
startCommand: text("start_command"),
|
||||
chatContext: text("chat_context", { mode: "json" }),
|
||||
});
|
||||
|
||||
|
||||
@@ -83,17 +83,28 @@ async function executeApp({
|
||||
appId,
|
||||
event, // Keep event for local-node case
|
||||
isNeon,
|
||||
installCommand,
|
||||
startCommand,
|
||||
}: {
|
||||
appPath: string;
|
||||
appId: number;
|
||||
event: Electron.IpcMainInvokeEvent;
|
||||
isNeon: boolean;
|
||||
installCommand?: string | null;
|
||||
startCommand?: string | null;
|
||||
}): Promise<void> {
|
||||
if (proxyWorker) {
|
||||
proxyWorker.terminate();
|
||||
proxyWorker = null;
|
||||
}
|
||||
await executeAppLocalNode({ appPath, appId, event, isNeon });
|
||||
await executeAppLocalNode({
|
||||
appPath,
|
||||
appId,
|
||||
event,
|
||||
isNeon,
|
||||
installCommand,
|
||||
startCommand,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeAppLocalNode({
|
||||
@@ -101,22 +112,28 @@ async function executeAppLocalNode({
|
||||
appId,
|
||||
event,
|
||||
isNeon,
|
||||
installCommand,
|
||||
startCommand,
|
||||
}: {
|
||||
appPath: string;
|
||||
appId: number;
|
||||
event: Electron.IpcMainInvokeEvent;
|
||||
isNeon: boolean;
|
||||
installCommand?: string | null;
|
||||
startCommand?: string | null;
|
||||
}): Promise<void> {
|
||||
const spawnedProcess = spawn(
|
||||
"(pnpm install && pnpm run dev --port 32100) || (npm install --legacy-peer-deps && npm run dev -- --port 32100)",
|
||||
[],
|
||||
{
|
||||
cwd: appPath,
|
||||
shell: true,
|
||||
stdio: "pipe", // Ensure stdio is piped so we can capture output/errors and detect close
|
||||
detached: false, // Ensure child process is attached to the main process lifecycle unless explicitly backgrounded
|
||||
},
|
||||
);
|
||||
const defaultCommand =
|
||||
"(pnpm install && pnpm run dev --port 32100) || (npm install --legacy-peer-deps && npm run dev -- --port 32100)";
|
||||
const hasCustomCommands = !!installCommand?.trim() && !!startCommand?.trim();
|
||||
const command = hasCustomCommands
|
||||
? `${installCommand!.trim()} && ${startCommand!.trim()}`
|
||||
: defaultCommand;
|
||||
const spawnedProcess = spawn(command, [], {
|
||||
cwd: appPath,
|
||||
shell: true,
|
||||
stdio: "pipe", // Ensure stdio is piped so we can capture output/errors and detect close
|
||||
detached: false, // Ensure child process is attached to the main process lifecycle unless explicitly backgrounded
|
||||
});
|
||||
|
||||
// Check if process spawned correctly
|
||||
if (!spawnedProcess.pid) {
|
||||
@@ -375,6 +392,8 @@ export function registerAppHandlers() {
|
||||
supabaseProjectId: null,
|
||||
githubOrg: null,
|
||||
githubRepo: null,
|
||||
installCommand: originalApp.installCommand,
|
||||
startCommand: originalApp.startCommand,
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -511,6 +530,8 @@ export function registerAppHandlers() {
|
||||
appId,
|
||||
event,
|
||||
isNeon: !!app.neonProjectId,
|
||||
installCommand: app.installCommand,
|
||||
startCommand: app.startCommand,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -646,6 +667,8 @@ export function registerAppHandlers() {
|
||||
appId,
|
||||
event,
|
||||
isNeon: !!app.neonProjectId,
|
||||
installCommand: app.installCommand,
|
||||
startCommand: app.startCommand,
|
||||
}); // This will handle starting either mode
|
||||
|
||||
return;
|
||||
|
||||
@@ -69,7 +69,12 @@ export function registerImportHandlers() {
|
||||
"import-app",
|
||||
async (
|
||||
_,
|
||||
{ path: sourcePath, appName }: ImportAppParams,
|
||||
{
|
||||
path: sourcePath,
|
||||
appName,
|
||||
installCommand,
|
||||
startCommand,
|
||||
}: ImportAppParams,
|
||||
): Promise<ImportAppResult> => {
|
||||
// Validate the source path exists
|
||||
try {
|
||||
@@ -128,6 +133,8 @@ export function registerImportHandlers() {
|
||||
name: appName,
|
||||
// Use the name as the path for now
|
||||
path: appName,
|
||||
installCommand: installCommand ?? null,
|
||||
startCommand: startCommand ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ export interface App {
|
||||
vercelProjectName: string | null;
|
||||
vercelTeamSlug: string | null;
|
||||
vercelDeploymentUrl: string | null;
|
||||
installCommand: string | null;
|
||||
startCommand: string | null;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
@@ -226,6 +228,8 @@ export interface ApproveProposalResult {
|
||||
export interface ImportAppParams {
|
||||
path: string;
|
||||
appName: string;
|
||||
installCommand?: string;
|
||||
startCommand?: string;
|
||||
}
|
||||
|
||||
export interface CopyAppParams {
|
||||
|
||||
Reference in New Issue
Block a user