@joshski/dust 0.1.19 → 0.1.20

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/dist/dust.js CHANGED
@@ -162,23 +162,21 @@ function createHooksManager(cwd, fileSystem, settings) {
162
162
  return {
163
163
  isGitRepo: () => fileSystem.exists(gitDir),
164
164
  isHookInstalled: async () => {
165
- if (!fileSystem.exists(prePushPath)) {
166
- return false;
167
- }
168
165
  try {
169
166
  const content = await fileSystem.readFile(prePushPath);
170
167
  return content.includes(DUST_HOOK_START);
171
- } catch {
172
- return false;
168
+ } catch (error) {
169
+ if (error.code === "ENOENT") {
170
+ return false;
171
+ }
172
+ throw error;
173
173
  }
174
174
  },
175
175
  installHook: async () => {
176
- if (!fileSystem.exists(hooksDir)) {
177
- await fileSystem.mkdir(hooksDir, { recursive: true });
178
- }
176
+ await fileSystem.mkdir(hooksDir, { recursive: true });
179
177
  const hookContent = generateHookContent(settings.dustCommand);
180
178
  let finalContent;
181
- if (fileSystem.exists(prePushPath)) {
179
+ try {
182
180
  const existingContent = await fileSystem.readFile(prePushPath);
183
181
  if (existingContent.includes(DUST_HOOK_START)) {
184
182
  const withoutDust = removeDustSection(existingContent);
@@ -195,19 +193,20 @@ ${hookContent}
195
193
  ${hookContent}
196
194
  `;
197
195
  }
198
- } else {
199
- finalContent = `#!/bin/sh
196
+ } catch (error) {
197
+ if (error.code === "ENOENT") {
198
+ finalContent = `#!/bin/sh
200
199
 
201
200
  ${hookContent}
202
201
  `;
202
+ } else {
203
+ throw error;
204
+ }
203
205
  }
204
206
  await fileSystem.writeFile(prePushPath, finalContent);
205
207
  await fileSystem.chmod(prePushPath, 493);
206
208
  },
207
209
  getHookBinaryPath: async () => {
208
- if (!fileSystem.exists(prePushPath)) {
209
- return null;
210
- }
211
210
  try {
212
211
  const content = await fileSystem.readFile(prePushPath);
213
212
  const dustSection = extractDustSection(content);
@@ -216,15 +215,23 @@ ${hookContent}
216
215
  }
217
216
  const match = dustSection.match(/^(.+) pre push$/m);
218
217
  return match ? match[1] : null;
219
- } catch {
220
- return null;
218
+ } catch (error) {
219
+ if (error.code === "ENOENT") {
220
+ return null;
221
+ }
222
+ throw error;
221
223
  }
222
224
  },
223
225
  updateHookBinaryPath: async (newPath) => {
224
- if (!fileSystem.exists(prePushPath)) {
225
- return;
226
+ let content;
227
+ try {
228
+ content = await fileSystem.readFile(prePushPath);
229
+ } catch (error) {
230
+ if (error.code === "ENOENT") {
231
+ return;
232
+ }
233
+ throw error;
226
234
  }
227
- const content = await fileSystem.readFile(prePushPath);
228
235
  const dustSection = extractDustSection(content);
229
236
  if (!dustSection) {
230
237
  return;
@@ -657,34 +664,63 @@ function validateNoCycles(allGoalRelationships) {
657
664
  }
658
665
  return violations;
659
666
  }
667
+ async function safeScanDir(glob, dirPath) {
668
+ const files = [];
669
+ try {
670
+ for await (const file of glob.scan(dirPath)) {
671
+ files.push(file);
672
+ }
673
+ return { files, exists: true };
674
+ } catch (error) {
675
+ if (error.code === "ENOENT") {
676
+ return { files: [], exists: false };
677
+ }
678
+ throw error;
679
+ }
680
+ }
660
681
  async function lintMarkdown(dependencies) {
661
682
  const { context, fileSystem, globScanner: glob } = dependencies;
662
683
  const dustPath = `${context.cwd}/.dust`;
663
- if (!fileSystem.exists(dustPath)) {
684
+ const dustScan = await safeScanDir(glob, dustPath);
685
+ if (!dustScan.exists) {
664
686
  context.stderr("Error: .dust directory not found");
665
687
  context.stderr("Run 'dust init' to initialize a Dust repository");
666
688
  return { exitCode: 1 };
667
689
  }
690
+ const dustFiles = dustScan.files;
668
691
  const violations = [];
669
692
  context.stdout("Validating links in .dust/...");
670
- for await (const file of glob.scan(dustPath)) {
693
+ for (const file of dustFiles) {
671
694
  if (!file.endsWith(".md"))
672
695
  continue;
673
696
  const filePath = `${dustPath}/${file}`;
674
- const content = await fileSystem.readFile(filePath);
675
- violations.push(...validateLinks(filePath, content, fileSystem));
697
+ try {
698
+ const content = await fileSystem.readFile(filePath);
699
+ violations.push(...validateLinks(filePath, content, fileSystem));
700
+ } catch (error) {
701
+ if (error.code !== "ENOENT") {
702
+ throw error;
703
+ }
704
+ }
676
705
  }
677
706
  const contentDirs = ["goals", "facts", "ideas", "tasks"];
678
707
  context.stdout("Validating content files...");
679
708
  for (const dir of contentDirs) {
680
709
  const dirPath = `${dustPath}/${dir}`;
681
- if (!fileSystem.exists(dirPath))
682
- continue;
683
- for await (const file of glob.scan(dirPath)) {
710
+ const { files } = await safeScanDir(glob, dirPath);
711
+ for (const file of files) {
684
712
  if (!file.endsWith(".md"))
685
713
  continue;
686
714
  const filePath = `${dirPath}/${file}`;
687
- const content = await fileSystem.readFile(filePath);
715
+ let content;
716
+ try {
717
+ content = await fileSystem.readFile(filePath);
718
+ } catch (error) {
719
+ if (error.code === "ENOENT") {
720
+ continue;
721
+ }
722
+ throw error;
723
+ }
688
724
  const openingSentenceViolation = validateOpeningSentence(filePath, content);
689
725
  if (openingSentenceViolation) {
690
726
  violations.push(openingSentenceViolation);
@@ -700,13 +736,22 @@ async function lintMarkdown(dependencies) {
700
736
  }
701
737
  }
702
738
  const tasksPath = `${dustPath}/tasks`;
703
- if (fileSystem.exists(tasksPath)) {
739
+ const { files: taskFiles } = await safeScanDir(glob, tasksPath);
740
+ if (taskFiles.length > 0) {
704
741
  context.stdout("Validating task files in .dust/tasks/...");
705
- for await (const file of glob.scan(tasksPath)) {
742
+ for (const file of taskFiles) {
706
743
  if (!file.endsWith(".md"))
707
744
  continue;
708
745
  const filePath = `${tasksPath}/${file}`;
709
- const content = await fileSystem.readFile(filePath);
746
+ let content;
747
+ try {
748
+ content = await fileSystem.readFile(filePath);
749
+ } catch (error) {
750
+ if (error.code === "ENOENT") {
751
+ continue;
752
+ }
753
+ throw error;
754
+ }
710
755
  const filenameViolation = validateFilename(filePath);
711
756
  if (filenameViolation) {
712
757
  violations.push(filenameViolation);
@@ -716,14 +761,23 @@ async function lintMarkdown(dependencies) {
716
761
  }
717
762
  }
718
763
  const goalsPath = `${dustPath}/goals`;
719
- if (fileSystem.exists(goalsPath)) {
764
+ const { files: goalFiles } = await safeScanDir(glob, goalsPath);
765
+ if (goalFiles.length > 0) {
720
766
  context.stdout("Validating goal hierarchy in .dust/goals/...");
721
767
  const allGoalRelationships = [];
722
- for await (const file of glob.scan(goalsPath)) {
768
+ for (const file of goalFiles) {
723
769
  if (!file.endsWith(".md"))
724
770
  continue;
725
771
  const filePath = `${goalsPath}/${file}`;
726
- const content = await fileSystem.readFile(filePath);
772
+ let content;
773
+ try {
774
+ content = await fileSystem.readFile(filePath);
775
+ } catch (error) {
776
+ if (error.code === "ENOENT") {
777
+ continue;
778
+ }
779
+ throw error;
780
+ }
727
781
  violations.push(...validateGoalHierarchySections(filePath, content));
728
782
  violations.push(...validateGoalHierarchyLinks(filePath, content));
729
783
  allGoalRelationships.push(extractGoalRelationships(filePath, content));
@@ -934,37 +988,59 @@ async function init(dependencies) {
934
988
  const dustPath = `${context.cwd}/.dust`;
935
989
  const dustCommand = detectDustCommand(context.cwd, fileSystem);
936
990
  const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
937
- if (fileSystem.exists(dustPath)) {
938
- context.stdout(`${colors.yellow}\uD83D\uDCE6 Note:${colors.reset} ${colors.cyan}.dust${colors.reset} directory already exists, skipping creation`);
939
- } else {
940
- await fileSystem.mkdir(dustPath, { recursive: true });
941
- for (const dir of DUST_DIRECTORIES) {
942
- await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
991
+ await fileSystem.mkdir(dustPath, { recursive: true });
992
+ for (const dir of DUST_DIRECTORIES) {
993
+ await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
994
+ }
995
+ let dustDirCreated = false;
996
+ try {
997
+ await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT, { flag: "wx" });
998
+ dustDirCreated = true;
999
+ } catch (error) {
1000
+ if (error.code !== "EEXIST") {
1001
+ throw error;
943
1002
  }
944
- await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT);
1003
+ }
1004
+ try {
945
1005
  const settings = generateSettings(context.cwd, fileSystem);
946
1006
  await fileSystem.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
947
- `);
1007
+ `, { flag: "wx" });
1008
+ } catch (error) {
1009
+ if (error.code !== "EEXIST") {
1010
+ throw error;
1011
+ }
1012
+ }
1013
+ if (dustDirCreated) {
948
1014
  context.stdout(`${colors.green}✨ Initialized${colors.reset} Dust repository in ${colors.cyan}.dust/${colors.reset}`);
949
1015
  context.stdout(`${colors.green}\uD83D\uDCC1 Created directories:${colors.reset} ${colors.dim}${DUST_DIRECTORIES.join(", ")}${colors.reset}`);
950
1016
  context.stdout(`${colors.green}\uD83D\uDCC4 Created initial fact:${colors.reset} ${colors.cyan}.dust/facts/use-dust-for-planning.md${colors.reset}`);
951
1017
  context.stdout(`${colors.green}⚙️ Created settings:${colors.reset} ${colors.cyan}.dust/config/settings.json${colors.reset}`);
1018
+ } else {
1019
+ context.stdout(`${colors.yellow}\uD83D\uDCE6 Note:${colors.reset} ${colors.cyan}.dust${colors.reset} directory already exists, skipping creation`);
952
1020
  }
953
1021
  const claudeMdPath = `${context.cwd}/CLAUDE.md`;
954
- if (fileSystem.exists(claudeMdPath)) {
955
- context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
956
- } else {
1022
+ try {
957
1023
  const claudeContent = loadTemplate("claude-md", { dustCommand });
958
- await fileSystem.writeFile(claudeMdPath, claudeContent);
1024
+ await fileSystem.writeFile(claudeMdPath, claudeContent, { flag: "wx" });
959
1025
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
1026
+ } catch (error) {
1027
+ if (error.code === "EEXIST") {
1028
+ context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
1029
+ } else {
1030
+ throw error;
1031
+ }
960
1032
  }
961
1033
  const agentsMdPath = `${context.cwd}/AGENTS.md`;
962
- if (fileSystem.exists(agentsMdPath)) {
963
- context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
964
- } else {
1034
+ try {
965
1035
  const agentsContent = loadTemplate("agents-md", { dustCommand });
966
- await fileSystem.writeFile(agentsMdPath, agentsContent);
1036
+ await fileSystem.writeFile(agentsMdPath, agentsContent, { flag: "wx" });
967
1037
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
1038
+ } catch (error) {
1039
+ if (error.code === "EEXIST") {
1040
+ context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
1041
+ } else {
1042
+ throw error;
1043
+ }
968
1044
  }
969
1045
  const runner = dustCommand.split(" ")[0];
970
1046
  context.stdout("");
@@ -1608,13 +1684,14 @@ function formatEvent(event) {
1608
1684
  case "loop.started":
1609
1685
  return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
1610
1686
  case "loop.syncing":
1611
- return "\uD83D\uDD04 Syncing with remote...";
1687
+ return "\uD83C\uDF0D Syncing with remote";
1612
1688
  case "loop.sync_skipped":
1613
1689
  return `Note: git pull skipped (${event.reason})`;
1614
1690
  case "loop.checking_tasks":
1615
- return "\uD83D\uDD0D Checking for available tasks...";
1691
+ return null;
1616
1692
  case "loop.no_tasks":
1617
- return "\uD83D\uDCA4 No tasks available. Sleeping...";
1693
+ return `\uD83D\uDE34 No tasks available. Sleeping...
1694
+ `;
1618
1695
  case "loop.tasks_found":
1619
1696
  return "✨ Found task(s). \uD83E\uDD16 Starting Claude...";
1620
1697
  case "claude.started":
@@ -2066,7 +2143,10 @@ function createFileSystem(primitives) {
2066
2143
  return {
2067
2144
  exists: primitives.existsSync,
2068
2145
  readFile: (path) => primitives.readFile(path, "utf-8"),
2069
- writeFile: (path, content) => primitives.writeFile(path, content, "utf-8"),
2146
+ writeFile: (path, content, options) => primitives.writeFile(path, content, {
2147
+ encoding: "utf-8",
2148
+ flag: options?.flag
2149
+ }),
2070
2150
  mkdir: async (path, options) => {
2071
2151
  await primitives.mkdir(path, options);
2072
2152
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,10 +4,11 @@
4
4
  {{/if}}{{#unless isClaudeCodeWeb}}Follow these steps:
5
5
  {{/unless}}
6
6
  1. Run `{{bin}} next` to identify the (unblocked) task the user is referring to
7
- 2. Run `{{bin}} check` to verify the project is in a good state
8
- 3. Implement the task
9
- {{#unless hooksInstalled}}4. Run `{{bin}} check` before committing
10
- 5.{{/unless}}{{#if hooksInstalled}}4.{{/if}} Create a single atomic commit that includes:
7
+ 2. Run `{{bin}} focus "<task name>"` (so everyone knows you're working on it)
8
+ 3. Run `{{bin}} check` to verify the project is in a good state
9
+ 4. Implement the task
10
+ {{#unless hooksInstalled}}5. Run `{{bin}} check` before committing
11
+ 6.{{/unless}}{{#if hooksInstalled}}5.{{/if}} Create a single atomic commit that includes:
11
12
  - All implementation changes
12
13
  - Deletion of the completed task file
13
14
  - Updates to any facts that changed
@@ -20,6 +21,6 @@
20
21
  Add validation for user input
21
22
  ```
22
23
 
23
- {{#unless hooksInstalled}}6.{{/unless}}{{#if hooksInstalled}}5.{{/if}} Push your commit to the remote repository
24
+ {{#unless hooksInstalled}}7.{{/unless}}{{#if hooksInstalled}}6.{{/if}} Push your commit to the remote repository
24
25
 
25
26
  Keep your change small and focused. One task, one commit.
@@ -4,4 +4,4 @@ Follow these steps:
4
4
 
5
5
  1. Run `{{bin}} next` to see available tasks
6
6
  2. Pick ONE task and read its file to understand the requirements
7
- 3. Run `{{bin}} agent implement task` for instructions about how to implement
7
+ 3. Run `{{bin}} implement task` for instructions about how to implement
@@ -4,10 +4,11 @@
4
4
  {{/if}}{{#unless isClaudeCodeWeb}}Follow these steps:
5
5
  {{/unless}}
6
6
  1. Run `{{bin}} next` to identify the (unblocked) task the user is referring to
7
- 2. Run `{{bin}} check` to verify the project is in a good state
8
- 3. Implement the task
9
- {{#unless hooksInstalled}}4. Run `{{bin}} check` before committing
10
- 5.{{/unless}}{{#if hooksInstalled}}4.{{/if}} Create a single atomic commit that includes:
7
+ 2. Run `{{bin}} focus "<task name>"` (so everyone knows you're working on it)
8
+ 3. Run `{{bin}} check` to verify the project is in a good state
9
+ 4. Implement the task
10
+ {{#unless hooksInstalled}}5. Run `{{bin}} check` before committing
11
+ 6.{{/unless}}{{#if hooksInstalled}}5.{{/if}} Create a single atomic commit that includes:
11
12
  - All implementation changes
12
13
  - Deletion of the completed task file
13
14
  - Updates to any facts that changed
@@ -20,6 +21,6 @@
20
21
  Add validation for user input
21
22
  ```
22
23
 
23
- {{#unless hooksInstalled}}6.{{/unless}}{{#if hooksInstalled}}5.{{/if}} Push your commit to the remote repository
24
+ {{#unless hooksInstalled}}7.{{/unless}}{{#if hooksInstalled}}6.{{/if}} Push your commit to the remote repository
24
25
 
25
26
  Keep your change small and focused. One task, one commit.
@@ -4,4 +4,4 @@ Follow these steps:
4
4
 
5
5
  1. Run `{{bin}} next` to see available tasks
6
6
  2. Pick ONE task and read its file to understand the requirements
7
- 3. Run `{{bin}} agent implement task` for instructions about how to implement
7
+ 3. Run `{{bin}} implement task` for instructions about how to implement