stack-agent 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -19
- package/dist/index.js +189 -108
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -5,22 +5,16 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
AI-powered CLI that helps developers choose and scaffold full-stack applications through
|
|
8
|
+
AI-powered CLI that helps developers choose and scaffold full-stack applications through a fullscreen terminal UI.
|
|
9
9
|
|
|
10
|
-
A senior software architect in your terminal — it
|
|
10
|
+
A senior software architect in your terminal — it recommends your entire stack, lets you review and refine each decision, then scaffolds the project with integration code.
|
|
11
11
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
1. **Conversation** — The agent asks what you're building, then guides you through frontend, backend, database, auth, payments, AI/LLM, and deployment choices
|
|
15
|
-
2. **Recommendations** — Each stage presents 2-3 options with a recommended pick and trade-off context
|
|
16
|
-
3. **Review** — Once all decisions are made, the agent presents your full stack for approval
|
|
17
|
-
4. **Scaffold** — The agent runs official tools (create-next-app, create-vite, etc.) and generates integration code grounded by current documentation
|
|
18
|
-
|
|
19
|
-
## Quick start
|
|
12
|
+
## Usage
|
|
20
13
|
|
|
21
14
|
```bash
|
|
22
15
|
export ANTHROPIC_API_KEY=your-key-here
|
|
23
|
-
npx stack-agent
|
|
16
|
+
npx stack-agent # Start or resume a session
|
|
17
|
+
npx stack-agent --fresh # Clear saved progress and start over
|
|
24
18
|
```
|
|
25
19
|
|
|
26
20
|
## Requirements
|
|
@@ -28,19 +22,13 @@ npx stack-agent
|
|
|
28
22
|
- Node.js 20+
|
|
29
23
|
- An [Anthropic API key](https://console.anthropic.com/settings/keys)
|
|
30
24
|
|
|
31
|
-
## What it does
|
|
32
|
-
|
|
33
|
-
- Delegates base scaffolding to official framework CLIs (create-next-app, create-vite, etc.)
|
|
34
|
-
- Generates integration code (auth, database, payments) using Claude, grounded by up-to-date documentation via MCP
|
|
35
|
-
- Writes `.env.example` with required environment variables
|
|
36
|
-
- Installs dependencies automatically
|
|
37
|
-
|
|
38
25
|
## Development
|
|
39
26
|
|
|
40
27
|
```bash
|
|
41
28
|
npm install
|
|
42
29
|
npm run dev # Run with tsx
|
|
43
|
-
npm
|
|
30
|
+
npm run mockup # Run interactive TUI mockup (no LLM calls)
|
|
31
|
+
npm test # Run tests (176 tests)
|
|
44
32
|
npm run build # Build with tsup
|
|
45
33
|
```
|
|
46
34
|
|
package/dist/index.js
CHANGED
|
@@ -6,14 +6,14 @@ import { withFullScreen } from "fullscreen-ink";
|
|
|
6
6
|
|
|
7
7
|
// src/cli/app.tsx
|
|
8
8
|
import { useState as useState5, useEffect as useEffect2, useCallback } from "react";
|
|
9
|
-
import { Box as
|
|
9
|
+
import { Box as Box8, Text as Text8, useApp, useInput as useInput3 } from "ink";
|
|
10
10
|
import { useScreenSize } from "fullscreen-ink";
|
|
11
11
|
|
|
12
12
|
// src/cli/components/header.tsx
|
|
13
13
|
import { Box, Text } from "ink";
|
|
14
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
-
function Header({ appName, currentStage, stages, showDots = false }) {
|
|
16
|
-
const stageName = currentStage?.label ?? "Stack Progress";
|
|
15
|
+
function Header({ appName, currentStage, stages, showDots = false, title }) {
|
|
16
|
+
const stageName = title ?? currentStage?.label ?? "Stack Progress";
|
|
17
17
|
return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderBottom: false, paddingX: 1, justifyContent: "space-between", children: [
|
|
18
18
|
/* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
19
19
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: appName }),
|
|
@@ -53,6 +53,9 @@ function Footer({ progress, stages, terminalWidth, mode = "decisions" }) {
|
|
|
53
53
|
case "input":
|
|
54
54
|
display = "Enter submit \xB7 Esc stages";
|
|
55
55
|
break;
|
|
56
|
+
case "scaffold":
|
|
57
|
+
display = "Scaffolding your project...";
|
|
58
|
+
break;
|
|
56
59
|
default:
|
|
57
60
|
display = buildDecisionsDisplay(progress, stages, terminalWidth);
|
|
58
61
|
break;
|
|
@@ -259,12 +262,12 @@ function deserializeSession(json) {
|
|
|
259
262
|
|
|
260
263
|
// src/cli/components/stage-list.tsx
|
|
261
264
|
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
262
|
-
function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
265
|
+
function StageListView({ stages, currentStageId, progress, showFresh = false, onResult }) {
|
|
263
266
|
const [showConfirm, setShowConfirm] = useState3(false);
|
|
264
267
|
const [showWarning, setShowWarning] = useState3("");
|
|
265
268
|
const [cursor, setCursor] = useState3(0);
|
|
266
269
|
const canBuild = isComplete(progress);
|
|
267
|
-
const itemCount = stages.length + 1;
|
|
270
|
+
const itemCount = stages.length + 1 + (showFresh ? 1 : 0);
|
|
268
271
|
useInput2((input, key) => {
|
|
269
272
|
if (showConfirm) return;
|
|
270
273
|
if (key.upArrow) {
|
|
@@ -278,13 +281,15 @@ function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
|
278
281
|
if (key.return) {
|
|
279
282
|
if (cursor < stages.length) {
|
|
280
283
|
onResult({ kind: "select", stageId: stages[cursor].id });
|
|
281
|
-
} else {
|
|
284
|
+
} else if (cursor === stages.length) {
|
|
282
285
|
if (canBuild) {
|
|
283
286
|
setShowConfirm(true);
|
|
284
287
|
} else {
|
|
285
288
|
const missing = getMissingDecisions(progress);
|
|
286
289
|
setShowWarning(`Complete ${missing.join(", ")} first.`);
|
|
287
290
|
}
|
|
291
|
+
} else {
|
|
292
|
+
onResult({ kind: "fresh" });
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
295
|
if (key.escape) {
|
|
@@ -322,6 +327,10 @@ function StageListView({ stages, currentStageId, progress, onResult }) {
|
|
|
322
327
|
requiredRemaining(progress),
|
|
323
328
|
" remaining)"
|
|
324
329
|
] })
|
|
330
|
+
] }),
|
|
331
|
+
showFresh && /* @__PURE__ */ jsxs3(Box5, { children: [
|
|
332
|
+
/* @__PURE__ */ jsx5(Text5, { children: cursor === stages.length + 1 ? "\u276F " : " " }),
|
|
333
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Start fresh" })
|
|
325
334
|
] })
|
|
326
335
|
] });
|
|
327
336
|
}
|
|
@@ -391,7 +400,7 @@ function getMissingDecisions(progress) {
|
|
|
391
400
|
}
|
|
392
401
|
|
|
393
402
|
// src/cli/app.tsx
|
|
394
|
-
import { TextInput as TextInput2, Spinner as
|
|
403
|
+
import { TextInput as TextInput2, Spinner as Spinner3 } from "@inkjs/ui";
|
|
395
404
|
|
|
396
405
|
// src/cli/components/project-info-form.tsx
|
|
397
406
|
import { useState as useState4 } from "react";
|
|
@@ -444,14 +453,34 @@ function ProjectInfoForm({ onSubmit }) {
|
|
|
444
453
|
] });
|
|
445
454
|
}
|
|
446
455
|
|
|
456
|
+
// src/cli/components/scaffold-view.tsx
|
|
457
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
458
|
+
import { Spinner as Spinner2 } from "@inkjs/ui";
|
|
459
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
460
|
+
function ScaffoldView({ steps }) {
|
|
461
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: steps.map((step, i) => /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
462
|
+
/* @__PURE__ */ jsxs5(Box7, { children: [
|
|
463
|
+
step.status === "running" && /* @__PURE__ */ jsx7(Spinner2, {}),
|
|
464
|
+
step.status === "done" && /* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
465
|
+
step.status === "error" && /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717" }),
|
|
466
|
+
/* @__PURE__ */ jsxs5(Text7, { bold: step.status === "running", children: [
|
|
467
|
+
" ",
|
|
468
|
+
step.name
|
|
469
|
+
] })
|
|
470
|
+
] }),
|
|
471
|
+
step.status === "done" && step.files && step.files.length > 0 && /* @__PURE__ */ jsx7(Box7, { paddingLeft: 3, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: step.files.join(", ") }) }),
|
|
472
|
+
step.status === "error" && step.error && /* @__PURE__ */ jsx7(Box7, { paddingLeft: 3, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: step.error }) })
|
|
473
|
+
] }, i)) });
|
|
474
|
+
}
|
|
475
|
+
|
|
447
476
|
// src/cli/bridge.ts
|
|
448
477
|
function createBridge() {
|
|
449
478
|
const listeners = /* @__PURE__ */ new Map();
|
|
450
479
|
let pendingResolve = null;
|
|
451
|
-
function emit(event, ...
|
|
480
|
+
function emit(event, ...args) {
|
|
452
481
|
const set = listeners.get(event);
|
|
453
482
|
if (set) {
|
|
454
|
-
for (const fn of set) fn(...
|
|
483
|
+
for (const fn of set) fn(...args);
|
|
455
484
|
}
|
|
456
485
|
}
|
|
457
486
|
return {
|
|
@@ -488,20 +517,48 @@ import { join as join2 } from "path";
|
|
|
488
517
|
import Anthropic from "@anthropic-ai/sdk";
|
|
489
518
|
|
|
490
519
|
// src/util/logger.ts
|
|
491
|
-
|
|
492
|
-
var
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
520
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
521
|
+
var currentLevel = process.env.LOG_LEVEL ?? "info";
|
|
522
|
+
function log(level, component, msg, data) {
|
|
523
|
+
if (LEVELS[level] < LEVELS[currentLevel]) return;
|
|
524
|
+
const prefix = `[${level}] [${component}]`;
|
|
525
|
+
const line = data ? `${prefix} ${msg} ${JSON.stringify(data)}` : `${prefix} ${msg}`;
|
|
526
|
+
process.stderr.write(line + "\n");
|
|
527
|
+
}
|
|
496
528
|
function createLogger(component) {
|
|
497
|
-
return
|
|
529
|
+
return {
|
|
530
|
+
debug: (data, msg) => log("debug", component, msg, data),
|
|
531
|
+
info: (data, msg) => log("info", component, msg, data),
|
|
532
|
+
warn: (data, msg) => log("warn", component, msg, data),
|
|
533
|
+
error: (data, msg) => log("error", component, msg, data)
|
|
534
|
+
};
|
|
498
535
|
}
|
|
499
536
|
|
|
500
537
|
// src/llm/client.ts
|
|
501
|
-
var
|
|
538
|
+
var log2 = createLogger("llm");
|
|
502
539
|
function summarizeMessages(messages) {
|
|
503
540
|
return `${messages.length} messages, last: ${messages.at(-1)?.role ?? "none"}`;
|
|
504
541
|
}
|
|
542
|
+
function withMessageCaching(messages) {
|
|
543
|
+
if (messages.length < 2) return messages;
|
|
544
|
+
const cached = messages.map((m, i) => {
|
|
545
|
+
if (i !== messages.length - 2) return m;
|
|
546
|
+
const content = m.content;
|
|
547
|
+
if (typeof content === "string") {
|
|
548
|
+
return { ...m, content: [{ type: "text", text: content, cache_control: { type: "ephemeral" } }] };
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(content)) {
|
|
551
|
+
const blocks = [...content];
|
|
552
|
+
const last = blocks[blocks.length - 1];
|
|
553
|
+
if (last) {
|
|
554
|
+
blocks[blocks.length - 1] = { ...last, cache_control: { type: "ephemeral" } };
|
|
555
|
+
}
|
|
556
|
+
return { ...m, content: blocks };
|
|
557
|
+
}
|
|
558
|
+
return m;
|
|
559
|
+
});
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
505
562
|
function getClient() {
|
|
506
563
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
507
564
|
if (!apiKey) {
|
|
@@ -520,7 +577,7 @@ function client() {
|
|
|
520
577
|
}
|
|
521
578
|
async function chat(options) {
|
|
522
579
|
const { system, messages, tools, maxTokens, mcpServers } = options;
|
|
523
|
-
|
|
580
|
+
log2.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chat request");
|
|
524
581
|
let response;
|
|
525
582
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
526
583
|
const mcpServerList = Object.entries(mcpServers).map(([name, config]) => ({
|
|
@@ -550,39 +607,39 @@ async function chat(options) {
|
|
|
550
607
|
response = await client().messages.create({
|
|
551
608
|
model: "claude-sonnet-4-6",
|
|
552
609
|
max_tokens: maxTokens,
|
|
553
|
-
system,
|
|
554
|
-
messages,
|
|
610
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
611
|
+
messages: withMessageCaching(messages),
|
|
555
612
|
...tools && tools.length > 0 && { tools }
|
|
556
613
|
});
|
|
557
614
|
}
|
|
558
|
-
|
|
615
|
+
log2.info({
|
|
559
616
|
stopReason: response.stop_reason,
|
|
560
617
|
contentBlocks: response.content.length,
|
|
561
618
|
usage: response.usage
|
|
562
619
|
}, "chat response");
|
|
563
|
-
|
|
620
|
+
log2.debug({ content: response.content }, "chat response content");
|
|
564
621
|
return response;
|
|
565
622
|
}
|
|
566
623
|
async function chatStream(options, callbacks) {
|
|
567
624
|
const { system, messages, tools, maxTokens } = options;
|
|
568
|
-
|
|
625
|
+
log2.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chatStream request");
|
|
569
626
|
const stream = client().messages.stream({
|
|
570
627
|
model: "claude-sonnet-4-6",
|
|
571
628
|
max_tokens: maxTokens,
|
|
572
|
-
system,
|
|
573
|
-
messages,
|
|
629
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
630
|
+
messages: withMessageCaching(messages),
|
|
574
631
|
...tools && tools.length > 0 && { tools }
|
|
575
632
|
});
|
|
576
633
|
stream.on("text", (text) => {
|
|
577
634
|
callbacks.onText(text);
|
|
578
635
|
});
|
|
579
636
|
const finalMessage = await stream.finalMessage();
|
|
580
|
-
|
|
637
|
+
log2.info({
|
|
581
638
|
stopReason: finalMessage.stop_reason,
|
|
582
639
|
contentBlocks: finalMessage.content.length,
|
|
583
640
|
usage: finalMessage.usage
|
|
584
641
|
}, "chatStream response");
|
|
585
|
-
|
|
642
|
+
log2.debug({ content: finalMessage.content }, "chatStream response content");
|
|
586
643
|
for (const block of finalMessage.content) {
|
|
587
644
|
if (block.type === "tool_use") {
|
|
588
645
|
callbacks.onToolUse(block);
|
|
@@ -709,6 +766,10 @@ function scaffoldToolDefinitions() {
|
|
|
709
766
|
input_schema: {
|
|
710
767
|
type: "object",
|
|
711
768
|
properties: {
|
|
769
|
+
name: {
|
|
770
|
+
type: "string",
|
|
771
|
+
description: 'Short name for this integration (e.g. "Database", "Auth", "AI Chat", "Deploy Config").'
|
|
772
|
+
},
|
|
712
773
|
files: {
|
|
713
774
|
type: "object",
|
|
714
775
|
additionalProperties: { type: "string" },
|
|
@@ -735,7 +796,7 @@ function scaffoldToolDefinitions() {
|
|
|
735
796
|
description: "List of environment variable names required by the integration."
|
|
736
797
|
}
|
|
737
798
|
},
|
|
738
|
-
required: ["files"]
|
|
799
|
+
required: ["name", "files"]
|
|
739
800
|
}
|
|
740
801
|
}
|
|
741
802
|
];
|
|
@@ -878,7 +939,7 @@ Do not ask for confirmation. Proceed through all steps automatically.`;
|
|
|
878
939
|
import { execFileSync } from "child_process";
|
|
879
940
|
import { readdirSync, existsSync } from "fs";
|
|
880
941
|
import { join } from "path";
|
|
881
|
-
function runScaffold(tool,
|
|
942
|
+
function runScaffold(tool, args, projectName, cwd) {
|
|
882
943
|
const outputDir = join(cwd, projectName);
|
|
883
944
|
if (existsSync(outputDir)) {
|
|
884
945
|
const entries = readdirSync(outputDir);
|
|
@@ -888,7 +949,7 @@ function runScaffold(tool, args2, projectName, cwd) {
|
|
|
888
949
|
);
|
|
889
950
|
}
|
|
890
951
|
}
|
|
891
|
-
const spawnArgs = [`${tool}@latest`, projectName, ...
|
|
952
|
+
const spawnArgs = [`${tool}@latest`, projectName, ...args];
|
|
892
953
|
const opts = { cwd, stdio: "pipe" };
|
|
893
954
|
execFileSync("npx", spawnArgs, opts);
|
|
894
955
|
return outputDir;
|
|
@@ -1068,14 +1129,7 @@ async function runStageLoop(stage, manager, bridge, mcpServers) {
|
|
|
1068
1129
|
messages.push({ role: "user", content: inputResult.value });
|
|
1069
1130
|
}
|
|
1070
1131
|
}
|
|
1071
|
-
function
|
|
1072
|
-
return {
|
|
1073
|
-
start: (msg) => process.stdout.write(` ${msg}...`),
|
|
1074
|
-
stop: (msg) => process.stdout.write(` ${msg}
|
|
1075
|
-
`)
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
async function runScaffoldLoop(progress, mcpServers) {
|
|
1132
|
+
async function runScaffoldLoop(progress, onProgress, mcpServers) {
|
|
1079
1133
|
const messages = [];
|
|
1080
1134
|
const system = buildScaffoldPrompt(progress);
|
|
1081
1135
|
const cwd = process.cwd();
|
|
@@ -1083,11 +1137,22 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1083
1137
|
const projectDir = join2(cwd, projectName);
|
|
1084
1138
|
let toolCallCount = 0;
|
|
1085
1139
|
const maxToolCalls = 30;
|
|
1140
|
+
const steps = [];
|
|
1141
|
+
function pushStep(step) {
|
|
1142
|
+
steps.push(step);
|
|
1143
|
+
onProgress?.([...steps]);
|
|
1144
|
+
}
|
|
1145
|
+
function updateLastStep(patch) {
|
|
1146
|
+
const last = steps[steps.length - 1];
|
|
1147
|
+
if (last) Object.assign(last, patch);
|
|
1148
|
+
onProgress?.([...steps]);
|
|
1149
|
+
}
|
|
1086
1150
|
messages.push({
|
|
1087
1151
|
role: "user",
|
|
1088
1152
|
content: "Begin scaffolding the project according to the plan."
|
|
1089
1153
|
});
|
|
1090
1154
|
while (true) {
|
|
1155
|
+
pushStep({ name: "Planning next step...", status: "running" });
|
|
1091
1156
|
const response = await chat({
|
|
1092
1157
|
system,
|
|
1093
1158
|
messages,
|
|
@@ -1095,6 +1160,8 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1095
1160
|
maxTokens: 16384,
|
|
1096
1161
|
mcpServers
|
|
1097
1162
|
});
|
|
1163
|
+
steps.pop();
|
|
1164
|
+
onProgress?.([...steps]);
|
|
1098
1165
|
const contentBlocks = response.content;
|
|
1099
1166
|
const toolUseBlocks = contentBlocks.filter(
|
|
1100
1167
|
(b) => b.type === "tool_use"
|
|
@@ -1108,30 +1175,30 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1108
1175
|
const toolBlock = block;
|
|
1109
1176
|
toolCallCount++;
|
|
1110
1177
|
if (toolCallCount > maxToolCalls) {
|
|
1111
|
-
|
|
1178
|
+
pushStep({ name: "Tool call limit exceeded", status: "error", error: `Exceeded ${maxToolCalls} tool calls` });
|
|
1112
1179
|
return false;
|
|
1113
1180
|
}
|
|
1114
|
-
const spinner = createSimpleSpinner();
|
|
1115
1181
|
try {
|
|
1116
1182
|
if (toolBlock.name === "run_scaffold") {
|
|
1117
|
-
|
|
1183
|
+
pushStep({ name: "Creating project", status: "running" });
|
|
1118
1184
|
const outputDir = runScaffold(
|
|
1119
1185
|
toolBlock.input.tool,
|
|
1120
1186
|
toolBlock.input.args,
|
|
1121
1187
|
projectName,
|
|
1122
1188
|
cwd
|
|
1123
1189
|
);
|
|
1124
|
-
|
|
1190
|
+
updateLastStep({ name: "Created project", status: "done" });
|
|
1125
1191
|
toolResults.push({
|
|
1126
1192
|
type: "tool_result",
|
|
1127
1193
|
tool_use_id: toolBlock.id,
|
|
1128
1194
|
content: `Scaffold completed. Project created at ${outputDir}`
|
|
1129
1195
|
});
|
|
1130
1196
|
} else if (toolBlock.name === "add_integration") {
|
|
1131
|
-
const
|
|
1197
|
+
const files = Object.keys(
|
|
1132
1198
|
toolBlock.input.files ?? {}
|
|
1133
|
-
)
|
|
1134
|
-
|
|
1199
|
+
);
|
|
1200
|
+
const integrationName = toolBlock.input.name ?? "Integration";
|
|
1201
|
+
pushStep({ name: `Adding ${integrationName}`, status: "running" });
|
|
1135
1202
|
writeIntegration(projectDir, {
|
|
1136
1203
|
files: toolBlock.input.files ?? {},
|
|
1137
1204
|
dependencies: toolBlock.input.dependencies,
|
|
@@ -1139,14 +1206,14 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1139
1206
|
scripts: toolBlock.input.scripts,
|
|
1140
1207
|
envVars: toolBlock.input.envVars
|
|
1141
1208
|
});
|
|
1142
|
-
|
|
1209
|
+
updateLastStep({ name: integrationName, status: "done", files });
|
|
1143
1210
|
toolResults.push({
|
|
1144
1211
|
type: "tool_result",
|
|
1145
1212
|
tool_use_id: toolBlock.id,
|
|
1146
1213
|
content: "Integration written successfully."
|
|
1147
1214
|
});
|
|
1148
1215
|
} else {
|
|
1149
|
-
|
|
1216
|
+
pushStep({ name: `Unknown tool: ${toolBlock.name}`, status: "error", error: `Unknown tool: "${toolBlock.name}"` });
|
|
1150
1217
|
toolResults.push({
|
|
1151
1218
|
type: "tool_result",
|
|
1152
1219
|
tool_use_id: toolBlock.id,
|
|
@@ -1156,7 +1223,7 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1156
1223
|
}
|
|
1157
1224
|
} catch (err) {
|
|
1158
1225
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1159
|
-
|
|
1226
|
+
updateLastStep({ status: "error", error: errorMessage });
|
|
1160
1227
|
toolResults.push({
|
|
1161
1228
|
type: "tool_result",
|
|
1162
1229
|
tool_use_id: toolBlock.id,
|
|
@@ -1170,7 +1237,7 @@ async function runScaffoldLoop(progress, mcpServers) {
|
|
|
1170
1237
|
}
|
|
1171
1238
|
|
|
1172
1239
|
// src/agent/recommend.ts
|
|
1173
|
-
var
|
|
1240
|
+
var log3 = createLogger("recommend");
|
|
1174
1241
|
var RECOMMEND_PROMPT = `You are a senior software architect. Based on the project description, recommend a complete technology stack.
|
|
1175
1242
|
|
|
1176
1243
|
For each category, provide your recommendation as a JSON object. If a category is not needed for this project, set it to null.
|
|
@@ -1204,15 +1271,15 @@ Description: ${description}`
|
|
|
1204
1271
|
maxTokens: 1024
|
|
1205
1272
|
});
|
|
1206
1273
|
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
1207
|
-
|
|
1208
|
-
|
|
1274
|
+
log3.info({ rawLength: text.length }, "received recommendation response");
|
|
1275
|
+
log3.debug({ rawText: text }, "recommendation raw text");
|
|
1209
1276
|
const jsonStr = text.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1210
1277
|
const parsed = JSON.parse(jsonStr);
|
|
1211
1278
|
const categories = Object.keys(parsed).filter((k) => parsed[k] !== null);
|
|
1212
|
-
|
|
1279
|
+
log3.info({ recommended: categories, skipped: Object.keys(parsed).filter((k) => parsed[k] === null) }, "parsed recommendations");
|
|
1213
1280
|
return parsed;
|
|
1214
1281
|
} catch (err) {
|
|
1215
|
-
|
|
1282
|
+
log3.error({ err }, "recommendation pass failed");
|
|
1216
1283
|
return {};
|
|
1217
1284
|
}
|
|
1218
1285
|
}
|
|
@@ -1256,8 +1323,8 @@ function applyRecommendations(progress, stages, recommendations) {
|
|
|
1256
1323
|
}
|
|
1257
1324
|
|
|
1258
1325
|
// src/cli/app.tsx
|
|
1259
|
-
import { jsx as
|
|
1260
|
-
function App({ manager, onBuild, onExit }) {
|
|
1326
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1327
|
+
function App({ manager, isResumed, onBuild, onFresh, onExit }) {
|
|
1261
1328
|
const app = useApp();
|
|
1262
1329
|
const { width, height } = useScreenSize();
|
|
1263
1330
|
const [view, setView] = useState5(
|
|
@@ -1269,6 +1336,7 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1269
1336
|
const [stages, setStages] = useState5([...manager.stages]);
|
|
1270
1337
|
const [options, setOptions] = useState5([]);
|
|
1271
1338
|
const [errorMsg, setErrorMsg] = useState5("");
|
|
1339
|
+
const [scaffoldSteps, setScaffoldSteps] = useState5([]);
|
|
1272
1340
|
const syncState = useCallback(() => {
|
|
1273
1341
|
setProgress({ ...manager.progress });
|
|
1274
1342
|
setStages([...manager.stages]);
|
|
@@ -1330,7 +1398,7 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1330
1398
|
syncState();
|
|
1331
1399
|
setView("stage_list");
|
|
1332
1400
|
}, [manager, bridge, syncState]);
|
|
1333
|
-
const handleStageResult = useCallback((result) => {
|
|
1401
|
+
const handleStageResult = useCallback(async (result) => {
|
|
1334
1402
|
if (result.kind === "select") {
|
|
1335
1403
|
const stage = manager.stages.find((s) => s.id === result.stageId);
|
|
1336
1404
|
if (stage && (stage.status === "complete" || stage.status === "skipped")) {
|
|
@@ -1338,8 +1406,28 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1338
1406
|
syncState();
|
|
1339
1407
|
}
|
|
1340
1408
|
runStage(result.stageId);
|
|
1409
|
+
} else if (result.kind === "fresh") {
|
|
1410
|
+
onFresh();
|
|
1411
|
+
app.exit();
|
|
1341
1412
|
} else if (result.kind === "build") {
|
|
1342
|
-
|
|
1413
|
+
setView("scaffold");
|
|
1414
|
+
setScaffoldSteps([]);
|
|
1415
|
+
const onScaffoldProgress = (steps) => {
|
|
1416
|
+
setScaffoldSteps([...steps]);
|
|
1417
|
+
};
|
|
1418
|
+
try {
|
|
1419
|
+
const success = await runScaffoldLoop(manager.progress, onScaffoldProgress);
|
|
1420
|
+
if (success) {
|
|
1421
|
+
manager.cleanup();
|
|
1422
|
+
onBuild();
|
|
1423
|
+
} else {
|
|
1424
|
+
onBuild();
|
|
1425
|
+
}
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
setErrorMsg(err.message);
|
|
1428
|
+
setView("error");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1343
1431
|
app.exit();
|
|
1344
1432
|
} else if (result.kind === "cancel") {
|
|
1345
1433
|
manager.save();
|
|
@@ -1385,34 +1473,36 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1385
1473
|
syncState();
|
|
1386
1474
|
setView("stage_list");
|
|
1387
1475
|
}, [manager, syncState]);
|
|
1388
|
-
const footerMode = view === "stage_list" ? "stage_list" : view === "options" ? "options" : view === "input" || view === "project_info" ? "input" : "decisions";
|
|
1476
|
+
const footerMode = view === "scaffold" ? "scaffold" : view === "stage_list" ? "stage_list" : view === "options" ? "options" : view === "input" || view === "project_info" ? "input" : "decisions";
|
|
1389
1477
|
const contentHeight = height - 4;
|
|
1390
|
-
return /* @__PURE__ */
|
|
1391
|
-
/* @__PURE__ */
|
|
1478
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", width, height, children: [
|
|
1479
|
+
/* @__PURE__ */ jsx8(
|
|
1392
1480
|
Header,
|
|
1393
1481
|
{
|
|
1394
1482
|
appName: "stack-agent",
|
|
1395
|
-
currentStage: view === "stage_list" ? null : currentStage,
|
|
1483
|
+
currentStage: view === "stage_list" || view === "scaffold" ? null : currentStage,
|
|
1396
1484
|
stages,
|
|
1397
|
-
showDots: view !== "stage_list"
|
|
1485
|
+
showDots: view !== "stage_list" && view !== "scaffold",
|
|
1486
|
+
title: view === "scaffold" ? "Scaffolding" : void 0
|
|
1398
1487
|
}
|
|
1399
1488
|
),
|
|
1400
|
-
/* @__PURE__ */
|
|
1401
|
-
view === "project_info" && /* @__PURE__ */
|
|
1402
|
-
view === "loading" && /* @__PURE__ */
|
|
1403
|
-
view === "stage_list" && /* @__PURE__ */
|
|
1489
|
+
/* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", height: contentHeight, paddingX: 1, borderStyle: "single", borderTop: false, borderBottom: false, children: [
|
|
1490
|
+
view === "project_info" && /* @__PURE__ */ jsx8(ProjectInfoForm, { onSubmit: handleProjectInfo }),
|
|
1491
|
+
view === "loading" && /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx8(Spinner3, { label: "Analyzing your project and recommending a stack..." }) }),
|
|
1492
|
+
view === "stage_list" && /* @__PURE__ */ jsx8(
|
|
1404
1493
|
StageListView,
|
|
1405
1494
|
{
|
|
1406
1495
|
stages,
|
|
1407
1496
|
currentStageId: currentStage?.id ?? null,
|
|
1408
1497
|
progress,
|
|
1498
|
+
showFresh: isResumed,
|
|
1409
1499
|
onResult: handleStageResult
|
|
1410
1500
|
}
|
|
1411
1501
|
),
|
|
1412
|
-
view === "conversation" && /* @__PURE__ */
|
|
1413
|
-
view === "input" && /* @__PURE__ */
|
|
1414
|
-
/* @__PURE__ */
|
|
1415
|
-
/* @__PURE__ */
|
|
1502
|
+
view === "conversation" && /* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight }),
|
|
1503
|
+
view === "input" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1504
|
+
/* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight - 3 }),
|
|
1505
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(
|
|
1416
1506
|
TextInput2,
|
|
1417
1507
|
{
|
|
1418
1508
|
placeholder: "Type your response...",
|
|
@@ -1420,19 +1510,20 @@ function App({ manager, onBuild, onExit }) {
|
|
|
1420
1510
|
}
|
|
1421
1511
|
) })
|
|
1422
1512
|
] }),
|
|
1423
|
-
view === "options" && /* @__PURE__ */
|
|
1424
|
-
/* @__PURE__ */
|
|
1425
|
-
/* @__PURE__ */
|
|
1513
|
+
view === "options" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1514
|
+
/* @__PURE__ */ jsx8(ConversationView, { bridge, maxLines: contentHeight - 8 }),
|
|
1515
|
+
/* @__PURE__ */ jsx8(OptionSelect, { options, onSelect: handleOptionSelect })
|
|
1426
1516
|
] }),
|
|
1427
|
-
view === "error" && /* @__PURE__ */
|
|
1428
|
-
/* @__PURE__ */
|
|
1517
|
+
view === "error" && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1518
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "red", bold: true, children: [
|
|
1429
1519
|
"Error: ",
|
|
1430
1520
|
errorMsg
|
|
1431
1521
|
] }),
|
|
1432
|
-
/* @__PURE__ */
|
|
1433
|
-
] })
|
|
1522
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Press Esc to return to stage list" })
|
|
1523
|
+
] }),
|
|
1524
|
+
view === "scaffold" && /* @__PURE__ */ jsx8(ScaffoldView, { steps: scaffoldSteps })
|
|
1434
1525
|
] }),
|
|
1435
|
-
/* @__PURE__ */
|
|
1526
|
+
/* @__PURE__ */ jsx8(
|
|
1436
1527
|
Footer,
|
|
1437
1528
|
{
|
|
1438
1529
|
progress,
|
|
@@ -1939,70 +2030,60 @@ What needs to change?`;
|
|
|
1939
2030
|
}
|
|
1940
2031
|
};
|
|
1941
2032
|
}
|
|
1942
|
-
async function main(
|
|
2033
|
+
async function main(startFresh = false) {
|
|
1943
2034
|
const cwd = process.cwd();
|
|
1944
2035
|
const invalidationFn = createInvalidationFn();
|
|
1945
2036
|
process.on("SIGINT", () => {
|
|
1946
2037
|
process.exit(0);
|
|
1947
2038
|
});
|
|
1948
|
-
if (
|
|
2039
|
+
if (startFresh) {
|
|
1949
2040
|
const tempManager = StageManager.resume(cwd);
|
|
1950
2041
|
tempManager?.cleanup();
|
|
1951
|
-
console.log("Session cleared. Starting fresh.\n");
|
|
1952
2042
|
}
|
|
1953
2043
|
let manager;
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
1956
|
-
console.log(`
|
|
1957
|
-
Found saved progress for "${existingSession.progress.projectName ?? "unnamed"}"`);
|
|
1958
|
-
console.log("Run with --fresh to start over.\n");
|
|
2044
|
+
const isResumed = !startFresh && StageManager.detect(cwd) !== null;
|
|
2045
|
+
if (isResumed) {
|
|
1959
2046
|
const resumed = StageManager.resume(cwd, invalidationFn);
|
|
1960
|
-
|
|
1961
|
-
console.log("Could not restore session. Starting fresh.");
|
|
1962
|
-
manager = StageManager.start(cwd, invalidationFn);
|
|
1963
|
-
} else {
|
|
1964
|
-
manager = resumed;
|
|
1965
|
-
}
|
|
2047
|
+
manager = resumed ?? StageManager.start(cwd, invalidationFn);
|
|
1966
2048
|
} else {
|
|
1967
2049
|
manager = StageManager.start(cwd, invalidationFn);
|
|
1968
2050
|
}
|
|
1969
2051
|
let shouldBuild = false;
|
|
2052
|
+
let shouldStartFresh = false;
|
|
1970
2053
|
const ink = withFullScreen(
|
|
1971
2054
|
React6.createElement(App, {
|
|
1972
2055
|
manager,
|
|
2056
|
+
isResumed,
|
|
1973
2057
|
onBuild: () => {
|
|
1974
2058
|
shouldBuild = true;
|
|
1975
2059
|
},
|
|
2060
|
+
onFresh: () => {
|
|
2061
|
+
shouldStartFresh = true;
|
|
2062
|
+
},
|
|
1976
2063
|
onExit: () => {
|
|
1977
|
-
shouldBuild = false;
|
|
1978
2064
|
}
|
|
1979
2065
|
})
|
|
1980
2066
|
);
|
|
1981
2067
|
await ink.start();
|
|
1982
2068
|
await ink.waitUntilExit();
|
|
2069
|
+
if (shouldStartFresh) {
|
|
2070
|
+
manager.cleanup();
|
|
2071
|
+
return main(true);
|
|
2072
|
+
}
|
|
1983
2073
|
if (shouldBuild) {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
const readiness = manager.progress.deployment ? checkDeployReadiness(manager.progress.deployment.component) : null;
|
|
1988
|
-
renderPostScaffold(manager.progress.projectName, readiness);
|
|
1989
|
-
manager.cleanup();
|
|
1990
|
-
console.log("\nHappy building!\n");
|
|
1991
|
-
} else {
|
|
1992
|
-
console.error("\nScaffolding encountered errors. Check the output above.\n");
|
|
1993
|
-
}
|
|
2074
|
+
const readiness = manager.progress.deployment ? checkDeployReadiness(manager.progress.deployment.component) : null;
|
|
2075
|
+
renderPostScaffold(manager.progress.projectName, readiness);
|
|
2076
|
+
console.log("\nHappy building!\n");
|
|
1994
2077
|
}
|
|
1995
2078
|
}
|
|
1996
|
-
var
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
if (!command || command === "init" || command === "--fresh") {
|
|
2000
|
-
main(isFresh).catch((err) => {
|
|
2079
|
+
var command = process.argv[2];
|
|
2080
|
+
if (!command || command === "init") {
|
|
2081
|
+
main().catch((err) => {
|
|
2001
2082
|
console.error(err);
|
|
2002
2083
|
process.exit(1);
|
|
2003
2084
|
});
|
|
2004
2085
|
} else {
|
|
2005
2086
|
console.error(`Unknown command: ${command}`);
|
|
2006
|
-
console.error("Usage: stack-agent [init]
|
|
2087
|
+
console.error("Usage: stack-agent [init]");
|
|
2007
2088
|
process.exit(1);
|
|
2008
2089
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stack-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "AI-powered CLI that helps developers choose and scaffold full-stack applications through conversational interaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"ink": "^6.8.0",
|
|
47
47
|
"marked": "^15.0.12",
|
|
48
48
|
"marked-terminal": "^7.3.0",
|
|
49
|
-
"pino": "^10.3.1",
|
|
50
49
|
"react": "^19.2.4",
|
|
51
50
|
"zod": "^4.3.6"
|
|
52
51
|
},
|
|
@@ -55,7 +54,6 @@
|
|
|
55
54
|
"@types/node": "^25.5.0",
|
|
56
55
|
"@types/react": "^19.2.14",
|
|
57
56
|
"ink-testing-library": "^4.0.0",
|
|
58
|
-
"pino-pretty": "^13.1.3",
|
|
59
57
|
"tsup": "^8.5.1",
|
|
60
58
|
"tsx": "^4.21.0",
|
|
61
59
|
"typescript": "^5.9.3",
|