Skip to content

Commit 8e85e17

Browse files
authored
Merge pull request #112 from codervisor/copilot/implement-spec-195
[WIP] Implement spec 195
2 parents ab1e052 + fa15af0 commit 8e85e17

File tree

15 files changed

+650
-98
lines changed

15 files changed

+650
-98
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { NextResponse } from 'next/server';
2+
import pkg from '../../../../package.json';
3+
4+
export async function GET() {
5+
return NextResponse.json({
6+
status: 'ok',
7+
version: process.env.npm_package_version || pkg.version,
8+
});
9+
}

packages/ui/src/app/api/projects/[id]/dependencies/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export async function GET(
1818
const { id } = await params;
1919

2020
// Use 'default' project as undefined for backward compatibility
21+
const specsMode = process.env.SPECS_MODE || 'filesystem';
22+
if (!isDefaultProject(id) && specsMode !== 'multi-project') {
23+
return NextResponse.json(
24+
{ error: 'Project not found', code: 'PROJECT_NOT_FOUND' },
25+
{ status: 404 }
26+
);
27+
}
28+
2129
const projectId = isDefaultProject(id) ? undefined : id;
2230
const graph = await getDependencyGraph(projectId);
2331
return NextResponse.json(graph);
@@ -29,4 +37,3 @@ export async function GET(
2937
);
3038
}
3139
}
32-

packages/ui/src/app/api/projects/[id]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function GET(
3535
const specsDir = process.env.SPECS_DIR || 'specs';
3636
return NextResponse.json({
3737
project: {
38-
id: 'default',
38+
id,
3939
displayName: 'Local Project',
4040
specsDir,
4141
isFeatured: true,

packages/ui/src/app/api/projects/[id]/specs/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,27 @@ import { NextResponse } from 'next/server';
1313
import { getSpecs } from '@/lib/db/service-queries';
1414
import { isDefaultProject } from '@/lib/projects/constants';
1515

16+
const VALID_STATUSES = ['planned', 'in-progress', 'complete', 'archived'];
17+
18+
function isValidStatusFilter(statusParam: string | null): boolean {
19+
if (!statusParam) return true;
20+
return statusParam.split(',').every((s) => VALID_STATUSES.includes(s));
21+
}
22+
1623
export async function GET(
1724
request: Request,
1825
{ params }: { params: Promise<{ id: string }> }
1926
) {
2027
try {
2128
const { id } = await params;
29+
const url = new URL(request.url);
30+
const statusParam = url.searchParams.get('status');
31+
if (!isValidStatusFilter(statusParam)) {
32+
return NextResponse.json(
33+
{ error: 'Invalid status filter', code: 'INVALID_REQUEST' },
34+
{ status: 400 }
35+
);
36+
}
2237

2338
// Use 'default' project as undefined for backward compatibility
2439
// This routes to filesystem source in single-project mode

packages/ui/src/app/api/projects/[id]/stats/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function GET(
2020
// Use 'default' project as undefined for backward compatibility
2121
const projectId = isDefaultProject(id) ? undefined : id;
2222
const stats = await getStats(projectId);
23-
return NextResponse.json({ stats });
23+
return NextResponse.json(stats);
2424
} catch (error) {
2525
console.error('Error fetching stats:', error);
2626
return NextResponse.json(

packages/ui/src/app/api/projects/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function GET() {
3333
return NextResponse.json({
3434
mode: 'single-project',
3535
projects: [{
36-
id: 'local',
36+
id: 'default',
3737
displayName: 'Local Project',
3838
specsDir,
3939
isFeatured: true,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { NextResponse } from 'next/server';
2+
import { getSpecsWithMetadata } from '@/lib/db/service-queries';
3+
import { isDefaultProject } from '@/lib/projects/constants';
4+
5+
export async function POST(request: Request) {
6+
let body: any;
7+
try {
8+
body = await request.json();
9+
} catch {
10+
return NextResponse.json(
11+
{ error: 'Invalid request body', code: 'INVALID_REQUEST' },
12+
{ status: 400 }
13+
);
14+
}
15+
16+
const { query, projectId } = body || {};
17+
if (!query || typeof query !== 'string') {
18+
return NextResponse.json(
19+
{ error: 'Query is required', code: 'INVALID_REQUEST' },
20+
{ status: 400 }
21+
);
22+
}
23+
24+
const scopedProjectId = isDefaultProject(projectId) ? undefined : projectId;
25+
26+
try {
27+
const specs = await getSpecsWithMetadata(scopedProjectId);
28+
const queryLower = query.toLowerCase();
29+
30+
const results = specs.filter((spec) => {
31+
const title = spec.title || spec.specName || '';
32+
const path = spec.specName || '';
33+
const tags = spec.tags || [];
34+
35+
return (
36+
title.toLowerCase().includes(queryLower) ||
37+
path.toLowerCase().includes(queryLower) ||
38+
tags.some((tag) => tag.toLowerCase().includes(queryLower))
39+
);
40+
});
41+
42+
return NextResponse.json({
43+
results,
44+
total: results.length,
45+
query,
46+
});
47+
} catch (error) {
48+
console.error('Error performing search:', error);
49+
return NextResponse.json(
50+
{ error: 'Failed to perform search', code: 'INTERNAL_ERROR' },
51+
{ status: 500 }
52+
);
53+
}
54+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { NextResponse } from 'next/server';
2+
import pkg from '../../../package.json';
3+
4+
export async function GET() {
5+
return NextResponse.json({
6+
status: 'ok',
7+
version: process.env.npm_package_version || pkg.version,
8+
});
9+
}

rust/leanspec-http/src/handlers/projects.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ use crate::state::AppState;
1414
#[derive(Debug, Serialize)]
1515
#[serde(rename_all = "camelCase")]
1616
pub struct ProjectsListResponse {
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub mode: Option<String>,
1719
pub projects: Vec<ProjectResponse>,
20+
#[serde(skip_serializing_if = "Option::is_none")]
21+
pub recent_projects: Option<Vec<String>>,
22+
#[serde(skip_serializing_if = "Option::is_none")]
23+
pub favorite_projects: Option<Vec<String>>,
1824
pub current_project_id: Option<String>,
1925
}
2026

@@ -32,6 +38,13 @@ pub struct ProjectResponse {
3238
pub added_at: String,
3339
}
3440

41+
/// Project detail wrapper response
42+
#[derive(Debug, Serialize)]
43+
#[serde(rename_all = "camelCase")]
44+
pub struct SingleProjectResponse {
45+
pub project: ProjectResponse,
46+
}
47+
3548
impl From<&Project> for ProjectResponse {
3649
fn from(p: &Project) -> Self {
3750
Self {
@@ -52,9 +65,19 @@ pub async fn list_projects(State(state): State<AppState>) -> Json<ProjectsListRe
5265
let registry = state.registry.read().await;
5366
let projects: Vec<ProjectResponse> = registry.all().iter().map(|p| (*p).into()).collect();
5467
let current_project_id = registry.current_id().map(|s| s.to_string());
68+
let mode = if projects.len() > 1 {
69+
Some("multi-project".to_string())
70+
} else {
71+
Some("single-project".to_string())
72+
};
73+
let recent_projects = Some(registry.recent(5).iter().map(|p| p.id.clone()).collect());
74+
let favorite_projects = Some(registry.favorites().iter().map(|p| p.id.clone()).collect());
5575

5676
Json(ProjectsListResponse {
5777
projects,
78+
mode,
79+
recent_projects,
80+
favorite_projects,
5881
current_project_id,
5982
})
6083
}
@@ -88,7 +111,7 @@ pub async fn add_project(
88111
pub async fn get_project(
89112
State(state): State<AppState>,
90113
Path(id): Path<String>,
91-
) -> ApiResult<Json<ProjectResponse>> {
114+
) -> ApiResult<Json<SingleProjectResponse>> {
92115
let registry = state.registry.read().await;
93116
let project = registry.get(&id).ok_or_else(|| {
94117
(
@@ -97,7 +120,9 @@ pub async fn get_project(
97120
)
98121
})?;
99122

100-
Ok(Json(project.into()))
123+
Ok(Json(SingleProjectResponse {
124+
project: project.into(),
125+
}))
101126
}
102127

103128
/// PATCH /api/projects/:id - Update a project

0 commit comments

Comments
 (0)