@joshski/dust 0.1.9 → 0.1.10

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // lib/cli/entry.ts
3
3
  import { existsSync } from "node:fs";
4
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
5
5
 
6
6
  // lib/cli/templates.ts
7
7
  import { readFileSync } from "node:fs";
@@ -9,69 +9,246 @@ import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
11
11
  var templatesDir = join(__dirname2, "../templates");
12
+ function isTruthy(value) {
13
+ return value !== undefined && value !== "" && value !== "false";
14
+ }
15
+ function processConditionals(content, variables) {
16
+ let result = content.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, varName, block) => {
17
+ return isTruthy(variables[varName]) ? block : "";
18
+ });
19
+ result = result.replace(/\{\{#unless (\w+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (_, varName, block) => {
20
+ return !isTruthy(variables[varName]) ? block : "";
21
+ });
22
+ return result;
23
+ }
12
24
  function loadTemplate(name, variables = {}) {
13
25
  const templatePath = join(templatesDir, `${name}.txt`);
14
26
  let content = readFileSync(templatePath, "utf-8");
27
+ content = processConditionals(content, variables);
15
28
  for (const [key, value] of Object.entries(variables)) {
16
29
  content = content.replaceAll(`{{${key}}}`, value);
17
30
  }
18
31
  return content;
19
32
  }
20
33
 
21
- // lib/cli/commands/agent.ts
22
- var AGENT_SUBCOMMANDS = [
23
- "new task",
24
- "new goal",
25
- "new idea",
26
- "implement task",
27
- "understand goals",
28
- "pick task",
29
- "help"
30
- ];
31
- function templateVariables(settings) {
34
+ // lib/git/hooks.ts
35
+ import { join as join2 } from "node:path";
36
+ var DUST_HOOK_START = "# BEGIN DUST HOOK";
37
+ var DUST_HOOK_END = "# END DUST HOOK";
38
+ function generateHookContent(dustCommand) {
39
+ return `${DUST_HOOK_START}
40
+ ${dustCommand} pre push
41
+ if [ $? -ne 0 ]; then
42
+ echo "dust pre-push check failed"
43
+ exit 1
44
+ fi
45
+ ${DUST_HOOK_END}`;
46
+ }
47
+ function extractDustSection(content) {
48
+ const startIndex = content.indexOf(DUST_HOOK_START);
49
+ const endIndex = content.indexOf(DUST_HOOK_END);
50
+ if (startIndex === -1 || endIndex === -1) {
51
+ return null;
52
+ }
53
+ return content.substring(startIndex, endIndex + DUST_HOOK_END.length);
54
+ }
55
+ function removeDustSection(content) {
56
+ const startIndex = content.indexOf(DUST_HOOK_START);
57
+ const endIndex = content.indexOf(DUST_HOOK_END);
58
+ if (startIndex === -1 || endIndex === -1) {
59
+ return content;
60
+ }
61
+ const before = content.substring(0, startIndex);
62
+ const after = content.substring(endIndex + DUST_HOOK_END.length);
63
+ return (before + after).replace(/\n{3,}/g, `
64
+
65
+ `).trim();
66
+ }
67
+ function createHooksManager(cwd, fileSystem, settings) {
68
+ const gitDir = join2(cwd, ".git");
69
+ const hooksDir = join2(gitDir, "hooks");
70
+ const prePushPath = join2(hooksDir, "pre-push");
71
+ return {
72
+ isGitRepo: () => fileSystem.exists(gitDir),
73
+ isHookInstalled: async () => {
74
+ if (!fileSystem.exists(prePushPath)) {
75
+ return false;
76
+ }
77
+ try {
78
+ const content = await fileSystem.readFile(prePushPath);
79
+ return content.includes(DUST_HOOK_START);
80
+ } catch {
81
+ return false;
82
+ }
83
+ },
84
+ installHook: async () => {
85
+ if (!fileSystem.exists(hooksDir)) {
86
+ await fileSystem.mkdir(hooksDir, { recursive: true });
87
+ }
88
+ const hookContent = generateHookContent(settings.dustCommand);
89
+ let finalContent;
90
+ if (fileSystem.exists(prePushPath)) {
91
+ const existingContent = await fileSystem.readFile(prePushPath);
92
+ if (existingContent.includes(DUST_HOOK_START)) {
93
+ const withoutDust = removeDustSection(existingContent);
94
+ finalContent = withoutDust ? `${withoutDust}
95
+
96
+ ${hookContent}
97
+ ` : `#!/bin/sh
98
+
99
+ ${hookContent}
100
+ `;
101
+ } else {
102
+ finalContent = `${existingContent.trimEnd()}
103
+
104
+ ${hookContent}
105
+ `;
106
+ }
107
+ } else {
108
+ finalContent = `#!/bin/sh
109
+
110
+ ${hookContent}
111
+ `;
112
+ }
113
+ await fileSystem.writeFile(prePushPath, finalContent);
114
+ await fileSystem.chmod(prePushPath, 493);
115
+ },
116
+ getHookBinaryPath: async () => {
117
+ if (!fileSystem.exists(prePushPath)) {
118
+ return null;
119
+ }
120
+ try {
121
+ const content = await fileSystem.readFile(prePushPath);
122
+ const dustSection = extractDustSection(content);
123
+ if (!dustSection) {
124
+ return null;
125
+ }
126
+ const match = dustSection.match(/^(.+) pre push$/m);
127
+ return match ? match[1] : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ },
132
+ updateHookBinaryPath: async (newPath) => {
133
+ if (!fileSystem.exists(prePushPath)) {
134
+ return;
135
+ }
136
+ const content = await fileSystem.readFile(prePushPath);
137
+ const dustSection = extractDustSection(content);
138
+ if (!dustSection) {
139
+ return;
140
+ }
141
+ const withoutDust = removeDustSection(content);
142
+ const newHookContent = generateHookContent(newPath);
143
+ const finalContent = withoutDust ? `${withoutDust}
144
+
145
+ ${newHookContent}
146
+ ` : `#!/bin/sh
147
+
148
+ ${newHookContent}
149
+ `;
150
+ await fileSystem.writeFile(prePushPath, finalContent);
151
+ await fileSystem.chmod(prePushPath, 493);
152
+ }
153
+ };
154
+ }
155
+
156
+ // lib/cli/commands/agent-shared.ts
157
+ function templateVariables(settings, hooksInstalled) {
32
158
  return {
33
159
  bin: settings.dustCommand,
34
- installDependenciesHint: settings.installDependenciesHint || "Install any dependencies"
160
+ installDependenciesHint: settings.installDependenciesHint || "Install any dependencies",
161
+ hooksInstalled: hooksInstalled ? "true" : "false"
35
162
  };
36
163
  }
37
- async function agent(deps) {
38
- const { arguments: args, context: ctx, settings } = deps;
39
- const verb = args[0];
40
- const noun = args[1];
41
- const vars = templateVariables(settings);
42
- if (!verb) {
43
- ctx.stdout(loadTemplate("agent-greeting", vars));
44
- return { exitCode: 0 };
164
+ async function manageGitHooks(dependencies) {
165
+ const { context, fileSystem, settings } = dependencies;
166
+ const hooks = createHooksManager(context.cwd, fileSystem, settings);
167
+ if (!hooks.isGitRepo()) {
168
+ return false;
45
169
  }
46
- if (verb === "help" && !noun) {
47
- ctx.stdout(loadTemplate("agent-help", vars));
48
- return { exitCode: 0 };
170
+ const isInstalled = await hooks.isHookInstalled();
171
+ if (!isInstalled) {
172
+ await hooks.installHook();
173
+ return true;
49
174
  }
50
- const subcommand = noun ? `${verb} ${noun}` : verb;
51
- switch (subcommand) {
52
- case "new task":
53
- ctx.stdout(loadTemplate("agent-new-task", vars));
54
- return { exitCode: 0 };
55
- case "new goal":
56
- ctx.stdout(loadTemplate("agent-new-goal", vars));
57
- return { exitCode: 0 };
58
- case "new idea":
59
- ctx.stdout(loadTemplate("agent-new-idea", vars));
60
- return { exitCode: 0 };
61
- case "implement task":
62
- ctx.stdout(loadTemplate("agent-implement-task", vars));
63
- return { exitCode: 0 };
64
- case "understand goals":
65
- ctx.stdout(loadTemplate("agent-understand-goals", vars));
66
- return { exitCode: 0 };
67
- case "pick task":
68
- ctx.stdout(loadTemplate("agent-pick-task", vars));
69
- return { exitCode: 0 };
70
- default:
71
- ctx.stderr(`Unknown subcommand: ${subcommand}`);
72
- ctx.stderr(`Available: ${AGENT_SUBCOMMANDS.join(", ")}`);
73
- return { exitCode: 1 };
175
+ const hookBinaryPath = await hooks.getHookBinaryPath();
176
+ if (hookBinaryPath && hookBinaryPath !== settings.dustCommand) {
177
+ await hooks.updateHookBinaryPath(settings.dustCommand);
74
178
  }
179
+ return true;
180
+ }
181
+
182
+ // lib/cli/commands/agent.ts
183
+ async function agent(dependencies) {
184
+ const { context, settings } = dependencies;
185
+ const hooksInstalled = await manageGitHooks(dependencies);
186
+ const vars = templateVariables(settings, hooksInstalled);
187
+ context.stdout(loadTemplate("agent-greeting", vars));
188
+ return { exitCode: 0 };
189
+ }
190
+
191
+ // lib/cli/commands/agent-help.ts
192
+ async function agentHelp(dependencies) {
193
+ const { context, settings } = dependencies;
194
+ const hooksInstalled = await manageGitHooks(dependencies);
195
+ const vars = templateVariables(settings, hooksInstalled);
196
+ context.stdout(loadTemplate("agent-help", vars));
197
+ return { exitCode: 0 };
198
+ }
199
+
200
+ // lib/cli/commands/agent-implement-task.ts
201
+ async function agentImplementTask(dependencies) {
202
+ const { context, settings } = dependencies;
203
+ const hooksInstalled = await manageGitHooks(dependencies);
204
+ const vars = templateVariables(settings, hooksInstalled);
205
+ context.stdout(loadTemplate("agent-implement-task", vars));
206
+ return { exitCode: 0 };
207
+ }
208
+
209
+ // lib/cli/commands/agent-new-goal.ts
210
+ async function agentNewGoal(dependencies) {
211
+ const { context, settings } = dependencies;
212
+ const hooksInstalled = await manageGitHooks(dependencies);
213
+ const vars = templateVariables(settings, hooksInstalled);
214
+ context.stdout(loadTemplate("agent-new-goal", vars));
215
+ return { exitCode: 0 };
216
+ }
217
+
218
+ // lib/cli/commands/agent-new-idea.ts
219
+ async function agentNewIdea(dependencies) {
220
+ const { context, settings } = dependencies;
221
+ const hooksInstalled = await manageGitHooks(dependencies);
222
+ const vars = templateVariables(settings, hooksInstalled);
223
+ context.stdout(loadTemplate("agent-new-idea", vars));
224
+ return { exitCode: 0 };
225
+ }
226
+
227
+ // lib/cli/commands/agent-new-task.ts
228
+ async function agentNewTask(dependencies) {
229
+ const { context, settings } = dependencies;
230
+ const hooksInstalled = await manageGitHooks(dependencies);
231
+ const vars = templateVariables(settings, hooksInstalled);
232
+ context.stdout(loadTemplate("agent-new-task", vars));
233
+ return { exitCode: 0 };
234
+ }
235
+
236
+ // lib/cli/commands/agent-pick-task.ts
237
+ async function agentPickTask(dependencies) {
238
+ const { context, settings } = dependencies;
239
+ const hooksInstalled = await manageGitHooks(dependencies);
240
+ const vars = templateVariables(settings, hooksInstalled);
241
+ context.stdout(loadTemplate("agent-pick-task", vars));
242
+ return { exitCode: 0 };
243
+ }
244
+
245
+ // lib/cli/commands/agent-understand-goals.ts
246
+ async function agentUnderstandGoals(dependencies) {
247
+ const { context, settings } = dependencies;
248
+ const hooksInstalled = await manageGitHooks(dependencies);
249
+ const vars = templateVariables(settings, hooksInstalled);
250
+ context.stdout(loadTemplate("agent-understand-goals", vars));
251
+ return { exitCode: 0 };
75
252
  }
76
253
 
77
254
  // lib/cli/commands/check.ts
@@ -113,7 +290,7 @@ function validateTaskHeadings(filePath, content) {
113
290
  }
114
291
  return violations;
115
292
  }
116
- function validateLinks(filePath, content, fs) {
293
+ function validateLinks(filePath, content, fileSystem) {
117
294
  const violations = [];
118
295
  const lines = content.split(`
119
296
  `);
@@ -127,7 +304,7 @@ function validateLinks(filePath, content, fs) {
127
304
  if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
128
305
  const targetPath = linkTarget.split("#")[0];
129
306
  const resolvedPath = resolve(fileDir, targetPath);
130
- if (!fs.exists(resolvedPath)) {
307
+ if (!fileSystem.exists(resolvedPath)) {
131
308
  violations.push({
132
309
  file: filePath,
133
310
  message: `Broken link: "${linkTarget}"`,
@@ -203,31 +380,31 @@ function validateSemanticLinks(filePath, content) {
203
380
  }
204
381
  return violations;
205
382
  }
206
- async function validate(deps) {
207
- const { context: ctx, fileSystem: fs, globScanner: glob } = deps;
208
- const dustPath = `${ctx.cwd}/.dust`;
209
- if (!fs.exists(dustPath)) {
210
- ctx.stderr("Error: .dust directory not found");
211
- ctx.stderr("Run 'dust init' to initialize a Dust repository");
383
+ async function validate(dependencies) {
384
+ const { context, fileSystem, globScanner: glob } = dependencies;
385
+ const dustPath = `${context.cwd}/.dust`;
386
+ if (!fileSystem.exists(dustPath)) {
387
+ context.stderr("Error: .dust directory not found");
388
+ context.stderr("Run 'dust init' to initialize a Dust repository");
212
389
  return { exitCode: 1 };
213
390
  }
214
391
  const violations = [];
215
- ctx.stdout("Validating links in .dust/...");
392
+ context.stdout("Validating links in .dust/...");
216
393
  for await (const file of glob.scan(dustPath)) {
217
394
  if (!file.endsWith(".md"))
218
395
  continue;
219
396
  const filePath = `${dustPath}/${file}`;
220
- const content = await fs.readFile(filePath);
221
- violations.push(...validateLinks(filePath, content, fs));
397
+ const content = await fileSystem.readFile(filePath);
398
+ violations.push(...validateLinks(filePath, content, fileSystem));
222
399
  }
223
400
  const tasksPath = `${dustPath}/tasks`;
224
- if (fs.exists(tasksPath)) {
225
- ctx.stdout("Validating task files in .dust/tasks/...");
401
+ if (fileSystem.exists(tasksPath)) {
402
+ context.stdout("Validating task files in .dust/tasks/...");
226
403
  for await (const file of glob.scan(tasksPath)) {
227
404
  if (!file.endsWith(".md"))
228
405
  continue;
229
406
  const filePath = `${tasksPath}/${file}`;
230
- const content = await fs.readFile(filePath);
407
+ const content = await fileSystem.readFile(filePath);
231
408
  const filenameViolation = validateFilename(filePath);
232
409
  if (filenameViolation) {
233
410
  violations.push(filenameViolation);
@@ -237,15 +414,15 @@ async function validate(deps) {
237
414
  }
238
415
  }
239
416
  if (violations.length === 0) {
240
- ctx.stdout("All validations passed!");
417
+ context.stdout("All validations passed!");
241
418
  return { exitCode: 0 };
242
419
  }
243
- ctx.stderr(`Found ${violations.length} violation(s):`);
244
- ctx.stderr("");
420
+ context.stderr(`Found ${violations.length} violation(s):`);
421
+ context.stderr("");
245
422
  for (const v of violations) {
246
423
  const location = v.line ? `:${v.line}` : "";
247
- ctx.stderr(` ${v.file}${location}`);
248
- ctx.stderr(` ${v.message}`);
424
+ context.stderr(` ${v.file}${location}`);
425
+ context.stderr(` ${v.message}`);
249
426
  }
250
427
  return { exitCode: 1 };
251
428
  }
@@ -266,8 +443,8 @@ function createBufferedRunner(spawnFn) {
266
443
  proc.on("close", (code) => {
267
444
  resolve2({ exitCode: code ?? 1, output: chunks.join("") });
268
445
  });
269
- proc.on("error", (err) => {
270
- resolve2({ exitCode: 1, output: err.message });
446
+ proc.on("error", (error) => {
447
+ resolve2({ exitCode: 1, output: error.message });
271
448
  });
272
449
  });
273
450
  }
@@ -287,16 +464,16 @@ async function runConfiguredChecks(checks, cwd, runner) {
287
464
  });
288
465
  return Promise.all(promises);
289
466
  }
290
- async function runValidationCheck(deps) {
467
+ async function runValidationCheck(dependencies) {
291
468
  const outputLines = [];
292
- const bufferedCtx = {
293
- cwd: deps.context.cwd,
469
+ const bufferedContext = {
470
+ cwd: dependencies.context.cwd,
294
471
  stdout: (msg) => outputLines.push(msg),
295
472
  stderr: (msg) => outputLines.push(msg)
296
473
  };
297
474
  const result = await validate({
298
- ...deps,
299
- context: bufferedCtx,
475
+ ...dependencies,
476
+ context: bufferedContext,
300
477
  arguments: []
301
478
  });
302
479
  return {
@@ -308,54 +485,54 @@ async function runValidationCheck(deps) {
308
485
  isBuiltIn: true
309
486
  };
310
487
  }
311
- function displayResults(results, ctx) {
488
+ function displayResults(results, context) {
312
489
  const passed = results.filter((r) => r.exitCode === 0);
313
490
  const failed = results.filter((r) => r.exitCode !== 0);
314
491
  for (const result of results) {
315
492
  if (result.exitCode === 0) {
316
- ctx.stdout(`✓ ${result.name}`);
493
+ context.stdout(`✓ ${result.name}`);
317
494
  } else {
318
- ctx.stdout(`✗ ${result.name}`);
495
+ context.stdout(`✗ ${result.name}`);
319
496
  }
320
497
  }
321
498
  for (const result of failed) {
322
- ctx.stdout("");
323
- ctx.stdout(`> ${result.command}`);
499
+ context.stdout("");
500
+ context.stdout(`> ${result.command}`);
324
501
  if (result.output.trim()) {
325
- ctx.stdout(result.output.trimEnd());
502
+ context.stdout(result.output.trimEnd());
326
503
  }
327
504
  if (result.hints && result.hints.length > 0) {
328
- ctx.stdout("");
329
- ctx.stdout(`Hints for fixing '${result.name}':`);
505
+ context.stdout("");
506
+ context.stdout(`Hints for fixing '${result.name}':`);
330
507
  for (const hint of result.hints) {
331
- ctx.stdout(` - ${hint}`);
508
+ context.stdout(` - ${hint}`);
332
509
  }
333
510
  }
334
511
  }
335
- ctx.stdout("");
336
- ctx.stdout(`${passed.length}/${results.length} checks passed`);
512
+ context.stdout("");
513
+ context.stdout(`${passed.length}/${results.length} checks passed`);
337
514
  return failed.length > 0 ? 1 : 0;
338
515
  }
339
- async function check(deps, bufferedRunner = defaultBufferedRunner) {
340
- const { context: ctx, fileSystem: fs, settings } = deps;
516
+ async function check(dependencies, bufferedRunner = defaultBufferedRunner) {
517
+ const { context, fileSystem, settings } = dependencies;
341
518
  if (!settings.checks || settings.checks.length === 0) {
342
- ctx.stderr("Error: No checks configured in .dust/config/settings.json");
343
- ctx.stderr("");
344
- ctx.stderr("Add checks to your settings.json:");
345
- ctx.stderr(" {");
346
- ctx.stderr(' "checks": [');
347
- ctx.stderr(' { "name": "lint", "command": "npm run lint" },');
348
- ctx.stderr(' { "name": "test", "command": "npm test" }');
349
- ctx.stderr(" ]");
350
- ctx.stderr(" }");
519
+ context.stderr("Error: No checks configured in .dust/config/settings.json");
520
+ context.stderr("");
521
+ context.stderr("Add checks to your settings.json:");
522
+ context.stderr(" {");
523
+ context.stderr(' "checks": [');
524
+ context.stderr(' { "name": "lint", "command": "npm run lint" },');
525
+ context.stderr(' { "name": "test", "command": "npm test" }');
526
+ context.stderr(" ]");
527
+ context.stderr(" }");
351
528
  return { exitCode: 1 };
352
529
  }
353
530
  const checkPromises = [];
354
- const dustPath = `${ctx.cwd}/.dust`;
355
- if (fs.exists(dustPath)) {
356
- checkPromises.push(runValidationCheck(deps));
531
+ const dustPath = `${context.cwd}/.dust`;
532
+ if (fileSystem.exists(dustPath)) {
533
+ checkPromises.push(runValidationCheck(dependencies));
357
534
  }
358
- checkPromises.push(runConfiguredChecks(settings.checks, ctx.cwd, bufferedRunner));
535
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, bufferedRunner));
359
536
  const promiseResults = await Promise.all(checkPromises);
360
537
  const results = [];
361
538
  for (const result of promiseResults) {
@@ -365,7 +542,7 @@ async function check(deps, bufferedRunner = defaultBufferedRunner) {
365
542
  results.push(result);
366
543
  }
367
544
  }
368
- const exitCode = displayResults(results, ctx);
545
+ const exitCode = displayResults(results, context);
369
546
  return { exitCode };
370
547
  }
371
548
 
@@ -373,25 +550,40 @@ async function check(deps, bufferedRunner = defaultBufferedRunner) {
373
550
  function generateHelpText(settings) {
374
551
  return loadTemplate("help", { bin: settings.dustCommand });
375
552
  }
376
- async function help(deps) {
377
- deps.context.stdout(generateHelpText(deps.settings));
553
+ async function help(dependencies) {
554
+ dependencies.context.stdout(generateHelpText(dependencies.settings));
378
555
  return { exitCode: 0 };
379
556
  }
380
557
 
381
558
  // lib/cli/settings.ts
382
- import { join as join2 } from "node:path";
559
+ import { join as join3 } from "node:path";
383
560
  var DEFAULT_SETTINGS = {
384
561
  dustCommand: "npx dust",
385
562
  installDependenciesHint: "Install any dependencies"
386
563
  };
387
- function detectDustCommand(cwd, fs) {
388
- if (fs.exists(join2(cwd, "bun.lockb"))) {
564
+ function detectInstallDependenciesHint(cwd, fileSystem) {
565
+ if (fileSystem.exists(join3(cwd, "bun.lockb")) || fileSystem.exists(join3(cwd, "bun.lock"))) {
566
+ return "Run `bun install`";
567
+ }
568
+ if (fileSystem.exists(join3(cwd, "pnpm-lock.yaml"))) {
569
+ return "Run `pnpm install`";
570
+ }
571
+ if (fileSystem.exists(join3(cwd, "package-lock.json"))) {
572
+ return "Run `npm install`";
573
+ }
574
+ if (fileSystem.exists(join3(cwd, "yarn.lock"))) {
575
+ return "Run `yarn install`";
576
+ }
577
+ return "Install any dependencies";
578
+ }
579
+ function detectDustCommand(cwd, fileSystem) {
580
+ if (fileSystem.exists(join3(cwd, "bun.lockb"))) {
389
581
  return "bunx dust";
390
582
  }
391
- if (fs.exists(join2(cwd, "pnpm-lock.yaml"))) {
583
+ if (fileSystem.exists(join3(cwd, "pnpm-lock.yaml"))) {
392
584
  return "pnpx dust";
393
585
  }
394
- if (fs.exists(join2(cwd, "package-lock.json"))) {
586
+ if (fileSystem.exists(join3(cwd, "package-lock.json"))) {
395
587
  return "npx dust";
396
588
  }
397
589
  if (process.env.BUN_INSTALL) {
@@ -399,47 +591,49 @@ function detectDustCommand(cwd, fs) {
399
591
  }
400
592
  return "npx dust";
401
593
  }
402
- async function loadSettings(cwd, fs) {
403
- const settingsPath = join2(cwd, ".dust", "config", "settings.json");
404
- if (!fs.exists(settingsPath)) {
594
+ async function loadSettings(cwd, fileSystem) {
595
+ const settingsPath = join3(cwd, ".dust", "config", "settings.json");
596
+ if (!fileSystem.exists(settingsPath)) {
405
597
  return {
406
- dustCommand: detectDustCommand(cwd, fs)
598
+ dustCommand: detectDustCommand(cwd, fileSystem),
599
+ installDependenciesHint: detectInstallDependenciesHint(cwd, fileSystem)
407
600
  };
408
601
  }
409
602
  try {
410
- const content = await fs.readFile(settingsPath);
603
+ const content = await fileSystem.readFile(settingsPath);
411
604
  const parsed = JSON.parse(content);
412
- if (!parsed.dustCommand) {
413
- return {
414
- ...DEFAULT_SETTINGS,
415
- ...parsed,
416
- dustCommand: detectDustCommand(cwd, fs)
417
- };
418
- }
419
- return {
605
+ const result = {
420
606
  ...DEFAULT_SETTINGS,
421
607
  ...parsed
422
608
  };
609
+ if (!parsed.dustCommand) {
610
+ result.dustCommand = detectDustCommand(cwd, fileSystem);
611
+ }
612
+ if (!parsed.installDependenciesHint) {
613
+ result.installDependenciesHint = detectInstallDependenciesHint(cwd, fileSystem);
614
+ }
615
+ return result;
423
616
  } catch {
424
617
  return {
425
- dustCommand: detectDustCommand(cwd, fs)
618
+ dustCommand: detectDustCommand(cwd, fileSystem),
619
+ installDependenciesHint: detectInstallDependenciesHint(cwd, fileSystem)
426
620
  };
427
621
  }
428
622
  }
429
623
 
430
624
  // lib/cli/commands/init.ts
431
625
  var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
432
- function generateSettings(cwd, fs) {
433
- const dustCommand = detectDustCommand(cwd, fs);
626
+ function generateSettings(cwd, fileSystem) {
627
+ const dustCommand = detectDustCommand(cwd, fileSystem);
434
628
  const checks = [];
435
629
  let installDependenciesHint = "Install any dependencies";
436
- if (fs.exists(`${cwd}/bun.lockb`)) {
630
+ if (fileSystem.exists(`${cwd}/bun.lockb`)) {
437
631
  checks.push({ name: "test", command: "bun test" });
438
632
  installDependenciesHint = "Run `bun install`";
439
- } else if (fs.exists(`${cwd}/pnpm-lock.yaml`)) {
633
+ } else if (fileSystem.exists(`${cwd}/pnpm-lock.yaml`)) {
440
634
  checks.push({ name: "test", command: "pnpm test" });
441
635
  installDependenciesHint = "Run `pnpm install`";
442
- } else if (fs.exists(`${cwd}/package-lock.json`) || fs.exists(`${cwd}/package.json`)) {
636
+ } else if (fileSystem.exists(`${cwd}/package-lock.json`) || fileSystem.exists(`${cwd}/package.json`)) {
443
637
  checks.push({ name: "test", command: "npm test" });
444
638
  installDependenciesHint = "Run `npm install`";
445
639
  }
@@ -449,99 +643,277 @@ var USE_DUST_FACT = `# Use dust for planning
449
643
 
450
644
  This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
451
645
  `;
452
- async function init(deps) {
453
- const { context: ctx, fileSystem: fs } = deps;
454
- const dustPath = `${ctx.cwd}/.dust`;
455
- const dustCommand = detectDustCommand(ctx.cwd, fs);
646
+ async function init(dependencies) {
647
+ const { context, fileSystem } = dependencies;
648
+ const dustPath = `${context.cwd}/.dust`;
649
+ const dustCommand = detectDustCommand(context.cwd, fileSystem);
456
650
  const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
457
- if (fs.exists(dustPath)) {
458
- ctx.stdout("Note: .dust directory already exists, skipping creation");
651
+ if (fileSystem.exists(dustPath)) {
652
+ context.stdout("Note: .dust directory already exists, skipping creation");
459
653
  } else {
460
- await fs.mkdir(dustPath, { recursive: true });
654
+ await fileSystem.mkdir(dustPath, { recursive: true });
461
655
  for (const dir of DUST_DIRECTORIES) {
462
- await fs.mkdir(`${dustPath}/${dir}`, { recursive: true });
656
+ await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
463
657
  }
464
- await fs.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT);
465
- const settings = generateSettings(ctx.cwd, fs);
466
- await fs.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
658
+ await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT);
659
+ const settings = generateSettings(context.cwd, fileSystem);
660
+ await fileSystem.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
467
661
  `);
468
- ctx.stdout("Initialized Dust repository in .dust/");
469
- ctx.stdout(`Created directories: ${DUST_DIRECTORIES.join(", ")}`);
470
- ctx.stdout("Created initial fact: .dust/facts/use-dust-for-planning.md");
471
- ctx.stdout("Created settings: .dust/config/settings.json");
472
- }
473
- const claudeMdPath = `${ctx.cwd}/CLAUDE.md`;
474
- if (fs.exists(claudeMdPath)) {
475
- ctx.stdout(`Warning: CLAUDE.md already exists. Consider adding: "${agentInstruction}"`);
662
+ context.stdout("Initialized Dust repository in .dust/");
663
+ context.stdout(`Created directories: ${DUST_DIRECTORIES.join(", ")}`);
664
+ context.stdout("Created initial fact: .dust/facts/use-dust-for-planning.md");
665
+ context.stdout("Created settings: .dust/config/settings.json");
666
+ }
667
+ const claudeMdPath = `${context.cwd}/CLAUDE.md`;
668
+ if (fileSystem.exists(claudeMdPath)) {
669
+ context.stdout(`Warning: CLAUDE.md already exists. Consider adding: "${agentInstruction}"`);
476
670
  } else {
477
671
  const claudeContent = loadTemplate("claude-md", { dustCommand });
478
- await fs.writeFile(claudeMdPath, claudeContent);
479
- ctx.stdout("Created CLAUDE.md with agent instructions");
672
+ await fileSystem.writeFile(claudeMdPath, claudeContent);
673
+ context.stdout("Created CLAUDE.md with agent instructions");
480
674
  }
481
- const agentsMdPath = `${ctx.cwd}/AGENTS.md`;
482
- if (fs.exists(agentsMdPath)) {
483
- ctx.stdout(`Warning: AGENTS.md already exists. Consider adding: "${agentInstruction}"`);
675
+ const agentsMdPath = `${context.cwd}/AGENTS.md`;
676
+ if (fileSystem.exists(agentsMdPath)) {
677
+ context.stdout(`Warning: AGENTS.md already exists. Consider adding: "${agentInstruction}"`);
484
678
  } else {
485
679
  const agentsContent = loadTemplate("agents-md", { dustCommand });
486
- await fs.writeFile(agentsMdPath, agentsContent);
487
- ctx.stdout("Created AGENTS.md with agent instructions");
680
+ await fileSystem.writeFile(agentsMdPath, agentsContent);
681
+ context.stdout("Created AGENTS.md with agent instructions");
488
682
  }
489
683
  const runner = dustCommand.split(" ")[0];
490
- ctx.stdout("");
491
- ctx.stdout("Commit the changes if you are happy, then get planning!");
492
- ctx.stdout("");
493
- ctx.stdout("If this is a new repository, you can start adding ideas or tasks right away:");
494
- ctx.stdout(`> ${runner} claude "Idea: friendly UI for non-technical users"`);
495
- ctx.stdout(`> ${runner} codex "Task: set up code coverage"`);
496
- ctx.stdout("");
497
- ctx.stdout("If this is an existing codebase, you might want to backfill goals and facts:");
498
- ctx.stdout(`> ${runner} claude "Add goals and facts based on the code in this repository"`);
684
+ context.stdout("");
685
+ context.stdout("Commit the changes if you are happy, then get planning!");
686
+ context.stdout("");
687
+ context.stdout("If this is a new repository, you can start adding ideas or tasks right away:");
688
+ context.stdout(`> ${runner} claude "Idea: friendly UI for non-technical users"`);
689
+ context.stdout(`> ${runner} codex "Task: set up code coverage"`);
690
+ context.stdout("");
691
+ context.stdout("If this is an existing codebase, you might want to backfill goals and facts:");
692
+ context.stdout(`> ${runner} claude "Add goals and facts based on the code in this repository"`);
499
693
  return { exitCode: 0 };
500
694
  }
501
695
 
502
696
  // lib/cli/commands/list.ts
503
697
  var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
504
- async function list(deps) {
505
- const { arguments: args, context: ctx, fileSystem: fs } = deps;
506
- const dustPath = `${ctx.cwd}/.dust`;
507
- if (!fs.exists(dustPath)) {
508
- ctx.stderr("Error: .dust directory not found");
509
- ctx.stderr("Run 'dust init' to initialize a Dust repository");
698
+ async function list(dependencies) {
699
+ const { arguments: commandArguments, context, fileSystem } = dependencies;
700
+ const dustPath = `${context.cwd}/.dust`;
701
+ if (!fileSystem.exists(dustPath)) {
702
+ context.stderr("Error: .dust directory not found");
703
+ context.stderr("Run 'dust init' to initialize a Dust repository");
510
704
  return { exitCode: 1 };
511
705
  }
512
- const typesToList = args.length === 0 ? [...VALID_TYPES] : args.filter((a) => VALID_TYPES.includes(a));
513
- if (args.length > 0 && typesToList.length === 0) {
514
- ctx.stderr(`Invalid type: ${args[0]}`);
515
- ctx.stderr(`Valid types: ${VALID_TYPES.join(", ")}`);
706
+ const typesToList = commandArguments.length === 0 ? [...VALID_TYPES] : commandArguments.filter((a) => VALID_TYPES.includes(a));
707
+ if (commandArguments.length > 0 && typesToList.length === 0) {
708
+ context.stderr(`Invalid type: ${commandArguments[0]}`);
709
+ context.stderr(`Valid types: ${VALID_TYPES.join(", ")}`);
516
710
  return { exitCode: 1 };
517
711
  }
518
712
  for (const type of typesToList) {
519
713
  const dirPath = `${dustPath}/${type}`;
520
- if (!fs.exists(dirPath)) {
714
+ if (!fileSystem.exists(dirPath)) {
521
715
  continue;
522
716
  }
523
- const files = await fs.readdir(dirPath);
717
+ const files = await fileSystem.readdir(dirPath);
524
718
  const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
525
719
  if (mdFiles.length === 0) {
526
720
  continue;
527
721
  }
528
- ctx.stdout(`${type}:`);
722
+ context.stdout(`${type}:`);
529
723
  for (const file of mdFiles) {
530
724
  const filePath = `${dirPath}/${file}`;
531
- const content = await fs.readFile(filePath);
725
+ const content = await fileSystem.readFile(filePath);
532
726
  const title = extractTitle(content);
533
727
  const name = file.replace(/\.md$/, "");
534
728
  if (title) {
535
- ctx.stdout(` ${name} - ${title}`);
729
+ context.stdout(` ${name} - ${title}`);
536
730
  } else {
537
- ctx.stdout(` ${name}`);
731
+ context.stdout(` ${name}`);
538
732
  }
539
733
  }
540
- ctx.stdout("");
734
+ context.stdout("");
541
735
  }
542
736
  return { exitCode: 0 };
543
737
  }
544
738
 
739
+ // lib/cli/commands/loop.ts
740
+ import { spawn as nodeSpawn2 } from "node:child_process";
741
+
742
+ // lib/claude/spawn-claude-code.ts
743
+ import { spawn as nodeSpawn } from "node:child_process";
744
+ import { createInterface as nodeCreateInterface } from "node:readline";
745
+ var defaultDependencies = {
746
+ spawn: nodeSpawn,
747
+ createInterface: nodeCreateInterface
748
+ };
749
+ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDependencies) {
750
+ const {
751
+ cwd,
752
+ allowedTools,
753
+ maxTurns,
754
+ model,
755
+ systemPrompt,
756
+ sessionId,
757
+ dangerouslySkipPermissions
758
+ } = options;
759
+ const claudeArguments = [
760
+ "-p",
761
+ prompt,
762
+ "--output-format",
763
+ "stream-json",
764
+ "--verbose",
765
+ "--include-partial-messages"
766
+ ];
767
+ if (allowedTools?.length) {
768
+ claudeArguments.push("--allowedTools", ...allowedTools);
769
+ }
770
+ if (maxTurns) {
771
+ claudeArguments.push("--max-turns", String(maxTurns));
772
+ }
773
+ if (model) {
774
+ claudeArguments.push("--model", model);
775
+ }
776
+ if (systemPrompt) {
777
+ claudeArguments.push("--system-prompt", systemPrompt);
778
+ }
779
+ if (sessionId) {
780
+ claudeArguments.push("--session-id", sessionId);
781
+ }
782
+ if (dangerouslySkipPermissions) {
783
+ claudeArguments.push("--dangerously-skip-permissions");
784
+ }
785
+ const proc = dependencies.spawn("claude", claudeArguments, {
786
+ cwd,
787
+ stdio: ["ignore", "pipe", "pipe"]
788
+ });
789
+ if (!proc.stdout) {
790
+ throw new Error("Failed to get stdout from claude process");
791
+ }
792
+ const rl = dependencies.createInterface({ input: proc.stdout });
793
+ for await (const line of rl) {
794
+ if (!line.trim())
795
+ continue;
796
+ try {
797
+ yield JSON.parse(line);
798
+ } catch {}
799
+ }
800
+ await new Promise((resolve2, reject) => {
801
+ proc.on("close", (code) => {
802
+ if (code === 0 || code === null)
803
+ resolve2();
804
+ else
805
+ reject(new Error(`claude exited with code ${code}`));
806
+ });
807
+ proc.on("error", reject);
808
+ });
809
+ }
810
+
811
+ // lib/claude/event-parser.ts
812
+ function* parseRawEvent(raw) {
813
+ if (raw.type === "stream_event") {
814
+ const event = raw.event;
815
+ if (event?.delta?.type === "text_delta" && event.delta.text) {
816
+ yield { type: "text_delta", text: event.delta.text };
817
+ }
818
+ } else if (raw.type === "assistant") {
819
+ const msg = raw;
820
+ const content = msg.message?.content ?? [];
821
+ yield { type: "assistant_message", content };
822
+ for (const block of content) {
823
+ if (block.type === "tool_use" && block.id && block.name && block.input) {
824
+ yield {
825
+ type: "tool_use",
826
+ id: block.id,
827
+ name: block.name,
828
+ input: block.input
829
+ };
830
+ }
831
+ }
832
+ } else if (raw.type === "user") {
833
+ const msg = raw;
834
+ for (const block of msg.message?.content ?? []) {
835
+ if (block.type === "tool_result" && block.tool_use_id) {
836
+ yield {
837
+ type: "tool_result",
838
+ toolUseId: block.tool_use_id,
839
+ content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
840
+ };
841
+ }
842
+ }
843
+ } else if (raw.type === "result") {
844
+ const r = raw;
845
+ yield {
846
+ type: "result",
847
+ subtype: r.subtype ?? "success",
848
+ result: r.result,
849
+ error: r.error,
850
+ cost_usd: r.total_cost_usd ?? r.cost_usd ?? 0,
851
+ duration_ms: r.duration_ms ?? 0,
852
+ num_turns: r.num_turns ?? 0,
853
+ session_id: r.session_id ?? ""
854
+ };
855
+ }
856
+ }
857
+
858
+ // lib/claude/streamer.ts
859
+ async function streamEvents(events, sink) {
860
+ let hadTextOutput = false;
861
+ for await (const raw of events) {
862
+ for (const event of parseRawEvent(raw)) {
863
+ processEvent(event, sink, { hadTextOutput });
864
+ if (event.type === "text_delta") {
865
+ hadTextOutput = true;
866
+ } else if (event.type === "tool_use") {
867
+ hadTextOutput = false;
868
+ }
869
+ }
870
+ }
871
+ }
872
+ function processEvent(event, sink, state) {
873
+ switch (event.type) {
874
+ case "text_delta":
875
+ sink.write(event.text);
876
+ break;
877
+ case "tool_use":
878
+ if (state.hadTextOutput) {
879
+ sink.line("");
880
+ sink.line("");
881
+ }
882
+ sink.line(`\uD83D\uDD27 Tool: ${event.name}`);
883
+ sink.line(` Input: ${JSON.stringify(event.input, null, 2).replace(/\n/g, `
884
+ `)}`);
885
+ break;
886
+ case "tool_result":
887
+ sink.line(`✅ Result (${event.content.length} chars)`);
888
+ sink.line("");
889
+ break;
890
+ case "result":
891
+ sink.line("");
892
+ sink.line(`\uD83C\uDFC1 Done: ${event.subtype}, ${event.num_turns} turns, $${event.cost_usd.toFixed(4)}`);
893
+ break;
894
+ case "assistant_message":
895
+ break;
896
+ }
897
+ }
898
+ function createStdoutSink() {
899
+ return {
900
+ write: (text) => process.stdout.write(text),
901
+ line: (text) => console.log(text)
902
+ };
903
+ }
904
+
905
+ // lib/claude/run.ts
906
+ var defaultRunnerDependencies = {
907
+ spawnClaudeCode,
908
+ createStdoutSink,
909
+ streamEvents
910
+ };
911
+ async function run(prompt, options = {}, dependencies = defaultRunnerDependencies) {
912
+ const events = dependencies.spawnClaudeCode(prompt, options);
913
+ const sink = dependencies.createStdoutSink();
914
+ await dependencies.streamEvents(events, sink);
915
+ }
916
+
545
917
  // lib/cli/commands/next.ts
546
918
  function extractBlockedBy(content) {
547
919
  const blockedByMatch = content.match(/^## Blocked by\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
@@ -561,19 +933,19 @@ function extractBlockedBy(content) {
561
933
  }
562
934
  return blockers;
563
935
  }
564
- async function next(deps) {
565
- const { context: ctx, fileSystem: fs } = deps;
566
- const dustPath = `${ctx.cwd}/.dust`;
567
- if (!fs.exists(dustPath)) {
568
- ctx.stderr("Error: .dust directory not found");
569
- ctx.stderr("Run 'dust init' to initialize a Dust repository");
936
+ async function next(dependencies) {
937
+ const { context, fileSystem } = dependencies;
938
+ const dustPath = `${context.cwd}/.dust`;
939
+ if (!fileSystem.exists(dustPath)) {
940
+ context.stderr("Error: .dust directory not found");
941
+ context.stderr("Run 'dust init' to initialize a Dust repository");
570
942
  return { exitCode: 1 };
571
943
  }
572
944
  const tasksPath = `${dustPath}/tasks`;
573
- if (!fs.exists(tasksPath)) {
945
+ if (!fileSystem.exists(tasksPath)) {
574
946
  return { exitCode: 0 };
575
947
  }
576
- const files = await fs.readdir(tasksPath);
948
+ const files = await fileSystem.readdir(tasksPath);
577
949
  const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
578
950
  if (mdFiles.length === 0) {
579
951
  return { exitCode: 0 };
@@ -582,7 +954,7 @@ async function next(deps) {
582
954
  const unblockedTasks = [];
583
955
  for (const file of mdFiles) {
584
956
  const filePath = `${tasksPath}/${file}`;
585
- const content = await fs.readFile(filePath);
957
+ const content = await fileSystem.readFile(filePath);
586
958
  const blockers = extractBlockedBy(content);
587
959
  const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
588
960
  if (!hasIncompleteBlocker) {
@@ -594,17 +966,111 @@ async function next(deps) {
594
966
  if (unblockedTasks.length === 0) {
595
967
  return { exitCode: 0 };
596
968
  }
597
- ctx.stdout("Next tasks:");
969
+ context.stdout("Next tasks:");
598
970
  for (const task of unblockedTasks) {
599
971
  if (task.title) {
600
- ctx.stdout(` ${task.path} - ${task.title}`);
972
+ context.stdout(` ${task.path} - ${task.title}`);
601
973
  } else {
602
- ctx.stdout(` ${task.path}`);
974
+ context.stdout(` ${task.path}`);
603
975
  }
604
976
  }
605
977
  return { exitCode: 0 };
606
978
  }
607
979
 
980
+ // lib/cli/commands/loop.ts
981
+ function createDefaultDependencies() {
982
+ return {
983
+ spawn: nodeSpawn2,
984
+ run,
985
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
986
+ };
987
+ }
988
+ var SLEEP_INTERVAL_MS = 30000;
989
+ async function gitPull(cwd, spawn2) {
990
+ return new Promise((resolve2) => {
991
+ const proc = spawn2("git", ["pull"], {
992
+ cwd,
993
+ stdio: ["ignore", "pipe", "pipe"]
994
+ });
995
+ let stderr = "";
996
+ proc.stderr?.on("data", (data) => {
997
+ stderr += data.toString();
998
+ });
999
+ proc.on("close", (code) => {
1000
+ if (code === 0) {
1001
+ resolve2({ success: true });
1002
+ } else {
1003
+ resolve2({ success: false, message: stderr.trim() || "git pull failed" });
1004
+ }
1005
+ });
1006
+ proc.on("error", (error) => {
1007
+ resolve2({ success: false, message: error.message });
1008
+ });
1009
+ });
1010
+ }
1011
+ async function hasAvailableTasks(dependencies) {
1012
+ let hasOutput = false;
1013
+ const captureContext = {
1014
+ ...dependencies.context,
1015
+ stdout: () => {
1016
+ hasOutput = true;
1017
+ }
1018
+ };
1019
+ await next({ ...dependencies, context: captureContext });
1020
+ return hasOutput;
1021
+ }
1022
+ async function runOneIteration(dependencies, loopDependencies) {
1023
+ const { context } = dependencies;
1024
+ const { spawn: spawn2, run: run2 } = loopDependencies;
1025
+ context.stdout("Syncing with remote...");
1026
+ const pullResult = await gitPull(context.cwd, spawn2);
1027
+ if (!pullResult.success) {
1028
+ context.stdout(`Note: git pull skipped (${pullResult.message})`);
1029
+ }
1030
+ context.stdout("Checking for available tasks...");
1031
+ const hasTasks = await hasAvailableTasks(dependencies);
1032
+ if (!hasTasks) {
1033
+ context.stdout("No tasks available. Sleeping...");
1034
+ context.stdout("");
1035
+ return "no_tasks";
1036
+ }
1037
+ context.stdout("Found task(s). Starting Claude...");
1038
+ context.stdout("");
1039
+ try {
1040
+ await run2("go", { cwd: context.cwd, dangerouslySkipPermissions: true });
1041
+ context.stdout("");
1042
+ context.stdout("Claude session complete. Continuing loop...");
1043
+ context.stdout("");
1044
+ return "ran_claude";
1045
+ } catch (error) {
1046
+ const message = error instanceof Error ? error.message : String(error);
1047
+ context.stderr(`Claude exited with error: ${message}`);
1048
+ context.stdout("");
1049
+ context.stdout("Claude session complete. Continuing loop...");
1050
+ context.stdout("");
1051
+ return "claude_error";
1052
+ }
1053
+ }
1054
+ async function loop(dependencies, loopDependencies = createDefaultDependencies()) {
1055
+ const { context } = dependencies;
1056
+ context.stdout("WARNING: This command skips all permission checks. Only use in a sandbox environment!");
1057
+ context.stdout("");
1058
+ context.stdout("Starting dust loop...");
1059
+ context.stdout("Press Ctrl+C to stop");
1060
+ context.stdout("");
1061
+ while (true) {
1062
+ const result = await runOneIteration(dependencies, loopDependencies);
1063
+ if (result === "no_tasks") {
1064
+ await loopDependencies.sleep(SLEEP_INTERVAL_MS);
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ // lib/cli/commands/pre-push.ts
1070
+ async function prePush(dependencies) {
1071
+ return check(dependencies);
1072
+ }
1073
+
608
1074
  // lib/cli/main.ts
609
1075
  var commandRegistry = {
610
1076
  init,
@@ -613,9 +1079,18 @@ var commandRegistry = {
613
1079
  next,
614
1080
  check,
615
1081
  agent,
1082
+ "agent-help": agentHelp,
1083
+ "agent-new-task": agentNewTask,
1084
+ "agent-new-goal": agentNewGoal,
1085
+ "agent-new-idea": agentNewIdea,
1086
+ "agent-implement-task": agentImplementTask,
1087
+ "agent-pick-task": agentPickTask,
1088
+ "agent-understand-goals": agentUnderstandGoals,
1089
+ loop,
1090
+ "pre-push": prePush,
616
1091
  help
617
1092
  };
618
- var COMMANDS = Object.keys(commandRegistry);
1093
+ var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes("-"));
619
1094
  var HELP_TEXT = generateHelpText({ dustCommand: "dust" });
620
1095
  function isHelpRequest(command) {
621
1096
  return !command || command === "help" || command === "--help" || command === "-h";
@@ -623,60 +1098,80 @@ function isHelpRequest(command) {
623
1098
  function isValidCommand(command) {
624
1099
  return command in commandRegistry;
625
1100
  }
626
- async function runCommand(command, deps) {
627
- return commandRegistry[command](deps);
1101
+ async function runCommand(command, dependencies) {
1102
+ return commandRegistry[command](dependencies);
1103
+ }
1104
+ function resolveCommand(commandArguments) {
1105
+ for (let i = commandArguments.length;i > 0; i--) {
1106
+ const candidate = commandArguments.slice(0, i).join("-");
1107
+ if (candidate in commandRegistry) {
1108
+ return { command: candidate, remaining: commandArguments.slice(i) };
1109
+ }
1110
+ }
1111
+ return { command: null, remaining: commandArguments };
628
1112
  }
629
1113
  async function main(options) {
630
- const { args, ctx, fs, glob } = options;
631
- const command = args[0];
632
- const commandArgs = args.slice(1);
633
- const settings = await loadSettings(ctx.cwd, fs);
1114
+ const { commandArguments, context, fileSystem, glob } = options;
1115
+ const settings = await loadSettings(context.cwd, fileSystem);
634
1116
  const helpText = generateHelpText(settings);
635
- if (isHelpRequest(command)) {
636
- ctx.stdout(helpText);
1117
+ if (isHelpRequest(commandArguments[0])) {
1118
+ context.stdout(helpText);
637
1119
  return { exitCode: 0 };
638
1120
  }
639
- if (!isValidCommand(command)) {
640
- ctx.stderr(`Unknown command: ${command}`);
641
- ctx.stderr(`Run '${settings.dustCommand} help' for available commands`);
1121
+ const { command, remaining } = resolveCommand(commandArguments);
1122
+ if (!command || !isValidCommand(command)) {
1123
+ context.stderr(`Unknown command: ${commandArguments.join(" ")}`);
1124
+ context.stderr(`Run '${settings.dustCommand} help' for available commands`);
642
1125
  return { exitCode: 1 };
643
1126
  }
644
- const deps = {
645
- arguments: commandArgs,
646
- context: ctx,
647
- fileSystem: fs,
1127
+ const dependencies = {
1128
+ arguments: remaining,
1129
+ context,
1130
+ fileSystem,
648
1131
  globScanner: glob,
649
1132
  settings
650
1133
  };
651
- return runCommand(command, deps);
1134
+ return runCommand(command, dependencies);
652
1135
  }
653
1136
 
654
- // lib/cli/entry.ts
655
- var fs = {
656
- exists: existsSync,
657
- readFile: (path) => readFile(path, "utf-8"),
658
- writeFile: (path, content) => writeFile(path, content, "utf-8"),
659
- mkdir: async (path, options) => {
660
- await mkdir(path, options);
661
- },
662
- readdir: (path) => readdir(path)
663
- };
664
- var glob = {
665
- scan: async function* (dir) {
666
- for (const entry of await readdir(dir, { recursive: true })) {
667
- if (entry.endsWith(".md"))
668
- yield entry;
1137
+ // lib/cli/entry-wiring.ts
1138
+ function createFileSystem(primitives) {
1139
+ return {
1140
+ exists: primitives.existsSync,
1141
+ readFile: (path) => primitives.readFile(path, "utf-8"),
1142
+ writeFile: (path, content) => primitives.writeFile(path, content, "utf-8"),
1143
+ mkdir: async (path, options) => {
1144
+ await primitives.mkdir(path, options);
1145
+ },
1146
+ readdir: (path) => primitives.readdir(path),
1147
+ chmod: (path, mode) => primitives.chmod(path, mode)
1148
+ };
1149
+ }
1150
+ function createGlobScanner(readdir) {
1151
+ return {
1152
+ scan: async function* (dir) {
1153
+ for (const entry of await readdir(dir, { recursive: true })) {
1154
+ if (entry.endsWith(".md"))
1155
+ yield entry;
1156
+ }
669
1157
  }
670
- }
671
- };
672
- var result = await main({
673
- args: process.argv.slice(2),
674
- ctx: {
675
- cwd: process.cwd(),
676
- stdout: console.log,
677
- stderr: console.error
678
- },
679
- fs,
680
- glob
681
- });
682
- process.exit(result.exitCode);
1158
+ };
1159
+ }
1160
+ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
1161
+ const fileSystem = createFileSystem(fsPrimitives);
1162
+ const glob = createGlobScanner(fsPrimitives.readdir);
1163
+ const result = await main({
1164
+ commandArguments: processPrimitives.argv.slice(2),
1165
+ context: {
1166
+ cwd: processPrimitives.cwd(),
1167
+ stdout: consolePrimitives.log,
1168
+ stderr: consolePrimitives.error
1169
+ },
1170
+ fileSystem,
1171
+ glob
1172
+ });
1173
+ processPrimitives.exit(result.exitCode);
1174
+ }
1175
+
1176
+ // lib/cli/entry.ts
1177
+ await wireEntry({ existsSync, readFile, writeFile, mkdir, readdir, chmod }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });