specstocode 0.5.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.
@@ -0,0 +1,84 @@
1
+ // src/lib/story-context.ts
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ var CONTEXT_DIR = ".specstocode/stories";
5
+ function storyContextPath(storyId) {
6
+ return join(process.cwd(), CONTEXT_DIR, `${storyId}.md`);
7
+ }
8
+ function writeStoryContext(story) {
9
+ const dir = join(process.cwd(), CONTEXT_DIR);
10
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
11
+ const acLines = story.acceptance_criteria.length ? story.acceptance_criteria.map((ac) => `- [${ac.done ? "x" : " "}] ${ac.text}`).join("\n") : "_No acceptance criteria defined._";
12
+ const specSection = story.notes?.trim() ? [`## Spec`, ``, story.notes.trim(), ``] : [];
13
+ const md = [
14
+ `# ${story.title}`,
15
+ ``,
16
+ `**ID:** \`${story.id}\``,
17
+ `**Activity:** ${story.activity}`,
18
+ `**Priority:** ${story.priority} | **Effort:** ${story.effort} | **Release:** ${story.release}`,
19
+ `**Status:** ${story.status}`,
20
+ ``,
21
+ `## User story`,
22
+ ``,
23
+ story.user_story ?? "_No user story defined._",
24
+ ``,
25
+ `## Done when`,
26
+ ``,
27
+ acLines,
28
+ ``,
29
+ ...specSection,
30
+ `## Approach`,
31
+ ``,
32
+ `_Plan your approach here before writing code._`,
33
+ ``,
34
+ `## Implementation notes`,
35
+ ``,
36
+ `_Notes added via \`npx specstocode note ${story.id.slice(0, 8)} "..."\`_`,
37
+ ``,
38
+ `## Decisions`,
39
+ ``,
40
+ `_Log decisions via \`npx specstocode decide "..." -s ${story.id.slice(0, 8)}\`_`,
41
+ ``,
42
+ `## Relevant files`,
43
+ ``,
44
+ `_Add file paths here as you explore the codebase._`,
45
+ ``,
46
+ `---`,
47
+ ``,
48
+ `Mark done: \`npx specstocode done ${story.id.slice(0, 8)}\``
49
+ ].join("\n");
50
+ const path = storyContextPath(story.id);
51
+ writeFileSync(path, md);
52
+ return path;
53
+ }
54
+ function appendNoteToContext(storyId, note) {
55
+ const path = storyContextPath(storyId);
56
+ if (!existsSync(path)) return false;
57
+ const content = readFileSync(path, "utf-8");
58
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
59
+ const noteEntry = `- [${timestamp}] ${note}`;
60
+ const marker = "## Implementation notes";
61
+ const markerIdx = content.indexOf(marker);
62
+ if (markerIdx === -1) return false;
63
+ const placeholder = "_Notes added via";
64
+ if (content.includes(placeholder)) {
65
+ writeFileSync(path, content.replace(
66
+ /^_Notes added via.*$/m,
67
+ `${noteEntry}`
68
+ ));
69
+ } else {
70
+ const nextSection = content.indexOf("\n## ", markerIdx + marker.length);
71
+ const insertAt = nextSection === -1 ? content.length : nextSection;
72
+ writeFileSync(
73
+ path,
74
+ content.slice(0, insertAt).trimEnd() + "\n" + noteEntry + "\n" + content.slice(insertAt)
75
+ );
76
+ }
77
+ return true;
78
+ }
79
+
80
+ export {
81
+ storyContextPath,
82
+ writeStoryContext,
83
+ appendNoteToContext
84
+ };
@@ -0,0 +1,39 @@
1
+ // src/lib/config.ts
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ var CONFIG_DIR = ".specstocode";
5
+ var CONFIG_FILE = "config.json";
6
+ function configPath() {
7
+ return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
8
+ }
9
+ function hasConfig() {
10
+ return existsSync(configPath());
11
+ }
12
+ function readConfig() {
13
+ try {
14
+ const raw = readFileSync(configPath(), "utf-8");
15
+ return JSON.parse(raw);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function writeConfig(config) {
21
+ const dir = join(process.cwd(), CONFIG_DIR);
22
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
23
+ writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n");
24
+ }
25
+ function requireConfig() {
26
+ const config = readConfig();
27
+ if (!config) {
28
+ console.error("Not initialized. Run: npx specstocode init");
29
+ process.exit(1);
30
+ }
31
+ return config;
32
+ }
33
+
34
+ export {
35
+ hasConfig,
36
+ readConfig,
37
+ writeConfig,
38
+ requireConfig
39
+ };
@@ -0,0 +1,249 @@
1
+ import {
2
+ API_BASE,
3
+ requireAuth
4
+ } from "./chunk-CYA6I7NV.js";
5
+ import {
6
+ getContext
7
+ } from "./chunk-QKMZ2SBR.js";
8
+ import {
9
+ ask,
10
+ closePrompt,
11
+ confirm
12
+ } from "./chunk-WPVDURTJ.js";
13
+ import {
14
+ writeConfig
15
+ } from "./chunk-NAOZWXOF.js";
16
+
17
+ // src/commands/scope.ts
18
+ import { writeFileSync, existsSync } from "fs";
19
+ import { join } from "path";
20
+ async function apiFetch(path, token, opts = {}) {
21
+ return fetch(`${API_BASE}${path}`, {
22
+ ...opts,
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ Authorization: `Bearer ${token}`,
26
+ ...opts.headers ?? {}
27
+ }
28
+ });
29
+ }
30
+ async function scope(projectId) {
31
+ const auth = requireAuth();
32
+ const { token } = auth;
33
+ if (!projectId) {
34
+ projectId = await ask(" Project ID (from `npx specstocode start`): ");
35
+ }
36
+ if (!projectId?.trim()) {
37
+ console.log(" Project ID required.");
38
+ closePrompt();
39
+ return;
40
+ }
41
+ console.log("\n \u{1F52C} Scoping your product\n");
42
+ console.log(" I'll ask a few questions to build your story map.\n");
43
+ const journey = await ask(
44
+ " What are the main steps a user takes through your product?\n (e.g. Sign up \u2192 Browse \u2192 Purchase \u2192 Review)\n > "
45
+ );
46
+ const personas = await ask(
47
+ "\n Who are the key personas? Describe 1-2 types of user.\n (e.g. 'Sarah, a busy marketing manager who needs...')\n > "
48
+ );
49
+ const mvp = await ask(
50
+ "\n What's the minimum you need to build to test the idea?\n (e.g. 'A simple landing page with a sign-up form and one core workflow')\n > "
51
+ );
52
+ const constraints = await ask(
53
+ "\n Any technical constraints or preferences?\n (e.g. 'Must be a web app, using React and Supabase')\n > "
54
+ );
55
+ console.log("\n Generating blueprint + story map...");
56
+ console.log(" This takes 1-3 minutes. Doing this manually takes hours.\n");
57
+ const scopingContext = [
58
+ journey ? `User journey: ${journey}` : "",
59
+ personas ? `Key personas: ${personas}` : "",
60
+ mvp ? `MVP scope: ${mvp}` : "",
61
+ constraints ? `Technical constraints: ${constraints}` : ""
62
+ ].filter(Boolean).join("\n");
63
+ const res = await apiFetch("/api/blueprint/generate", token, {
64
+ method: "POST",
65
+ body: JSON.stringify({
66
+ projectId,
67
+ phase: "generation",
68
+ idea: scopingContext,
69
+ messages: [
70
+ { role: "user", content: scopingContext }
71
+ ]
72
+ })
73
+ });
74
+ if (!res.ok) {
75
+ const errText = await res.text();
76
+ if (errText.includes("RATE_LIMIT")) {
77
+ console.error(" Rate limit reached. Try again later or upgrade your plan.");
78
+ } else {
79
+ console.error(` Generation failed: ${errText.slice(0, 200)}`);
80
+ }
81
+ closePrompt();
82
+ return;
83
+ }
84
+ const reader = res.body?.getReader();
85
+ const decoder = new TextDecoder();
86
+ let fullResponse = "";
87
+ let dots = 0;
88
+ if (reader) {
89
+ while (true) {
90
+ const { done, value } = await reader.read();
91
+ if (done) break;
92
+ fullResponse += decoder.decode(value, { stream: true });
93
+ dots++;
94
+ if (dots % 20 === 0) process.stdout.write(".");
95
+ }
96
+ }
97
+ console.log("\n");
98
+ try {
99
+ const cleaned = fullResponse.replace(/^\s*```json\s*/m, "").replace(/\s*```\s*$/m, "").trim();
100
+ const firstBrace = cleaned.indexOf("{");
101
+ const lastBrace = cleaned.lastIndexOf("}");
102
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
103
+ const bp = JSON.parse(cleaned.slice(firstBrace, lastBrace + 1));
104
+ console.log(" \u2713 Blueprint generated!\n");
105
+ if (bp.founding_hypothesis) {
106
+ console.log(` Hypothesis: ${bp.founding_hypothesis.slice(0, 150)}${bp.founding_hypothesis.length > 150 ? "..." : ""}
107
+ `);
108
+ }
109
+ if (bp.personas?.length) {
110
+ console.log(` Personas: ${bp.personas.map((p) => p.name).join(", ")}`);
111
+ }
112
+ if (bp.stories?.length) {
113
+ console.log(` Stories: ${bp.stories.length} user stories generated`);
114
+ }
115
+ if (bp.build_order?.length) {
116
+ console.log(` Build order: ${bp.build_order.length} steps`);
117
+ }
118
+ }
119
+ } catch {
120
+ console.log(" \u2713 Blueprint generated (couldn't parse summary)\n");
121
+ }
122
+ console.log("\n Connecting project to your codebase...");
123
+ const mapsRes = await apiFetch(
124
+ `/api/storymapper/project/${projectId}`,
125
+ token
126
+ );
127
+ let syncToken = null;
128
+ if (mapsRes.ok) {
129
+ const mapData = await mapsRes.json();
130
+ if (mapData.mapId) {
131
+ const tokenRes = await apiFetch(
132
+ `/api/storymapper/${mapData.mapId}/sync-token`,
133
+ token,
134
+ { method: "POST" }
135
+ );
136
+ if (tokenRes.ok) {
137
+ const tokenData = await tokenRes.json();
138
+ syncToken = tokenData.sync_token;
139
+ }
140
+ }
141
+ }
142
+ if (syncToken) {
143
+ writeConfig({ syncToken, apiBase: API_BASE });
144
+ console.log(" \u2713 Connected\n");
145
+ try {
146
+ const config = { syncToken, apiBase: API_BASE };
147
+ const md = await getContext(config);
148
+ writeFileSync(join(process.cwd(), "SPECSTOCODE.md"), md);
149
+ console.log(" \u2713 Downloaded SPECSTOCODE.md\n");
150
+ } catch {
151
+ console.log(" \u26A0 Could not download SPECSTOCODE.md\n");
152
+ }
153
+ }
154
+ const wantMockup = await confirm(
155
+ " Want to generate an HTML mockup of your product?"
156
+ );
157
+ if (wantMockup) {
158
+ const designSystem = await ask(
159
+ "\n Any existing design system? (e.g. Tailwind + shadcn, Material UI, or 'none')\n > "
160
+ );
161
+ const inspiration = await ask(
162
+ "\n Any products or sites that inspire the look & feel?\n > "
163
+ );
164
+ const mockupPrompt = buildMockupPrompt(projectId, designSystem, inspiration);
165
+ const mockupPath = join(process.cwd(), "MOCKUP_PROMPT.md");
166
+ writeFileSync(mockupPath, mockupPrompt);
167
+ console.log(`
168
+ \u2713 Saved MOCKUP_PROMPT.md
169
+ `);
170
+ console.log(" To generate the mockup, tell your AI coding tool:");
171
+ console.log(" 'Read MOCKUP_PROMPT.md and generate the HTML mockup'\n");
172
+ }
173
+ const hasGit = existsSync(join(process.cwd(), ".git"));
174
+ if (!hasGit) {
175
+ const wantRepo = await confirm(" Initialize a git repo for this project?");
176
+ if (wantRepo) {
177
+ const { execSync } = await import("child_process");
178
+ try {
179
+ execSync("git init", { stdio: "ignore" });
180
+ execSync("git add SPECSTOCODE.md", { stdio: "ignore" });
181
+ if (existsSync(join(process.cwd(), "MOCKUP_PROMPT.md"))) {
182
+ execSync("git add MOCKUP_PROMPT.md", { stdio: "ignore" });
183
+ }
184
+ execSync('git commit -m "Initial commit \u2014 specstocode project context"', {
185
+ stdio: "ignore"
186
+ });
187
+ console.log(" \u2713 Git repo initialized with first commit\n");
188
+ } catch {
189
+ console.log(" \u26A0 Git init failed \u2014 do it manually\n");
190
+ }
191
+ }
192
+ }
193
+ const { confirm: confirmSetup } = await import("./prompt-WAWCRGHN.js");
194
+ const wantSetup = await confirmSetup(
195
+ " Configure your AI tool (Claude Code / Cursor) with specstocode agent workflows?"
196
+ );
197
+ if (wantSetup) {
198
+ const { setup } = await import("./setup-VBFEFGTK.js");
199
+ await setup();
200
+ return;
201
+ }
202
+ console.log(`
203
+ \u2705 Your project is scoped and ready to build!
204
+
205
+ Next steps:
206
+ \u2022 npx specstocode setup \u2014 configure your AI tool
207
+ \u2022 npx specstocode status \u2014 see your story map progress
208
+ \u2022 npx specstocode next \u2014 see what to build first
209
+ \u2022 npx specstocode done <id> \u2014 mark stories as you complete them
210
+
211
+ Your story map is also live at ${API_BASE}
212
+ Seek advice from the community and our resources.
213
+ `);
214
+ closePrompt();
215
+ }
216
+ function buildMockupPrompt(projectId, designSystem, inspiration) {
217
+ return `# Mockup Generation Prompt
218
+
219
+ Read SPECSTOCODE.md for the full product context (personas, stories, acceptance criteria).
220
+
221
+ ## Task
222
+ Generate a single-file HTML mockup that demonstrates the core user journey.
223
+ The mockup should be a clickable prototype \u2014 not production code, but enough
224
+ to validate the UX with real users.
225
+
226
+ ## Requirements
227
+ - Single HTML file with inline CSS and minimal JS
228
+ - Mobile-first responsive design
229
+ - All key screens from the user journey (see SPECSTOCODE.md)
230
+ - Realistic placeholder content (not lorem ipsum)
231
+ - Navigation between screens via anchor links or simple JS
232
+
233
+ ## Design
234
+ ${designSystem && designSystem !== "none" ? `- Design system: ${designSystem}` : "- Use a clean, modern design with system fonts"}
235
+ ${inspiration ? `- Inspiration: ${inspiration}` : "- Keep it simple and functional"}
236
+ - Dark mode preferred
237
+ - Accessible (proper contrast, semantic HTML)
238
+
239
+ ## Output
240
+ Save as \`mockup.html\` in the project root.
241
+
242
+ ## Project ID
243
+ ${projectId}
244
+ `;
245
+ }
246
+
247
+ export {
248
+ scope
249
+ };
@@ -0,0 +1,39 @@
1
+ // src/lib/api.ts
2
+ function apiUrl(config, path) {
3
+ return `${config.apiBase}/api/sync/${config.syncToken}${path}`;
4
+ }
5
+ async function listStories(config) {
6
+ const res = await fetch(apiUrl(config, "/stories"));
7
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
8
+ return res.json();
9
+ }
10
+ async function getContext(config) {
11
+ const res = await fetch(apiUrl(config, "/context"));
12
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
13
+ return res.text();
14
+ }
15
+ async function updateStories(config, stories) {
16
+ const res = await fetch(apiUrl(config, "/stories"), {
17
+ method: "PATCH",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ stories })
20
+ });
21
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
22
+ return res.json();
23
+ }
24
+ async function createStories(config, stories) {
25
+ const res = await fetch(apiUrl(config, "/stories"), {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ stories })
29
+ });
30
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
31
+ return res.json();
32
+ }
33
+
34
+ export {
35
+ listStories,
36
+ getContext,
37
+ updateStories,
38
+ createStories
39
+ };
@@ -0,0 +1,79 @@
1
+ // src/lib/prompt.ts
2
+ import { createInterface } from "readline";
3
+ var rl = null;
4
+ function getRL() {
5
+ if (!rl) {
6
+ rl = createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+ }
11
+ return rl;
12
+ }
13
+ function ask(question) {
14
+ return new Promise((resolve) => {
15
+ getRL().question(question, (answer) => {
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+ function askMultiline(question) {
21
+ console.log(question);
22
+ console.log(" (Press Enter twice to finish)\n");
23
+ return new Promise((resolve) => {
24
+ const lines = [];
25
+ let emptyCount = 0;
26
+ const handler = (line) => {
27
+ if (line === "") {
28
+ emptyCount++;
29
+ if (emptyCount >= 2) {
30
+ process.stdin.removeListener("line", handler);
31
+ resolve(lines.join("\n").trim());
32
+ return;
33
+ }
34
+ } else {
35
+ emptyCount = 0;
36
+ }
37
+ lines.push(line);
38
+ };
39
+ process.stdin.on("line", handler);
40
+ });
41
+ }
42
+ async function confirm(question) {
43
+ const answer = await ask(`${question} (y/n) `);
44
+ return answer.toLowerCase().startsWith("y");
45
+ }
46
+ async function choose(question, options) {
47
+ console.log(`
48
+ ${question}
49
+ `);
50
+ for (let i = 0; i < options.length; i++) {
51
+ console.log(` ${i + 1}. ${options[i]}`);
52
+ }
53
+ const answer = await ask(`
54
+ Choice (1-${options.length}): `);
55
+ const idx = parseInt(answer, 10) - 1;
56
+ if (idx < 0 || idx >= options.length) return 0;
57
+ return idx;
58
+ }
59
+ function closePrompt() {
60
+ if (rl) {
61
+ rl.close();
62
+ rl = null;
63
+ }
64
+ }
65
+ function pausePrompt() {
66
+ if (rl) {
67
+ rl.close();
68
+ rl = null;
69
+ }
70
+ }
71
+
72
+ export {
73
+ ask,
74
+ askMultiline,
75
+ confirm,
76
+ choose,
77
+ closePrompt,
78
+ pausePrompt
79
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ listStories
3
+ } from "./chunk-QKMZ2SBR.js";
4
+ import {
5
+ requireConfig
6
+ } from "./chunk-NAOZWXOF.js";
7
+
8
+ // src/commands/complexity.ts
9
+ import ora from "ora";
10
+ async function complexity(storyId) {
11
+ const config = requireConfig();
12
+ const spinner = ora(" Analyzing complexity...").start();
13
+ try {
14
+ const data = await listStories(config);
15
+ let storyIds;
16
+ if (storyId) {
17
+ const match = data.stories.find((s) => s.id.startsWith(storyId));
18
+ if (!match) {
19
+ spinner.fail(` No story found matching "${storyId}"`);
20
+ return;
21
+ }
22
+ storyIds = [match.id];
23
+ } else {
24
+ storyIds = data.stories.filter((s) => s.status !== "done").map((s) => s.id);
25
+ }
26
+ if (!storyIds.length) {
27
+ spinner.succeed(" No stories to analyze \u2014 all done!");
28
+ return;
29
+ }
30
+ const res = await fetch(
31
+ `${config.apiBase}/api/sync/${config.syncToken}/complexity`,
32
+ {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({ story_ids: storyIds })
36
+ }
37
+ );
38
+ if (!res.ok) {
39
+ spinner.fail(" Complexity analysis failed");
40
+ return;
41
+ }
42
+ const result = await res.json();
43
+ spinner.succeed(` Analyzed ${result.stories.length} stories
44
+ `);
45
+ const sorted = result.stories.sort((a, b) => b.complexity - a.complexity);
46
+ for (const s of sorted) {
47
+ const bar = "\u2588".repeat(s.complexity) + "\u2591".repeat(10 - s.complexity);
48
+ const color = s.complexity >= 8 ? "\x1B[31m" : s.complexity >= 5 ? "\x1B[33m" : "\x1B[32m";
49
+ const reset = "\x1B[0m";
50
+ console.log(` ${color}${bar}${reset} ${s.complexity}/10 ${s.title}`);
51
+ console.log(` ${s.reasoning}`);
52
+ if (s.risks?.length) {
53
+ console.log(` \u26A0 ${s.risks.join(", ")}`);
54
+ }
55
+ console.log(` Suggested effort: ${s.suggested_effort}
56
+ `);
57
+ }
58
+ const avg = sorted.reduce((a, s) => a + s.complexity, 0) / sorted.length;
59
+ const high = sorted.filter((s) => s.complexity >= 7).length;
60
+ console.log(` \u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
61
+ console.log(` Average complexity: ${avg.toFixed(1)}/10`);
62
+ console.log(` High complexity (7+): ${high} stories`);
63
+ console.log(` Total analyzed: ${sorted.length}
64
+ `);
65
+ } catch (err) {
66
+ spinner.fail(` Error: ${err.message}`);
67
+ }
68
+ }
69
+ export {
70
+ complexity
71
+ };
@@ -0,0 +1,114 @@
1
+ import {
2
+ closePrompt,
3
+ confirm
4
+ } from "./chunk-WPVDURTJ.js";
5
+ import {
6
+ requireConfig
7
+ } from "./chunk-NAOZWXOF.js";
8
+
9
+ // src/commands/import-prd.ts
10
+ import { readFileSync, existsSync } from "fs";
11
+ import ora from "ora";
12
+ async function importPrd(filePath) {
13
+ const config = requireConfig();
14
+ let prdText;
15
+ let format = "PRD";
16
+ if (filePath) {
17
+ if (!existsSync(filePath)) {
18
+ console.error(` File not found: ${filePath}`);
19
+ return;
20
+ }
21
+ prdText = readFileSync(filePath, "utf-8");
22
+ if (filePath.endsWith(".md")) format = "Markdown";
23
+ else if (filePath.endsWith(".txt")) format = "text";
24
+ } else {
25
+ console.log(" Paste your PRD below (press Ctrl+D when done):\n");
26
+ const chunks = [];
27
+ for await (const chunk of process.stdin) {
28
+ chunks.push(chunk);
29
+ }
30
+ prdText = Buffer.concat(chunks).toString("utf-8");
31
+ }
32
+ if (!prdText.trim()) {
33
+ console.error(" No content to import.");
34
+ return;
35
+ }
36
+ console.log(`
37
+ Importing ${format} (${prdText.length} chars)...
38
+ `);
39
+ const spinner = ora(" Parsing into stories...").start();
40
+ try {
41
+ const res = await fetch(
42
+ `${config.apiBase}/api/sync/${config.syncToken}/import`,
43
+ {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ prd: prdText, format })
47
+ }
48
+ );
49
+ if (!res.ok) {
50
+ spinner.fail(" Import failed");
51
+ const errText = await res.text();
52
+ console.error(` ${errText.slice(0, 200)}`);
53
+ return;
54
+ }
55
+ const result = await res.json();
56
+ spinner.succeed(` Parsed ${result.count} stories
57
+ `);
58
+ const byActivity = /* @__PURE__ */ new Map();
59
+ for (const story of result.stories) {
60
+ const activity = story.activity || "Uncategorised";
61
+ if (!byActivity.has(activity)) byActivity.set(activity, []);
62
+ byActivity.get(activity).push(story);
63
+ }
64
+ for (const [activity, stories] of byActivity) {
65
+ console.log(` ${activity}:`);
66
+ for (const s of stories) {
67
+ const priorityColor = s.priority === "must" ? "\x1B[31m" : s.priority === "should" ? "\x1B[33m" : "\x1B[32m";
68
+ const reset = "\x1B[0m";
69
+ console.log(` ${priorityColor}[${s.priority}]${reset} ${s.title} (${s.effort})`);
70
+ if (s.acceptance_criteria?.length) {
71
+ console.log(` ${s.acceptance_criteria.length} acceptance criteria`);
72
+ }
73
+ }
74
+ console.log();
75
+ }
76
+ const doImport = await confirm(
77
+ ` Import these ${result.count} stories into your story map?`
78
+ );
79
+ if (doImport) {
80
+ const importSpinner = ora(" Creating stories...").start();
81
+ const createRes = await fetch(
82
+ `${config.apiBase}/api/sync/${config.syncToken}/stories`,
83
+ {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({
87
+ stories: result.stories.map((s) => ({
88
+ title: s.title,
89
+ activity: s.activity,
90
+ priority: s.priority,
91
+ effort: s.effort,
92
+ acceptance_criteria: s.acceptance_criteria
93
+ }))
94
+ })
95
+ }
96
+ );
97
+ if (createRes.ok) {
98
+ const createResult = await createRes.json();
99
+ importSpinner.succeed(` \u2713 Created ${createResult.created} stories in your story map
100
+ `);
101
+ } else {
102
+ importSpinner.fail(" Failed to create stories");
103
+ }
104
+ } else {
105
+ console.log(" Import cancelled.\n");
106
+ }
107
+ closePrompt();
108
+ } catch (err) {
109
+ spinner.fail(` Error: ${err.message}`);
110
+ }
111
+ }
112
+ export {
113
+ importPrd
114
+ };