@prmichaelsen/acp-visualizer 0.5.2 → 0.5.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -15,6 +15,11 @@ export async function getFileWatcher() {
15
15
  const clients = new Set<Controller>()
16
16
 
17
17
  try {
18
+ // existsSync will throw on Workers where fs is stubbed
19
+ const { existsSync } = await import('fs')
20
+ if (!existsSync(filePath)) {
21
+ throw new Error(`File not found: ${filePath}`)
22
+ }
18
23
  watch(filePath, (eventType) => {
19
24
  if (eventType === 'change') {
20
25
  for (const controller of clients) {
@@ -27,7 +32,7 @@ export async function getFileWatcher() {
27
32
  }
28
33
  })
29
34
  } catch (err) {
30
- console.warn('[FileWatcher] Could not watch file:', err)
35
+ throw new Error(`[FileWatcher] Cannot watch: ${err}`)
31
36
  }
32
37
 
33
38
  watcher = {
@@ -5,6 +5,9 @@ export function useAutoRefresh() {
5
5
  const router = useRouter()
6
6
 
7
7
  useEffect(() => {
8
+ // Skip file-watcher SSE in hosted mode — no local filesystem to watch
9
+ if (import.meta.env.VITE_HOSTED) return
10
+
8
11
  const eventSource = new EventSource('/api/watch')
9
12
 
10
13
  eventSource.onmessage = () => {
@@ -1,28 +1,34 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
- import { getFileWatcher } from '../../lib/file-watcher'
3
2
 
4
3
  export const Route = createFileRoute('/api/watch')({
5
4
  server: {
6
5
  handlers: {
7
6
  GET: async () => {
8
- const watcher = await getFileWatcher()
7
+ // In hosted mode (Cloudflare Workers), there's no filesystem to watch.
8
+ // Return 204 immediately instead of opening a stream that hangs forever.
9
+ try {
10
+ const { getFileWatcher } = await import('../../lib/file-watcher')
11
+ const watcher = await getFileWatcher()
9
12
 
10
- const stream = new ReadableStream({
11
- start(controller) {
12
- watcher.addClient(controller)
13
- },
14
- cancel(controller) {
15
- watcher.removeClient(controller)
16
- },
17
- })
13
+ const stream = new ReadableStream({
14
+ start(controller) {
15
+ watcher.addClient(controller)
16
+ },
17
+ cancel(controller) {
18
+ watcher.removeClient(controller)
19
+ },
20
+ })
18
21
 
19
- return new Response(stream, {
20
- headers: {
21
- 'Content-Type': 'text/event-stream',
22
- 'Cache-Control': 'no-cache',
23
- 'Connection': 'keep-alive',
24
- },
25
- })
22
+ return new Response(stream, {
23
+ headers: {
24
+ 'Content-Type': 'text/event-stream',
25
+ 'Cache-Control': 'no-cache',
26
+ 'Connection': 'keep-alive',
27
+ },
28
+ })
29
+ } catch {
30
+ return new Response(null, { status: 204 })
31
+ }
26
32
  },
27
33
  },
28
34
  },
@@ -5,47 +5,71 @@ export type GitHubResult =
5
5
  | { ok: true; data: ProgressData }
6
6
  | { ok: false; error: string; message: string }
7
7
 
8
+ async function fetchBranch(
9
+ parseProgressYaml: (raw: string) => ProgressData,
10
+ owner: string,
11
+ repo: string,
12
+ branch: string,
13
+ headers: Record<string, string>,
14
+ ): Promise<GitHubResult> {
15
+ const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/agent/progress.yaml`
16
+ try {
17
+ const response = await fetch(url, { headers })
18
+ if (!response.ok) {
19
+ if (response.status === 404) {
20
+ return { ok: false, error: 'NOT_FOUND', message: `No progress.yaml found at ${owner}/${repo} (branch: ${branch})` }
21
+ }
22
+ return { ok: false, error: 'FETCH_ERROR', message: `GitHub returned ${response.status}: ${response.statusText}` }
23
+ }
24
+ const raw = await response.text()
25
+ const data = parseProgressYaml(raw)
26
+ return { ok: true, data }
27
+ } catch (err) {
28
+ return { ok: false, error: 'NETWORK_ERROR', message: err instanceof Error ? err.message : 'Failed to fetch from GitHub' }
29
+ }
30
+ }
31
+
8
32
  export const fetchGitHubProgress = createServerFn({ method: 'GET' })
9
33
  .inputValidator((input: { owner: string; repo: string; branch?: string; token?: string }) => input)
10
34
  .handler(async ({ data: input }): Promise<GitHubResult> => {
11
35
  const { parseProgressYaml } = await import('../lib/yaml-loader')
12
36
 
13
- const branch = input.branch || 'main'
14
- const url = `https://raw.githubusercontent.com/${input.owner}/${input.repo}/${branch}/agent/progress.yaml`
37
+ const fetchHeaders: Record<string, string> = { 'Accept': 'text/plain' }
38
+ if (input.token) {
39
+ fetchHeaders['Authorization'] = `token ${input.token}`
40
+ }
41
+
42
+ // If branch specified, use it directly
43
+ if (input.branch) {
44
+ return fetchBranch(parseProgressYaml, input.owner, input.repo, input.branch, fetchHeaders)
45
+ }
15
46
 
47
+ // Try GitHub API for default branch, fall back to trying main → mainline → master
48
+ let detectedBranch: string | null = null
16
49
  try {
17
- const headers: Record<string, string> = {
18
- 'Accept': 'text/plain',
19
- }
20
- if (input.token) {
21
- headers['Authorization'] = `token ${input.token}`
50
+ const metaRes = await fetch(`https://api.github.com/repos/${input.owner}/${input.repo}`, {
51
+ headers: { 'User-Agent': 'acp-visualizer', ...(input.token ? { Authorization: `token ${input.token}` } : {}) },
52
+ })
53
+ if (metaRes.ok) {
54
+ const meta = await metaRes.json() as { default_branch?: string }
55
+ detectedBranch = meta.default_branch || null
22
56
  }
57
+ } catch {
58
+ // Rate limited or network error — fall through to branch probing
59
+ }
23
60
 
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
- }
61
+ const branches = detectedBranch
62
+ ? [detectedBranch]
63
+ : ['main', 'mainline', 'master']
40
64
 
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
- }
65
+ for (const branch of branches) {
66
+ const result = await fetchBranch(parseProgressYaml, input.owner, input.repo, branch, fetchHeaders)
67
+ if (result.ok || result.error !== 'NOT_FOUND') return result
68
+ }
69
+
70
+ return {
71
+ ok: false,
72
+ error: 'NOT_FOUND',
73
+ message: `No progress.yaml found at ${input.owner}/${input.repo} (tried branches: ${branches.join(', ')})`,
50
74
  }
51
75
  })
package/vite.config.ts CHANGED
@@ -7,10 +7,10 @@ import { cloudflare } from '@cloudflare/vite-plugin'
7
7
 
8
8
  export default defineConfig(({ command }) => ({
9
9
  plugins: [
10
- cloudflare({
11
- viteEnvironment: { name: 'ssr' },
12
- ...(command === 'serve' ? { config: { observability: { enabled: false } } } : {}),
13
- }),
10
+ // Only use Cloudflare plugin for production builds — local dev uses Node.js SSR
11
+ ...(command === 'build'
12
+ ? [cloudflare({ viteEnvironment: { name: 'ssr' } })]
13
+ : []),
14
14
  viteTsConfigPaths({
15
15
  projects: ['./tsconfig.json'],
16
16
  }),