@lovelybunch/api 1.0.69-alpha.4 → 1.0.69-alpha.6

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/dist/lib/git.d.ts CHANGED
@@ -43,7 +43,8 @@ export declare function getCredentialConfig(): Promise<{
43
43
  helper?: string;
44
44
  origin?: string;
45
45
  }>;
46
- export declare function storeCredentials(username: string, password: string): Promise<void>;
46
+ export declare function setRemoteUrl(remoteUrl: string): Promise<void>;
47
+ export declare function storeCredentials(username: string, password: string, remoteUrl?: string): Promise<void>;
47
48
  export interface WorktreeInfo {
48
49
  name: string;
49
50
  path: string;
@@ -63,4 +64,31 @@ export declare function commitInWorktree(name: string, message: string, files?:
63
64
  }>;
64
65
  export declare function pushWorktree(name: string): Promise<string>;
65
66
  export declare function pullWorktree(name: string, strategy?: PullStrategy): Promise<string>;
67
+ export interface CommitFileChange {
68
+ path: string;
69
+ status: string;
70
+ insertions: number;
71
+ deletions: number;
72
+ }
73
+ export interface CommitDetails {
74
+ sha: string;
75
+ message: string;
76
+ author: {
77
+ name: string;
78
+ email: string;
79
+ date: string;
80
+ };
81
+ committer: {
82
+ name: string;
83
+ email: string;
84
+ date: string;
85
+ };
86
+ branch?: string;
87
+ filesChanged: number;
88
+ insertions: number;
89
+ deletions: number;
90
+ files: CommitFileChange[];
91
+ }
92
+ export declare function getCommitDetails(sha: string): Promise<CommitDetails>;
93
+ export declare function getFileDiff(sha: string, filepath: string): Promise<string>;
66
94
  export {};
package/dist/lib/git.js CHANGED
@@ -211,10 +211,41 @@ export async function getCredentialConfig() {
211
211
  return {};
212
212
  }
213
213
  }
214
- export async function storeCredentials(username, password) {
214
+ export async function setRemoteUrl(remoteUrl) {
215
+ // Validate the remote URL
216
+ if (!remoteUrl.trim()) {
217
+ throw new Error('Remote URL is required');
218
+ }
219
+ const trimmed = remoteUrl.trim();
220
+ // Check if it's a valid URL format
221
+ if (!trimmed.startsWith('https://') && !trimmed.startsWith('http://') && !trimmed.startsWith('git@')) {
222
+ throw new Error('Remote URL must start with https://, http://, or git@');
223
+ }
224
+ // Check if origin already exists
225
+ try {
226
+ await runGit(['config', '--get', 'remote.origin.url']);
227
+ // If we get here, origin exists, so update it
228
+ await runGit(['remote', 'set-url', 'origin', trimmed]);
229
+ }
230
+ catch {
231
+ // Origin doesn't exist, add it
232
+ await runGit(['remote', 'add', 'origin', trimmed]);
233
+ }
234
+ }
235
+ export async function storeCredentials(username, password, remoteUrl) {
236
+ // If a remote URL is provided, set it first
237
+ if (remoteUrl && remoteUrl.trim()) {
238
+ await setRemoteUrl(remoteUrl);
239
+ }
215
240
  // Get remote URL to determine protocol and host
216
- const { stdout: remoteUrl } = await runGit(['config', '--get', 'remote.origin.url']);
217
- const remote = remoteUrl.trim();
241
+ let remote;
242
+ try {
243
+ const { stdout: remoteUrlOutput } = await runGit(['config', '--get', 'remote.origin.url']);
244
+ remote = remoteUrlOutput.trim();
245
+ }
246
+ catch {
247
+ throw new Error('No git remote configured. Please provide a remote URL.');
248
+ }
218
249
  // Parse the remote URL to extract protocol and host
219
250
  let protocol = 'https';
220
251
  let host = '';
@@ -366,3 +397,114 @@ export async function pullWorktree(name, strategy = 'rebase') {
366
397
  const { stdout } = await runGit(args, { timeout: 30000 }); // 30 second timeout
367
398
  return stdout;
368
399
  }
400
+ export async function getCommitDetails(sha) {
401
+ // Get commit metadata
402
+ const { stdout: metaOutput } = await runGit([
403
+ 'show',
404
+ '--format=%H%n%an%n%ae%n%aI%n%cn%n%ce%n%cI%n%B',
405
+ '--no-patch',
406
+ sha
407
+ ]);
408
+ const lines = metaOutput.trim().split('\n');
409
+ const commitSha = lines[0];
410
+ const authorName = lines[1];
411
+ const authorEmail = lines[2];
412
+ const authorDate = lines[3];
413
+ const committerName = lines[4];
414
+ const committerEmail = lines[5];
415
+ const committerDate = lines[6];
416
+ const message = lines.slice(7).join('\n').trim();
417
+ // Get file changes with status
418
+ const { stdout: filesOutput } = await runGit([
419
+ 'diff-tree',
420
+ '--no-commit-id',
421
+ '--name-status',
422
+ '-r',
423
+ sha
424
+ ]);
425
+ // Get numstat for insertions/deletions per file
426
+ const { stdout: numstatOutput } = await runGit([
427
+ 'show',
428
+ '--numstat',
429
+ '--format=',
430
+ sha
431
+ ]);
432
+ // Parse file changes
433
+ const fileStatusMap = new Map();
434
+ filesOutput.trim().split('\n').filter(Boolean).forEach((line) => {
435
+ const [status, ...pathParts] = line.split('\t');
436
+ const path = pathParts.join('\t'); // Handle filenames with tabs
437
+ fileStatusMap.set(path, status);
438
+ });
439
+ const files = [];
440
+ let totalInsertions = 0;
441
+ let totalDeletions = 0;
442
+ numstatOutput.trim().split('\n').filter(Boolean).forEach((line) => {
443
+ const parts = line.split('\t');
444
+ if (parts.length >= 3) {
445
+ const insertions = parts[0] === '-' ? 0 : parseInt(parts[0], 10);
446
+ const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10);
447
+ const path = parts.slice(2).join('\t');
448
+ const status = fileStatusMap.get(path) || 'M';
449
+ files.push({
450
+ path,
451
+ status,
452
+ insertions,
453
+ deletions
454
+ });
455
+ totalInsertions += insertions;
456
+ totalDeletions += deletions;
457
+ }
458
+ });
459
+ // Try to get branch name (may not always be available)
460
+ let branch;
461
+ try {
462
+ const { stdout: branchOutput } = await runGit([
463
+ 'branch',
464
+ '--contains',
465
+ sha,
466
+ '--format=%(refname:short)'
467
+ ]);
468
+ const branches = branchOutput.trim().split('\n').filter(Boolean);
469
+ branch = branches[0]; // Use first branch that contains this commit
470
+ }
471
+ catch {
472
+ // Branch info not available
473
+ }
474
+ return {
475
+ sha: commitSha,
476
+ message,
477
+ author: {
478
+ name: authorName,
479
+ email: authorEmail,
480
+ date: authorDate
481
+ },
482
+ committer: {
483
+ name: committerName,
484
+ email: committerEmail,
485
+ date: committerDate
486
+ },
487
+ branch,
488
+ filesChanged: files.length,
489
+ insertions: totalInsertions,
490
+ deletions: totalDeletions,
491
+ files
492
+ };
493
+ }
494
+ export async function getFileDiff(sha, filepath) {
495
+ const { stdout } = await runGit([
496
+ 'show',
497
+ `${sha}:${filepath}`,
498
+ '--',
499
+ filepath
500
+ ]);
501
+ // Get the actual diff for this file
502
+ const { stdout: diffOutput } = await runGit([
503
+ 'show',
504
+ '--format=',
505
+ sha,
506
+ '--',
507
+ filepath
508
+ ]);
509
+ return diffOutput;
510
+ }
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import crypto from 'crypto';
3
- import { getRepoStatus, listBranches, createBranch, deleteBranch, switchBranch, mergeBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, checkRemoteAuth, getCredentialConfig, storeCredentials, } from '../../../../lib/git.js';
3
+ import { getRepoStatus, listBranches, createBranch, deleteBranch, switchBranch, mergeBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, checkRemoteAuth, getCredentialConfig, storeCredentials, getCommitDetails, getFileDiff, } from '../../../../lib/git.js';
4
4
  import { saveGithubToken, clearGithubToken } from '../../../../lib/github-token.js';
5
5
  import { createGithubAuthState, consumeGithubAuthState } from '../../../../lib/github-auth-state.js';
6
6
  import { resolveCoconutId, resolveControlPlaneUrl } from '../../../../lib/coconut-context.js';
@@ -84,10 +84,11 @@ app.post('/credentials', async (c) => {
84
84
  const body = await c.req.json();
85
85
  const username = String(body?.username || '');
86
86
  const password = String(body?.password || '');
87
+ const remoteUrl = body?.remoteUrl ? String(body.remoteUrl) : undefined;
87
88
  if (!username || !password) {
88
89
  return c.json({ success: false, error: { message: 'Username and password are required' } }, 400);
89
90
  }
90
- await storeCredentials(username, password);
91
+ await storeCredentials(username, password, remoteUrl);
91
92
  return c.json({ success: true, data: { message: 'Credentials stored successfully' } });
92
93
  }
93
94
  catch (e) {
@@ -523,4 +524,31 @@ app.post('/worktrees/:name/pull', async (c) => {
523
524
  return c.json({ success: false, error: { message: e.message } }, 500);
524
525
  }
525
526
  });
527
+ // Get commit details
528
+ app.get('/commits/:sha', async (c) => {
529
+ try {
530
+ const sha = c.req.param('sha');
531
+ const details = await getCommitDetails(sha);
532
+ return c.json({ success: true, data: details });
533
+ }
534
+ catch (e) {
535
+ return c.json({ success: false, error: { message: e.message || 'Failed to get commit details' } }, 500);
536
+ }
537
+ });
538
+ // Get file diff for a specific commit
539
+ app.get('/commits/:sha/files/:filepath{.+}', async (c) => {
540
+ try {
541
+ const sha = c.req.param('sha');
542
+ const filepath = c.req.param('filepath');
543
+ if (!filepath) {
544
+ return c.json({ success: false, error: { message: 'filepath is required' } }, 400);
545
+ }
546
+ const diff = await getFileDiff(sha, filepath);
547
+ // Return as plain text for easier viewing
548
+ return c.text(diff);
549
+ }
550
+ catch (e) {
551
+ return c.json({ success: false, error: { message: e.message || 'Failed to get file diff' } }, 500);
552
+ }
553
+ });
526
554
  export default app;
@@ -0,0 +1,14 @@
1
+ import { Context } from 'hono';
2
+ export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
3
+ success: false;
4
+ error: {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }, 404, "json">) | (Response & import("hono").TypedResponse<string, import("hono/utils/http-status").ContentfulStatusCode, "text">) | (Response & import("hono").TypedResponse<{
9
+ success: false;
10
+ error: {
11
+ code: string;
12
+ message: any;
13
+ };
14
+ }, 500, "json">)>;
@@ -0,0 +1,64 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { JobStore } from '../../../../../../../../lib/jobs/job-store.js';
4
+ import { getProjectRoot } from '../../../../../../../../lib/project-paths.js';
5
+ const store = new JobStore();
6
+ export async function GET(c) {
7
+ try {
8
+ const jobId = c.req.param('id');
9
+ const runId = c.req.param('runId');
10
+ const job = await store.getJob(jobId);
11
+ if (!job) {
12
+ return c.json({
13
+ success: false,
14
+ error: {
15
+ code: 'JOB_NOT_FOUND',
16
+ message: `Job ${jobId} not found`
17
+ }
18
+ }, 404);
19
+ }
20
+ const run = job.runs.find((r) => r.id === runId);
21
+ if (!run) {
22
+ return c.json({
23
+ success: false,
24
+ error: {
25
+ code: 'RUN_NOT_FOUND',
26
+ message: `Run ${runId} not found for job ${jobId}`
27
+ }
28
+ }, 404);
29
+ }
30
+ // Construct log path - use outputPath from run if available, otherwise construct it
31
+ const projectRoot = await getProjectRoot();
32
+ const logPath = run.outputPath
33
+ ? path.join(projectRoot, run.outputPath)
34
+ : path.join(projectRoot, '.nut', 'jobs', 'logs', jobId, `${runId}.log`);
35
+ try {
36
+ const logContent = await fs.readFile(logPath, 'utf-8');
37
+ // Return as plain text for easier viewing
38
+ return c.text(logContent);
39
+ }
40
+ catch (fileError) {
41
+ if (fileError?.code === 'ENOENT') {
42
+ return c.json({
43
+ success: false,
44
+ error: {
45
+ code: 'LOG_NOT_FOUND',
46
+ message: `Log file not found for run ${runId}`,
47
+ logPath: run.outputPath || `(expected at ${logPath})`
48
+ }
49
+ }, 404);
50
+ }
51
+ throw fileError;
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.error('Failed to fetch job run log:', error);
56
+ return c.json({
57
+ success: false,
58
+ error: {
59
+ code: 'GET_LOG_ERROR',
60
+ message: error?.message ?? 'Unknown error retrieving job run log'
61
+ }
62
+ }, 500);
63
+ }
64
+ }
@@ -0,0 +1,28 @@
1
+ import { Context } from 'hono';
2
+ export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
3
+ success: false;
4
+ error: {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
9
+ success: true;
10
+ data: {
11
+ id: string;
12
+ jobId: string;
13
+ trigger: import("@lovelybunch/types").ScheduledJobTrigger;
14
+ status: import("@lovelybunch/types").ScheduledJobRunStatus;
15
+ startedAt: string;
16
+ finishedAt?: string;
17
+ outputPath?: string;
18
+ summary?: string;
19
+ error?: string;
20
+ cliCommand?: string;
21
+ };
22
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
23
+ success: false;
24
+ error: {
25
+ code: string;
26
+ message: any;
27
+ };
28
+ }, 500, "json">)>;
@@ -0,0 +1,39 @@
1
+ import { JobStore } from '../../../../../../../lib/jobs/job-store.js';
2
+ const store = new JobStore();
3
+ export async function GET(c) {
4
+ try {
5
+ const jobId = c.req.param('id');
6
+ const runId = c.req.param('runId');
7
+ const job = await store.getJob(jobId);
8
+ if (!job) {
9
+ return c.json({
10
+ success: false,
11
+ error: {
12
+ code: 'JOB_NOT_FOUND',
13
+ message: `Job ${jobId} not found`
14
+ }
15
+ }, 404);
16
+ }
17
+ const run = job.runs.find((r) => r.id === runId);
18
+ if (!run) {
19
+ return c.json({
20
+ success: false,
21
+ error: {
22
+ code: 'RUN_NOT_FOUND',
23
+ message: `Run ${runId} not found for job ${jobId}`
24
+ }
25
+ }, 404);
26
+ }
27
+ return c.json({ success: true, data: run });
28
+ }
29
+ catch (error) {
30
+ console.error('Failed to fetch job run:', error);
31
+ return c.json({
32
+ success: false,
33
+ error: {
34
+ code: 'GET_RUN_ERROR',
35
+ message: error?.message ?? 'Unknown error retrieving job run'
36
+ }
37
+ }, 500);
38
+ }
39
+ }
@@ -2,6 +2,8 @@ import { Hono } from 'hono';
2
2
  import { GET, POST } from './route.js';
3
3
  import { GET as getJob, PATCH as patchJob, DELETE as deleteJob } from './[id]/route.js';
4
4
  import { POST as runJob } from './[id]/run/route.js';
5
+ import { GET as getRun } from './[id]/runs/[runId]/route.js';
6
+ import { GET as getRunLog } from './[id]/runs/[runId]/log/route.js';
5
7
  import { GET as getStatus } from './status/route.js';
6
8
  const jobs = new Hono();
7
9
  jobs.get('/', GET);
@@ -11,4 +13,6 @@ jobs.get('/:id', getJob);
11
13
  jobs.patch('/:id', patchJob);
12
14
  jobs.delete('/:id', deleteJob);
13
15
  jobs.post('/:id/run', runJob);
16
+ jobs.get('/:id/runs/:runId', getRun);
17
+ jobs.get('/:id/runs/:runId/log', getRunLog);
14
18
  export default jobs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.69-alpha.4",
3
+ "version": "1.0.69-alpha.6",
4
4
  "type": "module",
5
5
  "main": "dist/server-with-static.js",
6
6
  "exports": {
@@ -36,9 +36,9 @@
36
36
  "dependencies": {
37
37
  "@hono/node-server": "^1.13.7",
38
38
  "@hono/node-ws": "^1.0.6",
39
- "@lovelybunch/core": "^1.0.69-alpha.4",
40
- "@lovelybunch/mcp": "^1.0.69-alpha.4",
41
- "@lovelybunch/types": "^1.0.69-alpha.4",
39
+ "@lovelybunch/core": "^1.0.69-alpha.6",
40
+ "@lovelybunch/mcp": "^1.0.69-alpha.6",
41
+ "@lovelybunch/types": "^1.0.69-alpha.6",
42
42
  "arctic": "^1.9.2",
43
43
  "bcrypt": "^5.1.1",
44
44
  "cookie": "^0.6.0",