diff --git a/drizzle/0010_nappy_fat_cobra.sql b/drizzle/0010_nappy_fat_cobra.sql new file mode 100644 index 0000000..16cd764 --- /dev/null +++ b/drizzle/0010_nappy_fat_cobra.sql @@ -0,0 +1,2 @@ +ALTER TABLE `apps` ADD `install_command` text;--> statement-breakpoint +ALTER TABLE `apps` ADD `start_command` text; diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..bbf6ba1 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,524 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a7f4a6e1-2a38-4dc8-a37e-b473b6304bab", + "prevId": "4d1fc225-7395-4d56-8d0d-7f76fed4a8d8", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_branch": { + "name": "github_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supabase_project_id": { + "name": "supabase_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_project_id": { + "name": "neon_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_development_branch_id": { + "name": "neon_development_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_preview_branch_id": { + "name": "neon_preview_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_id": { + "name": "vercel_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_name": { + "name": "vercel_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_team_id": { + "name": "vercel_team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_deployment_url": { + "name": "vercel_deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "install_command": { + "name": "install_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_context": { + "name": "chat_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initial_commit_hash": { + "name": "initial_commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_model_providers": { + "name": "language_model_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_models": { + "name": "language_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_name": { + "name": "api_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builtin_provider_id": { + "name": "builtin_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_provider_id": { + "name": "custom_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "language_models_custom_provider_id_language_model_providers_id_fk": { + "name": "language_models_custom_provider_id_language_model_providers_id_fk", + "tableFrom": "language_models", + "tableTo": "language_model_providers", + "columnsFrom": [ + "custom_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "versions": { + "name": "versions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "neon_db_timestamp": { + "name": "neon_db_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "versions_app_commit_unique": { + "name": "versions_app_commit_unique", + "columns": [ + "app_id", + "commit_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "versions_app_id_apps_id_fk": { + "name": "versions_app_id_apps_id_fk", + "tableFrom": "versions", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d8af21d..1d65b1f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1753473275674, "tag": "0009_previous_misty_knight", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1755110011615, + "tag": "0010_nappy_fat_cobra", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e-tests/import.spec.ts b/e2e-tests/import.spec.ts index 36a0d0d..78b9c72 100644 --- a/e2e-tests/import.spec.ts +++ b/e2e-tests/import.spec.ts @@ -1,5 +1,6 @@ import path from "path"; import { testSkipIfWindows } from "./helpers/test_helper"; +import { expect } from "@playwright/test"; import * as eph from "electron-playwright-helpers"; testSkipIfWindows("import app", async ({ po }) => { @@ -43,3 +44,58 @@ testSkipIfWindows("import app with AI rules", async ({ po }) => { await po.snapshotServerDump(); await po.snapshotMessages({ replaceDumpPath: true }); }); + +testSkipIfWindows("import app with custom commands", async ({ po }) => { + await po.setUp(); + await po.page.getByRole("button", { name: "Import App" }).click(); + await eph.stubDialog(po.electronApp, "showOpenDialog", { + filePaths: [path.join(__dirname, "fixtures", "import-app", "minimal")], + }); + await po.page.getByRole("button", { name: "Select Folder" }).click(); + await po.page + .getByRole("textbox", { name: "Enter new app name" }) + .fill("minimal-imported-app"); + + await po.page.getByRole("button", { name: "Advanced options" }).click(); + + await po.page.getByPlaceholder("pnpm install").fill(""); + await po.page.getByPlaceholder("pnpm dev").fill("npm start"); + await expect(po.page.getByRole("button", { name: "Import" })).toBeDisabled(); + await expect( + po.page.getByText("Both commands are required when customizing."), + ).toBeVisible(); + + await po.page.getByPlaceholder("pnpm install").fill("npm i"); + await expect(po.page.getByRole("button", { name: "Import" })).toBeEnabled(); + await expect( + po.page.getByText("Both commands are required when customizing."), + ).toHaveCount(0); + + await po.page.getByRole("button", { name: "Import" }).click(); +}); + +testSkipIfWindows( + "advanced options: both cleared are valid and use defaults", + async ({ po }) => { + await po.setUp(); + await po.page.getByRole("button", { name: "Import App" }).click(); + await eph.stubDialog(po.electronApp, "showOpenDialog", { + filePaths: [path.join(__dirname, "fixtures", "import-app", "minimal")], + }); + await po.page.getByRole("button", { name: "Select Folder" }).click(); + + await po.page + .getByRole("textbox", { name: "Enter new app name" }) + .fill("both-cleared"); + + await po.page.getByRole("button", { name: "Advanced options" }).click(); + await po.page.getByPlaceholder("pnpm install").fill(""); + await po.page.getByPlaceholder("pnpm dev").fill(""); + + await expect(po.page.getByRole("button", { name: "Import" })).toBeEnabled(); + + await po.page.getByRole("button", { name: "Import" }).click(); + + await po.snapshotPreview(); + }, +); diff --git a/e2e-tests/snapshots/import.spec.ts_advanced-options-both-cleared-are-valid-and-use-defaults-1.aria.yml b/e2e-tests/snapshots/import.spec.ts_advanced-options-both-cleared-are-valid-and-use-defaults-1.aria.yml new file mode 100644 index 0000000..4efac3b --- /dev/null +++ b/e2e-tests/snapshots/import.spec.ts_advanced-options-both-cleared-are-valid-and-use-defaults-1.aria.yml @@ -0,0 +1 @@ +- text: Minimal imported app \ No newline at end of file diff --git a/src/components/ImportAppDialog.tsx b/src/components/ImportAppDialog.tsx index 37a86c9..c845e6a 100644 --- a/src/components/ImportAppDialog.tsx +++ b/src/components/ImportAppDialog.tsx @@ -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(""); const [nameExists, setNameExists] = useState(false); const [isCheckingName, setIsCheckingName] = useState(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 ( @@ -221,6 +237,41 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { + + + + Advanced options + + +
+ + setInstallCommand(e.target.value)} + placeholder="pnpm install" + disabled={importAppMutation.isPending} + /> +
+
+ + setStartCommand(e.target.value)} + placeholder="pnpm dev" + disabled={importAppMutation.isPending} + /> +
+ {!commandsValid && ( +

+ Both commands are required when customizing. +

+ )} +
+
+
+ {hasAiRules === false && ( @@ -264,7 +315,10 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {