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.
Files changed (3) hide show
  1. package/README.md +7 -19
  2. package/dist/index.js +189 -108
  3. package/package.json +1 -3
package/README.md CHANGED
@@ -5,22 +5,16 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js](https://img.shields.io/node/v/stack-agent)](https://nodejs.org)
7
7
 
8
- AI-powered CLI that helps developers choose and scaffold full-stack applications through conversational interaction.
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 walks you through stack decisions, explains trade-offs, and scaffolds your project using official framework tools.
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
- ## How it works
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 test # Run tests
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 Box7, Text as Text7, useApp, useInput as useInput3 } from "ink";
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 Spinner2 } from "@inkjs/ui";
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, ...args2) {
480
+ function emit(event, ...args) {
452
481
  const set = listeners.get(event);
453
482
  if (set) {
454
- for (const fn of set) fn(...args2);
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
- import pino from "pino";
492
- var logger = pino({
493
- level: process.env.LOG_LEVEL ?? "info",
494
- transport: process.env.NODE_ENV !== "production" ? { target: "pino-pretty", options: { destination: 2 } } : void 0
495
- }, process.env.NODE_ENV === "production" ? pino.destination(2) : void 0);
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 logger.child({ component });
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 log = createLogger("llm");
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
- log.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chat request");
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
- log.info({
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
- log.debug({ content: response.content }, "chat response content");
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
- log.debug({ maxTokens, toolCount: tools?.length ?? 0, messages: summarizeMessages(messages) }, "chatStream request");
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
- log.info({
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
- log.debug({ content: finalMessage.content }, "chatStream response content");
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, args2, projectName, cwd) {
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, ...args2];
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 createSimpleSpinner() {
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
- console.error(`Tool call limit exceeded (${maxToolCalls}). Stopping scaffold loop.`);
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
- spinner.start(`Running scaffold: ${toolBlock.input.tool}`);
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
- spinner.stop(`Scaffold complete: ${outputDir}`);
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 integrationDesc = Object.keys(
1197
+ const files = Object.keys(
1132
1198
  toolBlock.input.files ?? {}
1133
- ).join(", ");
1134
- spinner.start(`Adding integration: ${integrationDesc}`);
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
- spinner.stop("Integration added");
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
- spinner.stop(`Unknown tool: ${toolBlock.name}`);
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
- spinner.stop(`Error: ${errorMessage}`);
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 log2 = createLogger("recommend");
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
- log2.info({ rawLength: text.length }, "received recommendation response");
1208
- log2.debug({ rawText: text }, "recommendation raw text");
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
- log2.info({ recommended: categories, skipped: Object.keys(parsed).filter((k) => parsed[k] === null) }, "parsed recommendations");
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
- log2.error({ err }, "recommendation pass failed");
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 jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
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
- onBuild();
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__ */ jsxs5(Box7, { flexDirection: "column", width, height, children: [
1391
- /* @__PURE__ */ jsx7(
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__ */ jsxs5(Box7, { flexDirection: "column", height: contentHeight, paddingX: 1, borderStyle: "single", borderTop: false, borderBottom: false, children: [
1401
- view === "project_info" && /* @__PURE__ */ jsx7(ProjectInfoForm, { onSubmit: handleProjectInfo }),
1402
- view === "loading" && /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Spinner2, { label: "Analyzing your project and recommending a stack..." }) }),
1403
- view === "stage_list" && /* @__PURE__ */ jsx7(
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__ */ jsx7(ConversationView, { bridge, maxLines: contentHeight }),
1413
- view === "input" && /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
1414
- /* @__PURE__ */ jsx7(ConversationView, { bridge, maxLines: contentHeight - 3 }),
1415
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
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__ */ jsxs5(Box7, { flexDirection: "column", children: [
1424
- /* @__PURE__ */ jsx7(ConversationView, { bridge, maxLines: contentHeight - 8 }),
1425
- /* @__PURE__ */ jsx7(OptionSelect, { options, onSelect: handleOptionSelect })
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__ */ jsxs5(Box7, { flexDirection: "column", children: [
1428
- /* @__PURE__ */ jsxs5(Text7, { color: "red", bold: true, children: [
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__ */ jsx7(Text7, { dimColor: true, children: "Press Esc to return to stage list" })
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__ */ jsx7(
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(fresh = false) {
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 (fresh) {
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 existingSession = fresh ? null : StageManager.detect(cwd);
1955
- if (existingSession) {
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
- if (!resumed) {
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
- console.log("\nScaffolding your project...\n");
1985
- const success = await runScaffoldLoop(manager.progress);
1986
- if (success) {
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 args = process.argv.slice(2);
1997
- var command = args[0];
1998
- var isFresh = args.includes("--fresh");
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] [--fresh]");
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.2.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",