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:
2
drizzle/0010_nappy_fat_cobra.sql
Normal file
2
drizzle/0010_nappy_fat_cobra.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `apps` ADD `install_command` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `apps` ADD `start_command` text;
|
||||||
524
drizzle/meta/0010_snapshot.json
Normal file
524
drizzle/meta/0010_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,13 @@
|
|||||||
"when": 1753473275674,
|
"when": 1753473275674,
|
||||||
"tag": "0009_previous_misty_knight",
|
"tag": "0009_previous_misty_knight",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1755110011615,
|
||||||
|
"tag": "0010_nappy_fat_cobra",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { testSkipIfWindows } from "./helpers/test_helper";
|
import { testSkipIfWindows } from "./helpers/test_helper";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
import * as eph from "electron-playwright-helpers";
|
import * as eph from "electron-playwright-helpers";
|
||||||
|
|
||||||
testSkipIfWindows("import app", async ({ po }) => {
|
testSkipIfWindows("import app", async ({ po }) => {
|
||||||
@@ -43,3 +44,58 @@ testSkipIfWindows("import app with AI rules", async ({ po }) => {
|
|||||||
await po.snapshotServerDump();
|
await po.snapshotServerDump();
|
||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
- text: Minimal imported app
|
||||||
@@ -27,6 +27,12 @@ import {
|
|||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "./ui/accordion";
|
||||||
|
|
||||||
interface ImportAppDialogProps {
|
interface ImportAppDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -39,6 +45,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
|||||||
const [customAppName, setCustomAppName] = useState<string>("");
|
const [customAppName, setCustomAppName] = useState<string>("");
|
||||||
const [nameExists, setNameExists] = useState<boolean>(false);
|
const [nameExists, setNameExists] = useState<boolean>(false);
|
||||||
const [isCheckingName, setIsCheckingName] = 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 navigate = useNavigate();
|
||||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||||
const { refreshApps } = useLoadApps();
|
const { refreshApps } = useLoadApps();
|
||||||
@@ -89,6 +97,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
|||||||
return IpcClient.getInstance().importApp({
|
return IpcClient.getInstance().importApp({
|
||||||
path: selectedPath,
|
path: selectedPath,
|
||||||
appName: customAppName,
|
appName: customAppName,
|
||||||
|
installCommand: installCommand || undefined,
|
||||||
|
startCommand: startCommand || undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: async (result) => {
|
onSuccess: async (result) => {
|
||||||
@@ -128,6 +138,8 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
|||||||
setHasAiRules(null);
|
setHasAiRules(null);
|
||||||
setCustomAppName("");
|
setCustomAppName("");
|
||||||
setNameExists(false);
|
setNameExists(false);
|
||||||
|
setInstallCommand("pnpm install");
|
||||||
|
setStartCommand("pnpm dev");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAppNameChange = async (
|
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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -221,6 +237,41 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{hasAiRules === false && (
|
||||||
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
|
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -264,7 +315,10 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleImport}
|
onClick={handleImport}
|
||||||
disabled={
|
disabled={
|
||||||
!selectedPath || importAppMutation.isPending || nameExists
|
!selectedPath ||
|
||||||
|
importAppMutation.isPending ||
|
||||||
|
nameExists ||
|
||||||
|
!commandsValid
|
||||||
}
|
}
|
||||||
className="min-w-[80px]"
|
className="min-w-[80px]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export const apps = sqliteTable("apps", {
|
|||||||
vercelProjectName: text("vercel_project_name"),
|
vercelProjectName: text("vercel_project_name"),
|
||||||
vercelTeamId: text("vercel_team_id"),
|
vercelTeamId: text("vercel_team_id"),
|
||||||
vercelDeploymentUrl: text("vercel_deployment_url"),
|
vercelDeploymentUrl: text("vercel_deployment_url"),
|
||||||
|
installCommand: text("install_command"),
|
||||||
|
startCommand: text("start_command"),
|
||||||
chatContext: text("chat_context", { mode: "json" }),
|
chatContext: text("chat_context", { mode: "json" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,17 +83,28 @@ async function executeApp({
|
|||||||
appId,
|
appId,
|
||||||
event, // Keep event for local-node case
|
event, // Keep event for local-node case
|
||||||
isNeon,
|
isNeon,
|
||||||
|
installCommand,
|
||||||
|
startCommand,
|
||||||
}: {
|
}: {
|
||||||
appPath: string;
|
appPath: string;
|
||||||
appId: number;
|
appId: number;
|
||||||
event: Electron.IpcMainInvokeEvent;
|
event: Electron.IpcMainInvokeEvent;
|
||||||
isNeon: boolean;
|
isNeon: boolean;
|
||||||
|
installCommand?: string | null;
|
||||||
|
startCommand?: string | null;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (proxyWorker) {
|
if (proxyWorker) {
|
||||||
proxyWorker.terminate();
|
proxyWorker.terminate();
|
||||||
proxyWorker = null;
|
proxyWorker = null;
|
||||||
}
|
}
|
||||||
await executeAppLocalNode({ appPath, appId, event, isNeon });
|
await executeAppLocalNode({
|
||||||
|
appPath,
|
||||||
|
appId,
|
||||||
|
event,
|
||||||
|
isNeon,
|
||||||
|
installCommand,
|
||||||
|
startCommand,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeAppLocalNode({
|
async function executeAppLocalNode({
|
||||||
@@ -101,22 +112,28 @@ async function executeAppLocalNode({
|
|||||||
appId,
|
appId,
|
||||||
event,
|
event,
|
||||||
isNeon,
|
isNeon,
|
||||||
|
installCommand,
|
||||||
|
startCommand,
|
||||||
}: {
|
}: {
|
||||||
appPath: string;
|
appPath: string;
|
||||||
appId: number;
|
appId: number;
|
||||||
event: Electron.IpcMainInvokeEvent;
|
event: Electron.IpcMainInvokeEvent;
|
||||||
isNeon: boolean;
|
isNeon: boolean;
|
||||||
|
installCommand?: string | null;
|
||||||
|
startCommand?: string | null;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const spawnedProcess = spawn(
|
const defaultCommand =
|
||||||
"(pnpm install && pnpm run dev --port 32100) || (npm install --legacy-peer-deps && npm run dev -- --port 32100)",
|
"(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
|
||||||
cwd: appPath,
|
? `${installCommand!.trim()} && ${startCommand!.trim()}`
|
||||||
shell: true,
|
: defaultCommand;
|
||||||
stdio: "pipe", // Ensure stdio is piped so we can capture output/errors and detect close
|
const spawnedProcess = spawn(command, [], {
|
||||||
detached: false, // Ensure child process is attached to the main process lifecycle unless explicitly backgrounded
|
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
|
// Check if process spawned correctly
|
||||||
if (!spawnedProcess.pid) {
|
if (!spawnedProcess.pid) {
|
||||||
@@ -375,6 +392,8 @@ export function registerAppHandlers() {
|
|||||||
supabaseProjectId: null,
|
supabaseProjectId: null,
|
||||||
githubOrg: null,
|
githubOrg: null,
|
||||||
githubRepo: null,
|
githubRepo: null,
|
||||||
|
installCommand: originalApp.installCommand,
|
||||||
|
startCommand: originalApp.startCommand,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -511,6 +530,8 @@ export function registerAppHandlers() {
|
|||||||
appId,
|
appId,
|
||||||
event,
|
event,
|
||||||
isNeon: !!app.neonProjectId,
|
isNeon: !!app.neonProjectId,
|
||||||
|
installCommand: app.installCommand,
|
||||||
|
startCommand: app.startCommand,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -646,6 +667,8 @@ export function registerAppHandlers() {
|
|||||||
appId,
|
appId,
|
||||||
event,
|
event,
|
||||||
isNeon: !!app.neonProjectId,
|
isNeon: !!app.neonProjectId,
|
||||||
|
installCommand: app.installCommand,
|
||||||
|
startCommand: app.startCommand,
|
||||||
}); // This will handle starting either mode
|
}); // This will handle starting either mode
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ export function registerImportHandlers() {
|
|||||||
"import-app",
|
"import-app",
|
||||||
async (
|
async (
|
||||||
_,
|
_,
|
||||||
{ path: sourcePath, appName }: ImportAppParams,
|
{
|
||||||
|
path: sourcePath,
|
||||||
|
appName,
|
||||||
|
installCommand,
|
||||||
|
startCommand,
|
||||||
|
}: ImportAppParams,
|
||||||
): Promise<ImportAppResult> => {
|
): Promise<ImportAppResult> => {
|
||||||
// Validate the source path exists
|
// Validate the source path exists
|
||||||
try {
|
try {
|
||||||
@@ -128,6 +133,8 @@ export function registerImportHandlers() {
|
|||||||
name: appName,
|
name: appName,
|
||||||
// Use the name as the path for now
|
// Use the name as the path for now
|
||||||
path: appName,
|
path: appName,
|
||||||
|
installCommand: installCommand ?? null,
|
||||||
|
startCommand: startCommand ?? null,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ export interface App {
|
|||||||
vercelProjectName: string | null;
|
vercelProjectName: string | null;
|
||||||
vercelTeamSlug: string | null;
|
vercelTeamSlug: string | null;
|
||||||
vercelDeploymentUrl: string | null;
|
vercelDeploymentUrl: string | null;
|
||||||
|
installCommand: string | null;
|
||||||
|
startCommand: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
@@ -226,6 +228,8 @@ export interface ApproveProposalResult {
|
|||||||
export interface ImportAppParams {
|
export interface ImportAppParams {
|
||||||
path: string;
|
path: string;
|
||||||
appName: string;
|
appName: string;
|
||||||
|
installCommand?: string;
|
||||||
|
startCommand?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopyAppParams {
|
export interface CopyAppParams {
|
||||||
|
|||||||
Reference in New Issue
Block a user