lean-spec 0.2.10 → 0.2.15-dev.21022397862

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 (38) hide show
  1. package/README.md +100 -8
  2. package/bin/lean-spec-rust.js +116 -0
  3. package/bin/lean-spec.js +9 -1
  4. package/binaries/darwin-arm64/lean-spec +0 -0
  5. package/binaries/darwin-arm64/package.json +20 -0
  6. package/binaries/darwin-x64/lean-spec +0 -0
  7. package/binaries/darwin-x64/package.json +20 -0
  8. package/binaries/linux-arm64/lean-spec +0 -0
  9. package/binaries/linux-arm64/package.json +20 -0
  10. package/binaries/linux-x64/lean-spec +0 -0
  11. package/binaries/linux-x64/package.json +20 -0
  12. package/binaries/windows-x64/lean-spec.exe +0 -0
  13. package/binaries/windows-x64/package.json +20 -0
  14. package/package.json +9 -43
  15. package/templates/detailed/AGENTS.md +18 -1
  16. package/templates/standard/AGENTS.md +18 -1
  17. package/dist/backfill-446GBTBC.js +0 -5
  18. package/dist/backfill-446GBTBC.js.map +0 -1
  19. package/dist/chunk-CJMVV46H.js +0 -2990
  20. package/dist/chunk-CJMVV46H.js.map +0 -1
  21. package/dist/chunk-H5MCUMBK.js +0 -741
  22. package/dist/chunk-H5MCUMBK.js.map +0 -1
  23. package/dist/chunk-KTNU4LUR.js +0 -8214
  24. package/dist/chunk-KTNU4LUR.js.map +0 -1
  25. package/dist/chunk-RF5PKL6L.js +0 -298
  26. package/dist/chunk-RF5PKL6L.js.map +0 -1
  27. package/dist/chunk-VN5BUHTV.js +0 -300
  28. package/dist/chunk-VN5BUHTV.js.map +0 -1
  29. package/dist/cli.d.ts +0 -2
  30. package/dist/cli.js +0 -129
  31. package/dist/cli.js.map +0 -1
  32. package/dist/frontmatter-6ZBAGOEU.js +0 -3
  33. package/dist/frontmatter-6ZBAGOEU.js.map +0 -1
  34. package/dist/mcp-server.d.ts +0 -16
  35. package/dist/mcp-server.js +0 -8
  36. package/dist/mcp-server.js.map +0 -1
  37. package/dist/validate-DIWYTDEF.js +0 -5
  38. package/dist/validate-DIWYTDEF.js.map +0 -1
@@ -1,298 +0,0 @@
1
- import { getSpecFile, parseFrontmatter, matchesFilter } from './chunk-VN5BUHTV.js';
2
- import * as fs2 from 'fs/promises';
3
- import * as path2 from 'path';
4
-
5
- var DEFAULT_CONFIG = {
6
- template: "spec-template.md",
7
- templates: {
8
- default: "spec-template.md"
9
- },
10
- specsDir: "specs",
11
- structure: {
12
- pattern: "flat",
13
- // Default to flat for new projects
14
- prefix: "",
15
- // No prefix by default - global sequence numbers only
16
- dateFormat: "YYYYMMDD",
17
- sequenceDigits: 3,
18
- defaultFile: "README.md"
19
- },
20
- features: {
21
- aiAgents: true,
22
- examples: true
23
- }
24
- };
25
- async function loadConfig(cwd = process.cwd()) {
26
- const configPath = path2.join(cwd, ".lean-spec", "config.json");
27
- try {
28
- const content = await fs2.readFile(configPath, "utf-8");
29
- const userConfig = JSON.parse(content);
30
- const merged = { ...DEFAULT_CONFIG, ...userConfig };
31
- normalizeLegacyPattern(merged);
32
- return merged;
33
- } catch {
34
- return DEFAULT_CONFIG;
35
- }
36
- }
37
- async function saveConfig(config, cwd = process.cwd()) {
38
- const configDir = path2.join(cwd, ".lean-spec");
39
- const configPath = path2.join(configDir, "config.json");
40
- await fs2.mkdir(configDir, { recursive: true });
41
- await fs2.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
42
- }
43
- function getToday(format = "YYYYMMDD") {
44
- const now = /* @__PURE__ */ new Date();
45
- const year = now.getFullYear();
46
- const month = String(now.getMonth() + 1).padStart(2, "0");
47
- const day = String(now.getDate()).padStart(2, "0");
48
- switch (format) {
49
- case "YYYYMMDD":
50
- return `${year}${month}${day}`;
51
- case "YYYY-MM-DD":
52
- return `${year}-${month}-${day}`;
53
- case "YYYY-MM":
54
- return `${year}-${month}`;
55
- case "YYYY/MM":
56
- return `${year}/${month}`;
57
- case "YYYY":
58
- return String(year);
59
- case "MM":
60
- return month;
61
- case "DD":
62
- return day;
63
- default:
64
- return `${year}${month}${day}`;
65
- }
66
- }
67
- function normalizeLegacyPattern(config) {
68
- const pattern = config.structure.pattern;
69
- if (pattern && pattern.includes("{date}") && pattern.includes("{seq}") && pattern.includes("{name}")) {
70
- config.structure.pattern = "custom";
71
- config.structure.groupExtractor = `{${config.structure.dateFormat}}`;
72
- }
73
- }
74
- function resolvePrefix(prefix, dateFormat = "YYYYMMDD") {
75
- const dateReplacements = {
76
- "{YYYYMMDD}": () => getToday("YYYYMMDD"),
77
- "{YYYY-MM-DD}": () => getToday("YYYY-MM-DD"),
78
- "{YYYY-MM}": () => getToday("YYYY-MM"),
79
- "{YYYY}": () => getToday("YYYY"),
80
- "{MM}": () => getToday("MM"),
81
- "{DD}": () => getToday("DD")
82
- };
83
- let result = prefix;
84
- for (const [pattern, fn] of Object.entries(dateReplacements)) {
85
- result = result.replace(pattern, fn());
86
- }
87
- return result;
88
- }
89
- function extractGroup(extractor, dateFormat = "YYYYMMDD", fields, fallback) {
90
- const dateReplacements = {
91
- "{YYYYMMDD}": () => getToday("YYYYMMDD"),
92
- "{YYYY-MM-DD}": () => getToday("YYYY-MM-DD"),
93
- "{YYYY-MM}": () => getToday("YYYY-MM"),
94
- "{YYYY}": () => getToday("YYYY"),
95
- "{MM}": () => getToday("MM"),
96
- "{DD}": () => getToday("DD")
97
- };
98
- let result = extractor;
99
- for (const [pattern, fn] of Object.entries(dateReplacements)) {
100
- result = result.replace(pattern, fn());
101
- }
102
- const fieldMatches = result.match(/\{([^}]+)\}/g);
103
- if (fieldMatches) {
104
- for (const match of fieldMatches) {
105
- const fieldName = match.slice(1, -1);
106
- const fieldValue = fields?.[fieldName];
107
- if (fieldValue === void 0) {
108
- if (!fallback) {
109
- throw new Error(`Custom field '${fieldName}' required but not provided. Set structure.groupFallback in config or provide --field ${fieldName}=<value>`);
110
- }
111
- return fallback;
112
- }
113
- result = result.replace(match, String(fieldValue));
114
- }
115
- }
116
- return result;
117
- }
118
- async function loadSubFiles(specDir, options = {}) {
119
- const subFiles = [];
120
- try {
121
- const entries = await fs2.readdir(specDir, { withFileTypes: true });
122
- for (const entry of entries) {
123
- if (entry.name === "README.md") continue;
124
- if (entry.isDirectory()) continue;
125
- const filePath = path2.join(specDir, entry.name);
126
- const stat2 = await fs2.stat(filePath);
127
- const ext = path2.extname(entry.name).toLowerCase();
128
- const isDocument = ext === ".md";
129
- const subFile = {
130
- name: entry.name,
131
- path: filePath,
132
- size: stat2.size,
133
- type: isDocument ? "document" : "asset"
134
- };
135
- if (isDocument && options.includeContent) {
136
- subFile.content = await fs2.readFile(filePath, "utf-8");
137
- }
138
- subFiles.push(subFile);
139
- }
140
- } catch (error) {
141
- return [];
142
- }
143
- return subFiles.sort((a, b) => {
144
- if (a.type !== b.type) {
145
- return a.type === "document" ? -1 : 1;
146
- }
147
- return a.name.localeCompare(b.name);
148
- });
149
- }
150
- async function loadAllSpecs(options = {}) {
151
- const config = await loadConfig();
152
- const cwd = process.cwd();
153
- const specsDir = path2.join(cwd, config.specsDir);
154
- const specs = [];
155
- try {
156
- await fs2.access(specsDir);
157
- } catch {
158
- return [];
159
- }
160
- const specPattern = /^(\d{2,})-/;
161
- async function loadSpecsFromDir(dir, relativePath = "") {
162
- try {
163
- const entries = await fs2.readdir(dir, { withFileTypes: true });
164
- for (const entry of entries) {
165
- if (!entry.isDirectory()) continue;
166
- if (entry.name === "archived" && relativePath === "") continue;
167
- const entryPath = path2.join(dir, entry.name);
168
- const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
169
- if (specPattern.test(entry.name)) {
170
- const specFile = await getSpecFile(entryPath, config.structure.defaultFile);
171
- if (specFile) {
172
- const frontmatter = await parseFrontmatter(specFile, config);
173
- if (frontmatter) {
174
- if (options.filter && !matchesFilter(frontmatter, options.filter)) {
175
- continue;
176
- }
177
- const dateMatch = entryRelativePath.match(/(\d{8})/);
178
- let date;
179
- if (dateMatch) {
180
- date = dateMatch[1];
181
- } else if (typeof frontmatter.created === "string") {
182
- date = frontmatter.created;
183
- } else if (frontmatter.created) {
184
- date = String(frontmatter.created);
185
- } else {
186
- date = "";
187
- }
188
- const specInfo = {
189
- path: entryRelativePath,
190
- fullPath: entryPath,
191
- filePath: specFile,
192
- name: entry.name,
193
- date,
194
- frontmatter
195
- };
196
- if (options.includeContent) {
197
- specInfo.content = await fs2.readFile(specFile, "utf-8");
198
- }
199
- if (options.includeSubFiles) {
200
- specInfo.subFiles = await loadSubFiles(entryPath, {
201
- includeContent: options.includeContent
202
- });
203
- }
204
- specs.push(specInfo);
205
- }
206
- }
207
- } else {
208
- await loadSpecsFromDir(entryPath, entryRelativePath);
209
- }
210
- }
211
- } catch {
212
- }
213
- }
214
- await loadSpecsFromDir(specsDir);
215
- if (options.includeArchived) {
216
- const archivedPath = path2.join(specsDir, "archived");
217
- await loadSpecsFromDir(archivedPath, "archived");
218
- }
219
- const sortBy = options.sortBy || "id";
220
- const sortOrder = options.sortOrder || "desc";
221
- specs.sort((a, b) => {
222
- let comparison = 0;
223
- switch (sortBy) {
224
- case "id":
225
- case "number": {
226
- const aNum = parseInt(a.name.match(/^(\d+)/)?.[1] || "0", 10);
227
- const bNum = parseInt(b.name.match(/^(\d+)/)?.[1] || "0", 10);
228
- comparison = aNum - bNum;
229
- break;
230
- }
231
- case "created": {
232
- const aDate2 = String(a.frontmatter.created || "");
233
- const bDate2 = String(b.frontmatter.created || "");
234
- comparison = aDate2.localeCompare(bDate2);
235
- break;
236
- }
237
- case "name": {
238
- comparison = a.name.localeCompare(b.name);
239
- break;
240
- }
241
- case "status": {
242
- comparison = a.frontmatter.status.localeCompare(b.frontmatter.status);
243
- break;
244
- }
245
- case "priority": {
246
- const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
247
- const aPriority = a.frontmatter.priority ? priorityOrder[a.frontmatter.priority] : 0;
248
- const bPriority = b.frontmatter.priority ? priorityOrder[b.frontmatter.priority] : 0;
249
- comparison = aPriority - bPriority;
250
- break;
251
- }
252
- default:
253
- const aDate = String(a.frontmatter.created || "");
254
- const bDate = String(b.frontmatter.created || "");
255
- comparison = aDate.localeCompare(bDate);
256
- }
257
- return sortOrder === "desc" ? -comparison : comparison;
258
- });
259
- return specs;
260
- }
261
- async function getSpec(specPath) {
262
- const config = await loadConfig();
263
- const cwd = process.cwd();
264
- const specsDir = path2.join(cwd, config.specsDir);
265
- let fullPath;
266
- if (path2.isAbsolute(specPath)) {
267
- fullPath = specPath;
268
- } else {
269
- fullPath = path2.join(specsDir, specPath);
270
- }
271
- try {
272
- await fs2.access(fullPath);
273
- } catch {
274
- return null;
275
- }
276
- const specFile = await getSpecFile(fullPath, config.structure.defaultFile);
277
- if (!specFile) return null;
278
- const frontmatter = await parseFrontmatter(specFile, config);
279
- if (!frontmatter) return null;
280
- const content = await fs2.readFile(specFile, "utf-8");
281
- const relativePath = path2.relative(specsDir, fullPath);
282
- const parts = relativePath.split(path2.sep);
283
- const date = parts[0] === "archived" ? parts[1] : parts[0];
284
- const name = parts[parts.length - 1];
285
- return {
286
- path: relativePath,
287
- fullPath,
288
- filePath: specFile,
289
- name,
290
- date,
291
- frontmatter,
292
- content
293
- };
294
- }
295
-
296
- export { extractGroup, getSpec, loadAllSpecs, loadConfig, loadSubFiles, resolvePrefix, saveConfig };
297
- //# sourceMappingURL=chunk-RF5PKL6L.js.map
298
- //# sourceMappingURL=chunk-RF5PKL6L.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/config.ts","../src/spec-loader.ts"],"names":["path","fs","stat","aDate","bDate"],"mappings":";;;;AAiCA,IAAM,cAAA,GAAiC;AAAA,EACrC,QAAA,EAAU,kBAAA;AAAA,EACV,SAAA,EAAW;AAAA,IACT,OAAA,EAAS;AAAA,GACX;AAAA,EACA,QAAA,EAAU,OAAA;AAAA,EACV,SAAA,EAAW;AAAA,IACT,OAAA,EAAS,MAAA;AAAA;AAAA,IACT,MAAA,EAAQ,EAAA;AAAA;AAAA,IACR,UAAA,EAAY,UAAA;AAAA,IACZ,cAAA,EAAgB,CAAA;AAAA,IAChB,WAAA,EAAa;AAAA,GACf;AAAA,EACA,QAAA,EAAU;AAAA,IACR,QAAA,EAAU,IAAA;AAAA,IACV,QAAA,EAAU;AAAA;AAEd,CAAA;AAEA,eAAsB,UAAA,CAAW,GAAA,GAAc,OAAA,CAAQ,GAAA,EAAI,EAA4B;AACrF,EAAA,MAAM,UAAA,GAAkBA,KAAA,CAAA,IAAA,CAAK,GAAA,EAAK,YAAA,EAAc,aAAa,CAAA;AAE7D,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,MAASC,GAAA,CAAA,QAAA,CAAS,UAAA,EAAY,OAAO,CAAA;AACrD,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AACrC,IAAA,MAAM,MAAA,GAAS,EAAE,GAAG,cAAA,EAAgB,GAAG,UAAA,EAAW;AAGlD,IAAA,sBAAA,CAAuB,MAAM,CAAA;AAE7B,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AAEN,IAAA,OAAO,cAAA;AAAA,EACT;AACF;AAEA,eAAsB,UAAA,CACpB,MAAA,EACA,GAAA,GAAc,OAAA,CAAQ,KAAI,EACX;AACf,EAAA,MAAM,SAAA,GAAiBD,KAAA,CAAA,IAAA,CAAK,GAAA,EAAK,YAAY,CAAA;AAC7C,EAAA,MAAM,UAAA,GAAkBA,KAAA,CAAA,IAAA,CAAK,SAAA,EAAW,aAAa,CAAA;AAErD,EAAA,MAASC,GAAA,CAAA,KAAA,CAAM,SAAA,EAAW,EAAE,SAAA,EAAW,MAAM,CAAA;AAC7C,EAAA,MAASA,GAAA,CAAA,SAAA,CAAU,YAAY,IAAA,CAAK,SAAA,CAAU,QAAQ,IAAA,EAAM,CAAC,GAAG,OAAO,CAAA;AACzE;AAEO,SAAS,QAAA,CAAS,SAAiB,UAAA,EAAoB;AAC5D,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY;AAC7B,EAAA,MAAM,KAAA,GAAQ,OAAO,GAAA,CAAI,QAAA,KAAa,CAAC,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AACxD,EAAA,MAAM,GAAA,GAAM,OAAO,GAAA,CAAI,OAAA,EAAS,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAEjD,EAAA,QAAQ,MAAA;AAAQ,IACd,KAAK,UAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,KAAK,GAAG,GAAG,CAAA,CAAA;AAAA,IAC9B,KAAK,YAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,IAAI,GAAG,CAAA,CAAA;AAAA,IAChC,KAAK,SAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,IACzB,KAAK,SAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,IACzB,KAAK,MAAA;AACH,MAAA,OAAO,OAAO,IAAI,CAAA;AAAA,IACpB,KAAK,IAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,IAAA;AACH,MAAA,OAAO,GAAA;AAAA,IACT;AACE,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,KAAK,GAAG,GAAG,CAAA,CAAA;AAAA;AAElC;AAKO,SAAS,uBAAuB,MAAA,EAA8B;AACnE,EAAA,MAAM,OAAA,GAAU,OAAO,SAAA,CAAU,OAAA;AAGjC,EAAA,IAAI,OAAA,IAAW,OAAA,CAAQ,QAAA,CAAS,QAAQ,CAAA,IAAK,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA,IAAK,OAAA,CAAQ,QAAA,CAAS,QAAQ,CAAA,EAAG;AACpG,IAAA,MAAA,CAAO,UAAU,OAAA,GAAU,QAAA;AAC3B,IAAA,MAAA,CAAO,SAAA,CAAU,cAAA,GAAiB,CAAA,CAAA,EAAI,MAAA,CAAO,UAAU,UAAU,CAAA,CAAA,CAAA;AAAA,EACnE;AACF;AAKO,SAAS,aAAA,CACd,MAAA,EACA,UAAA,GAAqB,UAAA,EACb;AACR,EAAA,MAAM,gBAAA,GAAiD;AAAA,IACrD,YAAA,EAAc,MAAM,QAAA,CAAS,UAAU,CAAA;AAAA,IACvC,cAAA,EAAgB,MAAM,QAAA,CAAS,YAAY,CAAA;AAAA,IAC3C,WAAA,EAAa,MAAM,QAAA,CAAS,SAAS,CAAA;AAAA,IACrC,QAAA,EAAU,MAAM,QAAA,CAAS,MAAM,CAAA;AAAA,IAC/B,MAAA,EAAQ,MAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IAC3B,MAAA,EAAQ,MAAM,QAAA,CAAS,IAAI;AAAA,GAC7B;AAEA,EAAA,IAAI,MAAA,GAAS,MAAA;AACb,EAAA,KAAA,MAAW,CAAC,OAAA,EAAS,EAAE,KAAK,MAAA,CAAO,OAAA,CAAQ,gBAAgB,CAAA,EAAG;AAC5D,IAAA,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,OAAA,EAAS,EAAA,EAAI,CAAA;AAAA,EACvC;AAEA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,YAAA,CACd,SAAA,EACA,UAAA,GAAqB,UAAA,EACrB,QACA,QAAA,EACQ;AACR,EAAA,MAAM,gBAAA,GAAiD;AAAA,IACrD,YAAA,EAAc,MAAM,QAAA,CAAS,UAAU,CAAA;AAAA,IACvC,cAAA,EAAgB,MAAM,QAAA,CAAS,YAAY,CAAA;AAAA,IAC3C,WAAA,EAAa,MAAM,QAAA,CAAS,SAAS,CAAA;AAAA,IACrC,QAAA,EAAU,MAAM,QAAA,CAAS,MAAM,CAAA;AAAA,IAC/B,MAAA,EAAQ,MAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IAC3B,MAAA,EAAQ,MAAM,QAAA,CAAS,IAAI;AAAA,GAC7B;AAEA,EAAA,IAAI,MAAA,GAAS,SAAA;AAGb,EAAA,KAAA,MAAW,CAAC,OAAA,EAAS,EAAE,KAAK,MAAA,CAAO,OAAA,CAAQ,gBAAgB,CAAA,EAAG;AAC5D,IAAA,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,OAAA,EAAS,EAAA,EAAI,CAAA;AAAA,EACvC;AAGA,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,KAAA,CAAM,cAAc,CAAA;AAChD,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,KAAA,MAAW,SAAS,YAAA,EAAc;AAChC,MAAA,MAAM,SAAA,GAAY,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACnC,MAAA,MAAM,UAAA,GAAa,SAAS,SAAS,CAAA;AAErC,MAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,QAAA,IAAI,CAAC,QAAA,EAAU;AACb,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,SAAS,CAAA,sFAAA,EAAyF,SAAS,CAAA,QAAA,CAAU,CAAA;AAAA,QACxJ;AACA,QAAA,OAAO,QAAA;AAAA,MACT;AAEA,MAAA,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,MAAA,CAAO,UAAU,CAAC,CAAA;AAAA,IACnD;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;ACnKA,eAAsB,YAAA,CACpB,OAAA,EACA,OAAA,GAAwC,EAAC,EACjB;AACxB,EAAA,MAAM,WAA0B,EAAC;AAEjC,EAAA,IAAI;AACF,IAAA,MAAM,UAAU,MAAS,GAAA,CAAA,OAAA,CAAQ,SAAS,EAAE,aAAA,EAAe,MAAM,CAAA;AAEjE,IAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAE3B,MAAA,IAAI,KAAA,CAAM,SAAS,WAAA,EAAa;AAGhC,MAAA,IAAI,KAAA,CAAM,aAAY,EAAG;AAEzB,MAAA,MAAM,QAAA,GAAgB,KAAA,CAAA,IAAA,CAAK,OAAA,EAAS,KAAA,CAAM,IAAI,CAAA;AAC9C,MAAA,MAAMC,KAAAA,GAAO,MAAS,GAAA,CAAA,IAAA,CAAK,QAAQ,CAAA;AAGnC,MAAA,MAAM,GAAA,GAAW,KAAA,CAAA,OAAA,CAAQ,KAAA,CAAM,IAAI,EAAE,WAAA,EAAY;AACjD,MAAA,MAAM,aAAa,GAAA,KAAQ,KAAA;AAE3B,MAAA,MAAM,OAAA,GAAuB;AAAA,QAC3B,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,IAAA,EAAM,QAAA;AAAA,QACN,MAAMA,KAAAA,CAAK,IAAA;AAAA,QACX,IAAA,EAAM,aAAa,UAAA,GAAa;AAAA,OAClC;AAGA,MAAA,IAAI,UAAA,IAAc,QAAQ,cAAA,EAAgB;AACxC,QAAA,OAAA,CAAQ,OAAA,GAAU,MAAS,GAAA,CAAA,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAAA,MACvD;AAEA,MAAA,QAAA,CAAS,KAAK,OAAO,CAAA;AAAA,IACvB;AAAA,EACF,SAAS,KAAA,EAAO;AAGd,IAAA,OAAO,EAAC;AAAA,EACV;AAGA,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AAC7B,IAAA,IAAI,CAAA,CAAE,IAAA,KAAS,CAAA,CAAE,IAAA,EAAM;AACrB,MAAA,OAAO,CAAA,CAAE,IAAA,KAAS,UAAA,GAAa,EAAA,GAAK,CAAA;AAAA,IACtC;AACA,IAAA,OAAO,CAAA,CAAE,IAAA,CAAK,aAAA,CAAc,CAAA,CAAE,IAAI,CAAA;AAAA,EACpC,CAAC,CAAA;AACH;AAGA,eAAsB,YAAA,CAAa,OAAA,GAO/B,EAAC,EAAwB;AAC3B,EAAA,MAAM,MAAA,GAAS,MAAM,UAAA,EAAW;AAChC,EAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,EAAI;AACxB,EAAA,MAAM,QAAA,GAAgB,KAAA,CAAA,IAAA,CAAK,GAAA,EAAK,MAAA,CAAO,QAAQ,CAAA;AAE/C,EAAA,MAAM,QAAoB,EAAC;AAG3B,EAAA,IAAI;AACF,IAAA,MAAS,WAAO,QAAQ,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AAGA,EAAA,MAAM,WAAA,GAAc,YAAA;AAGpB,EAAA,eAAe,gBAAA,CAAiB,GAAA,EAAa,YAAA,GAAuB,EAAA,EAAmB;AACrF,IAAA,IAAI;AACF,MAAA,MAAM,UAAU,MAAS,GAAA,CAAA,OAAA,CAAQ,KAAK,EAAE,aAAA,EAAe,MAAM,CAAA;AAE7D,MAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,QAAA,IAAI,CAAC,KAAA,CAAM,WAAA,EAAY,EAAG;AAG1B,QAAA,IAAI,KAAA,CAAM,IAAA,KAAS,UAAA,IAAc,YAAA,KAAiB,EAAA,EAAI;AAEtD,QAAA,MAAM,SAAA,GAAiB,KAAA,CAAA,IAAA,CAAK,GAAA,EAAK,KAAA,CAAM,IAAI,CAAA;AAC3C,QAAA,MAAM,iBAAA,GAAoB,eAAe,CAAA,EAAG,YAAY,IAAI,KAAA,CAAM,IAAI,KAAK,KAAA,CAAM,IAAA;AAGjF,QAAA,IAAI,WAAA,CAAY,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,EAAG;AAChC,UAAA,MAAM,WAAW,MAAM,WAAA,CAAY,SAAA,EAAW,MAAA,CAAO,UAAU,WAAW,CAAA;AAE1E,UAAA,IAAI,QAAA,EAAU;AACZ,YAAA,MAAM,WAAA,GAAc,MAAM,gBAAA,CAAiB,QAAA,EAAU,MAAM,CAAA;AAE3D,YAAA,IAAI,WAAA,EAAa;AAEf,cAAA,IAAI,QAAQ,MAAA,IAAU,CAAC,cAAc,WAAA,EAAa,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjE,gBAAA;AAAA,cACF;AAGA,cAAA,MAAM,SAAA,GAAY,iBAAA,CAAkB,KAAA,CAAM,SAAS,CAAA;AACnD,cAAA,IAAI,IAAA;AAEJ,cAAA,IAAI,SAAA,EAAW;AACb,gBAAA,IAAA,GAAO,UAAU,CAAC,CAAA;AAAA,cACpB,CAAA,MAAA,IAAW,OAAO,WAAA,CAAY,OAAA,KAAY,QAAA,EAAU;AAClD,gBAAA,IAAA,GAAO,WAAA,CAAY,OAAA;AAAA,cACrB,CAAA,MAAA,IAAW,YAAY,OAAA,EAAS;AAC9B,gBAAA,IAAA,GAAO,MAAA,CAAO,YAAY,OAAO,CAAA;AAAA,cACnC,CAAA,MAAO;AACL,gBAAA,IAAA,GAAO,EAAA;AAAA,cACT;AAEA,cAAA,MAAM,QAAA,GAAqB;AAAA,gBACzB,IAAA,EAAM,iBAAA;AAAA,gBACN,QAAA,EAAU,SAAA;AAAA,gBACV,QAAA,EAAU,QAAA;AAAA,gBACV,MAAM,KAAA,CAAM,IAAA;AAAA,gBACZ,IAAA;AAAA,gBACA;AAAA,eACF;AAGA,cAAA,IAAI,QAAQ,cAAA,EAAgB;AAC1B,gBAAA,QAAA,CAAS,OAAA,GAAU,MAAS,GAAA,CAAA,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAAA,cACxD;AAGA,cAAA,IAAI,QAAQ,eAAA,EAAiB;AAC3B,gBAAA,QAAA,CAAS,QAAA,GAAW,MAAM,YAAA,CAAa,SAAA,EAAW;AAAA,kBAChD,gBAAgB,OAAA,CAAQ;AAAA,iBACzB,CAAA;AAAA,cACH;AAEA,cAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AAAA,YACrB;AAAA,UACF;AAAA,QACF,CAAA,MAAO;AAEL,UAAA,MAAM,gBAAA,CAAiB,WAAW,iBAAiB,CAAA;AAAA,QACrD;AAAA,MACF;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAGA,EAAA,MAAM,iBAAiB,QAAQ,CAAA;AAG/B,EAAA,IAAI,QAAQ,eAAA,EAAiB;AAC3B,IAAA,MAAM,YAAA,GAAoB,KAAA,CAAA,IAAA,CAAK,QAAA,EAAU,UAAU,CAAA;AACnD,IAAA,MAAM,gBAAA,CAAiB,cAAc,UAAU,CAAA;AAAA,EACjD;AAGA,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,IAAA;AACjC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,MAAA;AAEvC,EAAA,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACnB,IAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,IAAA,QAAQ,MAAA;AAAQ,MACd,KAAK,IAAA;AAAA,MACL,KAAK,QAAA,EAAU;AAEb,QAAA,MAAM,IAAA,GAAO,QAAA,CAAS,CAAA,CAAE,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA,GAAI,CAAC,CAAA,IAAK,GAAA,EAAK,EAAE,CAAA;AAC5D,QAAA,MAAM,IAAA,GAAO,QAAA,CAAS,CAAA,CAAE,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA,GAAI,CAAC,CAAA,IAAK,GAAA,EAAK,EAAE,CAAA;AAC5D,QAAA,UAAA,GAAa,IAAA,GAAO,IAAA;AACpB,QAAA;AAAA,MACF;AAAA,MACA,KAAK,SAAA,EAAW;AAEd,QAAA,MAAMC,MAAAA,GAAQ,MAAA,CAAO,CAAA,CAAE,WAAA,CAAY,WAAW,EAAE,CAAA;AAChD,QAAA,MAAMC,MAAAA,GAAQ,MAAA,CAAO,CAAA,CAAE,WAAA,CAAY,WAAW,EAAE,CAAA;AAChD,QAAA,UAAA,GAAaD,MAAAA,CAAM,cAAcC,MAAK,CAAA;AACtC,QAAA;AAAA,MACF;AAAA,MACA,KAAK,MAAA,EAAQ;AACX,QAAA,UAAA,GAAa,CAAA,CAAE,IAAA,CAAK,aAAA,CAAc,CAAA,CAAE,IAAI,CAAA;AACxC,QAAA;AAAA,MACF;AAAA,MACA,KAAK,QAAA,EAAU;AACb,QAAA,UAAA,GAAa,EAAE,WAAA,CAAY,MAAA,CAAO,aAAA,CAAc,CAAA,CAAE,YAAY,MAAM,CAAA;AACpE,QAAA;AAAA,MACF;AAAA,MACA,KAAK,UAAA,EAAY;AAEf,QAAA,MAAM,aAAA,GAAgB,EAAE,QAAA,EAAU,CAAA,EAAG,MAAM,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAG,GAAA,EAAK,CAAA,EAAE;AAChE,QAAA,MAAM,SAAA,GAAY,EAAE,WAAA,CAAY,QAAA,GAAW,cAAc,CAAA,CAAE,WAAA,CAAY,QAAQ,CAAA,GAAI,CAAA;AACnF,QAAA,MAAM,SAAA,GAAY,EAAE,WAAA,CAAY,QAAA,GAAW,cAAc,CAAA,CAAE,WAAA,CAAY,QAAQ,CAAA,GAAI,CAAA;AACnF,QAAA,UAAA,GAAa,SAAA,GAAY,SAAA;AACzB,QAAA;AAAA,MACF;AAAA,MACA;AAEE,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,CAAA,CAAE,WAAA,CAAY,WAAW,EAAE,CAAA;AAChD,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,CAAA,CAAE,WAAA,CAAY,WAAW,EAAE,CAAA;AAChD,QAAA,UAAA,GAAa,KAAA,CAAM,cAAc,KAAK,CAAA;AAAA;AAI1C,IAAA,OAAO,SAAA,KAAc,MAAA,GAAS,CAAC,UAAA,GAAa,UAAA;AAAA,EAC9C,CAAC,CAAA;AAED,EAAA,OAAO,KAAA;AACT;AAGA,eAAsB,QAAQ,QAAA,EAA4C;AACxE,EAAA,MAAM,MAAA,GAAS,MAAM,UAAA,EAAW;AAChC,EAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,EAAI;AACxB,EAAA,MAAM,QAAA,GAAgB,KAAA,CAAA,IAAA,CAAK,GAAA,EAAK,MAAA,CAAO,QAAQ,CAAA;AAG/C,EAAA,IAAI,QAAA;AACJ,EAAA,IAAS,KAAA,CAAA,UAAA,CAAW,QAAQ,CAAA,EAAG;AAC7B,IAAA,QAAA,GAAW,QAAA;AAAA,EACb,CAAA,MAAO;AACL,IAAA,QAAA,GAAgB,KAAA,CAAA,IAAA,CAAK,UAAU,QAAQ,CAAA;AAAA,EACzC;AAGA,EAAA,IAAI;AACF,IAAA,MAAS,WAAO,QAAQ,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,WAAW,MAAM,WAAA,CAAY,QAAA,EAAU,MAAA,CAAO,UAAU,WAAW,CAAA;AACzE,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AAEtB,EAAA,MAAM,WAAA,GAAc,MAAM,gBAAA,CAAiB,QAAA,EAAU,MAAM,CAAA;AAC3D,EAAA,IAAI,CAAC,aAAa,OAAO,IAAA;AAEzB,EAAA,MAAM,OAAA,GAAU,MAAS,GAAA,CAAA,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAGnD,EAAA,MAAM,YAAA,GAAoB,KAAA,CAAA,QAAA,CAAS,QAAA,EAAU,QAAQ,CAAA;AACrD,EAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,KAAA,CAAW,KAAA,CAAA,GAAG,CAAA;AACzC,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA,KAAM,aAAa,KAAA,CAAM,CAAC,CAAA,GAAI,KAAA,CAAM,CAAC,CAAA;AACzD,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA;AAEnC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,QAAA;AAAA,IACA,QAAA,EAAU,QAAA;AAAA,IACV,IAAA;AAAA,IACA,IAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF","file":"chunk-RF5PKL6L.js","sourcesContent":["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\n\nexport interface LeanSpecConfig {\n template: string;\n templates?: Record<string, string>; // Maps template name to filename\n specsDir: string;\n autoCheck?: boolean; // Enable/disable auto-check for sequence conflicts (default: true)\n structure: {\n pattern: 'flat' | 'custom' | string; // 'flat' or 'custom', or legacy pattern string\n dateFormat: string;\n sequenceDigits: number;\n defaultFile: string;\n prefix?: string; // For flat pattern: \"{YYYYMMDD}-\" or \"spec-\" (optional, default: empty for global numbering)\n groupExtractor?: string; // For custom pattern: \"{YYYYMMDD}\" or \"milestone-{milestone}\"\n groupFallback?: string; // Fallback folder if field missing (only for non-date extractors)\n };\n features?: {\n aiAgents?: boolean;\n examples?: boolean;\n collaboration?: boolean;\n compliance?: boolean;\n approvals?: boolean;\n apiDocs?: boolean;\n };\n frontmatter?: {\n required?: string[];\n optional?: string[];\n custom?: Record<string, 'string' | 'number' | 'boolean' | 'array'>;\n };\n variables?: Record<string, string>;\n}\n\nconst DEFAULT_CONFIG: LeanSpecConfig = {\n template: 'spec-template.md',\n templates: {\n default: 'spec-template.md',\n },\n specsDir: 'specs',\n structure: {\n pattern: 'flat', // Default to flat for new projects\n prefix: '', // No prefix by default - global sequence numbers only\n dateFormat: 'YYYYMMDD',\n sequenceDigits: 3,\n defaultFile: 'README.md',\n },\n features: {\n aiAgents: true,\n examples: true,\n },\n};\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<LeanSpecConfig> {\n const configPath = path.join(cwd, '.lean-spec', 'config.json');\n\n try {\n const content = await fs.readFile(configPath, 'utf-8');\n const userConfig = JSON.parse(content);\n const merged = { ...DEFAULT_CONFIG, ...userConfig };\n \n // Normalize legacy pattern format\n normalizeLegacyPattern(merged);\n \n return merged;\n } catch {\n // No config file, use defaults\n return DEFAULT_CONFIG;\n }\n}\n\nexport async function saveConfig(\n config: LeanSpecConfig,\n cwd: string = process.cwd(),\n): Promise<void> {\n const configDir = path.join(cwd, '.lean-spec');\n const configPath = path.join(configDir, 'config.json');\n\n await fs.mkdir(configDir, { recursive: true });\n await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');\n}\n\nexport function getToday(format: string = 'YYYYMMDD'): string {\n const now = new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n\n switch (format) {\n case 'YYYYMMDD':\n return `${year}${month}${day}`;\n case 'YYYY-MM-DD':\n return `${year}-${month}-${day}`;\n case 'YYYY-MM':\n return `${year}-${month}`;\n case 'YYYY/MM':\n return `${year}/${month}`;\n case 'YYYY':\n return String(year);\n case 'MM':\n return month;\n case 'DD':\n return day;\n default:\n return `${year}${month}${day}`;\n }\n}\n\n/**\n * Detect if a config uses legacy pattern format and convert it\n */\nexport function normalizeLegacyPattern(config: LeanSpecConfig): void {\n const pattern = config.structure.pattern;\n \n // If pattern contains {date}/{seq}-{name}/, convert to custom with date grouping\n if (pattern && pattern.includes('{date}') && pattern.includes('{seq}') && pattern.includes('{name}')) {\n config.structure.pattern = 'custom';\n config.structure.groupExtractor = `{${config.structure.dateFormat}}`;\n }\n}\n\n/**\n * Resolve prefix string for flat pattern (e.g., \"{YYYYMMDD}-\" becomes \"20251103-\")\n */\nexport function resolvePrefix(\n prefix: string,\n dateFormat: string = 'YYYYMMDD'\n): string {\n const dateReplacements: Record<string, () => string> = {\n '{YYYYMMDD}': () => getToday('YYYYMMDD'),\n '{YYYY-MM-DD}': () => getToday('YYYY-MM-DD'),\n '{YYYY-MM}': () => getToday('YYYY-MM'),\n '{YYYY}': () => getToday('YYYY'),\n '{MM}': () => getToday('MM'),\n '{DD}': () => getToday('DD'),\n };\n\n let result = prefix;\n for (const [pattern, fn] of Object.entries(dateReplacements)) {\n result = result.replace(pattern, fn());\n }\n\n return result;\n}\n\n/**\n * Extract group folder from extractor pattern\n */\nexport function extractGroup(\n extractor: string,\n dateFormat: string = 'YYYYMMDD',\n fields?: Record<string, unknown>,\n fallback?: string\n): string {\n const dateReplacements: Record<string, () => string> = {\n '{YYYYMMDD}': () => getToday('YYYYMMDD'),\n '{YYYY-MM-DD}': () => getToday('YYYY-MM-DD'),\n '{YYYY-MM}': () => getToday('YYYY-MM'),\n '{YYYY}': () => getToday('YYYY'),\n '{MM}': () => getToday('MM'),\n '{DD}': () => getToday('DD'),\n };\n\n let result = extractor;\n\n // Replace date functions first\n for (const [pattern, fn] of Object.entries(dateReplacements)) {\n result = result.replace(pattern, fn());\n }\n\n // Replace frontmatter fields: {fieldname}\n const fieldMatches = result.match(/\\{([^}]+)\\}/g);\n if (fieldMatches) {\n for (const match of fieldMatches) {\n const fieldName = match.slice(1, -1); // Remove { }\n const fieldValue = fields?.[fieldName];\n\n if (fieldValue === undefined) {\n if (!fallback) {\n throw new Error(`Custom field '${fieldName}' required but not provided. Set structure.groupFallback in config or provide --field ${fieldName}=<value>`);\n }\n return fallback;\n }\n\n result = result.replace(match, String(fieldValue));\n }\n }\n\n return result;\n}\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { loadConfig } from './config.js';\nimport { parseFrontmatter, getSpecFile, matchesFilter, type SpecFrontmatter, type SpecFilterOptions } from './frontmatter.js';\n\nexport interface SpecInfo {\n path: string; // Relative path like \"20251101/003-pm-visualization-tools\"\n fullPath: string; // Absolute path to spec directory\n filePath: string; // Absolute path to spec file (README.md)\n name: string; // Just the spec name like \"003-pm-visualization-tools\"\n date?: string; // Date folder like \"20251101\" (optional for flat/nested patterns)\n frontmatter: SpecFrontmatter;\n content?: string; // Full file content (optional, for search)\n subFiles?: SubFileInfo[]; // Sub-documents and assets\n}\n\nexport interface SubFileInfo {\n name: string; // e.g., \"TESTING.md\" or \"diagram.png\"\n path: string; // Absolute path to the file\n size: number; // File size in bytes\n type: 'document' | 'asset'; // Classification based on file type\n content?: string; // Optional content for documents\n}\n\n// Load sub-files for a spec (all files except README.md)\nexport async function loadSubFiles(\n specDir: string,\n options: { includeContent?: boolean } = {}\n): Promise<SubFileInfo[]> {\n const subFiles: SubFileInfo[] = [];\n\n try {\n const entries = await fs.readdir(specDir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip README.md (main spec file)\n if (entry.name === 'README.md') continue;\n\n // Skip directories for now (could be assets folder)\n if (entry.isDirectory()) continue;\n\n const filePath = path.join(specDir, entry.name);\n const stat = await fs.stat(filePath);\n\n // Determine type based on extension\n const ext = path.extname(entry.name).toLowerCase();\n const isDocument = ext === '.md';\n\n const subFile: SubFileInfo = {\n name: entry.name,\n path: filePath,\n size: stat.size,\n type: isDocument ? 'document' : 'asset',\n };\n\n // Load content for documents if requested\n if (isDocument && options.includeContent) {\n subFile.content = await fs.readFile(filePath, 'utf-8');\n }\n\n subFiles.push(subFile);\n }\n } catch (error) {\n // Directory doesn't exist or can't be read - return empty array\n // This is expected for specs without sub-files\n return [];\n }\n\n // Sort: documents first, then alphabetically\n return subFiles.sort((a, b) => {\n if (a.type !== b.type) {\n return a.type === 'document' ? -1 : 1;\n }\n return a.name.localeCompare(b.name);\n });\n}\n\n// Load all specs from the specs directory\nexport async function loadAllSpecs(options: {\n includeArchived?: boolean;\n includeContent?: boolean;\n includeSubFiles?: boolean;\n filter?: SpecFilterOptions;\n sortBy?: string;\n sortOrder?: 'asc' | 'desc';\n} = {}): Promise<SpecInfo[]> {\n const config = await loadConfig();\n const cwd = process.cwd();\n const specsDir = path.join(cwd, config.specsDir);\n\n const specs: SpecInfo[] = [];\n\n // Check if specs directory exists\n try {\n await fs.access(specsDir);\n } catch {\n return [];\n }\n\n // Pattern to match spec directories (2 or more digits followed by dash)\n const specPattern = /^(\\d{2,})-/;\n\n // Recursively load all specs from the directory structure\n async function loadSpecsFromDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n \n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n \n // Skip archived directory in main scan (handle separately)\n if (entry.name === 'archived' && relativePath === '') continue;\n \n const entryPath = path.join(dir, entry.name);\n const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;\n \n // Check if this is a spec directory (NNN-name format)\n if (specPattern.test(entry.name)) {\n const specFile = await getSpecFile(entryPath, config.structure.defaultFile);\n \n if (specFile) {\n const frontmatter = await parseFrontmatter(specFile, config);\n \n if (frontmatter) {\n // Apply filter if provided\n if (options.filter && !matchesFilter(frontmatter, options.filter)) {\n continue;\n }\n \n // Extract date from path or frontmatter\n const dateMatch = entryRelativePath.match(/(\\d{8})/);\n let date: string;\n \n if (dateMatch) {\n date = dateMatch[1];\n } else if (typeof frontmatter.created === 'string') {\n date = frontmatter.created;\n } else if (frontmatter.created) {\n date = String(frontmatter.created);\n } else {\n date = '';\n }\n \n const specInfo: SpecInfo = {\n path: entryRelativePath,\n fullPath: entryPath,\n filePath: specFile,\n name: entry.name,\n date: date,\n frontmatter,\n };\n \n // Load content if requested\n if (options.includeContent) {\n specInfo.content = await fs.readFile(specFile, 'utf-8');\n }\n \n // Load sub-files if requested\n if (options.includeSubFiles) {\n specInfo.subFiles = await loadSubFiles(entryPath, {\n includeContent: options.includeContent,\n });\n }\n \n specs.push(specInfo);\n }\n }\n } else {\n // Not a spec directory, scan recursively for nested structure\n await loadSpecsFromDir(entryPath, entryRelativePath);\n }\n }\n } catch {\n // Directory doesn't exist or can't be read\n }\n }\n \n // Load active specs\n await loadSpecsFromDir(specsDir);\n\n // Load archived specs if requested\n if (options.includeArchived) {\n const archivedPath = path.join(specsDir, 'archived');\n await loadSpecsFromDir(archivedPath, 'archived');\n }\n\n // Sort specs based on options (default: id desc)\n const sortBy = options.sortBy || 'id';\n const sortOrder = options.sortOrder || 'desc';\n \n specs.sort((a, b) => {\n let comparison = 0;\n \n switch (sortBy) {\n case 'id':\n case 'number': { // Keep 'number' for backwards compatibility\n // Extract leading digits from spec name\n const aNum = parseInt(a.name.match(/^(\\d+)/)?.[1] || '0', 10);\n const bNum = parseInt(b.name.match(/^(\\d+)/)?.[1] || '0', 10);\n comparison = aNum - bNum;\n break;\n }\n case 'created': {\n // Sort by created date from frontmatter\n const aDate = String(a.frontmatter.created || '');\n const bDate = String(b.frontmatter.created || '');\n comparison = aDate.localeCompare(bDate);\n break;\n }\n case 'name': {\n comparison = a.name.localeCompare(b.name);\n break;\n }\n case 'status': {\n comparison = a.frontmatter.status.localeCompare(b.frontmatter.status);\n break;\n }\n case 'priority': {\n // Priority order: critical > high > medium > low > (none)\n const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };\n const aPriority = a.frontmatter.priority ? priorityOrder[a.frontmatter.priority] : 0;\n const bPriority = b.frontmatter.priority ? priorityOrder[b.frontmatter.priority] : 0;\n comparison = aPriority - bPriority;\n break;\n }\n default:\n // Default to created date\n const aDate = String(a.frontmatter.created || '');\n const bDate = String(b.frontmatter.created || '');\n comparison = aDate.localeCompare(bDate);\n }\n \n // Apply sort order\n return sortOrder === 'desc' ? -comparison : comparison;\n });\n\n return specs;\n}\n\n// Get a specific spec by path\nexport async function getSpec(specPath: string): Promise<SpecInfo | null> {\n const config = await loadConfig();\n const cwd = process.cwd();\n const specsDir = path.join(cwd, config.specsDir);\n\n // Resolve the full path\n let fullPath: string;\n if (path.isAbsolute(specPath)) {\n fullPath = specPath;\n } else {\n fullPath = path.join(specsDir, specPath);\n }\n\n // Check if directory exists\n try {\n await fs.access(fullPath);\n } catch {\n return null;\n }\n\n const specFile = await getSpecFile(fullPath, config.structure.defaultFile);\n if (!specFile) return null;\n\n const frontmatter = await parseFrontmatter(specFile, config);\n if (!frontmatter) return null;\n\n const content = await fs.readFile(specFile, 'utf-8');\n\n // Parse path components\n const relativePath = path.relative(specsDir, fullPath);\n const parts = relativePath.split(path.sep);\n const date = parts[0] === 'archived' ? parts[1] : parts[0];\n const name = parts[parts.length - 1];\n\n return {\n path: relativePath,\n fullPath,\n filePath: specFile,\n name,\n date,\n frontmatter,\n content,\n };\n}\n"]}
@@ -1,300 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import matter from 'gray-matter';
4
- import yaml from 'js-yaml';
5
- import dayjs from 'dayjs';
6
-
7
- // src/frontmatter.ts
8
- function normalizeDateFields(data) {
9
- const dateFields = ["created", "completed", "updated", "due"];
10
- for (const field of dateFields) {
11
- if (data[field] instanceof Date) {
12
- data[field] = data[field].toISOString().split("T")[0];
13
- }
14
- }
15
- }
16
- function enrichWithTimestamps(data, previousData) {
17
- const now = (/* @__PURE__ */ new Date()).toISOString();
18
- if (!data.created_at) {
19
- data.created_at = now;
20
- }
21
- if (previousData) {
22
- data.updated_at = now;
23
- }
24
- if (data.status === "complete" && previousData?.status !== "complete" && !data.completed_at) {
25
- data.completed_at = now;
26
- if (!data.completed) {
27
- data.completed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
28
- }
29
- }
30
- if (previousData && data.status !== previousData.status) {
31
- if (!Array.isArray(data.transitions)) {
32
- data.transitions = [];
33
- }
34
- data.transitions.push({
35
- status: data.status,
36
- at: now
37
- });
38
- }
39
- }
40
- function normalizeTagsField(data) {
41
- if (data.tags && typeof data.tags === "string") {
42
- try {
43
- const parsed = JSON.parse(data.tags);
44
- if (Array.isArray(parsed)) {
45
- data.tags = parsed;
46
- }
47
- } catch {
48
- data.tags = data.tags.split(",").map((t) => t.trim());
49
- }
50
- }
51
- }
52
- function validateCustomField(value, expectedType) {
53
- switch (expectedType) {
54
- case "string":
55
- if (typeof value === "string") {
56
- return { valid: true, coerced: value };
57
- }
58
- return { valid: true, coerced: String(value) };
59
- case "number":
60
- if (typeof value === "number") {
61
- return { valid: true, coerced: value };
62
- }
63
- const num = Number(value);
64
- if (!isNaN(num)) {
65
- return { valid: true, coerced: num };
66
- }
67
- return { valid: false, error: `Cannot convert '${value}' to number` };
68
- case "boolean":
69
- if (typeof value === "boolean") {
70
- return { valid: true, coerced: value };
71
- }
72
- if (value === "true" || value === "yes" || value === "1") {
73
- return { valid: true, coerced: true };
74
- }
75
- if (value === "false" || value === "no" || value === "0") {
76
- return { valid: true, coerced: false };
77
- }
78
- return { valid: false, error: `Cannot convert '${value}' to boolean` };
79
- case "array":
80
- if (Array.isArray(value)) {
81
- return { valid: true, coerced: value };
82
- }
83
- return { valid: false, error: `Expected array but got ${typeof value}` };
84
- default:
85
- return { valid: false, error: `Unknown type: ${expectedType}` };
86
- }
87
- }
88
- function validateCustomFields(frontmatter, config) {
89
- if (!config?.frontmatter?.custom) {
90
- return frontmatter;
91
- }
92
- const result = { ...frontmatter };
93
- for (const [fieldName, expectedType] of Object.entries(config.frontmatter.custom)) {
94
- if (fieldName in result) {
95
- const validation = validateCustomField(result[fieldName], expectedType);
96
- if (validation.valid) {
97
- result[fieldName] = validation.coerced;
98
- } else {
99
- console.warn(`Warning: Invalid custom field '${fieldName}': ${validation.error}`);
100
- }
101
- }
102
- }
103
- return result;
104
- }
105
- async function parseFrontmatter(filePath, config) {
106
- try {
107
- const content = await fs.readFile(filePath, "utf-8");
108
- const parsed = matter(content, {
109
- engines: {
110
- yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
111
- }
112
- });
113
- if (!parsed.data || Object.keys(parsed.data).length === 0) {
114
- return parseFallbackFields(content);
115
- }
116
- if (!parsed.data.status) {
117
- console.warn(`Warning: Missing required field 'status' in ${filePath}`);
118
- return null;
119
- }
120
- if (!parsed.data.created) {
121
- console.warn(`Warning: Missing required field 'created' in ${filePath}`);
122
- return null;
123
- }
124
- const validStatuses = ["planned", "in-progress", "complete", "archived"];
125
- if (!validStatuses.includes(parsed.data.status)) {
126
- console.warn(`Warning: Invalid status '${parsed.data.status}' in ${filePath}. Valid values: ${validStatuses.join(", ")}`);
127
- }
128
- if (parsed.data.priority) {
129
- const validPriorities = ["low", "medium", "high", "critical"];
130
- if (!validPriorities.includes(parsed.data.priority)) {
131
- console.warn(`Warning: Invalid priority '${parsed.data.priority}' in ${filePath}. Valid values: ${validPriorities.join(", ")}`);
132
- }
133
- }
134
- normalizeTagsField(parsed.data);
135
- const knownFields = [
136
- "status",
137
- "created",
138
- "tags",
139
- "priority",
140
- "depends_on",
141
- "updated",
142
- "completed",
143
- "assignee",
144
- "reviewer",
145
- "issue",
146
- "pr",
147
- "epic",
148
- "breaking",
149
- "due",
150
- "created_at",
151
- "updated_at",
152
- "completed_at",
153
- "transitions"
154
- ];
155
- const customFields = config?.frontmatter?.custom ? Object.keys(config.frontmatter.custom) : [];
156
- const allKnownFields = [...knownFields, ...customFields];
157
- const unknownFields = Object.keys(parsed.data).filter((k) => !allKnownFields.includes(k));
158
- if (unknownFields.length > 0) {
159
- console.warn(`Info: Unknown fields in ${filePath}: ${unknownFields.join(", ")}`);
160
- }
161
- const validatedData = validateCustomFields(parsed.data, config);
162
- return validatedData;
163
- } catch (error) {
164
- console.error(`Error parsing frontmatter from ${filePath}:`, error);
165
- return null;
166
- }
167
- }
168
- function parseFallbackFields(content) {
169
- const statusMatch = content.match(/\*\*Status\*\*:\s*(?:📅\s*)?(\w+(?:-\w+)?)/i);
170
- const createdMatch = content.match(/\*\*Created\*\*:\s*(\d{4}-\d{2}-\d{2})/);
171
- if (statusMatch && createdMatch) {
172
- const status = statusMatch[1].toLowerCase().replace(/\s+/g, "-");
173
- const created = createdMatch[1];
174
- return {
175
- status,
176
- created
177
- };
178
- }
179
- return null;
180
- }
181
- async function updateFrontmatter(filePath, updates) {
182
- const content = await fs.readFile(filePath, "utf-8");
183
- const parsed = matter(content, {
184
- engines: {
185
- yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
186
- }
187
- });
188
- const previousData = { ...parsed.data };
189
- const newData = { ...parsed.data, ...updates };
190
- normalizeDateFields(newData);
191
- enrichWithTimestamps(newData, previousData);
192
- if (updates.status === "complete" && !newData.completed) {
193
- newData.completed = dayjs().format("YYYY-MM-DD");
194
- }
195
- if ("updated" in parsed.data) {
196
- newData.updated = dayjs().format("YYYY-MM-DD");
197
- }
198
- let updatedContent = parsed.content;
199
- updatedContent = updateVisualMetadata(updatedContent, newData);
200
- const newContent = matter.stringify(updatedContent, newData);
201
- await fs.writeFile(filePath, newContent, "utf-8");
202
- }
203
- function updateVisualMetadata(content, frontmatter) {
204
- if (!frontmatter.status || !frontmatter.created) {
205
- return content;
206
- }
207
- const statusEmoji = getStatusEmojiPlain(frontmatter.status);
208
- const statusLabel = frontmatter.status.charAt(0).toUpperCase() + frontmatter.status.slice(1).replace("-", " ");
209
- const created = dayjs(frontmatter.created).format("YYYY-MM-DD");
210
- let metadataLine = `> **Status**: ${statusEmoji} ${statusLabel}`;
211
- if (frontmatter.priority) {
212
- const priorityLabel = frontmatter.priority.charAt(0).toUpperCase() + frontmatter.priority.slice(1);
213
- metadataLine += ` \xB7 **Priority**: ${priorityLabel}`;
214
- }
215
- metadataLine += ` \xB7 **Created**: ${created}`;
216
- if (frontmatter.tags && frontmatter.tags.length > 0) {
217
- metadataLine += ` \xB7 **Tags**: ${frontmatter.tags.join(", ")}`;
218
- }
219
- let secondLine = "";
220
- if (frontmatter.assignee || frontmatter.reviewer) {
221
- const assignee = frontmatter.assignee || "TBD";
222
- const reviewer = frontmatter.reviewer || "TBD";
223
- secondLine = `
224
- > **Assignee**: ${assignee} \xB7 **Reviewer**: ${reviewer}`;
225
- }
226
- const metadataPattern = /^>\s+\*\*Status\*\*:.*(?:\n>\s+\*\*Assignee\*\*:.*)?/m;
227
- if (metadataPattern.test(content)) {
228
- return content.replace(metadataPattern, metadataLine + secondLine);
229
- } else {
230
- const titleMatch = content.match(/^#\s+.+$/m);
231
- if (titleMatch) {
232
- const insertPos = titleMatch.index + titleMatch[0].length;
233
- return content.slice(0, insertPos) + "\n\n" + metadataLine + secondLine + "\n" + content.slice(insertPos);
234
- }
235
- }
236
- return content;
237
- }
238
- function getStatusEmojiPlain(status) {
239
- switch (status) {
240
- case "planned":
241
- return "\u{1F5D3}\uFE0F";
242
- case "in-progress":
243
- return "\u23F3";
244
- case "complete":
245
- return "\u2705";
246
- case "archived":
247
- return "\u{1F4E6}";
248
- default:
249
- return "\u{1F4C4}";
250
- }
251
- }
252
- async function getSpecFile(specDir, defaultFile = "README.md") {
253
- const specFile = path.join(specDir, defaultFile);
254
- try {
255
- await fs.access(specFile);
256
- return specFile;
257
- } catch {
258
- return null;
259
- }
260
- }
261
- function matchesFilter(frontmatter, filter) {
262
- if (filter.status) {
263
- const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
264
- if (!statuses.includes(frontmatter.status)) {
265
- return false;
266
- }
267
- }
268
- if (filter.tags && filter.tags.length > 0) {
269
- if (!frontmatter.tags || frontmatter.tags.length === 0) {
270
- return false;
271
- }
272
- const hasAllTags = filter.tags.every((tag) => frontmatter.tags.includes(tag));
273
- if (!hasAllTags) {
274
- return false;
275
- }
276
- }
277
- if (filter.priority) {
278
- const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
279
- if (!frontmatter.priority || !priorities.includes(frontmatter.priority)) {
280
- return false;
281
- }
282
- }
283
- if (filter.assignee) {
284
- if (frontmatter.assignee !== filter.assignee) {
285
- return false;
286
- }
287
- }
288
- if (filter.customFields) {
289
- for (const [key, value] of Object.entries(filter.customFields)) {
290
- if (frontmatter[key] !== value) {
291
- return false;
292
- }
293
- }
294
- }
295
- return true;
296
- }
297
-
298
- export { enrichWithTimestamps, getSpecFile, matchesFilter, normalizeDateFields, normalizeTagsField, parseFrontmatter, updateFrontmatter, validateCustomField, validateCustomFields };
299
- //# sourceMappingURL=chunk-VN5BUHTV.js.map
300
- //# sourceMappingURL=chunk-VN5BUHTV.js.map