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.
- package/bin/cli.js +13 -6
- package/package.json +1 -1
- package/template/.claude-pipeline/dashboard/package.json +1 -3
- package/template/.claude-pipeline/dashboard/server.ts +4 -3
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/events/route.ts +85 -0
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/stream/route.ts +72 -0
- package/template/.claude-pipeline/dashboard/src/hooks/use-pipeline-detail.ts +31 -45
- package/template/.claude-pipeline/dashboard/src/hooks/use-pipelines.ts +25 -38
- package/template/.claude-pipeline/dashboard/src/hooks/use-sse.ts +61 -0
- package/template/.claude-pipeline/dashboard/src/lib/pipelines.ts +2 -4
- package/template/.claude-pipeline/dashboard/src/lib/sse.ts +55 -0
- package/template/.claude-pipeline/dashboard/src/types/pipeline.ts +1 -7
- package/template/.claude-pipeline/runner/dist/context-watcher.d.ts +10 -6
- package/template/.claude-pipeline/runner/dist/context-watcher.js +53 -40
- package/template/.claude-pipeline/runner/dist/context-watcher.js.map +1 -1
- package/template/.claude-pipeline/runner/dist/pipeline-runner.js +8 -0
- package/template/.claude-pipeline/runner/dist/pipeline-runner.js.map +1 -1
- package/template/.claude-pipeline/runner/src/context-watcher.ts +58 -40
- package/template/.claude-pipeline/runner/src/pipeline-runner.ts +8 -0
- package/template/CLAUDE.md +22 -2
- package/template/.claude-pipeline/dashboard/src/hooks/use-websocket.ts +0 -58
- package/template/.claude-pipeline/dashboard/src/lib/watcher.ts +0 -90
- 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
|
-
--
|
|
287
|
-
--
|
|
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\
|
|
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
|
@@ -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,
|
|
4
|
-
import type { PipelineState, CheckpointInfo
|
|
5
|
-
import {
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} else if (
|
|
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
|
-
|
|
22
|
+
const activity = d.activity as PipelineState["activities"][0];
|
|
23
|
+
return { ...prev, activities: [...prev.activities, activity] };
|
|
36
24
|
});
|
|
37
|
-
} else if (
|
|
38
|
-
setCheckpoint(
|
|
39
|
-
} else if (
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
4
|
-
import type { PipelineSummary,
|
|
5
|
-
import {
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 ===
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
|
9
|
-
private
|
|
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
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
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.
|
|
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.
|
|
40
|
-
// Scan existing files
|
|
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
|
|
43
|
-
for (const file of
|
|
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
|
|
65
|
+
// directory may not exist
|
|
49
66
|
}
|
|
67
|
+
}
|
|
68
|
+
pollDirectory(dir, isRootFallback) {
|
|
50
69
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,+DAA+D;AAC/D,MAAM,mBAAmB,GAA2B;IAClD,oBAAoB,EAAE,CAAC;IACvB,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC;IACjB,mBAAmB,EAAE,CAAC;IACtB,gBAAgB,EAAE,CAAC;IACnB,WAAW,EAAE,CAAC;IACd,eAAe,EAAE,CAAC;IAClB,eAAe,EAAE,CAAC;IAClB,kBAAkB,EAAE,CAAC;IACrB,eAAe,EAAE,CAAC;IAClB,gBAAgB,EAAE,CAAC;IACnB,mBAAmB,EAAE,CAAC;IACtB,cAAc,EAAE,CAAC;IACjB,oBAAoB,EAAE,CAAC;CACxB,CAAC;AAEF
|
|
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":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;AAC5C,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;AAChD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAEhF,IAAI,CAAC,WAAW,IAAI,CAAC,aAAa,EAAE,CAAC;IACnC,OAAO,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;AACtD,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;AAE9C,uEAAuE;AACvE,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,SAAS,WAAW,CAAC,YAAoB,EAAE,UAAkB;IAC3D,OAAO;QACL,0BAA0B;QAC1B,EAAE;QACF,gBAAgB,UAAU,EAAE;QAC5B,EAAE;QACF,OAAO;QACP,YAAY;QACZ,EAAE;QACF,+BAA+B;QAC/B,2DAA2D;
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
|
29
|
-
private
|
|
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.
|
|
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.
|
|
52
|
+
fs.mkdirSync(this.pipelineContextDir, { recursive: true });
|
|
48
53
|
|
|
49
|
-
// Scan existing files
|
|
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
|
|
52
|
-
for (const file of
|
|
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
|
|
79
|
+
// directory may not exist
|
|
57
80
|
}
|
|
81
|
+
}
|
|
58
82
|
|
|
83
|
+
private pollDirectory(dir: string, isRootFallback: boolean): void {
|
|
59
84
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
package/template/CLAUDE.md
CHANGED
|
@@ -305,8 +305,28 @@ context/
|
|
|
305
305
|
## Pipeline Dashboard Integration
|
|
306
306
|
|
|
307
307
|
환경변수 `PIPELINE_ID`가 설정되어 있으면 **대시보드에서 실행 중**이다.
|
|
308
|
-
아래
|
|
309
|
-
|
|
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
|
-
}
|