create-claude-pipeline 0.2.0 → 0.3.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.
Files changed (23) hide show
  1. package/bin/cli.js +13 -6
  2. package/package.json +1 -1
  3. package/template/.claude-pipeline/dashboard/package.json +1 -3
  4. package/template/.claude-pipeline/dashboard/server.ts +4 -3
  5. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/events/route.ts +85 -0
  6. package/template/.claude-pipeline/dashboard/src/app/api/pipelines/stream/route.ts +72 -0
  7. package/template/.claude-pipeline/dashboard/src/hooks/use-pipeline-detail.ts +31 -45
  8. package/template/.claude-pipeline/dashboard/src/hooks/use-pipelines.ts +25 -38
  9. package/template/.claude-pipeline/dashboard/src/hooks/use-sse.ts +61 -0
  10. package/template/.claude-pipeline/dashboard/src/lib/pipelines.ts +2 -4
  11. package/template/.claude-pipeline/dashboard/src/lib/sse.ts +55 -0
  12. package/template/.claude-pipeline/dashboard/src/types/pipeline.ts +1 -7
  13. package/template/.claude-pipeline/runner/dist/context-watcher.d.ts +10 -6
  14. package/template/.claude-pipeline/runner/dist/context-watcher.js +53 -40
  15. package/template/.claude-pipeline/runner/dist/context-watcher.js.map +1 -1
  16. package/template/.claude-pipeline/runner/dist/pipeline-runner.js +8 -0
  17. package/template/.claude-pipeline/runner/dist/pipeline-runner.js.map +1 -1
  18. package/template/.claude-pipeline/runner/src/context-watcher.ts +58 -40
  19. package/template/.claude-pipeline/runner/src/pipeline-runner.ts +8 -0
  20. package/template/CLAUDE.md +22 -2
  21. package/template/.claude-pipeline/dashboard/src/hooks/use-websocket.ts +0 -58
  22. package/template/.claude-pipeline/dashboard/src/lib/watcher.ts +0 -90
  23. package/template/.claude-pipeline/dashboard/src/lib/ws-server.ts +0 -123
package/bin/cli.js CHANGED
@@ -275,7 +275,7 @@ async function startDashboard(targetDir) {
275
275
  var TOTAL_STEPS = 6;
276
276
  function showHelp() {
277
277
  console.log(`
278
- Usage: npx create-claude-pipeline
278
+ Usage: npx create-claude-pipeline [options]
279
279
 
280
280
  Claude Code \uD30C\uC774\uD504\uB77C\uC778 \uC2DC\uC2A4\uD15C\uC744 \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC124\uCE58\uD558\uACE0 \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
281
281
 
@@ -283,8 +283,9 @@ function showHelp() {
283
283
  \uC7AC\uC2E4\uD589: \uB300\uC2DC\uBCF4\uB4DC\uB9CC \uC2E4\uD589
284
284
 
285
285
  Options:
286
- --help \uC774 \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
287
- --version \uBC84\uC804 \uD45C\uC2DC
286
+ --update, -u \uD30C\uC774\uD504\uB77C\uC778 \uC2DC\uC2A4\uD15C\uC744 \uCD5C\uC2E0 \uBC84\uC804\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8
287
+ --help \uC774 \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC
288
+ --version \uBC84\uC804 \uD45C\uC2DC
288
289
  `);
289
290
  }
290
291
  async function showVersion() {
@@ -308,11 +309,17 @@ async function main() {
308
309
  }
309
310
  const cwd = process.cwd();
310
311
  const dashboardPkg = import_path7.default.join(cwd, ".claude-pipeline", "dashboard", "package.json");
312
+ const forceUpdate = args.includes("--update") || args.includes("-u");
311
313
  if (await import_fs_extra5.default.pathExists(dashboardPkg)) {
314
+ if (!forceUpdate) {
315
+ console.log();
316
+ success("\uC774\uBBF8 \uC124\uCE58\uB428 \u2014 \uB300\uC2DC\uBCF4\uB4DC\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4");
317
+ success("\uC5C5\uB370\uC774\uD2B8\uD558\uB824\uBA74: npx create-claude-pipeline --update");
318
+ await startDashboard(cwd);
319
+ return;
320
+ }
312
321
  console.log();
313
- success("\uC774\uBBF8 \uC124\uCE58\uB428 \u2014 \uB300\uC2DC\uBCF4\uB4DC\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4");
314
- await startDashboard(cwd);
315
- return;
322
+ success("\uC5C5\uB370\uC774\uD2B8 \uBAA8\uB4DC \u2014 \uD30C\uC77C\uC744 \uB36E\uC5B4\uC4F0\uACE0 \uC7AC\uC124\uCE58\uD569\uB2C8\uB2E4");
316
323
  }
317
324
  banner();
318
325
  step(1, TOTAL_STEPS, "\uD30C\uC77C \uBCF5\uC0AC \uC911...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-pipeline",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Claude Code 파이프라인 시스템을 프로젝트에 설치하고 대시보드를 실행합니다",
5
5
  "bin": {
6
6
  "create-claude-pipeline": "./bin/cli.js"
@@ -16,8 +16,7 @@
16
16
  "react-markdown": "^10.1.0",
17
17
  "react-syntax-highlighter": "^16.1.1",
18
18
  "remark-gfm": "^4.0.1",
19
- "uuid": "^13.0.0",
20
- "ws": "^8.20.0"
19
+ "uuid": "^13.0.0"
21
20
  },
22
21
  "devDependencies": {
23
22
  "@types/node": "^20",
@@ -25,7 +24,6 @@
25
24
  "@types/react-dom": "^18",
26
25
  "@types/react-syntax-highlighter": "^15.5.13",
27
26
  "@types/uuid": "^10.0.0",
28
- "@types/ws": "^8.18.1",
29
27
  "eslint": "^8",
30
28
  "eslint-config-next": "14.2.35",
31
29
  "postcss": "^8",
@@ -1,7 +1,6 @@
1
1
  import { createServer } from "http";
2
2
  import { parse } from "url";
3
3
  import next from "next";
4
- import { createWSServer } from "./src/lib/ws-server";
5
4
 
6
5
  const dev = process.env.NODE_ENV !== "production";
7
6
  const hostname = "localhost";
@@ -16,9 +15,11 @@ app.prepare().then(() => {
16
15
  handle(req, res, parsedUrl);
17
16
  });
18
17
 
19
- createWSServer(server);
20
-
21
18
  server.listen(port, () => {
22
19
  console.log(`> Ready on http://${hostname}:${port}`);
20
+ console.log(`> PIPELINES_DIR: ${process.env.PIPELINES_DIR || "(default: ../pipelines)"}`);
23
21
  });
22
+ }).catch((err) => {
23
+ console.error("Failed to start dashboard:", err);
24
+ process.exit(1);
24
25
  });
@@ -0,0 +1,85 @@
1
+ import { createSSEResponse } from "@/lib/sse";
2
+ import { readPipelineState, getPipelineDir } from "@/lib/pipelines";
3
+ import { detectCheckpoint } from "@/lib/checkpoint";
4
+ import fs from "fs";
5
+ import path from "path";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function GET(
10
+ _request: Request,
11
+ { params }: { params: { id: string } },
12
+ ) {
13
+ const { id } = params;
14
+ const pipelineDir = getPipelineDir(id);
15
+
16
+ if (!fs.existsSync(pipelineDir)) {
17
+ return new Response(JSON.stringify({ error: "Pipeline not found" }), {
18
+ status: 404,
19
+ });
20
+ }
21
+
22
+ return createSSEResponse((writer, signal) => {
23
+ let prevActivitiesCount = 0;
24
+ let lastMtime = 0;
25
+
26
+ // Send initial state
27
+ const initialState = readPipelineState(id);
28
+ if (initialState) {
29
+ writer.write("pipeline:updated", { id, state: initialState });
30
+ prevActivitiesCount = initialState.activities.length;
31
+
32
+ const checkpoint = detectCheckpoint(initialState.activities);
33
+ if (checkpoint) {
34
+ writer.write("pipeline:checkpoint", { id, checkpoint });
35
+ }
36
+ }
37
+
38
+ // Poll state.json for changes
39
+ const stateFile = path.join(pipelineDir, "state.json");
40
+
41
+ const interval = setInterval(() => {
42
+ if (signal.aborted) {
43
+ clearInterval(interval);
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const stat = fs.statSync(stateFile);
49
+ if (stat.mtimeMs === lastMtime) return;
50
+ lastMtime = stat.mtimeMs;
51
+
52
+ const state = readPipelineState(id);
53
+ if (!state) return;
54
+
55
+ // Send full state update
56
+ writer.write("pipeline:updated", { id, state });
57
+
58
+ // Send new activities individually
59
+ const newActivities = state.activities.slice(prevActivitiesCount);
60
+ for (const activity of newActivities) {
61
+ writer.write("pipeline:activity", { id, activity });
62
+ }
63
+ prevActivitiesCount = state.activities.length;
64
+
65
+ // Check for checkpoint
66
+ const checkpoint = detectCheckpoint(state.activities);
67
+ if (checkpoint) {
68
+ writer.write("pipeline:checkpoint", { id, checkpoint });
69
+ }
70
+ } catch {
71
+ // state.json may be mid-write or pipeline removed
72
+ if (!fs.existsSync(pipelineDir)) {
73
+ writer.write("pipeline:removed", { id });
74
+ clearInterval(interval);
75
+ writer.close();
76
+ }
77
+ }
78
+ }, 500);
79
+
80
+ signal.addEventListener("abort", () => {
81
+ clearInterval(interval);
82
+ writer.close();
83
+ });
84
+ });
85
+ }
@@ -0,0 +1,72 @@
1
+ import { createSSEResponse } from "@/lib/sse";
2
+ import { listPipelines, readPipelineState, getPipelinesDir } from "@/lib/pipelines";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export async function GET() {
9
+ return createSSEResponse((writer, signal) => {
10
+ // Send initial pipeline list
11
+ const pipelines = listPipelines();
12
+ writer.write("pipeline:init", { pipelines });
13
+
14
+ const pipelinesDir = getPipelinesDir();
15
+ if (!fs.existsSync(pipelinesDir)) {
16
+ fs.mkdirSync(pipelinesDir, { recursive: true });
17
+ }
18
+
19
+ // Track known pipeline state file mtimes for change detection
20
+ const lastMtimes = new Map<string, number>();
21
+
22
+ // Poll for changes (simpler and more reliable than fs.watch across platforms)
23
+ const interval = setInterval(() => {
24
+ if (signal.aborted) {
25
+ clearInterval(interval);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const entries = fs.readdirSync(pipelinesDir, { withFileTypes: true });
31
+ const currentIds = new Set<string>();
32
+
33
+ for (const entry of entries) {
34
+ if (!entry.isDirectory()) continue;
35
+ currentIds.add(entry.name);
36
+
37
+ const stateFile = path.join(pipelinesDir, entry.name, "state.json");
38
+ try {
39
+ const stat = fs.statSync(stateFile);
40
+ const mtime = stat.mtimeMs;
41
+ const prev = lastMtimes.get(entry.name);
42
+
43
+ if (prev === undefined || prev !== mtime) {
44
+ lastMtimes.set(entry.name, mtime);
45
+ const state = readPipelineState(entry.name);
46
+ if (state) {
47
+ writer.write("pipeline:updated", { id: entry.name, state });
48
+ }
49
+ }
50
+ } catch {
51
+ // state.json doesn't exist yet
52
+ }
53
+ }
54
+
55
+ // Detect removed pipelines
56
+ for (const id of lastMtimes.keys()) {
57
+ if (!currentIds.has(id)) {
58
+ lastMtimes.delete(id);
59
+ writer.write("pipeline:removed", { id });
60
+ }
61
+ }
62
+ } catch {
63
+ // pipelines dir may not exist yet
64
+ }
65
+ }, 1000);
66
+
67
+ signal.addEventListener("abort", () => {
68
+ clearInterval(interval);
69
+ writer.close();
70
+ });
71
+ });
72
+ }
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback } from "react";
4
- import type { PipelineState, CheckpointInfo, ServerMessage } from "@/types/pipeline";
5
- import { useWebSocket } from "./use-websocket";
3
+ import { useState, useCallback } from "react";
4
+ import type { PipelineState, CheckpointInfo } from "@/types/pipeline";
5
+ import { useSSE } from "./use-sse";
6
6
 
7
7
  export function usePipelineDetail(id: string) {
8
8
  const [pipeline, setPipeline] = useState<PipelineState | null>(null);
@@ -10,56 +10,42 @@ export function usePipelineDetail(id: string) {
10
10
  const [loading, setLoading] = useState(true);
11
11
  const [notFound, setNotFound] = useState(false);
12
12
 
13
- const fetchPipeline = useCallback(async () => {
14
- try {
15
- const res = await fetch(`/api/pipelines/${id}`);
16
- if (res.status === 404) {
17
- setNotFound(true);
18
- return;
19
- }
20
- const data = await res.json();
21
- setPipeline(data);
22
- } catch {
23
- // Ignore
24
- } finally {
25
- setLoading(false);
26
- }
27
- }, [id]);
13
+ const handleEvent = useCallback((event: string, data: unknown) => {
14
+ const d = data as Record<string, unknown>;
28
15
 
29
- const handleMessage = useCallback((msg: ServerMessage) => {
30
- if (msg.type === "pipeline:updated" && msg.id === id) {
31
- setPipeline(msg.state);
32
- } else if (msg.type === "pipeline:activity" && msg.id === id) {
16
+ if (event === "pipeline:updated" && d.id === id) {
17
+ setPipeline(d.state as PipelineState);
18
+ setLoading(false);
19
+ } else if (event === "pipeline:activity" && d.id === id) {
33
20
  setPipeline((prev) => {
34
21
  if (!prev) return prev;
35
- return { ...prev, activities: [...prev.activities, msg.activity] };
22
+ const activity = d.activity as PipelineState["activities"][0];
23
+ return { ...prev, activities: [...prev.activities, activity] };
36
24
  });
37
- } else if (msg.type === "pipeline:checkpoint" && msg.id === id) {
38
- setCheckpoint(msg.checkpoint);
39
- } else if (msg.type === "pipeline:removed" && msg.id === id) {
25
+ } else if (event === "pipeline:checkpoint" && d.id === id) {
26
+ setCheckpoint(d.checkpoint as CheckpointInfo);
27
+ } else if (event === "pipeline:removed" && d.id === id) {
40
28
  setNotFound(true);
41
29
  }
42
30
  }, [id]);
43
31
 
44
- const { send, connected } = useWebSocket(handleMessage, fetchPipeline);
45
-
46
- useEffect(() => {
47
- fetchPipeline();
48
- }, [fetchPipeline]);
49
-
50
- useEffect(() => {
51
- if (connected) {
52
- send({ type: "subscribe", pipelineId: id });
53
- return () => {
54
- send({ type: "unsubscribe", pipelineId: id });
55
- };
56
- }
57
- }, [connected, id, send]);
58
-
59
- const respondToCheckpoint = useCallback((action: "approve" | "reject", message?: string) => {
60
- send({ type: "checkpoint:respond", pipelineId: id, action, message });
61
- setCheckpoint(null);
62
- }, [id, send]);
32
+ useSSE(`/api/pipelines/${id}/events`, handleEvent);
33
+
34
+ const respondToCheckpoint = useCallback(
35
+ async (action: "approve" | "reject", message?: string) => {
36
+ try {
37
+ await fetch(`/api/pipelines/${id}/checkpoint`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({ action, message }),
41
+ });
42
+ setCheckpoint(null);
43
+ } catch {
44
+ // Ignore fetch errors
45
+ }
46
+ },
47
+ [id],
48
+ );
63
49
 
64
50
  return { pipeline, checkpoint, loading, notFound, respondToCheckpoint };
65
51
  }
@@ -1,37 +1,32 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback } from "react";
4
- import type { PipelineSummary, ServerMessage } from "@/types/pipeline";
5
- import { useWebSocket } from "./use-websocket";
3
+ import { useState, useCallback } from "react";
4
+ import type { PipelineSummary, PipelineState } from "@/types/pipeline";
5
+ import { useSSE } from "./use-sse";
6
6
 
7
7
  export function usePipelines() {
8
8
  const [pipelines, setPipelines] = useState<PipelineSummary[]>([]);
9
9
  const [loading, setLoading] = useState(true);
10
10
 
11
- const fetchPipelines = useCallback(async () => {
12
- try {
13
- const res = await fetch("/api/pipelines");
14
- const data = await res.json();
15
- setPipelines(data.pipelines);
16
- } catch {
17
- // Ignore fetch errors
18
- } finally {
19
- setLoading(false);
20
- }
21
- }, []);
11
+ const handleEvent = useCallback((event: string, data: unknown) => {
12
+ const d = data as Record<string, unknown>;
22
13
 
23
- const handleMessage = useCallback((msg: ServerMessage) => {
24
- if (msg.type === "pipeline:updated") {
14
+ if (event === "pipeline:init") {
15
+ const list = d.pipelines as PipelineSummary[];
16
+ setPipelines(list);
17
+ setLoading(false);
18
+ } else if (event === "pipeline:updated") {
19
+ const state = d.state as PipelineState;
20
+ const summary: PipelineSummary = {
21
+ id: state.id,
22
+ requirements: state.requirements,
23
+ status: state.status,
24
+ currentPhase: state.currentPhase,
25
+ createdAt: state.createdAt,
26
+ agents: state.agents,
27
+ };
25
28
  setPipelines((prev) => {
26
- const idx = prev.findIndex((p) => p.id === msg.id);
27
- const summary: PipelineSummary = {
28
- id: msg.state.id,
29
- requirements: msg.state.requirements,
30
- status: msg.state.status,
31
- currentPhase: msg.state.currentPhase,
32
- createdAt: msg.state.createdAt,
33
- agents: msg.state.agents,
34
- };
29
+ const idx = prev.findIndex((p) => p.id === state.id);
35
30
  if (idx >= 0) {
36
31
  const next = [...prev];
37
32
  next[idx] = summary;
@@ -39,22 +34,14 @@ export function usePipelines() {
39
34
  }
40
35
  return [summary, ...prev];
41
36
  });
42
- } else if (msg.type === "pipeline:removed") {
43
- setPipelines((prev) => prev.filter((p) => p.id !== msg.id));
37
+ setLoading(false);
38
+ } else if (event === "pipeline:removed") {
39
+ const id = d.id as string;
40
+ setPipelines((prev) => prev.filter((p) => p.id !== id));
44
41
  }
45
42
  }, []);
46
43
 
47
- const { send, connected } = useWebSocket(handleMessage, fetchPipelines);
48
-
49
- useEffect(() => {
50
- fetchPipelines();
51
- }, [fetchPipelines]);
52
-
53
- useEffect(() => {
54
- if (connected) {
55
- send({ type: "subscribe:all" });
56
- }
57
- }, [connected, send]);
44
+ useSSE("/api/pipelines/stream", handleEvent);
58
45
 
59
46
  return { pipelines, loading };
60
47
  }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useCallback, useState } from "react";
4
+
5
+ export type SSEHandler = (event: string, data: unknown) => void;
6
+
7
+ /**
8
+ * React hook for Server-Sent Events.
9
+ * Browser-native EventSource with automatic reconnection.
10
+ */
11
+ export function useSSE(url: string | null, onEvent: SSEHandler) {
12
+ const [connected, setConnected] = useState(false);
13
+ const eventSourceRef = useRef<EventSource | null>(null);
14
+ const onEventRef = useRef(onEvent);
15
+ onEventRef.current = onEvent;
16
+
17
+ useEffect(() => {
18
+ if (!url) return;
19
+
20
+ const es = new EventSource(url);
21
+ eventSourceRef.current = es;
22
+
23
+ es.onopen = () => {
24
+ setConnected(true);
25
+ };
26
+
27
+ es.onerror = () => {
28
+ setConnected(false);
29
+ // EventSource auto-reconnects natively
30
+ };
31
+
32
+ // Named event listener helper
33
+ const events = [
34
+ "pipeline:updated",
35
+ "pipeline:activity",
36
+ "pipeline:checkpoint",
37
+ "pipeline:removed",
38
+ "pipeline:init",
39
+ "error",
40
+ ];
41
+
42
+ for (const eventName of events) {
43
+ es.addEventListener(eventName, (e: MessageEvent) => {
44
+ try {
45
+ const data = JSON.parse(e.data);
46
+ onEventRef.current(eventName, data);
47
+ } catch {
48
+ // Ignore parse errors
49
+ }
50
+ });
51
+ }
52
+
53
+ return () => {
54
+ es.close();
55
+ eventSourceRef.current = null;
56
+ setConnected(false);
57
+ };
58
+ }, [url]);
59
+
60
+ return { connected };
61
+ }
@@ -2,10 +2,8 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import type { PipelineState, PipelineSummary } from "@/types/pipeline";
4
4
 
5
- const PIPELINES_DIR = process.env.PIPELINES_DIR;
6
- if (!PIPELINES_DIR) {
7
- throw new Error("PIPELINES_DIR 환경변수가 설정되지 않았습니다. npx create-claude-pipeline으로 실행해주세요.");
8
- }
5
+ const PIPELINES_DIR = process.env.PIPELINES_DIR
6
+ || path.resolve(process.cwd(), "..", "pipelines");
9
7
 
10
8
  export function getPipelinesDir(): string {
11
9
  return path.resolve(PIPELINES_DIR);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SSE (Server-Sent Events) utility for Next.js API routes.
3
+ * Replaces WebSocket for server→client push.
4
+ */
5
+
6
+ export interface SSEWriter {
7
+ write(event: string, data: unknown): void;
8
+ close(): void;
9
+ }
10
+
11
+ export function createSSEResponse(
12
+ handler: (writer: SSEWriter, signal: AbortSignal) => void | Promise<void>,
13
+ ): Response {
14
+ const encoder = new TextEncoder();
15
+ let controllerRef: ReadableStreamDefaultController | null = null;
16
+ const abortController = new AbortController();
17
+
18
+ const stream = new ReadableStream({
19
+ start(controller) {
20
+ controllerRef = controller;
21
+
22
+ const writer: SSEWriter = {
23
+ write(event: string, data: unknown) {
24
+ try {
25
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
26
+ controller.enqueue(encoder.encode(payload));
27
+ } catch {
28
+ // Stream may be closed
29
+ }
30
+ },
31
+ close() {
32
+ try {
33
+ controller.close();
34
+ } catch {
35
+ // Already closed
36
+ }
37
+ },
38
+ };
39
+
40
+ handler(writer, abortController.signal);
41
+ },
42
+ cancel() {
43
+ abortController.abort();
44
+ },
45
+ });
46
+
47
+ return new Response(stream, {
48
+ headers: {
49
+ "Content-Type": "text/event-stream",
50
+ "Cache-Control": "no-cache, no-transform",
51
+ Connection: "keep-alive",
52
+ "X-Accel-Buffering": "no",
53
+ },
54
+ });
55
+ }
@@ -46,16 +46,10 @@ export interface PipelineSummary {
46
46
  agents: Record<string, AgentState>;
47
47
  }
48
48
 
49
- // WebSocket messages
49
+ // SSE event data types (server → client only)
50
50
  export type ServerMessage =
51
51
  | { type: "pipeline:updated"; id: string; state: PipelineState }
52
52
  | { type: "pipeline:activity"; id: string; activity: Activity }
53
53
  | { type: "pipeline:checkpoint"; id: string; checkpoint: CheckpointInfo }
54
54
  | { type: "pipeline:removed"; id: string }
55
55
  | { type: "error"; message: string };
56
-
57
- export type ClientMessage =
58
- | { type: "subscribe:all" }
59
- | { type: "subscribe"; pipelineId: string }
60
- | { type: "unsubscribe"; pipelineId: string }
61
- | { type: "checkpoint:respond"; pipelineId: string; action: "approve" | "reject"; message?: string };
@@ -1,19 +1,23 @@
1
1
  import type { StateManager } from "./state-manager.js";
2
2
  /**
3
- * Fallback watcher: monitors the context/ directory for file creation.
4
- * Only updates state when SignalWatcher hasn't reported recently.
3
+ * Watches for context file creation in TWO locations:
4
+ * 1. pipelines/{id}/context/ primary (dashboard-aware Claude)
5
+ * 2. project-root/context/ — fallback (Claude ignoring dashboard instructions)
6
+ *
7
+ * When a file is found at project root, it's copied into the pipeline dir.
5
8
  */
6
9
  export declare class ContextWatcher {
7
10
  private stateManager;
8
- private contextDir;
9
- private watcher;
11
+ private pipelineContextDir;
12
+ private rootContextDir;
10
13
  private seenFiles;
11
14
  private lastSignalTime;
15
+ private intervals;
12
16
  constructor(stateManager: StateManager, pipelinesDir: string, pipelineId: string);
13
- /** Call this whenever SignalWatcher processes a signal */
14
17
  notifySignalProcessed(): void;
15
18
  start(): void;
16
19
  stop(): void;
17
- private handleFile;
20
+ private scanDir;
18
21
  private pollDirectory;
22
+ private handleFile;
19
23
  }
@@ -18,79 +18,92 @@ const CONTEXT_FILE_PHASES = {
18
18
  "security_report.md": 4,
19
19
  };
20
20
  /**
21
- * Fallback watcher: monitors the context/ directory for file creation.
22
- * Only updates state when SignalWatcher hasn't reported recently.
21
+ * Watches for context file creation in TWO locations:
22
+ * 1. pipelines/{id}/context/ primary (dashboard-aware Claude)
23
+ * 2. project-root/context/ — fallback (Claude ignoring dashboard instructions)
24
+ *
25
+ * When a file is found at project root, it's copied into the pipeline dir.
23
26
  */
24
27
  export class ContextWatcher {
25
28
  stateManager;
26
- contextDir;
27
- watcher = null;
29
+ pipelineContextDir;
30
+ rootContextDir;
28
31
  seenFiles = new Set();
29
32
  lastSignalTime = 0;
33
+ intervals = [];
30
34
  constructor(stateManager, pipelinesDir, pipelineId) {
31
35
  this.stateManager = stateManager;
32
- this.contextDir = path.join(pipelinesDir, pipelineId, "context");
36
+ this.pipelineContextDir = path.join(pipelinesDir, pipelineId, "context");
37
+ // Project root is one level up from pipelines dir
38
+ this.rootContextDir = path.join(pipelinesDir, "..", "context");
33
39
  }
34
- /** Call this whenever SignalWatcher processes a signal */
35
40
  notifySignalProcessed() {
36
41
  this.lastSignalTime = Date.now();
37
42
  }
38
43
  start() {
39
- fs.mkdirSync(this.contextDir, { recursive: true });
40
- // Scan existing files first
44
+ fs.mkdirSync(this.pipelineContextDir, { recursive: true });
45
+ // Scan existing files
46
+ this.scanDir(this.pipelineContextDir);
47
+ this.scanDir(this.rootContextDir);
48
+ // Poll both directories
49
+ this.intervals.push(setInterval(() => this.pollDirectory(this.pipelineContextDir, false), 2000), setInterval(() => this.pollDirectory(this.rootContextDir, true), 2000));
50
+ }
51
+ stop() {
52
+ for (const interval of this.intervals) {
53
+ clearInterval(interval);
54
+ }
55
+ this.intervals = [];
56
+ }
57
+ scanDir(dir) {
41
58
  try {
42
- const existing = fs.readdirSync(this.contextDir);
43
- for (const file of existing) {
59
+ const files = fs.readdirSync(dir);
60
+ for (const file of files) {
44
61
  this.seenFiles.add(file);
45
62
  }
46
63
  }
47
64
  catch {
48
- // directory might not exist yet
65
+ // directory may not exist
49
66
  }
67
+ }
68
+ pollDirectory(dir, isRootFallback) {
50
69
  try {
51
- this.watcher = fs.watch(this.contextDir, (eventType, filename) => {
52
- if (eventType !== "rename" || !filename)
53
- return;
54
- this.handleFile(filename);
55
- });
70
+ if (!fs.existsSync(dir))
71
+ return;
72
+ const files = fs.readdirSync(dir);
73
+ for (const file of files) {
74
+ this.handleFile(file, dir, isRootFallback);
75
+ }
56
76
  }
57
77
  catch {
58
- // fs.watch may fail on some systems, fall back to polling
59
- setInterval(() => this.pollDirectory(), 3000);
60
- }
61
- }
62
- stop() {
63
- if (this.watcher) {
64
- this.watcher.close();
65
- this.watcher = null;
78
+ // directory may not exist yet
66
79
  }
67
80
  }
68
- handleFile(filename) {
81
+ handleFile(filename, sourceDir, isRootFallback) {
69
82
  if (this.seenFiles.has(filename))
70
83
  return;
71
- // Check if file actually exists (rename events fire on delete too)
72
- const filePath = path.join(this.contextDir, filename);
84
+ const filePath = path.join(sourceDir, filename);
73
85
  if (!fs.existsSync(filePath))
74
86
  return;
75
87
  this.seenFiles.add(filename);
76
- // Skip if SignalWatcher was active in the last 5 seconds
88
+ // If found at project root, copy to pipeline context dir
89
+ if (isRootFallback) {
90
+ const destPath = path.join(this.pipelineContextDir, filename);
91
+ if (!fs.existsSync(destPath)) {
92
+ try {
93
+ fs.copyFileSync(filePath, destPath);
94
+ }
95
+ catch {
96
+ // copy may fail if file is being written
97
+ }
98
+ }
99
+ }
100
+ // Skip state update if SignalWatcher was active recently
77
101
  if (Date.now() - this.lastSignalTime < 5000)
78
102
  return;
79
103
  const phase = CONTEXT_FILE_PHASES[filename];
80
104
  if (phase !== undefined) {
81
105
  this.stateManager.addOutput(`context/${filename}`, phase);
82
- this.stateManager.addActivity("system", "info", `산출물 감지: context/${filename} (Phase ${phase})`);
83
- }
84
- }
85
- pollDirectory() {
86
- try {
87
- const files = fs.readdirSync(this.contextDir);
88
- for (const file of files) {
89
- this.handleFile(file);
90
- }
91
- }
92
- catch {
93
- // directory may not exist yet
106
+ this.stateManager.addActivity("system", "info", `산출물 감지: context/${filename} (Phase ${phase})${isRootFallback ? " [루트에서 복사됨]" : ""}`);
94
107
  }
95
108
  }
96
109
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context-watcher.js","sourceRoot":"","sources":["../src/context-watcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-watcher.js","sourceRoot":"","sources":["../src/context-watcher.ts"],"names":[],"mappings":"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"}
@@ -39,6 +39,14 @@ function buildPrompt(requirements, pipelineId) {
39
39
  "",
40
40
  "중요: 이 세션은 파이프라인 대시보드에서 실행됩니다.",
41
41
  'CLAUDE.md의 "Pipeline Dashboard Integration" 섹션을 반드시 따르세요.',
42
+ "",
43
+ "특히 context 파일 경로에 주의하세요:",
44
+ `- 모든 산출물(context 파일)은 pipelines/${pipelineId}/context/ 에 생성`,
45
+ `- 시그널 파일은 pipelines/${pipelineId}/signals/ 에 생성`,
46
+ `- 예: pipelines/${pipelineId}/context/00_requirements.md`,
47
+ `- 예: pipelines/${pipelineId}/context/01_plan.md`,
48
+ "",
49
+ "절대 프로젝트 루트의 context/ 폴더에 파일을 만들지 마세요.",
42
50
  ].join("\n");
43
51
  }
44
52
  // ── Main ────────────────────────────────────────────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline-runner.js","sourceRoot":"","sources":["../src/pipeline-runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pipeline-runner.js","sourceRoot":"","sources":["../src/pipeline-runner.ts"],"names":[],"mappings":"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"}
@@ -21,69 +21,98 @@ const CONTEXT_FILE_PHASES: Record<string, number> = {
21
21
  };
22
22
 
23
23
  /**
24
- * Fallback watcher: monitors the context/ directory for file creation.
25
- * Only updates state when SignalWatcher hasn't reported recently.
24
+ * Watches for context file creation in TWO locations:
25
+ * 1. pipelines/{id}/context/ primary (dashboard-aware Claude)
26
+ * 2. project-root/context/ — fallback (Claude ignoring dashboard instructions)
27
+ *
28
+ * When a file is found at project root, it's copied into the pipeline dir.
26
29
  */
27
30
  export class ContextWatcher {
28
- private contextDir: string;
29
- private watcher: fs.FSWatcher | null = null;
31
+ private pipelineContextDir: string;
32
+ private rootContextDir: string;
30
33
  private seenFiles = new Set<string>();
31
34
  private lastSignalTime = 0;
35
+ private intervals: ReturnType<typeof setInterval>[] = [];
32
36
 
33
37
  constructor(
34
38
  private stateManager: StateManager,
35
39
  pipelinesDir: string,
36
40
  pipelineId: string,
37
41
  ) {
38
- this.contextDir = path.join(pipelinesDir, pipelineId, "context");
42
+ this.pipelineContextDir = path.join(pipelinesDir, pipelineId, "context");
43
+ // Project root is one level up from pipelines dir
44
+ this.rootContextDir = path.join(pipelinesDir, "..", "context");
39
45
  }
40
46
 
41
- /** Call this whenever SignalWatcher processes a signal */
42
47
  notifySignalProcessed(): void {
43
48
  this.lastSignalTime = Date.now();
44
49
  }
45
50
 
46
51
  start(): void {
47
- fs.mkdirSync(this.contextDir, { recursive: true });
52
+ fs.mkdirSync(this.pipelineContextDir, { recursive: true });
48
53
 
49
- // Scan existing files first
54
+ // Scan existing files
55
+ this.scanDir(this.pipelineContextDir);
56
+ this.scanDir(this.rootContextDir);
57
+
58
+ // Poll both directories
59
+ this.intervals.push(
60
+ setInterval(() => this.pollDirectory(this.pipelineContextDir, false), 2000),
61
+ setInterval(() => this.pollDirectory(this.rootContextDir, true), 2000),
62
+ );
63
+ }
64
+
65
+ stop(): void {
66
+ for (const interval of this.intervals) {
67
+ clearInterval(interval);
68
+ }
69
+ this.intervals = [];
70
+ }
71
+
72
+ private scanDir(dir: string): void {
50
73
  try {
51
- const existing = fs.readdirSync(this.contextDir);
52
- for (const file of existing) {
74
+ const files = fs.readdirSync(dir);
75
+ for (const file of files) {
53
76
  this.seenFiles.add(file);
54
77
  }
55
78
  } catch {
56
- // directory might not exist yet
79
+ // directory may not exist
57
80
  }
81
+ }
58
82
 
83
+ private pollDirectory(dir: string, isRootFallback: boolean): void {
59
84
  try {
60
- this.watcher = fs.watch(this.contextDir, (eventType, filename) => {
61
- if (eventType !== "rename" || !filename) return;
62
- this.handleFile(filename);
63
- });
85
+ if (!fs.existsSync(dir)) return;
86
+ const files = fs.readdirSync(dir);
87
+ for (const file of files) {
88
+ this.handleFile(file, dir, isRootFallback);
89
+ }
64
90
  } catch {
65
- // fs.watch may fail on some systems, fall back to polling
66
- setInterval(() => this.pollDirectory(), 3000);
67
- }
68
- }
69
-
70
- stop(): void {
71
- if (this.watcher) {
72
- this.watcher.close();
73
- this.watcher = null;
91
+ // directory may not exist yet
74
92
  }
75
93
  }
76
94
 
77
- private handleFile(filename: string): void {
95
+ private handleFile(filename: string, sourceDir: string, isRootFallback: boolean): void {
78
96
  if (this.seenFiles.has(filename)) return;
79
97
 
80
- // Check if file actually exists (rename events fire on delete too)
81
- const filePath = path.join(this.contextDir, filename);
98
+ const filePath = path.join(sourceDir, filename);
82
99
  if (!fs.existsSync(filePath)) return;
83
100
 
84
101
  this.seenFiles.add(filename);
85
102
 
86
- // Skip if SignalWatcher was active in the last 5 seconds
103
+ // If found at project root, copy to pipeline context dir
104
+ if (isRootFallback) {
105
+ const destPath = path.join(this.pipelineContextDir, filename);
106
+ if (!fs.existsSync(destPath)) {
107
+ try {
108
+ fs.copyFileSync(filePath, destPath);
109
+ } catch {
110
+ // copy may fail if file is being written
111
+ }
112
+ }
113
+ }
114
+
115
+ // Skip state update if SignalWatcher was active recently
87
116
  if (Date.now() - this.lastSignalTime < 5000) return;
88
117
 
89
118
  const phase = CONTEXT_FILE_PHASES[filename];
@@ -92,19 +121,8 @@ export class ContextWatcher {
92
121
  this.stateManager.addActivity(
93
122
  "system",
94
123
  "info",
95
- `산출물 감지: context/${filename} (Phase ${phase})`,
124
+ `산출물 감지: context/${filename} (Phase ${phase})${isRootFallback ? " [루트에서 복사됨]" : ""}`,
96
125
  );
97
126
  }
98
127
  }
99
-
100
- private pollDirectory(): void {
101
- try {
102
- const files = fs.readdirSync(this.contextDir);
103
- for (const file of files) {
104
- this.handleFile(file);
105
- }
106
- } catch {
107
- // directory may not exist yet
108
- }
109
- }
110
128
  }
@@ -45,6 +45,14 @@ function buildPrompt(requirements: string, pipelineId: string): string {
45
45
  "",
46
46
  "중요: 이 세션은 파이프라인 대시보드에서 실행됩니다.",
47
47
  'CLAUDE.md의 "Pipeline Dashboard Integration" 섹션을 반드시 따르세요.',
48
+ "",
49
+ "특히 context 파일 경로에 주의하세요:",
50
+ `- 모든 산출물(context 파일)은 pipelines/${pipelineId}/context/ 에 생성`,
51
+ `- 시그널 파일은 pipelines/${pipelineId}/signals/ 에 생성`,
52
+ `- 예: pipelines/${pipelineId}/context/00_requirements.md`,
53
+ `- 예: pipelines/${pipelineId}/context/01_plan.md`,
54
+ "",
55
+ "절대 프로젝트 루트의 context/ 폴더에 파일을 만들지 마세요.",
48
56
  ].join("\n");
49
57
  }
50
58
 
@@ -305,8 +305,28 @@ context/
305
305
  ## Pipeline Dashboard Integration
306
306
 
307
307
  환경변수 `PIPELINE_ID`가 설정되어 있으면 **대시보드에서 실행 중**이다.
308
- 아래 시그널 프로토콜을 반드시 따른다. 시그널 파일은 단순 텍스트 파일이며,
309
- 대시보드의 Runner가 이 파일들을 감지하여 실시간 상태를 업데이트한다.
308
+ 아래 규칙을 반드시 따른다.
309
+
310
+ ### context 파일 경로 (중요!)
311
+
312
+ 대시보드 실행 시, **모든 context 파일은 파이프라인 전용 디렉토리에 생성한다:**
313
+ ```
314
+ pipelines/$PIPELINE_ID/context/
315
+ ```
316
+
317
+ 즉, 일반 실행에서 `context/00_requirements.md`에 저장하던 파일을
318
+ 대시보드 실행 시에는 `pipelines/$PIPELINE_ID/context/00_requirements.md`에 저장한다.
319
+
320
+ **모든 Phase의 산출물이 이 경로를 따라야 한다:**
321
+ - `pipelines/$PIPELINE_ID/context/00_requirements.md` (Phase 0)
322
+ - `pipelines/$PIPELINE_ID/context/01_plan.md` (Phase 1)
323
+ - `pipelines/$PIPELINE_ID/context/02_design_spec.md` (Phase 2)
324
+ - `pipelines/$PIPELINE_ID/context/03_api_spec.md` (Phase 2)
325
+ - 등등...
326
+
327
+ ### 시그널 프로토콜
328
+
329
+ 시그널 파일은 단순 텍스트 파일이며, 대시보드의 Runner가 감지하여 실시간 상태를 업데이트한다.
310
330
 
311
331
  ### 시그널 디렉토리
312
332
 
@@ -1,58 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useRef, useCallback, useState } from "react";
4
- import type { ServerMessage, ClientMessage } from "@/types/pipeline";
5
-
6
- export function useWebSocket(onMessage: (msg: ServerMessage) => void, onReconnect?: () => void) {
7
- const wsRef = useRef<WebSocket | null>(null);
8
- const reconnectDelay = useRef(1000);
9
- const [connected, setConnected] = useState(false);
10
-
11
- const connect = useCallback(() => {
12
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
13
- const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
14
-
15
- ws.onopen = () => {
16
- const wasDisconnected = !connected;
17
- setConnected(true);
18
- reconnectDelay.current = 1000;
19
- // Signal reconnect so data hooks can re-fetch
20
- if (wasDisconnected && onReconnect) onReconnect();
21
- };
22
-
23
- ws.onmessage = (event) => {
24
- try {
25
- const msg = JSON.parse(event.data) as ServerMessage;
26
- onMessage(msg);
27
- } catch {
28
- // Ignore parse errors
29
- }
30
- };
31
-
32
- ws.onclose = () => {
33
- setConnected(false);
34
- wsRef.current = null;
35
- // Exponential backoff reconnection
36
- const delay = reconnectDelay.current;
37
- reconnectDelay.current = Math.min(delay * 2, 30000);
38
- setTimeout(connect, delay);
39
- };
40
-
41
- wsRef.current = ws;
42
- }, [onMessage]);
43
-
44
- useEffect(() => {
45
- connect();
46
- return () => {
47
- wsRef.current?.close();
48
- };
49
- }, [connect]);
50
-
51
- const send = useCallback((msg: ClientMessage) => {
52
- if (wsRef.current?.readyState === WebSocket.OPEN) {
53
- wsRef.current.send(JSON.stringify(msg));
54
- }
55
- }, []);
56
-
57
- return { send, connected };
58
- }
@@ -1,90 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { getPipelinesDir, readPipelineState } from "./pipelines";
4
- import type { PipelineState } from "@/types/pipeline";
5
-
6
- type WatchCallback = (id: string, state: PipelineState | null) => void;
7
-
8
- export class PipelineWatcher {
9
- private watchers = new Map<string, fs.FSWatcher>();
10
- private debounceTimers = new Map<string, NodeJS.Timeout>();
11
- private dirWatcher: fs.FSWatcher | null = null;
12
- private callback: WatchCallback;
13
-
14
- constructor(callback: WatchCallback) {
15
- this.callback = callback;
16
- }
17
-
18
- start() {
19
- const dir = getPipelinesDir();
20
- if (!fs.existsSync(dir)) {
21
- fs.mkdirSync(dir, { recursive: true });
22
- }
23
-
24
- this.dirWatcher = fs.watch(dir, (_, filename) => {
25
- if (!filename) return;
26
- const fullPath = path.join(dir, filename);
27
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
28
- this.watchPipeline(filename);
29
- } else {
30
- this.unwatchPipeline(filename);
31
- }
32
- });
33
-
34
- const entries = fs.readdirSync(dir, { withFileTypes: true });
35
- for (const entry of entries) {
36
- if (entry.isDirectory()) {
37
- this.watchPipeline(entry.name);
38
- }
39
- }
40
- }
41
-
42
- private watchPipeline(id: string) {
43
- if (this.watchers.has(id)) return;
44
-
45
- const stateFile = path.join(getPipelinesDir(), id, "state.json");
46
- if (!fs.existsSync(stateFile)) return;
47
-
48
- try {
49
- const watcher = fs.watch(stateFile, () => {
50
- const existing = this.debounceTimers.get(id);
51
- if (existing) clearTimeout(existing);
52
-
53
- this.debounceTimers.set(id, setTimeout(() => {
54
- this.debounceTimers.delete(id);
55
- const state = readPipelineState(id);
56
- this.callback(id, state);
57
- }, 100));
58
- });
59
-
60
- watcher.on("error", () => {
61
- this.unwatchPipeline(id);
62
- this.callback(id, null);
63
- });
64
-
65
- this.watchers.set(id, watcher);
66
- } catch {
67
- // Ignore watch errors
68
- }
69
- }
70
-
71
- private unwatchPipeline(id: string) {
72
- const watcher = this.watchers.get(id);
73
- if (watcher) {
74
- watcher.close();
75
- this.watchers.delete(id);
76
- }
77
- const timer = this.debounceTimers.get(id);
78
- if (timer) {
79
- clearTimeout(timer);
80
- this.debounceTimers.delete(id);
81
- }
82
- }
83
-
84
- stop() {
85
- this.dirWatcher?.close();
86
- this.watchers.forEach((_, id) => {
87
- this.unwatchPipeline(id);
88
- });
89
- }
90
- }
@@ -1,123 +0,0 @@
1
- import { WebSocketServer, WebSocket } from "ws";
2
- import type { Server } from "http";
3
- import type { PipelineState, ServerMessage, ClientMessage } from "@/types/pipeline";
4
- import { PipelineWatcher } from "./watcher";
5
- import { readPipelineState, writeCheckpointResponse, getPipelineDir } from "./pipelines";
6
- import { detectCheckpoint } from "./checkpoint";
7
- import fs from "fs";
8
-
9
- interface ClientState {
10
- ws: WebSocket;
11
- mode: "none" | "all" | "single";
12
- pipelineId?: string;
13
- }
14
-
15
- export function createWSServer(server: Server) {
16
- const wss = new WebSocketServer({ server, path: "/ws" });
17
- const clients = new Map<WebSocket, ClientState>();
18
- const prevActivitiesCount = new Map<string, number>();
19
-
20
- const watcher = new PipelineWatcher((id, state) => {
21
- if (state === null) {
22
- broadcast({ type: "pipeline:removed", id }, (c) => c.mode !== "none");
23
- prevActivitiesCount.delete(id);
24
- return;
25
- }
26
-
27
- broadcastToAll({ type: "pipeline:updated", id, state });
28
-
29
- const prevCount = prevActivitiesCount.get(id) || 0;
30
- const newActivities = state.activities.slice(prevCount);
31
- prevActivitiesCount.set(id, state.activities.length);
32
-
33
- for (const activity of newActivities) {
34
- broadcastToSingle(id, { type: "pipeline:activity", id, activity });
35
- }
36
-
37
- const checkpoint = detectCheckpoint(state.activities);
38
- if (checkpoint) {
39
- broadcastToSingle(id, { type: "pipeline:checkpoint", id, checkpoint });
40
- }
41
-
42
- broadcastToSingle(id, { type: "pipeline:updated", id, state });
43
- });
44
-
45
- watcher.start();
46
-
47
- function broadcast(msg: ServerMessage, filter: (c: ClientState) => boolean) {
48
- const data = JSON.stringify(msg);
49
- clients.forEach((client) => {
50
- if (client.ws.readyState === WebSocket.OPEN && filter(client)) {
51
- client.ws.send(data);
52
- }
53
- });
54
- }
55
-
56
- function broadcastToAll(msg: ServerMessage) {
57
- broadcast(msg, (c) => c.mode === "all");
58
- }
59
-
60
- function broadcastToSingle(pipelineId: string, msg: ServerMessage) {
61
- broadcast(msg, (c) => c.mode === "single" && c.pipelineId === pipelineId);
62
- }
63
-
64
- wss.on("connection", (ws) => {
65
- const clientState: ClientState = { ws, mode: "none" };
66
- clients.set(ws, clientState);
67
-
68
- ws.on("message", (raw) => {
69
- try {
70
- const msg = JSON.parse(raw.toString()) as ClientMessage;
71
- handleMessage(clientState, msg);
72
- } catch {
73
- ws.send(JSON.stringify({ type: "error", message: "Invalid message" }));
74
- }
75
- });
76
-
77
- ws.on("close", () => {
78
- clients.delete(ws);
79
- });
80
- });
81
-
82
- function handleMessage(client: ClientState, msg: ClientMessage) {
83
- switch (msg.type) {
84
- case "subscribe:all":
85
- client.mode = "all";
86
- client.pipelineId = undefined;
87
- break;
88
-
89
- case "subscribe": {
90
- const dir = getPipelineDir(msg.pipelineId);
91
- if (!fs.existsSync(dir)) {
92
- client.ws.send(JSON.stringify({ type: "error", message: "Pipeline not found" }));
93
- return;
94
- }
95
- client.mode = "single";
96
- client.pipelineId = msg.pipelineId;
97
-
98
- const state = readPipelineState(msg.pipelineId);
99
- if (state) {
100
- prevActivitiesCount.set(msg.pipelineId, state.activities.length);
101
- client.ws.send(JSON.stringify({ type: "pipeline:updated", id: msg.pipelineId, state }));
102
- const checkpoint = detectCheckpoint(state.activities);
103
- if (checkpoint) {
104
- client.ws.send(JSON.stringify({ type: "pipeline:checkpoint", id: msg.pipelineId, checkpoint }));
105
- }
106
- }
107
- break;
108
- }
109
-
110
- case "unsubscribe":
111
- client.mode = "none";
112
- client.pipelineId = undefined;
113
- break;
114
-
115
- case "checkpoint:respond": {
116
- writeCheckpointResponse(msg.pipelineId, msg.action, msg.message);
117
- break;
118
- }
119
- }
120
- }
121
-
122
- return { wss, watcher };
123
- }