newpr 0.6.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/history/store.ts +25 -0
- package/src/stack/balance.ts +128 -0
- package/src/stack/coupling.test.ts +158 -0
- package/src/stack/coupling.ts +135 -0
- package/src/stack/delta.test.ts +223 -0
- package/src/stack/delta.ts +264 -0
- package/src/stack/execute.test.ts +176 -0
- package/src/stack/execute.ts +194 -0
- package/src/stack/feasibility.test.ts +185 -0
- package/src/stack/feasibility.ts +286 -0
- package/src/stack/integration.test.ts +266 -0
- package/src/stack/merge-groups.test.ts +97 -0
- package/src/stack/merge-groups.ts +87 -0
- package/src/stack/partition.test.ts +233 -0
- package/src/stack/partition.ts +273 -0
- package/src/stack/plan.test.ts +154 -0
- package/src/stack/plan.ts +139 -0
- package/src/stack/pr-title.ts +64 -0
- package/src/stack/publish.ts +96 -0
- package/src/stack/split.ts +173 -0
- package/src/stack/types.ts +202 -0
- package/src/stack/verify.test.ts +137 -0
- package/src/stack/verify.ts +201 -0
- package/src/web/client/components/FeasibilityAlert.tsx +64 -0
- package/src/web/client/components/InputScreen.tsx +100 -89
- package/src/web/client/components/ResultsScreen.tsx +10 -2
- package/src/web/client/components/StackGroupCard.tsx +171 -0
- package/src/web/client/components/StackWarnings.tsx +135 -0
- package/src/web/client/hooks/useStack.ts +301 -0
- package/src/web/client/panels/StackPanel.tsx +289 -0
- package/src/web/server/routes.ts +114 -0
- package/src/web/server/stack-manager.ts +580 -0
- package/src/web/server.ts +15 -0
- package/src/web/styles/built.css +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Loader2, Play, Upload, RotateCcw, CheckCircle2, AlertTriangle, Circle, GitPullRequestArrow, ArrowRight, Layers } from "lucide-react";
|
|
2
|
+
import { useStack } from "../hooks/useStack.ts";
|
|
3
|
+
import { FeasibilityAlert } from "../components/FeasibilityAlert.tsx";
|
|
4
|
+
import { StackGroupCard } from "../components/StackGroupCard.tsx";
|
|
5
|
+
import { StackWarnings } from "../components/StackWarnings.tsx";
|
|
6
|
+
|
|
7
|
+
type StackPhase = "idle" | "partitioning" | "planning" | "executing" | "publishing" | "done" | "error";
|
|
8
|
+
|
|
9
|
+
const PIPELINE_STEPS = [
|
|
10
|
+
{ phase: "partitioning" as const, label: "Partition", description: "Assigning files to groups" },
|
|
11
|
+
{ phase: "planning" as const, label: "Plan", description: "Building stack plan" },
|
|
12
|
+
{ phase: "executing" as const, label: "Execute", description: "Creating commits" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
function getStepState(stepPhase: string, currentPhase: StackPhase, isDone: boolean) {
|
|
16
|
+
const order = ["partitioning", "planning", "executing"];
|
|
17
|
+
const stepIdx = order.indexOf(stepPhase);
|
|
18
|
+
const currentIdx = order.indexOf(currentPhase);
|
|
19
|
+
|
|
20
|
+
if (isDone || (currentIdx > stepIdx)) return "done";
|
|
21
|
+
if (currentPhase === stepPhase) return "active";
|
|
22
|
+
return "pending";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function PipelineTimeline({ phase }: { phase: StackPhase }) {
|
|
26
|
+
const isDone = phase === "done" || phase === "publishing";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative flex flex-col gap-0 py-1">
|
|
30
|
+
{PIPELINE_STEPS.map((step, i) => {
|
|
31
|
+
const state = getStepState(step.phase, phase, isDone);
|
|
32
|
+
const isLast = i === PIPELINE_STEPS.length - 1;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div key={step.phase} className="relative flex items-start gap-3">
|
|
36
|
+
{!isLast && (
|
|
37
|
+
<div className={`absolute left-[9px] top-[20px] w-px h-[calc(100%-8px)] ${
|
|
38
|
+
state === "done" ? "bg-foreground/20" : "bg-border"
|
|
39
|
+
}`} />
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<div className="relative z-10 mt-0.5 shrink-0">
|
|
43
|
+
{state === "done" ? (
|
|
44
|
+
<CheckCircle2 className="h-[18px] w-[18px] text-foreground/70" />
|
|
45
|
+
) : state === "active" ? (
|
|
46
|
+
<Loader2 className="h-[18px] w-[18px] text-foreground animate-spin" />
|
|
47
|
+
) : (
|
|
48
|
+
<Circle className="h-[18px] w-[18px] text-muted-foreground/20" />
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className={`pb-4 min-w-0 ${isLast ? "pb-0" : ""}`}>
|
|
53
|
+
<span className={`text-[12px] font-medium ${
|
|
54
|
+
state === "done" ? "text-muted-foreground"
|
|
55
|
+
: state === "active" ? "text-foreground"
|
|
56
|
+
: "text-muted-foreground/40"
|
|
57
|
+
}`}>
|
|
58
|
+
{step.label}
|
|
59
|
+
</span>
|
|
60
|
+
{state === "active" && (
|
|
61
|
+
<p className="text-[11px] text-muted-foreground/50 mt-0.5">{step.description}</p>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function StackPanel({ sessionId }: { sessionId?: string | null }) {
|
|
72
|
+
const stack = useStack(sessionId);
|
|
73
|
+
|
|
74
|
+
if (stack.phase === "idle") {
|
|
75
|
+
return (
|
|
76
|
+
<div className="pt-6 px-1">
|
|
77
|
+
<div className="flex items-center gap-2.5 mb-5">
|
|
78
|
+
<div className="h-8 w-8 rounded-lg bg-foreground/[0.04] flex items-center justify-center">
|
|
79
|
+
<Layers className="h-4 w-4 text-foreground/50" />
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<h3 className="text-[13px] font-semibold text-foreground">PR Stacking</h3>
|
|
83
|
+
<p className="text-[11px] text-muted-foreground/50">Split into focused, reviewable PRs</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<p className="text-[11px] text-muted-foreground/40 leading-[1.6] mb-5">
|
|
88
|
+
Automatically split this PR into a stack of smaller draft PRs based on the analysis groups. Each group becomes its own PR with proper dependency ordering.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center gap-3 mb-5">
|
|
92
|
+
<span className="text-[11px] text-muted-foreground/50">Max PRs</span>
|
|
93
|
+
<div className="flex items-center">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => stack.setMaxGroups(Math.max(1, (stack.maxGroups ?? 2) - 1))}
|
|
97
|
+
className="h-7 w-7 rounded-l-md border border-r-0 bg-transparent text-[11px] text-muted-foreground/50 hover:bg-accent/30 transition-colors flex items-center justify-center"
|
|
98
|
+
>
|
|
99
|
+
−
|
|
100
|
+
</button>
|
|
101
|
+
<input
|
|
102
|
+
type="number"
|
|
103
|
+
min={1}
|
|
104
|
+
placeholder="auto"
|
|
105
|
+
value={stack.maxGroups ?? ""}
|
|
106
|
+
onChange={(e) => stack.setMaxGroups(e.target.value ? Number(e.target.value) : null)}
|
|
107
|
+
className="h-7 w-12 border-y bg-transparent text-[11px] text-center tabular-nums placeholder:text-muted-foreground/25 focus:outline-none"
|
|
108
|
+
/>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => stack.setMaxGroups((stack.maxGroups ?? 2) + 1)}
|
|
112
|
+
className="h-7 w-7 rounded-r-md border border-l-0 bg-transparent text-[11px] text-muted-foreground/50 hover:bg-accent/30 transition-colors flex items-center justify-center"
|
|
113
|
+
>
|
|
114
|
+
+
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={stack.runFullPipeline}
|
|
122
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
123
|
+
>
|
|
124
|
+
<Play className="h-3.5 w-3.5" />
|
|
125
|
+
Start Stacking
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const isRunning = ["partitioning", "planning", "executing", "publishing"].includes(stack.phase);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="pt-6 px-1 space-y-5">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
137
|
+
<Layers className="h-3.5 w-3.5 text-muted-foreground/40 shrink-0" />
|
|
138
|
+
<span className="text-[12px] font-semibold text-foreground">PR Stacking</span>
|
|
139
|
+
{stack.phase === "done" && !stack.publishResult && (
|
|
140
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-foreground/[0.06] text-muted-foreground/60">Ready</span>
|
|
141
|
+
)}
|
|
142
|
+
{stack.publishResult && (
|
|
143
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400">Published</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
{stack.phase === "done" && (
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={stack.reset}
|
|
150
|
+
className="flex items-center gap-1.5 text-[10px] text-muted-foreground/30 hover:text-muted-foreground transition-colors"
|
|
151
|
+
>
|
|
152
|
+
<RotateCcw className="h-3 w-3" />
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{isRunning && (
|
|
158
|
+
<>
|
|
159
|
+
<PipelineTimeline phase={stack.phase} />
|
|
160
|
+
{stack.progressMessage && (
|
|
161
|
+
<p className="text-[10px] text-muted-foreground/30 px-1 -mt-2">{stack.progressMessage}</p>
|
|
162
|
+
)}
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{stack.phase === "error" && (
|
|
167
|
+
<div className="rounded-lg bg-red-500/[0.04] px-3.5 py-3 space-y-3">
|
|
168
|
+
<div className="flex items-start gap-2.5">
|
|
169
|
+
<AlertTriangle className="h-3.5 w-3.5 text-red-500/70 shrink-0 mt-px" />
|
|
170
|
+
<span className="text-[11px] text-red-600/80 dark:text-red-400/80 break-all leading-relaxed">
|
|
171
|
+
{stack.error}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={stack.reset}
|
|
177
|
+
className="flex items-center gap-1.5 text-[11px] text-muted-foreground/40 hover:text-foreground transition-colors"
|
|
178
|
+
>
|
|
179
|
+
<RotateCcw className="h-3 w-3" />
|
|
180
|
+
Try again
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{stack.feasibility && (
|
|
186
|
+
<FeasibilityAlert result={stack.feasibility} />
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{stack.partition && stack.partition.structured_warnings.length > 0 && (
|
|
190
|
+
<StackWarnings warnings={stack.partition.structured_warnings} />
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{stack.plan && (
|
|
194
|
+
<div className="space-y-1">
|
|
195
|
+
<div className="flex items-center justify-between mb-2">
|
|
196
|
+
<span className="text-[11px] font-medium text-muted-foreground/40">
|
|
197
|
+
{stack.plan.groups.length} PRs
|
|
198
|
+
</span>
|
|
199
|
+
{stack.plan.groups.length > 0 && (
|
|
200
|
+
<span className="text-[10px] text-muted-foreground/25 tabular-nums">
|
|
201
|
+
{stack.plan.groups.reduce((sum, g) => sum + g.files.length, 0)} files total
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
<div className="space-y-0">
|
|
206
|
+
{stack.plan.groups.map((group) => {
|
|
207
|
+
const commit = stack.execResult?.group_commits.find((gc) => gc.group_id === group.id);
|
|
208
|
+
const pr = stack.publishResult?.prs.find((p) => p.group_id === group.id);
|
|
209
|
+
return (
|
|
210
|
+
<StackGroupCard
|
|
211
|
+
key={group.id}
|
|
212
|
+
group={group}
|
|
213
|
+
commit={commit}
|
|
214
|
+
pr={pr}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{stack.verifyResult && (
|
|
223
|
+
<div className="space-y-2">
|
|
224
|
+
<div className={`flex items-center gap-2.5 rounded-lg px-3.5 py-2.5 ${
|
|
225
|
+
stack.verifyResult.verified
|
|
226
|
+
? "bg-green-500/[0.04]"
|
|
227
|
+
: "bg-red-500/[0.04]"
|
|
228
|
+
}`}>
|
|
229
|
+
{stack.verifyResult.verified
|
|
230
|
+
? <CheckCircle2 className="h-3.5 w-3.5 text-green-600/70 dark:text-green-400/70 shrink-0" />
|
|
231
|
+
: <AlertTriangle className="h-3.5 w-3.5 text-red-500/70 shrink-0" />
|
|
232
|
+
}
|
|
233
|
+
<span className={`text-[11px] ${
|
|
234
|
+
stack.verifyResult.verified
|
|
235
|
+
? "text-green-700/70 dark:text-green-300/70"
|
|
236
|
+
: "text-red-600/80 dark:text-red-400/80"
|
|
237
|
+
}`}>
|
|
238
|
+
{stack.verifyResult.verified
|
|
239
|
+
? "Tree equivalence verified"
|
|
240
|
+
: `Verification failed: ${stack.verifyResult.errors.join(", ")}`
|
|
241
|
+
}
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
{stack.verifyResult.structured_warnings.length > 0 && (
|
|
245
|
+
<StackWarnings warnings={stack.verifyResult.structured_warnings} defaultCollapsed={stack.verifyResult.verified} />
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{stack.phase === "done" && stack.execResult && !stack.publishResult && (
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={stack.startPublish}
|
|
254
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<Upload className="h-3.5 w-3.5" />
|
|
257
|
+
Publish as Draft PRs
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{stack.publishResult && stack.publishResult.prs.length > 0 && (
|
|
262
|
+
<div className="space-y-1.5">
|
|
263
|
+
{stack.publishResult.prs.map((pr) => (
|
|
264
|
+
<a
|
|
265
|
+
key={pr.number}
|
|
266
|
+
href={pr.url}
|
|
267
|
+
target="_blank"
|
|
268
|
+
rel="noopener noreferrer"
|
|
269
|
+
className="group flex items-center gap-3 rounded-lg px-3.5 py-2.5 hover:bg-accent/30 transition-colors"
|
|
270
|
+
>
|
|
271
|
+
<GitPullRequestArrow className="h-3.5 w-3.5 text-green-600/60 dark:text-green-400/60 shrink-0" />
|
|
272
|
+
<div className="flex-1 min-w-0">
|
|
273
|
+
<div className="flex items-center gap-2">
|
|
274
|
+
<span className="text-[11px] font-medium truncate">{pr.title}</span>
|
|
275
|
+
<span className="text-[10px] text-muted-foreground/25 tabular-nums shrink-0">#{pr.number}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
278
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.base_branch}</span>
|
|
279
|
+
<ArrowRight className="h-2.5 w-2.5 text-muted-foreground/20" />
|
|
280
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.head_branch}</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</a>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
package/src/web/server/routes.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { getPlugin, getAllPlugins } from "../../plugins/registry.ts";
|
|
|
15
15
|
import { chatWithTools, createLlmClient, type ChatTool, type ChatStreamEvent } from "../../llm/client.ts";
|
|
16
16
|
import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { publishStack } from "../../stack/publish.ts";
|
|
19
|
+
import { startStack, getStackState, cancelStack, subscribeStack } from "./stack-manager.ts";
|
|
18
20
|
|
|
19
21
|
function json(data: unknown, status = 200): Response {
|
|
20
22
|
return new Response(JSON.stringify(data), {
|
|
@@ -1703,5 +1705,117 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
1703
1705
|
if (!job) return json({ status: "idle" });
|
|
1704
1706
|
return json(job);
|
|
1705
1707
|
},
|
|
1708
|
+
|
|
1709
|
+
"POST /api/stack/start": async (req: Request) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const body = await req.json() as { sessionId: string; maxGroups?: number };
|
|
1712
|
+
if (!body.sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1713
|
+
const result = startStack(body.sessionId, body.maxGroups ?? null, token, config);
|
|
1714
|
+
if ("error" in result) return json({ error: result.error }, result.status);
|
|
1715
|
+
return json({ ok: true });
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1718
|
+
return json({ error: msg }, 500);
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
|
|
1722
|
+
"GET /api/stack/:id": (req: Request) => {
|
|
1723
|
+
const url = new URL(req.url);
|
|
1724
|
+
const id = url.pathname.split("/").pop()!;
|
|
1725
|
+
const state = getStackState(id);
|
|
1726
|
+
if (!state) return json({ state: null });
|
|
1727
|
+
return json({ state });
|
|
1728
|
+
},
|
|
1729
|
+
|
|
1730
|
+
"POST /api/stack/:id/cancel": (req: Request) => {
|
|
1731
|
+
const url = new URL(req.url);
|
|
1732
|
+
const segments = url.pathname.split("/");
|
|
1733
|
+
const id = segments[segments.length - 2]!;
|
|
1734
|
+
const ok = cancelStack(id);
|
|
1735
|
+
return json({ ok });
|
|
1736
|
+
},
|
|
1737
|
+
|
|
1738
|
+
"GET /api/stack/:id/events": (req: Request) => {
|
|
1739
|
+
const url = new URL(req.url);
|
|
1740
|
+
const segments = url.pathname.split("/");
|
|
1741
|
+
const id = segments[segments.length - 2]!;
|
|
1742
|
+
|
|
1743
|
+
const stream = new ReadableStream({
|
|
1744
|
+
start(controller) {
|
|
1745
|
+
const encoder = new TextEncoder();
|
|
1746
|
+
const send = (eventType: string, data: string) => {
|
|
1747
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${data}\n\n`));
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
const unsubscribe = subscribeStack(id, (event) => {
|
|
1751
|
+
try {
|
|
1752
|
+
if ("type" in event && event.type === "done") {
|
|
1753
|
+
send("done", JSON.stringify({ state: getStackState(id) }));
|
|
1754
|
+
controller.close();
|
|
1755
|
+
} else if ("type" in event && event.type === "error") {
|
|
1756
|
+
send("stack_error", JSON.stringify({ message: event.data ?? "Unknown error", state: getStackState(id) }));
|
|
1757
|
+
controller.close();
|
|
1758
|
+
} else {
|
|
1759
|
+
send("progress", JSON.stringify({ ...event, state: getStackState(id) }));
|
|
1760
|
+
}
|
|
1761
|
+
} catch {
|
|
1762
|
+
controller.close();
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
if (!unsubscribe) {
|
|
1767
|
+
const existingState = getStackState(id);
|
|
1768
|
+
if (existingState) {
|
|
1769
|
+
send("done", JSON.stringify({ state: existingState }));
|
|
1770
|
+
} else {
|
|
1771
|
+
send("stack_error", JSON.stringify({ message: "No stack session found" }));
|
|
1772
|
+
}
|
|
1773
|
+
controller.close();
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
req.signal.addEventListener("abort", () => {
|
|
1777
|
+
unsubscribe?.();
|
|
1778
|
+
try { controller.close(); } catch {}
|
|
1779
|
+
});
|
|
1780
|
+
},
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
return new Response(stream, {
|
|
1784
|
+
headers: {
|
|
1785
|
+
"Content-Type": "text/event-stream",
|
|
1786
|
+
"Cache-Control": "no-cache",
|
|
1787
|
+
Connection: "keep-alive",
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
},
|
|
1791
|
+
|
|
1792
|
+
"POST /api/stack/publish": async (req: Request) => {
|
|
1793
|
+
try {
|
|
1794
|
+
const body = await req.json() as { sessionId: string };
|
|
1795
|
+
if (!body.sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1796
|
+
|
|
1797
|
+
const state = getStackState(body.sessionId);
|
|
1798
|
+
if (!state) return json({ error: "No stack state found" }, 404);
|
|
1799
|
+
if (!state.execResult) return json({ error: "Stack not executed yet" }, 400);
|
|
1800
|
+
if (!state.context) return json({ error: "Missing context" }, 400);
|
|
1801
|
+
|
|
1802
|
+
const stored = await loadSession(body.sessionId);
|
|
1803
|
+
if (!stored) return json({ error: "Session not found" }, 404);
|
|
1804
|
+
|
|
1805
|
+
const result = await publishStack({
|
|
1806
|
+
repo_path: state.context.repo_path,
|
|
1807
|
+
exec_result: state.execResult,
|
|
1808
|
+
pr_meta: stored.meta,
|
|
1809
|
+
base_branch: state.context.base_branch,
|
|
1810
|
+
owner: state.context.owner,
|
|
1811
|
+
repo: state.context.repo,
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
return json({ publish_result: result });
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1817
|
+
return json({ error: msg }, 500);
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1706
1820
|
};
|
|
1707
1821
|
}
|