intentdocs 0.11.0 → 0.12.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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-KPQ42YE6.js";
5
5
  import {
6
6
  requireConfig
7
- } from "./chunk-K5SNF6CG.js";
7
+ } from "./chunk-RGFGTIIH.js";
8
8
  import {
9
9
  closePrompt,
10
10
  confirm,
@@ -5,7 +5,7 @@ import {
5
5
  checkoutConfig,
6
6
  hasConfig,
7
7
  writeConfig
8
- } from "./chunk-K5SNF6CG.js";
8
+ } from "./chunk-RGFGTIIH.js";
9
9
  import {
10
10
  ask,
11
11
  closePrompt,
@@ -196,7 +196,7 @@ async function scope(projectId) {
196
196
  " Configure your AI tool (Claude Code / Cursor) with intentdocs agent workflows?"
197
197
  );
198
198
  if (wantSetup) {
199
- const { setup } = await import("./setup-UILQVYIB.js");
199
+ const { setup } = await import("./setup-W4PKG6GB.js");
200
200
  await setup();
201
201
  return;
202
202
  }
@@ -0,0 +1,536 @@
1
+ import {
2
+ canonicalApiBase
3
+ } from "./chunk-UYTVTYNB.js";
4
+
5
+ // src/lib/intent-folder.ts
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
7
+ import { basename, dirname, join, relative } from "path";
8
+ import { parse } from "yaml";
9
+ var VISION_SECTIONS = ["Users", "Problem", "Switch", "First", "Proof"];
10
+ var STORY_ID = /^[A-Z][A-Z0-9]{0,9}-[0-9]+$/;
11
+ var PRIORITIES = ["must", "should", "could", "wont"];
12
+ var STATUS = { todo: "todo", in_progress: "in_progress", needs_rework: "needs_rework", done: "dev_complete" };
13
+ function findIntentDir(start) {
14
+ return existsSync(join(start, "storymap.yaml")) ? start : join(start, "intent");
15
+ }
16
+ function walk(dir, ext) {
17
+ if (!existsSync(dir)) return [];
18
+ return readdirSync(dir).sort().flatMap((f) => {
19
+ const p = join(dir, f);
20
+ if (statSync(p).isDirectory()) return walk(p, ext);
21
+ return p.endsWith(ext) ? [p] : [];
22
+ });
23
+ }
24
+ function frontMatter(text) {
25
+ const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
26
+ if (!m) return null;
27
+ const meta = parse(m[1]) ?? {};
28
+ return { meta, body: text.slice(m[0].length) };
29
+ }
30
+ function sections(body) {
31
+ return [...body.matchAll(/^## (.+)\n([\s\S]*?)(?=^## |(?![\s\S]))/gm)].map((s) => ({ title: s[1].trim(), text: s[2].trim() }));
32
+ }
33
+ function tagsOf(line) {
34
+ return line.trim().split(/\s+/).filter((t) => t.startsWith("@")).map((t) => t.slice(1));
35
+ }
36
+ var EFFORTS = ["XS", "S", "M", "L", "XL"];
37
+ function effortOf(tags) {
38
+ const tag = tags.find((t) => t.startsWith("effort-"))?.slice("effort-".length).toUpperCase();
39
+ if (!tag) return null;
40
+ return EFFORTS.includes(tag) ? tag : "M";
41
+ }
42
+ function priorityOf(tags) {
43
+ return tags.find((t) => PRIORITIES.includes(t)) ?? null;
44
+ }
45
+ function featureFiles(intentDir) {
46
+ const files = /* @__PURE__ */ new Map();
47
+ for (const path of walk(join(intentDir, "features"), ".feature")) {
48
+ const parsed = parseFeature(readFileSync(path, "utf-8"));
49
+ const key = parsed?.tags.find((t) => /^[A-Z][A-Z0-9]*-\d+$/.test(t));
50
+ if (key && !files.has(key)) files.set(key, path);
51
+ }
52
+ return files;
53
+ }
54
+ function stepsSentence(steps) {
55
+ return steps.map(({ keyword, text }, i) => {
56
+ const k = keyword.toLowerCase();
57
+ if (k === "and" || k === "but") return `${k} ${text}`;
58
+ return i === 0 ? `${keyword} ${text}` : `, ${k} ${text}`;
59
+ }).join(" ").replace(/ ,/g, ",");
60
+ }
61
+ function parseFeature(text) {
62
+ const lines = text.split("\n");
63
+ const featureAt = lines.findIndex((l) => l.trim().startsWith("Feature:"));
64
+ if (featureAt < 0) return null;
65
+ const tags = lines.slice(0, featureAt).flatMap(tagsOf);
66
+ const title = lines[featureAt].trim().slice("Feature:".length).trim();
67
+ let asA = "";
68
+ let iWant = "";
69
+ let soThat = "";
70
+ const criteria = [];
71
+ let pendingTags = [];
72
+ let current = null;
73
+ const flush = () => {
74
+ if (current) criteria.push({ label: current.label, title: current.title, text: stepsSentence(current.steps), priority: current.priority });
75
+ current = null;
76
+ };
77
+ for (const raw of lines.slice(featureAt + 1)) {
78
+ const line = raw.trim();
79
+ if (!line || line.startsWith("#")) continue;
80
+ if (line.startsWith("@")) {
81
+ pendingTags.push(...tagsOf(line));
82
+ continue;
83
+ }
84
+ const scenario = /^Scenario(?: Outline)?:\s*(.*)$/.exec(line);
85
+ if (scenario) {
86
+ flush();
87
+ const name = scenario[1].trim();
88
+ const label = pendingTags.find((t) => /^AC\d+$/.test(t)) ?? /^(AC\d+)\b/.exec(name)?.[1] ?? "";
89
+ current = { label, title: name.replace(/^AC\d+\s*[—–-]\s*/, ""), priority: priorityOf(pendingTags), steps: [] };
90
+ pendingTags = [];
91
+ continue;
92
+ }
93
+ const step = /^(Given|When|Then|And|But)\s+(.*)$/.exec(line);
94
+ if (step && current) {
95
+ current.steps.push({ keyword: step[1], text: step[2] });
96
+ continue;
97
+ }
98
+ if (!current) {
99
+ const story = /^(As (?:a|an|the)\b.*|I want\b.*|So that\b.*)$/i.exec(line);
100
+ if (story) {
101
+ const s = story[1];
102
+ if (/^as /i.test(s)) asA = s.replace(/^As (a|an|the)\s+/i, "");
103
+ else if (/^i want/i.test(s)) iWant = s.replace(/^I want( to)?\s+/i, "");
104
+ else soThat = s.replace(/^So that\s+/i, "");
105
+ }
106
+ }
107
+ }
108
+ flush();
109
+ return { tags, title, asA, iWant, soThat, criteria };
110
+ }
111
+ function parsePlanSteps(body) {
112
+ return sections(body).map(({ title, text }) => {
113
+ const field = (name) => new RegExp(`^\\*\\*${name}:\\*\\*\\s*(.*)$`, "m").exec(text)?.[1]?.trim() ?? "";
114
+ const list = (name) => field(name).split(",").map((s) => s.trim()).filter(Boolean);
115
+ const notes = text.split("\n").filter((l) => !/^\*\*(Why|Criteria|Files|Tests|Needs you):\*\*/.test(l.trim())).join("\n").trim();
116
+ const needs = field("Needs you");
117
+ return {
118
+ title: title.replace(/^\d+\.\s*/, ""),
119
+ why: field("Why"),
120
+ criteria: list("Criteria"),
121
+ files: list("Files"),
122
+ tests: list("Tests"),
123
+ needs_you: needs ? { question: needs } : null,
124
+ agent_notes: notes
125
+ };
126
+ });
127
+ }
128
+ function parseDecisions(text) {
129
+ const uncommented = text.replace(/<!--[\s\S]*?-->/g, "");
130
+ return sections(uncommented).map(({ title, text: body }) => {
131
+ const heading = /^(\d{4}-\d{2}-\d{2})\s*[—–-]\s*(.*?)(?:\s+\(([A-Z][A-Z0-9]{0,9}-\d+)\))?$/.exec(title);
132
+ const category = /^\*\*Category:\*\*\s*(.*)$/m.exec(body)?.[1]?.trim() ?? "architecture";
133
+ const description = body.split("\n").filter((l) => !/^\*\*(Category|Supersedes):\*\*/.test(l.trim())).join("\n").trim();
134
+ return {
135
+ title: heading?.[2] ?? title,
136
+ description,
137
+ category,
138
+ date: heading?.[1] ?? "",
139
+ story: heading?.[3] ?? null
140
+ };
141
+ });
142
+ }
143
+ function readIntentFolder(start) {
144
+ const dir = findIntentDir(start);
145
+ const errors = [];
146
+ const fail = () => ({ ok: false, dir, errors });
147
+ const file = (name) => join(dir, name);
148
+ if (!existsSync(dir)) return { ok: false, dir, errors: ["No intent/ folder here. Run `npx nikos init` first, or push from the repo root."] };
149
+ for (const name of ["constitution.md", "storymap.yaml", "state.yaml"]) {
150
+ if (!existsSync(file(name))) errors.push(`${name} is missing`);
151
+ }
152
+ if (errors.length) return fail();
153
+ let storymap;
154
+ let state;
155
+ try {
156
+ storymap = parse(readFileSync(file("storymap.yaml"), "utf-8")) ?? {};
157
+ state = parse(readFileSync(file("state.yaml"), "utf-8")) ?? {};
158
+ } catch (err) {
159
+ errors.push(`invalid YAML: ${err.message.split("\n")[0]}`);
160
+ return fail();
161
+ }
162
+ const constitution = frontMatter(readFileSync(file("constitution.md"), "utf-8"));
163
+ const vision = {};
164
+ if (!constitution) {
165
+ errors.push("constitution.md has no front matter (name and idea)");
166
+ } else {
167
+ for (const key of ["name", "idea"]) {
168
+ if (typeof constitution.meta[key] !== "string" || !constitution.meta[key].trim()) errors.push(`constitution.md front matter is missing '${key}'`);
169
+ }
170
+ const found = sections(constitution.body);
171
+ for (const name of VISION_SECTIONS) {
172
+ const text = found.find((s) => s.title === name)?.text ?? "";
173
+ if (!text) errors.push(`constitution.md ## ${name} is missing or empty`);
174
+ vision[name.toLowerCase()] = text;
175
+ }
176
+ }
177
+ if (!storymap.key || !/^[A-Z]{2,5}$/.test(storymap.key)) errors.push("storymap.yaml needs a key of 2\u20135 capital letters, like HT");
178
+ const activities = new Map((storymap.activities ?? []).map((a) => [a.id, a.title]));
179
+ const releaseOf = /* @__PURE__ */ new Map();
180
+ for (const r of storymap.releases ?? []) for (const id of r.stories ?? []) releaseOf.set(id, r.number);
181
+ const features = /* @__PURE__ */ new Map();
182
+ for (const path of walk(file("features"), ".feature")) {
183
+ const where = relative(dir, path);
184
+ const parsed = parseFeature(readFileSync(path, "utf-8"));
185
+ if (!parsed) {
186
+ errors.push(`${where} has no Feature:`);
187
+ continue;
188
+ }
189
+ const ids = parsed.tags.filter((t) => STORY_ID.test(t));
190
+ if (ids.length !== 1) {
191
+ errors.push(`${where} needs exactly one story id tag (found ${ids.length})`);
192
+ continue;
193
+ }
194
+ features.set(ids[0], { path, parsed });
195
+ }
196
+ const stories = [];
197
+ for (const [key, release] of releaseOf) {
198
+ const feature = features.get(key);
199
+ if (!feature) {
200
+ errors.push(`${key} is in storymap.yaml but has no feature file`);
201
+ continue;
202
+ }
203
+ const { parsed, path } = feature;
204
+ const where = relative(dir, path);
205
+ const activityId = parsed.tags.find((t) => t.startsWith("activity-"))?.slice("activity-".length) ?? basename(dirname(path));
206
+ const activity = activities.get(activityId);
207
+ if (!activity) errors.push(`${where}: activity '${activityId}' is not in storymap.yaml`);
208
+ if (!parsed.asA || !parsed.iWant || !parsed.soThat) errors.push(`${where}: needs "As a / I want / So that" under Feature:`);
209
+ if (!parsed.criteria.length) errors.push(`${where}: has no scenarios`);
210
+ for (const c of parsed.criteria) if (!c.label) errors.push(`${where}: scenario "${c.title}" has no @AC label`);
211
+ const entry = state.stories?.[key];
212
+ if (!entry) errors.push(`state.yaml has no entry for ${key}`);
213
+ const status = STATUS[entry?.status ?? "todo"];
214
+ if (!status) errors.push(`state.yaml ${key} has unknown status '${entry?.status}'`);
215
+ stories.push({
216
+ key,
217
+ title: parsed.title,
218
+ as_a: parsed.asA,
219
+ i_want: parsed.iWant,
220
+ so_that: parsed.soThat,
221
+ priority: priorityOf(parsed.tags),
222
+ release,
223
+ activity: activity ?? activityId,
224
+ activity_id: activityId,
225
+ effort: effortOf(parsed.tags),
226
+ status: status ?? "todo",
227
+ blockers: entry?.blockers ?? [],
228
+ claimed_by: entry?.claimed_by ?? null,
229
+ plan_version: entry?.plan_version ?? null,
230
+ criteria: parsed.criteria
231
+ });
232
+ }
233
+ for (const key of features.keys()) if (!releaseOf.has(key)) errors.push(`${key} has a feature file but is in no release in storymap.yaml`);
234
+ const plans = [];
235
+ for (const path of walk(file("plans"), ".md")) {
236
+ const m = /^([A-Z][A-Z0-9]{0,9}-\d+)\.v(\d+)\.md$/.exec(basename(path));
237
+ if (!m) continue;
238
+ const fm = frontMatter(readFileSync(path, "utf-8"));
239
+ if (!fm) {
240
+ errors.push(`plans/${basename(path)} has no front matter`);
241
+ continue;
242
+ }
243
+ plans.push({
244
+ story: m[1],
245
+ version: Number(m[2]),
246
+ summary: typeof fm.meta.summary === "string" ? fm.meta.summary : "",
247
+ risk: typeof fm.meta.risk === "string" ? fm.meta.risk : null,
248
+ introduces: Array.isArray(fm.meta.introduces) ? fm.meta.introduces.map(String) : [],
249
+ steps: parsePlanSteps(fm.body)
250
+ });
251
+ }
252
+ plans.sort((a, b) => a.story.localeCompare(b.story) || a.version - b.version);
253
+ const decisions = existsSync(file("decisions.md")) ? parseDecisions(readFileSync(file("decisions.md"), "utf-8")) : [];
254
+ if (errors.length) return fail();
255
+ return {
256
+ ok: true,
257
+ dir,
258
+ payload: {
259
+ format_version: 1,
260
+ project: { name: constitution.meta.name, idea: constitution.meta.idea, key: storymap.key, vision },
261
+ releases: (storymap.releases ?? []).map((r) => ({ number: r.number, title: r.title ?? `Release ${r.number}`, stories: r.stories ?? [] })),
262
+ stories,
263
+ plans,
264
+ decisions,
265
+ run_queue: state.run?.queue ?? []
266
+ }
267
+ };
268
+ }
269
+
270
+ // src/lib/pull-feature.ts
271
+ var emptyBase = () => ({ stories: {}, releases: {}, activities: {}, plans: {}, statuses: {} });
272
+ var storyPriority = (p) => p === "must" ? "must" : p === "could" || p === "wont" ? "could" : "should";
273
+ var criterionPriority = (p) => p === "must" ? "must" : "should";
274
+ var effortOrM = (e) => effortOf(e ? [`effort-${e}`] : []) ?? "M";
275
+ var slug = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "activity";
276
+ function specOfPushStory(s) {
277
+ return {
278
+ title: s.title,
279
+ as_a: s.as_a,
280
+ i_want: s.i_want,
281
+ so_that: s.so_that,
282
+ priority: storyPriority(s.priority),
283
+ effort: effortOrM(s.effort),
284
+ release: s.release,
285
+ activity: s.activity_id,
286
+ criteria: s.criteria.map((c) => ({ text: c.text.trim(), priority: criterionPriority(c.priority) }))
287
+ };
288
+ }
289
+ function userStoryParts(story) {
290
+ if (story.as_a != null || story.i_want != null || story.so_that != null) {
291
+ return { as_a: story.as_a ?? "", i_want: story.i_want ?? "", so_that: story.so_that ?? "" };
292
+ }
293
+ const m = /^As an? (.*), I want (.*), so that (.*)$/s.exec(story.user_story ?? "");
294
+ return { as_a: m?.[1] ?? "", i_want: m?.[2] ?? "", so_that: m?.[3] ?? "" };
295
+ }
296
+ function activityIdOf(ref, title, local = []) {
297
+ if (ref?.startsWith("nikos:")) return ref.slice("nikos:".length);
298
+ return local.find((a) => a.title === title)?.id ?? slug(title);
299
+ }
300
+ function specOfStory(story, localActivities = []) {
301
+ return {
302
+ title: story.title,
303
+ ...userStoryParts(story),
304
+ priority: storyPriority(story.priority),
305
+ effort: effortOrM(story.effort),
306
+ release: story.release,
307
+ activity: activityIdOf(story.activity_ref, story.activity, localActivities),
308
+ criteria: story.acceptance_criteria.filter((c) => !c.cancelled_at).map((c) => ({ text: c.text.trim(), priority: criterionPriority(c.priority) }))
309
+ };
310
+ }
311
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
312
+ function classify(local, remote, base) {
313
+ if (same(local, remote)) return null;
314
+ if (base === void 0) return "unrecorded";
315
+ if (same(local, base)) return "web";
316
+ if (same(remote, base)) return "local";
317
+ return "clash";
318
+ }
319
+ function sentenceSteps(text) {
320
+ const gwt = /^Given (.+?), when (.+?), then (.+)$/is.exec(text);
321
+ if (gwt) return [{ keyword: "Given", text: gwt[1] }, { keyword: "When", text: gwt[2] }, { keyword: "Then", text: gwt[3] }];
322
+ const wt = /^When (.+?), then (.+)$/is.exec(text);
323
+ if (wt) return [{ keyword: "When", text: wt[1] }, { keyword: "Then", text: wt[2] }];
324
+ return [{ keyword: "Then", text: text.replace(/^then\s+/i, "") }];
325
+ }
326
+ var PRIORITY_TAG = /^@(must|should|could|wont)$/;
327
+ var isTagLine = (l) => l.trim().startsWith("@");
328
+ var isScenario = (l) => /^\s*Scenario(?: Outline)?:/.test(l);
329
+ var indentOf = (l) => /^\s*/.exec(l)[0];
330
+ function withPriority(line, priority, equal) {
331
+ const tokens = line.trim().split(/\s+/);
332
+ const at = tokens.findIndex((t) => PRIORITY_TAG.test(t));
333
+ if (at >= 0) {
334
+ if (equal(tokens[at].slice(1))) return line;
335
+ tokens[at] = `@${priority}`;
336
+ } else {
337
+ tokens.push(`@${priority}`);
338
+ }
339
+ return indentOf(line) + tokens.join(" ");
340
+ }
341
+ function scenarioBlocks(lines) {
342
+ const blocks = [];
343
+ let current = [];
344
+ const close = () => {
345
+ while (current.length && !current[current.length - 1].trim()) current.pop();
346
+ if (!current.length) return;
347
+ const steps = current.map((l) => /^\s*(Given|When|Then|And|But)\s+(.*)$/.exec(l)).filter((m) => !!m).map((m) => ({ keyword: m[1], text: m[2] }));
348
+ const label = current.join(" ").match(/\bAC(\d+)\b/);
349
+ blocks.push({ lines: current, sentence: stepsSentence(steps).trim(), label: label ? Number(label[1]) : null });
350
+ current = [];
351
+ };
352
+ for (const line of lines) {
353
+ const last = [...current].reverse().find((l) => l.trim());
354
+ if ((isTagLine(line) || isScenario(line)) && last !== void 0 && !isTagLine(last)) close();
355
+ current.push(line);
356
+ }
357
+ close();
358
+ return blocks;
359
+ }
360
+ function renderFeature(text, remote) {
361
+ const lines = text.split("\n");
362
+ const featureAt = lines.findIndex((l) => l.trim().startsWith("Feature:"));
363
+ if (featureAt < 0) return text;
364
+ const head = lines.slice(0, featureAt).map((line, n) => {
365
+ if (!isTagLine(line)) return line;
366
+ let out2 = line;
367
+ if (n === lines.findIndex(isTagLine)) {
368
+ out2 = withPriority(out2, remote.priority, (tag) => storyPriority(tag) === remote.priority);
369
+ const tokens = out2.trim().split(/\s+/);
370
+ const at = tokens.findIndex((t) => t.startsWith("@effort-"));
371
+ if (at >= 0 && effortOrM(tokens[at].slice("@effort-".length)) !== remote.effort) tokens[at] = `@effort-${remote.effort}`;
372
+ if (at < 0 && remote.effort !== "M") tokens.push(`@effort-${remote.effort}`);
373
+ const swap = (prefix, value) => {
374
+ const i = tokens.findIndex((t) => t.startsWith(`@${prefix}`));
375
+ if (i >= 0 && tokens[i] !== `@${prefix}${value}`) tokens[i] = `@${prefix}${value}`;
376
+ };
377
+ swap("release-", String(remote.release));
378
+ swap("activity-", remote.activity);
379
+ out2 = indentOf(out2) + tokens.join(" ");
380
+ }
381
+ return out2;
382
+ });
383
+ const featureLine = lines[featureAt];
384
+ const parsedTitle = featureLine.trim().slice("Feature:".length).trim();
385
+ const feature = parsedTitle === remote.title ? featureLine : `${indentOf(featureLine)}Feature: ${remote.title}`;
386
+ const bodyStart = featureAt + 1;
387
+ let scenariosAt = lines.findIndex((l, n) => n >= bodyStart && (isScenario(l) || isTagLine(l)));
388
+ if (scenariosAt < 0) scenariosAt = lines.length;
389
+ const intro = lines.slice(bodyStart, scenariosAt).map((line) => {
390
+ const as = /^(\s*)As (a|an|the)\s+(.*)$/i.exec(line);
391
+ if (as) return as[3] === remote.as_a ? line : `${as[1]}As ${as[2]} ${remote.as_a}`;
392
+ const want = /^(\s*)I want( to)?\s+(.*)$/i.exec(line);
393
+ if (want) return want[3] === remote.i_want ? line : `${want[1]}I want${want[2] ?? ""} ${remote.i_want}`;
394
+ const so = /^(\s*)So that\s+(.*)$/i.exec(line);
395
+ if (so) return so[2] === remote.so_that ? line : `${so[1]}So that ${remote.so_that}`;
396
+ return line;
397
+ });
398
+ while (intro.length && !intro[intro.length - 1].trim()) intro.pop();
399
+ const blocks = scenarioBlocks(lines.slice(scenariosAt));
400
+ const scenarioIndent = indentOf(blocks[0]?.lines.find(isScenario) ?? " Scenario:") || " ";
401
+ const stepIndent = scenarioIndent + " ";
402
+ let nextLabel = Math.max(0, ...blocks.map((b) => b.label ?? 0)) + 1;
403
+ const used = /* @__PURE__ */ new Set();
404
+ const rendered = remote.criteria.map((criterion) => {
405
+ const kept = blocks.find((b) => !used.has(b) && b.sentence === criterion.text);
406
+ if (kept) {
407
+ used.add(kept);
408
+ const tagAt = kept.lines.findIndex(isTagLine);
409
+ if (tagAt < 0) return kept.lines.join("\n");
410
+ const out2 = [...kept.lines];
411
+ out2[tagAt] = withPriority(out2[tagAt], criterion.priority, (tag) => criterionPriority(tag) === criterion.priority);
412
+ return out2.join("\n");
413
+ }
414
+ const label = nextLabel++;
415
+ const steps = sentenceSteps(criterion.text);
416
+ const then = steps[steps.length - 1].text;
417
+ return [
418
+ `${scenarioIndent}@AC${label} @${criterion.priority}`,
419
+ `${scenarioIndent}Scenario: AC${label} \u2014 ${then.charAt(0).toUpperCase()}${then.slice(1)}`,
420
+ ...steps.map((s) => `${stepIndent}${s.keyword} ${s.text}`)
421
+ ].join("\n");
422
+ });
423
+ const top = [...head, feature, ...intro].join("\n");
424
+ const out = rendered.length ? `${top}
425
+
426
+ ${rendered.join("\n\n")}` : top;
427
+ return text.endsWith("\n") ? `${out}
428
+ ` : out;
429
+ }
430
+ function newFeature(key, remote) {
431
+ const tags = [`@${key}`, `@release-${remote.release}`, `@${remote.priority}`, `@activity-${remote.activity}`, `@effort-${remote.effort}`];
432
+ const scenarios = remote.criteria.map((c, i) => {
433
+ const steps = sentenceSteps(c.text);
434
+ const then = steps[steps.length - 1].text;
435
+ return [
436
+ ` @AC${i + 1} @${c.priority}`,
437
+ ` Scenario: AC${i + 1} \u2014 ${then.charAt(0).toUpperCase()}${then.slice(1)}`,
438
+ ...steps.map((st) => ` ${st.keyword} ${st.text}`)
439
+ ].join("\n");
440
+ });
441
+ const head = [tags.join(" "), `Feature: ${remote.title}`, "", ` As a ${remote.as_a}`, ` I want ${remote.i_want}`, ` So that ${remote.so_that}`].join("\n");
442
+ return `${[head, ...scenarios].join("\n\n")}
443
+ `;
444
+ }
445
+
446
+ // src/lib/config.ts
447
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
448
+ import { join as join2, dirname as dirname2 } from "path";
449
+ function checkoutConfig(input) {
450
+ return {
451
+ apiBase: input.apiBase,
452
+ mapTitle: input.map.title,
453
+ projectId: input.project.id,
454
+ mapId: input.map.id,
455
+ workspace: input.project.workspace ? { orgId: input.project.workspace.orgId, name: input.project.workspace.name } : { orgId: null, name: null },
456
+ ...input.appUrl && { appUrl: input.appUrl }
457
+ };
458
+ }
459
+ var CONFIG_DIR = ".intent";
460
+ var CONFIG_FILE = "config.json";
461
+ function configPath() {
462
+ let dir = process.cwd();
463
+ while (true) {
464
+ const candidate = join2(dir, CONFIG_DIR, CONFIG_FILE);
465
+ if (existsSync2(candidate)) return candidate;
466
+ const parent = dirname2(dir);
467
+ if (parent === dir) break;
468
+ dir = parent;
469
+ }
470
+ return join2(process.cwd(), CONFIG_DIR, CONFIG_FILE);
471
+ }
472
+ function hasConfig() {
473
+ return existsSync2(configPath());
474
+ }
475
+ function readConfig() {
476
+ try {
477
+ const raw = readFileSync2(configPath(), "utf-8");
478
+ const config = JSON.parse(raw);
479
+ return config.apiBase ? { ...config, apiBase: canonicalApiBase(config.apiBase) } : config;
480
+ } catch {
481
+ return null;
482
+ }
483
+ }
484
+ function writeConfig(config) {
485
+ const existing = existsSync2(configPath()) ? configPath() : null;
486
+ const dir = existing ? dirname2(existing) : join2(process.cwd(), CONFIG_DIR);
487
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
488
+ writeFileSync(join2(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
489
+ }
490
+ var SYNC_BASE_FILE = "sync-base.json";
491
+ function readSyncBase() {
492
+ try {
493
+ return { ...emptyBase(), ...JSON.parse(readFileSync2(join2(dirname2(configPath()), SYNC_BASE_FILE), "utf-8")) };
494
+ } catch {
495
+ return emptyBase();
496
+ }
497
+ }
498
+ function writeSyncBase(base) {
499
+ const dir = dirname2(configPath());
500
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
501
+ writeFileSync(join2(dir, SYNC_BASE_FILE), JSON.stringify(base, null, 2) + "\n");
502
+ }
503
+ function requireConfig() {
504
+ const config = readConfig();
505
+ if (!config) {
506
+ console.error("Not initialized. Run: npx intentdocs init");
507
+ process.exit(1);
508
+ }
509
+ if (!config.mapId) {
510
+ console.error(
511
+ "This checkout was connected with a sync token, which has been retired: every sync now runs as you.\nRemove .intent/config.json and run `npx intentdocs login` then `npx intentdocs init` to reconnect."
512
+ );
513
+ process.exit(1);
514
+ }
515
+ return config;
516
+ }
517
+
518
+ export {
519
+ findIntentDir,
520
+ featureFiles,
521
+ parseFeature,
522
+ readIntentFolder,
523
+ slug,
524
+ specOfPushStory,
525
+ specOfStory,
526
+ classify,
527
+ renderFeature,
528
+ newFeature,
529
+ checkoutConfig,
530
+ hasConfig,
531
+ readConfig,
532
+ writeConfig,
533
+ readSyncBase,
534
+ writeSyncBase,
535
+ requireConfig
536
+ };
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  hasConfig,
15
15
  requireConfig
16
- } from "./chunk-K5SNF6CG.js";
16
+ } from "./chunk-RGFGTIIH.js";
17
17
  import {
18
18
  choose,
19
19
  closePrompt
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  ci
3
- } from "./chunk-65EDM7QH.js";
3
+ } from "./chunk-5GATVQEI.js";
4
4
  import "./chunk-KPQ42YE6.js";
5
- import "./chunk-K5SNF6CG.js";
5
+ import "./chunk-RGFGTIIH.js";
6
6
  import "./chunk-WRRUCCVZ.js";
7
7
  import "./chunk-XZE2XP6O.js";
8
8
  import "./chunk-UYTVTYNB.js";
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-KTGQO3AE.js";
13
13
  import {
14
14
  requireConfig
15
- } from "./chunk-K5SNF6CG.js";
15
+ } from "./chunk-RGFGTIIH.js";
16
16
  import "./chunk-XZE2XP6O.js";
17
17
  import "./chunk-UYTVTYNB.js";
18
18
 
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-KTGQO3AE.js";
9
9
  import {
10
10
  requireConfig
11
- } from "./chunk-K5SNF6CG.js";
11
+ } from "./chunk-RGFGTIIH.js";
12
12
  import {
13
13
  closePrompt,
14
14
  confirm