@joshski/dust 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +107 -0
  2. package/bin/dust +3 -0
  3. package/dist/dust.js +499 -0
  4. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Dust
2
+
3
+ A lightweight planning system and work tracker optimised for humans working with AI agents.
4
+
5
+ [![CI](https://github.com/joshski/dust/actions/workflows/ci.yml/badge.svg)](https://github.com/joshski/dust/actions/workflows/ci.yml)
6
+
7
+ ## Usage
8
+
9
+ Document your system and any future plans in a [.dust](./.dust) directory in your repository:
10
+
11
+ ```
12
+ .dust/
13
+ ├── goals/ # Mission statements explaining why the project exists
14
+ ├── ideas/ # Brief notes about future tasks (intentionally vague)
15
+ ├── tasks/ # Detailed work plans with dependencies and definition of done
16
+ ├── facts/ # Current state: design, architecture, rules, invariants
17
+ └── hooks/ # Executable scripts for CLI integration (e.g., quality gates)
18
+ ```
19
+
20
+ The `goals`, `ideas`, `tasks`, and `facts` directories should be flat (no subdirectories) and contain only markdown files with slug-style names (alphanumeric and hyphens only).
21
+
22
+ The `hooks` directory contains executable scripts that integrate with the `dust` CLI. For example, the `check` hook is run by `dust check` to execute project-defined quality gates.
23
+
24
+ ## CLI Commands
25
+
26
+ The `dust` CLI provides commands for managing your planning repository:
27
+
28
+ | Command | Description |
29
+ |---------|-------------|
30
+ | `dust init` | Initialize a new Dust repository with the standard directory structure |
31
+ | `dust prompt <name>` | Output a prompt by name from the `prompts/` directory |
32
+ | `dust validate` | Run validation checks on `.dust/` files (links, task structure, naming) |
33
+ | `dust list [type]` | List items by type (tasks, ideas, goals, facts) or all if no type specified |
34
+ | `dust next` | Show tasks ready to work on (not blocked by other incomplete tasks) |
35
+ | `dust check` | Run `validate` then execute the project's quality gate hook at `.dust/hooks/check` |
36
+ | `dust help` | Show help message with all available commands |
37
+
38
+ ### Examples
39
+
40
+ ```bash
41
+ # Initialize a new project
42
+ dust init
43
+
44
+ # List all tasks
45
+ dust list tasks
46
+
47
+ # List everything (tasks, ideas, goals, facts)
48
+ dust list
49
+
50
+ # Show tasks that are ready to start
51
+ dust next
52
+
53
+ # Validate all markdown files under ./.dust
54
+ dust validate
55
+
56
+ # Run quality checks (validation + custom hook)
57
+ dust check
58
+
59
+ # Output a prompt by name
60
+ dust prompt work
61
+ ```
62
+
63
+ ## Workflow
64
+
65
+ Dust is designed for successive cycles of human planning (AI-assisted, of course) followed by agent autonomy, followed by human planning, and so on.
66
+
67
+ In order for work to begin, there must be a task. A worker (an AI agent or human) chooses any task to work on. In a team environment, the worker must “claim” the task i.e. let the team know they are working on it. The team can use a version control system (like git) claim the task by making a branch with the same name as the task. If any attempt to claim fails (e.g. a branch with that name already exists) then the agent must choose an alternative task.
68
+
69
+ When the worker completes their task, they make a single commit that includes the work, but also deletes the task, and removes any references to the task. The commit should often update one or more facts as well.
70
+
71
+ Tasks should be small units of work that can be completed quickly and result in a single commit that leaves the system in a reasonable state (e.g. no broken or half-implemented features exposed to end users). When in doubt, workers are encouraged to split the task into smaller sub-tasks, and abandon the attempt to finish any ambitious work in one go.
72
+
73
+ Over time, new ideas emerge, and ideas become more detailed plans in the form of "tasks". This should be deferred until the last responsible moment. Since humans like control over plans, ideas become plans in the "human-in-the-loop" phase at the start of a sprint.
74
+
75
+ ## Tasks
76
+
77
+ Tasks are the only markdown files that have a strict structure. Tasks must have each of the following subheadings:
78
+
79
+ ```
80
+ ## Goals
81
+ ## Blocked by
82
+ ## Definition of done
83
+ ```
84
+
85
+ * Goals - a list of relative links to other markdown files, always under ./.dust/goals
86
+ * Blocked by - a list relative links to other markdown files, each of which nominates a task that must be implemented before this task can be started.
87
+ * Definition of done - A short description of how the implementor of the task can decide when the task has been completed successfully
88
+
89
+ These special headings and sections are required, but the remainder of the document is free form.
90
+
91
+ ## The single commit
92
+
93
+ Each task should be a small unit of work. If it was underestimated, the agent implementing the task should commit any progress that does not have a negative impact on end users, and create another "follow up" task to complete the remainder of the work.
94
+
95
+ ## Links between documents
96
+
97
+ Documents should include links to all relevant related documents, regardless of the type. These should be relative links in markdown format. The link text should typically match the title of the target document.
98
+
99
+ ## Change history
100
+
101
+ Commits delete tasks, but commit history can be traversed to retrieve the thinking behind any changes. Tools can be implemented to make this easier or build indexes. The current working copy is kept intentionally free of this detail, to keep commits clean and reduce noise in the current repository state.
102
+
103
+ ## Hygiene
104
+
105
+ A linter can be used for static analysis of task files, and to ensure there are no broken relative links as the result of any changes.
106
+
107
+ Regular semantic and logic checks are expected to be carried out to ensure ideas have not drifted from reality. This would typically happen after one or more commits, e.g. at the end of a sprint.
package/bin/dust ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import "../lib/cli/entry";
package/dist/dust.js ADDED
@@ -0,0 +1,499 @@
1
+ // lib/cli/entry.ts
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
4
+
5
+ // lib/cli/check.ts
6
+ import { spawn } from "node:child_process";
7
+
8
+ // lib/cli/validate.ts
9
+ import { dirname, resolve } from "node:path";
10
+ var REQUIRED_HEADINGS = ["## Goals", "## Blocked by", "## Definition of done"];
11
+ var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
12
+ function validateFilename(filePath) {
13
+ const parts = filePath.split("/");
14
+ const filename = parts[parts.length - 1];
15
+ if (!SLUG_PATTERN.test(filename)) {
16
+ return {
17
+ file: filePath,
18
+ message: `Filename "${filename}" does not match slug-style naming`
19
+ };
20
+ }
21
+ return null;
22
+ }
23
+ function validateTaskHeadings(filePath, content) {
24
+ const violations = [];
25
+ for (const heading of REQUIRED_HEADINGS) {
26
+ if (!content.includes(heading)) {
27
+ violations.push({
28
+ file: filePath,
29
+ message: `Missing required heading: "${heading}"`
30
+ });
31
+ }
32
+ }
33
+ return violations;
34
+ }
35
+ function validateLinks(filePath, content, fs) {
36
+ const violations = [];
37
+ const lines = content.split(`
38
+ `);
39
+ const fileDir = dirname(filePath);
40
+ for (let i = 0;i < lines.length; i++) {
41
+ const line = lines[i];
42
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
43
+ let match = linkPattern.exec(line);
44
+ while (match) {
45
+ const linkTarget = match[2];
46
+ if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
47
+ const targetPath = linkTarget.split("#")[0];
48
+ const resolvedPath = resolve(fileDir, targetPath);
49
+ if (!fs.exists(resolvedPath)) {
50
+ violations.push({
51
+ file: filePath,
52
+ message: `Broken link: "${linkTarget}"`,
53
+ line: i + 1
54
+ });
55
+ }
56
+ }
57
+ match = linkPattern.exec(line);
58
+ }
59
+ }
60
+ return violations;
61
+ }
62
+ async function validate(ctx, fs, _args, glob) {
63
+ const dustPath = `${ctx.cwd}/.dust`;
64
+ if (!fs.exists(dustPath)) {
65
+ ctx.stderr("Error: .dust directory not found");
66
+ ctx.stderr("Run 'dust init' to initialize a Dust repository");
67
+ return { exitCode: 1 };
68
+ }
69
+ const violations = [];
70
+ ctx.stdout("Validating links in .dust/...");
71
+ for await (const file of glob.scan(dustPath)) {
72
+ if (!file.endsWith(".md"))
73
+ continue;
74
+ const filePath = `${dustPath}/${file}`;
75
+ const content = await fs.readFile(filePath);
76
+ violations.push(...validateLinks(filePath, content, fs));
77
+ }
78
+ const tasksPath = `${dustPath}/tasks`;
79
+ if (fs.exists(tasksPath)) {
80
+ ctx.stdout("Validating task files in .dust/tasks/...");
81
+ for await (const file of glob.scan(tasksPath)) {
82
+ if (!file.endsWith(".md"))
83
+ continue;
84
+ const filePath = `${tasksPath}/${file}`;
85
+ const content = await fs.readFile(filePath);
86
+ const filenameViolation = validateFilename(filePath);
87
+ if (filenameViolation) {
88
+ violations.push(filenameViolation);
89
+ }
90
+ violations.push(...validateTaskHeadings(filePath, content));
91
+ }
92
+ }
93
+ if (violations.length === 0) {
94
+ ctx.stdout("All validations passed!");
95
+ return { exitCode: 0 };
96
+ }
97
+ ctx.stderr(`Found ${violations.length} violation(s):`);
98
+ ctx.stderr("");
99
+ for (const v of violations) {
100
+ const location = v.line ? `:${v.line}` : "";
101
+ ctx.stderr(` ${v.file}${location}`);
102
+ ctx.stderr(` ${v.message}`);
103
+ }
104
+ return { exitCode: 1 };
105
+ }
106
+
107
+ // lib/cli/check.ts
108
+ function createProcessRunner(spawnFn) {
109
+ return {
110
+ spawn: (command, args, options) => {
111
+ return new Promise((resolve2) => {
112
+ const proc = spawnFn(command, args, options);
113
+ proc.on("close", (code) => resolve2(code ?? 1));
114
+ proc.on("error", () => resolve2(1));
115
+ });
116
+ }
117
+ };
118
+ }
119
+ var defaultProcessRunner = createProcessRunner(spawn);
120
+ async function check(ctx, fs, _args, runner = defaultProcessRunner, glob) {
121
+ if (glob) {
122
+ const validationResult = await validate(ctx, fs, [], glob);
123
+ if (validationResult.exitCode !== 0) {
124
+ return validationResult;
125
+ }
126
+ ctx.stdout("");
127
+ }
128
+ const hookPath = `${ctx.cwd}/.dust/hooks/check`;
129
+ if (!fs.exists(hookPath)) {
130
+ ctx.stderr("Error: No check hook found at .dust/hooks/check");
131
+ ctx.stderr("");
132
+ ctx.stderr("To create a check hook:");
133
+ ctx.stderr(" 1. Create the hooks directory: mkdir -p .dust/hooks");
134
+ ctx.stderr(" 2. Create the check script: touch .dust/hooks/check");
135
+ ctx.stderr(" 3. Make it executable: chmod +x .dust/hooks/check");
136
+ ctx.stderr(" 4. Add your quality checks (tests, linting, etc.)");
137
+ return { exitCode: 1 };
138
+ }
139
+ const exitCode = await runner.spawn(hookPath, [], {
140
+ cwd: ctx.cwd,
141
+ stdio: "inherit"
142
+ });
143
+ return { exitCode };
144
+ }
145
+
146
+ // lib/cli/init.ts
147
+ var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts"];
148
+ var DEFAULT_GOAL = `# Project Goal
149
+
150
+ Describe the high-level mission of this project.
151
+ `;
152
+ async function init(ctx, fs, _args) {
153
+ const dustPath = `${ctx.cwd}/.dust`;
154
+ if (fs.exists(dustPath)) {
155
+ ctx.stderr("Error: .dust directory already exists");
156
+ return { exitCode: 1 };
157
+ }
158
+ await fs.mkdir(dustPath, { recursive: true });
159
+ for (const dir of DUST_DIRECTORIES) {
160
+ await fs.mkdir(`${dustPath}/${dir}`, { recursive: true });
161
+ }
162
+ await fs.writeFile(`${dustPath}/goals/project-goal.md`, DEFAULT_GOAL);
163
+ ctx.stdout("Initialized Dust repository in .dust/");
164
+ ctx.stdout(`Created directories: ${DUST_DIRECTORIES.join(", ")}`);
165
+ ctx.stdout("Created initial goal: .dust/goals/project-goal.md");
166
+ return { exitCode: 0 };
167
+ }
168
+
169
+ // lib/cli/list.ts
170
+ var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
171
+ function extractTitle(content) {
172
+ const match = content.match(/^#\s+(.+)$/m);
173
+ return match ? match[1].trim() : null;
174
+ }
175
+ async function list(ctx, fs, args) {
176
+ const dustPath = `${ctx.cwd}/.dust`;
177
+ if (!fs.exists(dustPath)) {
178
+ ctx.stderr("Error: .dust directory not found");
179
+ ctx.stderr("Run 'dust init' to initialize a Dust repository");
180
+ return { exitCode: 1 };
181
+ }
182
+ const typesToList = args.length === 0 ? [...VALID_TYPES] : args.filter((a) => VALID_TYPES.includes(a));
183
+ if (args.length > 0 && typesToList.length === 0) {
184
+ ctx.stderr(`Invalid type: ${args[0]}`);
185
+ ctx.stderr(`Valid types: ${VALID_TYPES.join(", ")}`);
186
+ return { exitCode: 1 };
187
+ }
188
+ for (const type of typesToList) {
189
+ const dirPath = `${dustPath}/${type}`;
190
+ if (!fs.exists(dirPath)) {
191
+ continue;
192
+ }
193
+ const files = await fs.readdir(dirPath);
194
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
195
+ if (mdFiles.length === 0) {
196
+ continue;
197
+ }
198
+ ctx.stdout(`${type}:`);
199
+ for (const file of mdFiles) {
200
+ const filePath = `${dirPath}/${file}`;
201
+ const content = await fs.readFile(filePath);
202
+ const title = extractTitle(content);
203
+ const name = file.replace(/\.md$/, "");
204
+ if (title) {
205
+ ctx.stdout(` ${name} - ${title}`);
206
+ } else {
207
+ ctx.stdout(` ${name}`);
208
+ }
209
+ }
210
+ ctx.stdout("");
211
+ }
212
+ return { exitCode: 0 };
213
+ }
214
+
215
+ // lib/cli/next.ts
216
+ function extractTitle2(content) {
217
+ const match = content.match(/^#\s+(.+)$/m);
218
+ return match ? match[1].trim() : null;
219
+ }
220
+ function extractBlockedBy(content) {
221
+ const blockedByMatch = content.match(/^## Blocked by\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
222
+ if (!blockedByMatch) {
223
+ return [];
224
+ }
225
+ const section = blockedByMatch[1].trim();
226
+ if (section === "(none)") {
227
+ return [];
228
+ }
229
+ const linkPattern = /\[.*?\]\(([^)]+\.md)\)/g;
230
+ const blockers = [];
231
+ let match = linkPattern.exec(section);
232
+ while (match !== null) {
233
+ blockers.push(match[1]);
234
+ match = linkPattern.exec(section);
235
+ }
236
+ return blockers;
237
+ }
238
+ async function next(ctx, fs, _args) {
239
+ const dustPath = `${ctx.cwd}/.dust`;
240
+ if (!fs.exists(dustPath)) {
241
+ ctx.stderr("Error: .dust directory not found");
242
+ ctx.stderr("Run 'dust init' to initialize a Dust repository");
243
+ return { exitCode: 1 };
244
+ }
245
+ const tasksPath = `${dustPath}/tasks`;
246
+ if (!fs.exists(tasksPath)) {
247
+ return { exitCode: 0 };
248
+ }
249
+ const files = await fs.readdir(tasksPath);
250
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
251
+ if (mdFiles.length === 0) {
252
+ return { exitCode: 0 };
253
+ }
254
+ const existingTasks = new Set(mdFiles);
255
+ const unblockedTasks = [];
256
+ for (const file of mdFiles) {
257
+ const filePath = `${tasksPath}/${file}`;
258
+ const content = await fs.readFile(filePath);
259
+ const blockers = extractBlockedBy(content);
260
+ const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
261
+ if (!hasIncompleteBlocker) {
262
+ const title = extractTitle2(content);
263
+ const name = file.replace(/\.md$/, "");
264
+ unblockedTasks.push({ name, title });
265
+ }
266
+ }
267
+ if (unblockedTasks.length === 0) {
268
+ return { exitCode: 0 };
269
+ }
270
+ ctx.stdout("Next tasks:");
271
+ for (const task of unblockedTasks) {
272
+ if (task.title) {
273
+ ctx.stdout(` ${task.name} - ${task.title}`);
274
+ } else {
275
+ ctx.stdout(` ${task.name}`);
276
+ }
277
+ }
278
+ return { exitCode: 0 };
279
+ }
280
+
281
+ // lib/cli/prompt.ts
282
+ async function prompt(ctx, fs, args) {
283
+ const promptsDir = `${ctx.cwd}/prompts`;
284
+ if (args.length === 0) {
285
+ ctx.stderr("Usage: dust prompt <name>");
286
+ ctx.stderr("Example: dust prompt work");
287
+ return { exitCode: 1 };
288
+ }
289
+ const promptName = args[0];
290
+ const promptFile = `${promptsDir}/${promptName}.md`;
291
+ if (!fs.exists(promptFile)) {
292
+ ctx.stderr(`Error: Prompt '${promptName}' not found`);
293
+ ctx.stderr("Available prompts:");
294
+ try {
295
+ const files = await fs.readdir(promptsDir);
296
+ const prompts = files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
297
+ for (const p of prompts) {
298
+ ctx.stderr(` ${p}`);
299
+ }
300
+ } catch {
301
+ ctx.stderr(" (no prompts directory found)");
302
+ }
303
+ return { exitCode: 1 };
304
+ }
305
+ const content = await fs.readFile(promptFile);
306
+ ctx.stdout(content);
307
+ return { exitCode: 0 };
308
+ }
309
+
310
+ // lib/cli/settings.ts
311
+ import { join } from "node:path";
312
+ var DEFAULT_SETTINGS = {
313
+ binaryPath: "dust"
314
+ };
315
+ async function loadSettings(cwd, fs) {
316
+ const settingsPath = join(cwd, ".dust", "config", "settings.json");
317
+ if (!fs.exists(settingsPath)) {
318
+ return DEFAULT_SETTINGS;
319
+ }
320
+ try {
321
+ const content = await fs.readFile(settingsPath);
322
+ const parsed = JSON.parse(content);
323
+ return {
324
+ ...DEFAULT_SETTINGS,
325
+ ...parsed
326
+ };
327
+ } catch {
328
+ return DEFAULT_SETTINGS;
329
+ }
330
+ }
331
+
332
+ // lib/cli/main.ts
333
+ var COMMANDS = [
334
+ "init",
335
+ "prompt",
336
+ "validate",
337
+ "list",
338
+ "next",
339
+ "check",
340
+ "help"
341
+ ];
342
+ function generateHelpText(settings) {
343
+ const bin = settings.binaryPath;
344
+ return `dust - A lightweight planning system for human-AI collaboration
345
+
346
+ Usage: ${bin} <command> [options]
347
+
348
+ Commands:
349
+ init Initialize a new Dust repository
350
+ prompt <name> Output a prompt by name (e.g., ${bin} prompt work)
351
+ validate Run validation checks on .dust/ files
352
+ list [type] List items (tasks, ideas, goals, facts)
353
+ next Show tasks ready to work on (not blocked)
354
+ check Run project-defined quality gate hook
355
+ help Show this help message
356
+
357
+ Examples:
358
+ ${bin} init
359
+ ${bin} prompt work
360
+ ${bin} validate
361
+ ${bin} list tasks
362
+ ${bin} list
363
+ ${bin} next
364
+ ${bin} check
365
+
366
+ ---
367
+
368
+ ## Agent Guide
369
+
370
+ This section provides comprehensive guidance for AI agents working with dust.
371
+
372
+ ### Directory Structure
373
+
374
+ The \`.dust/\` directory contains all planning artifacts:
375
+
376
+ - **\`.dust/goals/\`** - Mission statements and guiding principles
377
+ - **\`.dust/ideas/\`** - Future feature notes and proposals (intentionally vague)
378
+ - **\`.dust/tasks/\`** - Detailed work plans with dependencies and definitions of done
379
+ - **\`.dust/facts/\`** - Documentation of current system state and architecture
380
+ - **\`.dust/hooks/\`** - Executable scripts for quality gates (e.g., \`check\` hook)
381
+
382
+ All files are markdown with slug-style names (lowercase, hyphens, no spaces).
383
+
384
+ ### Working on Tasks
385
+
386
+ **Run \`${bin} check\` before starting work** to verify the project is in a good state before making changes.
387
+
388
+ Run \`${bin} next\` to find tasks ready to work on. Each task file contains:
389
+
390
+ - \`## Goals\` - Links to goals this task supports
391
+ - \`## Blocked by\` - Tasks that must complete first (empty or "(none)" means ready)
392
+ - \`## Definition of done\` - Criteria for completion
393
+
394
+ A task is **unblocked** when its "Blocked by" section is empty, says "(none)", or all referenced task files have been deleted.
395
+
396
+ ### Completing a Task
397
+
398
+ **Run \`${bin} check\` before committing** to ensure all quality gates pass.
399
+
400
+ When finishing a task, create a single atomic commit that includes:
401
+
402
+ 1. All implementation changes
403
+ 2. Deletion of the completed task file
404
+ 3. Updates to any facts that changed
405
+ 4. Deletion of any ideas that were fully realized
406
+ 5. Updates to any tasks that referenced this one in their "Blocked by" sections
407
+
408
+ ### Common Workflows
409
+
410
+ - **"Work on the next task"** - Run \`${bin} next\`, pick a task, implement it
411
+ - **"Work on task X"** - Implement \`.dust/tasks/X.md\` directly
412
+ - **"Convert idea Y to tasks"** - Break down \`.dust/ideas/Y.md\` into tasks
413
+ - **"Validate facts"** - Check \`.dust/facts/\` for accuracy against the codebase
414
+
415
+ ### Configuring Agent Files
416
+
417
+ Projects using dust should add a minimal pointer to their agent configuration files (CLAUDE.md, AGENTS.md, etc.):
418
+
419
+ \`\`\`markdown
420
+ This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
421
+ Always run \`dust help\` when you start working in this repository.
422
+ \`\`\`
423
+
424
+ This approach keeps agent instructions minimal, ensures agents get current documentation, and reduces maintenance burden.
425
+ `;
426
+ }
427
+ var HELP_TEXT = generateHelpText({ binaryPath: "dust" });
428
+ function isHelpRequest(command) {
429
+ return !command || command === "help" || command === "--help" || command === "-h";
430
+ }
431
+ function isValidCommand(command) {
432
+ return COMMANDS.includes(command);
433
+ }
434
+ async function runCommand(command, commandArgs, ctx, fs, glob, settings) {
435
+ switch (command) {
436
+ case "init":
437
+ return init(ctx, fs, commandArgs);
438
+ case "prompt":
439
+ return prompt(ctx, fs, commandArgs);
440
+ case "validate":
441
+ return validate(ctx, fs, commandArgs, glob);
442
+ case "list":
443
+ return list(ctx, fs, commandArgs);
444
+ case "next":
445
+ return next(ctx, fs, commandArgs);
446
+ case "check":
447
+ return check(ctx, fs, commandArgs, defaultProcessRunner, glob);
448
+ case "help":
449
+ ctx.stdout(generateHelpText(settings));
450
+ return { exitCode: 0 };
451
+ }
452
+ }
453
+ async function main(options) {
454
+ const { args, ctx, fs, glob } = options;
455
+ const command = args[0];
456
+ const commandArgs = args.slice(1);
457
+ const settings = await loadSettings(ctx.cwd, fs);
458
+ const helpText = generateHelpText(settings);
459
+ if (isHelpRequest(command)) {
460
+ ctx.stdout(helpText);
461
+ return { exitCode: 0 };
462
+ }
463
+ if (!isValidCommand(command)) {
464
+ ctx.stderr(`Unknown command: ${command}`);
465
+ ctx.stderr(`Run '${settings.binaryPath} help' for available commands`);
466
+ return { exitCode: 1 };
467
+ }
468
+ return runCommand(command, commandArgs, ctx, fs, glob, settings);
469
+ }
470
+
471
+ // lib/cli/entry.ts
472
+ var fs = {
473
+ exists: existsSync,
474
+ readFile: (path) => readFile(path, "utf-8"),
475
+ writeFile: (path, content) => writeFile(path, content, "utf-8"),
476
+ mkdir: async (path, options) => {
477
+ await mkdir(path, options);
478
+ },
479
+ readdir: (path) => readdir(path)
480
+ };
481
+ var glob = {
482
+ scan: async function* (dir) {
483
+ for (const entry of await readdir(dir, { recursive: true })) {
484
+ if (entry.endsWith(".md"))
485
+ yield entry;
486
+ }
487
+ }
488
+ };
489
+ var result = await main({
490
+ args: process.argv.slice(2),
491
+ ctx: {
492
+ cwd: process.cwd(),
493
+ stdout: console.log,
494
+ stderr: console.error
495
+ },
496
+ fs,
497
+ glob
498
+ });
499
+ process.exit(result.exitCode);
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@joshski/dust",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight planning system for human-AI collaboration",
5
+ "type": "module",
6
+ "bin": {
7
+ "dust": "./dist/dust.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/joshski/dust.git"
16
+ },
17
+ "keywords": [
18
+ "ai",
19
+ "planning",
20
+ "collaboration",
21
+ "cli"
22
+ ],
23
+ "author": "joshski",
24
+ "license": "MIT",
25
+ "scripts": {
26
+ "build": "bun build lib/cli/entry.ts --target node --outfile dist/dust.js",
27
+ "test": "vitest run",
28
+ "test:coverage": "vitest run --coverage"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.3.13",
32
+ "@types/bun": "^1.3.6",
33
+ "@vitest/coverage-v8": "^4.0.18",
34
+ "vitest": "^4.0.18"
35
+ }
36
+ }