beads-kanban-ui 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /package/{npm/scripts → scripts}/postinstall.js +0 -0
package/server/src/routes/fs.rs
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
//! Filesystem API route handlers.
|
|
2
|
-
//!
|
|
3
|
-
//! Provides endpoints for listing directories and checking path existence.
|
|
4
|
-
|
|
5
|
-
use axum::{
|
|
6
|
-
extract::Query,
|
|
7
|
-
http::StatusCode,
|
|
8
|
-
response::IntoResponse,
|
|
9
|
-
Json,
|
|
10
|
-
};
|
|
11
|
-
use serde::{Deserialize, Serialize};
|
|
12
|
-
use std::path::PathBuf;
|
|
13
|
-
|
|
14
|
-
use super::validate_path_security;
|
|
15
|
-
|
|
16
|
-
/// Query parameters for the list directory endpoint.
|
|
17
|
-
#[derive(Debug, Deserialize)]
|
|
18
|
-
pub struct FsListParams {
|
|
19
|
-
/// The directory path to list
|
|
20
|
-
pub path: String,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/// Query parameters for the path exists endpoint.
|
|
24
|
-
#[derive(Debug, Deserialize)]
|
|
25
|
-
pub struct FsExistsParams {
|
|
26
|
-
/// The path to check for existence
|
|
27
|
-
pub path: String,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Query parameters for the read file endpoint.
|
|
31
|
-
#[derive(Debug, Deserialize)]
|
|
32
|
-
pub struct FsReadParams {
|
|
33
|
-
/// The file path to read (relative, e.g., ".designs/epic.md")
|
|
34
|
-
pub path: String,
|
|
35
|
-
/// The project path (absolute directory path)
|
|
36
|
-
pub project_path: String,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// Request body for opening a path in an external application.
|
|
40
|
-
#[derive(Debug, Deserialize)]
|
|
41
|
-
pub struct OpenExternalRequest {
|
|
42
|
-
/// The path to open
|
|
43
|
-
pub path: String,
|
|
44
|
-
/// Target application: "vscode", "cursor", or "finder"
|
|
45
|
-
pub target: String,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/// A single directory entry.
|
|
49
|
-
#[derive(Debug, Serialize)]
|
|
50
|
-
pub struct DirectoryEntry {
|
|
51
|
-
/// The file/directory name
|
|
52
|
-
pub name: String,
|
|
53
|
-
/// The full path
|
|
54
|
-
pub path: String,
|
|
55
|
-
/// Whether this entry is a directory
|
|
56
|
-
#[serde(rename = "isDirectory")]
|
|
57
|
-
pub is_directory: bool,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/// GET /api/fs/list?path=/some/directory
|
|
61
|
-
///
|
|
62
|
-
/// Lists the contents of a directory, filtering out hidden files
|
|
63
|
-
/// except for .beads directories.
|
|
64
|
-
pub async fn list_directory(Query(params): Query<FsListParams>) -> impl IntoResponse {
|
|
65
|
-
let dir_path = PathBuf::from(¶ms.path);
|
|
66
|
-
|
|
67
|
-
// Security: Validate path is within allowed directories
|
|
68
|
-
if let Err(e) = validate_path_security(&dir_path) {
|
|
69
|
-
return (
|
|
70
|
-
StatusCode::FORBIDDEN,
|
|
71
|
-
Json(serde_json::json!({ "error": e })),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check if path exists and is a directory
|
|
76
|
-
if !dir_path.exists() {
|
|
77
|
-
return (
|
|
78
|
-
StatusCode::NOT_FOUND,
|
|
79
|
-
Json(serde_json::json!({ "error": "Path does not exist" })),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if !dir_path.is_dir() {
|
|
84
|
-
return (
|
|
85
|
-
StatusCode::BAD_REQUEST,
|
|
86
|
-
Json(serde_json::json!({ "error": "Path is not a directory" })),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Read directory entries
|
|
91
|
-
let read_dir = match std::fs::read_dir(&dir_path) {
|
|
92
|
-
Ok(rd) => rd,
|
|
93
|
-
Err(e) => {
|
|
94
|
-
return (
|
|
95
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
96
|
-
Json(serde_json::json!({ "error": format!("Failed to read directory: {}", e) })),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
let mut entries: Vec<DirectoryEntry> = Vec::new();
|
|
102
|
-
|
|
103
|
-
for entry_result in read_dir {
|
|
104
|
-
let entry = match entry_result {
|
|
105
|
-
Ok(e) => e,
|
|
106
|
-
Err(e) => {
|
|
107
|
-
tracing::warn!("Failed to read directory entry: {}", e);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
let name = entry.file_name().to_string_lossy().to_string();
|
|
113
|
-
|
|
114
|
-
// Filter out hidden files except .beads
|
|
115
|
-
if name.starts_with('.') && name != ".beads" {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let path = entry.path();
|
|
120
|
-
let is_directory = path.is_dir();
|
|
121
|
-
|
|
122
|
-
entries.push(DirectoryEntry {
|
|
123
|
-
name,
|
|
124
|
-
path: path.to_string_lossy().to_string(),
|
|
125
|
-
is_directory,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Sort entries: directories first, then alphabetically
|
|
130
|
-
entries.sort_by(|a, b| {
|
|
131
|
-
match (a.is_directory, b.is_directory) {
|
|
132
|
-
(true, false) => std::cmp::Ordering::Less,
|
|
133
|
-
(false, true) => std::cmp::Ordering::Greater,
|
|
134
|
-
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
(StatusCode::OK, Json(serde_json::json!({ "entries": entries })))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/// GET /api/fs/exists?path=/some/path
|
|
142
|
-
///
|
|
143
|
-
/// Checks if a path exists on the filesystem.
|
|
144
|
-
pub async fn path_exists(Query(params): Query<FsExistsParams>) -> impl IntoResponse {
|
|
145
|
-
let path = PathBuf::from(¶ms.path);
|
|
146
|
-
|
|
147
|
-
// Security: Validate path is within allowed directories
|
|
148
|
-
if let Err(e) = validate_path_security(&path) {
|
|
149
|
-
return (
|
|
150
|
-
StatusCode::FORBIDDEN,
|
|
151
|
-
Json(serde_json::json!({ "error": e })),
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let exists = path.exists();
|
|
156
|
-
|
|
157
|
-
(StatusCode::OK, Json(serde_json::json!({ "exists": exists })))
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/// GET /api/fs/read?path=.designs/{EPIC_ID}.md&project_path=/absolute/path
|
|
161
|
-
///
|
|
162
|
-
/// Reads a design document file from the .designs directory.
|
|
163
|
-
///
|
|
164
|
-
/// # Security constraints:
|
|
165
|
-
/// - Max file size: 100KB
|
|
166
|
-
/// - Only .md extension allowed
|
|
167
|
-
/// - Path must be within project directory
|
|
168
|
-
/// - Path must start with ".designs/"
|
|
169
|
-
pub async fn read_file(Query(params): Query<FsReadParams>) -> impl IntoResponse {
|
|
170
|
-
// Security: Path must start with .designs/
|
|
171
|
-
if !params.path.starts_with(".designs/") {
|
|
172
|
-
return (
|
|
173
|
-
StatusCode::FORBIDDEN,
|
|
174
|
-
Json(serde_json::json!({
|
|
175
|
-
"error": "Access denied: path must start with .designs/"
|
|
176
|
-
})),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Parse relative path to validate extension
|
|
181
|
-
let relative_path = PathBuf::from(¶ms.path);
|
|
182
|
-
|
|
183
|
-
// Security: Only .md extension allowed
|
|
184
|
-
if relative_path.extension().and_then(|s| s.to_str()) != Some("md") {
|
|
185
|
-
return (
|
|
186
|
-
StatusCode::FORBIDDEN,
|
|
187
|
-
Json(serde_json::json!({
|
|
188
|
-
"error": "Access denied: only .md files are allowed"
|
|
189
|
-
})),
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Join project path with relative design doc path to get absolute path
|
|
194
|
-
let project_root = PathBuf::from(¶ms.project_path);
|
|
195
|
-
let file_path = project_root.join(¶ms.path);
|
|
196
|
-
|
|
197
|
-
// Security: Validate absolute path is within allowed directories
|
|
198
|
-
if let Err(e) = validate_path_security(&file_path) {
|
|
199
|
-
return (
|
|
200
|
-
StatusCode::FORBIDDEN,
|
|
201
|
-
Json(serde_json::json!({ "error": e })),
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Check if file exists
|
|
206
|
-
if !file_path.exists() {
|
|
207
|
-
return (
|
|
208
|
-
StatusCode::NOT_FOUND,
|
|
209
|
-
Json(serde_json::json!({ "error": "File does not exist" })),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check if path is a file (not a directory)
|
|
214
|
-
if !file_path.is_file() {
|
|
215
|
-
return (
|
|
216
|
-
StatusCode::BAD_REQUEST,
|
|
217
|
-
Json(serde_json::json!({ "error": "Path is not a file" })),
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Security: Check file size (max 100KB)
|
|
222
|
-
let metadata = match std::fs::metadata(&file_path) {
|
|
223
|
-
Ok(m) => m,
|
|
224
|
-
Err(e) => {
|
|
225
|
-
return (
|
|
226
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
227
|
-
Json(serde_json::json!({
|
|
228
|
-
"error": format!("Failed to read file metadata: {}", e)
|
|
229
|
-
})),
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const MAX_FILE_SIZE: u64 = 100 * 1024; // 100KB
|
|
235
|
-
if metadata.len() > MAX_FILE_SIZE {
|
|
236
|
-
return (
|
|
237
|
-
StatusCode::PAYLOAD_TOO_LARGE,
|
|
238
|
-
Json(serde_json::json!({
|
|
239
|
-
"error": format!("File too large: {} bytes (max {} bytes)", metadata.len(), MAX_FILE_SIZE)
|
|
240
|
-
})),
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Read file contents
|
|
245
|
-
let contents = match std::fs::read_to_string(&file_path) {
|
|
246
|
-
Ok(c) => c,
|
|
247
|
-
Err(e) => {
|
|
248
|
-
return (
|
|
249
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
250
|
-
Json(serde_json::json!({
|
|
251
|
-
"error": format!("Failed to read file: {}", e)
|
|
252
|
-
})),
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
(
|
|
258
|
-
StatusCode::OK,
|
|
259
|
-
Json(serde_json::json!({
|
|
260
|
-
"content": contents,
|
|
261
|
-
"path": params.path
|
|
262
|
-
})),
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/// POST /api/fs/open-external
|
|
267
|
-
///
|
|
268
|
-
/// Opens a path in an external application (VS Code, Cursor, or Finder/Explorer).
|
|
269
|
-
///
|
|
270
|
-
/// # Security constraints:
|
|
271
|
-
/// - Path must be within user's home directory
|
|
272
|
-
/// - Target must be one of: "vscode", "cursor", "finder"
|
|
273
|
-
pub async fn open_external(Json(request): Json<OpenExternalRequest>) -> impl IntoResponse {
|
|
274
|
-
let path = PathBuf::from(&request.path);
|
|
275
|
-
|
|
276
|
-
// Security: Validate path is within allowed directories
|
|
277
|
-
if let Err(e) = validate_path_security(&path) {
|
|
278
|
-
return (
|
|
279
|
-
StatusCode::FORBIDDEN,
|
|
280
|
-
Json(serde_json::json!({ "error": e })),
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check if path exists
|
|
285
|
-
if !path.exists() {
|
|
286
|
-
return (
|
|
287
|
-
StatusCode::NOT_FOUND,
|
|
288
|
-
Json(serde_json::json!({ "error": "Path does not exist" })),
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Execute the appropriate command based on target
|
|
293
|
-
let result = match request.target.as_str() {
|
|
294
|
-
"vscode" => {
|
|
295
|
-
// Use "code" command for VS Code
|
|
296
|
-
std::process::Command::new("code").arg(&path).spawn()
|
|
297
|
-
}
|
|
298
|
-
"cursor" => {
|
|
299
|
-
// Use "cursor" command for Cursor
|
|
300
|
-
std::process::Command::new("cursor").arg(&path).spawn()
|
|
301
|
-
}
|
|
302
|
-
"finder" => {
|
|
303
|
-
// Use the `open` crate for cross-platform support
|
|
304
|
-
// On macOS: opens Finder, on Linux: file manager, on Windows: Explorer
|
|
305
|
-
match open::that(&path) {
|
|
306
|
-
Ok(_) => {
|
|
307
|
-
return (
|
|
308
|
-
StatusCode::OK,
|
|
309
|
-
Json(serde_json::json!({ "success": true })),
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
Err(e) => {
|
|
313
|
-
return (
|
|
314
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
315
|
-
Json(serde_json::json!({
|
|
316
|
-
"error": format!("Failed to open: {}", e)
|
|
317
|
-
})),
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
_ => {
|
|
323
|
-
return (
|
|
324
|
-
StatusCode::BAD_REQUEST,
|
|
325
|
-
Json(serde_json::json!({
|
|
326
|
-
"error": "Invalid target. Must be 'vscode', 'cursor', or 'finder'"
|
|
327
|
-
})),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
match result {
|
|
333
|
-
Ok(_) => (
|
|
334
|
-
StatusCode::OK,
|
|
335
|
-
Json(serde_json::json!({ "success": true })),
|
|
336
|
-
),
|
|
337
|
-
Err(e) => (
|
|
338
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
339
|
-
Json(serde_json::json!({
|
|
340
|
-
"error": format!("Failed to open: {}. Make sure the application is installed.", e)
|
|
341
|
-
})),
|
|
342
|
-
),
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
#[cfg(test)]
|
|
347
|
-
mod tests {
|
|
348
|
-
use super::*;
|
|
349
|
-
|
|
350
|
-
#[test]
|
|
351
|
-
fn test_directory_entry_serialization() {
|
|
352
|
-
let entry = DirectoryEntry {
|
|
353
|
-
name: "test".to_string(),
|
|
354
|
-
path: "/home/user/test".to_string(),
|
|
355
|
-
is_directory: true,
|
|
356
|
-
};
|
|
357
|
-
let json = serde_json::to_string(&entry).unwrap();
|
|
358
|
-
assert!(json.contains("\"isDirectory\":true"));
|
|
359
|
-
}
|
|
360
|
-
}
|
package/server/src/routes/git.rs
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
//! Git route handlers for checking repository status.
|
|
2
|
-
//!
|
|
3
|
-
//! Provides endpoints for querying git branch status and repository state.
|
|
4
|
-
|
|
5
|
-
use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json};
|
|
6
|
-
use serde::{Deserialize, Serialize};
|
|
7
|
-
use std::path::Path;
|
|
8
|
-
use tokio::process::Command;
|
|
9
|
-
|
|
10
|
-
/// Query parameters for the branch status endpoint.
|
|
11
|
-
#[derive(Deserialize)]
|
|
12
|
-
pub struct GitStatusParams {
|
|
13
|
-
/// Path to the git repository.
|
|
14
|
-
pub path: String,
|
|
15
|
-
/// Branch name to check status for.
|
|
16
|
-
pub branch: String,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/// Response body for the branch status endpoint.
|
|
20
|
-
#[derive(Serialize)]
|
|
21
|
-
pub struct BranchStatusResponse {
|
|
22
|
-
/// Whether the branch exists.
|
|
23
|
-
pub exists: bool,
|
|
24
|
-
/// Number of commits ahead of main.
|
|
25
|
-
pub ahead: i32,
|
|
26
|
-
/// Number of commits behind main.
|
|
27
|
-
pub behind: i32,
|
|
28
|
-
/// Whether there are uncommitted changes.
|
|
29
|
-
pub dirty: bool,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// Get the status of a git branch relative to main.
|
|
33
|
-
///
|
|
34
|
-
/// # Endpoint
|
|
35
|
-
///
|
|
36
|
-
/// `GET /api/git/branch-status?path=...&branch=...`
|
|
37
|
-
///
|
|
38
|
-
/// # Response
|
|
39
|
-
///
|
|
40
|
-
/// Returns branch existence, ahead/behind counts, and dirty status.
|
|
41
|
-
pub async fn branch_status(Query(params): Query<GitStatusParams>) -> impl IntoResponse {
|
|
42
|
-
let repo_path = Path::new(¶ms.path);
|
|
43
|
-
|
|
44
|
-
// Validate repository path exists
|
|
45
|
-
if !repo_path.exists() {
|
|
46
|
-
return (
|
|
47
|
-
StatusCode::BAD_REQUEST,
|
|
48
|
-
Json(serde_json::json!({
|
|
49
|
-
"error": format!("Repository path does not exist: {}", params.path)
|
|
50
|
-
})),
|
|
51
|
-
)
|
|
52
|
-
.into_response();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if !repo_path.is_dir() {
|
|
56
|
-
return (
|
|
57
|
-
StatusCode::BAD_REQUEST,
|
|
58
|
-
Json(serde_json::json!({
|
|
59
|
-
"error": format!("Path is not a directory: {}", params.path)
|
|
60
|
-
})),
|
|
61
|
-
)
|
|
62
|
-
.into_response();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Check if branch exists
|
|
66
|
-
let branch_exists = check_branch_exists(¶ms.path, ¶ms.branch).await;
|
|
67
|
-
|
|
68
|
-
if !branch_exists {
|
|
69
|
-
return Json(BranchStatusResponse {
|
|
70
|
-
exists: false,
|
|
71
|
-
ahead: 0,
|
|
72
|
-
behind: 0,
|
|
73
|
-
dirty: false,
|
|
74
|
-
})
|
|
75
|
-
.into_response();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Get ahead/behind counts relative to main
|
|
79
|
-
let (ahead, behind) = get_ahead_behind(¶ms.path, ¶ms.branch).await;
|
|
80
|
-
|
|
81
|
-
// Check for uncommitted changes
|
|
82
|
-
let dirty = check_dirty(¶ms.path).await;
|
|
83
|
-
|
|
84
|
-
Json(BranchStatusResponse {
|
|
85
|
-
exists: true,
|
|
86
|
-
ahead,
|
|
87
|
-
behind,
|
|
88
|
-
dirty,
|
|
89
|
-
})
|
|
90
|
-
.into_response()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/// Check if a branch exists in the repository.
|
|
94
|
-
async fn check_branch_exists(repo_path: &str, branch: &str) -> bool {
|
|
95
|
-
let output = Command::new("git")
|
|
96
|
-
.args(["rev-parse", "--verify", branch])
|
|
97
|
-
.current_dir(repo_path)
|
|
98
|
-
.output()
|
|
99
|
-
.await;
|
|
100
|
-
|
|
101
|
-
matches!(output, Ok(o) if o.status.success())
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// Get the number of commits ahead and behind relative to main.
|
|
105
|
-
async fn get_ahead_behind(repo_path: &str, branch: &str) -> (i32, i32) {
|
|
106
|
-
// Try both 'main' and 'master' as the base branch
|
|
107
|
-
let base_branches = ["main", "master"];
|
|
108
|
-
|
|
109
|
-
for base in base_branches {
|
|
110
|
-
let output = Command::new("git")
|
|
111
|
-
.args([
|
|
112
|
-
"rev-list",
|
|
113
|
-
"--left-right",
|
|
114
|
-
"--count",
|
|
115
|
-
&format!("{}...{}", base, branch),
|
|
116
|
-
])
|
|
117
|
-
.current_dir(repo_path)
|
|
118
|
-
.output()
|
|
119
|
-
.await;
|
|
120
|
-
|
|
121
|
-
if let Ok(output) = output {
|
|
122
|
-
if output.status.success() {
|
|
123
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
124
|
-
let parts: Vec<&str> = stdout.trim().split('\t').collect();
|
|
125
|
-
if parts.len() == 2 {
|
|
126
|
-
let behind = parts[0].parse().unwrap_or(0);
|
|
127
|
-
let ahead = parts[1].parse().unwrap_or(0);
|
|
128
|
-
return (ahead, behind);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
(0, 0)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/// Check if the repository has uncommitted changes.
|
|
138
|
-
async fn check_dirty(repo_path: &str) -> bool {
|
|
139
|
-
let output = Command::new("git")
|
|
140
|
-
.args(["status", "--porcelain"])
|
|
141
|
-
.current_dir(repo_path)
|
|
142
|
-
.output()
|
|
143
|
-
.await;
|
|
144
|
-
|
|
145
|
-
match output {
|
|
146
|
-
Ok(o) => !o.stdout.is_empty(),
|
|
147
|
-
Err(_) => false,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
#[cfg(test)]
|
|
152
|
-
mod tests {
|
|
153
|
-
use super::*;
|
|
154
|
-
|
|
155
|
-
#[test]
|
|
156
|
-
fn test_branch_status_response_serialization() {
|
|
157
|
-
let response = BranchStatusResponse {
|
|
158
|
-
exists: true,
|
|
159
|
-
ahead: 5,
|
|
160
|
-
behind: 2,
|
|
161
|
-
dirty: false,
|
|
162
|
-
};
|
|
163
|
-
let json = serde_json::to_string(&response).unwrap();
|
|
164
|
-
assert!(json.contains("\"exists\":true"));
|
|
165
|
-
assert!(json.contains("\"ahead\":5"));
|
|
166
|
-
assert!(json.contains("\"behind\":2"));
|
|
167
|
-
assert!(json.contains("\"dirty\":false"));
|
|
168
|
-
}
|
|
169
|
-
}
|
package/server/src/routes/mod.rs
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
//! Route handlers for the beads-server API.
|
|
2
|
-
//!
|
|
3
|
-
//! This module contains all HTTP route handlers.
|
|
4
|
-
//! Additional handlers will be added as API endpoints are implemented.
|
|
5
|
-
|
|
6
|
-
pub mod beads;
|
|
7
|
-
pub mod cli;
|
|
8
|
-
pub mod fs;
|
|
9
|
-
pub mod git;
|
|
10
|
-
pub mod projects;
|
|
11
|
-
pub mod watch;
|
|
12
|
-
|
|
13
|
-
pub use projects::project_routes;
|
|
14
|
-
pub use watch::watch_beads;
|
|
15
|
-
|
|
16
|
-
use axum::{response::IntoResponse, Json};
|
|
17
|
-
use directories::UserDirs;
|
|
18
|
-
use serde::Serialize;
|
|
19
|
-
use std::path::Path;
|
|
20
|
-
|
|
21
|
-
/// Health check response structure.
|
|
22
|
-
#[derive(Serialize)]
|
|
23
|
-
pub struct HealthResponse {
|
|
24
|
-
pub status: &'static str,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Health check endpoint handler.
|
|
28
|
-
///
|
|
29
|
-
/// Returns a JSON response indicating the server is running.
|
|
30
|
-
pub async fn health() -> impl IntoResponse {
|
|
31
|
-
Json(HealthResponse { status: "ok" })
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/// Validates that a path is within allowed directories (user home).
|
|
35
|
-
///
|
|
36
|
-
/// # Security
|
|
37
|
-
///
|
|
38
|
-
/// This function ensures that:
|
|
39
|
-
/// - The path is within the user's home directory
|
|
40
|
-
/// - No path traversal attacks are possible
|
|
41
|
-
///
|
|
42
|
-
/// # Returns
|
|
43
|
-
///
|
|
44
|
-
/// - `Ok(())` if the path is valid and within allowed directories
|
|
45
|
-
/// - `Err(String)` with an error message if validation fails
|
|
46
|
-
pub fn validate_path_security(path: &Path) -> Result<(), String> {
|
|
47
|
-
// Get the user's home directory
|
|
48
|
-
let user_dirs = match UserDirs::new() {
|
|
49
|
-
Some(u) => u,
|
|
50
|
-
None => return Err("Could not determine user directories".to_string()),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
let home_dir = user_dirs.home_dir();
|
|
54
|
-
|
|
55
|
-
// Canonicalize paths for comparison (resolves symlinks and ..)
|
|
56
|
-
let canonical_path = match path.canonicalize() {
|
|
57
|
-
Ok(p) => p,
|
|
58
|
-
Err(_) => {
|
|
59
|
-
// If path doesn't exist yet, check the parent
|
|
60
|
-
if let Some(parent) = path.parent() {
|
|
61
|
-
match parent.canonicalize() {
|
|
62
|
-
Ok(p) => p.join(path.file_name().unwrap_or_default()),
|
|
63
|
-
Err(_) => return Err("Invalid path".to_string()),
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
return Err("Invalid path".to_string());
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
let canonical_home = match home_dir.canonicalize() {
|
|
72
|
-
Ok(h) => h,
|
|
73
|
-
Err(_) => return Err("Could not canonicalize home directory".to_string()),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Check if the path starts with the home directory
|
|
77
|
-
if !canonical_path.starts_with(&canonical_home) {
|
|
78
|
-
return Err("Access denied: path must be within home directory".to_string());
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
Ok(())
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
#[cfg(test)]
|
|
85
|
-
mod tests {
|
|
86
|
-
use super::*;
|
|
87
|
-
use std::path::PathBuf;
|
|
88
|
-
|
|
89
|
-
#[test]
|
|
90
|
-
fn test_validate_home_path() {
|
|
91
|
-
if let Some(user_dirs) = UserDirs::new() {
|
|
92
|
-
let test_path = user_dirs.home_dir().join("test");
|
|
93
|
-
// This might fail if test doesn't exist, but the parent check should work
|
|
94
|
-
let result = validate_path_security(&test_path);
|
|
95
|
-
// Should either succeed or fail with "Invalid path" (if test doesn't exist)
|
|
96
|
-
assert!(result.is_ok() || result.unwrap_err().contains("Invalid"));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
#[test]
|
|
101
|
-
fn test_reject_outside_home() {
|
|
102
|
-
let result = validate_path_security(&PathBuf::from("/etc/passwd"));
|
|
103
|
-
assert!(result.is_err());
|
|
104
|
-
let err_msg = result.unwrap_err();
|
|
105
|
-
assert!(err_msg.contains("denied") || err_msg.contains("Invalid"));
|
|
106
|
-
}
|
|
107
|
-
}
|