idea-manager 0.7.8 → 0.7.9
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/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getProject } from '@/lib/db/queries/projects';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import type { IGitSyncResult } from '@/types';
|
|
7
|
+
|
|
8
|
+
function gitPull(cwd: string): Promise<{ stdout: string; stderr: string }> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
execFile('git', ['pull'], { cwd, timeout: 15000 }, (err, stdout, stderr) => {
|
|
11
|
+
if (err) reject(err);
|
|
12
|
+
else resolve({ stdout, stderr });
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function syncOneRepo(dirPath: string, name: string): Promise<IGitSyncResult> {
|
|
18
|
+
try {
|
|
19
|
+
const { stdout, stderr } = await gitPull(dirPath);
|
|
20
|
+
const message = (stdout || stderr || '').trim().slice(0, 500);
|
|
21
|
+
return {
|
|
22
|
+
projectId: name,
|
|
23
|
+
projectName: name,
|
|
24
|
+
projectPath: dirPath,
|
|
25
|
+
status: 'success',
|
|
26
|
+
message: message || 'Already up to date.',
|
|
27
|
+
};
|
|
28
|
+
} catch (err: unknown) {
|
|
29
|
+
const message = err instanceof Error ? err.message.slice(0, 500) : 'Unknown error';
|
|
30
|
+
return {
|
|
31
|
+
projectId: name,
|
|
32
|
+
projectName: name,
|
|
33
|
+
projectPath: dirPath,
|
|
34
|
+
status: 'error',
|
|
35
|
+
message,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findGitRepos(rootPath: string): { name: string; path: string }[] {
|
|
41
|
+
// If root itself is a git repo
|
|
42
|
+
if (existsSync(path.join(rootPath, '.git'))) {
|
|
43
|
+
return [{ name: path.basename(rootPath), path: rootPath }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Otherwise scan immediate subdirectories
|
|
47
|
+
const repos: { name: string; path: string }[] = [];
|
|
48
|
+
try {
|
|
49
|
+
const entries = readdirSync(rootPath);
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.startsWith('.')) continue;
|
|
52
|
+
const fullPath = path.join(rootPath, entry);
|
|
53
|
+
try {
|
|
54
|
+
if (statSync(fullPath).isDirectory() && existsSync(path.join(fullPath, '.git'))) {
|
|
55
|
+
repos.push({ name: entry, path: fullPath });
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// skip inaccessible dirs
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// skip
|
|
63
|
+
}
|
|
64
|
+
return repos;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function POST(
|
|
68
|
+
_request: NextRequest,
|
|
69
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
70
|
+
) {
|
|
71
|
+
const { id } = await params;
|
|
72
|
+
const project = getProject(id);
|
|
73
|
+
|
|
74
|
+
if (!project) {
|
|
75
|
+
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!project.project_path) {
|
|
79
|
+
return NextResponse.json([{
|
|
80
|
+
projectId: project.id,
|
|
81
|
+
projectName: project.name,
|
|
82
|
+
projectPath: '',
|
|
83
|
+
status: 'no-path',
|
|
84
|
+
message: 'No folder linked',
|
|
85
|
+
}] satisfies IGitSyncResult[]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectPath = project.project_path;
|
|
89
|
+
|
|
90
|
+
if (!existsSync(projectPath)) {
|
|
91
|
+
return NextResponse.json([{
|
|
92
|
+
projectId: project.id,
|
|
93
|
+
projectName: project.name,
|
|
94
|
+
projectPath,
|
|
95
|
+
status: 'error',
|
|
96
|
+
message: 'Directory not found',
|
|
97
|
+
}] satisfies IGitSyncResult[]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const repos = findGitRepos(projectPath);
|
|
101
|
+
|
|
102
|
+
if (repos.length === 0) {
|
|
103
|
+
return NextResponse.json([{
|
|
104
|
+
projectId: project.id,
|
|
105
|
+
projectName: project.name,
|
|
106
|
+
projectPath,
|
|
107
|
+
status: 'no-git',
|
|
108
|
+
message: 'No git repositories found',
|
|
109
|
+
}] satisfies IGitSyncResult[]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const results: IGitSyncResult[] = [];
|
|
113
|
+
for (const repo of repos) {
|
|
114
|
+
results.push(await syncOneRepo(repo.path, repo.name));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return NextResponse.json(results);
|
|
118
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import type { IGitSyncResult } from '@/types';
|
|
5
|
+
|
|
6
|
+
const STATUS_STYLE: Record<IGitSyncResult['status'], { icon: string; color: string }> = {
|
|
7
|
+
success: { icon: '\u2705', color: 'text-success' },
|
|
8
|
+
error: { icon: '\u274C', color: 'text-destructive' },
|
|
9
|
+
'no-git': { icon: '\u2796', color: 'text-muted-foreground' },
|
|
10
|
+
'no-path': { icon: '\u2796', color: 'text-muted-foreground' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function GitSyncResultsModal({
|
|
14
|
+
open,
|
|
15
|
+
results,
|
|
16
|
+
onClose,
|
|
17
|
+
}: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
results: IGitSyncResult[];
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!open) return;
|
|
26
|
+
const handler = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') onClose();
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener('keydown', handler);
|
|
30
|
+
return () => window.removeEventListener('keydown', handler);
|
|
31
|
+
}, [open, onClose]);
|
|
32
|
+
|
|
33
|
+
if (!open) return null;
|
|
34
|
+
|
|
35
|
+
const successCount = results.filter(r => r.status === 'success').length;
|
|
36
|
+
const errorCount = results.filter(r => r.status === 'error').length;
|
|
37
|
+
const skipCount = results.filter(r => r.status === 'no-git' || r.status === 'no-path').length;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
ref={overlayRef}
|
|
42
|
+
onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}
|
|
43
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
44
|
+
style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(2px)' }}
|
|
45
|
+
>
|
|
46
|
+
<div className="bg-card border border-border rounded-xl shadow-2xl shadow-black/40
|
|
47
|
+
w-full max-w-md mx-4 animate-dialog-in">
|
|
48
|
+
<div className="p-5 border-b border-border">
|
|
49
|
+
<h3 className="text-sm font-semibold text-foreground">Git Sync Results</h3>
|
|
50
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
51
|
+
{successCount} synced, {errorCount} failed, {skipCount} skipped
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="max-h-64 overflow-y-auto p-3 space-y-1.5">
|
|
56
|
+
{results.length === 0 ? (
|
|
57
|
+
<p className="text-xs text-muted-foreground text-center py-4">
|
|
58
|
+
No projects with linked folders
|
|
59
|
+
</p>
|
|
60
|
+
) : (
|
|
61
|
+
results.map((r) => {
|
|
62
|
+
const style = STATUS_STYLE[r.status];
|
|
63
|
+
return (
|
|
64
|
+
<div key={r.projectId} className="flex items-start gap-2 p-2 rounded-lg bg-muted/50">
|
|
65
|
+
<span className="text-sm flex-shrink-0">{style.icon}</span>
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<div className="text-xs font-medium truncate">{r.projectName}</div>
|
|
68
|
+
<div className={`text-xs ${style.color} truncate`} title={r.message}>
|
|
69
|
+
{r.message}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
})
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="flex justify-end px-5 pb-4 pt-2">
|
|
79
|
+
<button
|
|
80
|
+
onClick={onClose}
|
|
81
|
+
className="px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground
|
|
82
|
+
bg-muted hover:bg-card-hover border border-border rounded-md transition-colors"
|
|
83
|
+
>
|
|
84
|
+
Close
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -8,7 +8,8 @@ import TaskDetail from '@/components/task/TaskDetail';
|
|
|
8
8
|
import DirectoryPicker from '@/components/DirectoryPicker';
|
|
9
9
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
10
10
|
import AiPolicyModal from '@/components/ui/AiPolicyModal';
|
|
11
|
-
import
|
|
11
|
+
import GitSyncResultsModal from '@/components/dashboard/GitSyncResultsModal';
|
|
12
|
+
import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats, IGitSyncResult } from '@/types';
|
|
12
13
|
|
|
13
14
|
interface IProject {
|
|
14
15
|
id: string;
|
|
@@ -58,6 +59,10 @@ export default function WorkspacePanel({
|
|
|
58
59
|
const [showBrainstorm, setShowBrainstorm] = useState(true);
|
|
59
60
|
const [newSubName, setNewSubName] = useState('');
|
|
60
61
|
const [showAiPolicy, setShowAiPolicy] = useState(false);
|
|
62
|
+
const [syncing, setSyncing] = useState(false);
|
|
63
|
+
const [syncResults, setSyncResults] = useState<IGitSyncResult[] | null>(null);
|
|
64
|
+
const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);
|
|
65
|
+
const syncingRef = useRef(false);
|
|
61
66
|
|
|
62
67
|
// Resizable panel widths
|
|
63
68
|
const [leftWidth, setLeftWidth] = useState(500);
|
|
@@ -279,6 +284,35 @@ export default function WorkspacePanel({
|
|
|
279
284
|
}
|
|
280
285
|
};
|
|
281
286
|
|
|
287
|
+
const handleGitSync = useCallback(async (silent = false) => {
|
|
288
|
+
if (syncingRef.current) return;
|
|
289
|
+
syncingRef.current = true;
|
|
290
|
+
if (!silent) setSyncing(true);
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch(`/api/projects/${id}/git-sync`, { method: 'POST' });
|
|
293
|
+
if (res.ok) {
|
|
294
|
+
const results: IGitSyncResult[] = await res.json();
|
|
295
|
+
setLastSyncTime(new Date());
|
|
296
|
+
if (!silent) {
|
|
297
|
+
setSyncResults(results);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// silent fail
|
|
302
|
+
} finally {
|
|
303
|
+
syncingRef.current = false;
|
|
304
|
+
if (!silent) setSyncing(false);
|
|
305
|
+
}
|
|
306
|
+
}, [id]);
|
|
307
|
+
|
|
308
|
+
// Auto git-sync every 30 minutes
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (!project?.project_path) return;
|
|
311
|
+
const INTERVAL_MS = 30 * 60 * 1000;
|
|
312
|
+
const timer = setInterval(() => handleGitSync(true), INTERVAL_MS);
|
|
313
|
+
return () => clearInterval(timer);
|
|
314
|
+
}, [project?.project_path, handleGitSync]);
|
|
315
|
+
|
|
282
316
|
const handleToggleWatch = async () => {
|
|
283
317
|
if (!project) return;
|
|
284
318
|
const res = await fetch(`/api/projects/${id}`, {
|
|
@@ -370,7 +404,24 @@ export default function WorkspacePanel({
|
|
|
370
404
|
}`}>
|
|
371
405
|
AI Policy{project.ai_context ? ' *' : ''}
|
|
372
406
|
</button>
|
|
373
|
-
{
|
|
407
|
+
{project.project_path ? (
|
|
408
|
+
<div className="flex items-center gap-1.5">
|
|
409
|
+
<button
|
|
410
|
+
onClick={() => handleGitSync(false)}
|
|
411
|
+
disabled={syncing}
|
|
412
|
+
className="px-3 py-1.5 text-xs bg-muted hover:bg-card-hover text-foreground border border-border rounded-md transition-colors disabled:opacity-50 flex items-center gap-1.5"
|
|
413
|
+
title={lastSyncTime ? `Last sync: ${lastSyncTime.toLocaleTimeString()}` : 'Git pull'}
|
|
414
|
+
>
|
|
415
|
+
<span className={syncing ? 'animate-spin' : ''}>↻</span>
|
|
416
|
+
{syncing ? 'Syncing...' : 'Git Sync'}
|
|
417
|
+
</button>
|
|
418
|
+
{lastSyncTime && (
|
|
419
|
+
<span className="text-xs text-muted-foreground">
|
|
420
|
+
{lastSyncTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
421
|
+
</span>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
) : (
|
|
374
425
|
<button onClick={() => setShowDirPicker(true)}
|
|
375
426
|
className="px-3 py-1.5 text-xs bg-muted hover:bg-card-hover text-foreground border border-border rounded-md transition-colors">
|
|
376
427
|
Link folder
|
|
@@ -450,6 +501,11 @@ export default function WorkspacePanel({
|
|
|
450
501
|
onConfirm={handleConfirmAction} onCancel={() => setConfirmAction(null)} />
|
|
451
502
|
<AiPolicyModal open={showAiPolicy} content={project.ai_context || ''}
|
|
452
503
|
onSave={handleSaveAiPolicy} onClose={() => setShowAiPolicy(false)} />
|
|
504
|
+
<GitSyncResultsModal
|
|
505
|
+
open={!!syncResults}
|
|
506
|
+
results={syncResults || []}
|
|
507
|
+
onClose={() => setSyncResults(null)}
|
|
508
|
+
/>
|
|
453
509
|
</div>
|
|
454
510
|
);
|
|
455
511
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -63,6 +63,14 @@ export interface ITaskConversation {
|
|
|
63
63
|
created_at: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface IGitSyncResult {
|
|
67
|
+
projectId: string;
|
|
68
|
+
projectName: string;
|
|
69
|
+
projectPath: string;
|
|
70
|
+
status: 'success' | 'error' | 'no-git' | 'no-path';
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
export interface ISubProjectWithStats extends ISubProject {
|
|
67
75
|
task_count: number;
|
|
68
76
|
active_count: number;
|