diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7d7aaf9..5a4cfac2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: pull_request: - branches: [main] + branches: [ main ] workflow_dispatch: concurrency: @@ -58,5 +58,6 @@ jobs: - name: Checks run: | cargo fmt --all -- --check + npm run generate-types:check cargo test --workspace cargo clippy --all --all-targets --all-features -- -D warnings diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index ef01140d..d22f4168 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -1,6 +1,7 @@ use std::{env, fs, path::Path}; -use ts_rs::TS; // in [build-dependencies] +use ts_rs::TS; +// in [build-dependencies] fn generate_constants() -> String { r#"// Generated constants @@ -60,17 +61,14 @@ export const SOUND_LABELS: Record = { .to_string() } -fn main() { - // 1. Make sure ../shared exists - let shared_path = Path::new("../shared"); - fs::create_dir_all(shared_path).expect("cannot create ../shared"); +fn generate_types_content() -> String { + // 4. Friendly banner + const HEADER: &str = + "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs).\n\ + // Do not edit this file manually.\n\ + // Auto-generated from Rust backend types using ts-rs\n\n"; - println!("Generating TypeScript types…"); - - // 2. Let ts-rs write its per-type files here (handy for debugging) - env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap()); - - // 3. Grab every Rust type you want on the TS side + // 5. Add `export` if it’s missing, then join let decls = [ vibe_kanban::models::ApiResponse::<()>::decl(), vibe_kanban::models::config::Config::decl(), @@ -108,6 +106,7 @@ fn main() { vibe_kanban::models::task_attempt_activity::CreateTaskAttemptActivity::decl(), vibe_kanban::routes::filesystem::DirectoryEntry::decl(), vibe_kanban::routes::filesystem::DirectoryListResponse::decl(), + vibe_kanban::routes::auth::DeviceStartResponse::decl(), vibe_kanban::models::task_attempt::DiffChunkType::decl(), vibe_kanban::models::task_attempt::DiffChunk::decl(), vibe_kanban::models::task_attempt::FileDiff::decl(), @@ -128,16 +127,8 @@ fn main() { vibe_kanban::executor::NormalizedEntry::decl(), vibe_kanban::executor::NormalizedEntryType::decl(), vibe_kanban::executor::ActionType::decl(), - vibe_kanban::routes::auth::DeviceStartResponse::decl(), ]; - // 4. Friendly banner - const HEADER: &str = - "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs).\n\ - // Do not edit this file manually.\n\ - // Auto-generated from Rust backend types using ts-rs\n\n"; - - // 5. Add `export` if it’s missing, then join let body = decls .into_iter() .map(|d| { @@ -151,15 +142,39 @@ fn main() { .collect::>() .join("\n\n"); - // 6. Add constants let constants = generate_constants(); - - // 7. Write the consolidated types.ts - fs::write( - shared_path.join("types.ts"), - format!("{HEADER}{body}\n\n{constants}"), - ) - .expect("unable to write types.ts"); - - println!("✅ TypeScript types generated in ../shared/"); + format!("{HEADER}{body}\n\n{constants}") +} + +fn main() { + let args: Vec = env::args().collect(); + let check_mode = args.iter().any(|arg| arg == "--check"); + + // 1. Make sure ../shared exists + let shared_path = Path::new("../shared"); + fs::create_dir_all(shared_path).expect("cannot create ../shared"); + + println!("Generating TypeScript types…"); + + // 2. Let ts-rs write its per-type files here (handy for debugging) + env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap()); + + let generated = generate_types_content(); + let types_path = shared_path.join("types.ts"); + + if check_mode { + // Read the current file + let current = fs::read_to_string(&types_path).unwrap_or_default(); + if current == generated { + println!("✅ shared/types.ts is up to date."); + std::process::exit(0); + } else { + eprintln!("❌ shared/types.ts is not up to date. Please run 'npm run generate-types' and commit the changes."); + std::process::exit(1); + } + } else { + // Write the file as before + fs::write(&types_path, generated).expect("unable to write types.ts"); + println!("✅ TypeScript types generated in ../shared/"); + } } diff --git a/backend/src/routes/auth.rs b/backend/src/routes/auth.rs index 89909b61..1fe37343 100644 --- a/backend/src/routes/auth.rs +++ b/backend/src/routes/auth.rs @@ -5,6 +5,7 @@ use axum::{ routing::{get, post}, Json, Router, }; +use ts_rs::TS; use crate::{app_state::AppState, models::ApiResponse}; @@ -18,7 +19,7 @@ pub fn auth_router() -> Router { #[derive(serde::Deserialize)] struct DeviceStartRequest {} -#[derive(serde::Serialize, ts_rs::TS)] +#[derive(serde::Serialize, TS)] #[ts(export)] pub struct DeviceStartResponse { pub device_code: String, diff --git a/frontend/src/components/context/TaskDetailsContextProvider.tsx b/frontend/src/components/context/TaskDetailsContextProvider.tsx index 5ae4392f..82139ac8 100644 --- a/frontend/src/components/context/TaskDetailsContextProvider.tsx +++ b/frontend/src/components/context/TaskDetailsContextProvider.tsx @@ -17,7 +17,6 @@ import type { TaskWithAttemptStatus, WorktreeDiff, } from 'shared/types.ts'; -import type { AttemptData } from './taskDetailsContext'; import { attemptsApi, executionProcessesApi } from '@/lib/api.ts'; import { TaskAttemptDataContext, @@ -30,6 +29,7 @@ import { TaskExecutionStateContext, TaskSelectedAttemptContext, } from './taskDetailsContext.ts'; +import { AttemptData } from '@/lib/types.ts'; const TaskDetailsProvider: FC<{ task: TaskWithAttemptStatus; diff --git a/frontend/src/components/context/taskDetailsContext.ts b/frontend/src/components/context/taskDetailsContext.ts index 25c3b60d..efa965a0 100644 --- a/frontend/src/components/context/taskDetailsContext.ts +++ b/frontend/src/components/context/taskDetailsContext.ts @@ -5,17 +5,8 @@ import type { TaskAttemptState, TaskWithAttemptStatus, WorktreeDiff, - TaskAttemptActivityWithPrompt, - ExecutionProcessSummary, - ExecutionProcess, } from 'shared/types.ts'; - -// Frontend-only type for combining attempt data -export interface AttemptData { - activities: TaskAttemptActivityWithPrompt[]; - processes: ExecutionProcessSummary[]; - runningProcessDetails: Record; -} +import { AttemptData } from '@/lib/types.ts'; export interface TaskDetailsContextValue { task: TaskWithAttemptStatus; diff --git a/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx b/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx index 3aabc65a..dae84e7f 100644 --- a/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +++ b/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx @@ -1,8 +1,8 @@ import { Button } from '@/components/ui/button.tsx'; import { ChevronDown, ChevronUp } from 'lucide-react'; import type { DiffChunkType } from 'shared/types.ts'; -import type { ProcessedSection } from './DiffFile'; import { Dispatch, SetStateAction } from 'react'; +import { ProcessedSection } from '@/lib/types.ts'; type Props = { section: ProcessedSection; diff --git a/frontend/src/components/tasks/TaskDetails/DiffFile.tsx b/frontend/src/components/tasks/TaskDetails/DiffFile.tsx index ecb8ec76..1de429ae 100644 --- a/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +++ b/frontend/src/components/tasks/TaskDetails/DiffFile.tsx @@ -1,23 +1,7 @@ import { Button } from '@/components/ui/button.tsx'; import { ChevronDown, ChevronUp, Trash2 } from 'lucide-react'; import DiffChunkSection from '@/components/tasks/TaskDetails/DiffChunkSection.tsx'; -import { FileDiff, DiffChunkType } from 'shared/types.ts'; - -// Types for processing diff content in the frontend -export interface ProcessedLine { - content: string; - chunkType: DiffChunkType; - oldLineNumber?: number; - newLineNumber?: number; -} - -export interface ProcessedSection { - type: 'context' | 'change' | 'expanded'; - lines: ProcessedLine[]; - expandKey?: string; - expandedAbove?: boolean; - expandedBelow?: boolean; -} +import { FileDiff } from 'shared/types.ts'; import { Dispatch, SetStateAction, @@ -27,6 +11,7 @@ import { useState, } from 'react'; import { TaskDeletingFilesContext } from '@/components/context/taskDetailsContext.ts'; +import { ProcessedLine, ProcessedSection } from '@/lib/types.ts'; type Props = { collapsedFiles: Set; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5918d506..ba774fd7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -7,6 +7,7 @@ import { CreateTask, CreateTaskAndStart, CreateTaskAttempt, + DeviceStartResponse, DirectoryEntry, type EditorType, ExecutionProcess, @@ -15,7 +16,6 @@ import { NormalizedConversation, Project, ProjectWithBranch, - DeviceStartResponse, Task, TaskAttempt, TaskAttemptActivityWithPrompt, diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 00000000..8cb9456b --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,27 @@ +import { + DiffChunkType, + ExecutionProcess, + ExecutionProcessSummary, + TaskAttemptActivityWithPrompt, +} from 'shared/types.ts'; + +export type AttemptData = { + activities: TaskAttemptActivityWithPrompt[]; + processes: ExecutionProcessSummary[]; + runningProcessDetails: Record; +}; + +export interface ProcessedLine { + content: string; + chunkType: DiffChunkType; + oldLineNumber?: number; + newLineNumber?: number; +} + +export interface ProcessedSection { + type: 'context' | 'change' | 'expanded'; + lines: ProcessedLine[]; + expandKey?: string; + expandedAbove?: boolean; + expandedBelow?: boolean; +} diff --git a/package.json b/package.json index d74dfb62..70028ea2 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "backend:run": "npm run cargo -- run --manifest-path backend/Cargo.toml", "backend:test": "npm run cargo -- test --lib", "generate-types": "cd backend && cargo run --bin generate_types", + "generate-types:check": "cd backend && cargo run --bin generate_types -- --check", "prepare-db": "node scripts/prepare-db.js", "dev:clear-ports": "node scripts/setup-dev-environment.js clear" }, diff --git a/shared/types.ts b/shared/types.ts index fb0c2968..1bb08928 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -74,6 +74,8 @@ export type DirectoryEntry = { name: string, path: string, is_directory: boolean export type DirectoryListResponse = { entries: Array, current_path: string, }; +export type DeviceStartResponse = { device_code: string, user_code: string, verification_uri: string, expires_in: number, interval: number, }; + export type DiffChunkType = "Equal" | "Insert" | "Delete"; export type DiffChunk = { chunk_type: DiffChunkType, content: string, }; @@ -114,8 +116,6 @@ export type NormalizedEntryType = { "type": "user_message" } | { "type": "assist export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "other", description: string, }; -export type DeviceStartResponse = { device_code: string, user_code: string, verification_uri: string, expires_in: number, interval: number, }; - // Generated constants export const EXECUTOR_TYPES: string[] = [ "echo",