prjct-cli 0.11.0 → 0.11.1
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 +11 -1
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./src"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
First, run the development server:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
# or
|
|
10
|
+
yarn dev
|
|
11
|
+
# or
|
|
12
|
+
pnpm dev
|
|
13
|
+
# or
|
|
14
|
+
bun dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
+
|
|
19
|
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
20
|
+
|
|
21
|
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
+
|
|
23
|
+
## Learn More
|
|
24
|
+
|
|
25
|
+
To learn more about Next.js, take a look at the following resources:
|
|
26
|
+
|
|
27
|
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
+
|
|
30
|
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
+
|
|
32
|
+
## Deploy on Vercel
|
|
33
|
+
|
|
34
|
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
+
|
|
36
|
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { createClaudeSession, listSessions } from '@/lib/pty'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const sessions = listSessions()
|
|
9
|
+
return NextResponse.json({ success: true, data: sessions })
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ success: false, error: 'Failed to list sessions' },
|
|
13
|
+
{ status: 500 }
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(request: Request) {
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json()
|
|
21
|
+
const { sessionId, projectDir } = body
|
|
22
|
+
|
|
23
|
+
if (!sessionId || !projectDir) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ success: false, error: 'sessionId and projectDir are required' },
|
|
26
|
+
{ status: 400 }
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create PTY session
|
|
31
|
+
createClaudeSession(sessionId, projectDir)
|
|
32
|
+
|
|
33
|
+
return NextResponse.json({
|
|
34
|
+
success: true,
|
|
35
|
+
data: { sessionId, projectDir }
|
|
36
|
+
})
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to create session:', error)
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ success: false, error: 'Failed to create session' },
|
|
41
|
+
{ status: 500 }
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { exec } from 'child_process'
|
|
3
|
+
import { promisify } from 'util'
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec)
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
try {
|
|
11
|
+
// Check if claude is available
|
|
12
|
+
const { stdout } = await execAsync('which claude && claude --version 2>/dev/null || echo "not found"')
|
|
13
|
+
const lines = stdout.trim().split('\n')
|
|
14
|
+
|
|
15
|
+
const available = !stdout.includes('not found') && lines.length > 0
|
|
16
|
+
const version = available ? lines[lines.length - 1] : null
|
|
17
|
+
|
|
18
|
+
return NextResponse.json({
|
|
19
|
+
success: true,
|
|
20
|
+
data: {
|
|
21
|
+
available,
|
|
22
|
+
version
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return NextResponse.json({
|
|
27
|
+
success: true,
|
|
28
|
+
data: {
|
|
29
|
+
available: false,
|
|
30
|
+
version: null
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { moveToTrash } from '@/lib/projects'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function POST(
|
|
7
|
+
request: Request,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
const result = await moveToTrash(id)
|
|
13
|
+
return NextResponse.json({ success: true, ...result })
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const message = error instanceof Error ? error.message : 'Failed to delete project'
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ success: false, error: message },
|
|
18
|
+
{ status: 500 }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { promises as fs } from 'fs'
|
|
3
|
+
import { getProjects } from '@/lib/projects'
|
|
4
|
+
import { lookup } from 'mime-types'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
request: Request,
|
|
10
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const { id } = await params
|
|
14
|
+
const projects = await getProjects()
|
|
15
|
+
const project = projects.find(p => p.id === id)
|
|
16
|
+
|
|
17
|
+
if (!project?.iconPath) {
|
|
18
|
+
return new NextResponse(null, { status: 404 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const file = await fs.readFile(project.iconPath)
|
|
22
|
+
const mimeType = lookup(project.iconPath) || 'application/octet-stream'
|
|
23
|
+
|
|
24
|
+
return new NextResponse(file, {
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': mimeType,
|
|
27
|
+
'Cache-Control': 'public, max-age=86400'
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
} catch {
|
|
31
|
+
return new NextResponse(null, { status: 404 })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getProject } from '@/lib/projects'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
request: Request,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const project = await getProject(id)
|
|
14
|
+
|
|
15
|
+
if (!project) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ success: false, error: 'Project not found' },
|
|
18
|
+
{ status: 404 }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return NextResponse.json({ success: true, data: project })
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ success: false, error: 'Failed to get project' },
|
|
26
|
+
{ status: 500 }
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { getProjectStats, getRawProjectFiles } from '@/lib/parse-prjct-files'
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
const { id: projectId } = await params
|
|
9
|
+
|
|
10
|
+
if (!projectId) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ success: false, error: 'Project ID required' },
|
|
13
|
+
{ status: 400 }
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Get both parsed stats AND raw files
|
|
19
|
+
const [stats, raw] = await Promise.all([
|
|
20
|
+
getProjectStats(projectId),
|
|
21
|
+
getRawProjectFiles(projectId)
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
success: true,
|
|
26
|
+
data: stats,
|
|
27
|
+
raw // Raw markdown files for direct rendering
|
|
28
|
+
})
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('[API] Error getting project stats:', error)
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ success: false, error: 'Failed to get project stats' },
|
|
33
|
+
{ status: 500 }
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getProjectStatus } from '@/lib/projects'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
request: Request,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const status = await getProjectStatus(id)
|
|
14
|
+
return NextResponse.json({ success: true, data: status })
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ success: false, error: 'Failed to get status' },
|
|
18
|
+
{ status: 500 }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getProjects } from '@/lib/projects'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const projects = await getProjects()
|
|
9
|
+
return NextResponse.json({ success: true, data: projects })
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ success: false, error: 'Failed to list projects' },
|
|
13
|
+
{ status: 500 }
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { promises as fs } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
interface SessionEvent {
|
|
9
|
+
ts?: string
|
|
10
|
+
timestamp?: string
|
|
11
|
+
type?: string
|
|
12
|
+
action?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DailyStats {
|
|
16
|
+
date: string
|
|
17
|
+
tasks: number
|
|
18
|
+
ships: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function GET() {
|
|
22
|
+
try {
|
|
23
|
+
const globalStorage = join(homedir(), '.prjct-cli', 'projects')
|
|
24
|
+
|
|
25
|
+
let projects: string[]
|
|
26
|
+
try {
|
|
27
|
+
projects = await fs.readdir(globalStorage)
|
|
28
|
+
} catch {
|
|
29
|
+
return NextResponse.json({
|
|
30
|
+
success: true,
|
|
31
|
+
data: {
|
|
32
|
+
chartData: [],
|
|
33
|
+
totals: { tasks: 0, ships: 0 },
|
|
34
|
+
dateRange: { start: '', end: '' }
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Aggregate by date
|
|
40
|
+
const dailyMap = new Map<string, { tasks: number; ships: number }>()
|
|
41
|
+
|
|
42
|
+
// Calculate date range (last 90 days)
|
|
43
|
+
const endDate = new Date()
|
|
44
|
+
const startDate = new Date()
|
|
45
|
+
startDate.setDate(startDate.getDate() - 90)
|
|
46
|
+
|
|
47
|
+
for (const projectId of projects) {
|
|
48
|
+
const contextPath = join(globalStorage, projectId, 'memory', 'context.jsonl')
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(contextPath, 'utf-8')
|
|
52
|
+
const lines = content.trim().split('\n').filter(Boolean)
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
try {
|
|
56
|
+
const event: SessionEvent = JSON.parse(line)
|
|
57
|
+
const timestamp = event.ts || event.timestamp
|
|
58
|
+
if (!timestamp) continue
|
|
59
|
+
|
|
60
|
+
const eventDate = new Date(timestamp)
|
|
61
|
+
if (isNaN(eventDate.getTime())) continue
|
|
62
|
+
if (eventDate < startDate || eventDate > endDate) continue
|
|
63
|
+
|
|
64
|
+
const dateKey = eventDate.toISOString().split('T')[0]
|
|
65
|
+
const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
|
|
66
|
+
|
|
67
|
+
const eventType = event.type || event.action
|
|
68
|
+
|
|
69
|
+
// Count tasks completed
|
|
70
|
+
if (eventType === 'task_complete' || eventType === 'task_completed') {
|
|
71
|
+
current.tasks++
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Count features shipped
|
|
75
|
+
if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
76
|
+
current.ships++
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
dailyMap.set(dateKey, current)
|
|
80
|
+
} catch {
|
|
81
|
+
// Skip malformed lines
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Skip projects without context.jsonl
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Convert to sorted array
|
|
90
|
+
const data: DailyStats[] = Array.from(dailyMap.entries())
|
|
91
|
+
.map(([date, stats]) => ({
|
|
92
|
+
date,
|
|
93
|
+
tasks: stats.tasks,
|
|
94
|
+
ships: stats.ships
|
|
95
|
+
}))
|
|
96
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
97
|
+
|
|
98
|
+
// Calculate totals for summary
|
|
99
|
+
const totals = {
|
|
100
|
+
tasks: data.reduce((sum, d) => sum + d.tasks, 0),
|
|
101
|
+
ships: data.reduce((sum, d) => sum + d.ships, 0)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({
|
|
105
|
+
success: true,
|
|
106
|
+
data: {
|
|
107
|
+
chartData: data,
|
|
108
|
+
totals,
|
|
109
|
+
dateRange: {
|
|
110
|
+
start: startDate.toISOString().split('T')[0],
|
|
111
|
+
end: endDate.toISOString().split('T')[0]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Sessions history error:', error)
|
|
117
|
+
return NextResponse.json(
|
|
118
|
+
{ success: false, error: 'Failed to fetch session history' },
|
|
119
|
+
{ status: 500 }
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getProjects } from '@/lib/projects'
|
|
3
|
+
import { exec } from 'child_process'
|
|
4
|
+
import { promisify } from 'util'
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec)
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic'
|
|
9
|
+
|
|
10
|
+
async function getGitUserName(): Promise<string> {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execAsync('git config user.name')
|
|
13
|
+
return stdout.trim() || 'Developer'
|
|
14
|
+
} catch {
|
|
15
|
+
return 'Developer'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function GET() {
|
|
20
|
+
try {
|
|
21
|
+
const [projects, userName] = await Promise.all([
|
|
22
|
+
getProjects(),
|
|
23
|
+
getGitUserName()
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const stats = {
|
|
27
|
+
userName,
|
|
28
|
+
totalProjects: projects.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({ success: true, data: stats })
|
|
32
|
+
} catch {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ success: false, error: 'Failed to get stats' },
|
|
35
|
+
{ status: 500 }
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { AlertTriangle } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
export default function Error({
|
|
8
|
+
error,
|
|
9
|
+
reset,
|
|
10
|
+
}: {
|
|
11
|
+
error: Error & { digest?: string }
|
|
12
|
+
reset: () => void
|
|
13
|
+
}) {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
console.error(error)
|
|
16
|
+
}, [error])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center justify-center h-full">
|
|
20
|
+
<div className="text-center space-y-4">
|
|
21
|
+
<div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center mx-auto">
|
|
22
|
+
<AlertTriangle className="w-8 h-8 text-destructive" />
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<h2 className="text-lg font-medium">Something went wrong</h2>
|
|
26
|
+
<p className="text-sm text-muted-foreground mt-1">{error.message}</p>
|
|
27
|
+
</div>
|
|
28
|
+
<Button onClick={reset} variant="outline">
|
|
29
|
+
Try again
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "@xterm/xterm/css/xterm.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--color-background: var(--background);
|
|
9
|
+
--color-foreground: var(--foreground);
|
|
10
|
+
--font-sans: var(--font-geist-sans);
|
|
11
|
+
--font-mono: var(--font-geist-mono);
|
|
12
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
13
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
14
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
15
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
16
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
17
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
18
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
19
|
+
--color-sidebar: var(--sidebar);
|
|
20
|
+
--color-chart-5: var(--chart-5);
|
|
21
|
+
--color-chart-4: var(--chart-4);
|
|
22
|
+
--color-chart-3: var(--chart-3);
|
|
23
|
+
--color-chart-2: var(--chart-2);
|
|
24
|
+
--color-chart-1: var(--chart-1);
|
|
25
|
+
--color-ring: var(--ring);
|
|
26
|
+
--color-input: var(--input);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-destructive: var(--destructive);
|
|
29
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
30
|
+
--color-accent: var(--accent);
|
|
31
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
32
|
+
--color-muted: var(--muted);
|
|
33
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
34
|
+
--color-secondary: var(--secondary);
|
|
35
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
36
|
+
--color-primary: var(--primary);
|
|
37
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
38
|
+
--color-popover: var(--popover);
|
|
39
|
+
--color-card-foreground: var(--card-foreground);
|
|
40
|
+
--color-card: var(--card);
|
|
41
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
42
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
43
|
+
--radius-lg: var(--radius);
|
|
44
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:root {
|
|
48
|
+
--radius: 0.625rem;
|
|
49
|
+
--background: oklch(1 0 0);
|
|
50
|
+
--foreground: oklch(0.145 0 0);
|
|
51
|
+
--card: oklch(1 0 0);
|
|
52
|
+
--card-foreground: oklch(0.145 0 0);
|
|
53
|
+
--popover: oklch(1 0 0);
|
|
54
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
55
|
+
--primary: oklch(0.205 0 0);
|
|
56
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
57
|
+
--secondary: oklch(0.97 0 0);
|
|
58
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
59
|
+
--muted: oklch(0.97 0 0);
|
|
60
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
61
|
+
--accent: oklch(0.97 0 0);
|
|
62
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
63
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
64
|
+
--border: oklch(0.922 0 0);
|
|
65
|
+
--input: oklch(0.922 0 0);
|
|
66
|
+
--ring: oklch(0.708 0 0);
|
|
67
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
68
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
69
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
70
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
71
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
72
|
+
--sidebar: oklch(0.985 0 0);
|
|
73
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
74
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
75
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
76
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
77
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
78
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
79
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dark {
|
|
83
|
+
--background: oklch(0.145 0 0);
|
|
84
|
+
--foreground: oklch(0.985 0 0);
|
|
85
|
+
--card: oklch(0.205 0 0);
|
|
86
|
+
--card-foreground: oklch(0.985 0 0);
|
|
87
|
+
--popover: oklch(0.205 0 0);
|
|
88
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
89
|
+
--primary: oklch(0.922 0 0);
|
|
90
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
91
|
+
--secondary: oklch(0.269 0 0);
|
|
92
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
93
|
+
--muted: oklch(0.269 0 0);
|
|
94
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
95
|
+
--accent: oklch(0.269 0 0);
|
|
96
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
97
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
98
|
+
--border: oklch(1 0 0 / 10%);
|
|
99
|
+
--input: oklch(1 0 0 / 15%);
|
|
100
|
+
--ring: oklch(0.556 0 0);
|
|
101
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
102
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
103
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
104
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
105
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
106
|
+
--sidebar: oklch(0.205 0 0);
|
|
107
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
108
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
109
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
110
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
111
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
113
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@layer base {
|
|
117
|
+
* {
|
|
118
|
+
@apply border-border outline-ring/50;
|
|
119
|
+
}
|
|
120
|
+
body {
|
|
121
|
+
@apply bg-background text-foreground;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Terminal styles */
|
|
126
|
+
.xterm {
|
|
127
|
+
padding: 8px 4px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.xterm-viewport {
|
|
131
|
+
overflow-y: auto !important;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Logo - Fancy border animation */
|
|
135
|
+
@keyframes borderAnimation {
|
|
136
|
+
0% { background-position: 0% 50%; }
|
|
137
|
+
50% { background-position: 100% 50%; }
|
|
138
|
+
100% { background-position: 0% 50%; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.fancy-border {
|
|
142
|
+
position: absolute;
|
|
143
|
+
inset: -3px;
|
|
144
|
+
border-radius: 12px;
|
|
145
|
+
background: linear-gradient(45deg, #ff3d00, #00c6ff, #7a00ff, #09ff00, #ff3d00);
|
|
146
|
+
background-size: 400% 400%;
|
|
147
|
+
z-index: 0;
|
|
148
|
+
animation: borderAnimation 8s ease infinite;
|
|
149
|
+
filter: blur(4px);
|
|
150
|
+
opacity: 0.8;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.fancy-border.rounded-full {
|
|
154
|
+
border-radius: 9999px;
|
|
155
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Geist, Geist_Mono } from 'next/font/google'
|
|
3
|
+
import './globals.css'
|
|
4
|
+
import { Providers } from '@/components/providers'
|
|
5
|
+
import { AppSidebar } from '@/components/AppSidebar'
|
|
6
|
+
|
|
7
|
+
const geistSans = Geist({
|
|
8
|
+
variable: '--font-geist-sans',
|
|
9
|
+
subsets: ['latin'],
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const geistMono = Geist_Mono({
|
|
13
|
+
variable: '--font-geist-mono',
|
|
14
|
+
subsets: ['latin'],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const metadata: Metadata = {
|
|
18
|
+
title: 'prjct - Developer Momentum',
|
|
19
|
+
description: 'Ship fast, track progress, stay focused.',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function RootLayout({
|
|
23
|
+
children,
|
|
24
|
+
}: Readonly<{
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}>) {
|
|
27
|
+
return (
|
|
28
|
+
<html lang="en" suppressHydrationWarning>
|
|
29
|
+
<body
|
|
30
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
31
|
+
>
|
|
32
|
+
<Providers>
|
|
33
|
+
<div className="flex h-screen bg-background">
|
|
34
|
+
<AppSidebar />
|
|
35
|
+
<main className="flex-1 overflow-hidden">
|
|
36
|
+
{children}
|
|
37
|
+
</main>
|
|
38
|
+
</div>
|
|
39
|
+
</Providers>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
)
|
|
43
|
+
}
|