stack-agent 0.1.0 → 0.3.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/README.md +5 -0
- package/dist/index.js +1556 -267
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -1,46 +1,507 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import React6 from "react";
|
|
5
|
+
import { withFullScreen } from "fullscreen-ink";
|
|
5
6
|
|
|
6
|
-
// src/cli/
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// src/cli/app.tsx
|
|
8
|
+
import { useState as useState5, useEffect as useEffect2, useCallback } from "react";
|
|
9
|
+
import { Box as Box8, Text as Text8, useApp, useInput as useInput3 } from "ink";
|
|
10
|
+
import { useScreenSize } from "fullscreen-ink";
|
|
11
|
+
|
|
12
|
+
// src/cli/components/header.tsx
|
|
13
|
+
import { Box, Text } from "ink";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
function Header({ appName, currentStage, stages, showDots = false, title }) {
|
|
16
|
+
const stageName = title ?? currentStage?.label ?? "Stack Progress";
|
|
17
|
+
return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderBottom: false, paddingX: 1, justifyContent: "space-between", children: [
|
|
18
|
+
/* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
19
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: appName }),
|
|
20
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: stageName })
|
|
21
|
+
] }),
|
|
22
|
+
showDots && /* @__PURE__ */ jsx(Box, { gap: 0, children: stages.map((s, i) => /* @__PURE__ */ jsx(StageDot, { stage: s, isCurrent: s.id === currentStage?.id }, s.id)) })
|
|
23
|
+
] });
|
|
13
24
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
25
|
+
function StageDot({ stage, isCurrent }) {
|
|
26
|
+
if (isCurrent) {
|
|
27
|
+
return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" });
|
|
28
|
+
}
|
|
29
|
+
if (stage.status === "complete" && stage.confirmed) {
|
|
30
|
+
return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF" });
|
|
31
|
+
}
|
|
32
|
+
if (stage.status === "complete" && !stage.confirmed) {
|
|
33
|
+
return /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CF" });
|
|
34
|
+
}
|
|
35
|
+
if (stage.status === "skipped") {
|
|
36
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2013" });
|
|
37
|
+
}
|
|
38
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CB" });
|
|
16
39
|
}
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
|
|
41
|
+
// src/cli/components/footer.tsx
|
|
42
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
43
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
44
|
+
function Footer({ progress, stages, terminalWidth, mode = "decisions" }) {
|
|
45
|
+
let display;
|
|
46
|
+
switch (mode) {
|
|
47
|
+
case "stage_list":
|
|
48
|
+
display = "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc back";
|
|
49
|
+
break;
|
|
50
|
+
case "options":
|
|
51
|
+
display = "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc stages";
|
|
52
|
+
break;
|
|
53
|
+
case "input":
|
|
54
|
+
display = "Enter submit \xB7 Esc stages";
|
|
55
|
+
break;
|
|
56
|
+
case "scaffold":
|
|
57
|
+
display = "Scaffolding your project...";
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
display = buildDecisionsDisplay(progress, stages, terminalWidth);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return /* @__PURE__ */ jsx2(Box2, { borderStyle: "single", borderTop: false, paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: display }) });
|
|
19
64
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
65
|
+
function buildDecisionsDisplay(progress, stages, terminalWidth) {
|
|
66
|
+
const decisions = [];
|
|
67
|
+
if (progress.projectName) decisions.push(`Project: ${progress.projectName}`);
|
|
68
|
+
if (progress.frontend) decisions.push(`Frontend: ${progress.frontend.component}`);
|
|
69
|
+
if (progress.backend) decisions.push(`Backend: ${progress.backend.component}`);
|
|
70
|
+
if (progress.database) decisions.push(`DB: ${progress.database.component}`);
|
|
71
|
+
if (progress.auth) decisions.push(`Auth: ${progress.auth.component}`);
|
|
72
|
+
if (progress.payments) decisions.push(`Pay: ${progress.payments.component}`);
|
|
73
|
+
if (progress.ai) decisions.push(`AI: ${progress.ai.component}`);
|
|
74
|
+
if (progress.deployment) decisions.push(`Deploy: ${progress.deployment.component}`);
|
|
75
|
+
const nextStage = stages.find((s) => s.status === "pending");
|
|
76
|
+
const nextText = nextStage ? `Next: ${nextStage.label}` : "";
|
|
77
|
+
const separator = " \u2502 ";
|
|
78
|
+
let display = decisions.map((d) => `\u2713 ${d}`).join(separator);
|
|
79
|
+
if (nextText) {
|
|
80
|
+
display = display ? `${display}${separator}${nextText}` : nextText;
|
|
81
|
+
}
|
|
82
|
+
const maxWidth = terminalWidth - 4;
|
|
83
|
+
if (display.length > maxWidth) {
|
|
84
|
+
display = display.slice(0, maxWidth - 1) + "\u2026";
|
|
85
|
+
}
|
|
86
|
+
return display;
|
|
22
87
|
}
|
|
23
|
-
|
|
24
|
-
|
|
88
|
+
|
|
89
|
+
// src/cli/components/conversation.tsx
|
|
90
|
+
import { useState, useEffect } from "react";
|
|
91
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
92
|
+
import { Spinner } from "@inkjs/ui";
|
|
93
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
94
|
+
function ConversationView({ bridge, maxLines }) {
|
|
95
|
+
const [text, setText] = useState("");
|
|
96
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
97
|
+
const [showSpinner, setShowSpinner] = useState(false);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const unsubs = [
|
|
100
|
+
bridge.subscribe("spinnerStart", () => {
|
|
101
|
+
setShowSpinner(true);
|
|
102
|
+
setIsStreaming(false);
|
|
103
|
+
setText("");
|
|
104
|
+
}),
|
|
105
|
+
bridge.subscribe("streamText", (delta) => {
|
|
106
|
+
setShowSpinner(false);
|
|
107
|
+
setIsStreaming(true);
|
|
108
|
+
setText((prev) => prev + delta);
|
|
109
|
+
}),
|
|
110
|
+
bridge.subscribe("streamEnd", () => {
|
|
111
|
+
setIsStreaming(false);
|
|
112
|
+
})
|
|
113
|
+
];
|
|
114
|
+
return () => unsubs.forEach((fn) => fn());
|
|
115
|
+
}, [bridge]);
|
|
116
|
+
if (showSpinner) {
|
|
117
|
+
return /* @__PURE__ */ jsx3(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx3(Spinner, { label: "Thinking..." }) });
|
|
118
|
+
}
|
|
119
|
+
const lines = text.split("\n");
|
|
120
|
+
const visible = lines.slice(-maxLines);
|
|
121
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingX: 1, children: visible.map((line, i) => /* @__PURE__ */ jsx3(Text3, { children: line }, i)) });
|
|
25
122
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
123
|
+
|
|
124
|
+
// src/cli/components/option-select.tsx
|
|
125
|
+
import { useState as useState2 } from "react";
|
|
126
|
+
import { Box as Box4, Text as Text4, useInput } from "ink";
|
|
127
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
128
|
+
function OptionSelect({ options, onSelect }) {
|
|
129
|
+
const [cursor, setCursor] = useState2(0);
|
|
130
|
+
const [textValue, setTextValue] = useState2("");
|
|
131
|
+
const totalItems = options.length + 1;
|
|
132
|
+
const isOnTextField = cursor === options.length;
|
|
133
|
+
useInput((input, key) => {
|
|
134
|
+
if (key.upArrow) {
|
|
135
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.downArrow) {
|
|
139
|
+
setCursor((c) => Math.min(totalItems - 1, c + 1));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (key.return) {
|
|
143
|
+
if (isOnTextField) {
|
|
144
|
+
if (textValue.trim()) {
|
|
145
|
+
onSelect({ kind: "text", value: textValue.trim() });
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
onSelect({ kind: "select", value: options[cursor].label });
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (isOnTextField) {
|
|
153
|
+
if (key.backspace || key.delete) {
|
|
154
|
+
setTextValue((v) => v.slice(0, -1));
|
|
155
|
+
} else if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
156
|
+
setTextValue((v) => v + input);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
30
159
|
});
|
|
31
|
-
|
|
160
|
+
let hasRecommended = false;
|
|
161
|
+
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", paddingX: 1, children: [
|
|
162
|
+
options.map((opt, i) => {
|
|
163
|
+
const isHighlighted = cursor === i;
|
|
164
|
+
const pointer = isHighlighted ? "\u276F " : " ";
|
|
165
|
+
let label = opt.label;
|
|
166
|
+
if (opt.recommended && !hasRecommended) {
|
|
167
|
+
label += " (Recommended)";
|
|
168
|
+
hasRecommended = true;
|
|
169
|
+
}
|
|
170
|
+
return /* @__PURE__ */ jsxs2(Box4, { children: [
|
|
171
|
+
/* @__PURE__ */ jsxs2(Text4, { color: isHighlighted ? "cyan" : void 0, children: [
|
|
172
|
+
pointer,
|
|
173
|
+
label
|
|
174
|
+
] }),
|
|
175
|
+
opt.description && /* @__PURE__ */ jsxs2(Text4, { dimColor: true, children: [
|
|
176
|
+
" \u2014 ",
|
|
177
|
+
opt.description
|
|
178
|
+
] })
|
|
179
|
+
] }, opt.label);
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ jsxs2(Box4, { marginTop: 1, children: [
|
|
182
|
+
/* @__PURE__ */ jsx4(Text4, { color: isOnTextField ? "cyan" : void 0, children: isOnTextField ? "\u276F " : " " }),
|
|
183
|
+
isOnTextField ? /* @__PURE__ */ jsxs2(Text4, { children: [
|
|
184
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: textValue }),
|
|
185
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u258A" }),
|
|
186
|
+
!textValue && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Type a question or suggestion..." })
|
|
187
|
+
] }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Type a question or suggestion..." })
|
|
188
|
+
] })
|
|
189
|
+
] });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/cli/components/stage-list.tsx
|
|
193
|
+
import { useState as useState3 } from "react";
|
|
194
|
+
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
195
|
+
import { ConfirmInput } from "@inkjs/ui";
|
|
196
|
+
|
|
197
|
+
// src/agent/progress.ts
|
|
198
|
+
function createProgress() {
|
|
199
|
+
return {
|
|
200
|
+
projectName: null,
|
|
201
|
+
description: null,
|
|
202
|
+
frontend: null,
|
|
203
|
+
backend: null,
|
|
204
|
+
database: null,
|
|
205
|
+
auth: null,
|
|
206
|
+
payments: null,
|
|
207
|
+
ai: null,
|
|
208
|
+
deployment: null,
|
|
209
|
+
extras: []
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function setDecision(progress, category, choice) {
|
|
213
|
+
if (category === "extras") {
|
|
214
|
+
return { ...progress, extras: [...progress.extras, choice] };
|
|
215
|
+
}
|
|
216
|
+
return { ...progress, [category]: choice };
|
|
217
|
+
}
|
|
218
|
+
function clearDecision(progress, category) {
|
|
219
|
+
if (category === "extras") {
|
|
220
|
+
return { ...progress, extras: [] };
|
|
221
|
+
}
|
|
222
|
+
return { ...progress, [category]: null };
|
|
223
|
+
}
|
|
224
|
+
function clearProjectInfo(progress) {
|
|
225
|
+
return { ...progress, projectName: null, description: null };
|
|
226
|
+
}
|
|
227
|
+
function isComplete(progress) {
|
|
228
|
+
return progress.projectName !== null && progress.description !== null && progress.frontend !== null && progress.database !== null && progress.deployment !== null;
|
|
229
|
+
}
|
|
230
|
+
function formatChoice(choice) {
|
|
231
|
+
if (choice === null) return "not yet decided";
|
|
232
|
+
return choice.component;
|
|
233
|
+
}
|
|
234
|
+
function serializeProgress(progress) {
|
|
235
|
+
const lines = [
|
|
236
|
+
`Project Name: ${progress.projectName ?? "not yet decided"}`,
|
|
237
|
+
`Description: ${progress.description ?? "not yet decided"}`,
|
|
238
|
+
`Frontend: ${formatChoice(progress.frontend)}`,
|
|
239
|
+
`Backend: ${formatChoice(progress.backend)}`,
|
|
240
|
+
`Database: ${formatChoice(progress.database)}`,
|
|
241
|
+
`Auth: ${formatChoice(progress.auth)}`,
|
|
242
|
+
`Payments: ${formatChoice(progress.payments)}`,
|
|
243
|
+
`AI/LLM: ${formatChoice(progress.ai)}`,
|
|
244
|
+
`Deployment: ${formatChoice(progress.deployment)}`,
|
|
245
|
+
`Extras: ${progress.extras.length > 0 ? progress.extras.map((e) => e.component).join(", ") : "not yet decided"}`
|
|
246
|
+
];
|
|
247
|
+
return lines.join("\n");
|
|
248
|
+
}
|
|
249
|
+
function serializeSession(session) {
|
|
250
|
+
return JSON.stringify(session, null, 2);
|
|
251
|
+
}
|
|
252
|
+
function deserializeSession(json) {
|
|
253
|
+
try {
|
|
254
|
+
const data = JSON.parse(json);
|
|
255
|
+
if (data.version !== 1) return null;
|
|
256
|
+
if (!data.progress || !Array.isArray(data.stages) || !Array.isArray(data.messages)) return null;
|
|
257
|
+
return data;
|
|
258
|
+
} catch {
|
|
32
259
|
return null;
|
|
33
260
|
}
|
|
34
|
-
return result;
|
|
35
261
|
}
|
|
36
|
-
|
|
37
|
-
|
|
262
|
+
|
|
263
|
+
// src/cli/components/stage-list.tsx
|
|
264
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
265
|
+
function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
266
|
+
const [showConfirm, setShowConfirm] = useState3(false);
|
|
267
|
+
const [showWarning, setShowWarning] = useState3("");
|
|
268
|
+
const [cursor, setCursor] = useState3(0);
|
|
269
|
+
const canBuild = isComplete(progress);
|
|
270
|
+
const itemCount = stages.length + 1;
|
|
271
|
+
useInput2((input, key) => {
|
|
272
|
+
if (showConfirm) return;
|
|
273
|
+
if (key.upArrow) {
|
|
274
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
275
|
+
setShowWarning("");
|
|
276
|
+
}
|
|
277
|
+
if (key.downArrow) {
|
|
278
|
+
setCursor((c) => Math.min(itemCount - 1, c + 1));
|
|
279
|
+
setShowWarning("");
|
|
280
|
+
}
|
|
281
|
+
if (key.return) {
|
|
282
|
+
if (cursor < stages.length) {
|
|
283
|
+
onResult({ kind: "select", stageId: stages[cursor].id });
|
|
284
|
+
} else {
|
|
285
|
+
if (canBuild) {
|
|
286
|
+
setShowConfirm(true);
|
|
287
|
+
} else {
|
|
288
|
+
const missing = getMissingDecisions(progress);
|
|
289
|
+
setShowWarning(`Complete ${missing.join(", ")} first.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (key.escape) {
|
|
294
|
+
onResult({ kind: "cancel" });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
if (showConfirm) {
|
|
298
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", paddingX: 1, children: [
|
|
299
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Ready to build this stack?" }),
|
|
300
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(
|
|
301
|
+
ConfirmInput,
|
|
302
|
+
{
|
|
303
|
+
defaultChoice: "confirm",
|
|
304
|
+
onConfirm: () => onResult({ kind: "build" }),
|
|
305
|
+
onCancel: () => setShowConfirm(false)
|
|
306
|
+
}
|
|
307
|
+
) })
|
|
308
|
+
] });
|
|
309
|
+
}
|
|
310
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", paddingX: 1, children: [
|
|
311
|
+
showWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: showWarning }) }),
|
|
312
|
+
stages.map((stage, i) => {
|
|
313
|
+
const isHighlighted = cursor === i;
|
|
314
|
+
const pointer = isHighlighted ? "\u276F " : " ";
|
|
315
|
+
return /* @__PURE__ */ jsxs3(Box5, { children: [
|
|
316
|
+
/* @__PURE__ */ jsx5(Text5, { children: pointer }),
|
|
317
|
+
/* @__PURE__ */ jsx5(StageLabel, { stage, isCurrent: stage.id === currentStageId })
|
|
318
|
+
] }, stage.id);
|
|
319
|
+
}),
|
|
320
|
+
/* @__PURE__ */ jsxs3(Box5, { marginTop: 1, children: [
|
|
321
|
+
/* @__PURE__ */ jsx5(Text5, { children: cursor === stages.length ? "\u276F " : " " }),
|
|
322
|
+
/* @__PURE__ */ jsx5(Text5, { color: canBuild ? "green" : "yellow", bold: cursor === stages.length, children: "\u2605 Build" }),
|
|
323
|
+
!canBuild && /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
|
|
324
|
+
" (",
|
|
325
|
+
requiredRemaining(progress),
|
|
326
|
+
" remaining)"
|
|
327
|
+
] })
|
|
328
|
+
] })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
function StageLabel({ stage, isCurrent }) {
|
|
332
|
+
if (stage.status === "complete" && stage.confirmed) {
|
|
333
|
+
return /* @__PURE__ */ jsxs3(Text5, { children: [
|
|
334
|
+
/* @__PURE__ */ jsxs3(Text5, { color: "green", children: [
|
|
335
|
+
"\u2713 ",
|
|
336
|
+
stage.label
|
|
337
|
+
] }),
|
|
338
|
+
/* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
|
|
339
|
+
" \u2014 ",
|
|
340
|
+
stage.summary
|
|
341
|
+
] })
|
|
342
|
+
] });
|
|
343
|
+
}
|
|
344
|
+
if (stage.status === "complete" && !stage.confirmed) {
|
|
345
|
+
return /* @__PURE__ */ jsxs3(Text5, { children: [
|
|
346
|
+
/* @__PURE__ */ jsxs3(Text5, { color: "yellow", children: [
|
|
347
|
+
"\u25C6 ",
|
|
348
|
+
stage.label
|
|
349
|
+
] }),
|
|
350
|
+
/* @__PURE__ */ jsxs3(Text5, { color: "yellow", dimColor: true, children: [
|
|
351
|
+
" \u2014 ",
|
|
352
|
+
stage.summary,
|
|
353
|
+
" (suggested)"
|
|
354
|
+
] })
|
|
355
|
+
] });
|
|
356
|
+
}
|
|
357
|
+
if (stage.status === "skipped") {
|
|
358
|
+
return /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
|
|
359
|
+
"\u2013 ",
|
|
360
|
+
stage.label,
|
|
361
|
+
" \u2014 skipped"
|
|
362
|
+
] });
|
|
363
|
+
}
|
|
364
|
+
if (isCurrent) {
|
|
365
|
+
return /* @__PURE__ */ jsxs3(Text5, { children: [
|
|
366
|
+
/* @__PURE__ */ jsxs3(Text5, { color: "cyan", bold: true, children: [
|
|
367
|
+
"\u25CF ",
|
|
368
|
+
stage.label
|
|
369
|
+
] }),
|
|
370
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " \u2190 needs your input" })
|
|
371
|
+
] });
|
|
372
|
+
}
|
|
373
|
+
return /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
|
|
374
|
+
"\u25CB ",
|
|
375
|
+
stage.label
|
|
376
|
+
] });
|
|
377
|
+
}
|
|
378
|
+
function requiredRemaining(progress) {
|
|
379
|
+
let count = 0;
|
|
380
|
+
if (!progress.projectName) count++;
|
|
381
|
+
if (!progress.description) count++;
|
|
382
|
+
if (!progress.frontend) count++;
|
|
383
|
+
if (!progress.database) count++;
|
|
384
|
+
if (!progress.deployment) count++;
|
|
385
|
+
return count;
|
|
386
|
+
}
|
|
387
|
+
function getMissingDecisions(progress) {
|
|
388
|
+
const missing = [];
|
|
389
|
+
if (!progress.projectName || !progress.description) missing.push("Project Info");
|
|
390
|
+
if (!progress.frontend) missing.push("Frontend");
|
|
391
|
+
if (!progress.database) missing.push("Database");
|
|
392
|
+
if (!progress.deployment) missing.push("Deployment");
|
|
393
|
+
return missing;
|
|
38
394
|
}
|
|
39
|
-
|
|
40
|
-
|
|
395
|
+
|
|
396
|
+
// src/cli/app.tsx
|
|
397
|
+
import { TextInput as TextInput2, Spinner as Spinner3 } from "@inkjs/ui";
|
|
398
|
+
|
|
399
|
+
// src/cli/components/project-info-form.tsx
|
|
400
|
+
import { useState as useState4 } from "react";
|
|
401
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
402
|
+
import { TextInput } from "@inkjs/ui";
|
|
403
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
404
|
+
function ProjectInfoForm({ onSubmit }) {
|
|
405
|
+
const [field, setField] = useState4("name");
|
|
406
|
+
const [name, setName] = useState4("");
|
|
407
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
408
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Let's set up your project" }),
|
|
409
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
410
|
+
field === "name" && /* @__PURE__ */ jsxs4(Box6, { children: [
|
|
411
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "Project name: " }),
|
|
412
|
+
/* @__PURE__ */ jsx6(
|
|
413
|
+
TextInput,
|
|
414
|
+
{
|
|
415
|
+
placeholder: "my-app",
|
|
416
|
+
onSubmit: (value) => {
|
|
417
|
+
if (value.trim()) {
|
|
418
|
+
setName(value.trim());
|
|
419
|
+
setField("description");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
] }),
|
|
425
|
+
field === "description" && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
426
|
+
/* @__PURE__ */ jsxs4(Box6, { children: [
|
|
427
|
+
/* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713 Project name: " }),
|
|
428
|
+
/* @__PURE__ */ jsx6(Text6, { children: name })
|
|
429
|
+
] }),
|
|
430
|
+
/* @__PURE__ */ jsxs4(Box6, { children: [
|
|
431
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "What are you building: " }),
|
|
432
|
+
/* @__PURE__ */ jsx6(
|
|
433
|
+
TextInput,
|
|
434
|
+
{
|
|
435
|
+
placeholder: "a task management SaaS",
|
|
436
|
+
onSubmit: (value) => {
|
|
437
|
+
if (value.trim()) {
|
|
438
|
+
onSubmit(name, value.trim());
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
] })
|
|
444
|
+
] }),
|
|
445
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
446
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: field === "name" ? "Enter a name for your project" : "Briefly describe what you're building" })
|
|
447
|
+
] });
|
|
41
448
|
}
|
|
42
|
-
|
|
43
|
-
|
|
449
|
+
|
|
450
|
+
// src/cli/components/scaffold-view.tsx
|
|
451
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
452
|
+
import { Spinner as Spinner2 } from "@inkjs/ui";
|
|
453
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
454
|
+
function ScaffoldView({ steps }) {
|
|
455
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: steps.map((step, i) => /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
456
|
+
/* @__PURE__ */ jsxs5(Box7, { children: [
|
|
457
|
+
step.status === "running" && /* @__PURE__ */ jsx7(Spinner2, {}),
|
|
458
|
+
step.status === "done" && /* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
459
|
+
step.status === "error" && /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }),
|
|
460
|
+
/* @__PURE__ */ jsxs5(Text7, { bold: step.status === "running", children: [
|
|
461
|
+
" ",
|
|
462
|
+
step.name
|
|
463
|
+
] })
|
|
464
|
+
] }),
|
|
465
|
+
step.status === "done" && step.files && step.files.length > 0 && /* @__PURE__ */ jsx7(Box7, { paddingLeft: 3, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: step.files.join(", ") }) }),
|
|
466
|
+
step.status === "error" && step.error && /* @__PURE__ */ jsx7(Box7, { paddingLeft: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: step.error }) })
|
|
467
|
+
] }, i)) });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/cli/bridge.ts
|
|
471
|
+
function createBridge() {
|
|
472
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
473
|
+
let pendingResolve = null;
|
|
474
|
+
function emit(event, ...args2) {
|
|
475
|
+
const set = listeners.get(event);
|
|
476
|
+
if (set) {
|
|
477
|
+
for (const fn of set) fn(...args2);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
onStreamText: (delta) => emit("streamText", delta),
|
|
482
|
+
onStreamEnd: (fullText) => emit("streamEnd", fullText),
|
|
483
|
+
onPresentOptions: (options) => emit("presentOptions", options),
|
|
484
|
+
onSpinnerStart: () => emit("spinnerStart"),
|
|
485
|
+
onStageComplete: (summary) => emit("stageComplete", summary),
|
|
486
|
+
onError: (error) => emit("error", error),
|
|
487
|
+
waitForInput: () => new Promise((resolve2) => {
|
|
488
|
+
pendingResolve = resolve2;
|
|
489
|
+
}),
|
|
490
|
+
resolveInput: (result) => {
|
|
491
|
+
if (pendingResolve) {
|
|
492
|
+
const resolve2 = pendingResolve;
|
|
493
|
+
pendingResolve = null;
|
|
494
|
+
resolve2(result);
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
subscribe: (event, listener) => {
|
|
498
|
+
if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
|
|
499
|
+
listeners.get(event).add(listener);
|
|
500
|
+
return () => {
|
|
501
|
+
listeners.get(event)?.delete(listener);
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
};
|
|
44
505
|
}
|
|
45
506
|
|
|
46
507
|
// src/agent/loop.ts
|
|
@@ -48,6 +509,22 @@ import { join as join2 } from "path";
|
|
|
48
509
|
|
|
49
510
|
// src/llm/client.ts
|
|
50
511
|
import Anthropic from "@anthropic-ai/sdk";
|
|
512
|
+
|
|
513
|
+
// src/util/logger.ts
|
|
514
|
+
import pino from "pino";
|
|
515
|
+
var logger = pino({
|
|
516
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
517
|
+
transport: process.env.NODE_ENV !== "production" ? { target: "pino-pretty", options: { destination: 2 } } : void 0
|
|
518
|
+
}, process.env.NODE_ENV === "production" ? pino.destination(2) : void 0);
|
|
519
|
+
function createLogger(component) {
|
|
520
|
+
return logger.child({ component });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/llm/client.ts
|
|
524
|
+
var log = createLogger("llm");
|
|
525
|
+
function summarizeMessages(messages) {
|
|
526
|
+
return `${messages.length} messages, last: ${messages.at(-1)?.role ?? "none"}`;
|
|
527
|
+
}
|
|
51
528
|
function getClient() {
|
|
52
529
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
53
530
|
if (!apiKey) {
|
|
@@ -66,6 +543,8 @@ function client() {
|
|
|
66
543
|
}
|
|
67
544
|
async function chat(options) {
|
|
68
545
|
const { system, messages, tools, maxTokens, mcpServers } = options;
|
|
546
|
+
log.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chat request");
|
|
547
|
+
let response;
|
|
69
548
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
70
549
|
const mcpServerList = Object.entries(mcpServers).map(([name, config]) => ({
|
|
71
550
|
type: "url",
|
|
@@ -75,13 +554,13 @@ async function chat(options) {
|
|
|
75
554
|
authorization_token: config.apiKey
|
|
76
555
|
}
|
|
77
556
|
}));
|
|
78
|
-
|
|
557
|
+
response = await client().beta.messages.create(
|
|
79
558
|
{
|
|
80
559
|
model: "claude-sonnet-4-6",
|
|
81
560
|
max_tokens: maxTokens,
|
|
82
561
|
system,
|
|
83
562
|
messages,
|
|
84
|
-
tools,
|
|
563
|
+
...tools && tools.length > 0 && { tools },
|
|
85
564
|
mcp_servers: mcpServerList
|
|
86
565
|
},
|
|
87
566
|
{
|
|
@@ -90,28 +569,43 @@ async function chat(options) {
|
|
|
90
569
|
}
|
|
91
570
|
}
|
|
92
571
|
);
|
|
572
|
+
} else {
|
|
573
|
+
response = await client().messages.create({
|
|
574
|
+
model: "claude-sonnet-4-6",
|
|
575
|
+
max_tokens: maxTokens,
|
|
576
|
+
system,
|
|
577
|
+
messages,
|
|
578
|
+
...tools && tools.length > 0 && { tools }
|
|
579
|
+
});
|
|
93
580
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
581
|
+
log.info({
|
|
582
|
+
stopReason: response.stop_reason,
|
|
583
|
+
contentBlocks: response.content.length,
|
|
584
|
+
usage: response.usage
|
|
585
|
+
}, "chat response");
|
|
586
|
+
log.debug({ content: response.content }, "chat response content");
|
|
587
|
+
return response;
|
|
101
588
|
}
|
|
102
589
|
async function chatStream(options, callbacks) {
|
|
103
590
|
const { system, messages, tools, maxTokens } = options;
|
|
591
|
+
log.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chatStream request");
|
|
104
592
|
const stream = client().messages.stream({
|
|
105
593
|
model: "claude-sonnet-4-6",
|
|
106
594
|
max_tokens: maxTokens,
|
|
107
595
|
system,
|
|
108
596
|
messages,
|
|
109
|
-
tools
|
|
597
|
+
...tools && tools.length > 0 && { tools }
|
|
110
598
|
});
|
|
111
|
-
stream.on("text", (
|
|
112
|
-
callbacks.onText(
|
|
599
|
+
stream.on("text", (text) => {
|
|
600
|
+
callbacks.onText(text);
|
|
113
601
|
});
|
|
114
602
|
const finalMessage = await stream.finalMessage();
|
|
603
|
+
log.info({
|
|
604
|
+
stopReason: finalMessage.stop_reason,
|
|
605
|
+
contentBlocks: finalMessage.content.length,
|
|
606
|
+
usage: finalMessage.usage
|
|
607
|
+
}, "chatStream response");
|
|
608
|
+
log.debug({ content: finalMessage.content }, "chatStream response content");
|
|
115
609
|
for (const block of finalMessage.content) {
|
|
116
610
|
if (block.type === "tool_use") {
|
|
117
611
|
callbacks.onToolUse(block);
|
|
@@ -123,47 +617,6 @@ async function chatStream(options, callbacks) {
|
|
|
123
617
|
});
|
|
124
618
|
}
|
|
125
619
|
|
|
126
|
-
// src/agent/progress.ts
|
|
127
|
-
function createProgress() {
|
|
128
|
-
return {
|
|
129
|
-
projectName: null,
|
|
130
|
-
description: null,
|
|
131
|
-
frontend: null,
|
|
132
|
-
backend: null,
|
|
133
|
-
database: null,
|
|
134
|
-
auth: null,
|
|
135
|
-
payments: null,
|
|
136
|
-
ai: null,
|
|
137
|
-
deployment: null,
|
|
138
|
-
extras: []
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function setDecision(progress, category, choice) {
|
|
142
|
-
if (category === "extras") {
|
|
143
|
-
return { ...progress, extras: [...progress.extras, choice] };
|
|
144
|
-
}
|
|
145
|
-
return { ...progress, [category]: choice };
|
|
146
|
-
}
|
|
147
|
-
function formatChoice(choice) {
|
|
148
|
-
if (choice === null) return "not yet decided";
|
|
149
|
-
return choice.component;
|
|
150
|
-
}
|
|
151
|
-
function serializeProgress(progress) {
|
|
152
|
-
const lines = [
|
|
153
|
-
`Project Name: ${progress.projectName ?? "not yet decided"}`,
|
|
154
|
-
`Description: ${progress.description ?? "not yet decided"}`,
|
|
155
|
-
`Frontend: ${formatChoice(progress.frontend)}`,
|
|
156
|
-
`Backend: ${formatChoice(progress.backend)}`,
|
|
157
|
-
`Database: ${formatChoice(progress.database)}`,
|
|
158
|
-
`Auth: ${formatChoice(progress.auth)}`,
|
|
159
|
-
`Payments: ${formatChoice(progress.payments)}`,
|
|
160
|
-
`AI/LLM: ${formatChoice(progress.ai)}`,
|
|
161
|
-
`Deployment: ${formatChoice(progress.deployment)}`,
|
|
162
|
-
`Extras: ${progress.extras.length > 0 ? progress.extras.map((e) => e.component).join(", ") : "not yet decided"}`
|
|
163
|
-
];
|
|
164
|
-
return lines.join("\n");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
620
|
// src/agent/tools.ts
|
|
168
621
|
function conversationToolDefinitions() {
|
|
169
622
|
return [
|
|
@@ -185,15 +638,6 @@ function conversationToolDefinitions() {
|
|
|
185
638
|
reasoning: {
|
|
186
639
|
type: "string",
|
|
187
640
|
description: "Explanation for why this component was chosen."
|
|
188
|
-
},
|
|
189
|
-
scaffoldTool: {
|
|
190
|
-
type: "string",
|
|
191
|
-
description: "Optional CLI scaffold tool to use (e.g. create-next-app)."
|
|
192
|
-
},
|
|
193
|
-
scaffoldArgs: {
|
|
194
|
-
type: "array",
|
|
195
|
-
items: { type: "string" },
|
|
196
|
-
description: "Optional arguments to pass to the scaffold tool."
|
|
197
641
|
}
|
|
198
642
|
},
|
|
199
643
|
required: ["category", "component", "reasoning"]
|
|
@@ -236,12 +680,27 @@ function conversationToolDefinitions() {
|
|
|
236
680
|
}
|
|
237
681
|
},
|
|
238
682
|
{
|
|
239
|
-
name: "
|
|
240
|
-
description: "
|
|
683
|
+
name: "present_options",
|
|
684
|
+
description: "Presents technology options for the user to choose from. The UI renders these as selectable items.",
|
|
241
685
|
input_schema: {
|
|
242
686
|
type: "object",
|
|
243
|
-
properties: {
|
|
244
|
-
|
|
687
|
+
properties: {
|
|
688
|
+
options: {
|
|
689
|
+
type: "array",
|
|
690
|
+
items: {
|
|
691
|
+
type: "object",
|
|
692
|
+
properties: {
|
|
693
|
+
label: { type: "string", description: "Short name of the option (max 30 chars)." },
|
|
694
|
+
description: { type: "string", description: "One-line description (max 80 chars)." },
|
|
695
|
+
recommended: { type: "boolean", description: "Whether this is recommended. At most one true." }
|
|
696
|
+
},
|
|
697
|
+
required: ["label", "description"]
|
|
698
|
+
},
|
|
699
|
+
minItems: 2,
|
|
700
|
+
maxItems: 3
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
required: ["options"]
|
|
245
704
|
}
|
|
246
705
|
}
|
|
247
706
|
];
|
|
@@ -273,6 +732,10 @@ function scaffoldToolDefinitions() {
|
|
|
273
732
|
input_schema: {
|
|
274
733
|
type: "object",
|
|
275
734
|
properties: {
|
|
735
|
+
name: {
|
|
736
|
+
type: "string",
|
|
737
|
+
description: 'Short name for this integration (e.g. "Database", "Auth", "AI Chat", "Deploy Config").'
|
|
738
|
+
},
|
|
276
739
|
files: {
|
|
277
740
|
type: "object",
|
|
278
741
|
additionalProperties: { type: "string" },
|
|
@@ -288,13 +751,18 @@ function scaffoldToolDefinitions() {
|
|
|
288
751
|
additionalProperties: { type: "string" },
|
|
289
752
|
description: "Map of package names to versions to install as dev dependencies."
|
|
290
753
|
},
|
|
754
|
+
scripts: {
|
|
755
|
+
type: "object",
|
|
756
|
+
additionalProperties: { type: "string" },
|
|
757
|
+
description: "Map of script names to commands to merge into package.json scripts."
|
|
758
|
+
},
|
|
291
759
|
envVars: {
|
|
292
760
|
type: "array",
|
|
293
761
|
items: { type: "string" },
|
|
294
762
|
description: "List of environment variable names required by the integration."
|
|
295
763
|
}
|
|
296
764
|
},
|
|
297
|
-
required: ["files"]
|
|
765
|
+
required: ["name", "files"]
|
|
298
766
|
}
|
|
299
767
|
}
|
|
300
768
|
];
|
|
@@ -304,9 +772,7 @@ function executeConversationTool(name, input, progress, _messages) {
|
|
|
304
772
|
const category = input.category;
|
|
305
773
|
const choice = {
|
|
306
774
|
component: input.component,
|
|
307
|
-
reasoning: input.reasoning
|
|
308
|
-
...input.scaffoldTool !== void 0 && { scaffoldTool: input.scaffoldTool },
|
|
309
|
-
...input.scaffoldArgs !== void 0 && { scaffoldArgs: input.scaffoldArgs }
|
|
775
|
+
reasoning: input.reasoning
|
|
310
776
|
};
|
|
311
777
|
const updatedProgress = setDecision(progress, category, choice);
|
|
312
778
|
return {
|
|
@@ -331,12 +797,8 @@ function executeConversationTool(name, input, progress, _messages) {
|
|
|
331
797
|
response: input.summary
|
|
332
798
|
};
|
|
333
799
|
}
|
|
334
|
-
if (name === "
|
|
335
|
-
return {
|
|
336
|
-
progress,
|
|
337
|
-
response: "Plan is ready to present.",
|
|
338
|
-
signal: "present_plan"
|
|
339
|
-
};
|
|
800
|
+
if (name === "present_options") {
|
|
801
|
+
return { progress, response: "Options presented to user." };
|
|
340
802
|
}
|
|
341
803
|
return {
|
|
342
804
|
progress,
|
|
@@ -344,25 +806,65 @@ function executeConversationTool(name, input, progress, _messages) {
|
|
|
344
806
|
};
|
|
345
807
|
}
|
|
346
808
|
|
|
809
|
+
// src/agent/stages.ts
|
|
810
|
+
var DEFAULT_STAGES = [
|
|
811
|
+
{ id: "project_info", label: "Project Info", status: "pending", progressKeys: ["projectName", "description"] },
|
|
812
|
+
{ id: "frontend", label: "Frontend", status: "pending", progressKeys: ["frontend"] },
|
|
813
|
+
{ id: "backend", label: "Backend", status: "pending", progressKeys: ["backend"] },
|
|
814
|
+
{ id: "database", label: "Database", status: "pending", progressKeys: ["database"] },
|
|
815
|
+
{ id: "auth", label: "Auth", status: "pending", progressKeys: ["auth"] },
|
|
816
|
+
{ id: "payments", label: "Payments", status: "pending", progressKeys: ["payments"] },
|
|
817
|
+
{ id: "ai", label: "AI/LLM", status: "pending", progressKeys: ["ai"] },
|
|
818
|
+
{ id: "deployment", label: "Deployment", status: "pending", progressKeys: ["deployment"] },
|
|
819
|
+
{ id: "extras", label: "Extras", status: "pending", progressKeys: ["extras"] }
|
|
820
|
+
];
|
|
821
|
+
var STAGE_INSTRUCTIONS = {
|
|
822
|
+
project_info: "Ask for the project name and a brief description of what the user is building. Call set_project_info to record them.",
|
|
823
|
+
frontend: "Present 2-3 frontend framework options with trade-offs and your recommendation. Consider the project description when suggesting options.",
|
|
824
|
+
backend: "Present 2-3 backend/API options. Consider the chosen frontend \u2014 if it has built-in API routes (e.g., Next.js), that may be sufficient. If this stage is not needed, explain why and skip it.",
|
|
825
|
+
database: "Present 2-3 database options with ORM/query layer recommendations. Consider the chosen frontend and backend when suggesting options.",
|
|
826
|
+
auth: "Present 2-3 authentication options. If auth is not needed for this project, explain why and skip it.",
|
|
827
|
+
payments: "Present 2-3 payment processing options. If payments are not needed, explain why and skip it.",
|
|
828
|
+
ai: "Present 2-3 AI/LLM integration options. If AI is not needed, explain why and skip it.",
|
|
829
|
+
deployment: "Present 2-3 deployment platform options. Consider the chosen frontend and backend when suggesting options.",
|
|
830
|
+
extras: "Suggest any additional integrations that would benefit this project (analytics, email, monitoring, etc.). If none are needed, explain why and skip it."
|
|
831
|
+
};
|
|
832
|
+
|
|
347
833
|
// src/agent/system-prompt.ts
|
|
348
|
-
function buildConversationPrompt(progress) {
|
|
834
|
+
function buildConversationPrompt(progress, stageId, stages) {
|
|
835
|
+
const stage = stages.find((s) => s.id === stageId);
|
|
836
|
+
const stageLabel = stage?.label ?? stageId;
|
|
837
|
+
const instruction = STAGE_INSTRUCTIONS[stageId] ?? `Discuss the ${stageLabel} stage with the user.`;
|
|
838
|
+
const completedSummaries = stages.filter((s) => s.status === "complete" && s.summary).map((s) => `- ${s.label}: ${s.summary}`).join("\n");
|
|
839
|
+
const contextSection = completedSummaries ? `
|
|
840
|
+
|
|
841
|
+
Context from previous stages:
|
|
842
|
+
${completedSummaries}` : "";
|
|
349
843
|
return `You are a senior software architect helping a developer set up a new project.
|
|
350
844
|
|
|
351
|
-
|
|
845
|
+
Current project state:
|
|
846
|
+
${serializeProgress(progress)}
|
|
847
|
+
|
|
848
|
+
## Current Stage: ${stageLabel}
|
|
352
849
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
- For each set of options, explicitly label your top pick with "(Recommended)" next to it and explain WHY it's the best fit for this specific project. Example: "1. Next.js (Recommended) \u2014 server components, built-in API routes...". Then briefly describe the alternatives and their trade-offs. Be opinionated \u2014 you are a senior architect, not a menu.
|
|
356
|
-
- Keep the conversation focused and friendly. Ask one category at a time.
|
|
357
|
-
- When the user decides on something, call \`set_decision\` to commit that decision before moving on.
|
|
358
|
-
- Start by asking for a project name and a brief description of what they're building. Call \`set_project_info\` to record these before moving to stack decisions.
|
|
359
|
-
- As conversations get long, call \`summarize_stage\` when completing each category to keep context manageable.
|
|
360
|
-
- Once all decisions are made (frontend, database, and deployment are required; backend, auth, payments, and extras are optional), call \`present_plan\` to signal the plan is ready.
|
|
850
|
+
You are currently discussing the ${stageLabel} stage.
|
|
851
|
+
${instruction}
|
|
361
852
|
|
|
362
|
-
|
|
853
|
+
Response guidelines:
|
|
854
|
+
- When presenting technology choices, call \`present_options\` with 2-3 options. Do NOT write numbered lists in text.
|
|
855
|
+
- Option labels: max 30 characters (just the name).
|
|
856
|
+
- Option descriptions: max 80 characters (one-line trade-off summary).
|
|
857
|
+
- Mark at most one option as recommended.
|
|
858
|
+
- After a user selects an option, confirm in one short sentence (max 60 chars) and call set_decision immediately.
|
|
859
|
+
- When answering questions, keep responses under 500 characters. Most answers should be 1-2 sentences. Only approach 500 chars for genuinely complex comparisons.
|
|
860
|
+
- Never congratulate or explain why a choice is great. Just confirm and move on.
|
|
861
|
+
${contextSection}
|
|
363
862
|
|
|
364
|
-
|
|
365
|
-
${
|
|
863
|
+
Guidelines:
|
|
864
|
+
- Focus on ${stageLabel}. Do not discuss other undecided stages.
|
|
865
|
+
- When the user has made their choice, call \`set_decision\` to commit it, then call \`summarize_stage\` to summarize what was decided.
|
|
866
|
+
- If this stage is not relevant to the project, briefly explain why and call \`summarize_stage\` to skip it.
|
|
867
|
+
- Do not ask the user to confirm each tool call \u2014 just make the calls naturally as decisions are reached.`;
|
|
366
868
|
}
|
|
367
869
|
function buildScaffoldPrompt(progress) {
|
|
368
870
|
return `You are scaffolding a new software project based on an approved plan.
|
|
@@ -375,6 +877,26 @@ Instructions:
|
|
|
375
877
|
2. After scaffolding, call \`add_integration\` for each integration (database, auth, payments, deployment config, extras) to write necessary files, install dependencies, and declare required environment variables.
|
|
376
878
|
3. Use MCP tools to look up current documentation for any libraries or frameworks you integrate, ensuring you use up-to-date APIs and configuration patterns.
|
|
377
879
|
4. Generate complete, working code \u2014 no stubs, no placeholders, no TODO comments. Every file should be production-ready.
|
|
880
|
+
5. Generate a \`deploy.sh\` script at the project root via \`add_integration\`. The script must:
|
|
881
|
+
- Start with \`set -euo pipefail\`
|
|
882
|
+
- Check that the required CLI tool is installed (exit 1 with a helpful message if not)
|
|
883
|
+
- Check authentication status (exit 1 with auth instructions if not)
|
|
884
|
+
- Print what it is about to do
|
|
885
|
+
- Execute the deploy command for the chosen platform
|
|
886
|
+
- Be 15-30 lines max \u2014 no elaborate bash framework
|
|
887
|
+
Use the \`scripts\` property of \`add_integration\` to add \`"deploy": "bash deploy.sh"\` to package.json.
|
|
888
|
+
6. As the LAST \`add_integration\` call, generate a comprehensive \`README.md\` with these sections:
|
|
889
|
+
- **Project title and description**
|
|
890
|
+
- **Tech stack overview** \u2014 what was chosen and why
|
|
891
|
+
- **Prerequisites** \u2014 Node.js version, required CLI tools
|
|
892
|
+
- **Local development setup** \u2014 clone, \`npm install\`, configure \`.env\` from \`.env.example\`, \`npm run dev\`
|
|
893
|
+
- **Environment variables** \u2014 table with: variable name, what it is for, where to get it, required vs optional. Clearly state that \`.env\` is for local development only.
|
|
894
|
+
- **Deployment** \u2014 platform-specific instructions:
|
|
895
|
+
- How to install and authenticate the platform CLI
|
|
896
|
+
- How to set env vars ON THE PLATFORM (e.g. \`vercel env add\`, \`gcloud run services update --set-env-vars\`, AWS Parameter Store). Production env vars are NOT set via \`.env\` \u2014 they are configured through platform-native tools.
|
|
897
|
+
- The deploy command: \`npm run deploy\`
|
|
898
|
+
- Post-deploy verification steps
|
|
899
|
+
- **Project structure** \u2014 brief description of key directories and files
|
|
378
900
|
|
|
379
901
|
Do not ask for confirmation. Proceed through all steps automatically.`;
|
|
380
902
|
}
|
|
@@ -383,73 +905,7 @@ Do not ask for confirmation. Proceed through all steps automatically.`;
|
|
|
383
905
|
import { execFileSync } from "child_process";
|
|
384
906
|
import { readdirSync, existsSync } from "fs";
|
|
385
907
|
import { join } from "path";
|
|
386
|
-
|
|
387
|
-
"create-next-app",
|
|
388
|
-
"create-vite",
|
|
389
|
-
"create-remix",
|
|
390
|
-
"create-svelte",
|
|
391
|
-
"create-astro",
|
|
392
|
-
"nuxi"
|
|
393
|
-
]);
|
|
394
|
-
var TOOL_FLAG_ALLOWLISTS = {
|
|
395
|
-
"create-next-app": /* @__PURE__ */ new Set([
|
|
396
|
-
"--typescript",
|
|
397
|
-
"--js",
|
|
398
|
-
"--tailwind",
|
|
399
|
-
"--no-tailwind",
|
|
400
|
-
"--eslint",
|
|
401
|
-
"--no-eslint",
|
|
402
|
-
"--app",
|
|
403
|
-
"--src-dir",
|
|
404
|
-
"--no-src-dir",
|
|
405
|
-
"--import-alias",
|
|
406
|
-
"--use-npm",
|
|
407
|
-
"--use-pnpm",
|
|
408
|
-
"--use-yarn",
|
|
409
|
-
"--use-bun"
|
|
410
|
-
]),
|
|
411
|
-
"create-vite": /* @__PURE__ */ new Set(["--template"])
|
|
412
|
-
};
|
|
413
|
-
var URL_SCHEME_RE = /https?:|git\+|file:/i;
|
|
414
|
-
var SHELL_META_RE = /[;&|`$(){}[\]<>!#~*?\n\r]/;
|
|
415
|
-
var WHITESPACE_RE = /\s/;
|
|
416
|
-
function validateScaffoldTool(tool, approvedTool) {
|
|
417
|
-
if (!TOOL_ALLOWLIST.has(tool)) {
|
|
418
|
-
throw new Error(`Scaffold tool not in allowlist: "${tool}"`);
|
|
419
|
-
}
|
|
420
|
-
if (tool !== approvedTool) {
|
|
421
|
-
throw new Error(`Scaffold tool "${tool}" does not match approved tool "${approvedTool}"`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function validateScaffoldArgs(tool, args) {
|
|
425
|
-
const strictAllowlist = TOOL_FLAG_ALLOWLISTS[tool];
|
|
426
|
-
for (const arg of args) {
|
|
427
|
-
if (URL_SCHEME_RE.test(arg)) {
|
|
428
|
-
throw new Error(`Scaffold arg contains a URL scheme: "${arg}"`);
|
|
429
|
-
}
|
|
430
|
-
if (WHITESPACE_RE.test(arg)) {
|
|
431
|
-
throw new Error(`Scaffold arg contains whitespace: "${arg}"`);
|
|
432
|
-
}
|
|
433
|
-
if (SHELL_META_RE.test(arg)) {
|
|
434
|
-
throw new Error(`Scaffold arg contains shell metacharacters: "${arg}"`);
|
|
435
|
-
}
|
|
436
|
-
if (strictAllowlist !== void 0) {
|
|
437
|
-
const isFlag = arg.startsWith("--");
|
|
438
|
-
if (isFlag && !strictAllowlist.has(arg)) {
|
|
439
|
-
throw new Error(`Scaffold arg "${arg}" is not in the allowlist for tool "${tool}"`);
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
if (!arg.startsWith("--")) {
|
|
443
|
-
throw new Error(
|
|
444
|
-
`Scaffold arg "${arg}" must start with "--" for tool "${tool}"`
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function runScaffold(tool, args, approvedTool, projectName, cwd) {
|
|
451
|
-
validateScaffoldTool(tool, approvedTool);
|
|
452
|
-
validateScaffoldArgs(tool, args);
|
|
908
|
+
function runScaffold(tool, args2, projectName, cwd) {
|
|
453
909
|
const outputDir = join(cwd, projectName);
|
|
454
910
|
if (existsSync(outputDir)) {
|
|
455
911
|
const entries = readdirSync(outputDir);
|
|
@@ -459,7 +915,7 @@ function runScaffold(tool, args, approvedTool, projectName, cwd) {
|
|
|
459
915
|
);
|
|
460
916
|
}
|
|
461
917
|
}
|
|
462
|
-
const spawnArgs = [`${tool}@latest`, projectName, ...
|
|
918
|
+
const spawnArgs = [`${tool}@latest`, projectName, ...args2];
|
|
463
919
|
const opts = { cwd, stdio: "pipe" };
|
|
464
920
|
execFileSync("npx", spawnArgs, opts);
|
|
465
921
|
return outputDir;
|
|
@@ -481,14 +937,14 @@ function validateFilePaths(projectRoot, files) {
|
|
|
481
937
|
}
|
|
482
938
|
}
|
|
483
939
|
function writeIntegration(projectDir, input) {
|
|
484
|
-
const { files, dependencies, devDependencies, envVars } = input;
|
|
940
|
+
const { files, dependencies, devDependencies, scripts, envVars } = input;
|
|
485
941
|
validateFilePaths(projectDir, files);
|
|
486
942
|
for (const [filePath, content] of Object.entries(files)) {
|
|
487
943
|
const fullPath = resolve(projectDir, filePath);
|
|
488
944
|
mkdirSync(dirname(fullPath), { recursive: true });
|
|
489
945
|
writeFileSync(fullPath, content, "utf8");
|
|
490
946
|
}
|
|
491
|
-
if (dependencies !== void 0 || devDependencies !== void 0) {
|
|
947
|
+
if (dependencies !== void 0 || devDependencies !== void 0 || scripts !== void 0) {
|
|
492
948
|
const pkgPath = resolve(projectDir, "package.json");
|
|
493
949
|
let pkg = {};
|
|
494
950
|
if (existsSync2(pkgPath)) {
|
|
@@ -506,6 +962,12 @@ function writeIntegration(projectDir, input) {
|
|
|
506
962
|
...devDependencies
|
|
507
963
|
};
|
|
508
964
|
}
|
|
965
|
+
if (scripts !== void 0) {
|
|
966
|
+
pkg.scripts = {
|
|
967
|
+
...pkg.scripts,
|
|
968
|
+
...scripts
|
|
969
|
+
};
|
|
970
|
+
}
|
|
509
971
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
510
972
|
}
|
|
511
973
|
if (envVars !== void 0 && envVars.length > 0) {
|
|
@@ -516,15 +978,20 @@ function writeIntegration(projectDir, input) {
|
|
|
516
978
|
}
|
|
517
979
|
|
|
518
980
|
// src/agent/loop.ts
|
|
519
|
-
async function
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
messages.
|
|
981
|
+
async function runStageLoop(stage, manager, bridge, mcpServers) {
|
|
982
|
+
const messages = manager.messages;
|
|
983
|
+
let progress = manager.progress;
|
|
984
|
+
if (messages.length === 0) {
|
|
985
|
+
messages.push({ role: "user", content: "I want to start a new project." });
|
|
986
|
+
}
|
|
987
|
+
let hasCalledSetDecision = false;
|
|
523
988
|
while (true) {
|
|
524
|
-
const system = buildConversationPrompt(progress);
|
|
989
|
+
const system = buildConversationPrompt(progress, stage.id, manager.stages);
|
|
525
990
|
let contentBlocks = [];
|
|
526
991
|
const collectedToolUse = [];
|
|
527
992
|
let hasText = false;
|
|
993
|
+
let fullText = "";
|
|
994
|
+
bridge.onSpinnerStart();
|
|
528
995
|
await chatStream(
|
|
529
996
|
{
|
|
530
997
|
system,
|
|
@@ -535,11 +1002,9 @@ async function runConversationLoop(mcpServers) {
|
|
|
535
1002
|
},
|
|
536
1003
|
{
|
|
537
1004
|
onText: (delta) => {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
writeText(delta);
|
|
1005
|
+
hasText = true;
|
|
1006
|
+
fullText += delta;
|
|
1007
|
+
bridge.onStreamText(delta);
|
|
543
1008
|
},
|
|
544
1009
|
onToolUse: (block) => {
|
|
545
1010
|
collectedToolUse.push(block);
|
|
@@ -550,18 +1015,31 @@ async function runConversationLoop(mcpServers) {
|
|
|
550
1015
|
}
|
|
551
1016
|
);
|
|
552
1017
|
if (hasText) {
|
|
553
|
-
|
|
554
|
-
writeLine();
|
|
1018
|
+
bridge.onStreamText("\n");
|
|
555
1019
|
}
|
|
556
1020
|
const toolUseBlocks = collectedToolUse;
|
|
557
1021
|
if (toolUseBlocks.length > 0) {
|
|
558
1022
|
messages.push({ role: "assistant", content: contentBlocks });
|
|
559
1023
|
const toolResults = [];
|
|
560
|
-
let hasPresentPlan = false;
|
|
561
1024
|
let hasSummarizeStage = false;
|
|
562
1025
|
let summarizeSummary = "";
|
|
1026
|
+
let madeDecision = false;
|
|
563
1027
|
for (const block of toolUseBlocks) {
|
|
564
1028
|
const toolBlock = block;
|
|
1029
|
+
if (toolBlock.name === "present_options") {
|
|
1030
|
+
const options = toolBlock.input.options;
|
|
1031
|
+
bridge.onPresentOptions(options);
|
|
1032
|
+
const input = await bridge.waitForInput();
|
|
1033
|
+
toolResults.push({
|
|
1034
|
+
type: "tool_result",
|
|
1035
|
+
tool_use_id: toolBlock.id,
|
|
1036
|
+
content: input.kind === "select" ? `User selected: ${input.value}` : input.kind === "text" ? `User wrote: ${input.value}` : "User cancelled."
|
|
1037
|
+
});
|
|
1038
|
+
if (input.kind === "cancel") {
|
|
1039
|
+
return { outcome: "cancel" };
|
|
1040
|
+
}
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
565
1043
|
const result = executeConversationTool(
|
|
566
1044
|
toolBlock.name,
|
|
567
1045
|
toolBlock.input,
|
|
@@ -569,13 +1047,15 @@ async function runConversationLoop(mcpServers) {
|
|
|
569
1047
|
messages
|
|
570
1048
|
);
|
|
571
1049
|
progress = result.progress;
|
|
1050
|
+
manager.progress = progress;
|
|
572
1051
|
toolResults.push({
|
|
573
1052
|
type: "tool_result",
|
|
574
1053
|
tool_use_id: toolBlock.id,
|
|
575
1054
|
content: result.response
|
|
576
1055
|
});
|
|
577
|
-
if (
|
|
578
|
-
|
|
1056
|
+
if (toolBlock.name === "set_decision" || toolBlock.name === "set_project_info") {
|
|
1057
|
+
madeDecision = true;
|
|
1058
|
+
hasCalledSetDecision = true;
|
|
579
1059
|
}
|
|
580
1060
|
if (toolBlock.name === "summarize_stage") {
|
|
581
1061
|
hasSummarizeStage = true;
|
|
@@ -583,37 +1063,39 @@ async function runConversationLoop(mcpServers) {
|
|
|
583
1063
|
}
|
|
584
1064
|
}
|
|
585
1065
|
messages.push({ role: "user", content: toolResults });
|
|
1066
|
+
if (madeDecision || hasSummarizeStage) {
|
|
1067
|
+
manager.save();
|
|
1068
|
+
}
|
|
586
1069
|
if (hasSummarizeStage) {
|
|
587
1070
|
const lastAssistant = messages[messages.length - 2];
|
|
588
1071
|
const lastUser = messages[messages.length - 1];
|
|
589
1072
|
messages.length = 0;
|
|
590
|
-
messages.push({
|
|
591
|
-
|
|
592
|
-
content: summarizeSummary
|
|
593
|
-
});
|
|
594
|
-
messages.push({
|
|
595
|
-
role: "user",
|
|
596
|
-
content: "[Continuing]"
|
|
597
|
-
});
|
|
1073
|
+
messages.push({ role: "assistant", content: summarizeSummary });
|
|
1074
|
+
messages.push({ role: "user", content: "[Continuing]" });
|
|
598
1075
|
messages.push(lastAssistant);
|
|
599
1076
|
messages.push(lastUser);
|
|
1077
|
+
manager.messages = messages;
|
|
600
1078
|
}
|
|
601
|
-
if (
|
|
602
|
-
|
|
603
|
-
|
|
1079
|
+
if (hasSummarizeStage) {
|
|
1080
|
+
if (hasCalledSetDecision || stage.id === "project_info") {
|
|
1081
|
+
return { outcome: "complete", summary: summarizeSummary };
|
|
1082
|
+
}
|
|
1083
|
+
return { outcome: "skipped" };
|
|
604
1084
|
}
|
|
605
1085
|
continue;
|
|
606
1086
|
}
|
|
607
|
-
|
|
608
|
-
|
|
1087
|
+
bridge.onStreamEnd(fullText);
|
|
1088
|
+
const inputResult = await bridge.waitForInput();
|
|
1089
|
+
if (inputResult.kind === "cancel") return { outcome: "cancel" };
|
|
1090
|
+
if (inputResult.kind === "navigate") return { outcome: "navigate" };
|
|
609
1091
|
messages.push({
|
|
610
1092
|
role: "assistant",
|
|
611
1093
|
content: contentBlocks
|
|
612
1094
|
});
|
|
613
|
-
messages.push({ role: "user", content:
|
|
1095
|
+
messages.push({ role: "user", content: inputResult.value });
|
|
614
1096
|
}
|
|
615
1097
|
}
|
|
616
|
-
async function runScaffoldLoop(progress, mcpServers) {
|
|
1098
|
+
async function runScaffoldLoop(progress, onProgress, mcpServers) {
|
|
617
1099
|
const messages = [];
|
|
618
1100
|
const system = buildScaffoldPrompt(progress);
|
|
619
1101
|
const cwd = process.cwd();
|
|
@@ -621,11 +1103,22 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
621
1103
|
const projectDir = join2(cwd, projectName);
|
|
622
1104
|
let toolCallCount = 0;
|
|
623
1105
|
const maxToolCalls = 30;
|
|
1106
|
+
const steps = [];
|
|
1107
|
+
function pushStep(step) {
|
|
1108
|
+
steps.push(step);
|
|
1109
|
+
onProgress?.([...steps]);
|
|
1110
|
+
}
|
|
1111
|
+
function updateLastStep(patch) {
|
|
1112
|
+
const last = steps[steps.length - 1];
|
|
1113
|
+
if (last) Object.assign(last, patch);
|
|
1114
|
+
onProgress?.([...steps]);
|
|
1115
|
+
}
|
|
624
1116
|
messages.push({
|
|
625
1117
|
role: "user",
|
|
626
1118
|
content: "Begin scaffolding the project according to the plan."
|
|
627
1119
|
});
|
|
628
1120
|
while (true) {
|
|
1121
|
+
pushStep({ name: "Planning next step...", status: "running" });
|
|
629
1122
|
const response = await chat({
|
|
630
1123
|
system,
|
|
631
1124
|
messages,
|
|
@@ -633,6 +1126,8 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
633
1126
|
maxTokens: 16384,
|
|
634
1127
|
mcpServers
|
|
635
1128
|
});
|
|
1129
|
+
steps.pop();
|
|
1130
|
+
onProgress?.([...steps]);
|
|
636
1131
|
const contentBlocks = response.content;
|
|
637
1132
|
const toolUseBlocks = contentBlocks.filter(
|
|
638
1133
|
(b) => b.type === "tool_use"
|
|
@@ -646,46 +1141,45 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
646
1141
|
const toolBlock = block;
|
|
647
1142
|
toolCallCount++;
|
|
648
1143
|
if (toolCallCount > maxToolCalls) {
|
|
649
|
-
|
|
1144
|
+
pushStep({ name: "Tool call limit exceeded", status: "error", error: `Exceeded ${maxToolCalls} tool calls` });
|
|
650
1145
|
return false;
|
|
651
1146
|
}
|
|
652
|
-
const spinner2 = createSpinner();
|
|
653
1147
|
try {
|
|
654
1148
|
if (toolBlock.name === "run_scaffold") {
|
|
655
|
-
|
|
656
|
-
const approvedTool = findApprovedScaffoldTool(progress);
|
|
1149
|
+
pushStep({ name: "Creating project", status: "running" });
|
|
657
1150
|
const outputDir = runScaffold(
|
|
658
1151
|
toolBlock.input.tool,
|
|
659
1152
|
toolBlock.input.args,
|
|
660
|
-
approvedTool,
|
|
661
1153
|
projectName,
|
|
662
1154
|
cwd
|
|
663
1155
|
);
|
|
664
|
-
|
|
1156
|
+
updateLastStep({ name: "Created project", status: "done" });
|
|
665
1157
|
toolResults.push({
|
|
666
1158
|
type: "tool_result",
|
|
667
1159
|
tool_use_id: toolBlock.id,
|
|
668
1160
|
content: `Scaffold completed. Project created at ${outputDir}`
|
|
669
1161
|
});
|
|
670
1162
|
} else if (toolBlock.name === "add_integration") {
|
|
671
|
-
const
|
|
1163
|
+
const files = Object.keys(
|
|
672
1164
|
toolBlock.input.files ?? {}
|
|
673
|
-
)
|
|
674
|
-
|
|
1165
|
+
);
|
|
1166
|
+
const integrationName = toolBlock.input.name ?? "Integration";
|
|
1167
|
+
pushStep({ name: `Adding ${integrationName}`, status: "running" });
|
|
675
1168
|
writeIntegration(projectDir, {
|
|
676
1169
|
files: toolBlock.input.files ?? {},
|
|
677
1170
|
dependencies: toolBlock.input.dependencies,
|
|
678
1171
|
devDependencies: toolBlock.input.devDependencies,
|
|
1172
|
+
scripts: toolBlock.input.scripts,
|
|
679
1173
|
envVars: toolBlock.input.envVars
|
|
680
1174
|
});
|
|
681
|
-
|
|
1175
|
+
updateLastStep({ name: integrationName, status: "done", files });
|
|
682
1176
|
toolResults.push({
|
|
683
1177
|
type: "tool_result",
|
|
684
1178
|
tool_use_id: toolBlock.id,
|
|
685
1179
|
content: "Integration written successfully."
|
|
686
1180
|
});
|
|
687
1181
|
} else {
|
|
688
|
-
|
|
1182
|
+
pushStep({ name: `Unknown tool: ${toolBlock.name}`, status: "error", error: `Unknown tool: "${toolBlock.name}"` });
|
|
689
1183
|
toolResults.push({
|
|
690
1184
|
type: "tool_result",
|
|
691
1185
|
tool_use_id: toolBlock.id,
|
|
@@ -695,7 +1189,7 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
695
1189
|
}
|
|
696
1190
|
} catch (err) {
|
|
697
1191
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
698
|
-
|
|
1192
|
+
updateLastStep({ status: "error", error: errorMessage });
|
|
699
1193
|
toolResults.push({
|
|
700
1194
|
type: "tool_result",
|
|
701
1195
|
tool_use_id: toolBlock.id,
|
|
@@ -707,59 +1201,854 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
707
1201
|
messages.push({ role: "user", content: toolResults });
|
|
708
1202
|
}
|
|
709
1203
|
}
|
|
710
|
-
|
|
1204
|
+
|
|
1205
|
+
// src/agent/recommend.ts
|
|
1206
|
+
var log2 = createLogger("recommend");
|
|
1207
|
+
var RECOMMEND_PROMPT = `You are a senior software architect. Based on the project description, recommend a complete technology stack.
|
|
1208
|
+
|
|
1209
|
+
For each category, provide your recommendation as a JSON object. If a category is not needed for this project, set it to null.
|
|
1210
|
+
|
|
1211
|
+
Respond with ONLY a JSON object in this exact format:
|
|
1212
|
+
{
|
|
1213
|
+
"frontend": { "component": "Next.js", "reasoning": "Best for SaaS with built-in API routes" },
|
|
1214
|
+
"backend": null,
|
|
1215
|
+
"database": { "component": "Postgres + Drizzle", "reasoning": "Relational with great TypeScript support" },
|
|
1216
|
+
"auth": { "component": "Clerk", "reasoning": "Drop-in auth with good DX" },
|
|
1217
|
+
"payments": null,
|
|
1218
|
+
"ai": null,
|
|
1219
|
+
"deployment": { "component": "Vercel", "reasoning": "Native Next.js support" },
|
|
1220
|
+
"extras": null
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
Rules:
|
|
1224
|
+
- Be opinionated. Pick the best option, not the most popular.
|
|
1225
|
+
- Set categories to null if they are genuinely not needed for this project.
|
|
1226
|
+
- Keep reasoning under 80 characters.
|
|
1227
|
+
- component names should be max 30 characters.`;
|
|
1228
|
+
async function getRecommendations(projectName, description) {
|
|
1229
|
+
try {
|
|
1230
|
+
const response = await chat({
|
|
1231
|
+
system: RECOMMEND_PROMPT,
|
|
1232
|
+
messages: [{
|
|
1233
|
+
role: "user",
|
|
1234
|
+
content: `Project: "${projectName}"
|
|
1235
|
+
Description: ${description}`
|
|
1236
|
+
}],
|
|
1237
|
+
maxTokens: 1024
|
|
1238
|
+
});
|
|
1239
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1240
|
+
log2.info({ rawLength: text.length }, "received recommendation response");
|
|
1241
|
+
log2.debug({ rawText: text }, "recommendation raw text");
|
|
1242
|
+
const jsonStr = text.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1243
|
+
const parsed = JSON.parse(jsonStr);
|
|
1244
|
+
const categories = Object.keys(parsed).filter((k) => parsed[k] !== null);
|
|
1245
|
+
log2.info({ recommended: categories, skipped: Object.keys(parsed).filter((k) => parsed[k] === null) }, "parsed recommendations");
|
|
1246
|
+
return parsed;
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
log2.error({ err }, "recommendation pass failed");
|
|
1249
|
+
return {};
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function applyRecommendations(progress, stages, recommendations) {
|
|
1253
|
+
let updated = { ...progress };
|
|
711
1254
|
const categories = [
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1255
|
+
"frontend",
|
|
1256
|
+
"backend",
|
|
1257
|
+
"database",
|
|
1258
|
+
"auth",
|
|
1259
|
+
"payments",
|
|
1260
|
+
"ai",
|
|
1261
|
+
"deployment"
|
|
719
1262
|
];
|
|
720
|
-
for (const
|
|
721
|
-
if (
|
|
722
|
-
|
|
1263
|
+
for (const category of categories) {
|
|
1264
|
+
if (!(category in recommendations)) continue;
|
|
1265
|
+
const rec = recommendations[category];
|
|
1266
|
+
const stage = stages.find((s) => s.id === category);
|
|
1267
|
+
if (!stage) continue;
|
|
1268
|
+
if (rec) {
|
|
1269
|
+
updated = setDecision(updated, category, {
|
|
1270
|
+
component: rec.component,
|
|
1271
|
+
reasoning: rec.reasoning
|
|
1272
|
+
});
|
|
1273
|
+
stage.status = "complete";
|
|
1274
|
+
stage.summary = rec.component;
|
|
1275
|
+
stage.confirmed = false;
|
|
1276
|
+
} else {
|
|
1277
|
+
stage.status = "skipped";
|
|
1278
|
+
stage.summary = "not needed";
|
|
1279
|
+
stage.confirmed = false;
|
|
723
1280
|
}
|
|
724
1281
|
}
|
|
725
|
-
|
|
1282
|
+
const extrasStage = stages.find((s) => s.id === "extras");
|
|
1283
|
+
if (extrasStage && !recommendations["extras"]) {
|
|
1284
|
+
extrasStage.status = "skipped";
|
|
1285
|
+
extrasStage.summary = "none suggested";
|
|
1286
|
+
extrasStage.confirmed = false;
|
|
1287
|
+
}
|
|
1288
|
+
return { progress: updated, stages };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/cli/app.tsx
|
|
1292
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1293
|
+
function App({ manager, onBuild, onExit }) {
|
|
1294
|
+
const app = useApp();
|
|
1295
|
+
const { width, height } = useScreenSize();
|
|
1296
|
+
const [view, setView] = useState5(
|
|
1297
|
+
manager.progress.projectName ? "stage_list" : "project_info"
|
|
1298
|
+
);
|
|
1299
|
+
const [bridge] = useState5(() => createBridge());
|
|
1300
|
+
const [currentStage, setCurrentStage] = useState5(manager.currentStage());
|
|
1301
|
+
const [progress, setProgress] = useState5(manager.progress);
|
|
1302
|
+
const [stages, setStages] = useState5([...manager.stages]);
|
|
1303
|
+
const [options, setOptions] = useState5([]);
|
|
1304
|
+
const [errorMsg, setErrorMsg] = useState5("");
|
|
1305
|
+
const [scaffoldSteps, setScaffoldSteps] = useState5([]);
|
|
1306
|
+
const syncState = useCallback(() => {
|
|
1307
|
+
setProgress({ ...manager.progress });
|
|
1308
|
+
setStages([...manager.stages]);
|
|
1309
|
+
setCurrentStage(manager.currentStage());
|
|
1310
|
+
}, [manager]);
|
|
1311
|
+
useEffect2(() => {
|
|
1312
|
+
const unsubs = [
|
|
1313
|
+
bridge.subscribe("presentOptions", (opts) => {
|
|
1314
|
+
setOptions(opts);
|
|
1315
|
+
setView("options");
|
|
1316
|
+
}),
|
|
1317
|
+
bridge.subscribe("streamEnd", () => {
|
|
1318
|
+
setView("input");
|
|
1319
|
+
}),
|
|
1320
|
+
bridge.subscribe("spinnerStart", () => {
|
|
1321
|
+
setView("conversation");
|
|
1322
|
+
}),
|
|
1323
|
+
bridge.subscribe("error", (err) => {
|
|
1324
|
+
setErrorMsg(err.message);
|
|
1325
|
+
setView("error");
|
|
1326
|
+
})
|
|
1327
|
+
];
|
|
1328
|
+
return () => unsubs.forEach((fn) => fn());
|
|
1329
|
+
}, [bridge]);
|
|
1330
|
+
useInput3((_input, key) => {
|
|
1331
|
+
if (key.escape && view !== "stage_list") {
|
|
1332
|
+
bridge.resolveInput({ kind: "cancel" });
|
|
1333
|
+
setView("stage_list");
|
|
1334
|
+
syncState();
|
|
1335
|
+
}
|
|
1336
|
+
}, { isActive: view === "conversation" || view === "error" });
|
|
1337
|
+
const runStage = useCallback(async (stageId) => {
|
|
1338
|
+
const stage = manager.stages.find((s) => s.id === stageId);
|
|
1339
|
+
if (!stage) return;
|
|
1340
|
+
setCurrentStage(stage);
|
|
1341
|
+
setView("conversation");
|
|
1342
|
+
try {
|
|
1343
|
+
const result = await runStageLoop(stage, manager, bridge);
|
|
1344
|
+
switch (result.outcome) {
|
|
1345
|
+
case "complete":
|
|
1346
|
+
manager.completeStage(stageId, result.summary);
|
|
1347
|
+
const completedStage = manager.stages.find((s) => s.id === stageId);
|
|
1348
|
+
if (completedStage) completedStage.confirmed = true;
|
|
1349
|
+
manager.save();
|
|
1350
|
+
break;
|
|
1351
|
+
case "skipped":
|
|
1352
|
+
manager.skipStage(stageId);
|
|
1353
|
+
manager.save();
|
|
1354
|
+
break;
|
|
1355
|
+
case "cancel":
|
|
1356
|
+
manager.restorePendingNavigation();
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
setErrorMsg(err.message);
|
|
1361
|
+
setView("error");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
syncState();
|
|
1365
|
+
setView("stage_list");
|
|
1366
|
+
}, [manager, bridge, syncState]);
|
|
1367
|
+
const handleStageResult = useCallback(async (result) => {
|
|
1368
|
+
if (result.kind === "select") {
|
|
1369
|
+
const stage = manager.stages.find((s) => s.id === result.stageId);
|
|
1370
|
+
if (stage && (stage.status === "complete" || stage.status === "skipped")) {
|
|
1371
|
+
manager.navigateTo(result.stageId);
|
|
1372
|
+
syncState();
|
|
1373
|
+
}
|
|
1374
|
+
runStage(result.stageId);
|
|
1375
|
+
} else if (result.kind === "build") {
|
|
1376
|
+
setView("scaffold");
|
|
1377
|
+
setScaffoldSteps([]);
|
|
1378
|
+
const onScaffoldProgress = (steps) => {
|
|
1379
|
+
setScaffoldSteps([...steps]);
|
|
1380
|
+
};
|
|
1381
|
+
try {
|
|
1382
|
+
const success = await runScaffoldLoop(manager.progress, onScaffoldProgress);
|
|
1383
|
+
if (success) {
|
|
1384
|
+
manager.cleanup();
|
|
1385
|
+
onBuild();
|
|
1386
|
+
} else {
|
|
1387
|
+
onBuild();
|
|
1388
|
+
}
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
setErrorMsg(err.message);
|
|
1391
|
+
setView("error");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
app.exit();
|
|
1395
|
+
} else if (result.kind === "cancel") {
|
|
1396
|
+
manager.save();
|
|
1397
|
+
onExit();
|
|
1398
|
+
app.exit();
|
|
1399
|
+
}
|
|
1400
|
+
}, [manager, runStage, syncState, onBuild, onExit, app]);
|
|
1401
|
+
const handleOptionSelect = useCallback((result) => {
|
|
1402
|
+
setView("conversation");
|
|
1403
|
+
bridge.resolveInput(result);
|
|
1404
|
+
}, [bridge]);
|
|
1405
|
+
const handleTextSubmit = useCallback((value) => {
|
|
1406
|
+
setView("conversation");
|
|
1407
|
+
bridge.resolveInput({ kind: "text", value });
|
|
1408
|
+
}, [bridge]);
|
|
1409
|
+
const handleProjectInfo = useCallback(async (name, description) => {
|
|
1410
|
+
manager.progress = {
|
|
1411
|
+
...manager.progress,
|
|
1412
|
+
projectName: name,
|
|
1413
|
+
description
|
|
1414
|
+
};
|
|
1415
|
+
const stage = manager.stages.find((s) => s.id === "project_info");
|
|
1416
|
+
if (stage) {
|
|
1417
|
+
stage.status = "complete";
|
|
1418
|
+
stage.confirmed = true;
|
|
1419
|
+
stage.summary = `${name}: ${description}`;
|
|
1420
|
+
}
|
|
1421
|
+
manager.save();
|
|
1422
|
+
syncState();
|
|
1423
|
+
setView("loading");
|
|
1424
|
+
try {
|
|
1425
|
+
const recommendations = await getRecommendations(name, description);
|
|
1426
|
+
const { progress: updatedProgress } = applyRecommendations(
|
|
1427
|
+
manager.progress,
|
|
1428
|
+
manager.stages,
|
|
1429
|
+
recommendations
|
|
1430
|
+
);
|
|
1431
|
+
manager.progress = updatedProgress;
|
|
1432
|
+
manager.save();
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
console.error("Recommendation pass failed:", err);
|
|
1435
|
+
}
|
|
1436
|
+
syncState();
|
|
1437
|
+
setView("stage_list");
|
|
1438
|
+
}, [manager, syncState]);
|
|
1439
|
+
const footerMode = view === "scaffold" ? "scaffold" : view === "stage_list" ? "stage_list" : view === "options" ? "options" : view === "input" || view === "project_info" ? "input" : "decisions";
|
|
1440
|
+
const contentHeight = height - 4;
|
|
1441
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", width, height, children: [
|
|
1442
|
+
/* @__PURE__ */ jsx8(
|
|
1443
|
+
Header,
|
|
1444
|
+
{
|
|
1445
|
+
appName: "stack-agent",
|
|
1446
|
+
currentStage: view === "stage_list" || view === "scaffold" ? null : currentStage,
|
|
1447
|
+
stages,
|
|
1448
|
+
showDots: view !== "stage_list" && view !== "scaffold",
|
|
1449
|
+
title: view === "scaffold" ? "Scaffolding" : void 0
|
|
1450
|
+
}
|
|
1451
|
+
),
|
|
1452
|
+
/* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", height: contentHeight, paddingX: 1, borderStyle: "single", borderTop: false, borderBottom: false, children: [
|
|
1453
|
+
view === "project_info" && /* @__PURE__ */ jsx8(ProjectInfoForm, { onSubmit: handleProjectInfo }),
|
|
1454
|
+
view === "loading" && /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Spinner3, { label: "Analyzing your project and recommending a stack..." }) }),
|
|
1455
|
+
view === "stage_list" && /* @__PURE__ */ jsx8(
|
|
1456
|
+
StageListView,
|
|
1457
|
+
{
|
|
1458
|
+
stages,
|
|
1459
|
+
currentStageId: currentStage?.id ?? null,
|
|
1460
|
+
progress,
|
|
1461
|
+
onResult: handleStageResult
|
|
1462
|
+
}
|
|
1463
|
+
),
|
|
1464
|
+
view === "conversation" && /* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight }),
|
|
1465
|
+
view === "input" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1466
|
+
/* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight - 3 }),
|
|
1467
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
|
|
1468
|
+
TextInput2,
|
|
1469
|
+
{
|
|
1470
|
+
placeholder: "Type your response...",
|
|
1471
|
+
onSubmit: handleTextSubmit
|
|
1472
|
+
}
|
|
1473
|
+
) })
|
|
1474
|
+
] }),
|
|
1475
|
+
view === "options" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1476
|
+
/* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight - 8 }),
|
|
1477
|
+
/* @__PURE__ */ jsx8(OptionSelect, { options, onSelect: handleOptionSelect })
|
|
1478
|
+
] }),
|
|
1479
|
+
view === "error" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1480
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "red", bold: true, children: [
|
|
1481
|
+
"Error: ",
|
|
1482
|
+
errorMsg
|
|
1483
|
+
] }),
|
|
1484
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press Esc to return to stage list" })
|
|
1485
|
+
] }),
|
|
1486
|
+
view === "scaffold" && /* @__PURE__ */ jsx8(ScaffoldView, { steps: scaffoldSteps })
|
|
1487
|
+
] }),
|
|
1488
|
+
/* @__PURE__ */ jsx8(
|
|
1489
|
+
Footer,
|
|
1490
|
+
{
|
|
1491
|
+
progress,
|
|
1492
|
+
stages,
|
|
1493
|
+
terminalWidth: width,
|
|
1494
|
+
mode: footerMode
|
|
1495
|
+
}
|
|
1496
|
+
)
|
|
1497
|
+
] });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/agent/stage-manager.ts
|
|
1501
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync, existsSync as existsSync3 } from "fs";
|
|
1502
|
+
import { join as join3 } from "path";
|
|
1503
|
+
var SESSION_FILE = ".stack-agent.json";
|
|
1504
|
+
var SESSION_VERSION = 1;
|
|
1505
|
+
var StageManager = class _StageManager {
|
|
1506
|
+
session;
|
|
1507
|
+
filePath;
|
|
1508
|
+
onInvalidate;
|
|
1509
|
+
pendingNavigation;
|
|
1510
|
+
constructor(session, cwd, onInvalidate) {
|
|
1511
|
+
this.session = session;
|
|
1512
|
+
this.filePath = join3(cwd, SESSION_FILE);
|
|
1513
|
+
this.onInvalidate = onInvalidate;
|
|
1514
|
+
}
|
|
1515
|
+
static start(cwd, onInvalidate) {
|
|
1516
|
+
const session = {
|
|
1517
|
+
version: SESSION_VERSION,
|
|
1518
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1519
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1520
|
+
progress: createProgress(),
|
|
1521
|
+
stages: structuredClone(DEFAULT_STAGES),
|
|
1522
|
+
messages: []
|
|
1523
|
+
};
|
|
1524
|
+
return new _StageManager(session, cwd, onInvalidate);
|
|
1525
|
+
}
|
|
1526
|
+
static resume(cwd, onInvalidate) {
|
|
1527
|
+
const session = _StageManager.detect(cwd);
|
|
1528
|
+
if (!session) return null;
|
|
1529
|
+
return new _StageManager(session, cwd, onInvalidate);
|
|
1530
|
+
}
|
|
1531
|
+
static detect(cwd) {
|
|
1532
|
+
const filePath = join3(cwd, SESSION_FILE);
|
|
1533
|
+
if (!existsSync3(filePath)) return null;
|
|
1534
|
+
try {
|
|
1535
|
+
const json = readFileSync2(filePath, "utf-8");
|
|
1536
|
+
return deserializeSession(json);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
// --- Stage navigation ---
|
|
1542
|
+
currentStage() {
|
|
1543
|
+
return this.session.stages.find((s) => s.status === "pending") ?? null;
|
|
1544
|
+
}
|
|
1545
|
+
completeStage(id, summary) {
|
|
1546
|
+
const stage = this.session.stages.find((s) => s.id === id);
|
|
1547
|
+
if (!stage) return;
|
|
1548
|
+
stage.status = "complete";
|
|
1549
|
+
stage.summary = summary;
|
|
1550
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1551
|
+
if (this.pendingNavigation?.stageId === id) {
|
|
1552
|
+
this.pendingNavigation = void 0;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
skipStage(id) {
|
|
1556
|
+
const stage = this.session.stages.find((s) => s.id === id);
|
|
1557
|
+
if (!stage) return;
|
|
1558
|
+
stage.status = "skipped";
|
|
1559
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1560
|
+
}
|
|
1561
|
+
navigateTo(id) {
|
|
1562
|
+
const stage = this.session.stages.find((s) => s.id === id);
|
|
1563
|
+
if (!stage) return;
|
|
1564
|
+
if (stage.status === "complete") {
|
|
1565
|
+
if (id === "project_info") {
|
|
1566
|
+
this.pendingNavigation = {
|
|
1567
|
+
stageId: id,
|
|
1568
|
+
oldValue: null,
|
|
1569
|
+
oldProjectInfo: {
|
|
1570
|
+
projectName: this.session.progress.projectName,
|
|
1571
|
+
description: this.session.progress.description
|
|
1572
|
+
},
|
|
1573
|
+
oldSummary: stage.summary
|
|
1574
|
+
};
|
|
1575
|
+
} else {
|
|
1576
|
+
const key = stage.progressKeys[0];
|
|
1577
|
+
const oldValue = key === "extras" ? null : this.session.progress[key] ?? null;
|
|
1578
|
+
this.pendingNavigation = {
|
|
1579
|
+
stageId: id,
|
|
1580
|
+
oldValue,
|
|
1581
|
+
oldSummary: stage.summary
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
stage.status = "pending";
|
|
1586
|
+
stage.summary = void 0;
|
|
1587
|
+
}
|
|
1588
|
+
restorePendingNavigation() {
|
|
1589
|
+
if (!this.pendingNavigation) return;
|
|
1590
|
+
const { stageId, oldValue, oldProjectInfo, oldSummary } = this.pendingNavigation;
|
|
1591
|
+
const stage = this.session.stages.find((s) => s.id === stageId);
|
|
1592
|
+
if (!stage) return;
|
|
1593
|
+
if (stageId === "project_info" && oldProjectInfo) {
|
|
1594
|
+
this.session.progress = {
|
|
1595
|
+
...this.session.progress,
|
|
1596
|
+
projectName: oldProjectInfo.projectName,
|
|
1597
|
+
description: oldProjectInfo.description
|
|
1598
|
+
};
|
|
1599
|
+
} else if (stageId !== "project_info") {
|
|
1600
|
+
const category = stageId;
|
|
1601
|
+
if (oldValue) {
|
|
1602
|
+
this.session.progress = { ...this.session.progress, [category]: oldValue };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
stage.status = "complete";
|
|
1606
|
+
stage.summary = oldSummary;
|
|
1607
|
+
this.pendingNavigation = void 0;
|
|
1608
|
+
}
|
|
1609
|
+
isNavigating() {
|
|
1610
|
+
return this.pendingNavigation !== void 0;
|
|
1611
|
+
}
|
|
1612
|
+
getPendingOldValue() {
|
|
1613
|
+
return this.pendingNavigation?.oldValue ?? null;
|
|
1614
|
+
}
|
|
1615
|
+
// --- Dynamic stages ---
|
|
1616
|
+
addStage(entry, afterId) {
|
|
1617
|
+
const idx = this.session.stages.findIndex((s) => s.id === afterId);
|
|
1618
|
+
if (idx === -1) {
|
|
1619
|
+
this.session.stages.push(entry);
|
|
1620
|
+
} else {
|
|
1621
|
+
this.session.stages.splice(idx + 1, 0, entry);
|
|
1622
|
+
}
|
|
1623
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1624
|
+
}
|
|
1625
|
+
removeStage(id) {
|
|
1626
|
+
this.session.stages = this.session.stages.filter((s) => s.id !== id);
|
|
1627
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1628
|
+
}
|
|
1629
|
+
// --- Cascading invalidation ---
|
|
1630
|
+
async invalidateAfter(changedId, oldValue) {
|
|
1631
|
+
if (!this.onInvalidate) return;
|
|
1632
|
+
const stage = this.session.stages.find((s) => s.id === changedId);
|
|
1633
|
+
if (!stage) return;
|
|
1634
|
+
const key = stage.progressKeys[0];
|
|
1635
|
+
const newValue = key === "extras" || key === "projectName" || key === "description" ? null : this.session.progress[key] ?? null;
|
|
1636
|
+
const result = await this.onInvalidate(
|
|
1637
|
+
changedId,
|
|
1638
|
+
oldValue,
|
|
1639
|
+
newValue,
|
|
1640
|
+
this.session.progress,
|
|
1641
|
+
this.session.stages
|
|
1642
|
+
);
|
|
1643
|
+
const changedIdx = this.session.stages.findIndex((s) => s.id === changedId);
|
|
1644
|
+
for (const clearId of result.clear) {
|
|
1645
|
+
const clearIdx = this.session.stages.findIndex((s) => s.id === clearId);
|
|
1646
|
+
if (clearIdx <= changedIdx) continue;
|
|
1647
|
+
const clearStage = this.session.stages[clearIdx];
|
|
1648
|
+
if (!clearStage) continue;
|
|
1649
|
+
for (const pKey of clearStage.progressKeys) {
|
|
1650
|
+
if (pKey === "projectName" || pKey === "description") {
|
|
1651
|
+
this.session.progress = clearProjectInfo(this.session.progress);
|
|
1652
|
+
} else if (pKey === "extras") {
|
|
1653
|
+
this.session.progress = clearDecision(this.session.progress, "extras");
|
|
1654
|
+
} else {
|
|
1655
|
+
this.session.progress = clearDecision(this.session.progress, pKey);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
clearStage.status = "pending";
|
|
1659
|
+
clearStage.summary = void 0;
|
|
1660
|
+
}
|
|
1661
|
+
for (const removeId of result.remove) {
|
|
1662
|
+
const removeIdx = this.session.stages.findIndex((s) => s.id === removeId);
|
|
1663
|
+
if (removeIdx <= changedIdx) continue;
|
|
1664
|
+
this.removeStage(removeId);
|
|
1665
|
+
}
|
|
1666
|
+
let insertAfterId = changedId;
|
|
1667
|
+
for (const newStage of result.add) {
|
|
1668
|
+
this.addStage(newStage, insertAfterId);
|
|
1669
|
+
insertAfterId = newStage.id;
|
|
1670
|
+
}
|
|
1671
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1672
|
+
}
|
|
1673
|
+
// --- Persistence ---
|
|
1674
|
+
save() {
|
|
1675
|
+
this.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1676
|
+
const json = serializeSession(this.session);
|
|
1677
|
+
if (json.length > 5e5) {
|
|
1678
|
+
console.warn("Warning: session file exceeds 500KB. Consider summarizing more aggressively.");
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
const tmpPath = this.filePath + ".tmp";
|
|
1682
|
+
writeFileSync2(tmpPath, json, "utf-8");
|
|
1683
|
+
renameSync(tmpPath, this.filePath);
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
console.warn(`Warning: failed to save session: ${err instanceof Error ? err.message : String(err)}`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
cleanup() {
|
|
1689
|
+
try {
|
|
1690
|
+
if (existsSync3(this.filePath)) unlinkSync(this.filePath);
|
|
1691
|
+
const tmpPath = this.filePath + ".tmp";
|
|
1692
|
+
if (existsSync3(tmpPath)) unlinkSync(tmpPath);
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
// --- Accessors ---
|
|
1697
|
+
get progress() {
|
|
1698
|
+
return this.session.progress;
|
|
1699
|
+
}
|
|
1700
|
+
set progress(value) {
|
|
1701
|
+
this.session.progress = value;
|
|
1702
|
+
}
|
|
1703
|
+
get messages() {
|
|
1704
|
+
return this.session.messages;
|
|
1705
|
+
}
|
|
1706
|
+
set messages(value) {
|
|
1707
|
+
this.session.messages = value;
|
|
1708
|
+
}
|
|
1709
|
+
get stages() {
|
|
1710
|
+
return this.session.stages;
|
|
1711
|
+
}
|
|
1712
|
+
get updatedAt() {
|
|
1713
|
+
return this.session.updatedAt;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
// src/cli/chat.ts
|
|
1718
|
+
import { Marked } from "marked";
|
|
1719
|
+
import { markedTerminal } from "marked-terminal";
|
|
1720
|
+
var marked = new Marked(markedTerminal());
|
|
1721
|
+
function renderPostScaffold(projectName, readiness) {
|
|
1722
|
+
const localSteps = [
|
|
1723
|
+
`cd ${projectName}`,
|
|
1724
|
+
"cp .env.example .env # fill in your values",
|
|
1725
|
+
"npm install",
|
|
1726
|
+
"npm run dev"
|
|
1727
|
+
];
|
|
1728
|
+
console.log("Local Development\n " + localSteps.join("\n "));
|
|
1729
|
+
if (readiness === null) return;
|
|
1730
|
+
const lines = [];
|
|
1731
|
+
if (readiness.cliInstalled && readiness.authenticated === true) {
|
|
1732
|
+
lines.push("\u2713 Ready to deploy");
|
|
1733
|
+
lines.push(`\u2192 ${readiness.deployCmd}`);
|
|
1734
|
+
} else if (!readiness.cliInstalled) {
|
|
1735
|
+
lines.push(`\u2717 ${readiness.cliName || "CLI"} not found`);
|
|
1736
|
+
if (readiness.installCmd) lines.push(` Install: ${readiness.installCmd}`);
|
|
1737
|
+
if (readiness.authCmd) lines.push(` Then: ${readiness.authCmd}`);
|
|
1738
|
+
lines.push(` Then: ${readiness.deployCmd}`);
|
|
1739
|
+
} else {
|
|
1740
|
+
lines.push(`\u2713 ${readiness.cliName} CLI installed`);
|
|
1741
|
+
if (readiness.authenticated === false) {
|
|
1742
|
+
lines.push(`\u2717 Not authenticated`);
|
|
1743
|
+
lines.push(` Run: ${readiness.authCmd}`);
|
|
1744
|
+
} else {
|
|
1745
|
+
lines.push("? Authentication status unknown");
|
|
1746
|
+
lines.push(` Try: ${readiness.authCmd}`);
|
|
1747
|
+
}
|
|
1748
|
+
lines.push(` Then: ${readiness.deployCmd}`);
|
|
1749
|
+
}
|
|
1750
|
+
lines.push("");
|
|
1751
|
+
if (readiness.envVarCmd) {
|
|
1752
|
+
lines.push(`\u2139 Set production env vars with: ${readiness.envVarCmd}`);
|
|
1753
|
+
}
|
|
1754
|
+
lines.push("\u2139 See README.md \u2192 Deployment for full instructions");
|
|
1755
|
+
console.log(`Deployment (${readiness.platform})
|
|
1756
|
+
${lines.join("\n ")}`);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/deploy/readiness.ts
|
|
1760
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1761
|
+
var PLATFORMS = [
|
|
1762
|
+
{
|
|
1763
|
+
platform: "AWS Amplify",
|
|
1764
|
+
cliName: "amplify",
|
|
1765
|
+
cliBinary: "amplify",
|
|
1766
|
+
authCheckCmd: ["amplify", "status"],
|
|
1767
|
+
installCmd: "npm i -g @aws-amplify/cli",
|
|
1768
|
+
authCmd: "amplify configure",
|
|
1769
|
+
deployCmd: "npm run deploy",
|
|
1770
|
+
envVarCmd: "See AWS Amplify console for environment variables"
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
platform: "AWS CDK",
|
|
1774
|
+
cliName: "cdk",
|
|
1775
|
+
cliBinary: "cdk",
|
|
1776
|
+
authCheckCmd: ["aws", "sts", "get-caller-identity"],
|
|
1777
|
+
installCmd: "npm i -g aws-cdk",
|
|
1778
|
+
authCmd: "aws configure",
|
|
1779
|
+
deployCmd: "npm run deploy",
|
|
1780
|
+
envVarCmd: "aws ssm put-parameter --name KEY --value VAL --type String"
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
platform: "AWS SST",
|
|
1784
|
+
cliName: "sst",
|
|
1785
|
+
cliBinary: "npx",
|
|
1786
|
+
authCheckCmd: ["aws", "sts", "get-caller-identity"],
|
|
1787
|
+
installCmd: "(uses npx \u2014 no global install needed)",
|
|
1788
|
+
authCmd: "aws configure",
|
|
1789
|
+
deployCmd: "npm run deploy",
|
|
1790
|
+
envVarCmd: "aws ssm put-parameter --name KEY --value VAL --type String"
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
platform: "Vercel",
|
|
1794
|
+
cliName: "vercel",
|
|
1795
|
+
cliBinary: "vercel",
|
|
1796
|
+
authCheckCmd: ["vercel", "whoami"],
|
|
1797
|
+
installCmd: "npm i -g vercel",
|
|
1798
|
+
authCmd: "vercel login",
|
|
1799
|
+
deployCmd: "npm run deploy",
|
|
1800
|
+
envVarCmd: "vercel env add"
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
platform: "AWS",
|
|
1804
|
+
cliName: "aws",
|
|
1805
|
+
cliBinary: "aws",
|
|
1806
|
+
authCheckCmd: ["aws", "sts", "get-caller-identity"],
|
|
1807
|
+
installCmd: "See https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html",
|
|
1808
|
+
authCmd: "aws configure",
|
|
1809
|
+
deployCmd: "npm run deploy",
|
|
1810
|
+
envVarCmd: "aws ssm put-parameter --name KEY --value VAL --type String"
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
platform: "GCP",
|
|
1814
|
+
cliName: "gcloud",
|
|
1815
|
+
cliBinary: "gcloud",
|
|
1816
|
+
authCheckCmd: ["gcloud", "auth", "print-identity-token"],
|
|
1817
|
+
installCmd: "See https://cloud.google.com/sdk/docs/install",
|
|
1818
|
+
authCmd: "gcloud auth login",
|
|
1819
|
+
deployCmd: "npm run deploy",
|
|
1820
|
+
envVarCmd: "gcloud run services update SERVICE --set-env-vars KEY=VAL"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
platform: "Docker",
|
|
1824
|
+
cliName: "docker",
|
|
1825
|
+
cliBinary: "docker",
|
|
1826
|
+
authCheckCmd: ["docker", "info"],
|
|
1827
|
+
installCmd: "See https://docs.docker.com/get-docker/",
|
|
1828
|
+
authCmd: "docker login",
|
|
1829
|
+
deployCmd: "npm run deploy",
|
|
1830
|
+
envVarCmd: "Set variables in docker-compose.yml or .env"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
platform: "Railway",
|
|
1834
|
+
cliName: "railway",
|
|
1835
|
+
cliBinary: "railway",
|
|
1836
|
+
authCheckCmd: ["railway", "whoami"],
|
|
1837
|
+
installCmd: "npm i -g @railway/cli",
|
|
1838
|
+
authCmd: "railway login",
|
|
1839
|
+
deployCmd: "npm run deploy",
|
|
1840
|
+
envVarCmd: "railway variables set KEY=VAL"
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
platform: "Fly.io",
|
|
1844
|
+
cliName: "fly",
|
|
1845
|
+
cliBinary: "fly",
|
|
1846
|
+
authCheckCmd: ["fly", "auth", "whoami"],
|
|
1847
|
+
installCmd: "See https://fly.io/docs/flyctl/install/",
|
|
1848
|
+
authCmd: "fly auth login",
|
|
1849
|
+
deployCmd: "npm run deploy",
|
|
1850
|
+
envVarCmd: "fly secrets set KEY=VAL"
|
|
1851
|
+
}
|
|
1852
|
+
];
|
|
1853
|
+
var KEYWORD_MATCHERS = (() => {
|
|
1854
|
+
const byPlatform = (name) => PLATFORMS.find((p) => p.platform === name);
|
|
1855
|
+
return [
|
|
1856
|
+
{ test: (s) => s.includes("amplify"), platform: byPlatform("AWS Amplify") },
|
|
1857
|
+
{ test: (s) => s.includes("cdk"), platform: byPlatform("AWS CDK") },
|
|
1858
|
+
{ test: (s) => s.includes("sst"), platform: byPlatform("AWS SST") },
|
|
1859
|
+
{ test: (s) => s.includes("vercel"), platform: byPlatform("Vercel") },
|
|
1860
|
+
{ test: (s) => s.includes("aws") || s.includes("lambda") || s.includes("ec2"), platform: byPlatform("AWS") },
|
|
1861
|
+
{ test: (s) => s.includes("gcp") || s.includes("google cloud") || s.includes("cloud run"), platform: byPlatform("GCP") },
|
|
1862
|
+
{ test: (s) => s.includes("docker") || s.includes("container"), platform: byPlatform("Docker") },
|
|
1863
|
+
{ test: (s) => s.includes("railway"), platform: byPlatform("Railway") },
|
|
1864
|
+
{ test: (s) => s.includes("fly.io") || s === "fly" || s.startsWith("fly "), platform: byPlatform("Fly.io") }
|
|
1865
|
+
];
|
|
1866
|
+
})();
|
|
1867
|
+
var FALLBACK_CONFIG = {
|
|
1868
|
+
platform: "Unknown",
|
|
1869
|
+
cliName: "",
|
|
1870
|
+
cliBinary: "",
|
|
1871
|
+
authCheckCmd: [],
|
|
1872
|
+
installCmd: "",
|
|
1873
|
+
authCmd: "",
|
|
1874
|
+
deployCmd: "npm run deploy",
|
|
1875
|
+
envVarCmd: "See README.md for environment variable instructions"
|
|
1876
|
+
};
|
|
1877
|
+
function normalizePlatform(deploymentComponent) {
|
|
1878
|
+
const lower = deploymentComponent.toLowerCase();
|
|
1879
|
+
for (const matcher of KEYWORD_MATCHERS) {
|
|
1880
|
+
if (matcher.test(lower)) {
|
|
1881
|
+
return matcher.platform;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return FALLBACK_CONFIG;
|
|
1885
|
+
}
|
|
1886
|
+
function isCliInstalled(binary) {
|
|
1887
|
+
try {
|
|
1888
|
+
execFileSync2("which", [binary], { stdio: "pipe", timeout: 5e3 });
|
|
1889
|
+
return true;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function checkAuth(cmd) {
|
|
1895
|
+
if (cmd.length === 0) return null;
|
|
1896
|
+
try {
|
|
1897
|
+
execFileSync2(cmd[0], cmd.slice(1), { stdio: "pipe", timeout: 5e3 });
|
|
1898
|
+
return true;
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
const nodeErr = err;
|
|
1901
|
+
if (nodeErr.code === "ENOENT" || nodeErr.code === "ETIMEDOUT") {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function checkDeployReadiness(deploymentComponent) {
|
|
1908
|
+
const config = normalizePlatform(deploymentComponent);
|
|
1909
|
+
if (config.platform === "Unknown" || config.cliBinary === "") {
|
|
1910
|
+
return {
|
|
1911
|
+
platform: config.platform,
|
|
1912
|
+
cliInstalled: false,
|
|
1913
|
+
cliName: config.cliName,
|
|
1914
|
+
authenticated: null,
|
|
1915
|
+
installCmd: config.installCmd,
|
|
1916
|
+
authCmd: config.authCmd,
|
|
1917
|
+
deployCmd: config.deployCmd,
|
|
1918
|
+
envVarCmd: config.envVarCmd
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const cliInstalled = isCliInstalled(config.cliBinary);
|
|
1922
|
+
let authenticated = null;
|
|
1923
|
+
if (cliInstalled) {
|
|
1924
|
+
authenticated = checkAuth(config.authCheckCmd);
|
|
1925
|
+
}
|
|
1926
|
+
return {
|
|
1927
|
+
platform: config.platform,
|
|
1928
|
+
cliInstalled,
|
|
1929
|
+
cliName: config.cliName,
|
|
1930
|
+
authenticated,
|
|
1931
|
+
installCmd: config.installCmd,
|
|
1932
|
+
authCmd: config.authCmd,
|
|
1933
|
+
deployCmd: config.deployCmd,
|
|
1934
|
+
envVarCmd: config.envVarCmd
|
|
1935
|
+
};
|
|
726
1936
|
}
|
|
727
1937
|
|
|
728
1938
|
// src/index.ts
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1939
|
+
var INVALIDATION_PROMPT = `You are evaluating whether changing a technology stack decision affects other decisions.
|
|
1940
|
+
|
|
1941
|
+
The user changed their decision. Given the current state of all decisions, determine which OTHER decisions (if any) are now invalid and should be reconsidered.
|
|
1942
|
+
|
|
1943
|
+
Rules:
|
|
1944
|
+
- Only include stages that are GENUINELY affected by the change.
|
|
1945
|
+
- Only affect stages AFTER the changed stage in the ordered list.
|
|
1946
|
+
- Consider whether each decision was dependent on the changed decision.
|
|
1947
|
+
- If nothing needs to change, return empty arrays.
|
|
1948
|
+
|
|
1949
|
+
Examples:
|
|
1950
|
+
|
|
1951
|
+
Changed frontend from Next.js to Astro (backend was "Next.js API routes"):
|
|
1952
|
+
{"clear":["backend"],"add":[],"remove":[]}
|
|
1953
|
+
Reason: Backend was tied to Next.js. If backend had been "Express" (independent), it would NOT be cleared.
|
|
1954
|
+
|
|
1955
|
+
Changed auth from Clerk to Auth.js:
|
|
1956
|
+
{"clear":[],"add":[],"remove":[]}
|
|
1957
|
+
Reason: Swapping auth providers doesn't affect other decisions.
|
|
1958
|
+
|
|
1959
|
+
Changed frontend from Next.js to static HTML:
|
|
1960
|
+
{"clear":["backend","auth","ai"],"add":[],"remove":["payments"]}
|
|
1961
|
+
Reason: Static site fundamentally changes what's viable.
|
|
1962
|
+
|
|
1963
|
+
Respond with ONLY a JSON object: {"clear": [...], "add": [...], "remove": [...]}
|
|
1964
|
+
`;
|
|
1965
|
+
function createInvalidationFn() {
|
|
1966
|
+
return async (changedId, oldValue, newValue, progress, stages) => {
|
|
1967
|
+
const stageList = stages.map((s) => `${s.id} (${s.status}): ${s.summary ?? "no decision"}`).join("\n");
|
|
1968
|
+
const userPrompt = `The user changed "${changedId}" from "${oldValue?.component ?? "none"}" to "${newValue?.component ?? "none"}".
|
|
1969
|
+
|
|
1970
|
+
Current decisions:
|
|
1971
|
+
${serializeProgress(progress)}
|
|
1972
|
+
|
|
1973
|
+
Current stages:
|
|
1974
|
+
${stageList}
|
|
1975
|
+
|
|
1976
|
+
What needs to change?`;
|
|
1977
|
+
try {
|
|
1978
|
+
const response = await chat({
|
|
1979
|
+
system: INVALIDATION_PROMPT,
|
|
1980
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
1981
|
+
maxTokens: 1024
|
|
1982
|
+
});
|
|
1983
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1984
|
+
const parsed = JSON.parse(text);
|
|
1985
|
+
return {
|
|
1986
|
+
clear: Array.isArray(parsed.clear) ? parsed.clear : [],
|
|
1987
|
+
add: Array.isArray(parsed.add) ? parsed.add : [],
|
|
1988
|
+
remove: Array.isArray(parsed.remove) ? parsed.remove : []
|
|
1989
|
+
};
|
|
1990
|
+
} catch {
|
|
1991
|
+
return { clear: [], add: [], remove: [] };
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
async function main(fresh = false) {
|
|
1996
|
+
const cwd = process.cwd();
|
|
1997
|
+
const invalidationFn = createInvalidationFn();
|
|
1998
|
+
process.on("SIGINT", () => {
|
|
1999
|
+
process.exit(0);
|
|
738
2000
|
});
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
2001
|
+
if (fresh) {
|
|
2002
|
+
const tempManager = StageManager.resume(cwd);
|
|
2003
|
+
tempManager?.cleanup();
|
|
2004
|
+
console.log("Session cleared. Starting fresh.\n");
|
|
2005
|
+
}
|
|
2006
|
+
let manager;
|
|
2007
|
+
const existingSession = fresh ? null : StageManager.detect(cwd);
|
|
2008
|
+
if (existingSession) {
|
|
2009
|
+
console.log(`
|
|
2010
|
+
Found saved progress for "${existingSession.progress.projectName ?? "unnamed"}"`);
|
|
2011
|
+
console.log("Run with --fresh to start over.\n");
|
|
2012
|
+
const resumed = StageManager.resume(cwd, invalidationFn);
|
|
2013
|
+
if (!resumed) {
|
|
2014
|
+
console.log("Could not restore session. Starting fresh.");
|
|
2015
|
+
manager = StageManager.start(cwd, invalidationFn);
|
|
2016
|
+
} else {
|
|
2017
|
+
manager = resumed;
|
|
2018
|
+
}
|
|
750
2019
|
} else {
|
|
751
|
-
|
|
752
|
-
|
|
2020
|
+
manager = StageManager.start(cwd, invalidationFn);
|
|
2021
|
+
}
|
|
2022
|
+
let shouldBuild = false;
|
|
2023
|
+
const ink = withFullScreen(
|
|
2024
|
+
React6.createElement(App, {
|
|
2025
|
+
manager,
|
|
2026
|
+
onBuild: () => {
|
|
2027
|
+
shouldBuild = true;
|
|
2028
|
+
},
|
|
2029
|
+
onExit: () => {
|
|
2030
|
+
shouldBuild = false;
|
|
2031
|
+
}
|
|
2032
|
+
})
|
|
2033
|
+
);
|
|
2034
|
+
await ink.start();
|
|
2035
|
+
await ink.waitUntilExit();
|
|
2036
|
+
if (shouldBuild) {
|
|
2037
|
+
const readiness = manager.progress.deployment ? checkDeployReadiness(manager.progress.deployment.component) : null;
|
|
2038
|
+
renderPostScaffold(manager.progress.projectName, readiness);
|
|
2039
|
+
console.log("\nHappy building!\n");
|
|
753
2040
|
}
|
|
754
2041
|
}
|
|
755
|
-
var
|
|
756
|
-
|
|
757
|
-
|
|
2042
|
+
var args = process.argv.slice(2);
|
|
2043
|
+
var command = args[0];
|
|
2044
|
+
var isFresh = args.includes("--fresh");
|
|
2045
|
+
if (!command || command === "init" || command === "--fresh") {
|
|
2046
|
+
main(isFresh).catch((err) => {
|
|
758
2047
|
console.error(err);
|
|
759
2048
|
process.exit(1);
|
|
760
2049
|
});
|
|
761
2050
|
} else {
|
|
762
2051
|
console.error(`Unknown command: ${command}`);
|
|
763
|
-
console.error("Usage: stack-agent [init]");
|
|
2052
|
+
console.error("Usage: stack-agent [init] [--fresh]");
|
|
764
2053
|
process.exit(1);
|
|
765
2054
|
}
|