@lovelybunch/api 1.0.24 → 1.0.27
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/gait-path.d.ts +7 -19
- package/dist/lib/gait-path.js +46 -34
- package/dist/lib/git.d.ts +48 -0
- package/dist/lib/git.js +201 -0
- package/dist/lib/storage/file-storage.js +15 -2
- package/dist/routes/api/v1/git/index.d.ts +3 -0
- package/dist/routes/api/v1/git/index.js +175 -0
- package/dist/server.js +2 -0
- package/package.json +3 -3
- package/static/assets/index-8b0cnc7V.css +33 -0
- package/static/assets/index-CQ_uAUTe.js +631 -0
- package/static/assets/index-Czbfd9m7.js +631 -0
- package/static/assets/index-DdxjO25U.js +631 -0
- package/static/index.html +2 -2
package/dist/lib/gait-path.d.ts
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Priority order:
|
|
5
|
-
* 1. Explicit basePath parameter
|
|
6
|
-
* 2. Dev mode: GAIT_DEV_ROOT environment variable
|
|
7
|
-
* 3. Production mode: GAIT_DATA_PATH environment variable
|
|
8
|
-
* 4. Fallback: current working directory
|
|
2
|
+
* Find the .gait directory by traversing up from the current working directory
|
|
3
|
+
* If GAIT_DATA_PATH is set (from CLI), use that instead
|
|
9
4
|
*/
|
|
10
|
-
export declare function
|
|
5
|
+
export declare function findGaitDirectory(startDir?: string): Promise<string | null>;
|
|
11
6
|
/**
|
|
12
|
-
* Get the
|
|
7
|
+
* Get the path to a context file, automatically finding the .gait directory
|
|
13
8
|
*/
|
|
14
|
-
export declare function
|
|
9
|
+
export declare function getContextFilePath(fileName: string): Promise<string | null>;
|
|
15
10
|
/**
|
|
16
|
-
*
|
|
11
|
+
* Get the .gait config to read storage path settings
|
|
17
12
|
*/
|
|
18
|
-
export declare
|
|
19
|
-
readonly proposals: () => string;
|
|
20
|
-
readonly context: () => string;
|
|
21
|
-
readonly agents: () => string;
|
|
22
|
-
readonly resources: () => string;
|
|
23
|
-
readonly chats: () => string;
|
|
24
|
-
readonly config: () => string;
|
|
25
|
-
};
|
|
13
|
+
export declare function getGaitConfig(): Promise<any | null>;
|
package/dist/lib/gait-path.js
CHANGED
|
@@ -1,45 +1,57 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Priority order:
|
|
6
|
-
* 1. Explicit basePath parameter
|
|
7
|
-
* 2. Dev mode: GAIT_DEV_ROOT environment variable
|
|
8
|
-
* 3. Production mode: GAIT_DATA_PATH environment variable
|
|
9
|
-
* 4. Fallback: current working directory
|
|
4
|
+
* Find the .gait directory by traversing up from the current working directory
|
|
5
|
+
* If GAIT_DATA_PATH is set (from CLI), use that instead
|
|
10
6
|
*/
|
|
11
|
-
export function
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
export async function findGaitDirectory(startDir) {
|
|
8
|
+
// If running from CLI (gait serve), use the directory where command was run
|
|
9
|
+
if (process.env.GAIT_DATA_PATH) {
|
|
10
|
+
const gaitPath = path.join(process.env.GAIT_DATA_PATH, '.gait');
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(gaitPath);
|
|
13
|
+
return gaitPath;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Fall through to directory traversal
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
// Otherwise traverse up from start directory
|
|
20
|
+
let currentDir = startDir || process.cwd();
|
|
21
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
22
|
+
const gaitPath = path.join(currentDir, '.gait');
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(gaitPath);
|
|
25
|
+
return gaitPath;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
currentDir = path.dirname(currentDir);
|
|
29
|
+
}
|
|
26
30
|
}
|
|
31
|
+
return null;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
|
-
* Get the
|
|
34
|
+
* Get the path to a context file, automatically finding the .gait directory
|
|
30
35
|
*/
|
|
31
|
-
export function
|
|
32
|
-
const
|
|
33
|
-
|
|
36
|
+
export async function getContextFilePath(fileName) {
|
|
37
|
+
const gaitDir = await findGaitDirectory();
|
|
38
|
+
if (!gaitDir)
|
|
39
|
+
return null;
|
|
40
|
+
return path.join(gaitDir, 'context', fileName);
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
|
-
*
|
|
43
|
+
* Get the .gait config to read storage path settings
|
|
37
44
|
*/
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
export async function getGaitConfig() {
|
|
46
|
+
const gaitDir = await findGaitDirectory();
|
|
47
|
+
if (!gaitDir)
|
|
48
|
+
return null;
|
|
49
|
+
const configPath = path.join(gaitDir, 'config.json');
|
|
50
|
+
try {
|
|
51
|
+
const config = await fs.readFile(configPath, 'utf-8');
|
|
52
|
+
return JSON.parse(config);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare function getRepoRoot(): Promise<string>;
|
|
2
|
+
export declare function getWorktreesBase(): Promise<string>;
|
|
3
|
+
export declare function sanitizeBranchName(name: string): string;
|
|
4
|
+
export declare function resolveSafeWorktreePath(name: string): Promise<string>;
|
|
5
|
+
export declare function runGit(args: string[], opts?: {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getRepoStatus(): Promise<{
|
|
12
|
+
branch: string;
|
|
13
|
+
ahead: number;
|
|
14
|
+
behind: number;
|
|
15
|
+
upstream?: string;
|
|
16
|
+
head?: string;
|
|
17
|
+
changes: {
|
|
18
|
+
path: string;
|
|
19
|
+
status: string;
|
|
20
|
+
}[];
|
|
21
|
+
}>;
|
|
22
|
+
export declare function listBranches(): Promise<{
|
|
23
|
+
name: string;
|
|
24
|
+
current: boolean;
|
|
25
|
+
}[]>;
|
|
26
|
+
export declare function createBranch(name: string, from?: string): Promise<void>;
|
|
27
|
+
export declare function deleteBranch(name: string): Promise<void>;
|
|
28
|
+
export declare function pushCurrent(): Promise<string>;
|
|
29
|
+
export declare function pullCurrent(): Promise<string>;
|
|
30
|
+
export interface WorktreeInfo {
|
|
31
|
+
name: string;
|
|
32
|
+
path: string;
|
|
33
|
+
branch: string;
|
|
34
|
+
head?: string;
|
|
35
|
+
locked: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function listWorktrees(): Promise<WorktreeInfo[]>;
|
|
38
|
+
export declare function addWorktree(branch: string, from?: string): Promise<{
|
|
39
|
+
path: string;
|
|
40
|
+
branch: string;
|
|
41
|
+
from?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function removeWorktree(nameOrPath: string): Promise<void>;
|
|
44
|
+
export declare function commitInWorktree(name: string, message: string, files?: string[]): Promise<{
|
|
45
|
+
commitHash?: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function pushWorktree(name: string): Promise<string>;
|
|
48
|
+
export declare function pullWorktree(name: string): Promise<string>;
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { promisify } from 'util';
|
|
2
|
+
import { execFile as _execFile } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { findGaitDirectory } from './gait-path.js';
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
const execFile = promisify(_execFile);
|
|
7
|
+
// Base directory for worktrees under the repository root
|
|
8
|
+
export async function getRepoRoot() {
|
|
9
|
+
// Use .gait discovery to find project root, then go up one level
|
|
10
|
+
const gaitDir = await findGaitDirectory();
|
|
11
|
+
if (!gaitDir) {
|
|
12
|
+
// Fallback to process cwd
|
|
13
|
+
return process.cwd();
|
|
14
|
+
}
|
|
15
|
+
return path.dirname(gaitDir);
|
|
16
|
+
}
|
|
17
|
+
export async function getWorktreesBase() {
|
|
18
|
+
const repoRoot = await getRepoRoot();
|
|
19
|
+
return path.join(repoRoot, 'worktrees');
|
|
20
|
+
}
|
|
21
|
+
export function sanitizeBranchName(name) {
|
|
22
|
+
if (name.length > 120) {
|
|
23
|
+
throw new Error('Invalid branch/worktree name: too long');
|
|
24
|
+
}
|
|
25
|
+
if (!/^[A-Za-z0-9/_\-\.]+$/.test(name)) {
|
|
26
|
+
throw new Error('Invalid branch/worktree name');
|
|
27
|
+
}
|
|
28
|
+
return name;
|
|
29
|
+
}
|
|
30
|
+
export async function resolveSafeWorktreePath(name) {
|
|
31
|
+
const base = await getWorktreesBase();
|
|
32
|
+
const safeName = sanitizeBranchName(name);
|
|
33
|
+
const resolved = path.resolve(base, safeName);
|
|
34
|
+
if (!resolved.startsWith(base)) {
|
|
35
|
+
throw new Error('Invalid worktree path');
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export async function runGit(args, opts) {
|
|
40
|
+
const repoRoot = await getRepoRoot();
|
|
41
|
+
const cwd = opts?.cwd || repoRoot;
|
|
42
|
+
return execFile('git', args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
43
|
+
}
|
|
44
|
+
// --- Status ---
|
|
45
|
+
export async function getRepoStatus() {
|
|
46
|
+
const { stdout } = await runGit(['status', '--porcelain=v1', '-b']);
|
|
47
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
48
|
+
let branch = 'unknown';
|
|
49
|
+
let ahead = 0;
|
|
50
|
+
let behind = 0;
|
|
51
|
+
let upstream;
|
|
52
|
+
const changes = [];
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.startsWith('## ')) {
|
|
55
|
+
// Example: ## main...origin/main [ahead 1, behind 2]
|
|
56
|
+
const m = line.match(/^##\s+([^\.\s]+)(?:\.\.\.([^\s]+))?(?:.*?\bahead (\d+))?(?:.*?\bbehind (\d+))?/);
|
|
57
|
+
if (m) {
|
|
58
|
+
branch = m[1];
|
|
59
|
+
upstream = m[2] || undefined;
|
|
60
|
+
ahead = m[3] ? parseInt(m[3], 10) : 0;
|
|
61
|
+
behind = m[4] ? parseInt(m[4], 10) : 0;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Fallback to just branch name after ##
|
|
65
|
+
branch = line.slice(3).trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Porcelain entry e.g. " M src/app.ts" or "?? README.md"
|
|
70
|
+
const status = line.slice(0, 2).trim() || line.slice(0, 2);
|
|
71
|
+
const file = line.slice(3);
|
|
72
|
+
changes.push({ path: file, status });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let head;
|
|
76
|
+
try {
|
|
77
|
+
const { stdout: sha } = await runGit(['rev-parse', 'HEAD']);
|
|
78
|
+
head = sha.trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore if no commits yet
|
|
82
|
+
}
|
|
83
|
+
return { branch, ahead, behind, upstream, head, changes };
|
|
84
|
+
}
|
|
85
|
+
// --- Branches ---
|
|
86
|
+
export async function listBranches() {
|
|
87
|
+
const { stdout } = await runGit(['branch', '--list']);
|
|
88
|
+
return stdout
|
|
89
|
+
.split('\n')
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((l) => {
|
|
92
|
+
const current = l.trim().startsWith('* ');
|
|
93
|
+
const name = l.replace('*', '').trim();
|
|
94
|
+
return { name, current };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function createBranch(name, from = 'main') {
|
|
98
|
+
const safe = sanitizeBranchName(name);
|
|
99
|
+
await runGit(['branch', safe, from]);
|
|
100
|
+
}
|
|
101
|
+
export async function deleteBranch(name) {
|
|
102
|
+
const safe = sanitizeBranchName(name);
|
|
103
|
+
await runGit(['branch', '-D', safe]);
|
|
104
|
+
}
|
|
105
|
+
// --- Push / Pull ---
|
|
106
|
+
export async function pushCurrent() {
|
|
107
|
+
const { stdout } = await runGit(['push']);
|
|
108
|
+
return stdout;
|
|
109
|
+
}
|
|
110
|
+
export async function pullCurrent() {
|
|
111
|
+
const { stdout } = await runGit(['pull']);
|
|
112
|
+
return stdout;
|
|
113
|
+
}
|
|
114
|
+
export async function listWorktrees() {
|
|
115
|
+
const { stdout } = await runGit(['worktree', 'list', '--porcelain']);
|
|
116
|
+
// Parse porcelain groups separated by blank lines with fields: worktree <path>, HEAD <sha>, branch <refs/heads/x>, locked
|
|
117
|
+
const entries = [];
|
|
118
|
+
const blocks = stdout.split('\n\n').map((b) => b.trim()).filter(Boolean);
|
|
119
|
+
for (const block of blocks) {
|
|
120
|
+
const lines = block.split('\n');
|
|
121
|
+
let wtPath = '';
|
|
122
|
+
let head;
|
|
123
|
+
let branchRef = '';
|
|
124
|
+
let locked = false;
|
|
125
|
+
for (const l of lines) {
|
|
126
|
+
const [k, ...rest] = l.split(' ');
|
|
127
|
+
const v = rest.join(' ');
|
|
128
|
+
if (k === 'worktree')
|
|
129
|
+
wtPath = v;
|
|
130
|
+
if (k === 'HEAD')
|
|
131
|
+
head = v;
|
|
132
|
+
if (k === 'branch')
|
|
133
|
+
branchRef = v;
|
|
134
|
+
if (k === 'locked')
|
|
135
|
+
locked = true;
|
|
136
|
+
}
|
|
137
|
+
const branch = branchRef.replace('refs/heads/', '');
|
|
138
|
+
const name = path.basename(wtPath);
|
|
139
|
+
entries.push({ name, path: wtPath, branch, head, locked });
|
|
140
|
+
}
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
async function getWorktreeByName(name) {
|
|
144
|
+
const all = await listWorktrees();
|
|
145
|
+
return all.find((w) => w.name === name || w.branch === name);
|
|
146
|
+
}
|
|
147
|
+
export async function addWorktree(branch, from) {
|
|
148
|
+
const safeBranch = sanitizeBranchName(branch);
|
|
149
|
+
const wtPath = await resolveSafeWorktreePath(safeBranch);
|
|
150
|
+
const base = await getWorktreesBase();
|
|
151
|
+
await fs.mkdir(base, { recursive: true });
|
|
152
|
+
// Ensure parent directory exists
|
|
153
|
+
// Use git worktree add -b when creating from another branch
|
|
154
|
+
if (from) {
|
|
155
|
+
await runGit(['worktree', 'add', '-b', safeBranch, wtPath, from]);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await runGit(['worktree', 'add', wtPath, safeBranch]);
|
|
159
|
+
}
|
|
160
|
+
return { path: wtPath, branch: safeBranch, from };
|
|
161
|
+
}
|
|
162
|
+
export async function removeWorktree(nameOrPath) {
|
|
163
|
+
// If a named worktree exists, use its actual path; otherwise treat as path relative to base
|
|
164
|
+
let targetPath = nameOrPath;
|
|
165
|
+
if (!path.isAbsolute(nameOrPath)) {
|
|
166
|
+
const found = await getWorktreeByName(nameOrPath);
|
|
167
|
+
if (found) {
|
|
168
|
+
targetPath = found.path;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
targetPath = await resolveSafeWorktreePath(nameOrPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await runGit(['worktree', 'remove', targetPath]);
|
|
175
|
+
}
|
|
176
|
+
export async function commitInWorktree(name, message, files) {
|
|
177
|
+
const found = await getWorktreeByName(name);
|
|
178
|
+
const wtPath = found ? found.path : await resolveSafeWorktreePath(name);
|
|
179
|
+
// git -C <worktree> add ... && commit -m
|
|
180
|
+
if (files && files.length > 0) {
|
|
181
|
+
await runGit(['-C', wtPath, 'add', ...files]);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
await runGit(['-C', wtPath, 'add', '-A']);
|
|
185
|
+
}
|
|
186
|
+
await runGit(['-C', wtPath, 'commit', '-m', message]);
|
|
187
|
+
const { stdout } = await runGit(['-C', wtPath, 'rev-parse', 'HEAD']);
|
|
188
|
+
return { commitHash: stdout.trim() };
|
|
189
|
+
}
|
|
190
|
+
export async function pushWorktree(name) {
|
|
191
|
+
const found = await getWorktreeByName(name);
|
|
192
|
+
const wtPath = found ? found.path : await resolveSafeWorktreePath(name);
|
|
193
|
+
const { stdout } = await runGit(['-C', wtPath, 'push']);
|
|
194
|
+
return stdout;
|
|
195
|
+
}
|
|
196
|
+
export async function pullWorktree(name) {
|
|
197
|
+
const found = await getWorktreeByName(name);
|
|
198
|
+
const wtPath = found ? found.path : await resolveSafeWorktreePath(name);
|
|
199
|
+
const { stdout } = await runGit(['-C', wtPath, 'pull']);
|
|
200
|
+
return stdout;
|
|
201
|
+
}
|
|
@@ -2,11 +2,24 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import Fuse from 'fuse.js';
|
|
5
|
-
import { getGaitPath } from '../gait-path.js';
|
|
6
5
|
export class FileStorageAdapter {
|
|
7
6
|
basePath;
|
|
8
7
|
constructor(basePath) {
|
|
9
|
-
|
|
8
|
+
if (basePath) {
|
|
9
|
+
this.basePath = basePath;
|
|
10
|
+
}
|
|
11
|
+
else if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
|
|
12
|
+
// Dev mode: use project root .gait directory
|
|
13
|
+
this.basePath = process.env.GAIT_DEV_ROOT;
|
|
14
|
+
}
|
|
15
|
+
else if (process.env.GAIT_DATA_PATH) {
|
|
16
|
+
// Production mode: use GAIT_DATA_PATH (set by CLI)
|
|
17
|
+
this.basePath = path.join(process.env.GAIT_DATA_PATH, '.gait');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Fallback: use current directory .gait
|
|
21
|
+
this.basePath = '.gait';
|
|
22
|
+
}
|
|
10
23
|
}
|
|
11
24
|
async ensureDirectories() {
|
|
12
25
|
const dirs = ['proposals', 'specs', 'flags', 'experiments', 'templates'];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { getRepoStatus, listBranches, createBranch, deleteBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, } from '../../../../lib/git.js';
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
// Status
|
|
5
|
+
app.get('/status', async (c) => {
|
|
6
|
+
try {
|
|
7
|
+
const status = await getRepoStatus();
|
|
8
|
+
return c.json({ success: true, data: status });
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
// Branches
|
|
15
|
+
app.get('/branches', async (c) => {
|
|
16
|
+
try {
|
|
17
|
+
const branches = await listBranches();
|
|
18
|
+
return c.json({ success: true, data: branches });
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
app.post('/branches', async (c) => {
|
|
25
|
+
try {
|
|
26
|
+
const body = await c.req.json();
|
|
27
|
+
const name = String(body?.name);
|
|
28
|
+
const from = body?.from ? String(body.from) : 'main';
|
|
29
|
+
if (!name)
|
|
30
|
+
return c.json({ success: false, error: { message: 'name required' } }, 400);
|
|
31
|
+
await createBranch(name, from);
|
|
32
|
+
return c.json({ success: true, data: { created: name, from } });
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
app.delete('/branches/:branch', async (c) => {
|
|
39
|
+
try {
|
|
40
|
+
const name = c.req.param('branch');
|
|
41
|
+
await deleteBranch(name);
|
|
42
|
+
return c.json({ success: true, data: { deleted: name } });
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Commits (in current repo root)
|
|
49
|
+
app.post('/commits', async (c) => {
|
|
50
|
+
try {
|
|
51
|
+
const body = await c.req.json();
|
|
52
|
+
const message = String(body?.message || '');
|
|
53
|
+
const files = Array.isArray(body?.files) ? body.files.map(String) : undefined;
|
|
54
|
+
if (!message)
|
|
55
|
+
return c.json({ success: false, error: { message: 'message required' } }, 400);
|
|
56
|
+
// Use repo root, add files or all, then commit
|
|
57
|
+
// Reuse worktree commit logic with special name? Simpler: do add/commit at repo root using git.ts helpers
|
|
58
|
+
// Minimal inline since helpers are worktree-centric
|
|
59
|
+
const { runGit } = await import('../../../../lib/git.js');
|
|
60
|
+
if (files && files.length > 0) {
|
|
61
|
+
await runGit(['add', ...files]);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await runGit(['add', '-A']);
|
|
65
|
+
}
|
|
66
|
+
await runGit(['commit', '-m', message]);
|
|
67
|
+
const { stdout } = await runGit(['rev-parse', 'HEAD']);
|
|
68
|
+
return c.json({ success: true, data: { message, files: files ?? null, commitHash: stdout.trim() } });
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Push / Pull (current)
|
|
75
|
+
app.post('/push', async (c) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await pushCurrent();
|
|
78
|
+
return c.json({ success: true, data: { result } });
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
app.post('/pull', async (c) => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await pullCurrent();
|
|
87
|
+
return c.json({ success: true, data: { result } });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Worktrees
|
|
94
|
+
app.get('/worktrees', async (c) => {
|
|
95
|
+
try {
|
|
96
|
+
const data = await listWorktrees();
|
|
97
|
+
return c.json({ success: true, data });
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
app.post('/worktrees', async (c) => {
|
|
104
|
+
try {
|
|
105
|
+
const body = await c.req.json();
|
|
106
|
+
const branch = String(body?.branch || '');
|
|
107
|
+
const from = body?.from ? String(body.from) : undefined;
|
|
108
|
+
if (!branch)
|
|
109
|
+
return c.json({ success: false, error: { message: 'branch required' } }, 400);
|
|
110
|
+
const added = await addWorktree(branch, from);
|
|
111
|
+
return c.json({ success: true, data: { added } });
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
app.get('/worktrees/:name', async (c) => {
|
|
118
|
+
try {
|
|
119
|
+
const name = c.req.param('name');
|
|
120
|
+
const all = await listWorktrees();
|
|
121
|
+
const found = all.find((w) => w.name === name);
|
|
122
|
+
if (!found)
|
|
123
|
+
return c.json({ success: false, error: { message: 'Not found' } }, 404);
|
|
124
|
+
return c.json({ success: true, data: found });
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
app.delete('/worktrees/:name', async (c) => {
|
|
131
|
+
try {
|
|
132
|
+
const name = c.req.param('name');
|
|
133
|
+
await removeWorktree(name);
|
|
134
|
+
return c.json({ success: true, data: { removed: name } });
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
app.post('/worktrees/:name/commit', async (c) => {
|
|
141
|
+
try {
|
|
142
|
+
const name = c.req.param('name');
|
|
143
|
+
const body = await c.req.json();
|
|
144
|
+
const message = String(body?.message || '');
|
|
145
|
+
const files = Array.isArray(body?.files) ? body.files.map(String) : undefined;
|
|
146
|
+
if (!message)
|
|
147
|
+
return c.json({ success: false, error: { message: 'message required' } }, 400);
|
|
148
|
+
const res = await commitInWorktree(name, message, files);
|
|
149
|
+
return c.json({ success: true, data: { worktree: name, message, files: files ?? null, ...res } });
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
app.post('/worktrees/:name/push', async (c) => {
|
|
156
|
+
try {
|
|
157
|
+
const name = c.req.param('name');
|
|
158
|
+
const result = await pushWorktree(name);
|
|
159
|
+
return c.json({ success: true, data: { worktree: name, result } });
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
app.post('/worktrees/:name/pull', async (c) => {
|
|
166
|
+
try {
|
|
167
|
+
const name = c.req.param('name');
|
|
168
|
+
const result = await pullWorktree(name);
|
|
169
|
+
return c.json({ success: true, data: { worktree: name, result } });
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
return c.json({ success: false, error: { message: e.message } }, 500);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
export default app;
|
package/dist/server.js
CHANGED
|
@@ -64,6 +64,7 @@ import config from './routes/api/v1/config/index.js';
|
|
|
64
64
|
import user from './routes/api/v1/user/index.js';
|
|
65
65
|
import agents from './routes/api/v1/agents/index.js';
|
|
66
66
|
import agentsById from './routes/api/v1/agents/[id]/index.js';
|
|
67
|
+
import git from './routes/api/v1/git/index.js';
|
|
67
68
|
// Register API routes
|
|
68
69
|
app.route('/api/v1/proposals', proposals);
|
|
69
70
|
app.route('/api/v1/terminal/sessions', terminalSessions);
|
|
@@ -81,6 +82,7 @@ app.route('/api/v1/config', config);
|
|
|
81
82
|
app.route('/api/v1/user', user);
|
|
82
83
|
app.route('/api/v1/agents', agents);
|
|
83
84
|
app.route('/api/v1/agents/:id', agentsById);
|
|
85
|
+
app.route('/api/v1/git', git);
|
|
84
86
|
// Health check endpoint
|
|
85
87
|
app.get('/health', (c) => {
|
|
86
88
|
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovelybunch/api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/server-with-static.js",
|
|
6
6
|
"exports": {
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@hono/node-server": "^1.13.7",
|
|
34
34
|
"@hono/node-ws": "^1.0.6",
|
|
35
|
-
"@lovelybunch/core": "^1.0.
|
|
36
|
-
"@lovelybunch/types": "^1.0.
|
|
35
|
+
"@lovelybunch/core": "^1.0.27",
|
|
36
|
+
"@lovelybunch/types": "^1.0.27",
|
|
37
37
|
"dotenv": "^17.2.1",
|
|
38
38
|
"fuse.js": "^7.0.0",
|
|
39
39
|
"gray-matter": "^4.0.3",
|