@joshski/dust 0.1.58 → 0.1.60

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.
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from './cli/types';
1
+ import type { ReadableFileSystem } from '../cli/types';
2
2
  export interface IdeaOption {
3
3
  name: string;
4
4
  description: string;
@@ -22,4 +22,4 @@ export declare function parseOpenQuestions(content: string): IdeaOpenQuestion[];
22
22
  /**
23
23
  * Parses an idea markdown file into a structured Idea object.
24
24
  */
25
- export declare function parseIdea(fileSystem: FileSystem, dustPath: string, slug: string): Promise<Idea>;
25
+ export declare function parseIdea(fileSystem: ReadableFileSystem, dustPath: string, slug: string): Promise<Idea>;
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from './cli/types';
1
+ import type { FileSystem, ReadableFileSystem } from '../cli/types';
2
2
  export declare const IDEA_TRANSITION_PREFIXES: string[];
3
3
  export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
4
4
  export declare const BUILD_IDEA_PREFIX = "Build Idea: ";
@@ -11,7 +11,7 @@ export interface ParsedCaptureIdeaTask {
11
11
  ideaDescription: string;
12
12
  buildItNow: boolean;
13
13
  }
14
- export declare function findAllCaptureIdeaTasks(fileSystem: FileSystem, dustPath: string): Promise<IdeaInProgress[]>;
14
+ export declare function findAllCaptureIdeaTasks(fileSystem: ReadableFileSystem, dustPath: string): Promise<IdeaInProgress[]>;
15
15
  /**
16
16
  * Converts a markdown title to the expected filename using deterministic rules:
17
17
  * 1. Convert to lowercase
@@ -28,7 +28,7 @@ export interface WorkflowTaskMatch {
28
28
  ideaSlug: string;
29
29
  taskSlug: string;
30
30
  }
31
- export declare function findWorkflowTaskForIdea(fileSystem: FileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
31
+ export declare function findWorkflowTaskForIdea(fileSystem: ReadableFileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
32
32
  export interface CreateIdeaTransitionTaskResult {
33
33
  filePath: string;
34
34
  }
@@ -49,4 +49,4 @@ export declare function createCaptureIdeaTask(fileSystem: FileSystem, dustPath:
49
49
  description: string;
50
50
  buildItNow?: boolean;
51
51
  }): Promise<CreateIdeaTransitionTaskResult>;
52
- export declare function parseCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
52
+ export declare function parseCaptureIdeaTask(fileSystem: ReadableFileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
@@ -0,0 +1,599 @@
1
+ // lib/markdown/markdown-utilities.ts
2
+ function extractTitle(content) {
3
+ const match = content.match(/^#\s+(.+)$/m);
4
+ return match ? match[1].trim() : null;
5
+ }
6
+ var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
7
+ function extractOpeningSentence(content) {
8
+ const lines = content.split(`
9
+ `);
10
+ let h1Index = -1;
11
+ for (let i = 0;i < lines.length; i++) {
12
+ if (lines[i].match(/^#\s+.+$/)) {
13
+ h1Index = i;
14
+ break;
15
+ }
16
+ }
17
+ if (h1Index === -1) {
18
+ return null;
19
+ }
20
+ let paragraphStart = -1;
21
+ for (let i = h1Index + 1;i < lines.length; i++) {
22
+ const line = lines[i].trim();
23
+ if (line !== "") {
24
+ paragraphStart = i;
25
+ break;
26
+ }
27
+ }
28
+ if (paragraphStart === -1) {
29
+ return null;
30
+ }
31
+ const firstLine = lines[paragraphStart];
32
+ const trimmedFirstLine = firstLine.trim();
33
+ if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
34
+ return null;
35
+ }
36
+ let paragraph = "";
37
+ for (let i = paragraphStart;i < lines.length; i++) {
38
+ const line = lines[i].trim();
39
+ if (line === "")
40
+ break;
41
+ if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
42
+ break;
43
+ }
44
+ paragraph += (paragraph ? " " : "") + line;
45
+ }
46
+ const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
47
+ if (!sentenceMatch) {
48
+ return null;
49
+ }
50
+ return sentenceMatch[1];
51
+ }
52
+
53
+ // lib/artifacts/facts.ts
54
+ async function parseFact(fileSystem, dustPath, slug) {
55
+ const factPath = `${dustPath}/facts/${slug}.md`;
56
+ if (!fileSystem.exists(factPath)) {
57
+ throw new Error(`Fact not found: "${slug}" (expected file at ${factPath})`);
58
+ }
59
+ const content = await fileSystem.readFile(factPath);
60
+ const title = extractTitle(content);
61
+ if (!title) {
62
+ throw new Error(`Fact file has no title: ${factPath}`);
63
+ }
64
+ return {
65
+ slug,
66
+ title,
67
+ content
68
+ };
69
+ }
70
+
71
+ // lib/artifacts/ideas.ts
72
+ function parseOpenQuestions(content) {
73
+ const lines = content.split(`
74
+ `);
75
+ const questions = [];
76
+ let inOpenQuestions = false;
77
+ let currentQuestion = null;
78
+ let currentOption = null;
79
+ let descriptionLines = [];
80
+ function flushOption() {
81
+ if (currentOption) {
82
+ currentOption.description = descriptionLines.join(`
83
+ `).trim();
84
+ descriptionLines = [];
85
+ currentOption = null;
86
+ }
87
+ }
88
+ function flushQuestion() {
89
+ flushOption();
90
+ if (currentQuestion) {
91
+ questions.push(currentQuestion);
92
+ currentQuestion = null;
93
+ }
94
+ }
95
+ for (const line of lines) {
96
+ if (line.startsWith("## ")) {
97
+ if (inOpenQuestions) {
98
+ flushQuestion();
99
+ }
100
+ inOpenQuestions = line.trimEnd() === "## Open Questions";
101
+ continue;
102
+ }
103
+ if (!inOpenQuestions)
104
+ continue;
105
+ if (line.startsWith("### ")) {
106
+ flushQuestion();
107
+ currentQuestion = {
108
+ question: line.slice(4).trim(),
109
+ options: []
110
+ };
111
+ continue;
112
+ }
113
+ if (line.startsWith("#### ")) {
114
+ flushOption();
115
+ currentOption = {
116
+ name: line.slice(5).trim(),
117
+ description: ""
118
+ };
119
+ if (currentQuestion) {
120
+ currentQuestion.options.push(currentOption);
121
+ }
122
+ continue;
123
+ }
124
+ if (currentOption) {
125
+ descriptionLines.push(line);
126
+ }
127
+ }
128
+ flushQuestion();
129
+ return questions;
130
+ }
131
+ async function parseIdea(fileSystem, dustPath, slug) {
132
+ const ideaPath = `${dustPath}/ideas/${slug}.md`;
133
+ if (!fileSystem.exists(ideaPath)) {
134
+ throw new Error(`Idea not found: "${slug}" (expected file at ${ideaPath})`);
135
+ }
136
+ const content = await fileSystem.readFile(ideaPath);
137
+ const title = extractTitle(content);
138
+ if (!title) {
139
+ throw new Error(`Idea file has no title: ${ideaPath}`);
140
+ }
141
+ const openingSentence = extractOpeningSentence(content);
142
+ const openQuestions = parseOpenQuestions(content);
143
+ return {
144
+ slug,
145
+ title,
146
+ openingSentence,
147
+ content,
148
+ openQuestions
149
+ };
150
+ }
151
+
152
+ // lib/artifacts/principles.ts
153
+ function extractLinksFromSection(content, sectionHeading) {
154
+ const lines = content.split(`
155
+ `);
156
+ const links = [];
157
+ let inSection = false;
158
+ for (const line of lines) {
159
+ if (line.startsWith("## ")) {
160
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
161
+ continue;
162
+ }
163
+ if (!inSection)
164
+ continue;
165
+ if (line.startsWith("# "))
166
+ break;
167
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
168
+ if (linkMatch) {
169
+ const target = linkMatch[2];
170
+ const slugMatch = target.match(/([^/]+)\.md$/);
171
+ if (slugMatch) {
172
+ links.push(slugMatch[1]);
173
+ }
174
+ }
175
+ }
176
+ return links;
177
+ }
178
+ function extractSingleLinkFromSection(content, sectionHeading) {
179
+ const links = extractLinksFromSection(content, sectionHeading);
180
+ return links.length === 1 ? links[0] : null;
181
+ }
182
+ async function parsePrinciple(fileSystem, dustPath, slug) {
183
+ const principlePath = `${dustPath}/principles/${slug}.md`;
184
+ if (!fileSystem.exists(principlePath)) {
185
+ throw new Error(`Principle not found: "${slug}" (expected file at ${principlePath})`);
186
+ }
187
+ const content = await fileSystem.readFile(principlePath);
188
+ const title = extractTitle(content);
189
+ if (!title) {
190
+ throw new Error(`Principle file has no title: ${principlePath}`);
191
+ }
192
+ const parentPrinciple = extractSingleLinkFromSection(content, "Parent Principle");
193
+ const subPrinciples = extractLinksFromSection(content, "Sub-Principles");
194
+ return {
195
+ slug,
196
+ title,
197
+ content,
198
+ parentPrinciple,
199
+ subPrinciples
200
+ };
201
+ }
202
+
203
+ // lib/artifacts/tasks.ts
204
+ function extractLinksFromSection2(content, sectionHeading) {
205
+ const lines = content.split(`
206
+ `);
207
+ const links = [];
208
+ let inSection = false;
209
+ for (const line of lines) {
210
+ if (line.startsWith("## ")) {
211
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
212
+ continue;
213
+ }
214
+ if (!inSection)
215
+ continue;
216
+ if (line.startsWith("# "))
217
+ break;
218
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
219
+ if (linkMatch) {
220
+ const target = linkMatch[2];
221
+ const slugMatch = target.match(/([^/]+)\.md$/);
222
+ if (slugMatch) {
223
+ links.push(slugMatch[1]);
224
+ }
225
+ }
226
+ }
227
+ return links;
228
+ }
229
+ function extractDefinitionOfDone(content) {
230
+ const lines = content.split(`
231
+ `);
232
+ const items = [];
233
+ let inSection = false;
234
+ for (const line of lines) {
235
+ if (line.startsWith("## ")) {
236
+ inSection = line.trimEnd() === "## Definition of Done";
237
+ continue;
238
+ }
239
+ if (!inSection)
240
+ continue;
241
+ if (line.startsWith("# "))
242
+ break;
243
+ const checklistMatch = line.match(/^-\s+\[[x\s]\]\s+(.+)$/i);
244
+ if (checklistMatch) {
245
+ items.push(checklistMatch[1].trim());
246
+ }
247
+ }
248
+ return items;
249
+ }
250
+ async function parseTask(fileSystem, dustPath, slug) {
251
+ const taskPath = `${dustPath}/tasks/${slug}.md`;
252
+ if (!fileSystem.exists(taskPath)) {
253
+ throw new Error(`Task not found: "${slug}" (expected file at ${taskPath})`);
254
+ }
255
+ const content = await fileSystem.readFile(taskPath);
256
+ const title = extractTitle(content);
257
+ if (!title) {
258
+ throw new Error(`Task file has no title: ${taskPath}`);
259
+ }
260
+ const principles = extractLinksFromSection2(content, "Principles");
261
+ const blockedBy = extractLinksFromSection2(content, "Blocked By");
262
+ const definitionOfDone = extractDefinitionOfDone(content);
263
+ return {
264
+ slug,
265
+ title,
266
+ content,
267
+ principles,
268
+ blockedBy,
269
+ definitionOfDone
270
+ };
271
+ }
272
+
273
+ // lib/artifacts/workflow-tasks.ts
274
+ var CAPTURE_IDEA_PREFIX = "Add Idea: ";
275
+ var BUILD_IDEA_PREFIX = "Build Idea: ";
276
+ function titleToFilename(title) {
277
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
278
+ }
279
+ var WORKFLOW_TASK_TYPES = [
280
+ { type: "refine", prefix: "Refine Idea: " },
281
+ { type: "decompose-idea", prefix: "Decompose Idea: " },
282
+ { type: "shelve", prefix: "Shelve Idea: " }
283
+ ];
284
+ async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
285
+ const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
286
+ for (const { type, prefix } of WORKFLOW_TASK_TYPES) {
287
+ const filename = titleToFilename(`${prefix}${ideaTitle}`);
288
+ const filePath = `${dustPath}/tasks/${filename}`;
289
+ if (fileSystem.exists(filePath)) {
290
+ const taskSlug = filename.replace(/\.md$/, "");
291
+ return { type, ideaSlug, taskSlug };
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ async function readIdeaTitle(fileSystem, dustPath, ideaSlug) {
297
+ const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
298
+ if (!fileSystem.exists(ideaPath)) {
299
+ throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
300
+ }
301
+ const ideaContent = await fileSystem.readFile(ideaPath);
302
+ const ideaTitleMatch = ideaContent.match(/^#\s+(.+)$/m);
303
+ if (!ideaTitleMatch) {
304
+ throw new Error(`Idea file has no title: ${ideaPath}`);
305
+ }
306
+ return ideaTitleMatch[1].trim();
307
+ }
308
+ function renderResolvedQuestions(responses) {
309
+ const sections = responses.map((r) => `### ${r.question}
310
+
311
+ **Decision:** ${r.chosenOption}`);
312
+ return `## Resolved Questions
313
+
314
+ ${sections.join(`
315
+
316
+ `)}
317
+ `;
318
+ }
319
+ function renderTask(title, openingSentence, definitionOfDone, options) {
320
+ const descriptionParagraph = options?.description !== undefined ? `
321
+ ${options.description}
322
+ ` : "";
323
+ const resolvedSection = options?.resolvedQuestions && options.resolvedQuestions.length > 0 ? `
324
+ ${renderResolvedQuestions(options.resolvedQuestions)}
325
+ ` : "";
326
+ return `# ${title}
327
+
328
+ ${openingSentence}
329
+ ${descriptionParagraph}${resolvedSection}
330
+ ## Principles
331
+
332
+ (none)
333
+
334
+ ## Blocked By
335
+
336
+ (none)
337
+
338
+ ## Definition of Done
339
+
340
+ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
341
+ `)}
342
+ `;
343
+ }
344
+ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, taskOptions) {
345
+ const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
346
+ const taskTitle = `${prefix}${ideaTitle}`;
347
+ const filename = titleToFilename(taskTitle);
348
+ const filePath = `${dustPath}/tasks/${filename}`;
349
+ const openingSentence = openingSentenceTemplate(ideaTitle);
350
+ const content = renderTask(taskTitle, openingSentence, definitionOfDone, taskOptions);
351
+ await fileSystem.writeFile(filePath, content);
352
+ return { filePath };
353
+ }
354
+ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
355
+ return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/principles/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
356
+ "Idea is thoroughly researched with relevant codebase context",
357
+ "Open questions are added for any ambiguous or underspecified aspects",
358
+ "Open questions follow the required heading format and focus on high-value decisions",
359
+ "Idea file is updated with findings"
360
+ ], { description });
361
+ }
362
+ async function decomposeIdea(fileSystem, dustPath, options) {
363
+ return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/principles/\` to link relevant principles and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
364
+ "One or more new tasks are created in .dust/tasks/",
365
+ "Task's Principles section links to relevant principles from .dust/principles/",
366
+ "The original idea is deleted or updated to reflect remaining scope"
367
+ ], {
368
+ description: options.description,
369
+ resolvedQuestions: options.openQuestionResponses
370
+ });
371
+ }
372
+ async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
373
+ return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], { description });
374
+ }
375
+ async function createCaptureIdeaTask(fileSystem, dustPath, options) {
376
+ const { title, description, buildItNow } = options;
377
+ if (!title || !title.trim()) {
378
+ throw new Error("title is required and must not be whitespace-only");
379
+ }
380
+ if (!description || !description.trim()) {
381
+ throw new Error("description is required and must not be whitespace-only");
382
+ }
383
+ if (buildItNow) {
384
+ const taskTitle2 = `${BUILD_IDEA_PREFIX}${title}`;
385
+ const filename2 = titleToFilename(taskTitle2);
386
+ const filePath2 = `${dustPath}/tasks/${filename2}`;
387
+ const content2 = `# ${taskTitle2}
388
+
389
+ Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
390
+
391
+ ## Idea Description
392
+
393
+ ${description}
394
+
395
+ ## Principles
396
+
397
+ (none)
398
+
399
+ ## Blocked By
400
+
401
+ (none)
402
+
403
+ ## Definition of Done
404
+
405
+ - [ ] One or more new tasks are created in \`.dust/tasks/\`
406
+ - [ ] Tasks link to relevant principles from \`.dust/principles/\`
407
+ - [ ] Tasks are narrowly scoped vertical slices
408
+ `;
409
+ await fileSystem.writeFile(filePath2, content2);
410
+ return { filePath: filePath2 };
411
+ }
412
+ const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
413
+ const filename = titleToFilename(taskTitle);
414
+ const filePath = `${dustPath}/tasks/${filename}`;
415
+ const ideaFilename = titleToFilename(title);
416
+ const ideaPath = `.dust/ideas/${ideaFilename}`;
417
+ const content = `# ${taskTitle}
418
+
419
+ Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context.
420
+
421
+ ## Idea Description
422
+
423
+ ${description}
424
+
425
+ ## Principles
426
+
427
+ (none)
428
+
429
+ ## Blocked By
430
+
431
+ (none)
432
+
433
+ ## Definition of Done
434
+
435
+ - [ ] Idea file exists at ${ideaPath}
436
+ - [ ] Idea file has an H1 title matching "${title}"
437
+ - [ ] Idea includes relevant context from codebase exploration
438
+ - [ ] Open questions are added for any ambiguous or underspecified aspects
439
+ - [ ] Open questions follow the required heading format and focus on high-value decisions
440
+ `;
441
+ await fileSystem.writeFile(filePath, content);
442
+ return { filePath };
443
+ }
444
+ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
445
+ const filePath = `${dustPath}/tasks/${taskSlug}.md`;
446
+ if (!fileSystem.exists(filePath)) {
447
+ return null;
448
+ }
449
+ const content = await fileSystem.readFile(filePath);
450
+ const titleMatch = content.match(/^#\s+(.+)$/m);
451
+ if (!titleMatch) {
452
+ return null;
453
+ }
454
+ const title = titleMatch[1].trim();
455
+ let ideaTitle;
456
+ let buildItNow;
457
+ if (title.startsWith(BUILD_IDEA_PREFIX)) {
458
+ ideaTitle = title.slice(BUILD_IDEA_PREFIX.length);
459
+ buildItNow = true;
460
+ } else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
461
+ ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
462
+ buildItNow = false;
463
+ } else {
464
+ return null;
465
+ }
466
+ const descriptionMatch = content.match(/^## Idea Description\n\n([\s\S]*?)\n\n## /m);
467
+ if (!descriptionMatch) {
468
+ return null;
469
+ }
470
+ const ideaDescription = descriptionMatch[1];
471
+ return { ideaTitle, ideaDescription, buildItNow };
472
+ }
473
+
474
+ // lib/artifacts/index.ts
475
+ function buildArtifactsRepository(fileSystem, dustPath) {
476
+ return {
477
+ async parseIdea(options) {
478
+ return parseIdea(fileSystem, dustPath, options.slug);
479
+ },
480
+ async listIdeas() {
481
+ const ideasPath = `${dustPath}/ideas`;
482
+ if (!fileSystem.exists(ideasPath)) {
483
+ return [];
484
+ }
485
+ const files = await fileSystem.readdir(ideasPath);
486
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
487
+ },
488
+ async parsePrinciple(options) {
489
+ return parsePrinciple(fileSystem, dustPath, options.slug);
490
+ },
491
+ async listPrinciples() {
492
+ const principlesPath = `${dustPath}/principles`;
493
+ if (!fileSystem.exists(principlesPath)) {
494
+ return [];
495
+ }
496
+ const files = await fileSystem.readdir(principlesPath);
497
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
498
+ },
499
+ async parseFact(options) {
500
+ return parseFact(fileSystem, dustPath, options.slug);
501
+ },
502
+ async listFacts() {
503
+ const factsPath = `${dustPath}/facts`;
504
+ if (!fileSystem.exists(factsPath)) {
505
+ return [];
506
+ }
507
+ const files = await fileSystem.readdir(factsPath);
508
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
509
+ },
510
+ async parseTask(options) {
511
+ return parseTask(fileSystem, dustPath, options.slug);
512
+ },
513
+ async listTasks() {
514
+ const tasksPath = `${dustPath}/tasks`;
515
+ if (!fileSystem.exists(tasksPath)) {
516
+ return [];
517
+ }
518
+ const files = await fileSystem.readdir(tasksPath);
519
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
520
+ },
521
+ async createRefineIdeaTask(options) {
522
+ return createRefineIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description);
523
+ },
524
+ async createDecomposeIdeaTask(options) {
525
+ return decomposeIdea(fileSystem, dustPath, options);
526
+ },
527
+ async createShelveIdeaTask(options) {
528
+ return createShelveIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description);
529
+ },
530
+ async createCaptureIdeaTask(options) {
531
+ return createCaptureIdeaTask(fileSystem, dustPath, options);
532
+ },
533
+ async findWorkflowTaskForIdea(options) {
534
+ return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
535
+ },
536
+ async parseCaptureIdeaTask(options) {
537
+ return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
538
+ }
539
+ };
540
+ }
541
+ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
542
+ return {
543
+ async parseIdea(options) {
544
+ return parseIdea(fileSystem, dustPath, options.slug);
545
+ },
546
+ async listIdeas() {
547
+ const ideasPath = `${dustPath}/ideas`;
548
+ if (!fileSystem.exists(ideasPath)) {
549
+ return [];
550
+ }
551
+ const files = await fileSystem.readdir(ideasPath);
552
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
553
+ },
554
+ async parsePrinciple(options) {
555
+ return parsePrinciple(fileSystem, dustPath, options.slug);
556
+ },
557
+ async listPrinciples() {
558
+ const principlesPath = `${dustPath}/principles`;
559
+ if (!fileSystem.exists(principlesPath)) {
560
+ return [];
561
+ }
562
+ const files = await fileSystem.readdir(principlesPath);
563
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
564
+ },
565
+ async parseFact(options) {
566
+ return parseFact(fileSystem, dustPath, options.slug);
567
+ },
568
+ async listFacts() {
569
+ const factsPath = `${dustPath}/facts`;
570
+ if (!fileSystem.exists(factsPath)) {
571
+ return [];
572
+ }
573
+ const files = await fileSystem.readdir(factsPath);
574
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
575
+ },
576
+ async parseTask(options) {
577
+ return parseTask(fileSystem, dustPath, options.slug);
578
+ },
579
+ async listTasks() {
580
+ const tasksPath = `${dustPath}/tasks`;
581
+ if (!fileSystem.exists(tasksPath)) {
582
+ return [];
583
+ }
584
+ const files = await fileSystem.readdir(tasksPath);
585
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
586
+ },
587
+ async findWorkflowTaskForIdea(options) {
588
+ return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
589
+ },
590
+ async parseCaptureIdeaTask(options) {
591
+ return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
592
+ }
593
+ };
594
+ }
595
+ export {
596
+ parseOpenQuestions,
597
+ buildReadOnlyArtifactsRepository,
598
+ buildArtifactsRepository
599
+ };
@@ -13,17 +13,20 @@ export interface CommandResult {
13
13
  export interface WriteOptions {
14
14
  flag?: 'w' | 'wx';
15
15
  }
16
- export interface FileSystem {
16
+ export interface ReadableFileSystem {
17
17
  exists: (path: string) => boolean;
18
18
  readFile: (path: string) => Promise<string>;
19
+ readdir: (path: string) => Promise<string[]>;
20
+ isDirectory: (path: string) => boolean;
21
+ }
22
+ export interface FileSystem extends ReadableFileSystem {
19
23
  writeFile: (path: string, content: string, options?: WriteOptions) => Promise<void>;
20
24
  mkdir: (path: string, options?: {
21
25
  recursive?: boolean;
22
26
  }) => Promise<void>;
23
- readdir: (path: string) => Promise<string[]>;
24
27
  chmod: (path: string, mode: number) => Promise<void>;
25
- isDirectory: (path: string) => boolean;
26
28
  getFileCreationTime: (path: string) => number;
29
+ rename: (oldPath: string, newPath: string) => Promise<void>;
27
30
  }
28
31
  export interface GlobScanner {
29
32
  scan: (dir: string) => AsyncIterable<string>;
@@ -41,6 +44,7 @@ export interface DustSettings {
41
44
  eventsUrl?: string;
42
45
  extraDirectories?: string[];
43
46
  }
47
+ export type DirectoryFileSorter = (dir: string, files: string[]) => Promise<string[]>;
44
48
  /**
45
49
  * Dependencies passed to all CLI commands
46
50
  */
@@ -50,4 +54,5 @@ export interface CommandDependencies {
50
54
  fileSystem: FileSystem;
51
55
  globScanner: GlobScanner;
52
56
  settings: DustSettings;
57
+ directoryFileSorter?: DirectoryFileSorter;
53
58
  }