Skip to content

Commit 08fef4e

Browse files
Implemented CLI-driven documentation system that auto-generates and maintains project docs directly from the codebase.
1 parent 5171257 commit 08fef4e

33 files changed

Lines changed: 3401 additions & 0 deletions

File tree

.developerdoc/state.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"lastSyncedCommit": "5171257e133f63ffb0c67c6f9e50cf9f7980e3bf"
3+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ yarn-error.log*
3939
# typescript
4040
*.tsbuildinfo
4141
next-env.d.ts
42+
.developerdoc/config.json

app/api/cli/auth/confirm/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextResponse } from "next/server";
2+
import { getCurrentUser } from "@/lib/users";
3+
import { approveCliAuthSession } from "@/lib/sync/cli-auth.service";
4+
5+
export async function POST(request: Request) {
6+
try {
7+
const user = await getCurrentUser();
8+
if (!user) {
9+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
10+
}
11+
12+
const body = await request.json().catch(() => ({}));
13+
const userCode = typeof body?.userCode === "string" ? body.userCode.trim().toUpperCase() : "";
14+
if (!userCode) {
15+
return NextResponse.json({ error: "userCode is required" }, { status: 400 });
16+
}
17+
18+
const result = await approveCliAuthSession(userCode, user.id);
19+
if (!result.ok) {
20+
if (result.reason === "not_found") {
21+
return NextResponse.json({ error: "Invalid user code" }, { status: 404 });
22+
}
23+
if (result.reason === "expired") {
24+
return NextResponse.json({ error: "This link request has expired" }, { status: 410 });
25+
}
26+
return NextResponse.json({ error: "This link request is no longer valid" }, { status: 409 });
27+
}
28+
29+
return NextResponse.json({ success: true });
30+
} catch (error) {
31+
console.error("Error confirming CLI auth session:", error);
32+
return NextResponse.json({ error: "Failed to confirm CLI authentication" }, { status: 500 });
33+
}
34+
}

app/api/cli/auth/poll/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextResponse } from "next/server";
2+
import { pollCliAuthSession } from "@/lib/sync/cli-auth.service";
3+
4+
export async function POST(request: Request) {
5+
try {
6+
const body = await request.json().catch(() => ({}));
7+
const deviceCode = typeof body?.deviceCode === "string" ? body.deviceCode.trim() : "";
8+
if (!deviceCode) {
9+
return NextResponse.json({ error: "deviceCode is required" }, { status: 400 });
10+
}
11+
12+
const result = await pollCliAuthSession(deviceCode);
13+
if (result.status === "invalid") {
14+
return NextResponse.json({ error: "Invalid device code" }, { status: 404 });
15+
}
16+
if (result.status === "expired") {
17+
return NextResponse.json({ status: "expired" }, { status: 410 });
18+
}
19+
if (result.status === "used") {
20+
return NextResponse.json({ status: "used" }, { status: 409 });
21+
}
22+
if (result.status === "pending") {
23+
return NextResponse.json({ status: "pending" });
24+
}
25+
26+
return NextResponse.json({
27+
status: "approved",
28+
cliAuthToken: result.cliAuthToken,
29+
});
30+
} catch (error) {
31+
console.error("Error polling CLI auth session:", error);
32+
return NextResponse.json({ error: "Failed to poll CLI authentication status" }, { status: 500 });
33+
}
34+
}

app/api/cli/auth/start/route.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { NextResponse } from "next/server";
2+
import { startCliAuthSession } from "@/lib/sync/cli-auth.service";
3+
4+
export async function POST(request: Request) {
5+
try {
6+
const body = await request.json().catch(() => ({}));
7+
const requestedBaseUrl = typeof body?.apiUrl === "string" ? body.apiUrl : null;
8+
const origin = request.headers.get("origin");
9+
const baseUrl = requestedBaseUrl || origin || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
10+
11+
const session = await startCliAuthSession(baseUrl);
12+
return NextResponse.json({
13+
deviceCode: session.deviceCode,
14+
userCode: session.userCode,
15+
verificationUrl: session.verificationUrl,
16+
expiresAt: session.expiresAt.toISOString(),
17+
});
18+
} catch (error) {
19+
console.error("Error starting CLI auth session:", error);
20+
return NextResponse.json({ error: "Failed to start CLI authentication flow" }, { status: 500 });
21+
}
22+
}

app/api/cli/changes/route.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { storeSyncChange, validateSyncToken } from '@/lib/sync/sync.service';
3+
4+
function extractBearerToken(request: NextRequest): string | null {
5+
const header = request.headers.get('authorization');
6+
if (!header || !header.toLowerCase().startsWith('bearer ')) return null;
7+
return header.slice(7).trim() || null;
8+
}
9+
10+
function hasObviousSecrets(value: unknown): boolean {
11+
const suspectPatterns = [
12+
/-----BEGIN [A-Z ]+ PRIVATE KEY-----/i,
13+
/AKIA[0-9A-Z]{16}/,
14+
/xox[baprs]-[a-zA-Z0-9-]{10,}/,
15+
/(?:password|passwd|secret|api[_-]?key|token)\s*[:=]\s*["'][^"']{6,}["']/i,
16+
/\.env/i,
17+
];
18+
19+
const serialized = JSON.stringify(value || {});
20+
return suspectPatterns.some((pattern) => pattern.test(serialized));
21+
}
22+
23+
export async function POST(request: NextRequest) {
24+
try {
25+
const body = await request.json();
26+
const syncProjectId = body.syncProjectId as string | undefined;
27+
const syncToken = (body.syncToken as string | undefined) || extractBearerToken(request);
28+
const fromCommit = body.fromCommit as string | undefined;
29+
const toCommit = body.toCommit as string | undefined;
30+
const branch = body.branch as string | undefined;
31+
const changedFiles = body.changedFiles;
32+
const diffStat = body.diffStat;
33+
34+
if (!syncProjectId || !syncToken || !toCommit || !changedFiles) {
35+
return NextResponse.json(
36+
{ error: 'syncProjectId, syncToken, toCommit, and changedFiles are required' },
37+
{ status: 400 }
38+
);
39+
}
40+
41+
if (hasObviousSecrets(changedFiles)) {
42+
return NextResponse.json(
43+
{ error: 'Payload appears to include secrets and was rejected' },
44+
{ status: 400 }
45+
);
46+
}
47+
48+
const syncProject = await validateSyncToken(syncProjectId, syncToken);
49+
if (!syncProject) {
50+
return NextResponse.json({ error: 'Invalid sync credentials' }, { status: 401 });
51+
}
52+
53+
await storeSyncChange({
54+
syncProjectId: syncProject.id,
55+
fromCommit,
56+
toCommit,
57+
branch,
58+
changedFiles,
59+
diffStat,
60+
});
61+
62+
return NextResponse.json({ success: true });
63+
} catch (error) {
64+
console.error('Error storing CLI changes:', error);
65+
return NextResponse.json(
66+
{ error: 'Failed to store changes' },
67+
{ status: 500 }
68+
);
69+
}
70+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getCurrentUser } from '@/lib/users';
3+
import { prisma } from '@/lib/db';
4+
import {
5+
listProjectSyncStatus,
6+
listRecentSyncChanges,
7+
} from '@/lib/sync/sync.service';
8+
9+
export async function GET(request: NextRequest) {
10+
try {
11+
const user = await getCurrentUser();
12+
if (!user) {
13+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
14+
}
15+
16+
const projectId = request.nextUrl.searchParams.get('projectId');
17+
if (!projectId) {
18+
return NextResponse.json({ error: 'projectId is required' }, { status: 400 });
19+
}
20+
21+
const project = await prisma.project.findFirst({
22+
where: { id: projectId, userId: user.id },
23+
select: { id: true },
24+
});
25+
if (!project) {
26+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
27+
}
28+
29+
const [status, recentChanges] = await Promise.all([
30+
listProjectSyncStatus(projectId, user.id),
31+
listRecentSyncChanges(projectId, user.id),
32+
]);
33+
34+
return NextResponse.json({
35+
success: true,
36+
connected: status.connected,
37+
syncProject: status.syncProject
38+
? {
39+
id: status.syncProject.id,
40+
repoName: status.syncProject.repoName,
41+
framework: status.syncProject.framework,
42+
privacyMode: status.syncProject.privacyMode,
43+
lastSyncedCommit: status.syncProject.lastSyncedCommit,
44+
}
45+
: null,
46+
lastScanTime: status.lastScanTime,
47+
recentChangesCount: status.recentChangesCount,
48+
pendingSuggestionsCount: status.pendingSuggestionsCount,
49+
generatedDocumentation: status.generatedDocumentation,
50+
recentChanges: recentChanges.map((change) => ({
51+
id: change.id,
52+
fromCommit: change.fromCommit,
53+
toCommit: change.toCommit,
54+
branch: change.branch,
55+
status: change.status,
56+
changedFiles: change.changedFiles,
57+
createdAt: change.createdAt,
58+
})),
59+
});
60+
} catch (error) {
61+
console.error('Error getting project sync status:', error);
62+
return NextResponse.json(
63+
{ error: 'Failed to fetch sync status' },
64+
{ status: 500 }
65+
);
66+
}
67+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { NextResponse } from "next/server";
2+
import { prisma } from "@/lib/db";
3+
import { createSyncProject } from "@/lib/sync/sync.service";
4+
import { consumeCliAuthToken } from "@/lib/sync/cli-auth.service";
5+
6+
function toProjectLabel(projectName: string, repoName: string): string {
7+
const trimmed = projectName.trim();
8+
if (trimmed.length > 0) return trimmed;
9+
return repoName.trim() || "Untitled Project";
10+
}
11+
12+
function currentDateLabel(): string {
13+
return new Date().toLocaleDateString("en-US", {
14+
year: "numeric",
15+
month: "long",
16+
day: "numeric",
17+
});
18+
}
19+
20+
export async function POST(request: Request) {
21+
try {
22+
const body = await request.json().catch(() => ({}));
23+
const cliAuthToken = typeof body?.cliAuthToken === "string" ? body.cliAuthToken.trim() : "";
24+
const repoName = typeof body?.repoName === "string" ? body.repoName.trim() : "";
25+
const projectName = typeof body?.projectName === "string" ? body.projectName : "";
26+
const privacyMode = typeof body?.privacyMode === "string" ? body.privacyMode : "safe";
27+
28+
if (!cliAuthToken || !repoName) {
29+
return NextResponse.json({ error: "cliAuthToken and repoName are required" }, { status: 400 });
30+
}
31+
32+
const session = await consumeCliAuthToken(cliAuthToken);
33+
if (!session?.userId) {
34+
return NextResponse.json({ error: "Invalid or expired auth token" }, { status: 401 });
35+
}
36+
37+
const projectLabel = toProjectLabel(projectName, repoName);
38+
let project = await prisma.project.findFirst({
39+
where: {
40+
userId: session.userId,
41+
title: projectLabel,
42+
},
43+
select: { id: true },
44+
});
45+
46+
if (!project) {
47+
project = await prisma.project.create({
48+
data: {
49+
userId: session.userId,
50+
label: projectLabel,
51+
title: projectLabel,
52+
description: "Auto-created by Developerdoc CLI",
53+
lastUpdated: currentDateLabel(),
54+
},
55+
select: { id: true },
56+
});
57+
}
58+
59+
const { syncProject, token } = await createSyncProject({
60+
projectId: project.id,
61+
userId: session.userId,
62+
repoName,
63+
privacyMode,
64+
});
65+
66+
return NextResponse.json({
67+
success: true,
68+
projectId: project.id,
69+
syncProjectId: syncProject.id,
70+
syncToken: token,
71+
});
72+
} catch (error) {
73+
console.error("Error registering CLI project from auth token:", error);
74+
return NextResponse.json({ error: "Failed to register CLI project" }, { status: 500 });
75+
}
76+
}

app/api/cli/register/route.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getCurrentUser } from '@/lib/users';
3+
import { prisma } from '@/lib/db';
4+
import { createSyncProject } from '@/lib/sync/sync.service';
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const user = await getCurrentUser();
9+
if (!user) {
10+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
11+
}
12+
13+
const body = await request.json();
14+
const { projectId, repoName, privacyMode } = body;
15+
16+
if (!projectId || !repoName) {
17+
return NextResponse.json(
18+
{ error: 'projectId and repoName are required' },
19+
{ status: 400 }
20+
);
21+
}
22+
23+
const project = await prisma.project.findFirst({
24+
where: { id: projectId, userId: user.id },
25+
select: { id: true },
26+
});
27+
if (!project) {
28+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
29+
}
30+
31+
const { syncProject, token } = await createSyncProject({
32+
projectId,
33+
userId: user.id,
34+
repoName,
35+
privacyMode,
36+
});
37+
38+
return NextResponse.json({
39+
success: true,
40+
syncProjectId: syncProject.id,
41+
projectId: syncProject.projectId,
42+
syncToken: token,
43+
});
44+
} catch (error) {
45+
console.error('Error registering CLI sync project:', error);
46+
return NextResponse.json(
47+
{ error: 'Failed to register sync project' },
48+
{ status: 500 }
49+
);
50+
}
51+
}

0 commit comments

Comments
 (0)