lean-spec 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -1
- package/README.md +313 -213
- package/dist/chunk-OXTU3PN4.js +5429 -0
- package/dist/chunk-OXTU3PN4.js.map +1 -0
- package/dist/chunk-S4YNQ5KE.js +306 -0
- package/dist/chunk-S4YNQ5KE.js.map +1 -0
- package/dist/cli.js +186 -1908
- package/dist/cli.js.map +1 -1
- package/dist/frontmatter-26SOQGYM.js +23 -0
- package/dist/frontmatter-26SOQGYM.js.map +1 -0
- package/dist/mcp-server.js +9 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +23 -9
- package/templates/enterprise/config.json +2 -1
- package/templates/enterprise/spec-template.md +8 -3
- package/templates/minimal/config.json +2 -1
- package/templates/minimal/spec-template.md +2 -2
- package/templates/standard/config.json +2 -1
- package/templates/standard/spec-template.md +9 -2
- /package/bin/{lspec.js → lean-spec.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,1928 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addTemplate,
|
|
3
|
+
archiveSpec,
|
|
4
|
+
backfillTimestamps,
|
|
5
|
+
boardCommand,
|
|
6
|
+
checkSpecs,
|
|
7
|
+
copyTemplate,
|
|
8
|
+
createSpec,
|
|
9
|
+
depsCommand,
|
|
10
|
+
filesCommand,
|
|
11
|
+
ganttCommand,
|
|
12
|
+
initProject,
|
|
13
|
+
listSpecs,
|
|
14
|
+
listTemplates,
|
|
15
|
+
mcpCommand,
|
|
16
|
+
openCommand,
|
|
17
|
+
removeTemplate,
|
|
18
|
+
searchCommand,
|
|
19
|
+
showTemplate,
|
|
20
|
+
statsCommand,
|
|
21
|
+
timelineCommand,
|
|
22
|
+
updateSpec,
|
|
23
|
+
validateCommand,
|
|
24
|
+
viewCommand
|
|
25
|
+
} from "./chunk-OXTU3PN4.js";
|
|
26
|
+
import "./chunk-S4YNQ5KE.js";
|
|
27
|
+
|
|
1
28
|
// src/cli.ts
|
|
2
29
|
import { Command } from "commander";
|
|
3
30
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// src/config.ts
|
|
10
|
-
import * as fs from "fs/promises";
|
|
11
|
-
import * as path from "path";
|
|
12
|
-
var DEFAULT_CONFIG = {
|
|
13
|
-
template: "spec-template.md",
|
|
14
|
-
templates: {
|
|
15
|
-
default: "spec-template.md"
|
|
16
|
-
},
|
|
17
|
-
specsDir: "specs",
|
|
18
|
-
structure: {
|
|
19
|
-
pattern: "{date}/{seq}-{name}/",
|
|
20
|
-
dateFormat: "YYYYMMDD",
|
|
21
|
-
sequenceDigits: 3,
|
|
22
|
-
defaultFile: "README.md"
|
|
23
|
-
},
|
|
24
|
-
features: {
|
|
25
|
-
aiAgents: true,
|
|
26
|
-
examples: true
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
async function loadConfig(cwd = process.cwd()) {
|
|
30
|
-
const configPath = path.join(cwd, ".lspec", "config.json");
|
|
31
|
-
try {
|
|
32
|
-
const content = await fs.readFile(configPath, "utf-8");
|
|
33
|
-
const userConfig = JSON.parse(content);
|
|
34
|
-
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
35
|
-
} catch {
|
|
36
|
-
return DEFAULT_CONFIG;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function saveConfig(config, cwd = process.cwd()) {
|
|
40
|
-
const configDir = path.join(cwd, ".lspec");
|
|
41
|
-
const configPath = path.join(configDir, "config.json");
|
|
42
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
43
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
44
|
-
}
|
|
45
|
-
function getToday(format = "YYYYMMDD") {
|
|
46
|
-
const now = /* @__PURE__ */ new Date();
|
|
47
|
-
const year = now.getFullYear();
|
|
48
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
49
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
50
|
-
switch (format) {
|
|
51
|
-
case "YYYYMMDD":
|
|
52
|
-
return `${year}${month}${day}`;
|
|
53
|
-
case "YYYY-MM-DD":
|
|
54
|
-
return `${year}-${month}-${day}`;
|
|
55
|
-
case "YYYY/MM":
|
|
56
|
-
return `${year}/${month}`;
|
|
57
|
-
default:
|
|
58
|
-
return `${year}${month}${day}`;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/utils/path-helpers.ts
|
|
63
|
-
import * as fs2 from "fs/promises";
|
|
64
|
-
import * as path2 from "path";
|
|
65
|
-
async function getNextSeq(dateDir, digits) {
|
|
66
|
-
try {
|
|
67
|
-
const entries = await fs2.readdir(dateDir, { withFileTypes: true });
|
|
68
|
-
const seqNumbers = entries.filter((e) => e.isDirectory() && /^\d{2,3}-.+/.test(e.name)).map((e) => parseInt(e.name.split("-")[0], 10)).filter((n) => !isNaN(n));
|
|
69
|
-
if (seqNumbers.length === 0) {
|
|
70
|
-
return "1".padStart(digits, "0");
|
|
71
|
-
}
|
|
72
|
-
const maxSeq = Math.max(...seqNumbers);
|
|
73
|
-
return String(maxSeq + 1).padStart(digits, "0");
|
|
74
|
-
} catch {
|
|
75
|
-
return "1".padStart(digits, "0");
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function resolveSpecPath(specPath, cwd, specsDir) {
|
|
79
|
-
if (path2.isAbsolute(specPath)) {
|
|
80
|
-
try {
|
|
81
|
-
await fs2.access(specPath);
|
|
82
|
-
return specPath;
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const cwdPath = path2.resolve(cwd, specPath);
|
|
88
|
-
try {
|
|
89
|
-
await fs2.access(cwdPath);
|
|
90
|
-
return cwdPath;
|
|
91
|
-
} catch {
|
|
92
|
-
}
|
|
93
|
-
const specsPath = path2.join(specsDir, specPath);
|
|
94
|
-
try {
|
|
95
|
-
await fs2.access(specsPath);
|
|
96
|
-
return specsPath;
|
|
97
|
-
} catch {
|
|
98
|
-
}
|
|
99
|
-
const specName = specPath.replace(/^.*\//, "");
|
|
100
|
-
try {
|
|
101
|
-
const entries = await fs2.readdir(specsDir, { withFileTypes: true });
|
|
102
|
-
const dateDirs = entries.filter((e) => e.isDirectory() && e.name !== "archived");
|
|
103
|
-
for (const dateDir of dateDirs) {
|
|
104
|
-
const testPath = path2.join(specsDir, dateDir.name, specName);
|
|
105
|
-
try {
|
|
106
|
-
await fs2.access(testPath);
|
|
107
|
-
return testPath;
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/commands/create.ts
|
|
117
|
-
async function createSpec(name, options = {}) {
|
|
118
|
-
const config = await loadConfig();
|
|
119
|
-
const cwd = process.cwd();
|
|
120
|
-
const today = getToday(config.structure.dateFormat);
|
|
121
|
-
const specsDir = path3.join(cwd, config.specsDir);
|
|
122
|
-
const dateDir = path3.join(specsDir, today);
|
|
123
|
-
await fs3.mkdir(dateDir, { recursive: true });
|
|
124
|
-
const seq = await getNextSeq(dateDir, config.structure.sequenceDigits);
|
|
125
|
-
const specDir = path3.join(dateDir, `${seq}-${name}`);
|
|
126
|
-
const specFile = path3.join(specDir, config.structure.defaultFile);
|
|
127
|
-
try {
|
|
128
|
-
await fs3.access(specDir);
|
|
129
|
-
console.log(chalk.yellow(`Warning: Spec already exists: ${specDir}`));
|
|
130
|
-
process.exit(1);
|
|
131
|
-
} catch {
|
|
132
|
-
}
|
|
133
|
-
await fs3.mkdir(specDir, { recursive: true });
|
|
134
|
-
const templatesDir = path3.join(cwd, ".lspec", "templates");
|
|
135
|
-
let templateName;
|
|
136
|
-
if (options.template) {
|
|
137
|
-
if (config.templates?.[options.template]) {
|
|
138
|
-
templateName = config.templates[options.template];
|
|
139
|
-
} else {
|
|
140
|
-
console.error(chalk.red(`Template not found: ${options.template}`));
|
|
141
|
-
console.error(chalk.gray(`Available templates: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
templateName = config.template || "spec-template.md";
|
|
146
|
-
}
|
|
147
|
-
const templatePath = path3.join(templatesDir, templateName);
|
|
148
|
-
let content;
|
|
149
|
-
try {
|
|
150
|
-
const template = await fs3.readFile(templatePath, "utf-8");
|
|
151
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
152
|
-
const title = options.title || name;
|
|
153
|
-
content = template.replace(/{name}/g, title).replace(/{date}/g, date);
|
|
154
|
-
if (options.tags || options.priority || options.assignee) {
|
|
155
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
156
|
-
if (frontmatterMatch) {
|
|
157
|
-
let frontmatter = frontmatterMatch[1];
|
|
158
|
-
if (options.tags && options.tags.length > 0) {
|
|
159
|
-
frontmatter = frontmatter.replace(/tags: \[\]/, `tags: [${options.tags.join(", ")}]`);
|
|
160
|
-
}
|
|
161
|
-
if (options.priority) {
|
|
162
|
-
frontmatter = frontmatter.replace(/priority: medium/, `priority: ${options.priority}`);
|
|
163
|
-
}
|
|
164
|
-
if (options.assignee) {
|
|
165
|
-
frontmatter = frontmatter.replace(/(priority: \w+)/, `$1
|
|
166
|
-
assignee: ${options.assignee}`);
|
|
167
|
-
}
|
|
168
|
-
content = content.replace(/^---\n[\s\S]*?\n---/, `---
|
|
169
|
-
${frontmatter}
|
|
170
|
-
---`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (options.description) {
|
|
174
|
-
content = content.replace(
|
|
175
|
-
/## Overview\s+<!-- What are we solving\? Why now\? -->/,
|
|
176
|
-
`## Overview
|
|
177
|
-
|
|
178
|
-
${options.description}`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(chalk.red("Error: Template not found!"));
|
|
183
|
-
console.error(chalk.gray(`Expected: ${templatePath}`));
|
|
184
|
-
console.error(chalk.yellow("Run: lspec init"));
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
await fs3.writeFile(specFile, content, "utf-8");
|
|
188
|
-
console.log(chalk.green(`\u2713 Created: ${specDir}/`));
|
|
189
|
-
console.log(chalk.gray(` Edit: ${specFile}`));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/commands/archive.ts
|
|
193
|
-
import * as fs4 from "fs/promises";
|
|
194
|
-
import * as path4 from "path";
|
|
195
|
-
import chalk2 from "chalk";
|
|
196
|
-
async function archiveSpec(specPath) {
|
|
197
|
-
const config = await loadConfig();
|
|
198
|
-
const cwd = process.cwd();
|
|
199
|
-
const specsDir = path4.join(cwd, config.specsDir);
|
|
200
|
-
const resolvedPath = path4.resolve(specPath);
|
|
201
|
-
try {
|
|
202
|
-
await fs4.access(resolvedPath);
|
|
203
|
-
} catch {
|
|
204
|
-
console.error(chalk2.red(`Error: Spec not found: ${specPath}`));
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
const parentDir = path4.dirname(resolvedPath);
|
|
208
|
-
const dateFolder = path4.basename(parentDir);
|
|
209
|
-
const archiveDir = path4.join(specsDir, "archived", dateFolder);
|
|
210
|
-
await fs4.mkdir(archiveDir, { recursive: true });
|
|
211
|
-
const specName = path4.basename(resolvedPath);
|
|
212
|
-
const archivePath = path4.join(archiveDir, specName);
|
|
213
|
-
await fs4.rename(resolvedPath, archivePath);
|
|
214
|
-
console.log(chalk2.green(`\u2713 Archived: ${archivePath}`));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// src/commands/list.ts
|
|
218
|
-
import * as fs7 from "fs/promises";
|
|
219
|
-
import * as path7 from "path";
|
|
220
|
-
import chalk5 from "chalk";
|
|
221
|
-
|
|
222
|
-
// src/utils/ui.ts
|
|
223
|
-
import ora from "ora";
|
|
224
|
-
import chalk3 from "chalk";
|
|
225
|
-
async function withSpinner(text, fn, options) {
|
|
226
|
-
const spinner = ora(text).start();
|
|
227
|
-
try {
|
|
228
|
-
const result = await fn();
|
|
229
|
-
spinner.succeed(options?.successText || text);
|
|
230
|
-
return result;
|
|
231
|
-
} catch (error) {
|
|
232
|
-
spinner.fail(options?.failText || `${text} failed`);
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/spec-loader.ts
|
|
238
|
-
import * as fs6 from "fs/promises";
|
|
239
|
-
import * as path6 from "path";
|
|
240
|
-
|
|
241
|
-
// src/frontmatter.ts
|
|
242
|
-
import * as fs5 from "fs/promises";
|
|
243
|
-
import * as path5 from "path";
|
|
244
|
-
import matter from "gray-matter";
|
|
245
|
-
import dayjs from "dayjs";
|
|
246
|
-
async function parseFrontmatter(filePath) {
|
|
247
|
-
try {
|
|
248
|
-
const content = await fs5.readFile(filePath, "utf-8");
|
|
249
|
-
const parsed = matter(content);
|
|
250
|
-
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
251
|
-
return parseFallbackFields(content);
|
|
252
|
-
}
|
|
253
|
-
if (!parsed.data.status) {
|
|
254
|
-
console.warn(`Warning: Missing required field 'status' in ${filePath}`);
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
if (!parsed.data.created) {
|
|
258
|
-
console.warn(`Warning: Missing required field 'created' in ${filePath}`);
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
const validStatuses = ["planned", "in-progress", "complete", "archived"];
|
|
262
|
-
if (!validStatuses.includes(parsed.data.status)) {
|
|
263
|
-
console.warn(`Warning: Invalid status '${parsed.data.status}' in ${filePath}. Valid values: ${validStatuses.join(", ")}`);
|
|
264
|
-
}
|
|
265
|
-
if (parsed.data.priority) {
|
|
266
|
-
const validPriorities = ["low", "medium", "high", "critical"];
|
|
267
|
-
if (!validPriorities.includes(parsed.data.priority)) {
|
|
268
|
-
console.warn(`Warning: Invalid priority '${parsed.data.priority}' in ${filePath}. Valid values: ${validPriorities.join(", ")}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const knownFields = [
|
|
272
|
-
"status",
|
|
273
|
-
"created",
|
|
274
|
-
"tags",
|
|
275
|
-
"priority",
|
|
276
|
-
"related",
|
|
277
|
-
"depends_on",
|
|
278
|
-
"updated",
|
|
279
|
-
"completed",
|
|
280
|
-
"assignee",
|
|
281
|
-
"reviewer",
|
|
282
|
-
"issue",
|
|
283
|
-
"pr",
|
|
284
|
-
"epic",
|
|
285
|
-
"breaking"
|
|
286
|
-
];
|
|
287
|
-
const unknownFields = Object.keys(parsed.data).filter((k) => !knownFields.includes(k));
|
|
288
|
-
if (unknownFields.length > 0) {
|
|
289
|
-
console.warn(`Info: Unknown fields in ${filePath}: ${unknownFields.join(", ")}`);
|
|
290
|
-
}
|
|
291
|
-
return parsed.data;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
console.error(`Error parsing frontmatter from ${filePath}:`, error);
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function parseFallbackFields(content) {
|
|
298
|
-
const statusMatch = content.match(/\*\*Status\*\*:\s*(?:📅\s*)?(\w+(?:-\w+)?)/i);
|
|
299
|
-
const createdMatch = content.match(/\*\*Created\*\*:\s*(\d{4}-\d{2}-\d{2})/);
|
|
300
|
-
if (statusMatch && createdMatch) {
|
|
301
|
-
const status = statusMatch[1].toLowerCase().replace(/\s+/g, "-");
|
|
302
|
-
const created = createdMatch[1];
|
|
303
|
-
return {
|
|
304
|
-
status,
|
|
305
|
-
created
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
async function updateFrontmatter(filePath, updates) {
|
|
311
|
-
const content = await fs5.readFile(filePath, "utf-8");
|
|
312
|
-
const parsed = matter(content);
|
|
313
|
-
const newData = { ...parsed.data, ...updates };
|
|
314
|
-
if (updates.status === "complete" && !newData.completed) {
|
|
315
|
-
newData.completed = dayjs().format("YYYY-MM-DD");
|
|
316
|
-
}
|
|
317
|
-
if ("updated" in parsed.data) {
|
|
318
|
-
newData.updated = dayjs().format("YYYY-MM-DD");
|
|
31
|
+
// src/utils/cli-helpers.ts
|
|
32
|
+
function parseCustomFieldOptions(fieldOptions) {
|
|
33
|
+
const customFields = {};
|
|
34
|
+
if (!fieldOptions) {
|
|
35
|
+
return customFields;
|
|
319
36
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
function updateVisualMetadata(content, frontmatter) {
|
|
326
|
-
const statusEmoji = getStatusEmojiPlain(frontmatter.status);
|
|
327
|
-
const statusLabel = frontmatter.status.charAt(0).toUpperCase() + frontmatter.status.slice(1).replace("-", " ");
|
|
328
|
-
const created = dayjs(frontmatter.created).format("YYYY-MM-DD");
|
|
329
|
-
let metadataLine = `> **Status**: ${statusEmoji} ${statusLabel}`;
|
|
330
|
-
if (frontmatter.priority) {
|
|
331
|
-
const priorityLabel = frontmatter.priority.charAt(0).toUpperCase() + frontmatter.priority.slice(1);
|
|
332
|
-
metadataLine += ` \xB7 **Priority**: ${priorityLabel}`;
|
|
333
|
-
}
|
|
334
|
-
metadataLine += ` \xB7 **Created**: ${created}`;
|
|
335
|
-
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
336
|
-
metadataLine += ` \xB7 **Tags**: ${frontmatter.tags.join(", ")}`;
|
|
337
|
-
}
|
|
338
|
-
let secondLine = "";
|
|
339
|
-
if (frontmatter.assignee || frontmatter.reviewer) {
|
|
340
|
-
const assignee = frontmatter.assignee || "TBD";
|
|
341
|
-
const reviewer = frontmatter.reviewer || "TBD";
|
|
342
|
-
secondLine = `
|
|
343
|
-
> **Assignee**: ${assignee} \xB7 **Reviewer**: ${reviewer}`;
|
|
344
|
-
}
|
|
345
|
-
const metadataPattern = /^>\s+\*\*Status\*\*:.*(?:\n>\s+\*\*Assignee\*\*:.*)?/m;
|
|
346
|
-
if (metadataPattern.test(content)) {
|
|
347
|
-
return content.replace(metadataPattern, metadataLine + secondLine);
|
|
348
|
-
} else {
|
|
349
|
-
const titleMatch = content.match(/^#\s+.+$/m);
|
|
350
|
-
if (titleMatch) {
|
|
351
|
-
const insertPos = titleMatch.index + titleMatch[0].length;
|
|
352
|
-
return content.slice(0, insertPos) + "\n\n" + metadataLine + secondLine + "\n" + content.slice(insertPos);
|
|
37
|
+
for (const field of fieldOptions) {
|
|
38
|
+
const [key, ...valueParts] = field.split("=");
|
|
39
|
+
if (key && valueParts.length > 0) {
|
|
40
|
+
const value = valueParts.join("=");
|
|
41
|
+
customFields[key.trim()] = value.trim();
|
|
353
42
|
}
|
|
354
43
|
}
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
function getStatusEmojiPlain(status) {
|
|
358
|
-
switch (status) {
|
|
359
|
-
case "planned":
|
|
360
|
-
return "\u{1F4C5}";
|
|
361
|
-
case "in-progress":
|
|
362
|
-
return "\u{1F528}";
|
|
363
|
-
case "complete":
|
|
364
|
-
return "\u2705";
|
|
365
|
-
case "archived":
|
|
366
|
-
return "\u{1F4E6}";
|
|
367
|
-
default:
|
|
368
|
-
return "\u{1F4C4}";
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
async function getSpecFile(specDir, defaultFile = "README.md") {
|
|
372
|
-
const specFile = path5.join(specDir, defaultFile);
|
|
373
|
-
try {
|
|
374
|
-
await fs5.access(specFile);
|
|
375
|
-
return specFile;
|
|
376
|
-
} catch {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function matchesFilter(frontmatter, filter) {
|
|
381
|
-
if (filter.status) {
|
|
382
|
-
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
383
|
-
if (!statuses.includes(frontmatter.status)) {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
388
|
-
if (!frontmatter.tags || frontmatter.tags.length === 0) {
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
const hasAllTags = filter.tags.every((tag) => frontmatter.tags.includes(tag));
|
|
392
|
-
if (!hasAllTags) {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (filter.priority) {
|
|
397
|
-
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority];
|
|
398
|
-
if (!frontmatter.priority || !priorities.includes(frontmatter.priority)) {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (filter.assignee) {
|
|
403
|
-
if (frontmatter.assignee !== filter.assignee) {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return true;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/spec-loader.ts
|
|
411
|
-
async function loadSubFiles(specDir, options = {}) {
|
|
412
|
-
const subFiles = [];
|
|
413
|
-
try {
|
|
414
|
-
const entries = await fs6.readdir(specDir, { withFileTypes: true });
|
|
415
|
-
for (const entry of entries) {
|
|
416
|
-
if (entry.name === "README.md") continue;
|
|
417
|
-
if (entry.isDirectory()) continue;
|
|
418
|
-
const filePath = path6.join(specDir, entry.name);
|
|
419
|
-
const stat4 = await fs6.stat(filePath);
|
|
420
|
-
const ext = path6.extname(entry.name).toLowerCase();
|
|
421
|
-
const isDocument = ext === ".md";
|
|
422
|
-
const subFile = {
|
|
423
|
-
name: entry.name,
|
|
424
|
-
path: filePath,
|
|
425
|
-
size: stat4.size,
|
|
426
|
-
type: isDocument ? "document" : "asset"
|
|
427
|
-
};
|
|
428
|
-
if (isDocument && options.includeContent) {
|
|
429
|
-
subFile.content = await fs6.readFile(filePath, "utf-8");
|
|
430
|
-
}
|
|
431
|
-
subFiles.push(subFile);
|
|
432
|
-
}
|
|
433
|
-
} catch (error) {
|
|
434
|
-
return [];
|
|
435
|
-
}
|
|
436
|
-
return subFiles.sort((a, b) => {
|
|
437
|
-
if (a.type !== b.type) {
|
|
438
|
-
return a.type === "document" ? -1 : 1;
|
|
439
|
-
}
|
|
440
|
-
return a.name.localeCompare(b.name);
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
async function loadAllSpecs(options = {}) {
|
|
444
|
-
const config = await loadConfig();
|
|
445
|
-
const cwd = process.cwd();
|
|
446
|
-
const specsDir = path6.join(cwd, config.specsDir);
|
|
447
|
-
const specs = [];
|
|
448
|
-
try {
|
|
449
|
-
await fs6.access(specsDir);
|
|
450
|
-
} catch {
|
|
451
|
-
return [];
|
|
452
|
-
}
|
|
453
|
-
const entries = await fs6.readdir(specsDir, { withFileTypes: true });
|
|
454
|
-
const dateDirs = entries.filter((e) => e.isDirectory() && e.name !== "archived").sort((a, b) => b.name.localeCompare(a.name));
|
|
455
|
-
for (const dir of dateDirs) {
|
|
456
|
-
const dateDir = path6.join(specsDir, dir.name);
|
|
457
|
-
const specEntries = await fs6.readdir(dateDir, { withFileTypes: true });
|
|
458
|
-
const specDirs = specEntries.filter((s) => s.isDirectory());
|
|
459
|
-
for (const spec of specDirs) {
|
|
460
|
-
const specDir = path6.join(dateDir, spec.name);
|
|
461
|
-
const specFile = await getSpecFile(specDir, config.structure.defaultFile);
|
|
462
|
-
if (!specFile) continue;
|
|
463
|
-
const frontmatter = await parseFrontmatter(specFile);
|
|
464
|
-
if (!frontmatter) continue;
|
|
465
|
-
if (options.filter && !matchesFilter(frontmatter, options.filter)) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const specInfo = {
|
|
469
|
-
path: `${dir.name}/${spec.name}`,
|
|
470
|
-
fullPath: specDir,
|
|
471
|
-
filePath: specFile,
|
|
472
|
-
name: spec.name,
|
|
473
|
-
date: dir.name,
|
|
474
|
-
frontmatter
|
|
475
|
-
};
|
|
476
|
-
if (options.includeContent) {
|
|
477
|
-
specInfo.content = await fs6.readFile(specFile, "utf-8");
|
|
478
|
-
}
|
|
479
|
-
if (options.includeSubFiles) {
|
|
480
|
-
specInfo.subFiles = await loadSubFiles(specDir, {
|
|
481
|
-
includeContent: options.includeContent
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
specs.push(specInfo);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (options.includeArchived) {
|
|
488
|
-
const archivedPath = path6.join(specsDir, "archived");
|
|
489
|
-
try {
|
|
490
|
-
await fs6.access(archivedPath);
|
|
491
|
-
const archivedEntries = await fs6.readdir(archivedPath, { withFileTypes: true });
|
|
492
|
-
const archivedDirs = archivedEntries.filter((e) => e.isDirectory()).sort((a, b) => b.name.localeCompare(a.name));
|
|
493
|
-
for (const dir of archivedDirs) {
|
|
494
|
-
const dateDir = path6.join(archivedPath, dir.name);
|
|
495
|
-
const specEntries = await fs6.readdir(dateDir, { withFileTypes: true });
|
|
496
|
-
const specDirs = specEntries.filter((s) => s.isDirectory());
|
|
497
|
-
for (const spec of specDirs) {
|
|
498
|
-
const specDir = path6.join(dateDir, spec.name);
|
|
499
|
-
const specFile = await getSpecFile(specDir, config.structure.defaultFile);
|
|
500
|
-
if (!specFile) continue;
|
|
501
|
-
const frontmatter = await parseFrontmatter(specFile);
|
|
502
|
-
if (!frontmatter) continue;
|
|
503
|
-
if (options.filter && !matchesFilter(frontmatter, options.filter)) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
const specInfo = {
|
|
507
|
-
path: `archived/${dir.name}/${spec.name}`,
|
|
508
|
-
fullPath: specDir,
|
|
509
|
-
filePath: specFile,
|
|
510
|
-
name: spec.name,
|
|
511
|
-
date: dir.name,
|
|
512
|
-
frontmatter
|
|
513
|
-
};
|
|
514
|
-
if (options.includeContent) {
|
|
515
|
-
specInfo.content = await fs6.readFile(specFile, "utf-8");
|
|
516
|
-
}
|
|
517
|
-
if (options.includeSubFiles) {
|
|
518
|
-
specInfo.subFiles = await loadSubFiles(specDir, {
|
|
519
|
-
includeContent: options.includeContent
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
specs.push(specInfo);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
} catch {
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return specs;
|
|
529
|
-
}
|
|
530
|
-
async function getSpec(specPath) {
|
|
531
|
-
const config = await loadConfig();
|
|
532
|
-
const cwd = process.cwd();
|
|
533
|
-
const specsDir = path6.join(cwd, config.specsDir);
|
|
534
|
-
let fullPath;
|
|
535
|
-
if (path6.isAbsolute(specPath)) {
|
|
536
|
-
fullPath = specPath;
|
|
537
|
-
} else {
|
|
538
|
-
fullPath = path6.join(specsDir, specPath);
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
await fs6.access(fullPath);
|
|
542
|
-
} catch {
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
const specFile = await getSpecFile(fullPath, config.structure.defaultFile);
|
|
546
|
-
if (!specFile) return null;
|
|
547
|
-
const frontmatter = await parseFrontmatter(specFile);
|
|
548
|
-
if (!frontmatter) return null;
|
|
549
|
-
const content = await fs6.readFile(specFile, "utf-8");
|
|
550
|
-
const relativePath = path6.relative(specsDir, fullPath);
|
|
551
|
-
const parts = relativePath.split(path6.sep);
|
|
552
|
-
const date = parts[0] === "archived" ? parts[1] : parts[0];
|
|
553
|
-
const name = parts[parts.length - 1];
|
|
554
|
-
return {
|
|
555
|
-
path: relativePath,
|
|
556
|
-
fullPath,
|
|
557
|
-
filePath: specFile,
|
|
558
|
-
name,
|
|
559
|
-
date,
|
|
560
|
-
frontmatter,
|
|
561
|
-
content
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// src/utils/spec-helpers.ts
|
|
566
|
-
import chalk4 from "chalk";
|
|
567
|
-
function getStatusEmoji(status) {
|
|
568
|
-
switch (status) {
|
|
569
|
-
case "planned":
|
|
570
|
-
return chalk4.gray("\u{1F4C5}");
|
|
571
|
-
case "in-progress":
|
|
572
|
-
return chalk4.yellow("\u{1F528}");
|
|
573
|
-
case "complete":
|
|
574
|
-
return chalk4.green("\u2705");
|
|
575
|
-
case "archived":
|
|
576
|
-
return chalk4.gray("\u{1F4E6}");
|
|
577
|
-
default:
|
|
578
|
-
return "";
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
function getPriorityLabel(priority) {
|
|
582
|
-
switch (priority) {
|
|
583
|
-
case "low":
|
|
584
|
-
return chalk4.gray("low");
|
|
585
|
-
case "medium":
|
|
586
|
-
return chalk4.blue("med");
|
|
587
|
-
case "high":
|
|
588
|
-
return chalk4.yellow("high");
|
|
589
|
-
case "critical":
|
|
590
|
-
return chalk4.red("CRIT");
|
|
591
|
-
default:
|
|
592
|
-
return "";
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// src/commands/list.ts
|
|
597
|
-
async function listSpecs(options = {}) {
|
|
598
|
-
const config = await loadConfig();
|
|
599
|
-
const cwd = process.cwd();
|
|
600
|
-
const specsDir = path7.join(cwd, config.specsDir);
|
|
601
|
-
try {
|
|
602
|
-
await fs7.access(specsDir);
|
|
603
|
-
} catch {
|
|
604
|
-
console.log("");
|
|
605
|
-
console.log("No specs directory found. Initialize with: lspec init");
|
|
606
|
-
console.log("");
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const filter = {};
|
|
610
|
-
if (options.status) filter.status = options.status;
|
|
611
|
-
if (options.tags) filter.tags = options.tags;
|
|
612
|
-
if (options.priority) filter.priority = options.priority;
|
|
613
|
-
if (options.assignee) filter.assignee = options.assignee;
|
|
614
|
-
const specs = await withSpinner(
|
|
615
|
-
"Loading specs...",
|
|
616
|
-
() => loadAllSpecs({
|
|
617
|
-
includeArchived: options.showArchived || false,
|
|
618
|
-
filter
|
|
619
|
-
})
|
|
620
|
-
);
|
|
621
|
-
console.log("");
|
|
622
|
-
console.log(chalk5.green("=== Specs ==="));
|
|
623
|
-
console.log("");
|
|
624
|
-
if (specs.length === 0) {
|
|
625
|
-
if (Object.keys(filter).length > 0) {
|
|
626
|
-
console.log("No specs match the specified filters.");
|
|
627
|
-
} else {
|
|
628
|
-
console.log("No specs found. Create one with: lspec create <name>");
|
|
629
|
-
}
|
|
630
|
-
console.log("");
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const byDate = /* @__PURE__ */ new Map();
|
|
634
|
-
for (const spec of specs) {
|
|
635
|
-
const dateMatch = spec.path.match(/^(\d{8})\//);
|
|
636
|
-
const dateKey = dateMatch ? dateMatch[1] : "unknown";
|
|
637
|
-
if (!byDate.has(dateKey)) {
|
|
638
|
-
byDate.set(dateKey, []);
|
|
639
|
-
}
|
|
640
|
-
byDate.get(dateKey).push(spec);
|
|
641
|
-
}
|
|
642
|
-
const sortedDates = Array.from(byDate.keys()).sort((a, b) => b.localeCompare(a));
|
|
643
|
-
for (const date of sortedDates) {
|
|
644
|
-
const dateSpecs = byDate.get(date);
|
|
645
|
-
console.log(chalk5.cyan(`${date}/`));
|
|
646
|
-
for (const spec of dateSpecs) {
|
|
647
|
-
const specName = spec.path.replace(/^\d{8}\//, "").replace(/\/$/, "");
|
|
648
|
-
let line = ` ${specName}/`;
|
|
649
|
-
const meta = [];
|
|
650
|
-
meta.push(getStatusEmoji(spec.frontmatter.status));
|
|
651
|
-
if (spec.frontmatter.priority) {
|
|
652
|
-
meta.push(getPriorityLabel(spec.frontmatter.priority));
|
|
653
|
-
}
|
|
654
|
-
if (spec.frontmatter.tags && spec.frontmatter.tags.length > 0) {
|
|
655
|
-
meta.push(chalk5.gray(`[${spec.frontmatter.tags.join(", ")}]`));
|
|
656
|
-
}
|
|
657
|
-
if (meta.length > 0) {
|
|
658
|
-
line += ` ${meta.join(" ")}`;
|
|
659
|
-
}
|
|
660
|
-
console.log(line);
|
|
661
|
-
}
|
|
662
|
-
console.log("");
|
|
663
|
-
}
|
|
664
|
-
console.log("");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// src/commands/update.ts
|
|
668
|
-
import * as path8 from "path";
|
|
669
|
-
import chalk6 from "chalk";
|
|
670
|
-
async function updateSpec(specPath, updates) {
|
|
671
|
-
const config = await loadConfig();
|
|
672
|
-
const cwd = process.cwd();
|
|
673
|
-
const specsDir = path8.join(cwd, config.specsDir);
|
|
674
|
-
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
675
|
-
if (!resolvedPath) {
|
|
676
|
-
console.error(chalk6.red(`Error: Spec not found: ${specPath}`));
|
|
677
|
-
console.error(chalk6.gray(`Tried: ${specPath}, specs/${specPath}, and searching in date directories`));
|
|
678
|
-
process.exit(1);
|
|
679
|
-
}
|
|
680
|
-
const specFile = await getSpecFile(resolvedPath, config.structure.defaultFile);
|
|
681
|
-
if (!specFile) {
|
|
682
|
-
console.error(chalk6.red(`Error: No spec file found in: ${specPath}`));
|
|
683
|
-
process.exit(1);
|
|
684
|
-
}
|
|
685
|
-
await updateFrontmatter(specFile, updates);
|
|
686
|
-
console.log(chalk6.green(`\u2713 Updated: ${path8.relative(cwd, resolvedPath)}`));
|
|
687
|
-
const updatedFields = Object.keys(updates).join(", ");
|
|
688
|
-
console.log(chalk6.gray(` Fields: ${updatedFields}`));
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// src/commands/templates.ts
|
|
692
|
-
import * as fs8 from "fs/promises";
|
|
693
|
-
import * as path9 from "path";
|
|
694
|
-
import chalk7 from "chalk";
|
|
695
|
-
async function listTemplates(cwd = process.cwd()) {
|
|
696
|
-
const config = await loadConfig(cwd);
|
|
697
|
-
const templatesDir = path9.join(cwd, ".lspec", "templates");
|
|
698
|
-
console.log("");
|
|
699
|
-
console.log(chalk7.green("=== Project Templates ==="));
|
|
700
|
-
console.log("");
|
|
701
|
-
try {
|
|
702
|
-
await fs8.access(templatesDir);
|
|
703
|
-
} catch {
|
|
704
|
-
console.log(chalk7.yellow("No templates directory found."));
|
|
705
|
-
console.log(chalk7.gray("Run: lspec init"));
|
|
706
|
-
console.log("");
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
const files = await fs8.readdir(templatesDir);
|
|
710
|
-
const templateFiles = files.filter((f) => f.endsWith(".md"));
|
|
711
|
-
if (templateFiles.length === 0) {
|
|
712
|
-
console.log(chalk7.yellow("No templates found."));
|
|
713
|
-
console.log("");
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (config.templates && Object.keys(config.templates).length > 0) {
|
|
717
|
-
console.log(chalk7.cyan("Registered:"));
|
|
718
|
-
for (const [name, file] of Object.entries(config.templates)) {
|
|
719
|
-
const isDefault = config.template === file;
|
|
720
|
-
const marker = isDefault ? chalk7.green("\u2713 (default)") : "";
|
|
721
|
-
console.log(` ${chalk7.bold(name)}: ${file} ${marker}`);
|
|
722
|
-
}
|
|
723
|
-
console.log("");
|
|
724
|
-
}
|
|
725
|
-
console.log(chalk7.cyan("Available files:"));
|
|
726
|
-
for (const file of templateFiles) {
|
|
727
|
-
const filePath = path9.join(templatesDir, file);
|
|
728
|
-
const stat4 = await fs8.stat(filePath);
|
|
729
|
-
const sizeKB = (stat4.size / 1024).toFixed(1);
|
|
730
|
-
console.log(` ${file} (${sizeKB} KB)`);
|
|
731
|
-
}
|
|
732
|
-
console.log("");
|
|
733
|
-
console.log(chalk7.gray("Use templates with: lspec create <name> --template=<template-name>"));
|
|
734
|
-
console.log("");
|
|
735
|
-
}
|
|
736
|
-
async function showTemplate(templateName, cwd = process.cwd()) {
|
|
737
|
-
const config = await loadConfig(cwd);
|
|
738
|
-
if (!config.templates?.[templateName]) {
|
|
739
|
-
console.error(chalk7.red(`Template not found: ${templateName}`));
|
|
740
|
-
console.error(chalk7.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
741
|
-
process.exit(1);
|
|
742
|
-
}
|
|
743
|
-
const templatesDir = path9.join(cwd, ".lspec", "templates");
|
|
744
|
-
const templateFile = config.templates[templateName];
|
|
745
|
-
const templatePath = path9.join(templatesDir, templateFile);
|
|
746
|
-
try {
|
|
747
|
-
const content = await fs8.readFile(templatePath, "utf-8");
|
|
748
|
-
console.log("");
|
|
749
|
-
console.log(chalk7.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
|
|
750
|
-
console.log("");
|
|
751
|
-
console.log(content);
|
|
752
|
-
console.log("");
|
|
753
|
-
} catch (error) {
|
|
754
|
-
console.error(chalk7.red(`Error reading template: ${templateFile}`));
|
|
755
|
-
console.error(error);
|
|
756
|
-
process.exit(1);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
async function addTemplate(name, file, cwd = process.cwd()) {
|
|
760
|
-
const config = await loadConfig(cwd);
|
|
761
|
-
const templatesDir = path9.join(cwd, ".lspec", "templates");
|
|
762
|
-
const templatePath = path9.join(templatesDir, file);
|
|
763
|
-
try {
|
|
764
|
-
await fs8.access(templatePath);
|
|
765
|
-
} catch {
|
|
766
|
-
console.error(chalk7.red(`Template file not found: ${file}`));
|
|
767
|
-
console.error(chalk7.gray(`Expected at: ${templatePath}`));
|
|
768
|
-
console.error(
|
|
769
|
-
chalk7.yellow("Create the file first or use: lspec templates copy <source> <target>")
|
|
770
|
-
);
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
773
|
-
if (!config.templates) {
|
|
774
|
-
config.templates = {};
|
|
775
|
-
}
|
|
776
|
-
if (config.templates[name]) {
|
|
777
|
-
console.log(chalk7.yellow(`Warning: Template '${name}' already exists, updating...`));
|
|
778
|
-
}
|
|
779
|
-
config.templates[name] = file;
|
|
780
|
-
await saveConfig(config, cwd);
|
|
781
|
-
console.log(chalk7.green(`\u2713 Added template: ${name} \u2192 ${file}`));
|
|
782
|
-
console.log(chalk7.gray(` Use with: lspec create <spec-name> --template=${name}`));
|
|
783
|
-
}
|
|
784
|
-
async function removeTemplate(name, cwd = process.cwd()) {
|
|
785
|
-
const config = await loadConfig(cwd);
|
|
786
|
-
if (!config.templates?.[name]) {
|
|
787
|
-
console.error(chalk7.red(`Template not found: ${name}`));
|
|
788
|
-
console.error(chalk7.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
791
|
-
if (name === "default") {
|
|
792
|
-
console.error(chalk7.red("Cannot remove default template"));
|
|
793
|
-
process.exit(1);
|
|
794
|
-
}
|
|
795
|
-
const file = config.templates[name];
|
|
796
|
-
delete config.templates[name];
|
|
797
|
-
await saveConfig(config, cwd);
|
|
798
|
-
console.log(chalk7.green(`\u2713 Removed template: ${name}`));
|
|
799
|
-
console.log(chalk7.gray(` Note: Template file ${file} still exists in .lspec/templates/`));
|
|
800
|
-
}
|
|
801
|
-
async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
802
|
-
const config = await loadConfig(cwd);
|
|
803
|
-
const templatesDir = path9.join(cwd, ".lspec", "templates");
|
|
804
|
-
let sourceFile;
|
|
805
|
-
if (config.templates?.[source]) {
|
|
806
|
-
sourceFile = config.templates[source];
|
|
807
|
-
} else {
|
|
808
|
-
sourceFile = source;
|
|
809
|
-
}
|
|
810
|
-
const sourcePath = path9.join(templatesDir, sourceFile);
|
|
811
|
-
try {
|
|
812
|
-
await fs8.access(sourcePath);
|
|
813
|
-
} catch {
|
|
814
|
-
console.error(chalk7.red(`Source template not found: ${source}`));
|
|
815
|
-
console.error(chalk7.gray(`Expected at: ${sourcePath}`));
|
|
816
|
-
process.exit(1);
|
|
817
|
-
}
|
|
818
|
-
const targetFile = target.endsWith(".md") ? target : `${target}.md`;
|
|
819
|
-
const targetPath = path9.join(templatesDir, targetFile);
|
|
820
|
-
await fs8.copyFile(sourcePath, targetPath);
|
|
821
|
-
console.log(chalk7.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
|
|
822
|
-
if (!config.templates) {
|
|
823
|
-
config.templates = {};
|
|
824
|
-
}
|
|
825
|
-
const templateName = target.replace(/\.md$/, "");
|
|
826
|
-
config.templates[templateName] = targetFile;
|
|
827
|
-
await saveConfig(config, cwd);
|
|
828
|
-
console.log(chalk7.green(`\u2713 Registered template: ${templateName}`));
|
|
829
|
-
console.log(chalk7.gray(` Edit: ${targetPath}`));
|
|
830
|
-
console.log(chalk7.gray(` Use with: lspec create <spec-name> --template=${templateName}`));
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// src/commands/init.ts
|
|
834
|
-
import * as fs10 from "fs/promises";
|
|
835
|
-
import * as path11 from "path";
|
|
836
|
-
import { fileURLToPath } from "url";
|
|
837
|
-
import chalk9 from "chalk";
|
|
838
|
-
import { select } from "@inquirer/prompts";
|
|
839
|
-
|
|
840
|
-
// src/utils/template-helpers.ts
|
|
841
|
-
import * as fs9 from "fs/promises";
|
|
842
|
-
import * as path10 from "path";
|
|
843
|
-
import chalk8 from "chalk";
|
|
844
|
-
async function detectExistingSystemPrompts(cwd) {
|
|
845
|
-
const commonFiles = [
|
|
846
|
-
"AGENTS.md",
|
|
847
|
-
".cursorrules",
|
|
848
|
-
".github/copilot-instructions.md"
|
|
849
|
-
];
|
|
850
|
-
const found = [];
|
|
851
|
-
for (const file of commonFiles) {
|
|
852
|
-
try {
|
|
853
|
-
await fs9.access(path10.join(cwd, file));
|
|
854
|
-
found.push(file);
|
|
855
|
-
} catch {
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return found;
|
|
859
|
-
}
|
|
860
|
-
async function handleExistingFiles(action, existingFiles, templateDir, cwd, variables = {}) {
|
|
861
|
-
for (const file of existingFiles) {
|
|
862
|
-
const filePath = path10.join(cwd, file);
|
|
863
|
-
const templateFilePath = path10.join(templateDir, "files", file);
|
|
864
|
-
try {
|
|
865
|
-
await fs9.access(templateFilePath);
|
|
866
|
-
} catch {
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
if (action === "merge" && file === "AGENTS.md") {
|
|
870
|
-
const existing = await fs9.readFile(filePath, "utf-8");
|
|
871
|
-
let template = await fs9.readFile(templateFilePath, "utf-8");
|
|
872
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
873
|
-
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
874
|
-
}
|
|
875
|
-
const merged = `${existing}
|
|
876
|
-
|
|
877
|
-
---
|
|
878
|
-
|
|
879
|
-
## LeanSpec Integration
|
|
880
|
-
|
|
881
|
-
${template.split("\n").slice(1).join("\n")}`;
|
|
882
|
-
await fs9.writeFile(filePath, merged, "utf-8");
|
|
883
|
-
console.log(chalk8.green(`\u2713 Merged LeanSpec section into ${file}`));
|
|
884
|
-
} else if (action === "backup") {
|
|
885
|
-
const backupPath = `${filePath}.backup`;
|
|
886
|
-
await fs9.rename(filePath, backupPath);
|
|
887
|
-
console.log(chalk8.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
|
|
888
|
-
let content = await fs9.readFile(templateFilePath, "utf-8");
|
|
889
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
890
|
-
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
891
|
-
}
|
|
892
|
-
await fs9.writeFile(filePath, content, "utf-8");
|
|
893
|
-
console.log(chalk8.green(`\u2713 Created new ${file}`));
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
898
|
-
await fs9.mkdir(dest, { recursive: true });
|
|
899
|
-
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
900
|
-
for (const entry of entries) {
|
|
901
|
-
const srcPath = path10.join(src, entry.name);
|
|
902
|
-
const destPath = path10.join(dest, entry.name);
|
|
903
|
-
if (skipFiles.includes(entry.name)) {
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
if (entry.isDirectory()) {
|
|
907
|
-
await copyDirectory(srcPath, destPath, skipFiles, variables);
|
|
908
|
-
} else {
|
|
909
|
-
try {
|
|
910
|
-
await fs9.access(destPath);
|
|
911
|
-
} catch {
|
|
912
|
-
let content = await fs9.readFile(srcPath, "utf-8");
|
|
913
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
914
|
-
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
915
|
-
}
|
|
916
|
-
await fs9.writeFile(destPath, content, "utf-8");
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
async function getProjectName(cwd) {
|
|
922
|
-
try {
|
|
923
|
-
const packageJsonPath = path10.join(cwd, "package.json");
|
|
924
|
-
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
925
|
-
const pkg = JSON.parse(content);
|
|
926
|
-
if (pkg.name) {
|
|
927
|
-
return pkg.name;
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
}
|
|
931
|
-
return path10.basename(cwd);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// src/commands/init.ts
|
|
935
|
-
var __dirname2 = path11.dirname(fileURLToPath(import.meta.url));
|
|
936
|
-
var TEMPLATES_DIR = path11.join(__dirname2, "..", "templates");
|
|
937
|
-
async function initProject() {
|
|
938
|
-
const cwd = process.cwd();
|
|
939
|
-
try {
|
|
940
|
-
await fs10.access(path11.join(cwd, ".lspec", "config.json"));
|
|
941
|
-
console.log(chalk9.yellow("LeanSpec already initialized in this directory."));
|
|
942
|
-
console.log(chalk9.gray("To reinitialize, delete .lspec/ directory first."));
|
|
943
|
-
return;
|
|
944
|
-
} catch {
|
|
945
|
-
}
|
|
946
|
-
console.log("");
|
|
947
|
-
console.log(chalk9.green("Welcome to LeanSpec!"));
|
|
948
|
-
console.log("");
|
|
949
|
-
const setupMode = await select({
|
|
950
|
-
message: "How would you like to set up?",
|
|
951
|
-
choices: [
|
|
952
|
-
{
|
|
953
|
-
name: "Quick start (recommended)",
|
|
954
|
-
value: "quick",
|
|
955
|
-
description: "Use standard template, start immediately"
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
name: "Choose template",
|
|
959
|
-
value: "template",
|
|
960
|
-
description: "Pick from: minimal, standard, enterprise"
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
name: "Customize everything",
|
|
964
|
-
value: "custom",
|
|
965
|
-
description: "Full control over structure and settings"
|
|
966
|
-
}
|
|
967
|
-
]
|
|
968
|
-
});
|
|
969
|
-
let templateName = "standard";
|
|
970
|
-
if (setupMode === "template") {
|
|
971
|
-
templateName = await select({
|
|
972
|
-
message: "Select template:",
|
|
973
|
-
choices: [
|
|
974
|
-
{ name: "minimal", value: "minimal", description: "Just folder structure, no extras" },
|
|
975
|
-
{ name: "standard", value: "standard", description: "Recommended - includes AGENTS.md" },
|
|
976
|
-
{
|
|
977
|
-
name: "enterprise",
|
|
978
|
-
value: "enterprise",
|
|
979
|
-
description: "Governance with approvals and compliance"
|
|
980
|
-
}
|
|
981
|
-
]
|
|
982
|
-
});
|
|
983
|
-
} else if (setupMode === "custom") {
|
|
984
|
-
console.log(chalk9.yellow("Full customization coming soon. Using standard for now."));
|
|
985
|
-
}
|
|
986
|
-
const templateDir = path11.join(TEMPLATES_DIR, templateName);
|
|
987
|
-
const templateConfigPath = path11.join(templateDir, "config.json");
|
|
988
|
-
let templateConfig;
|
|
989
|
-
try {
|
|
990
|
-
const content = await fs10.readFile(templateConfigPath, "utf-8");
|
|
991
|
-
templateConfig = JSON.parse(content).config;
|
|
992
|
-
} catch {
|
|
993
|
-
console.error(chalk9.red(`Error: Template not found: ${templateName}`));
|
|
994
|
-
process.exit(1);
|
|
995
|
-
}
|
|
996
|
-
const templatesDir = path11.join(cwd, ".lspec", "templates");
|
|
997
|
-
try {
|
|
998
|
-
await fs10.mkdir(templatesDir, { recursive: true });
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
console.error(chalk9.red("Error creating templates directory:"), error);
|
|
1001
|
-
process.exit(1);
|
|
1002
|
-
}
|
|
1003
|
-
const templateSpecPath = path11.join(templateDir, "spec-template.md");
|
|
1004
|
-
const targetSpecPath = path11.join(templatesDir, "spec-template.md");
|
|
1005
|
-
try {
|
|
1006
|
-
await fs10.copyFile(templateSpecPath, targetSpecPath);
|
|
1007
|
-
console.log(chalk9.green("\u2713 Created .lspec/templates/spec-template.md"));
|
|
1008
|
-
} catch (error) {
|
|
1009
|
-
console.error(chalk9.red("Error copying template:"), error);
|
|
1010
|
-
process.exit(1);
|
|
1011
|
-
}
|
|
1012
|
-
templateConfig.template = "spec-template.md";
|
|
1013
|
-
templateConfig.templates = {
|
|
1014
|
-
default: "spec-template.md"
|
|
1015
|
-
};
|
|
1016
|
-
await saveConfig(templateConfig, cwd);
|
|
1017
|
-
console.log(chalk9.green("\u2713 Created .lspec/config.json"));
|
|
1018
|
-
const existingFiles = await detectExistingSystemPrompts(cwd);
|
|
1019
|
-
let skipFiles = [];
|
|
1020
|
-
if (existingFiles.length > 0) {
|
|
1021
|
-
console.log("");
|
|
1022
|
-
console.log(chalk9.yellow(`Found existing: ${existingFiles.join(", ")}`));
|
|
1023
|
-
const action = await select({
|
|
1024
|
-
message: "How would you like to proceed?",
|
|
1025
|
-
choices: [
|
|
1026
|
-
{
|
|
1027
|
-
name: "Merge - Add LeanSpec section to existing files",
|
|
1028
|
-
value: "merge",
|
|
1029
|
-
description: "Appends LeanSpec guidance to your existing AGENTS.md"
|
|
1030
|
-
},
|
|
1031
|
-
{
|
|
1032
|
-
name: "Backup - Save existing and create new",
|
|
1033
|
-
value: "backup",
|
|
1034
|
-
description: "Renames existing files to .backup and creates fresh ones"
|
|
1035
|
-
},
|
|
1036
|
-
{
|
|
1037
|
-
name: "Skip - Keep existing files as-is",
|
|
1038
|
-
value: "skip",
|
|
1039
|
-
description: "Only adds .lspec config and specs/ directory"
|
|
1040
|
-
}
|
|
1041
|
-
]
|
|
1042
|
-
});
|
|
1043
|
-
const projectName2 = await getProjectName(cwd);
|
|
1044
|
-
await handleExistingFiles(action, existingFiles, templateDir, cwd, { project_name: projectName2 });
|
|
1045
|
-
if (action === "skip") {
|
|
1046
|
-
skipFiles = existingFiles;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
const projectName = await getProjectName(cwd);
|
|
1050
|
-
const filesDir = path11.join(templateDir, "files");
|
|
1051
|
-
try {
|
|
1052
|
-
await copyDirectory(filesDir, cwd, skipFiles, { project_name: projectName });
|
|
1053
|
-
console.log(chalk9.green("\u2713 Initialized project structure"));
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
console.error(chalk9.red("Error copying template files:"), error);
|
|
1056
|
-
process.exit(1);
|
|
1057
|
-
}
|
|
1058
|
-
console.log("");
|
|
1059
|
-
console.log(chalk9.green("\u2713 LeanSpec initialized!"));
|
|
1060
|
-
console.log("");
|
|
1061
|
-
console.log("Next steps:");
|
|
1062
|
-
console.log(chalk9.gray(" - Review and customize AGENTS.md"));
|
|
1063
|
-
console.log(chalk9.gray(" - Check out example spec in specs/"));
|
|
1064
|
-
console.log(chalk9.gray(" - Create your first spec: lspec create my-feature"));
|
|
1065
|
-
console.log("");
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// src/commands/files.ts
|
|
1069
|
-
import * as fs11 from "fs/promises";
|
|
1070
|
-
import * as path12 from "path";
|
|
1071
|
-
import chalk10 from "chalk";
|
|
1072
|
-
async function filesCommand(specPath, options = {}) {
|
|
1073
|
-
const config = await loadConfig();
|
|
1074
|
-
const cwd = process.cwd();
|
|
1075
|
-
const specsDir = path12.join(cwd, config.specsDir);
|
|
1076
|
-
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1077
|
-
if (!resolvedPath) {
|
|
1078
|
-
console.error(chalk10.red(`Spec not found: ${specPath}`));
|
|
1079
|
-
console.error(
|
|
1080
|
-
chalk10.gray("Try using the full path or spec name (e.g., 001-my-spec)")
|
|
1081
|
-
);
|
|
1082
|
-
process.exit(1);
|
|
1083
|
-
}
|
|
1084
|
-
const spec = await getSpec(resolvedPath);
|
|
1085
|
-
if (!spec) {
|
|
1086
|
-
console.error(chalk10.red(`Could not load spec: ${specPath}`));
|
|
1087
|
-
process.exit(1);
|
|
1088
|
-
}
|
|
1089
|
-
const subFiles = await loadSubFiles(spec.fullPath);
|
|
1090
|
-
console.log("");
|
|
1091
|
-
console.log(chalk10.cyan(`\u{1F4C4} Files in ${spec.name}`));
|
|
1092
|
-
console.log("");
|
|
1093
|
-
console.log(chalk10.green("Required:"));
|
|
1094
|
-
const readmeStat = await fs11.stat(spec.filePath);
|
|
1095
|
-
const readmeSize = formatSize(readmeStat.size);
|
|
1096
|
-
console.log(chalk10.green(` \u2713 README.md (${readmeSize}) Main spec`));
|
|
1097
|
-
console.log("");
|
|
1098
|
-
let filteredFiles = subFiles;
|
|
1099
|
-
if (options.type === "docs") {
|
|
1100
|
-
filteredFiles = subFiles.filter((f) => f.type === "document");
|
|
1101
|
-
} else if (options.type === "assets") {
|
|
1102
|
-
filteredFiles = subFiles.filter((f) => f.type === "asset");
|
|
1103
|
-
}
|
|
1104
|
-
if (filteredFiles.length === 0) {
|
|
1105
|
-
console.log(chalk10.gray("No additional files"));
|
|
1106
|
-
console.log("");
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const documents = filteredFiles.filter((f) => f.type === "document");
|
|
1110
|
-
const assets = filteredFiles.filter((f) => f.type === "asset");
|
|
1111
|
-
if (documents.length > 0 && (!options.type || options.type === "docs")) {
|
|
1112
|
-
console.log(chalk10.cyan("Documents:"));
|
|
1113
|
-
for (const file of documents) {
|
|
1114
|
-
const size = formatSize(file.size);
|
|
1115
|
-
console.log(chalk10.cyan(` \u2713 ${file.name.padEnd(20)} (${size})`));
|
|
1116
|
-
}
|
|
1117
|
-
console.log("");
|
|
1118
|
-
}
|
|
1119
|
-
if (assets.length > 0 && (!options.type || options.type === "assets")) {
|
|
1120
|
-
console.log(chalk10.yellow("Assets:"));
|
|
1121
|
-
for (const file of assets) {
|
|
1122
|
-
const size = formatSize(file.size);
|
|
1123
|
-
console.log(chalk10.yellow(` \u2713 ${file.name.padEnd(20)} (${size})`));
|
|
1124
|
-
}
|
|
1125
|
-
console.log("");
|
|
1126
|
-
}
|
|
1127
|
-
const totalFiles = filteredFiles.length + 1;
|
|
1128
|
-
const totalSize = formatSize(
|
|
1129
|
-
readmeStat.size + filteredFiles.reduce((sum, f) => sum + f.size, 0)
|
|
1130
|
-
);
|
|
1131
|
-
console.log(chalk10.gray(`Total: ${totalFiles} files, ${totalSize}`));
|
|
1132
|
-
console.log("");
|
|
1133
|
-
}
|
|
1134
|
-
function formatSize(bytes) {
|
|
1135
|
-
if (bytes < 1024) {
|
|
1136
|
-
return `${bytes} B`;
|
|
1137
|
-
} else if (bytes < 1024 * 1024) {
|
|
1138
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1139
|
-
} else {
|
|
1140
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// src/commands/board.ts
|
|
1145
|
-
import React2 from "react";
|
|
1146
|
-
import { render } from "ink";
|
|
1147
|
-
|
|
1148
|
-
// src/components/Board.tsx
|
|
1149
|
-
import React from "react";
|
|
1150
|
-
import { Box, Text } from "ink";
|
|
1151
|
-
var STATUS_CONFIG = {
|
|
1152
|
-
planned: { emoji: "\u{1F4C5}", label: "Planned", color: "gray" },
|
|
1153
|
-
"in-progress": { emoji: "\u{1F528}", label: "In Progress", color: "yellow" },
|
|
1154
|
-
complete: { emoji: "\u2705", label: "Complete", color: "green" },
|
|
1155
|
-
archived: { emoji: "\u{1F4E6}", label: "Archived", color: "gray" }
|
|
1156
|
-
};
|
|
1157
|
-
var Column = ({ title, emoji, specs, expanded, color }) => {
|
|
1158
|
-
const width = 60;
|
|
1159
|
-
const count = specs.length;
|
|
1160
|
-
const header = `${emoji} ${title} (${count})`;
|
|
1161
|
-
const padding = Math.max(0, width - header.length - 4);
|
|
1162
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, null, "\u250C\u2500 ", header, " ", "\u2500".repeat(padding), "\u2510"), expanded && specs.length > 0 ? specs.map((spec, index) => /* @__PURE__ */ React.createElement(Box, { key: spec.path, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, null, "\u2502 ", /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, spec.path.padEnd(width - 2)), "\u2502"), (spec.frontmatter.tags?.length || spec.frontmatter.priority || spec.frontmatter.assignee) && (() => {
|
|
1163
|
-
const parts = [];
|
|
1164
|
-
if (spec.frontmatter.tags?.length) {
|
|
1165
|
-
parts.push(`[${spec.frontmatter.tags.join(", ")}]`);
|
|
1166
|
-
}
|
|
1167
|
-
if (spec.frontmatter.priority) {
|
|
1168
|
-
parts.push(`priority: ${spec.frontmatter.priority}`);
|
|
1169
|
-
}
|
|
1170
|
-
if (spec.frontmatter.assignee) {
|
|
1171
|
-
parts.push(`assignee: ${spec.frontmatter.assignee}`);
|
|
1172
|
-
}
|
|
1173
|
-
const metaText = parts.join(" ");
|
|
1174
|
-
const paddingNeeded = Math.max(0, width - 2 - metaText.length);
|
|
1175
|
-
return /* @__PURE__ */ React.createElement(Text, null, "\u2502 ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, metaText), " ".repeat(paddingNeeded), "\u2502");
|
|
1176
|
-
})(), index < specs.length - 1 && /* @__PURE__ */ React.createElement(Text, null, "\u2502 ", " ".repeat(width - 2), "\u2502"))) : !expanded && specs.length > 0 ? /* @__PURE__ */ React.createElement(Text, null, "\u2502 ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(collapsed, use --show-complete to expand)"), " ".repeat(Math.max(0, width - 47)), "\u2502") : /* @__PURE__ */ React.createElement(Text, null, "\u2502 ", /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(no specs)"), " ".repeat(Math.max(0, width - 13)), "\u2502"), /* @__PURE__ */ React.createElement(Text, null, "\u2514", "\u2500".repeat(width), "\u2518"));
|
|
1177
|
-
};
|
|
1178
|
-
var Board = ({ specs, showComplete, filter }) => {
|
|
1179
|
-
const columns = {
|
|
1180
|
-
planned: [],
|
|
1181
|
-
"in-progress": [],
|
|
1182
|
-
complete: [],
|
|
1183
|
-
archived: []
|
|
1184
|
-
};
|
|
1185
|
-
for (const spec of specs) {
|
|
1186
|
-
const status = columns[spec.frontmatter.status] !== void 0 ? spec.frontmatter.status : "planned";
|
|
1187
|
-
columns[status].push(spec);
|
|
1188
|
-
}
|
|
1189
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true, color: "green" }, "\u{1F4CB} Spec Board")), filter && (filter.tag || filter.assignee) && /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Filtered by: ", filter.tag && `tag=${filter.tag}`, filter.tag && filter.assignee && ", ", filter.assignee && `assignee=${filter.assignee}`)), /* @__PURE__ */ React.createElement(
|
|
1190
|
-
Column,
|
|
1191
|
-
{
|
|
1192
|
-
title: STATUS_CONFIG.planned.label,
|
|
1193
|
-
emoji: STATUS_CONFIG.planned.emoji,
|
|
1194
|
-
specs: columns.planned,
|
|
1195
|
-
expanded: true,
|
|
1196
|
-
color: STATUS_CONFIG.planned.color
|
|
1197
|
-
}
|
|
1198
|
-
), /* @__PURE__ */ React.createElement(
|
|
1199
|
-
Column,
|
|
1200
|
-
{
|
|
1201
|
-
title: STATUS_CONFIG["in-progress"].label,
|
|
1202
|
-
emoji: STATUS_CONFIG["in-progress"].emoji,
|
|
1203
|
-
specs: columns["in-progress"],
|
|
1204
|
-
expanded: true,
|
|
1205
|
-
color: STATUS_CONFIG["in-progress"].color
|
|
1206
|
-
}
|
|
1207
|
-
), /* @__PURE__ */ React.createElement(
|
|
1208
|
-
Column,
|
|
1209
|
-
{
|
|
1210
|
-
title: STATUS_CONFIG.complete.label,
|
|
1211
|
-
emoji: STATUS_CONFIG.complete.emoji,
|
|
1212
|
-
specs: columns.complete,
|
|
1213
|
-
expanded: showComplete || false,
|
|
1214
|
-
color: STATUS_CONFIG.complete.color
|
|
1215
|
-
}
|
|
1216
|
-
));
|
|
1217
|
-
};
|
|
1218
|
-
|
|
1219
|
-
// src/commands/board.ts
|
|
1220
|
-
async function boardCommand(options) {
|
|
1221
|
-
const filter = {};
|
|
1222
|
-
if (options.tag) {
|
|
1223
|
-
filter.tags = [options.tag];
|
|
1224
|
-
}
|
|
1225
|
-
if (options.assignee) {
|
|
1226
|
-
filter.assignee = options.assignee;
|
|
1227
|
-
}
|
|
1228
|
-
const specs = await withSpinner(
|
|
1229
|
-
"Loading specs...",
|
|
1230
|
-
() => loadAllSpecs({
|
|
1231
|
-
includeArchived: false,
|
|
1232
|
-
filter
|
|
1233
|
-
})
|
|
1234
|
-
);
|
|
1235
|
-
if (specs.length === 0) {
|
|
1236
|
-
console.log("No specs found.");
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
const filterOptions = {
|
|
1240
|
-
tag: options.tag,
|
|
1241
|
-
assignee: options.assignee
|
|
1242
|
-
};
|
|
1243
|
-
render(
|
|
1244
|
-
React2.createElement(Board, {
|
|
1245
|
-
specs,
|
|
1246
|
-
showComplete: options.showComplete,
|
|
1247
|
-
filter: options.tag || options.assignee ? filterOptions : void 0
|
|
1248
|
-
})
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// src/commands/stats.ts
|
|
1253
|
-
import React4 from "react";
|
|
1254
|
-
import { render as render2 } from "ink";
|
|
1255
|
-
|
|
1256
|
-
// src/components/StatsDisplay.tsx
|
|
1257
|
-
import React3 from "react";
|
|
1258
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
1259
|
-
var StatsDisplay = ({ specs, filter }) => {
|
|
1260
|
-
const statusCounts = {
|
|
1261
|
-
planned: 0,
|
|
1262
|
-
"in-progress": 0,
|
|
1263
|
-
complete: 0,
|
|
1264
|
-
archived: 0
|
|
1265
|
-
};
|
|
1266
|
-
const priorityCounts = {
|
|
1267
|
-
low: 0,
|
|
1268
|
-
medium: 0,
|
|
1269
|
-
high: 0,
|
|
1270
|
-
critical: 0
|
|
1271
|
-
};
|
|
1272
|
-
const tagCounts = {};
|
|
1273
|
-
for (const spec of specs) {
|
|
1274
|
-
statusCounts[spec.frontmatter.status]++;
|
|
1275
|
-
if (spec.frontmatter.priority) {
|
|
1276
|
-
priorityCounts[spec.frontmatter.priority]++;
|
|
1277
|
-
}
|
|
1278
|
-
if (spec.frontmatter.tags) {
|
|
1279
|
-
for (const tag of spec.frontmatter.tags) {
|
|
1280
|
-
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
const topTags = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1285
|
-
const totalWithPriority = Object.values(priorityCounts).reduce((sum, count) => sum + count, 0);
|
|
1286
|
-
return /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { bold: true, color: "green" }, "\u{1F4CA} Spec Statistics")), filter && (filter.tag || filter.assignee) && /* @__PURE__ */ React3.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, "Filtered by: ", filter.tag && `tag=${filter.tag}`, filter.tag && filter.assignee && ", ", filter.assignee && `assignee=${filter.assignee}`)), /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { bold: true }, "Status:"), /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F4C5} Planned: ", /* @__PURE__ */ React3.createElement(Text2, { color: "cyan" }, statusCounts.planned.toString().padStart(3))), /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F528} In Progress: ", /* @__PURE__ */ React3.createElement(Text2, { color: "yellow" }, statusCounts["in-progress"].toString().padStart(3))), /* @__PURE__ */ React3.createElement(Text2, null, " \u2705 Complete: ", /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, statusCounts.complete.toString().padStart(3))), /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F4E6} Archived: ", /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, statusCounts.archived.toString().padStart(3)))), totalWithPriority > 0 && /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { bold: true }, "Priority:"), priorityCounts.critical > 0 && /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F534} Critical: ", /* @__PURE__ */ React3.createElement(Text2, { color: "red" }, priorityCounts.critical.toString().padStart(3))), priorityCounts.high > 0 && /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F7E1} High: ", /* @__PURE__ */ React3.createElement(Text2, { color: "yellow" }, priorityCounts.high.toString().padStart(3))), priorityCounts.medium > 0 && /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F7E0} Medium: ", /* @__PURE__ */ React3.createElement(Text2, { color: "blue" }, priorityCounts.medium.toString().padStart(3))), priorityCounts.low > 0 && /* @__PURE__ */ React3.createElement(Text2, null, " \u{1F7E2} Low: ", /* @__PURE__ */ React3.createElement(Text2, { dimColor: true }, priorityCounts.low.toString().padStart(3)))), topTags.length > 0 && /* @__PURE__ */ React3.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text2, { bold: true }, "Tags (top ", topTags.length, "):"), topTags.map(([tag, count]) => /* @__PURE__ */ React3.createElement(Text2, { key: tag }, " ", tag.padEnd(20), " ", /* @__PURE__ */ React3.createElement(Text2, { color: "cyan" }, count.toString().padStart(3))))), /* @__PURE__ */ React3.createElement(Box2, null, /* @__PURE__ */ React3.createElement(Text2, { bold: true }, "Total Specs: ", /* @__PURE__ */ React3.createElement(Text2, { color: "green" }, specs.length.toString()))));
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
// src/commands/stats.ts
|
|
1290
|
-
async function statsCommand(options) {
|
|
1291
|
-
const filter = {};
|
|
1292
|
-
if (options.tag) {
|
|
1293
|
-
filter.tags = [options.tag];
|
|
1294
|
-
}
|
|
1295
|
-
if (options.assignee) {
|
|
1296
|
-
filter.assignee = options.assignee;
|
|
1297
|
-
}
|
|
1298
|
-
const specs = await withSpinner(
|
|
1299
|
-
"Loading specs...",
|
|
1300
|
-
() => loadAllSpecs({
|
|
1301
|
-
includeArchived: true,
|
|
1302
|
-
filter
|
|
1303
|
-
})
|
|
1304
|
-
);
|
|
1305
|
-
if (specs.length === 0) {
|
|
1306
|
-
console.log("No specs found.");
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
if (options.json) {
|
|
1310
|
-
const statusCounts = {
|
|
1311
|
-
planned: 0,
|
|
1312
|
-
"in-progress": 0,
|
|
1313
|
-
complete: 0,
|
|
1314
|
-
archived: 0
|
|
1315
|
-
};
|
|
1316
|
-
const priorityCounts = {
|
|
1317
|
-
low: 0,
|
|
1318
|
-
medium: 0,
|
|
1319
|
-
high: 0,
|
|
1320
|
-
critical: 0
|
|
1321
|
-
};
|
|
1322
|
-
const tagCounts = {};
|
|
1323
|
-
for (const spec of specs) {
|
|
1324
|
-
statusCounts[spec.frontmatter.status]++;
|
|
1325
|
-
if (spec.frontmatter.priority) {
|
|
1326
|
-
priorityCounts[spec.frontmatter.priority]++;
|
|
1327
|
-
}
|
|
1328
|
-
if (spec.frontmatter.tags) {
|
|
1329
|
-
for (const tag of spec.frontmatter.tags) {
|
|
1330
|
-
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
const data = {
|
|
1335
|
-
total: specs.length,
|
|
1336
|
-
status: statusCounts,
|
|
1337
|
-
priority: priorityCounts,
|
|
1338
|
-
tags: tagCounts,
|
|
1339
|
-
filter
|
|
1340
|
-
};
|
|
1341
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
const filterOptions = {
|
|
1345
|
-
tag: options.tag,
|
|
1346
|
-
assignee: options.assignee
|
|
1347
|
-
};
|
|
1348
|
-
render2(
|
|
1349
|
-
React4.createElement(StatsDisplay, {
|
|
1350
|
-
specs,
|
|
1351
|
-
filter: options.tag || options.assignee ? filterOptions : void 0
|
|
1352
|
-
})
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// src/commands/search.ts
|
|
1357
|
-
import chalk11 from "chalk";
|
|
1358
|
-
async function searchCommand(query, options) {
|
|
1359
|
-
const filter = {};
|
|
1360
|
-
if (options.status) filter.status = options.status;
|
|
1361
|
-
if (options.tag) filter.tags = [options.tag];
|
|
1362
|
-
if (options.priority) filter.priority = options.priority;
|
|
1363
|
-
if (options.assignee) filter.assignee = options.assignee;
|
|
1364
|
-
const specs = await withSpinner(
|
|
1365
|
-
"Searching specs...",
|
|
1366
|
-
() => loadAllSpecs({
|
|
1367
|
-
includeArchived: true,
|
|
1368
|
-
includeContent: true,
|
|
1369
|
-
filter
|
|
1370
|
-
})
|
|
1371
|
-
);
|
|
1372
|
-
if (specs.length === 0) {
|
|
1373
|
-
console.log("No specs found matching filters.");
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
const results = [];
|
|
1377
|
-
const queryLower = query.toLowerCase();
|
|
1378
|
-
for (const spec of specs) {
|
|
1379
|
-
if (!spec.content) continue;
|
|
1380
|
-
const matches = [];
|
|
1381
|
-
const lines = spec.content.split("\n");
|
|
1382
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1383
|
-
const line = lines[i];
|
|
1384
|
-
if (line.toLowerCase().includes(queryLower)) {
|
|
1385
|
-
const contextStart = Math.max(0, i - 1);
|
|
1386
|
-
const contextEnd = Math.min(lines.length - 1, i + 1);
|
|
1387
|
-
const context = lines.slice(contextStart, contextEnd + 1);
|
|
1388
|
-
const matchLine = context[i - contextStart];
|
|
1389
|
-
const highlighted = highlightMatch(matchLine, query);
|
|
1390
|
-
matches.push(highlighted);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (matches.length > 0) {
|
|
1394
|
-
results.push({ spec, matches });
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
if (results.length === 0) {
|
|
1398
|
-
console.log("");
|
|
1399
|
-
console.log(chalk11.yellow(`\u{1F50D} No specs found matching "${query}"`));
|
|
1400
|
-
if (Object.keys(filter).length > 0) {
|
|
1401
|
-
const filters = [];
|
|
1402
|
-
if (options.status) filters.push(`status=${options.status}`);
|
|
1403
|
-
if (options.tag) filters.push(`tag=${options.tag}`);
|
|
1404
|
-
if (options.priority) filters.push(`priority=${options.priority}`);
|
|
1405
|
-
if (options.assignee) filters.push(`assignee=${options.assignee}`);
|
|
1406
|
-
console.log(chalk11.gray(`With filters: ${filters.join(", ")}`));
|
|
1407
|
-
}
|
|
1408
|
-
console.log("");
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
console.log("");
|
|
1412
|
-
console.log(chalk11.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${query}"`));
|
|
1413
|
-
if (Object.keys(filter).length > 0) {
|
|
1414
|
-
const filters = [];
|
|
1415
|
-
if (options.status) filters.push(`status=${options.status}`);
|
|
1416
|
-
if (options.tag) filters.push(`tag=${options.tag}`);
|
|
1417
|
-
if (options.priority) filters.push(`priority=${options.priority}`);
|
|
1418
|
-
if (options.assignee) filters.push(`assignee=${options.assignee}`);
|
|
1419
|
-
console.log(chalk11.gray(`With filters: ${filters.join(", ")}`));
|
|
1420
|
-
}
|
|
1421
|
-
console.log("");
|
|
1422
|
-
for (const result of results) {
|
|
1423
|
-
const { spec, matches } = result;
|
|
1424
|
-
console.log(chalk11.cyan(`${spec.frontmatter.status === "in-progress" ? "\u{1F528}" : spec.frontmatter.status === "complete" ? "\u2705" : "\u{1F4C5}"} ${spec.path}`));
|
|
1425
|
-
const meta = [];
|
|
1426
|
-
if (spec.frontmatter.priority) {
|
|
1427
|
-
const priorityEmoji = spec.frontmatter.priority === "critical" ? "\u{1F534}" : spec.frontmatter.priority === "high" ? "\u{1F7E1}" : spec.frontmatter.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
1428
|
-
meta.push(`${priorityEmoji} ${spec.frontmatter.priority}`);
|
|
1429
|
-
}
|
|
1430
|
-
if (spec.frontmatter.tags && spec.frontmatter.tags.length > 0) {
|
|
1431
|
-
meta.push(`[${spec.frontmatter.tags.join(", ")}]`);
|
|
1432
|
-
}
|
|
1433
|
-
if (meta.length > 0) {
|
|
1434
|
-
console.log(chalk11.gray(` ${meta.join(" \u2022 ")}`));
|
|
1435
|
-
}
|
|
1436
|
-
const maxMatches = 3;
|
|
1437
|
-
for (let i = 0; i < Math.min(matches.length, maxMatches); i++) {
|
|
1438
|
-
console.log(` ${chalk11.gray("Match:")} ${matches[i].trim()}`);
|
|
1439
|
-
}
|
|
1440
|
-
if (matches.length > maxMatches) {
|
|
1441
|
-
console.log(chalk11.gray(` ... and ${matches.length - maxMatches} more match${matches.length - maxMatches === 1 ? "" : "es"}`));
|
|
1442
|
-
}
|
|
1443
|
-
console.log("");
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
function highlightMatch(text, query) {
|
|
1447
|
-
const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
|
|
1448
|
-
return text.replace(regex, chalk11.yellow("$1"));
|
|
1449
|
-
}
|
|
1450
|
-
function escapeRegex(str) {
|
|
1451
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// src/commands/deps.ts
|
|
1455
|
-
import chalk12 from "chalk";
|
|
1456
|
-
async function depsCommand(specPath, options) {
|
|
1457
|
-
const spec = await getSpec(specPath);
|
|
1458
|
-
if (!spec) {
|
|
1459
|
-
console.error(chalk12.red(`Error: Spec not found: ${specPath}`));
|
|
1460
|
-
process.exit(1);
|
|
1461
|
-
}
|
|
1462
|
-
const allSpecs = await loadAllSpecs({ includeArchived: true });
|
|
1463
|
-
const specMap = /* @__PURE__ */ new Map();
|
|
1464
|
-
for (const s of allSpecs) {
|
|
1465
|
-
specMap.set(s.path, s);
|
|
1466
|
-
}
|
|
1467
|
-
const dependsOn = findDependencies(spec, specMap);
|
|
1468
|
-
const blocks = findBlocking(spec, allSpecs);
|
|
1469
|
-
const related = findRelated(spec, specMap);
|
|
1470
|
-
if (options.json) {
|
|
1471
|
-
const data = {
|
|
1472
|
-
spec: spec.path,
|
|
1473
|
-
dependsOn: dependsOn.map((s) => ({ path: s.path, status: s.frontmatter.status })),
|
|
1474
|
-
blocks: blocks.map((s) => ({ path: s.path, status: s.frontmatter.status })),
|
|
1475
|
-
related: related.map((s) => ({ path: s.path, status: s.frontmatter.status })),
|
|
1476
|
-
chain: buildDependencyChain(spec, specMap, options.depth || 3)
|
|
1477
|
-
};
|
|
1478
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
console.log("");
|
|
1482
|
-
console.log(chalk12.green(`\u{1F4E6} Dependencies for ${chalk12.cyan(spec.path)}`));
|
|
1483
|
-
console.log("");
|
|
1484
|
-
console.log(chalk12.bold("Depends On:"));
|
|
1485
|
-
if (dependsOn.length > 0) {
|
|
1486
|
-
for (const dep of dependsOn) {
|
|
1487
|
-
const status = getStatusIndicator(dep.frontmatter.status);
|
|
1488
|
-
console.log(` \u2192 ${dep.path} ${status}`);
|
|
1489
|
-
}
|
|
1490
|
-
} else {
|
|
1491
|
-
console.log(chalk12.gray(" (none)"));
|
|
1492
|
-
}
|
|
1493
|
-
console.log("");
|
|
1494
|
-
console.log(chalk12.bold("Blocks:"));
|
|
1495
|
-
if (blocks.length > 0) {
|
|
1496
|
-
for (const blocked of blocks) {
|
|
1497
|
-
const status = getStatusIndicator(blocked.frontmatter.status);
|
|
1498
|
-
console.log(` \u2190 ${blocked.path} ${status}`);
|
|
1499
|
-
}
|
|
1500
|
-
} else {
|
|
1501
|
-
console.log(chalk12.gray(" (none)"));
|
|
1502
|
-
}
|
|
1503
|
-
console.log("");
|
|
1504
|
-
if (related.length > 0) {
|
|
1505
|
-
console.log(chalk12.bold("Related:"));
|
|
1506
|
-
for (const rel of related) {
|
|
1507
|
-
const status = getStatusIndicator(rel.frontmatter.status);
|
|
1508
|
-
console.log(` \u27F7 ${rel.path} ${status}`);
|
|
1509
|
-
}
|
|
1510
|
-
console.log("");
|
|
1511
|
-
}
|
|
1512
|
-
if (options.graph || dependsOn.length > 0) {
|
|
1513
|
-
console.log(chalk12.bold("Dependency Chain:"));
|
|
1514
|
-
const chain = buildDependencyChain(spec, specMap, options.depth || 3);
|
|
1515
|
-
displayChain(chain, 0);
|
|
1516
|
-
console.log("");
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
function findDependencies(spec, specMap) {
|
|
1520
|
-
if (!spec.frontmatter.depends_on) return [];
|
|
1521
|
-
const deps = [];
|
|
1522
|
-
for (const depPath of spec.frontmatter.depends_on) {
|
|
1523
|
-
const dep = specMap.get(depPath);
|
|
1524
|
-
if (dep) {
|
|
1525
|
-
deps.push(dep);
|
|
1526
|
-
} else {
|
|
1527
|
-
for (const [path13, s] of specMap.entries()) {
|
|
1528
|
-
if (path13.includes(depPath)) {
|
|
1529
|
-
deps.push(s);
|
|
1530
|
-
break;
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
return deps;
|
|
1536
|
-
}
|
|
1537
|
-
function findBlocking(spec, allSpecs) {
|
|
1538
|
-
const blocks = [];
|
|
1539
|
-
for (const other of allSpecs) {
|
|
1540
|
-
if (other.path === spec.path) continue;
|
|
1541
|
-
if (other.frontmatter.depends_on) {
|
|
1542
|
-
for (const depPath of other.frontmatter.depends_on) {
|
|
1543
|
-
if (depPath === spec.path || spec.path.includes(depPath)) {
|
|
1544
|
-
blocks.push(other);
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
return blocks;
|
|
1551
|
-
}
|
|
1552
|
-
function findRelated(spec, specMap) {
|
|
1553
|
-
if (!spec.frontmatter.related) return [];
|
|
1554
|
-
const related = [];
|
|
1555
|
-
for (const relPath of spec.frontmatter.related) {
|
|
1556
|
-
const rel = specMap.get(relPath);
|
|
1557
|
-
if (rel) {
|
|
1558
|
-
related.push(rel);
|
|
1559
|
-
} else {
|
|
1560
|
-
for (const [path13, s] of specMap.entries()) {
|
|
1561
|
-
if (path13.includes(relPath)) {
|
|
1562
|
-
related.push(s);
|
|
1563
|
-
break;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
return related;
|
|
1569
|
-
}
|
|
1570
|
-
function buildDependencyChain(spec, specMap, maxDepth, currentDepth = 0, visited = /* @__PURE__ */ new Set()) {
|
|
1571
|
-
const node = {
|
|
1572
|
-
spec,
|
|
1573
|
-
dependencies: []
|
|
1574
|
-
};
|
|
1575
|
-
if (visited.has(spec.path)) {
|
|
1576
|
-
return node;
|
|
1577
|
-
}
|
|
1578
|
-
visited.add(spec.path);
|
|
1579
|
-
if (currentDepth >= maxDepth) {
|
|
1580
|
-
return node;
|
|
1581
|
-
}
|
|
1582
|
-
const deps = findDependencies(spec, specMap);
|
|
1583
|
-
for (const dep of deps) {
|
|
1584
|
-
node.dependencies.push(buildDependencyChain(dep, specMap, maxDepth, currentDepth + 1, visited));
|
|
1585
|
-
}
|
|
1586
|
-
return node;
|
|
1587
|
-
}
|
|
1588
|
-
function displayChain(node, level) {
|
|
1589
|
-
const indent = " ".repeat(level);
|
|
1590
|
-
const status = getStatusIndicator(node.spec.frontmatter.status);
|
|
1591
|
-
const name = level === 0 ? chalk12.cyan(node.spec.path) : node.spec.path;
|
|
1592
|
-
console.log(`${indent}${name} ${status}`);
|
|
1593
|
-
for (const dep of node.dependencies) {
|
|
1594
|
-
const prefix = " ".repeat(level) + "\u2514\u2500 ";
|
|
1595
|
-
const depStatus = getStatusIndicator(dep.spec.frontmatter.status);
|
|
1596
|
-
console.log(`${prefix}${dep.spec.path} ${depStatus}`);
|
|
1597
|
-
for (const nestedDep of dep.dependencies) {
|
|
1598
|
-
displayChain(nestedDep, level + 2);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
function getStatusIndicator(status) {
|
|
1603
|
-
switch (status) {
|
|
1604
|
-
case "planned":
|
|
1605
|
-
return chalk12.gray("[planned]");
|
|
1606
|
-
case "in-progress":
|
|
1607
|
-
return chalk12.yellow("[in-progress]");
|
|
1608
|
-
case "complete":
|
|
1609
|
-
return chalk12.green("\u2713");
|
|
1610
|
-
case "archived":
|
|
1611
|
-
return chalk12.gray("[archived]");
|
|
1612
|
-
default:
|
|
1613
|
-
return "";
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
// src/commands/timeline.ts
|
|
1618
|
-
import chalk13 from "chalk";
|
|
1619
|
-
import dayjs2 from "dayjs";
|
|
1620
|
-
async function timelineCommand(options) {
|
|
1621
|
-
const days = options.days || 30;
|
|
1622
|
-
const specs = await loadAllSpecs({
|
|
1623
|
-
includeArchived: true
|
|
1624
|
-
});
|
|
1625
|
-
if (specs.length === 0) {
|
|
1626
|
-
console.log("No specs found.");
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
const today = dayjs2();
|
|
1630
|
-
const startDate = today.subtract(days, "day");
|
|
1631
|
-
const createdByDate = {};
|
|
1632
|
-
const completedByDate = {};
|
|
1633
|
-
const createdByMonth = {};
|
|
1634
|
-
for (const spec of specs) {
|
|
1635
|
-
const created = dayjs2(spec.frontmatter.created);
|
|
1636
|
-
if (created.isAfter(startDate)) {
|
|
1637
|
-
const dateKey = created.format("YYYY-MM-DD");
|
|
1638
|
-
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
1639
|
-
}
|
|
1640
|
-
const monthKey = created.format("MMM YYYY");
|
|
1641
|
-
createdByMonth[monthKey] = (createdByMonth[monthKey] || 0) + 1;
|
|
1642
|
-
if (spec.frontmatter.completed) {
|
|
1643
|
-
const completed = dayjs2(spec.frontmatter.completed);
|
|
1644
|
-
if (completed.isAfter(startDate)) {
|
|
1645
|
-
const dateKey = completed.format("YYYY-MM-DD");
|
|
1646
|
-
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
console.log("");
|
|
1651
|
-
console.log(chalk13.green(`\u{1F4C8} Spec Timeline (Last ${days} Days)`));
|
|
1652
|
-
console.log("");
|
|
1653
|
-
const allDates = /* @__PURE__ */ new Set([...Object.keys(createdByDate), ...Object.keys(completedByDate)]);
|
|
1654
|
-
const sortedDates = Array.from(allDates).sort();
|
|
1655
|
-
if (sortedDates.length > 0) {
|
|
1656
|
-
for (const date of sortedDates) {
|
|
1657
|
-
const created = createdByDate[date] || 0;
|
|
1658
|
-
const completed = completedByDate[date] || 0;
|
|
1659
|
-
const createdBar = "\u2588".repeat(created);
|
|
1660
|
-
const completedBar = "\u2588".repeat(completed);
|
|
1661
|
-
let line = `${date} `;
|
|
1662
|
-
if (created > 0) {
|
|
1663
|
-
line += `${chalk13.blue(createdBar)} ${created} created`;
|
|
1664
|
-
}
|
|
1665
|
-
if (completed > 0) {
|
|
1666
|
-
if (created > 0) line += " ";
|
|
1667
|
-
line += `${chalk13.green(completedBar)} ${completed} completed`;
|
|
1668
|
-
}
|
|
1669
|
-
console.log(line);
|
|
1670
|
-
}
|
|
1671
|
-
console.log("");
|
|
1672
|
-
}
|
|
1673
|
-
const sortedMonths = Object.entries(createdByMonth).sort((a, b) => {
|
|
1674
|
-
const dateA = dayjs2(a[0], "MMM YYYY");
|
|
1675
|
-
const dateB = dayjs2(b[0], "MMM YYYY");
|
|
1676
|
-
return dateB.diff(dateA);
|
|
1677
|
-
}).slice(0, 6);
|
|
1678
|
-
if (sortedMonths.length > 0) {
|
|
1679
|
-
console.log(chalk13.bold("Created by Month:"));
|
|
1680
|
-
for (const [month, count] of sortedMonths) {
|
|
1681
|
-
console.log(` ${month}: ${chalk13.cyan(count.toString())} specs`);
|
|
1682
|
-
}
|
|
1683
|
-
console.log("");
|
|
1684
|
-
}
|
|
1685
|
-
const last7Days = specs.filter((s) => {
|
|
1686
|
-
if (!s.frontmatter.completed) return false;
|
|
1687
|
-
const completed = dayjs2(s.frontmatter.completed);
|
|
1688
|
-
return completed.isAfter(today.subtract(7, "day"));
|
|
1689
|
-
}).length;
|
|
1690
|
-
const last30Days = specs.filter((s) => {
|
|
1691
|
-
if (!s.frontmatter.completed) return false;
|
|
1692
|
-
const completed = dayjs2(s.frontmatter.completed);
|
|
1693
|
-
return completed.isAfter(today.subtract(30, "day"));
|
|
1694
|
-
}).length;
|
|
1695
|
-
console.log(chalk13.bold("Completion Rate:"));
|
|
1696
|
-
console.log(` Last 7 days: ${chalk13.green(last7Days.toString())} specs completed`);
|
|
1697
|
-
console.log(` Last 30 days: ${chalk13.green(last30Days.toString())} specs completed`);
|
|
1698
|
-
console.log("");
|
|
1699
|
-
if (options.byTag) {
|
|
1700
|
-
const tagStats = {};
|
|
1701
|
-
for (const spec of specs) {
|
|
1702
|
-
const created = dayjs2(spec.frontmatter.created);
|
|
1703
|
-
const isInRange = created.isAfter(startDate);
|
|
1704
|
-
if (isInRange && spec.frontmatter.tags) {
|
|
1705
|
-
for (const tag of spec.frontmatter.tags) {
|
|
1706
|
-
if (!tagStats[tag]) tagStats[tag] = { created: 0, completed: 0 };
|
|
1707
|
-
tagStats[tag].created++;
|
|
1708
|
-
if (spec.frontmatter.completed) {
|
|
1709
|
-
const completed = dayjs2(spec.frontmatter.completed);
|
|
1710
|
-
if (completed.isAfter(startDate)) {
|
|
1711
|
-
tagStats[tag].completed++;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
const sortedTags = Object.entries(tagStats).sort((a, b) => b[1].created - a[1].created).slice(0, 10);
|
|
1718
|
-
if (sortedTags.length > 0) {
|
|
1719
|
-
console.log(chalk13.bold("By Tag:"));
|
|
1720
|
-
for (const [tag, stats] of sortedTags) {
|
|
1721
|
-
console.log(` ${tag.padEnd(20)} ${chalk13.blue(stats.created.toString())} created, ${chalk13.green(stats.completed.toString())} completed`);
|
|
1722
|
-
}
|
|
1723
|
-
console.log("");
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
if (options.byAssignee) {
|
|
1727
|
-
const assigneeStats = {};
|
|
1728
|
-
for (const spec of specs) {
|
|
1729
|
-
if (!spec.frontmatter.assignee) continue;
|
|
1730
|
-
const created = dayjs2(spec.frontmatter.created);
|
|
1731
|
-
const isInRange = created.isAfter(startDate);
|
|
1732
|
-
if (isInRange) {
|
|
1733
|
-
const assignee = spec.frontmatter.assignee;
|
|
1734
|
-
if (!assigneeStats[assignee]) assigneeStats[assignee] = { created: 0, completed: 0 };
|
|
1735
|
-
assigneeStats[assignee].created++;
|
|
1736
|
-
if (spec.frontmatter.completed) {
|
|
1737
|
-
const completed = dayjs2(spec.frontmatter.completed);
|
|
1738
|
-
if (completed.isAfter(startDate)) {
|
|
1739
|
-
assigneeStats[assignee].completed++;
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
const sortedAssignees = Object.entries(assigneeStats).sort((a, b) => b[1].created - a[1].created);
|
|
1745
|
-
if (sortedAssignees.length > 0) {
|
|
1746
|
-
console.log(chalk13.bold("By Assignee:"));
|
|
1747
|
-
for (const [assignee, stats] of sortedAssignees) {
|
|
1748
|
-
console.log(` ${assignee.padEnd(20)} ${chalk13.blue(stats.created.toString())} created, ${chalk13.green(stats.completed.toString())} completed`);
|
|
1749
|
-
}
|
|
1750
|
-
console.log("");
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// src/commands/gantt.ts
|
|
1756
|
-
import chalk14 from "chalk";
|
|
1757
|
-
import dayjs3 from "dayjs";
|
|
1758
|
-
async function ganttCommand(options) {
|
|
1759
|
-
const weeks = options.weeks || 4;
|
|
1760
|
-
const specs = await loadAllSpecs({
|
|
1761
|
-
includeArchived: false
|
|
1762
|
-
});
|
|
1763
|
-
if (specs.length === 0) {
|
|
1764
|
-
console.log("No specs found.");
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const relevantSpecs = specs.filter((spec) => {
|
|
1768
|
-
if (!options.showComplete && spec.frontmatter.status === "complete") {
|
|
1769
|
-
return false;
|
|
1770
|
-
}
|
|
1771
|
-
return spec.frontmatter.due || spec.frontmatter.depends_on || spec.frontmatter.status === "in-progress" || spec.frontmatter.status === "complete";
|
|
1772
|
-
});
|
|
1773
|
-
if (relevantSpecs.length === 0) {
|
|
1774
|
-
console.log("No specs found with due dates or dependencies.");
|
|
1775
|
-
console.log(chalk14.gray('Tip: Add a "due: YYYY-MM-DD" field to frontmatter to use gantt view.'));
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
const today = dayjs3();
|
|
1779
|
-
const startDate = today.startOf("week");
|
|
1780
|
-
const endDate = startDate.add(weeks, "week");
|
|
1781
|
-
console.log("");
|
|
1782
|
-
console.log(chalk14.green("\u{1F4C5} Gantt Chart"));
|
|
1783
|
-
console.log("");
|
|
1784
|
-
const timelineHeader = buildTimelineHeader(startDate, weeks);
|
|
1785
|
-
console.log(timelineHeader);
|
|
1786
|
-
console.log("|" + "--------|".repeat(weeks));
|
|
1787
|
-
console.log("");
|
|
1788
|
-
for (const spec of relevantSpecs) {
|
|
1789
|
-
displaySpecTimeline(spec, startDate, endDate, weeks, specs);
|
|
1790
|
-
console.log("");
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
function buildTimelineHeader(startDate, weeks) {
|
|
1794
|
-
const dates = [];
|
|
1795
|
-
for (let i = 0; i < weeks; i++) {
|
|
1796
|
-
const date = startDate.add(i, "week");
|
|
1797
|
-
dates.push(date.format("MMM D").padEnd(8));
|
|
1798
|
-
}
|
|
1799
|
-
return dates.join(" ");
|
|
1800
|
-
}
|
|
1801
|
-
function displaySpecTimeline(spec, startDate, endDate, weeks, allSpecs) {
|
|
1802
|
-
console.log(chalk14.cyan(spec.path));
|
|
1803
|
-
if (spec.frontmatter.depends_on && spec.frontmatter.depends_on.length > 0) {
|
|
1804
|
-
console.log(chalk14.gray(` \u21B3 depends on: ${spec.frontmatter.depends_on.join(", ")}`));
|
|
1805
|
-
}
|
|
1806
|
-
const bar = buildTimelineBar(spec, startDate, endDate, weeks);
|
|
1807
|
-
console.log(bar);
|
|
1808
|
-
const meta = [];
|
|
1809
|
-
meta.push(getStatusLabel(spec.frontmatter.status));
|
|
1810
|
-
if (spec.frontmatter.due) {
|
|
1811
|
-
meta.push(`due: ${spec.frontmatter.due}`);
|
|
1812
|
-
}
|
|
1813
|
-
console.log(chalk14.gray(` (${meta.join(", ")})`));
|
|
1814
|
-
}
|
|
1815
|
-
function buildTimelineBar(spec, startDate, endDate, weeks) {
|
|
1816
|
-
const charsPerWeek = 8;
|
|
1817
|
-
const totalChars = weeks * charsPerWeek;
|
|
1818
|
-
const created = dayjs3(spec.frontmatter.created);
|
|
1819
|
-
const due = spec.frontmatter.due ? dayjs3(spec.frontmatter.due) : null;
|
|
1820
|
-
const completed = spec.frontmatter.completed ? dayjs3(spec.frontmatter.completed) : null;
|
|
1821
|
-
let specStart = created;
|
|
1822
|
-
let specEnd = due || completed;
|
|
1823
|
-
if (!specEnd && spec.frontmatter.status !== "complete") {
|
|
1824
|
-
specEnd = created.add(2, "week");
|
|
1825
|
-
}
|
|
1826
|
-
if (!specEnd) {
|
|
1827
|
-
const daysFromStart = created.diff(startDate, "day");
|
|
1828
|
-
const position = Math.floor(daysFromStart / 7 * charsPerWeek);
|
|
1829
|
-
if (position >= 0 && position < totalChars) {
|
|
1830
|
-
const bar2 = " ".repeat(position) + "\u25A0" + " ".repeat(totalChars - position - 1);
|
|
1831
|
-
return bar2;
|
|
1832
|
-
}
|
|
1833
|
-
return " ".repeat(totalChars);
|
|
1834
|
-
}
|
|
1835
|
-
const startDaysFromStart = specStart.diff(startDate, "day");
|
|
1836
|
-
const endDaysFromStart = specEnd.diff(startDate, "day");
|
|
1837
|
-
const startPos = Math.floor(startDaysFromStart / 7 * charsPerWeek);
|
|
1838
|
-
const endPos = Math.floor(endDaysFromStart / 7 * charsPerWeek);
|
|
1839
|
-
const barStart = Math.max(0, startPos);
|
|
1840
|
-
const barEnd = Math.min(totalChars, endPos);
|
|
1841
|
-
const barLength = Math.max(1, barEnd - barStart);
|
|
1842
|
-
let fillChar = "\u25A0";
|
|
1843
|
-
let emptyChar = "\u25A1";
|
|
1844
|
-
let color = chalk14.blue;
|
|
1845
|
-
if (spec.frontmatter.status === "complete") {
|
|
1846
|
-
fillChar = "\u25A0";
|
|
1847
|
-
color = chalk14.green;
|
|
1848
|
-
} else if (spec.frontmatter.status === "in-progress") {
|
|
1849
|
-
fillChar = "\u25A0";
|
|
1850
|
-
emptyChar = "\u25A1";
|
|
1851
|
-
color = chalk14.yellow;
|
|
1852
|
-
const halfLength = Math.floor(barLength / 2);
|
|
1853
|
-
const filled = fillChar.repeat(halfLength);
|
|
1854
|
-
const empty = emptyChar.repeat(barLength - halfLength);
|
|
1855
|
-
const bar2 = " ".repeat(barStart) + color(filled + empty) + " ".repeat(Math.max(0, totalChars - barEnd));
|
|
1856
|
-
return bar2;
|
|
1857
|
-
} else {
|
|
1858
|
-
fillChar = "\u25A1";
|
|
1859
|
-
color = chalk14.gray;
|
|
1860
|
-
}
|
|
1861
|
-
const bar = " ".repeat(barStart) + color(fillChar.repeat(barLength)) + " ".repeat(Math.max(0, totalChars - barEnd));
|
|
1862
|
-
return bar;
|
|
1863
|
-
}
|
|
1864
|
-
function getStatusLabel(status) {
|
|
1865
|
-
switch (status) {
|
|
1866
|
-
case "planned":
|
|
1867
|
-
return "planned";
|
|
1868
|
-
case "in-progress":
|
|
1869
|
-
return "in-progress";
|
|
1870
|
-
case "complete":
|
|
1871
|
-
return "complete";
|
|
1872
|
-
case "archived":
|
|
1873
|
-
return "archived";
|
|
1874
|
-
default:
|
|
1875
|
-
return status;
|
|
1876
|
-
}
|
|
44
|
+
return customFields;
|
|
1877
45
|
}
|
|
1878
46
|
|
|
1879
47
|
// src/cli.ts
|
|
1880
48
|
var program = new Command();
|
|
1881
|
-
program.name("
|
|
1882
|
-
program.
|
|
1883
|
-
|
|
49
|
+
program.name("lean-spec").description("Manage LeanSpec documents").version("0.1.2");
|
|
50
|
+
program.addHelpText("after", `
|
|
51
|
+
Command Groups:
|
|
52
|
+
|
|
53
|
+
Core Commands:
|
|
54
|
+
init Initialize LeanSpec in current directory
|
|
55
|
+
create <name> Create new spec in folder structure
|
|
56
|
+
list List all specs
|
|
57
|
+
update <spec> Update spec metadata
|
|
58
|
+
archive <spec> Move spec to archived/
|
|
59
|
+
backfill [specs...] Backfill timestamps from git history
|
|
60
|
+
|
|
61
|
+
Viewing & Navigation:
|
|
62
|
+
view <spec> View spec content
|
|
63
|
+
open <spec> Open spec in editor
|
|
64
|
+
search <query> Full-text search with metadata filters
|
|
65
|
+
files <spec> List files in a spec
|
|
66
|
+
|
|
67
|
+
Project & Analytics:
|
|
68
|
+
board Show Kanban-style board view
|
|
69
|
+
stats Show aggregate statistics
|
|
70
|
+
timeline Show creation/completion over time
|
|
71
|
+
gantt Show timeline with dependencies
|
|
72
|
+
deps <spec> Show dependency graph for a spec
|
|
73
|
+
|
|
74
|
+
Maintenance:
|
|
75
|
+
check Check for sequence conflicts
|
|
76
|
+
validate [specs...] Validate specs for quality issues
|
|
77
|
+
templates Manage spec templates
|
|
78
|
+
|
|
79
|
+
Server:
|
|
80
|
+
mcp Start MCP server for AI assistants
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
$ lean-spec init
|
|
84
|
+
$ lean-spec create my-feature --priority high
|
|
85
|
+
$ lean-spec list --status in-progress
|
|
86
|
+
$ lean-spec view 042
|
|
87
|
+
$ lean-spec backfill --dry-run
|
|
88
|
+
$ lean-spec board --tag backend
|
|
89
|
+
$ lean-spec search "authentication"
|
|
90
|
+
$ lean-spec validate
|
|
91
|
+
$ lean-spec validate --verbose
|
|
92
|
+
$ lean-spec validate --quiet --rule max-lines
|
|
93
|
+
$ lean-spec validate 018 --max-lines 500
|
|
94
|
+
`);
|
|
95
|
+
program.command("archive <spec>").description("Move spec to archived/").action(async (specPath) => {
|
|
96
|
+
await archiveSpec(specPath);
|
|
97
|
+
});
|
|
98
|
+
program.command("backfill [specs...]").description("Backfill timestamps from git history").option("--dry-run", "Show what would be updated without making changes").option("--force", "Overwrite existing timestamp values").option("--assignee", "Include assignee from first commit author").option("--transitions", "Include full status transition history").option("--all", "Include all optional fields (assignee + transitions)").action(async (specs, options) => {
|
|
99
|
+
await backfillTimestamps({
|
|
100
|
+
dryRun: options.dryRun,
|
|
101
|
+
force: options.force,
|
|
102
|
+
includeAssignee: options.assignee || options.all,
|
|
103
|
+
includeTransitions: options.transitions || options.all,
|
|
104
|
+
specs: specs && specs.length > 0 ? specs : void 0
|
|
105
|
+
});
|
|
1884
106
|
});
|
|
1885
|
-
program.command("
|
|
107
|
+
program.command("board").description("Show Kanban-style board view with project completion summary").option("--complete", "Include complete specs (default: hidden)").option("--simple", "Hide completion summary (kanban only)").option("--completion-only", "Show only completion summary (no kanban)").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").action(async (options) => {
|
|
108
|
+
await boardCommand(options);
|
|
109
|
+
});
|
|
110
|
+
program.command("check").description("Check for sequence conflicts").option("-q, --quiet", "Brief output").action(async (options) => {
|
|
111
|
+
const hasNoConflicts = await checkSpecs(options);
|
|
112
|
+
process.exit(hasNoConflicts ? 0 : 1);
|
|
113
|
+
});
|
|
114
|
+
program.command("validate [specs...]").description("Validate specs for quality issues").option("--max-lines <number>", "Custom line limit (default: 400)", parseInt).option("--verbose", "Show passing specs").option("--quiet", "Suppress warnings, only show errors").option("--format <format>", "Output format: default, json, compact", "default").option("--rule <rule>", "Filter by specific rule name (e.g., max-lines, frontmatter)").action(async (specs, options) => {
|
|
115
|
+
const passed = await validateCommand({
|
|
116
|
+
maxLines: options.maxLines,
|
|
117
|
+
specs: specs && specs.length > 0 ? specs : void 0,
|
|
118
|
+
verbose: options.verbose,
|
|
119
|
+
quiet: options.quiet,
|
|
120
|
+
format: options.format,
|
|
121
|
+
rule: options.rule
|
|
122
|
+
});
|
|
123
|
+
process.exit(passed ? 0 : 1);
|
|
124
|
+
});
|
|
125
|
+
program.command("create <name>").description("Create new spec in folder structure").option("--title <title>", "Set custom title").option("--description <desc>", "Set initial description").option("--tags <tags>", "Set tags (comma-separated)").option("--priority <priority>", "Set priority (low, medium, high, critical)").option("--assignee <name>", "Set assignee").option("--template <template>", "Use a specific template").option("--field <name=value...>", "Set custom field (can specify multiple)").option("--no-prefix", "Skip date prefix even if configured").action(async (name, options) => {
|
|
126
|
+
const customFields = parseCustomFieldOptions(options.field);
|
|
1886
127
|
const createOptions = {
|
|
1887
128
|
title: options.title,
|
|
1888
129
|
description: options.description,
|
|
1889
130
|
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : void 0,
|
|
1890
131
|
priority: options.priority,
|
|
1891
132
|
assignee: options.assignee,
|
|
1892
|
-
template: options.template
|
|
133
|
+
template: options.template,
|
|
134
|
+
customFields: Object.keys(customFields).length > 0 ? customFields : void 0,
|
|
135
|
+
noPrefix: options.prefix === false
|
|
1893
136
|
};
|
|
1894
137
|
await createSpec(name, createOptions);
|
|
1895
138
|
});
|
|
1896
|
-
program.command("
|
|
1897
|
-
await
|
|
139
|
+
program.command("deps <spec>").description("Show dependency graph for a spec. Related specs (\u27F7) are shown bidirectionally, depends_on (\u2192) are directional.").option("--depth <n>", "Show N levels deep (default: 3)", parseInt).option("--graph", "ASCII graph visualization").option("--json", "Output as JSON").action(async (specPath, options) => {
|
|
140
|
+
await depsCommand(specPath, options);
|
|
141
|
+
});
|
|
142
|
+
program.command("files <spec>").description("List files in a spec").option("--type <type>", "Filter by type: docs, assets").option("--tree", "Show tree structure").action(async (specPath, options) => {
|
|
143
|
+
await filesCommand(specPath, options);
|
|
144
|
+
});
|
|
145
|
+
program.command("gantt").description("Show timeline with dependencies").option("--weeks <n>", "Show N weeks (default: 4)", parseInt).option("--show-complete", "Include completed specs").option("--critical-path", "Highlight critical path").action(async (options) => {
|
|
146
|
+
await ganttCommand(options);
|
|
147
|
+
});
|
|
148
|
+
program.command("init").description("Initialize LeanSpec in current directory").action(async () => {
|
|
149
|
+
await initProject();
|
|
1898
150
|
});
|
|
1899
|
-
program.command("list").description("List all specs").option("--archived", "Include archived specs").option("--status <status>", "Filter by status (planned, in-progress, complete, archived)").option("--tag <tag...>", "Filter by tag (can specify multiple)").option("--priority <priority>", "Filter by priority (low, medium, high, critical)").option("--assignee <name>", "Filter by assignee").action(async (options) => {
|
|
151
|
+
program.command("list").description("List all specs").option("--archived", "Include archived specs").option("--status <status>", "Filter by status (planned, in-progress, complete, archived)").option("--tag <tag...>", "Filter by tag (can specify multiple)").option("--priority <priority>", "Filter by priority (low, medium, high, critical)").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").option("--sort <field>", "Sort by field (id, created, name, status, priority)", "id").option("--order <order>", "Sort order (asc, desc)", "desc").action(async (options) => {
|
|
152
|
+
const customFields = parseCustomFieldOptions(options.field);
|
|
1900
153
|
const listOptions = {
|
|
1901
154
|
showArchived: options.archived,
|
|
1902
155
|
status: options.status,
|
|
1903
156
|
tags: options.tag,
|
|
1904
157
|
priority: options.priority,
|
|
1905
|
-
assignee: options.assignee
|
|
158
|
+
assignee: options.assignee,
|
|
159
|
+
customFields: Object.keys(customFields).length > 0 ? customFields : void 0,
|
|
160
|
+
sortBy: options.sort || "id",
|
|
161
|
+
sortOrder: options.order || "desc"
|
|
1906
162
|
};
|
|
1907
163
|
await listSpecs(listOptions);
|
|
1908
164
|
});
|
|
1909
|
-
program.command("
|
|
1910
|
-
|
|
165
|
+
program.command("open <spec>").description("Open spec in editor").option("--editor <editor>", "Specify editor command").action(async (specPath, options) => {
|
|
166
|
+
try {
|
|
167
|
+
await openCommand(specPath, {
|
|
168
|
+
editor: options.editor
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
program.command("search <query>").description("Full-text search with metadata filters").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").option("--priority <priority>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").action(async (query, options) => {
|
|
176
|
+
const customFields = parseCustomFieldOptions(options.field);
|
|
177
|
+
await searchCommand(query, {
|
|
1911
178
|
status: options.status,
|
|
179
|
+
tag: options.tag,
|
|
1912
180
|
priority: options.priority,
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
};
|
|
1916
|
-
Object.keys(updates).forEach((key) => {
|
|
1917
|
-
if (updates[key] === void 0) {
|
|
1918
|
-
delete updates[key];
|
|
1919
|
-
}
|
|
181
|
+
assignee: options.assignee,
|
|
182
|
+
customFields: Object.keys(customFields).length > 0 ? customFields : void 0
|
|
1920
183
|
});
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
}
|
|
1925
|
-
await updateSpec(specPath, updates);
|
|
184
|
+
});
|
|
185
|
+
program.command("stats").description("Show aggregate statistics (default: simplified view)").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").option("--full", "Show full detailed analytics (all sections)").option("--timeline", "Show only timeline section").option("--velocity", "Show only velocity section").option("--json", "Output as JSON").action(async (options) => {
|
|
186
|
+
await statsCommand(options);
|
|
1926
187
|
});
|
|
1927
188
|
var templatesCmd = program.command("templates").description("Manage spec templates");
|
|
1928
189
|
templatesCmd.command("list").description("List available templates").action(async () => {
|
|
@@ -1943,26 +204,43 @@ templatesCmd.command("copy <source> <target>").description("Copy a template to c
|
|
|
1943
204
|
templatesCmd.action(async () => {
|
|
1944
205
|
await listTemplates();
|
|
1945
206
|
});
|
|
1946
|
-
program.command("stats").description("Show aggregate statistics").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").option("--json", "Output as JSON").action(async (options) => {
|
|
1947
|
-
await statsCommand(options);
|
|
1948
|
-
});
|
|
1949
|
-
program.command("board").description("Show Kanban-style board view").option("--show-complete", "Expand complete column").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").action(async (options) => {
|
|
1950
|
-
await boardCommand(options);
|
|
1951
|
-
});
|
|
1952
207
|
program.command("timeline").description("Show creation/completion over time").option("--days <n>", "Show last N days (default: 30)", parseInt).option("--by-tag", "Group by tag").option("--by-assignee", "Group by assignee").action(async (options) => {
|
|
1953
208
|
await timelineCommand(options);
|
|
1954
209
|
});
|
|
1955
|
-
program.command("
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
210
|
+
program.command("update <spec>").description("Update spec metadata").option("--status <status>", "Set status (planned, in-progress, complete, archived)").option("--priority <priority>", "Set priority (low, medium, high, critical)").option("--tags <tags>", "Set tags (comma-separated)").option("--assignee <name>", "Set assignee").option("--field <name=value...>", "Set custom field (can specify multiple)").action(async (specPath, options) => {
|
|
211
|
+
const customFields = parseCustomFieldOptions(options.field);
|
|
212
|
+
const updates = {
|
|
213
|
+
status: options.status,
|
|
214
|
+
priority: options.priority,
|
|
215
|
+
tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : void 0,
|
|
216
|
+
assignee: options.assignee,
|
|
217
|
+
customFields: Object.keys(customFields).length > 0 ? customFields : void 0
|
|
218
|
+
};
|
|
219
|
+
Object.keys(updates).forEach((key) => {
|
|
220
|
+
if (updates[key] === void 0) {
|
|
221
|
+
delete updates[key];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (Object.keys(updates).length === 0) {
|
|
225
|
+
console.error("Error: At least one update option required (--status, --priority, --tags, --assignee, --field)");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
await updateSpec(specPath, updates);
|
|
1960
229
|
});
|
|
1961
|
-
program.command("
|
|
1962
|
-
|
|
230
|
+
program.command("view <spec>").description('View spec content (supports sub-specs like "045/DESIGN.md")').option("--raw", "Output raw markdown (for piping/scripting)").option("--json", "Output as JSON").option("--no-color", "Disable colors").action(async (specPath, options) => {
|
|
231
|
+
try {
|
|
232
|
+
await viewCommand(specPath, {
|
|
233
|
+
raw: options.raw,
|
|
234
|
+
json: options.json,
|
|
235
|
+
noColor: options.color === false
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
1963
241
|
});
|
|
1964
|
-
program.command("
|
|
1965
|
-
await
|
|
242
|
+
program.command("mcp").description("Start MCP server for AI assistants (Claude Desktop, Cline, etc.)").action(async () => {
|
|
243
|
+
await mcpCommand();
|
|
1966
244
|
});
|
|
1967
245
|
program.parse();
|
|
1968
246
|
//# sourceMappingURL=cli.js.map
|