@prmichaelsen/acp-visualizer 0.1.8 → 0.5.0

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.
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
12
12
  import { Route as TasksRouteImport } from './routes/tasks'
13
13
  import { Route as SearchRouteImport } from './routes/search'
14
14
  import { Route as MilestonesRouteImport } from './routes/milestones'
15
+ import { Route as ActivityRouteImport } from './routes/activity'
15
16
  import { Route as IndexRouteImport } from './routes/index'
16
17
  import { Route as ApiWatchRouteImport } from './routes/api/watch'
17
18
 
@@ -30,6 +31,11 @@ const MilestonesRoute = MilestonesRouteImport.update({
30
31
  path: '/milestones',
31
32
  getParentRoute: () => rootRouteImport,
32
33
  } as any)
34
+ const ActivityRoute = ActivityRouteImport.update({
35
+ id: '/activity',
36
+ path: '/activity',
37
+ getParentRoute: () => rootRouteImport,
38
+ } as any)
33
39
  const IndexRoute = IndexRouteImport.update({
34
40
  id: '/',
35
41
  path: '/',
@@ -43,6 +49,7 @@ const ApiWatchRoute = ApiWatchRouteImport.update({
43
49
 
44
50
  export interface FileRoutesByFullPath {
45
51
  '/': typeof IndexRoute
52
+ '/activity': typeof ActivityRoute
46
53
  '/milestones': typeof MilestonesRoute
47
54
  '/search': typeof SearchRoute
48
55
  '/tasks': typeof TasksRoute
@@ -50,6 +57,7 @@ export interface FileRoutesByFullPath {
50
57
  }
51
58
  export interface FileRoutesByTo {
52
59
  '/': typeof IndexRoute
60
+ '/activity': typeof ActivityRoute
53
61
  '/milestones': typeof MilestonesRoute
54
62
  '/search': typeof SearchRoute
55
63
  '/tasks': typeof TasksRoute
@@ -58,6 +66,7 @@ export interface FileRoutesByTo {
58
66
  export interface FileRoutesById {
59
67
  __root__: typeof rootRouteImport
60
68
  '/': typeof IndexRoute
69
+ '/activity': typeof ActivityRoute
61
70
  '/milestones': typeof MilestonesRoute
62
71
  '/search': typeof SearchRoute
63
72
  '/tasks': typeof TasksRoute
@@ -65,14 +74,28 @@ export interface FileRoutesById {
65
74
  }
66
75
  export interface FileRouteTypes {
67
76
  fileRoutesByFullPath: FileRoutesByFullPath
68
- fullPaths: '/' | '/milestones' | '/search' | '/tasks' | '/api/watch'
77
+ fullPaths:
78
+ | '/'
79
+ | '/activity'
80
+ | '/milestones'
81
+ | '/search'
82
+ | '/tasks'
83
+ | '/api/watch'
69
84
  fileRoutesByTo: FileRoutesByTo
70
- to: '/' | '/milestones' | '/search' | '/tasks' | '/api/watch'
71
- id: '__root__' | '/' | '/milestones' | '/search' | '/tasks' | '/api/watch'
85
+ to: '/' | '/activity' | '/milestones' | '/search' | '/tasks' | '/api/watch'
86
+ id:
87
+ | '__root__'
88
+ | '/'
89
+ | '/activity'
90
+ | '/milestones'
91
+ | '/search'
92
+ | '/tasks'
93
+ | '/api/watch'
72
94
  fileRoutesById: FileRoutesById
73
95
  }
74
96
  export interface RootRouteChildren {
75
97
  IndexRoute: typeof IndexRoute
98
+ ActivityRoute: typeof ActivityRoute
76
99
  MilestonesRoute: typeof MilestonesRoute
77
100
  SearchRoute: typeof SearchRoute
78
101
  TasksRoute: typeof TasksRoute
@@ -102,6 +125,13 @@ declare module '@tanstack/react-router' {
102
125
  preLoaderRoute: typeof MilestonesRouteImport
103
126
  parentRoute: typeof rootRouteImport
104
127
  }
128
+ '/activity': {
129
+ id: '/activity'
130
+ path: '/activity'
131
+ fullPath: '/activity'
132
+ preLoaderRoute: typeof ActivityRouteImport
133
+ parentRoute: typeof rootRouteImport
134
+ }
105
135
  '/': {
106
136
  id: '/'
107
137
  path: '/'
@@ -121,6 +151,7 @@ declare module '@tanstack/react-router' {
121
151
 
122
152
  const rootRouteChildren: RootRouteChildren = {
123
153
  IndexRoute: IndexRoute,
154
+ ActivityRoute: ActivityRoute,
124
155
  MilestonesRoute: MilestonesRoute,
125
156
  SearchRoute: SearchRoute,
126
157
  TasksRoute: TasksRoute,
@@ -1,26 +1,40 @@
1
- import { HeadContent, Scripts, createRootRoute, Outlet } from '@tanstack/react-router'
1
+ import { HeadContent, Scripts, createRootRoute, Outlet, useRouter } from '@tanstack/react-router'
2
+ import { useState, useCallback } from 'react'
2
3
  import { useAutoRefresh } from '../lib/useAutoRefresh'
3
4
  import { Sidebar } from '../components/Sidebar'
4
5
  import { Header } from '../components/Header'
5
6
  import { getProgressData } from '../services/progress-database.service'
7
+ import { listProjects, getProjectProgressPath } from '../services/projects.service'
8
+ import { fetchGitHubProgress } from '../services/github.service'
6
9
  import type { ProgressData } from '../lib/types'
10
+ import type { AcpProject } from '../services/projects.service'
11
+ import { ProgressProvider } from '../contexts/ProgressContext'
7
12
 
8
13
  import appCss from '../styles.css?url'
9
14
 
10
15
  export const Route = createRootRoute({
16
+ component: RootLayout,
17
+ notFoundComponent: NotFound,
18
+ shellComponent: RootDocument,
19
+
11
20
  beforeLoad: async () => {
12
21
  let progressData: ProgressData | null = null
22
+ let projects: AcpProject[] = []
13
23
 
14
24
  try {
15
- const result = await getProgressData()
25
+ const [result, projectList] = await Promise.all([
26
+ getProgressData({ data: {} }),
27
+ listProjects(),
28
+ ])
16
29
  if (result.ok) {
17
30
  progressData = result.data
18
31
  }
19
- } catch (error) {
20
- console.error('[Root] Failed to load progress data:', error)
32
+ projects = projectList
33
+ } catch {
34
+ // Cloudflare Workers or other environment without filesystem
21
35
  }
22
36
 
23
- return { progressData }
37
+ return { progressData, projects }
24
38
  },
25
39
 
26
40
  head: () => ({
@@ -40,34 +54,89 @@ export const Route = createRootRoute({
40
54
  { rel: 'stylesheet', href: appCss },
41
55
  ],
42
56
  }),
43
-
44
- shellComponent: RootDocument,
45
57
  })
46
58
 
59
+ function NotFound() {
60
+ return (
61
+ <div className="flex items-center justify-center h-full">
62
+ <div className="text-center">
63
+ <h2 className="text-xl font-semibold text-gray-200 mb-2">Page Not Found</h2>
64
+ <p className="text-sm text-gray-400">
65
+ The page you're looking for doesn't exist.
66
+ </p>
67
+ </div>
68
+ </div>
69
+ )
70
+ }
71
+
47
72
  function AutoRefresh() {
48
73
  useAutoRefresh()
49
74
  return null
50
75
  }
51
76
 
52
- function RootDocument({ children }: { children: React.ReactNode }) {
53
- const { progressData } = Route.useRouteContext()
77
+ function RootLayout() {
78
+ const context = Route.useRouteContext()
79
+ const [progressData, setProgressData] = useState(context.progressData)
80
+ const [currentProject, setCurrentProject] = useState<string | null>(
81
+ context.progressData?.project.name || null,
82
+ )
83
+
84
+ const handleGitHubLoad = useCallback(async (owner: string, repo: string) => {
85
+ const result = await fetchGitHubProgress({ data: { owner, repo } })
86
+ if (result.ok) {
87
+ setProgressData(result.data)
88
+ setCurrentProject(`${owner}/${repo}`)
89
+ } else {
90
+ throw new Error(result.message)
91
+ }
92
+ }, [])
93
+
94
+ const handleProjectSwitch = useCallback(async (projectId: string) => {
95
+ try {
96
+ const path = await getProjectProgressPath({ data: { projectId } })
97
+ if (path) {
98
+ const result = await getProgressData({ data: { path } })
99
+ if (result.ok) {
100
+ setProgressData(result.data)
101
+ setCurrentProject(projectId)
102
+ }
103
+ }
104
+ } catch {
105
+ // Project switch failed — likely no filesystem access
106
+ }
107
+ }, [])
54
108
 
55
109
  return (
56
- <html lang="en">
57
- <head>
58
- <HeadContent />
59
- </head>
60
- <body>
61
- <AutoRefresh />
62
- <div className="flex h-screen bg-gray-950 text-gray-100">
63
- <Sidebar />
110
+ <>
111
+ <AutoRefresh />
112
+ <div className="flex h-screen bg-gray-950 text-gray-100">
113
+ <Sidebar
114
+ projects={context.projects}
115
+ currentProject={currentProject}
116
+ onProjectSelect={handleProjectSwitch}
117
+ onGitHubLoad={handleGitHubLoad}
118
+ />
119
+ <ProgressProvider data={progressData}>
64
120
  <div className="flex-1 flex flex-col overflow-hidden">
65
121
  <Header data={progressData} />
66
122
  <main className="flex-1 overflow-auto">
67
- {children}
123
+ <Outlet />
68
124
  </main>
69
125
  </div>
70
- </div>
126
+ </ProgressProvider>
127
+ </div>
128
+ </>
129
+ )
130
+ }
131
+
132
+ function RootDocument({ children }: { children: React.ReactNode }) {
133
+ return (
134
+ <html lang="en">
135
+ <head>
136
+ <HeadContent />
137
+ </head>
138
+ <body>
139
+ {children}
71
140
  <Scripts />
72
141
  </body>
73
142
  </html>
@@ -0,0 +1,97 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { useProgressData } from '../contexts/ProgressContext'
3
+
4
+ export const Route = createFileRoute('/activity')({
5
+ component: ActivityPage,
6
+ })
7
+
8
+ function ActivityPage() {
9
+ const progressData = useProgressData()
10
+
11
+ if (!progressData) {
12
+ return (
13
+ <div className="p-6">
14
+ <p className="text-gray-600 text-sm">No data loaded</p>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ const entries = progressData.recent_work
20
+
21
+ return (
22
+ <div className="p-6">
23
+ <h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
24
+
25
+ {entries.length === 0 ? (
26
+ <p className="text-gray-600 text-sm">No recent work entries</p>
27
+ ) : (
28
+ <div className="relative">
29
+ {/* Timeline line */}
30
+ <div className="absolute left-[15px] top-2 bottom-2 w-px bg-gray-800" />
31
+
32
+ <div className="space-y-6">
33
+ {entries.map((entry, i) => (
34
+ <div key={i} className="relative flex gap-4">
35
+ {/* Timeline dot */}
36
+ <div className="relative z-10 mt-1">
37
+ <div className="w-[9px] h-[9px] rounded-full bg-blue-500 ring-4 ring-gray-950" />
38
+ </div>
39
+
40
+ {/* Content */}
41
+ <div className="flex-1 pb-2">
42
+ <div className="flex items-center gap-3 mb-1">
43
+ <span className="text-xs text-gray-500 font-mono">
44
+ {entry.date}
45
+ </span>
46
+ </div>
47
+ <p className="text-sm text-gray-200 mb-2">
48
+ {entry.description}
49
+ </p>
50
+ {entry.items.length > 0 && (
51
+ <ul className="space-y-1">
52
+ {entry.items.map((item, j) => (
53
+ <li
54
+ key={j}
55
+ className="text-xs text-gray-400 flex items-start gap-2"
56
+ >
57
+ <span className="text-gray-600 mt-0.5">•</span>
58
+ {item}
59
+ </li>
60
+ ))}
61
+ </ul>
62
+ )}
63
+ {Object.keys(entry.extra).length > 0 && (
64
+ <div className="mt-2 text-xs text-gray-600">
65
+ {Object.entries(entry.extra).map(([k, v]) => (
66
+ <span key={k} className="mr-3">
67
+ {k}: {String(v)}
68
+ </span>
69
+ ))}
70
+ </div>
71
+ )}
72
+ </div>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ )}
78
+
79
+ {/* Notes section */}
80
+ {progressData.notes.length > 0 && (
81
+ <div className="mt-8">
82
+ <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
83
+ Notes
84
+ </h3>
85
+ <ul className="space-y-1.5">
86
+ {progressData.notes.map((note, i) => (
87
+ <li key={i} className="text-sm text-gray-400 flex items-start gap-2">
88
+ <span className="text-gray-600 mt-0.5">•</span>
89
+ {note}
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ </div>
94
+ )}
95
+ </div>
96
+ )
97
+ }
@@ -1,13 +1,15 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
2
  import { StatusBadge } from '../components/StatusBadge'
3
3
  import { ProgressBar } from '../components/ProgressBar'
4
+ import { BurndownChart } from '../components/BurndownChart'
5
+ import { useProgressData } from '../contexts/ProgressContext'
4
6
 
5
7
  export const Route = createFileRoute('/')({
6
8
  component: HomePage,
7
9
  })
8
10
 
9
11
  function HomePage() {
10
- const { progressData } = Route.useRouteContext()
12
+ const progressData = useProgressData()
11
13
 
12
14
  if (!progressData) {
13
15
  return (
@@ -78,6 +80,9 @@ function HomePage() {
78
80
  </div>
79
81
  </div>
80
82
 
83
+ {/* Burndown Chart */}
84
+ <BurndownChart data={data} />
85
+
81
86
  {/* Next Steps */}
82
87
  {data.next_steps.length > 0 && (
83
88
  <div>
@@ -1,20 +1,27 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
- import { useState } from 'react'
2
+ import { useState, lazy, Suspense } from 'react'
3
3
  import { MilestoneTable } from '../components/MilestoneTable'
4
4
  import { MilestoneTree } from '../components/MilestoneTree'
5
- import { ViewToggle } from '../components/ViewToggle'
5
+ import { MilestoneKanban } from '../components/MilestoneKanban'
6
+ import { MilestoneGantt } from '../components/MilestoneGantt'
7
+ import { ViewToggle, type ViewMode } from '../components/ViewToggle'
6
8
  import { FilterBar } from '../components/FilterBar'
7
9
  import { SearchInput } from '../components/SearchInput'
8
10
  import { useFilteredData } from '../lib/useFilteredData'
11
+ import { useProgressData } from '../contexts/ProgressContext'
9
12
  import type { Status } from '../lib/types'
10
13
 
14
+ // Lazy-load DependencyGraph to keep dagre out of the SSR bundle
15
+ // (dagre uses CommonJS require() which fails on Cloudflare Workers)
16
+ const DependencyGraph = lazy(() => import('../components/DependencyGraph').then(m => ({ default: m.DependencyGraph })))
17
+
11
18
  export const Route = createFileRoute('/milestones')({
12
19
  component: MilestonesPage,
13
20
  })
14
21
 
15
22
  function MilestonesPage() {
16
- const { progressData } = Route.useRouteContext()
17
- const [view, setView] = useState<'table' | 'tree'>('table')
23
+ const progressData = useProgressData()
24
+ const [view, setView] = useState<ViewMode>('table')
18
25
  const [status, setStatus] = useState<Status | 'all'>('all')
19
26
  const [search, setSearch] = useState('')
20
27
 
@@ -34,16 +41,26 @@ function MilestonesPage() {
34
41
  <h2 className="text-lg font-semibold">Milestones</h2>
35
42
  <ViewToggle value={view} onChange={setView} />
36
43
  </div>
37
- <div className="flex items-center gap-3 mb-4">
38
- <FilterBar status={status} onStatusChange={setStatus} />
39
- <div className="w-64">
40
- <SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
44
+ {view !== 'kanban' && (
45
+ <div className="flex items-center gap-3 mb-4">
46
+ <FilterBar status={status} onStatusChange={setStatus} />
47
+ <div className="w-64">
48
+ <SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
49
+ </div>
41
50
  </div>
42
- </div>
51
+ )}
43
52
  {view === 'table' ? (
44
53
  <MilestoneTable milestones={filtered.milestones} tasks={filtered.tasks} />
45
- ) : (
54
+ ) : view === 'tree' ? (
46
55
  <MilestoneTree milestones={filtered.milestones} tasks={filtered.tasks} />
56
+ ) : view === 'kanban' ? (
57
+ <MilestoneKanban milestones={filtered.milestones} tasks={filtered.tasks} />
58
+ ) : view === 'gantt' ? (
59
+ <MilestoneGantt milestones={filtered.milestones} tasks={filtered.tasks} />
60
+ ) : (
61
+ <Suspense fallback={<p className="text-gray-500 text-sm">Loading graph...</p>}>
62
+ <DependencyGraph data={filtered} />
63
+ </Suspense>
47
64
  )}
48
65
  </div>
49
66
  )
@@ -4,13 +4,14 @@ import { SearchInput } from '../components/SearchInput'
4
4
  import { StatusBadge } from '../components/StatusBadge'
5
5
  import { StatusDot } from '../components/StatusDot'
6
6
  import { buildSearchIndex } from '../lib/search'
7
+ import { useProgressData } from '../contexts/ProgressContext'
7
8
 
8
9
  export const Route = createFileRoute('/search')({
9
10
  component: SearchPage,
10
11
  })
11
12
 
12
13
  function SearchPage() {
13
- const { progressData } = Route.useRouteContext()
14
+ const progressData = useProgressData()
14
15
  const [query, setQuery] = useState('')
15
16
 
16
17
  const results = useMemo(() => {
@@ -1,6 +1,7 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
2
  import { StatusDot } from '../components/StatusDot'
3
3
  import { ExtraFieldsBadge } from '../components/ExtraFieldsBadge'
4
+ import { useProgressData } from '../contexts/ProgressContext'
4
5
  import type { Task } from '../lib/types'
5
6
 
6
7
  export const Route = createFileRoute('/tasks')({
@@ -8,7 +9,7 @@ export const Route = createFileRoute('/tasks')({
8
9
  })
9
10
 
10
11
  function TasksPage() {
11
- const { progressData } = Route.useRouteContext()
12
+ const progressData = useProgressData()
12
13
 
13
14
  if (!progressData) {
14
15
  return (
package/src/server.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Cloudflare Workers Entry Point
3
+ *
4
+ * Minimal server — passes all requests to TanStack Start.
5
+ */
6
+
7
+ import startServer from '@tanstack/react-start/server-entry'
8
+
9
+ export default {
10
+ async fetch(request: Request, env: unknown, ctx: unknown) {
11
+ return (startServer as any).fetch(request, env, ctx)
12
+ },
13
+ }
@@ -0,0 +1,51 @@
1
+ import { createServerFn } from '@tanstack/react-start'
2
+ import type { ProgressData } from '../lib/types'
3
+
4
+ export type GitHubResult =
5
+ | { ok: true; data: ProgressData }
6
+ | { ok: false; error: string; message: string }
7
+
8
+ export const fetchGitHubProgress = createServerFn({ method: 'GET' })
9
+ .validator((input: { owner: string; repo: string; branch?: string; token?: string }) => input)
10
+ .handler(async ({ data: input }): Promise<GitHubResult> => {
11
+ const { parseProgressYaml } = await import('../lib/yaml-loader')
12
+
13
+ const branch = input.branch || 'main'
14
+ const url = `https://raw.githubusercontent.com/${input.owner}/${input.repo}/${branch}/agent/progress.yaml`
15
+
16
+ try {
17
+ const headers: Record<string, string> = {
18
+ 'Accept': 'text/plain',
19
+ }
20
+ if (input.token) {
21
+ headers['Authorization'] = `token ${input.token}`
22
+ }
23
+
24
+ const response = await fetch(url, { headers })
25
+
26
+ if (!response.ok) {
27
+ if (response.status === 404) {
28
+ return {
29
+ ok: false,
30
+ error: 'NOT_FOUND',
31
+ message: `No progress.yaml found at ${input.owner}/${input.repo} (branch: ${branch})`,
32
+ }
33
+ }
34
+ return {
35
+ ok: false,
36
+ error: 'FETCH_ERROR',
37
+ message: `GitHub returned ${response.status}: ${response.statusText}`,
38
+ }
39
+ }
40
+
41
+ const raw = await response.text()
42
+ const data = parseProgressYaml(raw)
43
+ return { ok: true, data }
44
+ } catch (err) {
45
+ return {
46
+ ok: false,
47
+ error: 'NETWORK_ERROR',
48
+ message: err instanceof Error ? err.message : 'Failed to fetch from GitHub',
49
+ }
50
+ }
51
+ })
@@ -3,47 +3,25 @@ import type { ProgressData } from '../lib/types'
3
3
 
4
4
  export type ProgressResult =
5
5
  | { ok: true; data: ProgressData }
6
- | { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR'; message: string; path: string }
7
-
8
- export const getProgressData = createServerFn({ method: 'GET' }).handler(
9
- async (): Promise<ProgressResult> => {
10
- // Dynamic imports keep fs and yaml-loader out of the client bundle
11
- const { readFileSync } = await import('fs')
12
- const { parseProgressYaml } = await import('../lib/yaml-loader')
13
- const { getProgressYamlPath } = await import('../lib/config')
14
-
15
- const filePath = getProgressYamlPath()
6
+ | { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR' | 'NO_FILESYSTEM'; message: string; path: string }
16
7
 
8
+ export const getProgressData = createServerFn({ method: 'GET' })
9
+ .validator((input: { path?: string }) => input)
10
+ .handler(async ({ data: input }): Promise<ProgressResult> => {
17
11
  try {
18
- const raw = readFileSync(filePath, 'utf-8')
12
+ const fs = await import('fs')
13
+ const { parseProgressYaml } = await import('../lib/yaml-loader')
14
+ const { getProgressYamlPath } = await import('../lib/config')
19
15
 
20
- try {
21
- const data = parseProgressYaml(raw)
22
- return { ok: true, data }
23
- } catch (err) {
24
- return {
25
- ok: false,
26
- error: 'PARSE_ERROR',
27
- message: err instanceof Error ? err.message : 'Failed to parse YAML',
28
- path: filePath,
29
- }
30
- }
31
- } catch (err) {
32
- const code = (err as NodeJS.ErrnoException).code
33
- if (code === 'ENOENT') {
34
- return {
35
- ok: false,
36
- error: 'FILE_NOT_FOUND',
37
- message: `progress.yaml not found at: ${filePath}`,
38
- path: filePath,
39
- }
40
- }
41
- return {
42
- ok: false,
43
- error: 'PARSE_ERROR',
44
- message: err instanceof Error ? err.message : 'Failed to read file',
45
- path: filePath,
16
+ const filePath = input.path || getProgressYamlPath()
17
+ const raw = fs.readFileSync(filePath, 'utf-8')
18
+ const data = parseProgressYaml(raw)
19
+ return { ok: true, data }
20
+ } catch (err: any) {
21
+ if (err?.code === 'ENOENT') {
22
+ return { ok: false, error: 'FILE_NOT_FOUND', message: `progress.yaml not found`, path: input.path || '' }
46
23
  }
24
+ // Cloudflare Workers: fs module exists but readFileSync throws
25
+ return { ok: false, error: 'NO_FILESYSTEM', message: 'No local filesystem — use GitHub input to load a project', path: '' }
47
26
  }
48
- },
49
- )
27
+ })
@@ -0,0 +1,69 @@
1
+ import { createServerFn } from '@tanstack/react-start'
2
+
3
+ export interface AcpProject {
4
+ id: string
5
+ path: string
6
+ type: string
7
+ description: string
8
+ status: string
9
+ hasProgress: boolean
10
+ }
11
+
12
+ export const listProjects = createServerFn({ method: 'GET' }).handler(
13
+ async (): Promise<AcpProject[]> => {
14
+ try {
15
+ const { readFileSync, existsSync } = await import('fs')
16
+ const { resolve } = await import('path')
17
+ const { homedir } = await import('os')
18
+ const yaml = await import('js-yaml')
19
+
20
+ const projectsFile = resolve(homedir(), '.acp', 'projects.yaml')
21
+ if (!existsSync(projectsFile)) return []
22
+
23
+ const raw = readFileSync(projectsFile, 'utf-8')
24
+ const doc = yaml.load(raw, { json: true }) as Record<string, unknown>
25
+ const projects = (doc?.projects || {}) as Record<string, Record<string, unknown>>
26
+
27
+ return Object.entries(projects).map(([id, p]) => {
28
+ const projectPath = String(p.path || '')
29
+ const progressPath = resolve(projectPath, 'agent', 'progress.yaml')
30
+ return {
31
+ id,
32
+ path: projectPath,
33
+ type: String(p.type || 'unknown'),
34
+ description: String(p.description || ''),
35
+ status: String(p.status || 'unknown'),
36
+ hasProgress: existsSync(progressPath),
37
+ }
38
+ })
39
+ } catch {
40
+ // Cloudflare Workers: no filesystem
41
+ return []
42
+ }
43
+ },
44
+ )
45
+
46
+ export const getProjectProgressPath = createServerFn({ method: 'GET' })
47
+ .validator((input: { projectId: string }) => input)
48
+ .handler(async ({ data }): Promise<string | null> => {
49
+ try {
50
+ const { readFileSync, existsSync } = await import('fs')
51
+ const { resolve } = await import('path')
52
+ const { homedir } = await import('os')
53
+ const yaml = await import('js-yaml')
54
+
55
+ const projectsFile = resolve(homedir(), '.acp', 'projects.yaml')
56
+ if (!existsSync(projectsFile)) return null
57
+
58
+ const raw = readFileSync(projectsFile, 'utf-8')
59
+ const doc = yaml.load(raw, { json: true }) as Record<string, unknown>
60
+ const projects = (doc?.projects || {}) as Record<string, Record<string, unknown>>
61
+ const project = projects[data.projectId]
62
+ if (!project) return null
63
+
64
+ const progressPath = resolve(String(project.path), 'agent', 'progress.yaml')
65
+ return existsSync(progressPath) ? progressPath : null
66
+ } catch {
67
+ return null
68
+ }
69
+ })