diff --git a/drizzle/0006_magenta_polaris.sql b/drizzle/0006_magenta_polaris.sql new file mode 100644 index 0000000..28c1503 --- /dev/null +++ b/drizzle/0006_magenta_polaris.sql @@ -0,0 +1 @@ +ALTER TABLE "apps" ADD COLUMN "subdirectory" varchar(255); \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..78d3542 --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,1109 @@ +{ + "id": "2bc0d5a8-be1b-46c0-b40b-195af132de1c", + "prevId": "f4683006-a4b7-4f2b-a2f3-dc154d1499e5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.apps": { + "name": "apps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_repo": { + "name": "github_repo", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "build_command": { + "name": "build_command", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "output_dir": { + "name": "output_dir", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "subdirectory": { + "name": "subdirectory", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "build_timeout": { + "name": "build_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 900 + }, + "active_deployment_id": { + "name": "active_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "build_pack": { + "name": "build_pack", + "type": "build_pack", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'static'" + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 80 + }, + "run_command": { + "name": "run_command", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "install_command": { + "name": "install_command", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "dockerfile_path": { + "name": "dockerfile_path", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "default": "'./Dockerfile'" + }, + "is_spa": { + "name": "is_spa", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "custom_nginx_config": { + "name": "custom_nginx_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_apps_org_id": { + "name": "idx_apps_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_apps_org_name_unique": { + "name": "idx_apps_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apps_organization_id_organizations_id_fk": { + "name": "apps_organization_id_organizations_id_fk", + "tableFrom": "apps", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.build_jobs": { + "name": "build_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_build_jobs_deployment_id": { + "name": "idx_build_jobs_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "build_jobs_deployment_id_deployments_id_fk": { + "name": "build_jobs_deployment_id_deployments_id_fk", + "tableFrom": "build_jobs", + "tableTo": "deployments", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_files": { + "name": "deployment_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_files_deployment_id": { + "name": "idx_deployment_files_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_files_deployment_id_deployments_id_fk": { + "name": "deployment_files_deployment_id_deployments_id_fk", + "tableFrom": "deployment_files", + "tableTo": "deployments", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_logs": { + "name": "deployment_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_logs_deployment_id": { + "name": "idx_deployment_logs_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "app_id": { + "name": "app_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "commit_message": { + "name": "commit_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "detected_framework": { + "name": "detected_framework", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "output_dir": { + "name": "output_dir", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployments_app_id": { + "name": "idx_deployments_app_id", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_status": { + "name": "idx_deployments_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_app_id_apps_id_fk": { + "name": "deployments_app_id_apps_id_fk", + "tableFrom": "deployments", + "tableTo": "apps", + "columnsFrom": ["app_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domains": { + "name": "domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "app_id": { + "name": "app_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_domains_app_id": { + "name": "idx_domains_app_id", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_domains_domain": { + "name": "idx_domains_domain", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domains_app_id_apps_id_fk": { + "name": "domains_app_id_apps_id_fk", + "tableFrom": "domains", + "tableTo": "apps", + "columnsFrom": ["app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domains_domain_unique": { + "name": "domains_domain_unique", + "nullsNotDistinct": false, + "columns": ["domain"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.env_vars": { + "name": "env_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "app_id": { + "name": "app_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_env_vars_app_id": { + "name": "idx_env_vars_app_id", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "env_vars_app_id_apps_id_fk": { + "name": "env_vars_app_id_apps_id_fk", + "tableFrom": "env_vars", + "tableTo": "apps", + "columnsFrom": ["app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_members": { + "name": "organization_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_members_user_id": { + "name": "idx_org_members_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_members_org_id": { + "name": "idx_org_members_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_organizations_slug": { + "name": "idx_organizations_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_token": { + "name": "idx_sessions_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "sessions_org_id_organizations_id_fk": { + "name": "sessions_org_id_organizations_id_fk", + "tableFrom": "sessions", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_id": { + "name": "github_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "github_username": { + "name": "github_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "github_access_token": { + "name": "github_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_users_github_id": { + "name": "idx_users_github_id", + "columns": [ + { + "expression": "github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_github_id_unique": { + "name": "users_github_id_unique", + "nullsNotDistinct": false, + "columns": ["github_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.build_pack": { + "name": "build_pack", + "schema": "public", + "values": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose", + "dockerimage" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 15534f3..0a98eef 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1778626000905, "tag": "0005_parallel_jack_flag", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1779071059753, + "tag": "0006_magenta_polaris", + "breakpoints": true } ] } diff --git a/packages/api/src/services/apps.ts b/packages/api/src/services/apps.ts index 436d948..a71cbd8 100644 --- a/packages/api/src/services/apps.ts +++ b/packages/api/src/services/apps.ts @@ -17,6 +17,7 @@ const safeColumns = { githubRepo: apps.githubRepo, buildCommand: apps.buildCommand, outputDir: apps.outputDir, + subdirectory: apps.subdirectory, branch: apps.branch, buildTimeout: apps.buildTimeout, activeDeploymentId: apps.activeDeploymentId, diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts index 2389a3f..e399b86 100644 --- a/packages/shared/src/schema.ts +++ b/packages/shared/src/schema.ts @@ -78,6 +78,7 @@ export const apps = pgTable( githubRepo: varchar("github_repo", { length: 500 }).notNull().default(""), buildCommand: varchar("build_command", { length: 500 }), outputDir: varchar("output_dir", { length: 255 }), + subdirectory: varchar("subdirectory", { length: 255 }), branch: varchar("branch", { length: 100 }).default("main"), buildTimeout: integer("build_timeout").default(900), activeDeploymentId: uuid("active_deployment_id"), diff --git a/packages/shared/src/validators/app.ts b/packages/shared/src/validators/app.ts index dd410c0..52c5faa 100644 --- a/packages/shared/src/validators/app.ts +++ b/packages/shared/src/validators/app.ts @@ -22,6 +22,20 @@ const appFieldDefs = { buildPack: z.enum(BUILD_PACKS), buildCommand: z.string().optional(), outputDir: z.string().optional(), + subdirectory: z + .string() + .optional() + .refine( + (val) => !val || noShellMeta.test(val), + "Contains unsafe shell characters", + ) + .refine( + (val) => !val?.split("/").includes(".."), + "Must not contain parent directory references", + ) + .transform((val) => + val?.trim() ? val.replace(/^\/+|\/+$/g, "") : undefined, + ), port: z.number().int().positive(), runCommand: safeCommandSchema.optional(), installCommand: installCommandSchema.optional(), @@ -78,6 +92,7 @@ const withDefaults = z.object({ buildPack: appFieldDefs.buildPack, buildCommand: appFieldDefs.buildCommand, outputDir: appFieldDefs.outputDir, + subdirectory: appFieldDefs.subdirectory, port: appFieldDefs.port.default(80), runCommand: appFieldDefs.runCommand, installCommand: appFieldDefs.installCommand, diff --git a/packages/web/src/components/CreateAppModal.tsx b/packages/web/src/components/CreateAppModal.tsx index d9a3179..012f377 100644 --- a/packages/web/src/components/CreateAppModal.tsx +++ b/packages/web/src/components/CreateAppModal.tsx @@ -12,6 +12,7 @@ function validate(input: { image: string; port: number; outputDir: string; + subdirectory: string; dockerfilePath: string; }): Record { const errs: Record = {}; @@ -83,6 +84,7 @@ export function CreateAppModal({ open, onClose }: Props) { const [branch, setBranch] = useState("main"); const [buildCommand, setBuildCommand] = useState(""); const [outputDir, setOutputDir] = useState("dist"); + const [subdirectory, setSubdirectory] = useState(""); const [port, setPort] = useState(80); const [runCommand, setRunCommand] = useState(""); const [dockerfilePath, setDockerfilePath] = useState("./Dockerfile"); @@ -101,6 +103,7 @@ export function CreateAppModal({ open, onClose }: Props) { setBuildPack("static"); setBuildCommand(""); setOutputDir("dist"); + setSubdirectory(""); setPort(80); setRunCommand(""); setDockerfilePath("./Dockerfile"); @@ -123,6 +126,7 @@ export function CreateAppModal({ open, onClose }: Props) { image, port, outputDir, + subdirectory, dockerfilePath, }); setFieldErrors(errs); @@ -144,6 +148,7 @@ export function CreateAppModal({ open, onClose }: Props) { if (buildPack === "static") { payload.outputDir = outputDir; payload.isSpa = isSpa; + if (subdirectory) payload.subdirectory = subdirectory; } if (buildPack === "dockerfile") { @@ -153,6 +158,7 @@ export function CreateAppModal({ open, onClose }: Props) { if (buildPack === "nixpacks") { if (runCommand) payload.runCommand = runCommand; if (outputDir) payload.outputDir = outputDir; + if (subdirectory) payload.subdirectory = subdirectory; } if (buildPack === "dockerimage") { @@ -298,6 +304,17 @@ export function CreateAppModal({ open, onClose }: Props) { /> +
+ + setSubdirectory(e.target.value)} + placeholder="e.g. frontend, packages/web" + className="w-full bg-ship-deep border border-ship-deck/50 px-3 py-2 font-mono text-sm text-white placeholder:text-ship-deck focus:outline-none focus:border-ship-buoy/50 transition-colors" + /> +