diff --git a/src/cli.ts b/src/cli.ts index a6bb95a..7aa1428 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -307,8 +307,8 @@ function suggestDoctorActions(findings: { rule: string; message: string }[]) { return actions; } -function runCommand( - fn: () => T, +async function runCommand( + fn: () => T | Promise, payload: (data: T) => { data: unknown; affectedFiles?: { path: string }[]; @@ -317,7 +317,7 @@ function runCommand( }, ) { try { - outputSuccess(payload(fn())); + outputSuccess(payload(await fn())); } catch (error) { const info = errorInfo(error); outputError({ code: info.code, message: info.message, details: info.details, actions: info.actions }); diff --git a/src/usecase-commands.ts b/src/usecase-commands.ts index 72e0367..e527181 100644 --- a/src/usecase-commands.ts +++ b/src/usecase-commands.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, promises as fsPromises, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { orderActorFrontmatter, @@ -84,11 +84,21 @@ export function createUseCase(args: { return { key, path: relativePath(path, root), format: "BRIEF" as const, affectedFiles }; } -export function listUseCases(args: { cwd?: string; status?: string; actor?: string; level?: string; q?: string }) { +export async function listUseCases(args: { cwd?: string; status?: string; actor?: string; level?: string; q?: string }) { const config = readConfig(args.cwd ?? process.cwd()); if (!config) throw new Error("NOT_INITIALIZED"); - return walkFiles(join(config.root, "specs/usecases"), (path) => path.endsWith(".md")) - .map((path) => ({ path, parsed: parseUseCaseMarkdown(readFileSync(path, "utf8")) })) + + const files = walkFiles(join(config.root, "specs/usecases"), (path) => path.endsWith(".md")); + + // ⚡ Bolt: Optimize I/O by reading files concurrently + const items = await Promise.all( + files.map(async (path) => { + const content = await fsPromises.readFile(path, "utf8"); + return { path, parsed: parseUseCaseMarkdown(content) }; + }) + ); + + return items .filter(({ parsed }) => !args.status || parsed.frontmatter.status === args.status.toUpperCase()) .filter(({ parsed }) => !args.actor || parsed.frontmatter.primary_actor === slugify(args.actor!)) .filter(({ parsed }) => !args.level || parsed.frontmatter.level === parseLevel(args.level!)) diff --git a/tests/authoring.test.ts b/tests/authoring.test.ts index 8e83335..fcedeb6 100644 --- a/tests/authoring.test.ts +++ b/tests/authoring.test.ts @@ -11,7 +11,7 @@ import { normalizeUseCaseMarkdown } from "../src/format/normalize.js"; import { runDoctor } from "../src/validate/doctor.js"; describe("use-case authoring loop", () => { - it("runs init -> usecase create -> round-trip -> doctor with no errors", () => { + it("runs init -> usecase create -> round-trip -> doctor with no errors", async () => { const root = join(tmpdir(), `vspec-authoring-${crypto.randomUUID()}`); mkdirSync(root, { recursive: true }); initProject({ root, key: "VSPEC" }); @@ -19,7 +19,7 @@ describe("use-case authoring loop", () => { const file = readFileSync(join(root, created.path), "utf8"); expect(serializeUseCase(parseUseCaseMarkdown(file))).toBe(normalizeUseCaseMarkdown(file)); expect(runDoctor({ root, target: created.key }).findings.filter((finding) => finding.level === "error")).toEqual([]); - expect(listUseCases({ cwd: root })).toHaveLength(1); + expect(await listUseCases({ cwd: root })).toHaveLength(1); expect(showUseCase({ cwd: root, key: created.key }).useCase.frontmatter.title).toBe("Author a use case"); rmSync(root, { recursive: true, force: true }); });