@ottocode/server 0.1.265 → 0.1.266
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 +3 -3
- package/src/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { getDb } from '@ottocode/database';
|
|
4
|
+
import { sessions } from '@ottocode/database/schema';
|
|
5
|
+
import {
|
|
6
|
+
appendCoAuthorTrailer,
|
|
7
|
+
getAuth,
|
|
8
|
+
getFastModelForAuth,
|
|
9
|
+
getProviderDefinition,
|
|
10
|
+
loadConfig,
|
|
11
|
+
type ProviderId,
|
|
12
|
+
} from '@ottocode/sdk';
|
|
13
|
+
import { generateText, streamText } from 'ai';
|
|
14
|
+
import { eq } from 'drizzle-orm';
|
|
15
|
+
import type { Context } from 'hono';
|
|
16
|
+
import {
|
|
17
|
+
detectOAuth,
|
|
18
|
+
adaptSimpleCall,
|
|
19
|
+
} from '../../runtime/provider/oauth-adapter.ts';
|
|
20
|
+
import { resolveModel } from '../../runtime/provider/index.ts';
|
|
21
|
+
import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
|
|
22
|
+
import { parseGitStatus, validateAndGetGitRoot } from './utils.ts';
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
export async function handleCommitChanges(c: Context) {
|
|
27
|
+
try {
|
|
28
|
+
const body = await c.req.json();
|
|
29
|
+
const { message, project } = gitCommitSchema.parse(body);
|
|
30
|
+
const requestedPath = project || process.cwd();
|
|
31
|
+
|
|
32
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
33
|
+
if ('error' in validation) {
|
|
34
|
+
return c.json(
|
|
35
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
36
|
+
400,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fullMessage = appendCoAuthorTrailer(message);
|
|
41
|
+
const { stdout } = await execFileAsync(
|
|
42
|
+
'git',
|
|
43
|
+
['commit', '-m', fullMessage],
|
|
44
|
+
{
|
|
45
|
+
cwd: validation.gitRoot,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return c.json({
|
|
50
|
+
status: 'ok',
|
|
51
|
+
data: {
|
|
52
|
+
message: stdout.trim(),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return c.json(
|
|
57
|
+
{
|
|
58
|
+
status: 'error',
|
|
59
|
+
error: error instanceof Error ? error.message : 'Failed to commit',
|
|
60
|
+
},
|
|
61
|
+
500,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getSessionProviderModel(sessionId?: string) {
|
|
67
|
+
if (!sessionId) return {};
|
|
68
|
+
const db = await getDb();
|
|
69
|
+
const [session] = await db
|
|
70
|
+
.select({ provider: sessions.provider, model: sessions.model })
|
|
71
|
+
.from(sessions)
|
|
72
|
+
.where(eq(sessions.id, sessionId));
|
|
73
|
+
return {
|
|
74
|
+
provider: session?.provider as ProviderId | undefined,
|
|
75
|
+
model: session?.model ?? undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildCommitPrompt(fileList: string, diff: string): string {
|
|
80
|
+
return `Generate a commit message for these git changes.
|
|
81
|
+
|
|
82
|
+
Staged files:
|
|
83
|
+
${fileList}
|
|
84
|
+
|
|
85
|
+
Diff (first 4000 chars):
|
|
86
|
+
${diff.slice(0, 4000)}
|
|
87
|
+
|
|
88
|
+
Guidelines:
|
|
89
|
+
- CAREFULLY READ the diff above - describe what ACTUALLY changed
|
|
90
|
+
- Use conventional commits format: type(scope): description
|
|
91
|
+
- First line under 72 characters
|
|
92
|
+
- Add a blank line, then 2-4 short bullet points
|
|
93
|
+
- Each bullet describes ONE specific change you see in the diff
|
|
94
|
+
- Be ACCURATE - don't invent changes that aren't in the diff
|
|
95
|
+
- Keep bullets short (under 80 chars each)
|
|
96
|
+
- Do not include markdown code blocks or backticks
|
|
97
|
+
- Return ONLY the commit message text, nothing else
|
|
98
|
+
|
|
99
|
+
Example (for a diff that adds boolean returns to functions):
|
|
100
|
+
refactor(auth): return success status from login functions
|
|
101
|
+
|
|
102
|
+
- Add boolean return type to auth functions
|
|
103
|
+
- Return false on user cancellation or failure
|
|
104
|
+
- Check return value before proceeding with auth flow
|
|
105
|
+
|
|
106
|
+
Commit message:`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function handleGenerateCommitMessage(c: Context) {
|
|
110
|
+
try {
|
|
111
|
+
const body = await c.req.json();
|
|
112
|
+
const { project, sessionId } = gitGenerateCommitMessageSchema.parse(body);
|
|
113
|
+
const requestedPath = project || process.cwd();
|
|
114
|
+
|
|
115
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
116
|
+
if ('error' in validation) {
|
|
117
|
+
return c.json(
|
|
118
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
119
|
+
400,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { stdout: diff } = await execFileAsync('git', ['diff', '--cached'], {
|
|
124
|
+
cwd: validation.gitRoot,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!diff.trim()) {
|
|
128
|
+
return c.json(
|
|
129
|
+
{
|
|
130
|
+
status: 'error',
|
|
131
|
+
error: 'No staged changes to generate message from',
|
|
132
|
+
},
|
|
133
|
+
400,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { stdout: statusOutput } = await execFileAsync(
|
|
138
|
+
'git',
|
|
139
|
+
['status', '--porcelain=v2'],
|
|
140
|
+
{ cwd: validation.gitRoot },
|
|
141
|
+
);
|
|
142
|
+
const { staged } = parseGitStatus(statusOutput, validation.gitRoot);
|
|
143
|
+
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
144
|
+
const config = await loadConfig();
|
|
145
|
+
const session = await getSessionProviderModel(sessionId);
|
|
146
|
+
const provider =
|
|
147
|
+
session.provider ??
|
|
148
|
+
((config.defaults?.provider || 'anthropic') as ProviderId);
|
|
149
|
+
const currentModel =
|
|
150
|
+
session.model ?? config.defaults?.model ?? 'claude-3-5-sonnet-20241022';
|
|
151
|
+
const auth = await getAuth(provider, config.projectRoot);
|
|
152
|
+
const oauth = detectOAuth(provider, auth);
|
|
153
|
+
const providerDefinition = getProviderDefinition(config, provider);
|
|
154
|
+
const modelId =
|
|
155
|
+
providerDefinition?.source === 'custom' ||
|
|
156
|
+
providerDefinition?.compatibility === 'ollama'
|
|
157
|
+
? currentModel
|
|
158
|
+
: (getFastModelForAuth(provider, auth?.type) ?? currentModel);
|
|
159
|
+
const model = await resolveModel(provider, modelId, config);
|
|
160
|
+
|
|
161
|
+
const adapted = adaptSimpleCall(oauth, {
|
|
162
|
+
instructions:
|
|
163
|
+
'You are a helpful assistant that generates accurate git commit messages based on the actual diff content.',
|
|
164
|
+
userContent: buildCommitPrompt(fileList, diff),
|
|
165
|
+
maxOutputTokens: 500,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (adapted.forceStream) {
|
|
169
|
+
const result = streamText({
|
|
170
|
+
model,
|
|
171
|
+
system: adapted.system,
|
|
172
|
+
messages: adapted.messages,
|
|
173
|
+
providerOptions: adapted.providerOptions,
|
|
174
|
+
});
|
|
175
|
+
let text = '';
|
|
176
|
+
for await (const chunk of result.textStream) {
|
|
177
|
+
text += chunk;
|
|
178
|
+
}
|
|
179
|
+
return c.json({ status: 'ok', data: { message: text.trim() } });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { text } = await generateText({
|
|
183
|
+
model,
|
|
184
|
+
system: adapted.system,
|
|
185
|
+
messages: adapted.messages,
|
|
186
|
+
maxOutputTokens: adapted.maxOutputTokens,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return c.json({
|
|
190
|
+
status: 'ok',
|
|
191
|
+
data: {
|
|
192
|
+
message: text.trim(),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return c.json(
|
|
197
|
+
{
|
|
198
|
+
status: 'error',
|
|
199
|
+
error:
|
|
200
|
+
error instanceof Error
|
|
201
|
+
? error.message
|
|
202
|
+
: 'Failed to generate commit message',
|
|
203
|
+
},
|
|
204
|
+
500,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/routes/git/commit.ts
CHANGED
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
import { generateText, streamText } from 'ai';
|
|
5
|
-
import { eq } from 'drizzle-orm';
|
|
6
|
-
import type { ProviderId } from '@ottocode/sdk';
|
|
7
|
-
import {
|
|
8
|
-
loadConfig,
|
|
9
|
-
getAuth,
|
|
10
|
-
getFastModelForAuth,
|
|
11
|
-
getProviderDefinition,
|
|
12
|
-
} from '@ottocode/sdk';
|
|
13
|
-
import { getDb } from '@ottocode/database';
|
|
14
|
-
import { sessions } from '@ottocode/database/schema';
|
|
15
|
-
import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
|
|
16
|
-
import { validateAndGetGitRoot, parseGitStatus } from './utils.ts';
|
|
17
|
-
import { resolveModel } from '../../runtime/provider/index.ts';
|
|
18
|
-
import {
|
|
19
|
-
detectOAuth,
|
|
20
|
-
adaptSimpleCall,
|
|
21
|
-
} from '../../runtime/provider/oauth-adapter.ts';
|
|
22
|
-
import { appendCoAuthorTrailer } from '@ottocode/sdk';
|
|
23
2
|
import { openApiRoute } from '../../openapi/route.ts';
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
import {
|
|
4
|
+
handleCommitChanges,
|
|
5
|
+
handleGenerateCommitMessage,
|
|
6
|
+
} from './commit-service.ts';
|
|
26
7
|
|
|
27
8
|
export function registerCommitRoutes(app: Hono) {
|
|
28
9
|
openApiRoute(
|
|
@@ -122,48 +103,7 @@ export function registerCommitRoutes(app: Hono) {
|
|
|
122
103
|
},
|
|
123
104
|
},
|
|
124
105
|
},
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const body = await c.req.json();
|
|
128
|
-
const { message, project } = gitCommitSchema.parse(body);
|
|
129
|
-
|
|
130
|
-
const requestedPath = project || process.cwd();
|
|
131
|
-
|
|
132
|
-
const validation = await validateAndGetGitRoot(requestedPath);
|
|
133
|
-
if ('error' in validation) {
|
|
134
|
-
return c.json(
|
|
135
|
-
{ status: 'error', error: validation.error, code: validation.code },
|
|
136
|
-
400,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const { gitRoot } = validation;
|
|
141
|
-
|
|
142
|
-
const fullMessage = appendCoAuthorTrailer(message);
|
|
143
|
-
const { stdout } = await execFileAsync(
|
|
144
|
-
'git',
|
|
145
|
-
['commit', '-m', fullMessage],
|
|
146
|
-
{
|
|
147
|
-
cwd: gitRoot,
|
|
148
|
-
},
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return c.json({
|
|
152
|
-
status: 'ok',
|
|
153
|
-
data: {
|
|
154
|
-
message: stdout.trim(),
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return c.json(
|
|
159
|
-
{
|
|
160
|
-
status: 'error',
|
|
161
|
-
error: error instanceof Error ? error.message : 'Failed to commit',
|
|
162
|
-
},
|
|
163
|
-
500,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
},
|
|
106
|
+
handleCommitChanges,
|
|
167
107
|
);
|
|
168
108
|
|
|
169
109
|
openApiRoute(
|
|
@@ -270,160 +210,6 @@ export function registerCommitRoutes(app: Hono) {
|
|
|
270
210
|
},
|
|
271
211
|
},
|
|
272
212
|
},
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const body = await c.req.json();
|
|
276
|
-
const { project, sessionId } =
|
|
277
|
-
gitGenerateCommitMessageSchema.parse(body);
|
|
278
|
-
|
|
279
|
-
const requestedPath = project || process.cwd();
|
|
280
|
-
|
|
281
|
-
const validation = await validateAndGetGitRoot(requestedPath);
|
|
282
|
-
if ('error' in validation) {
|
|
283
|
-
return c.json(
|
|
284
|
-
{ status: 'error', error: validation.error, code: validation.code },
|
|
285
|
-
400,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const { gitRoot } = validation;
|
|
290
|
-
|
|
291
|
-
const { stdout: diff } = await execFileAsync(
|
|
292
|
-
'git',
|
|
293
|
-
['diff', '--cached'],
|
|
294
|
-
{
|
|
295
|
-
cwd: gitRoot,
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (!diff.trim()) {
|
|
300
|
-
return c.json(
|
|
301
|
-
{
|
|
302
|
-
status: 'error',
|
|
303
|
-
error: 'No staged changes to generate message from',
|
|
304
|
-
},
|
|
305
|
-
400,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const { stdout: statusOutput } = await execFileAsync(
|
|
310
|
-
'git',
|
|
311
|
-
['status', '--porcelain=v2'],
|
|
312
|
-
{ cwd: gitRoot },
|
|
313
|
-
);
|
|
314
|
-
const { staged } = parseGitStatus(statusOutput, gitRoot);
|
|
315
|
-
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
316
|
-
|
|
317
|
-
const config = await loadConfig();
|
|
318
|
-
|
|
319
|
-
let provider = (config.defaults?.provider || 'anthropic') as ProviderId;
|
|
320
|
-
let currentModel =
|
|
321
|
-
config.defaults?.model ?? 'claude-3-5-sonnet-20241022';
|
|
322
|
-
|
|
323
|
-
if (sessionId) {
|
|
324
|
-
const db = await getDb();
|
|
325
|
-
const [session] = await db
|
|
326
|
-
.select({ provider: sessions.provider, model: sessions.model })
|
|
327
|
-
.from(sessions)
|
|
328
|
-
.where(eq(sessions.id, sessionId));
|
|
329
|
-
if (session?.provider) {
|
|
330
|
-
provider = session.provider as ProviderId;
|
|
331
|
-
}
|
|
332
|
-
if (session?.model) {
|
|
333
|
-
currentModel = session.model;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const auth = await getAuth(provider, config.projectRoot);
|
|
338
|
-
const oauth = detectOAuth(provider, auth);
|
|
339
|
-
const providerDefinition = getProviderDefinition(config, provider);
|
|
340
|
-
|
|
341
|
-
const modelId =
|
|
342
|
-
providerDefinition?.source === 'custom' ||
|
|
343
|
-
providerDefinition?.compatibility === 'ollama'
|
|
344
|
-
? currentModel
|
|
345
|
-
: (getFastModelForAuth(provider, auth?.type) ?? currentModel);
|
|
346
|
-
const model = await resolveModel(provider, modelId, config);
|
|
347
|
-
|
|
348
|
-
const userPrompt = `Generate a commit message for these git changes.
|
|
349
|
-
|
|
350
|
-
Staged files:
|
|
351
|
-
${fileList}
|
|
352
|
-
|
|
353
|
-
Diff (first 4000 chars):
|
|
354
|
-
${diff.slice(0, 4000)}
|
|
355
|
-
|
|
356
|
-
Guidelines:
|
|
357
|
-
- CAREFULLY READ the diff above - describe what ACTUALLY changed
|
|
358
|
-
- Use conventional commits format: type(scope): description
|
|
359
|
-
- First line under 72 characters
|
|
360
|
-
- Add a blank line, then 2-4 short bullet points
|
|
361
|
-
- Each bullet describes ONE specific change you see in the diff
|
|
362
|
-
- Be ACCURATE - don't invent changes that aren't in the diff
|
|
363
|
-
- Keep bullets short (under 80 chars each)
|
|
364
|
-
- Do not include markdown code blocks or backticks
|
|
365
|
-
- Return ONLY the commit message text, nothing else
|
|
366
|
-
|
|
367
|
-
Example (for a diff that adds boolean returns to functions):
|
|
368
|
-
refactor(auth): return success status from login functions
|
|
369
|
-
|
|
370
|
-
- Add boolean return type to auth functions
|
|
371
|
-
- Return false on user cancellation or failure
|
|
372
|
-
- Check return value before proceeding with auth flow
|
|
373
|
-
|
|
374
|
-
Commit message:`;
|
|
375
|
-
|
|
376
|
-
const commitInstructions =
|
|
377
|
-
'You are a helpful assistant that generates accurate git commit messages based on the actual diff content.';
|
|
378
|
-
|
|
379
|
-
const adapted = adaptSimpleCall(oauth, {
|
|
380
|
-
instructions: commitInstructions,
|
|
381
|
-
userContent: userPrompt,
|
|
382
|
-
maxOutputTokens: 500,
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
if (adapted.forceStream) {
|
|
386
|
-
const result = streamText({
|
|
387
|
-
model,
|
|
388
|
-
system: adapted.system,
|
|
389
|
-
messages: adapted.messages,
|
|
390
|
-
providerOptions: adapted.providerOptions,
|
|
391
|
-
});
|
|
392
|
-
let text = '';
|
|
393
|
-
for await (const chunk of result.textStream) {
|
|
394
|
-
text += chunk;
|
|
395
|
-
}
|
|
396
|
-
const message = text.trim();
|
|
397
|
-
return c.json({ status: 'ok', data: { message } });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const { text } = await generateText({
|
|
401
|
-
model,
|
|
402
|
-
system: adapted.system,
|
|
403
|
-
messages: adapted.messages,
|
|
404
|
-
maxOutputTokens: adapted.maxOutputTokens,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
const message = text.trim();
|
|
408
|
-
|
|
409
|
-
return c.json({
|
|
410
|
-
status: 'ok',
|
|
411
|
-
data: {
|
|
412
|
-
message,
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
} catch (error) {
|
|
416
|
-
return c.json(
|
|
417
|
-
{
|
|
418
|
-
status: 'error',
|
|
419
|
-
error:
|
|
420
|
-
error instanceof Error
|
|
421
|
-
? error.message
|
|
422
|
-
: 'Failed to generate commit message',
|
|
423
|
-
},
|
|
424
|
-
500,
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
},
|
|
213
|
+
handleGenerateCommitMessage,
|
|
428
214
|
);
|
|
429
215
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import type { Context } from 'hono';
|
|
4
|
+
import { gitRemoteAddSchema, gitRemoteRemoveSchema } from './schemas.ts';
|
|
5
|
+
import { validateAndGetGitRoot } from './utils.ts';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
type GitRemote = { name: string; url: string; type: string };
|
|
10
|
+
|
|
11
|
+
function parseRemoteOutput(output: string): GitRemote[] {
|
|
12
|
+
const remotes: GitRemote[] = [];
|
|
13
|
+
const seen = new Set<string>();
|
|
14
|
+
for (const line of output.trim().split('\n').filter(Boolean)) {
|
|
15
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
|
|
16
|
+
if (!match) continue;
|
|
17
|
+
const [, name, url, type] = match;
|
|
18
|
+
const key = `${name}:${type}`;
|
|
19
|
+
if (seen.has(key)) continue;
|
|
20
|
+
seen.add(key);
|
|
21
|
+
remotes.push({ name, url, type });
|
|
22
|
+
}
|
|
23
|
+
return remotes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handleGetGitRemotes(c: Context) {
|
|
27
|
+
try {
|
|
28
|
+
const project = c.req.query('project');
|
|
29
|
+
const requestedPath = project || process.cwd();
|
|
30
|
+
|
|
31
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
32
|
+
if ('error' in validation) {
|
|
33
|
+
return c.json(
|
|
34
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
35
|
+
400,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { stdout } = await execFileAsync('git', ['remote', '-v'], {
|
|
40
|
+
cwd: validation.gitRoot,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return c.json({
|
|
44
|
+
status: 'ok',
|
|
45
|
+
data: { remotes: parseRemoteOutput(stdout) },
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return c.json(
|
|
49
|
+
{
|
|
50
|
+
status: 'error',
|
|
51
|
+
error:
|
|
52
|
+
error instanceof Error ? error.message : 'Failed to list remotes',
|
|
53
|
+
},
|
|
54
|
+
500,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function handleAddGitRemote(c: Context) {
|
|
60
|
+
try {
|
|
61
|
+
const body = await c.req.json().catch(() => ({}));
|
|
62
|
+
const { project, name, url } = gitRemoteAddSchema.parse(body);
|
|
63
|
+
const requestedPath = project || process.cwd();
|
|
64
|
+
|
|
65
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
66
|
+
if ('error' in validation) {
|
|
67
|
+
return c.json(
|
|
68
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
69
|
+
400,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await execFileAsync('git', ['remote', 'add', name, url], {
|
|
74
|
+
cwd: validation.gitRoot,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return c.json({
|
|
78
|
+
status: 'ok',
|
|
79
|
+
data: { name, url },
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message =
|
|
83
|
+
error instanceof Error ? error.message : 'Failed to add remote';
|
|
84
|
+
const status = message.includes('already exists') ? 400 : 500;
|
|
85
|
+
return c.json({ status: 'error', error: message }, status);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function handleRemoveGitRemote(c: Context) {
|
|
90
|
+
try {
|
|
91
|
+
const body = await c.req.json().catch(() => ({}));
|
|
92
|
+
const { project, name } = gitRemoteRemoveSchema.parse(body);
|
|
93
|
+
const requestedPath = project || process.cwd();
|
|
94
|
+
|
|
95
|
+
const validation = await validateAndGetGitRoot(requestedPath);
|
|
96
|
+
if ('error' in validation) {
|
|
97
|
+
return c.json(
|
|
98
|
+
{ status: 'error', error: validation.error, code: validation.code },
|
|
99
|
+
400,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await execFileAsync('git', ['remote', 'remove', name], {
|
|
104
|
+
cwd: validation.gitRoot,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return c.json({
|
|
108
|
+
status: 'ok',
|
|
109
|
+
data: { removed: name },
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message =
|
|
113
|
+
error instanceof Error ? error.message : 'Failed to remove remote';
|
|
114
|
+
return c.json({ status: 'error', error: message }, 500);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/routes/git/remote.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
import { gitRemoteAddSchema, gitRemoteRemoveSchema } from './schemas.ts';
|
|
5
|
-
import { validateAndGetGitRoot } from './utils.ts';
|
|
6
2
|
import { openApiRoute } from '../../openapi/route.ts';
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import {
|
|
4
|
+
handleAddGitRemote,
|
|
5
|
+
handleGetGitRemotes,
|
|
6
|
+
handleRemoveGitRemote,
|
|
7
|
+
} from './remote-service.ts';
|
|
9
8
|
|
|
10
9
|
export function registerRemoteRoutes(app: Hono) {
|
|
11
10
|
openApiRoute(
|
|
@@ -118,54 +117,7 @@ export function registerRemoteRoutes(app: Hono) {
|
|
|
118
117
|
},
|
|
119
118
|
},
|
|
120
119
|
},
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const project = c.req.query('project');
|
|
124
|
-
const requestedPath = project || process.cwd();
|
|
125
|
-
|
|
126
|
-
const validation = await validateAndGetGitRoot(requestedPath);
|
|
127
|
-
if ('error' in validation) {
|
|
128
|
-
return c.json(
|
|
129
|
-
{ status: 'error', error: validation.error, code: validation.code },
|
|
130
|
-
400,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const { gitRoot } = validation;
|
|
135
|
-
|
|
136
|
-
const { stdout } = await execFileAsync('git', ['remote', '-v'], {
|
|
137
|
-
cwd: gitRoot,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const remotes: { name: string; url: string; type: string }[] = [];
|
|
141
|
-
const seen = new Set<string>();
|
|
142
|
-
for (const line of stdout.trim().split('\n').filter(Boolean)) {
|
|
143
|
-
const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
|
|
144
|
-
if (match) {
|
|
145
|
-
const key = `${match[1]}:${match[3]}`;
|
|
146
|
-
if (!seen.has(key)) {
|
|
147
|
-
seen.add(key);
|
|
148
|
-
remotes.push({
|
|
149
|
-
name: match[1],
|
|
150
|
-
url: match[2],
|
|
151
|
-
type: match[3],
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return c.json({ status: 'ok', data: { remotes } });
|
|
158
|
-
} catch (error) {
|
|
159
|
-
return c.json(
|
|
160
|
-
{
|
|
161
|
-
status: 'error',
|
|
162
|
-
error:
|
|
163
|
-
error instanceof Error ? error.message : 'Failed to list remotes',
|
|
164
|
-
},
|
|
165
|
-
500,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
},
|
|
120
|
+
handleGetGitRemotes,
|
|
169
121
|
);
|
|
170
122
|
|
|
171
123
|
openApiRoute(
|
|
@@ -276,37 +228,7 @@ export function registerRemoteRoutes(app: Hono) {
|
|
|
276
228
|
},
|
|
277
229
|
},
|
|
278
230
|
},
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
const body = await c.req.json().catch(() => ({}));
|
|
282
|
-
const { project, name, url } = gitRemoteAddSchema.parse(body);
|
|
283
|
-
const requestedPath = project || process.cwd();
|
|
284
|
-
|
|
285
|
-
const validation = await validateAndGetGitRoot(requestedPath);
|
|
286
|
-
if ('error' in validation) {
|
|
287
|
-
return c.json(
|
|
288
|
-
{ status: 'error', error: validation.error, code: validation.code },
|
|
289
|
-
400,
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const { gitRoot } = validation;
|
|
294
|
-
|
|
295
|
-
await execFileAsync('git', ['remote', 'add', name, url], {
|
|
296
|
-
cwd: gitRoot,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return c.json({
|
|
300
|
-
status: 'ok',
|
|
301
|
-
data: { name, url },
|
|
302
|
-
});
|
|
303
|
-
} catch (error) {
|
|
304
|
-
const message =
|
|
305
|
-
error instanceof Error ? error.message : 'Failed to add remote';
|
|
306
|
-
const status = message.includes('already exists') ? 400 : 500;
|
|
307
|
-
return c.json({ status: 'error', error: message }, status);
|
|
308
|
-
}
|
|
309
|
-
},
|
|
231
|
+
handleAddGitRemote,
|
|
310
232
|
);
|
|
311
233
|
|
|
312
234
|
openApiRoute(
|
|
@@ -388,35 +310,6 @@ export function registerRemoteRoutes(app: Hono) {
|
|
|
388
310
|
},
|
|
389
311
|
},
|
|
390
312
|
},
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
const body = await c.req.json().catch(() => ({}));
|
|
394
|
-
const { project, name } = gitRemoteRemoveSchema.parse(body);
|
|
395
|
-
const requestedPath = project || process.cwd();
|
|
396
|
-
|
|
397
|
-
const validation = await validateAndGetGitRoot(requestedPath);
|
|
398
|
-
if ('error' in validation) {
|
|
399
|
-
return c.json(
|
|
400
|
-
{ status: 'error', error: validation.error, code: validation.code },
|
|
401
|
-
400,
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const { gitRoot } = validation;
|
|
406
|
-
|
|
407
|
-
await execFileAsync('git', ['remote', 'remove', name], {
|
|
408
|
-
cwd: gitRoot,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
return c.json({
|
|
412
|
-
status: 'ok',
|
|
413
|
-
data: { removed: name },
|
|
414
|
-
});
|
|
415
|
-
} catch (error) {
|
|
416
|
-
const message =
|
|
417
|
-
error instanceof Error ? error.message : 'Failed to remove remote';
|
|
418
|
-
return c.json({ status: 'error', error: message }, 500);
|
|
419
|
-
}
|
|
420
|
-
},
|
|
313
|
+
handleRemoveGitRemote,
|
|
421
314
|
);
|
|
422
315
|
}
|