@lovelybunch/api 1.0.66 → 1.0.68
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/routes/api/v1/events/events.test.d.ts +4 -0
- package/dist/routes/api/v1/events/events.test.js +289 -0
- package/dist/routes/api/v1/events/index.d.ts +7 -0
- package/dist/routes/api/v1/events/index.js +19 -0
- package/dist/routes/api/v1/events/purge/route.d.ts +19 -0
- package/dist/routes/api/v1/events/purge/route.js +62 -0
- package/dist/routes/api/v1/events/route.d.ts +30 -0
- package/dist/routes/api/v1/events/route.js +109 -0
- package/dist/routes/api/v1/events/status/route.d.ts +20 -0
- package/dist/routes/api/v1/events/status/route.js +53 -0
- package/dist/routes/api/v1/events/stream/route.d.ts +9 -0
- package/dist/routes/api/v1/events/stream/route.js +132 -0
- package/dist/routes/api/v1/init/index.d.ts +1 -0
- package/dist/routes/api/v1/init/index.js +1 -0
- package/dist/routes/api/v1/init/route.d.ts +3 -0
- package/dist/routes/api/v1/init/route.js +129 -0
- package/dist/routes/api/v1/onboard/index.d.ts +3 -0
- package/dist/routes/api/v1/onboard/index.js +8 -0
- package/dist/routes/api/v1/onboard/route.d.ts +13 -0
- package/dist/routes/api/v1/onboard/route.js +311 -0
- package/dist/routes/api/v1/onboarding/check/index.d.ts +3 -0
- package/dist/routes/api/v1/onboarding/check/index.js +5 -0
- package/dist/routes/api/v1/onboarding/check/route.d.ts +12 -0
- package/dist/routes/api/v1/onboarding/check/route.js +24 -0
- package/dist/routes/api/v1/onboarding/index.d.ts +1 -0
- package/dist/routes/api/v1/onboarding/index.js +1 -0
- package/dist/routes/api/v1/onboarding/route.d.ts +3 -0
- package/dist/routes/api/v1/onboarding/route.js +158 -0
- package/dist/routes/api/v1/proposals/[id]/route.js +57 -0
- package/dist/routes/api/v1/proposals/route.js +18 -0
- package/dist/server-with-static.js +62 -0
- package/dist/server.js +63 -0
- package/package.json +4 -4
- package/static/assets/{index-DuLX7Zvh.js → index-COf7Bc1u.js} +33 -33
- package/static/index.html +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events streaming endpoint for real-time tailing
|
|
3
|
+
*/
|
|
4
|
+
import { streamSSE } from "hono/streaming";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import * as fsSync from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { findGaitDirectory } from "../../../../../lib/gait-path.js";
|
|
9
|
+
import readline from "readline";
|
|
10
|
+
/**
|
|
11
|
+
* Get the events directory path
|
|
12
|
+
*/
|
|
13
|
+
async function getEventsDir() {
|
|
14
|
+
const gaitDir = await findGaitDirectory();
|
|
15
|
+
if (!gaitDir)
|
|
16
|
+
return null;
|
|
17
|
+
return path.join(gaitDir, "logs");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/v1/events/stream?since_seq=
|
|
21
|
+
* Server-Sent Events stream for near real-time tails
|
|
22
|
+
*/
|
|
23
|
+
export async function GET(c) {
|
|
24
|
+
const eventsDir = await getEventsDir();
|
|
25
|
+
if (!eventsDir) {
|
|
26
|
+
return c.json({ error: "Events directory not found" }, 404);
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(c.req.url);
|
|
29
|
+
const sinceSeq = parseInt(url.searchParams.get("since_seq") || "0", 10);
|
|
30
|
+
const currentFile = path.join(eventsDir, "events-current.jsonl");
|
|
31
|
+
return streamSSE(c, async (stream) => {
|
|
32
|
+
let lastSeq = sinceSeq;
|
|
33
|
+
let running = true;
|
|
34
|
+
// Send initial events from file
|
|
35
|
+
try {
|
|
36
|
+
const fileExists = await fs
|
|
37
|
+
.access(currentFile)
|
|
38
|
+
.then(() => true)
|
|
39
|
+
.catch(() => false);
|
|
40
|
+
if (fileExists) {
|
|
41
|
+
const fileStream = fsSync.createReadStream(currentFile);
|
|
42
|
+
const rl = readline.createInterface({
|
|
43
|
+
input: fileStream,
|
|
44
|
+
crlfDelay: Infinity,
|
|
45
|
+
});
|
|
46
|
+
for await (const line of rl) {
|
|
47
|
+
if (!line.trim())
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const event = JSON.parse(line);
|
|
51
|
+
if (event.seq > sinceSeq) {
|
|
52
|
+
await stream.writeSSE({
|
|
53
|
+
data: JSON.stringify(event),
|
|
54
|
+
});
|
|
55
|
+
lastSeq = Math.max(lastSeq, event.seq);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// Skip malformed lines
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// Ignore errors during initial load
|
|
67
|
+
}
|
|
68
|
+
// Poll for new events
|
|
69
|
+
// In a production implementation, this would use file watching (e.g., chokidar)
|
|
70
|
+
// For now, we'll poll every 1 second
|
|
71
|
+
const pollInterval = setInterval(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const fileExists = await fs
|
|
74
|
+
.access(currentFile)
|
|
75
|
+
.then(() => true)
|
|
76
|
+
.catch(() => false);
|
|
77
|
+
if (!fileExists)
|
|
78
|
+
return;
|
|
79
|
+
const fileStream = fsSync.createReadStream(currentFile);
|
|
80
|
+
const rl = readline.createInterface({
|
|
81
|
+
input: fileStream,
|
|
82
|
+
crlfDelay: Infinity,
|
|
83
|
+
});
|
|
84
|
+
for await (const line of rl) {
|
|
85
|
+
if (!line.trim())
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
const event = JSON.parse(line);
|
|
89
|
+
if (event.seq > lastSeq) {
|
|
90
|
+
await stream.writeSSE({
|
|
91
|
+
data: JSON.stringify(event),
|
|
92
|
+
});
|
|
93
|
+
lastSeq = event.seq;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// Skip malformed lines
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Continue polling even on errors
|
|
104
|
+
}
|
|
105
|
+
}, 1000);
|
|
106
|
+
// Send heartbeat keepalive every 15 seconds
|
|
107
|
+
const heartbeatInterval = setInterval(async () => {
|
|
108
|
+
try {
|
|
109
|
+
// Send empty event to keep connection alive
|
|
110
|
+
await stream.writeSSE({
|
|
111
|
+
data: "",
|
|
112
|
+
event: "heartbeat",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
clearInterval(heartbeatInterval);
|
|
117
|
+
clearInterval(pollInterval);
|
|
118
|
+
running = false;
|
|
119
|
+
}
|
|
120
|
+
}, 15000);
|
|
121
|
+
// Clean up on disconnect
|
|
122
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
123
|
+
clearInterval(pollInterval);
|
|
124
|
+
clearInterval(heartbeatInterval);
|
|
125
|
+
running = false;
|
|
126
|
+
});
|
|
127
|
+
// Keep stream alive
|
|
128
|
+
while (running) {
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { fileStorage } from '@lovelybunch/core';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
// Check if Coconut is initialized
|
|
8
|
+
app.get('/', async (c) => {
|
|
9
|
+
try {
|
|
10
|
+
const isInitialized = await fileStorage.isInitialized();
|
|
11
|
+
return c.json({ initialized: isInitialized });
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error('Error checking initialization status:', error);
|
|
15
|
+
return c.json({ error: 'Failed to check initialization status' }, 500);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
// Initialize Coconut
|
|
19
|
+
app.post('/', async (c) => {
|
|
20
|
+
try {
|
|
21
|
+
const body = await c.req.json();
|
|
22
|
+
const { name, description, initGit = false } = body;
|
|
23
|
+
if (!name || !description) {
|
|
24
|
+
return c.json({ error: 'Name and description are required' }, 400);
|
|
25
|
+
}
|
|
26
|
+
// Check if already initialized
|
|
27
|
+
const isInitialized = await fileStorage.isInitialized();
|
|
28
|
+
if (isInitialized) {
|
|
29
|
+
return c.json({ error: 'Coconut is already initialized' }, 400);
|
|
30
|
+
}
|
|
31
|
+
// Create configuration
|
|
32
|
+
const config = {
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
repository: {
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
37
|
+
},
|
|
38
|
+
policies: {
|
|
39
|
+
requireApproval: true,
|
|
40
|
+
minApprovers: 1,
|
|
41
|
+
allowSelfApproval: false,
|
|
42
|
+
autoMerge: false,
|
|
43
|
+
},
|
|
44
|
+
storage: {
|
|
45
|
+
type: 'file',
|
|
46
|
+
path: '.nut',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// Initialize storage
|
|
50
|
+
await fileStorage.init();
|
|
51
|
+
await fileStorage.saveConfig(config);
|
|
52
|
+
// Initialize git if requested
|
|
53
|
+
if (initGit) {
|
|
54
|
+
try {
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
// Check if git is already initialized
|
|
57
|
+
try {
|
|
58
|
+
await execAsync('git rev-parse --git-dir', { cwd });
|
|
59
|
+
// Git is already initialized, skip
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Git is not initialized, initialize it
|
|
63
|
+
await execAsync('git init', { cwd });
|
|
64
|
+
await execAsync('git add .', { cwd });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (gitError) {
|
|
68
|
+
console.error('Git initialization failed:', gitError);
|
|
69
|
+
// Don't fail the whole operation if git fails
|
|
70
|
+
return c.json({
|
|
71
|
+
success: true,
|
|
72
|
+
message: 'Coconut initialized successfully, but git initialization failed',
|
|
73
|
+
gitError: gitError instanceof Error ? gitError.message : 'Unknown error',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return c.json({
|
|
78
|
+
success: true,
|
|
79
|
+
message: 'Coconut initialized successfully',
|
|
80
|
+
config,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error('Error initializing Coconut:', error);
|
|
85
|
+
return c.json({
|
|
86
|
+
error: 'Failed to initialize Coconut',
|
|
87
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
88
|
+
}, 500);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Clone a repository
|
|
92
|
+
app.post('/clone', async (c) => {
|
|
93
|
+
try {
|
|
94
|
+
const body = await c.req.json();
|
|
95
|
+
const { url } = body;
|
|
96
|
+
if (!url) {
|
|
97
|
+
return c.json({ error: 'Repository URL is required' }, 400);
|
|
98
|
+
}
|
|
99
|
+
// Validate URL format
|
|
100
|
+
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
|
101
|
+
if (!urlPattern.test(url)) {
|
|
102
|
+
return c.json({ error: 'Invalid repository URL' }, 400);
|
|
103
|
+
}
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
try {
|
|
106
|
+
// Clone into current directory
|
|
107
|
+
await execAsync(`git clone ${url} .`, { cwd });
|
|
108
|
+
return c.json({
|
|
109
|
+
success: true,
|
|
110
|
+
message: 'Repository cloned successfully',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (cloneError) {
|
|
114
|
+
console.error('Git clone failed:', cloneError);
|
|
115
|
+
return c.json({
|
|
116
|
+
error: 'Failed to clone repository',
|
|
117
|
+
details: cloneError instanceof Error ? cloneError.message : 'Unknown error',
|
|
118
|
+
}, 500);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error('Error cloning repository:', error);
|
|
123
|
+
return c.json({
|
|
124
|
+
error: 'Failed to clone repository',
|
|
125
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
126
|
+
}, 500);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
export default app;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import * as route from './route.js';
|
|
3
|
+
const app = new Hono();
|
|
4
|
+
// GET /api/v1/onboard/check - Check if .nut exists and onboarding is needed
|
|
5
|
+
app.get('/check', route.GET_CHECK);
|
|
6
|
+
// POST /api/v1/onboard/initialize - Initialize Coconut (empty or clone)
|
|
7
|
+
app.post('/initialize', route.POST_INITIALIZE);
|
|
8
|
+
export default app;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
export declare function GET_CHECK(c: Context): Promise<Response & import("hono").TypedResponse<{
|
|
3
|
+
onboardCheckEnabled: boolean;
|
|
4
|
+
initialized: boolean;
|
|
5
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
6
|
+
export declare function POST_INITIALIZE(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
7
|
+
error: string;
|
|
8
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
9
|
+
success: true;
|
|
10
|
+
message: string;
|
|
11
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
12
|
+
error: any;
|
|
13
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { findGaitDirectory } from '../../../../lib/gait-path.js';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
export async function GET_CHECK(c) {
|
|
8
|
+
try {
|
|
9
|
+
// Check if COCONUT_ONBOARD_CHECK environment variable is set
|
|
10
|
+
const onboardCheckEnabled = process.env.COCONUT_ONBOARD_CHECK === 'true';
|
|
11
|
+
// Check if .nut directory exists
|
|
12
|
+
const gaitDir = await findGaitDirectory();
|
|
13
|
+
const initialized = gaitDir !== null;
|
|
14
|
+
return c.json({
|
|
15
|
+
onboardCheckEnabled,
|
|
16
|
+
initialized,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Error checking onboarding status:', error);
|
|
21
|
+
return c.json({
|
|
22
|
+
onboardCheckEnabled: false,
|
|
23
|
+
initialized: true, // Default to initialized on error to avoid redirecting
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function POST_INITIALIZE(c) {
|
|
28
|
+
try {
|
|
29
|
+
const data = await c.req.json();
|
|
30
|
+
// Get the current working directory (where the server was started)
|
|
31
|
+
const cwd = process.env.GAIT_DATA_PATH || process.cwd();
|
|
32
|
+
if (data.type === 'empty') {
|
|
33
|
+
// Create an empty Coconut project
|
|
34
|
+
if (!data.name || data.name.trim().length === 0) {
|
|
35
|
+
return c.json({ error: 'Project name is required' }, 400);
|
|
36
|
+
}
|
|
37
|
+
// Initialize .nut directory
|
|
38
|
+
const nutDir = path.join(cwd, '.nut');
|
|
39
|
+
// Check if .nut already exists
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(nutDir);
|
|
42
|
+
return c.json({ error: '.nut directory already exists' }, 400);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Directory doesn't exist, which is what we want
|
|
46
|
+
}
|
|
47
|
+
// Create directory structure
|
|
48
|
+
await fs.mkdir(nutDir, { recursive: true });
|
|
49
|
+
await fs.mkdir(path.join(nutDir, 'proposals'), { recursive: true });
|
|
50
|
+
await fs.mkdir(path.join(nutDir, 'agents'), { recursive: true });
|
|
51
|
+
await fs.mkdir(path.join(nutDir, 'context'), { recursive: true });
|
|
52
|
+
await fs.mkdir(path.join(nutDir, 'context', 'knowledge'), { recursive: true });
|
|
53
|
+
await fs.mkdir(path.join(nutDir, 'resources'), { recursive: true });
|
|
54
|
+
await fs.mkdir(path.join(nutDir, 'resources', 'files'), { recursive: true });
|
|
55
|
+
await fs.mkdir(path.join(nutDir, 'resources', 'thumbnails'), { recursive: true });
|
|
56
|
+
await fs.mkdir(path.join(nutDir, 'jobs'), { recursive: true });
|
|
57
|
+
await fs.mkdir(path.join(nutDir, '.schema'), { recursive: true });
|
|
58
|
+
// Create config.json
|
|
59
|
+
const config = {
|
|
60
|
+
version: '1.0',
|
|
61
|
+
project: {
|
|
62
|
+
name: data.name.trim(),
|
|
63
|
+
description: data.description?.trim() || '',
|
|
64
|
+
},
|
|
65
|
+
created: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
await fs.writeFile(path.join(nutDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
68
|
+
// Create basic project.md
|
|
69
|
+
const projectMd = `---
|
|
70
|
+
version: '1.0'
|
|
71
|
+
updated: '${new Date().toISOString().split('T')[0]}'
|
|
72
|
+
type: project
|
|
73
|
+
status: active
|
|
74
|
+
category: overview
|
|
75
|
+
project:
|
|
76
|
+
name: ${data.name.trim()}
|
|
77
|
+
description: ${data.description?.trim() || 'A new Coconut project'}
|
|
78
|
+
version: '0.1.0'
|
|
79
|
+
stage: development
|
|
80
|
+
goals:
|
|
81
|
+
primary: Build an amazing product
|
|
82
|
+
metrics: []
|
|
83
|
+
non_goals: []
|
|
84
|
+
---
|
|
85
|
+
# ${data.name.trim()}
|
|
86
|
+
|
|
87
|
+
${data.description?.trim() || 'Welcome to your new Coconut project!'}
|
|
88
|
+
|
|
89
|
+
## Getting Started
|
|
90
|
+
|
|
91
|
+
This project was initialized with Coconut. Start by creating change proposals and working with AI agents to build your product.
|
|
92
|
+
`;
|
|
93
|
+
await fs.writeFile(path.join(nutDir, 'context', 'project.md'), projectMd, 'utf-8');
|
|
94
|
+
// Create basic architecture.md
|
|
95
|
+
const architectureMd = `---
|
|
96
|
+
version: '1.0'
|
|
97
|
+
updated: '${new Date().toISOString().split('T')[0]}'
|
|
98
|
+
type: architecture
|
|
99
|
+
category: design
|
|
100
|
+
stack:
|
|
101
|
+
runtime: node
|
|
102
|
+
framework: ''
|
|
103
|
+
language: typescript
|
|
104
|
+
database: ''
|
|
105
|
+
deployment: ''
|
|
106
|
+
---
|
|
107
|
+
# Architecture
|
|
108
|
+
|
|
109
|
+
Document your system architecture here.
|
|
110
|
+
|
|
111
|
+
## Stack
|
|
112
|
+
|
|
113
|
+
Describe your technology stack and key architectural decisions.
|
|
114
|
+
|
|
115
|
+
## Components
|
|
116
|
+
|
|
117
|
+
Outline the major components of your system.
|
|
118
|
+
`;
|
|
119
|
+
await fs.writeFile(path.join(nutDir, 'context', 'architecture.md'), architectureMd, 'utf-8');
|
|
120
|
+
// Initialize git repository
|
|
121
|
+
try {
|
|
122
|
+
await execAsync('git init', { cwd });
|
|
123
|
+
// Create .gitignore if it doesn't exist
|
|
124
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
125
|
+
let gitignoreContent = '';
|
|
126
|
+
try {
|
|
127
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// File doesn't exist, create it
|
|
131
|
+
}
|
|
132
|
+
// Add .nut/auth.json to .gitignore if not already there
|
|
133
|
+
if (!gitignoreContent.includes('.nut/auth.json')) {
|
|
134
|
+
gitignoreContent += '\n# Coconut authentication (contains secrets)\n.nut/auth.json\n';
|
|
135
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error('Failed to initialize git repository:', error);
|
|
140
|
+
// Continue even if git init fails
|
|
141
|
+
}
|
|
142
|
+
return c.json({
|
|
143
|
+
success: true,
|
|
144
|
+
message: `Coconut project "${data.name}" initialized successfully!`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else if (data.type === 'clone') {
|
|
148
|
+
// Clone a repository
|
|
149
|
+
if (!data.repoUrl || data.repoUrl.trim().length === 0) {
|
|
150
|
+
return c.json({ error: 'Repository URL is required' }, 400);
|
|
151
|
+
}
|
|
152
|
+
// Validate that the current directory is empty or only has .git
|
|
153
|
+
const files = await fs.readdir(cwd);
|
|
154
|
+
const nonGitFiles = files.filter(f => f !== '.git' && f !== '.gitignore');
|
|
155
|
+
if (nonGitFiles.length > 0) {
|
|
156
|
+
return c.json({
|
|
157
|
+
error: 'Current directory is not empty. Please run this in an empty directory or remove existing files.'
|
|
158
|
+
}, 400);
|
|
159
|
+
}
|
|
160
|
+
// Clone the repository into current directory
|
|
161
|
+
try {
|
|
162
|
+
await execAsync(`git clone "${data.repoUrl.trim()}" .`, { cwd });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('Failed to clone repository:', error);
|
|
166
|
+
return c.json({
|
|
167
|
+
error: `Failed to clone repository: ${error.message}`
|
|
168
|
+
}, 500);
|
|
169
|
+
}
|
|
170
|
+
// Initialize .nut directory (similar to empty project but without git init)
|
|
171
|
+
const nutDir = path.join(cwd, '.nut');
|
|
172
|
+
// Check if .nut already exists in the cloned repo
|
|
173
|
+
try {
|
|
174
|
+
await fs.access(nutDir);
|
|
175
|
+
return c.json({
|
|
176
|
+
success: true,
|
|
177
|
+
message: 'Repository cloned successfully. Coconut is already initialized in this project.',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Directory doesn't exist, initialize it
|
|
182
|
+
}
|
|
183
|
+
// Create directory structure
|
|
184
|
+
await fs.mkdir(nutDir, { recursive: true });
|
|
185
|
+
await fs.mkdir(path.join(nutDir, 'proposals'), { recursive: true });
|
|
186
|
+
await fs.mkdir(path.join(nutDir, 'agents'), { recursive: true });
|
|
187
|
+
await fs.mkdir(path.join(nutDir, 'context'), { recursive: true });
|
|
188
|
+
await fs.mkdir(path.join(nutDir, 'context', 'knowledge'), { recursive: true });
|
|
189
|
+
await fs.mkdir(path.join(nutDir, 'resources'), { recursive: true });
|
|
190
|
+
await fs.mkdir(path.join(nutDir, 'resources', 'files'), { recursive: true });
|
|
191
|
+
await fs.mkdir(path.join(nutDir, 'resources', 'thumbnails'), { recursive: true });
|
|
192
|
+
await fs.mkdir(path.join(nutDir, 'jobs'), { recursive: true });
|
|
193
|
+
await fs.mkdir(path.join(nutDir, '.schema'), { recursive: true });
|
|
194
|
+
// Try to extract project name from package.json if it exists
|
|
195
|
+
let projectName = 'Cloned Project';
|
|
196
|
+
let projectDescription = 'A project cloned from a Git repository';
|
|
197
|
+
try {
|
|
198
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
199
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
|
|
200
|
+
if (packageJson.name) {
|
|
201
|
+
projectName = packageJson.name;
|
|
202
|
+
}
|
|
203
|
+
if (packageJson.description) {
|
|
204
|
+
projectDescription = packageJson.description;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// package.json doesn't exist or couldn't be read
|
|
209
|
+
}
|
|
210
|
+
// Create config.json
|
|
211
|
+
const config = {
|
|
212
|
+
version: '1.0',
|
|
213
|
+
project: {
|
|
214
|
+
name: projectName,
|
|
215
|
+
description: projectDescription,
|
|
216
|
+
clonedFrom: data.repoUrl.trim(),
|
|
217
|
+
},
|
|
218
|
+
created: new Date().toISOString(),
|
|
219
|
+
};
|
|
220
|
+
await fs.writeFile(path.join(nutDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
221
|
+
// Create basic project.md
|
|
222
|
+
const projectMd = `---
|
|
223
|
+
version: '1.0'
|
|
224
|
+
updated: '${new Date().toISOString().split('T')[0]}'
|
|
225
|
+
type: project
|
|
226
|
+
status: active
|
|
227
|
+
category: overview
|
|
228
|
+
project:
|
|
229
|
+
name: ${projectName}
|
|
230
|
+
description: ${projectDescription}
|
|
231
|
+
version: '0.1.0'
|
|
232
|
+
stage: development
|
|
233
|
+
repository: ${data.repoUrl.trim()}
|
|
234
|
+
goals:
|
|
235
|
+
primary: Continue development with Coconut
|
|
236
|
+
metrics: []
|
|
237
|
+
non_goals: []
|
|
238
|
+
---
|
|
239
|
+
# ${projectName}
|
|
240
|
+
|
|
241
|
+
${projectDescription}
|
|
242
|
+
|
|
243
|
+
## About
|
|
244
|
+
|
|
245
|
+
This project was cloned from: ${data.repoUrl.trim()}
|
|
246
|
+
|
|
247
|
+
## Getting Started
|
|
248
|
+
|
|
249
|
+
Start by creating change proposals and working with AI agents to enhance this codebase.
|
|
250
|
+
`;
|
|
251
|
+
await fs.writeFile(path.join(nutDir, 'context', 'project.md'), projectMd, 'utf-8');
|
|
252
|
+
// Create basic architecture.md
|
|
253
|
+
const architectureMd = `---
|
|
254
|
+
version: '1.0'
|
|
255
|
+
updated: '${new Date().toISOString().split('T')[0]}'
|
|
256
|
+
type: architecture
|
|
257
|
+
category: design
|
|
258
|
+
stack:
|
|
259
|
+
runtime: node
|
|
260
|
+
framework: ''
|
|
261
|
+
language: typescript
|
|
262
|
+
database: ''
|
|
263
|
+
deployment: ''
|
|
264
|
+
---
|
|
265
|
+
# Architecture
|
|
266
|
+
|
|
267
|
+
Document your system architecture here.
|
|
268
|
+
|
|
269
|
+
## Stack
|
|
270
|
+
|
|
271
|
+
Describe your technology stack and key architectural decisions.
|
|
272
|
+
|
|
273
|
+
## Components
|
|
274
|
+
|
|
275
|
+
Outline the major components of your system.
|
|
276
|
+
`;
|
|
277
|
+
await fs.writeFile(path.join(nutDir, 'context', 'architecture.md'), architectureMd, 'utf-8');
|
|
278
|
+
// Add .nut/auth.json to .gitignore
|
|
279
|
+
try {
|
|
280
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
281
|
+
let gitignoreContent = '';
|
|
282
|
+
try {
|
|
283
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// File doesn't exist, create it
|
|
287
|
+
}
|
|
288
|
+
if (!gitignoreContent.includes('.nut/auth.json')) {
|
|
289
|
+
gitignoreContent += '\n# Coconut authentication (contains secrets)\n.nut/auth.json\n';
|
|
290
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error('Failed to update .gitignore:', error);
|
|
295
|
+
}
|
|
296
|
+
return c.json({
|
|
297
|
+
success: true,
|
|
298
|
+
message: `Repository cloned and Coconut initialized successfully!`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
return c.json({ error: 'Invalid onboarding type' }, 400);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
console.error('Onboarding initialization error:', error);
|
|
307
|
+
return c.json({
|
|
308
|
+
error: error.message || 'Failed to initialize Coconut'
|
|
309
|
+
}, 500);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
/**
|
|
3
|
+
* GET /api/v1/onboarding/check
|
|
4
|
+
* Check if .nut directory exists WITHOUT creating it
|
|
5
|
+
*/
|
|
6
|
+
export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
7
|
+
success: true;
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
10
|
+
success: false;
|
|
11
|
+
message: any;
|
|
12
|
+
}, 500, "json">)>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/v1/onboarding/check
|
|
5
|
+
* Check if .nut directory exists WITHOUT creating it
|
|
6
|
+
*/
|
|
7
|
+
export async function GET(c) {
|
|
8
|
+
try {
|
|
9
|
+
const cwd = process.env.GAIT_DATA_PATH || process.cwd();
|
|
10
|
+
const nutPath = join(cwd, '.nut');
|
|
11
|
+
const nutExists = existsSync(nutPath);
|
|
12
|
+
return c.json({
|
|
13
|
+
success: true,
|
|
14
|
+
initialized: nutExists,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Error checking .nut directory:', error);
|
|
19
|
+
return c.json({
|
|
20
|
+
success: false,
|
|
21
|
+
message: error.message || 'Failed to check initialization status',
|
|
22
|
+
}, 500);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|