vibepulse 0.1.1 → 0.1.2
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 +0 -29
- package/docs/session-status-detection.md +258 -0
- package/next.config.ts +11 -0
- package/package.json +14 -1
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/readme-cover.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/opencode-config/route.ts +304 -0
- package/src/app/api/opencode-config/status/route.ts +31 -0
- package/src/app/api/opencode-events/route.ts +86 -0
- package/src/app/api/opencode-models/route.test.ts +135 -0
- package/src/app/api/opencode-models/route.ts +58 -0
- package/src/app/api/profiles/[id]/apply/route.ts +49 -0
- package/src/app/api/profiles/[id]/route.ts +160 -0
- package/src/app/api/profiles/route.ts +107 -0
- package/src/app/api/sessions/[id]/archive/route.ts +35 -0
- package/src/app/api/sessions/[id]/delete/route.ts +26 -0
- package/src/app/api/sessions/[id]/route.ts +45 -0
- package/src/app/api/sessions/route.ts +596 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +66 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/page.tsx +239 -0
- package/src/components/ErrorBoundary.tsx +72 -0
- package/src/components/KanbanBoard.tsx +442 -0
- package/src/components/LoadingState.tsx +37 -0
- package/src/components/ProjectCard.tsx +382 -0
- package/src/components/QueryProvider.tsx +25 -0
- package/src/components/SessionCard.tsx +291 -0
- package/src/components/SessionList.tsx +60 -0
- package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
- package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
- package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
- package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
- package/src/components/opencode-config/ConfigButton.tsx +43 -0
- package/src/components/opencode-config/ConfigPanel.tsx +91 -0
- package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
- package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
- package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
- package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
- package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
- package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
- package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
- package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
- package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
- package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
- package/src/components/ui/Tabs.tsx +59 -0
- package/src/hooks/useOpencodeSync.ts +378 -0
- package/src/index.ts +2 -0
- package/src/lib/notificationSound.ts +266 -0
- package/src/lib/opencodeConfig.test.ts +81 -0
- package/src/lib/opencodeConfig.ts +48 -0
- package/src/lib/opencodeDiscovery.ts +154 -0
- package/src/lib/profiles/storage.ts +264 -0
- package/src/lib/transform.ts +84 -0
- package/src/test/setup.ts +8 -0
- package/src/types/index.ts +89 -0
- package/src/types/opencodeConfig.ts +133 -0
- package/src/types/testing-library-vitest.d.ts +17 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lib.json +17 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { detectConfig, CONFIG_PATH } from '@/lib/opencodeConfig';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
async function detectPlugin(): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
// Check if oh-my-opencode CLI is available
|
|
10
|
+
await execAsync('opencode --version');
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function GET() {
|
|
18
|
+
const hasConfig = detectConfig();
|
|
19
|
+
const hasPlugin = await detectPlugin();
|
|
20
|
+
|
|
21
|
+
const response: { hasConfig: boolean; hasPlugin: boolean; path?: string } = {
|
|
22
|
+
hasConfig,
|
|
23
|
+
hasPlugin,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (hasConfig) {
|
|
27
|
+
response.path = CONFIG_PATH;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Response.json(response);
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
2
|
+
import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const ports = discoverOpencodePorts();
|
|
7
|
+
|
|
8
|
+
if (!ports.length) {
|
|
9
|
+
return Response.json(
|
|
10
|
+
{
|
|
11
|
+
error: 'OpenCode server not found',
|
|
12
|
+
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
|
|
13
|
+
},
|
|
14
|
+
{ status: 503 }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const stream = new ReadableStream({
|
|
20
|
+
async start(controller) {
|
|
21
|
+
let isClosed = false;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Connect to each port independently - failures don't block others
|
|
25
|
+
const results = await Promise.allSettled(ports.map(async port => {
|
|
26
|
+
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
27
|
+
return client.global.event();
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const connectedStreams: AsyncIterable<unknown>[] = [];
|
|
31
|
+
for (const r of results) {
|
|
32
|
+
if (r.status === 'fulfilled') {
|
|
33
|
+
connectedStreams.push(r.value.stream);
|
|
34
|
+
} else {
|
|
35
|
+
console.warn('Failed to connect to OpenCode port:', r.reason);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!connectedStreams.length) {
|
|
40
|
+
console.error('All OpenCode port connections failed');
|
|
41
|
+
try { controller.close(); } catch { /* noop */ }
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tasks = connectedStreams.map(s => (async () => {
|
|
46
|
+
for await (const event of s) {
|
|
47
|
+
if (isClosed) break;
|
|
48
|
+
try {
|
|
49
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
50
|
+
} catch { break; }
|
|
51
|
+
}
|
|
52
|
+
})());
|
|
53
|
+
|
|
54
|
+
await Promise.allSettled(tasks);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error in event stream:', error);
|
|
57
|
+
} finally {
|
|
58
|
+
isClosed = true;
|
|
59
|
+
try {
|
|
60
|
+
controller.close();
|
|
61
|
+
} catch {
|
|
62
|
+
// Connection may already be closed
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Response(stream, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'text/event-stream',
|
|
71
|
+
'Cache-Control': 'no-cache',
|
|
72
|
+
Connection: 'keep-alive',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error creating event stream:', error);
|
|
77
|
+
return Response.json(
|
|
78
|
+
{
|
|
79
|
+
error: 'Failed to create event stream',
|
|
80
|
+
details: error instanceof Error ? error.message : String(error),
|
|
81
|
+
hint: 'Make sure OpenCode is running with an exposed API port. Example: opencode --port <PORT> (VibePulse auto-detects active ports).'
|
|
82
|
+
},
|
|
83
|
+
{ status: 500 }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { ExecException } from 'child_process';
|
|
3
|
+
import { handleExecResult, GET, setExecFn } from './route';
|
|
4
|
+
|
|
5
|
+
type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void;
|
|
6
|
+
type MockExecFn = (cmd: string, opts: unknown, callback: ExecCallback) => void;
|
|
7
|
+
|
|
8
|
+
describe('/api/opencode-models', () => {
|
|
9
|
+
describe('handleExecResult', () => {
|
|
10
|
+
it('should return error when CLI does not exist', () => {
|
|
11
|
+
const error = new Error('spawn opencode ENOENT') as ExecException;
|
|
12
|
+
const result = handleExecResult(error, '', 'command not found');
|
|
13
|
+
|
|
14
|
+
expect(result.source).toBe('error');
|
|
15
|
+
expect(result.models).toEqual([]);
|
|
16
|
+
expect(result.error).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return error on timeout', () => {
|
|
20
|
+
const error = new Error('timeout') as ExecException;
|
|
21
|
+
const result = handleExecResult(error, '', '');
|
|
22
|
+
|
|
23
|
+
expect(result.source).toBe('error');
|
|
24
|
+
expect(result.models).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return error on empty output', () => {
|
|
28
|
+
const result = handleExecResult(null, '', '');
|
|
29
|
+
|
|
30
|
+
expect(result.source).toBe('error');
|
|
31
|
+
expect(result.models).toEqual([]);
|
|
32
|
+
expect(result.error).toContain('No models found');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return models when only stderr but stdout is valid', () => {
|
|
36
|
+
const result = handleExecResult(null, 'anthropic/claude\nopenai/gpt-4', 'some warning from CLI');
|
|
37
|
+
|
|
38
|
+
expect(result.source).toBe('opencode');
|
|
39
|
+
expect(result.models).toContain('anthropic/claude');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return error when only stderr and stdout is empty', () => {
|
|
43
|
+
const result = handleExecResult(null, '', 'some error in stderr');
|
|
44
|
+
|
|
45
|
+
expect(result.source).toBe('error');
|
|
46
|
+
expect(result.models).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return CLI models in normal case', () => {
|
|
50
|
+
const result = handleExecResult(null, 'anthropic/claude\nopenai/gpt-4', '');
|
|
51
|
+
|
|
52
|
+
expect(result.source).toBe('opencode');
|
|
53
|
+
expect(result.models).toContain('anthropic/claude');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('GET API Integration Tests', () => {
|
|
58
|
+
const originalHome = process.env.HOME;
|
|
59
|
+
const originalPath = process.env.PATH;
|
|
60
|
+
|
|
61
|
+
let mockExec: ReturnType<typeof vi.fn<MockExecFn>>;
|
|
62
|
+
|
|
63
|
+
beforeAll(() => {
|
|
64
|
+
process.env.HOME = '/tmp';
|
|
65
|
+
process.env.PATH = '/usr/bin';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(() => {
|
|
69
|
+
process.env.HOME = originalHome;
|
|
70
|
+
process.env.PATH = originalPath;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
mockExec = vi.fn<MockExecFn>();
|
|
75
|
+
setExecFn(mockExec as never);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
const { exec } = vi.importActual<typeof import('child_process')>('child_process') as never as { exec: MockExecFn };
|
|
80
|
+
setExecFn(exec as never);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return source=opencode and real model list on successful GET', async () => {
|
|
84
|
+
mockExec.mockImplementation((_cmd: unknown, _opts: unknown, callback: ExecCallback) => {
|
|
85
|
+
callback(null, 'anthropic/claude-3.5-sonnet\nopenai/gpt-4o\ndeepseek/deepseek-chat\n', '');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const response = await GET();
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
|
|
91
|
+
expect(response.status).toBe(200);
|
|
92
|
+
expect(data.source).toBe('opencode');
|
|
93
|
+
expect(data.models).toContain('anthropic/claude-3.5-sonnet');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return source=opencode when CLI has stderr but stdout is valid', async () => {
|
|
97
|
+
mockExec.mockImplementation((_cmd: unknown, _opts: unknown, callback: ExecCallback) => {
|
|
98
|
+
callback(null, 'anthropic/claude-3.5-sonnet\n', 'Warning: newer version available');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const response = await GET();
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
|
|
104
|
+
expect(response.status).toBe(200);
|
|
105
|
+
expect(data.source).toBe('opencode');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return 503 error when CLI fails', async () => {
|
|
109
|
+
mockExec.mockImplementation((_cmd: unknown, _opts: unknown, callback: ExecCallback) => {
|
|
110
|
+
callback(new Error('spawn opencode ENOENT') as ExecException, '', 'command not found');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const response = await GET();
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(503);
|
|
117
|
+
expect(data.source).toBe('error');
|
|
118
|
+
expect(data.models).toEqual([]);
|
|
119
|
+
expect(data.error).toBeTruthy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return 503 error when GET returns empty models', async () => {
|
|
123
|
+
mockExec.mockImplementation((_cmd: unknown, _opts: unknown, callback: ExecCallback) => {
|
|
124
|
+
callback(null, '', '');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const response = await GET();
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
|
|
130
|
+
expect(response.status).toBe(503);
|
|
131
|
+
expect(data.source).toBe('error');
|
|
132
|
+
expect(data.models).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { type ExecException } from 'child_process';
|
|
3
|
+
|
|
4
|
+
type ExecFn = (
|
|
5
|
+
command: string,
|
|
6
|
+
options: { timeout: number; env: NodeJS.ProcessEnv },
|
|
7
|
+
callback: (error: ExecException | null, stdout: string, stderr: string) => void
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
let _execFn: ExecFn | null = null;
|
|
11
|
+
|
|
12
|
+
function getExecFn(): ExecFn {
|
|
13
|
+
if (_execFn) return _execFn;
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
return require('child_process').exec as ExecFn;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setExecFn(fn: ExecFn | null) {
|
|
19
|
+
_execFn = fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function handleExecResult(
|
|
23
|
+
error: ExecException | null,
|
|
24
|
+
stdout: string,
|
|
25
|
+
stderr: string
|
|
26
|
+
): { models: string[]; source: string; error?: string } {
|
|
27
|
+
if (error) {
|
|
28
|
+
return { models: [], source: 'error', error: error.message || 'Failed to fetch models from CLI' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (stderr) {
|
|
32
|
+
console.warn('[opencode-models] stderr:', stderr);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const models = stdout.trim().split('\n').filter(line => line.includes('/'));
|
|
37
|
+
if (models.length === 0) {
|
|
38
|
+
return { models: [], source: 'error', error: 'No models found. Please check your OpenCode installation.' };
|
|
39
|
+
}
|
|
40
|
+
return { models, source: 'opencode' };
|
|
41
|
+
} catch {
|
|
42
|
+
return { models: [], source: 'error', error: 'Failed to parse models output' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function GET(): Promise<Response> {
|
|
47
|
+
return new Promise<Response>((resolve) => {
|
|
48
|
+
const timeout = 5000;
|
|
49
|
+
|
|
50
|
+
const opencodePath = `${process.env.HOME}/.opencode/bin:${process.env.PATH}`;
|
|
51
|
+
|
|
52
|
+
getExecFn()('opencode models', { timeout, env: { ...process.env, PATH: opencodePath } }, (error, stdout, stderr) => {
|
|
53
|
+
const result = handleExecResult(error, stdout, stderr);
|
|
54
|
+
const status = result.error ? 503 : 200;
|
|
55
|
+
return resolve(NextResponse.json(result, { status }));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
readProfileConfig,
|
|
4
|
+
getProfileById,
|
|
5
|
+
setActiveProfileId,
|
|
6
|
+
} from '@/lib/profiles/storage';
|
|
7
|
+
import { readConfig, writeConfig } from '@/lib/opencodeConfig';
|
|
8
|
+
|
|
9
|
+
interface RouteParams {
|
|
10
|
+
params: Promise<{ id: string }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function POST(_request: NextRequest, { params }: RouteParams) {
|
|
14
|
+
try {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
const profile = await getProfileById(id);
|
|
17
|
+
|
|
18
|
+
if (!profile) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: 'Profile not found' },
|
|
21
|
+
{ status: 404 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const profileConfig = await readProfileConfig(id);
|
|
26
|
+
const currentConfig = await readConfig();
|
|
27
|
+
|
|
28
|
+
const mergedConfig = {
|
|
29
|
+
...currentConfig,
|
|
30
|
+
agents: profileConfig.agents,
|
|
31
|
+
categories: profileConfig.categories,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
await writeConfig(mergedConfig);
|
|
35
|
+
await setActiveProfileId(id);
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
message: 'Profile applied successfully',
|
|
39
|
+
profile,
|
|
40
|
+
config: profileConfig,
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error applying profile:', error);
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: 'Internal server error' },
|
|
46
|
+
{ status: 500 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
readProfileIndex,
|
|
4
|
+
writeProfileIndex,
|
|
5
|
+
getProfileById,
|
|
6
|
+
readProfileConfig,
|
|
7
|
+
writeProfileConfig,
|
|
8
|
+
deleteProfileConfig,
|
|
9
|
+
} from '@/lib/profiles/storage';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
interface RouteParams {
|
|
13
|
+
params: Promise<{ id: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET(_request: NextRequest, { params }: RouteParams) {
|
|
17
|
+
try {
|
|
18
|
+
const { id } = await params;
|
|
19
|
+
const profile = await getProfileById(id);
|
|
20
|
+
|
|
21
|
+
if (!profile) {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: 'Profile not found' },
|
|
24
|
+
{ status: 404 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = await readProfileConfig(id);
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
profile,
|
|
32
|
+
config,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error reading profile:', error);
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: 'Internal server error' },
|
|
38
|
+
{ status: 500 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|
44
|
+
try {
|
|
45
|
+
const { id } = await params;
|
|
46
|
+
const profile = await getProfileById(id);
|
|
47
|
+
|
|
48
|
+
if (!profile) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: 'Profile not found' },
|
|
51
|
+
{ status: 404 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const body = await request.json();
|
|
56
|
+
|
|
57
|
+
if (!body || typeof body !== 'object') {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Invalid request body' },
|
|
60
|
+
{ status: 400 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Support both { name, ... } and { profile: { name, ... }, config } formats
|
|
65
|
+
const profileData = body.profile || body;
|
|
66
|
+
const { name, description, emoji } = profileData;
|
|
67
|
+
const config = body.config || profileData.config;
|
|
68
|
+
|
|
69
|
+
if (name !== undefined) {
|
|
70
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: 'name must be a non-empty string' },
|
|
73
|
+
{ status: 400 }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
profile.name = name.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (description !== undefined) {
|
|
80
|
+
profile.description = description?.trim() || undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (emoji !== undefined) {
|
|
84
|
+
profile.emoji = emoji || '⚙️';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
profile.updatedAt = new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
const index = await readProfileIndex();
|
|
90
|
+
const profileIndex = index.profiles.findIndex(p => p.id === id);
|
|
91
|
+
|
|
92
|
+
if (profileIndex === -1) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: 'Profile not found in index' },
|
|
95
|
+
{ status: 404 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
index.profiles[profileIndex] = profile;
|
|
100
|
+
await writeProfileIndex(index);
|
|
101
|
+
|
|
102
|
+
if (config && typeof config === 'object') {
|
|
103
|
+
await writeProfileConfig(id, {
|
|
104
|
+
agents: config.agents || {},
|
|
105
|
+
categories: config.categories,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return NextResponse.json({ profile });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error updating profile:', error);
|
|
112
|
+
return NextResponse.json(
|
|
113
|
+
{ error: 'Internal server error' },
|
|
114
|
+
{ status: 500 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function DELETE(_request: NextRequest, { params }: RouteParams) {
|
|
120
|
+
try {
|
|
121
|
+
const { id } = await params;
|
|
122
|
+
const profile = await getProfileById(id);
|
|
123
|
+
|
|
124
|
+
if (!profile) {
|
|
125
|
+
return NextResponse.json(
|
|
126
|
+
{ error: 'Profile not found' },
|
|
127
|
+
{ status: 404 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (profile.isBuiltIn) {
|
|
132
|
+
await deleteProfileConfig(id);
|
|
133
|
+
|
|
134
|
+
return NextResponse.json({
|
|
135
|
+
message: 'Built-in profile reset to defaults',
|
|
136
|
+
profile,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const index = await readProfileIndex();
|
|
141
|
+
index.profiles = index.profiles.filter(p => p.id !== id);
|
|
142
|
+
|
|
143
|
+
if (index.activeProfileId === id) {
|
|
144
|
+
index.activeProfileId = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await writeProfileIndex(index);
|
|
148
|
+
await deleteProfileConfig(id);
|
|
149
|
+
|
|
150
|
+
return NextResponse.json({
|
|
151
|
+
message: 'Profile deleted successfully',
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Error deleting profile:', error);
|
|
155
|
+
return NextResponse.json(
|
|
156
|
+
{ error: 'Internal server error' },
|
|
157
|
+
{ status: 500 }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
readProfileIndex,
|
|
4
|
+
writeProfileIndex,
|
|
5
|
+
getProfileById,
|
|
6
|
+
writeProfileConfig,
|
|
7
|
+
} from '@/lib/profiles/storage';
|
|
8
|
+
import type { Profile, ProfileConfig } from '@/lib/profiles/storage';
|
|
9
|
+
|
|
10
|
+
export async function GET() {
|
|
11
|
+
try {
|
|
12
|
+
const index = await readProfileIndex();
|
|
13
|
+
|
|
14
|
+
return NextResponse.json({
|
|
15
|
+
profiles: index.profiles,
|
|
16
|
+
activeProfileId: index.activeProfileId,
|
|
17
|
+
});
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error reading profiles:', error);
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: 'Internal server error' },
|
|
22
|
+
{ status: 500 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function POST(request: NextRequest) {
|
|
28
|
+
try {
|
|
29
|
+
const body = await request.json();
|
|
30
|
+
|
|
31
|
+
if (!body || typeof body !== 'object') {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: 'Invalid request body' },
|
|
34
|
+
{ status: 400 }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Support both { id, name, ... } and { profile: { id, name, ... }, config } formats
|
|
39
|
+
const profileData = body.profile || body;
|
|
40
|
+
const { id, name, emoji, description } = profileData;
|
|
41
|
+
const config = body.config || profileData.config;
|
|
42
|
+
|
|
43
|
+
if (!id || typeof id !== 'string' || id.trim() === '') {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: 'id is required and must be a non-empty string' },
|
|
46
|
+
{ status: 400 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!name || typeof name !== 'string' || name.trim() === '') {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: 'name is required and must be a non-empty string' },
|
|
53
|
+
{ status: 400 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const existingProfile = await getProfileById(id);
|
|
58
|
+
if (existingProfile) {
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: `Profile with id '${id}' already exists` },
|
|
61
|
+
{ status: 400 }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: 'id must contain only letters, numbers, hyphens, and underscores' },
|
|
68
|
+
{ status: 400 }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const newProfile: Profile = {
|
|
74
|
+
id: id.trim(),
|
|
75
|
+
name: name.trim(),
|
|
76
|
+
emoji: emoji || '⚙️',
|
|
77
|
+
description: description?.trim() || undefined,
|
|
78
|
+
createdAt: now,
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const index = await readProfileIndex();
|
|
83
|
+
index.profiles.push(newProfile);
|
|
84
|
+
await writeProfileIndex(index);
|
|
85
|
+
|
|
86
|
+
if (config && typeof config === 'object') {
|
|
87
|
+
const profileConfig: ProfileConfig = {
|
|
88
|
+
agents: config.agents || {},
|
|
89
|
+
categories: config.categories,
|
|
90
|
+
};
|
|
91
|
+
await writeProfileConfig(id, profileConfig);
|
|
92
|
+
} else {
|
|
93
|
+
await writeProfileConfig(id, { agents: {} });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return NextResponse.json(
|
|
97
|
+
{ profile: newProfile },
|
|
98
|
+
{ status: 201 }
|
|
99
|
+
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Error creating profile:', error);
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: 'Internal server error' },
|
|
104
|
+
{ status: 500 }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
|
|
2
|
+
|
|
3
|
+
export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
4
|
+
const { id: sessionId } = await params;
|
|
5
|
+
const ports = discoverOpencodePorts();
|
|
6
|
+
if (!ports.length) {
|
|
7
|
+
return Response.json(
|
|
8
|
+
{ error: 'OpenCode server not found' },
|
|
9
|
+
{ status: 503 }
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
for (const port of ports) {
|
|
13
|
+
try {
|
|
14
|
+
const baseUrl = `http://localhost:${port}`;
|
|
15
|
+
const response = await fetch(`${baseUrl}/session/${sessionId}`, {
|
|
16
|
+
method: 'PATCH',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json'
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({ time: { archived: Date.now() } })
|
|
21
|
+
});
|
|
22
|
+
if (response.ok) {
|
|
23
|
+
return Response.json({ success: true });
|
|
24
|
+
}
|
|
25
|
+
console.error(`Failed to archive session on port ${port}:`, await response.text());
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`Failed to archive session on port ${port}:`, error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Response.json(
|
|
32
|
+
{ error: 'Session not found' },
|
|
33
|
+
{ status: 404 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
2
|
+
import { discoverOpencodePorts } from '@/lib/opencodeDiscovery';
|
|
3
|
+
|
|
4
|
+
export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
|
+
const { id: sessionId } = await params;
|
|
6
|
+
const ports = discoverOpencodePorts();
|
|
7
|
+
if (!ports.length) {
|
|
8
|
+
return Response.json(
|
|
9
|
+
{ error: 'OpenCode server not found' },
|
|
10
|
+
{ status: 503 }
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
for (const port of ports) {
|
|
14
|
+
try {
|
|
15
|
+
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
16
|
+
await client.session.delete({ path: { id: sessionId } });
|
|
17
|
+
return Response.json({ success: true });
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Response.json(
|
|
23
|
+
{ error: 'Session not found' },
|
|
24
|
+
{ status: 404 }
|
|
25
|
+
);
|
|
26
|
+
}
|