@joshski/dust 0.1.55 → 0.1.57
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/cli/types.d.ts +1 -0
- package/dist/dust.js +138 -62
- package/dist/logging/index.d.ts +29 -12
- package/dist/logging/sink.d.ts +25 -20
- package/dist/logging.js +62 -34
- package/dist/workflow-tasks.js +2 -2
- package/lib/istanbul/minimal-reporter.cjs +32 -16
- package/package.json +1 -1
package/dist/cli/types.d.ts
CHANGED
package/dist/dust.js
CHANGED
|
@@ -1177,7 +1177,7 @@ function getLogLines(buffer) {
|
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
1179
|
// lib/bucket/repository.ts
|
|
1180
|
-
import { dirname as
|
|
1180
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
1181
1181
|
|
|
1182
1182
|
// lib/claude/spawn-claude-code.ts
|
|
1183
1183
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
@@ -1595,6 +1595,9 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
|
|
|
1595
1595
|
await dependencies.streamEvents(events, sink, onRawEvent);
|
|
1596
1596
|
}
|
|
1597
1597
|
|
|
1598
|
+
// lib/logging/index.ts
|
|
1599
|
+
import { join as join5 } from "node:path";
|
|
1600
|
+
|
|
1598
1601
|
// lib/logging/match.ts
|
|
1599
1602
|
function parsePatterns(debug) {
|
|
1600
1603
|
if (!debug)
|
|
@@ -1617,41 +1620,47 @@ function formatLine(name, messages) {
|
|
|
1617
1620
|
|
|
1618
1621
|
// lib/logging/sink.ts
|
|
1619
1622
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
1620
|
-
import {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
ready = true;
|
|
1628
|
-
const dir = join5(process.cwd(), "log", "dust");
|
|
1629
|
-
logPath = join5(dir, `${scope}.log`);
|
|
1630
|
-
try {
|
|
1631
|
-
mkdirSync(dir, { recursive: true });
|
|
1632
|
-
} catch {
|
|
1633
|
-
logPath = undefined;
|
|
1634
|
-
}
|
|
1635
|
-
return logPath;
|
|
1636
|
-
}
|
|
1637
|
-
function setLogScope(name) {
|
|
1638
|
-
scope = name;
|
|
1639
|
-
process.env.DEBUG_LOG_SCOPE = name;
|
|
1640
|
-
logPath = undefined;
|
|
1623
|
+
import { dirname } from "node:path";
|
|
1624
|
+
|
|
1625
|
+
class FileSink {
|
|
1626
|
+
logPath;
|
|
1627
|
+
_appendFileSync;
|
|
1628
|
+
_mkdirSync;
|
|
1629
|
+
resolvedPath;
|
|
1641
1630
|
ready = false;
|
|
1631
|
+
constructor(logPath, _appendFileSync = appendFileSync, _mkdirSync = mkdirSync) {
|
|
1632
|
+
this.logPath = logPath;
|
|
1633
|
+
this._appendFileSync = _appendFileSync;
|
|
1634
|
+
this._mkdirSync = _mkdirSync;
|
|
1635
|
+
}
|
|
1636
|
+
ensureLogFile() {
|
|
1637
|
+
if (this.ready)
|
|
1638
|
+
return this.resolvedPath;
|
|
1639
|
+
this.ready = true;
|
|
1640
|
+
this.resolvedPath = this.logPath;
|
|
1641
|
+
try {
|
|
1642
|
+
this._mkdirSync(dirname(this.logPath), { recursive: true });
|
|
1643
|
+
} catch {
|
|
1644
|
+
this.resolvedPath = undefined;
|
|
1645
|
+
}
|
|
1646
|
+
return this.resolvedPath;
|
|
1647
|
+
}
|
|
1648
|
+
write(line) {
|
|
1649
|
+
const path = this.ensureLogFile();
|
|
1650
|
+
if (!path)
|
|
1651
|
+
return;
|
|
1652
|
+
try {
|
|
1653
|
+
this._appendFileSync(path, line);
|
|
1654
|
+
} catch {}
|
|
1655
|
+
}
|
|
1642
1656
|
}
|
|
1643
|
-
var writeToFile = (line) => {
|
|
1644
|
-
const path = ensureLogFile();
|
|
1645
|
-
if (!path)
|
|
1646
|
-
return;
|
|
1647
|
-
try {
|
|
1648
|
-
appendFileSync(path, line);
|
|
1649
|
-
} catch {}
|
|
1650
|
-
};
|
|
1651
1657
|
|
|
1652
1658
|
// lib/logging/index.ts
|
|
1659
|
+
var DUST_LOG_FILE = "DUST_LOG_FILE";
|
|
1653
1660
|
var patterns = null;
|
|
1654
1661
|
var initialized = false;
|
|
1662
|
+
var activeFileSink = null;
|
|
1663
|
+
var ownedDustLogFile = false;
|
|
1655
1664
|
function init() {
|
|
1656
1665
|
if (initialized)
|
|
1657
1666
|
return;
|
|
@@ -1659,12 +1668,26 @@ function init() {
|
|
|
1659
1668
|
const parsed = parsePatterns(process.env.DEBUG);
|
|
1660
1669
|
patterns = parsed.length > 0 ? parsed : null;
|
|
1661
1670
|
}
|
|
1662
|
-
function
|
|
1671
|
+
function enableFileLogs(scope, _sinkForTesting) {
|
|
1672
|
+
const existing = process.env[DUST_LOG_FILE];
|
|
1673
|
+
const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
|
|
1674
|
+
const path = existing ?? join5(logDir, `${scope}.log`);
|
|
1675
|
+
if (!existing) {
|
|
1676
|
+
process.env[DUST_LOG_FILE] = path;
|
|
1677
|
+
ownedDustLogFile = true;
|
|
1678
|
+
}
|
|
1679
|
+
activeFileSink = _sinkForTesting ?? new FileSink(path);
|
|
1680
|
+
}
|
|
1681
|
+
function createLogger(name) {
|
|
1663
1682
|
return (...messages) => {
|
|
1664
1683
|
init();
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1684
|
+
const line = formatLine(name, messages);
|
|
1685
|
+
if (activeFileSink) {
|
|
1686
|
+
activeFileSink.write(line);
|
|
1687
|
+
}
|
|
1688
|
+
if (patterns && matchesAny(name, patterns)) {
|
|
1689
|
+
process.stdout.write(line);
|
|
1690
|
+
}
|
|
1668
1691
|
};
|
|
1669
1692
|
}
|
|
1670
1693
|
|
|
@@ -1742,7 +1765,7 @@ function formatAgentEvent(event) {
|
|
|
1742
1765
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
1743
1766
|
import { readFileSync } from "node:fs";
|
|
1744
1767
|
import os from "node:os";
|
|
1745
|
-
import { dirname, join as join7 } from "node:path";
|
|
1768
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
1746
1769
|
import { fileURLToPath } from "node:url";
|
|
1747
1770
|
|
|
1748
1771
|
// lib/workflow-tasks.ts
|
|
@@ -1887,7 +1910,7 @@ async function next(dependencies) {
|
|
|
1887
1910
|
}
|
|
1888
1911
|
|
|
1889
1912
|
// lib/cli/commands/loop.ts
|
|
1890
|
-
var __dirname2 =
|
|
1913
|
+
var __dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
1891
1914
|
function getDustVersion() {
|
|
1892
1915
|
const candidates = [
|
|
1893
1916
|
join7(__dirname2, "../../../package.json"),
|
|
@@ -1925,8 +1948,7 @@ function formatLoopEvent(event) {
|
|
|
1925
1948
|
case "loop.checking_tasks":
|
|
1926
1949
|
return null;
|
|
1927
1950
|
case "loop.no_tasks":
|
|
1928
|
-
return
|
|
1929
|
-
`;
|
|
1951
|
+
return "\uD83D\uDE34 No tasks available. Sleeping...";
|
|
1930
1952
|
case "loop.tasks_found":
|
|
1931
1953
|
return `✨ Found a task. Going to work!
|
|
1932
1954
|
`;
|
|
@@ -1973,7 +1995,18 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
|
|
|
1973
1995
|
}
|
|
1974
1996
|
var log = createLogger("dust.cli.commands.loop");
|
|
1975
1997
|
var SLEEP_INTERVAL_MS = 30000;
|
|
1998
|
+
var SLEEP_STEP_MS = 1000;
|
|
1976
1999
|
var DEFAULT_MAX_ITERATIONS = 10;
|
|
2000
|
+
async function sleepWithProgress(sleep, totalMs, writeInline, writeLine) {
|
|
2001
|
+
let remainingMs = totalMs;
|
|
2002
|
+
while (remainingMs > 0) {
|
|
2003
|
+
const stepMs = Math.min(SLEEP_STEP_MS, remainingMs);
|
|
2004
|
+
await sleep(stepMs);
|
|
2005
|
+
writeInline(".");
|
|
2006
|
+
remainingMs -= stepMs;
|
|
2007
|
+
}
|
|
2008
|
+
writeLine("");
|
|
2009
|
+
}
|
|
1977
2010
|
async function gitPull(cwd, spawn) {
|
|
1978
2011
|
return new Promise((resolve) => {
|
|
1979
2012
|
const proc = spawn("git", ["pull"], {
|
|
@@ -2127,6 +2160,7 @@ function parseMaxIterations(commandArguments) {
|
|
|
2127
2160
|
return parsed;
|
|
2128
2161
|
}
|
|
2129
2162
|
async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
|
|
2163
|
+
enableFileLogs("loop");
|
|
2130
2164
|
const { context, settings } = dependencies;
|
|
2131
2165
|
const { postEvent } = loopDependencies;
|
|
2132
2166
|
const maxIterations = parseMaxIterations(dependencies.arguments);
|
|
@@ -2172,7 +2206,8 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
2172
2206
|
const result = await runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, iterationOptions);
|
|
2173
2207
|
if (result === "no_tasks") {
|
|
2174
2208
|
log("sleeping, no tasks");
|
|
2175
|
-
|
|
2209
|
+
const writeInline = context.stdoutInline ?? context.stdout;
|
|
2210
|
+
await sleepWithProgress(loopDependencies.sleep, SLEEP_INTERVAL_MS, writeInline, context.stdout);
|
|
2176
2211
|
} else {
|
|
2177
2212
|
completedIterations++;
|
|
2178
2213
|
log(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
|
|
@@ -2380,7 +2415,7 @@ async function addRepository(repository, manager, repoDeps, context) {
|
|
|
2380
2415
|
}
|
|
2381
2416
|
log3(`adding repository ${repository.name}`);
|
|
2382
2417
|
const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
|
|
2383
|
-
await repoDeps.fileSystem.mkdir(
|
|
2418
|
+
await repoDeps.fileSystem.mkdir(dirname3(repoPath), { recursive: true });
|
|
2384
2419
|
if (repoDeps.fileSystem.exists(repoPath)) {
|
|
2385
2420
|
await removeRepository(repoPath, repoDeps.spawn, context);
|
|
2386
2421
|
}
|
|
@@ -3301,6 +3336,7 @@ async function resolveToken(authDeps, context) {
|
|
|
3301
3336
|
}
|
|
3302
3337
|
}
|
|
3303
3338
|
async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
|
|
3339
|
+
enableFileLogs("bucket");
|
|
3304
3340
|
const { context, fileSystem } = dependencies;
|
|
3305
3341
|
const token = await resolveToken(bucketDeps.auth, context);
|
|
3306
3342
|
if (!token) {
|
|
@@ -3591,7 +3627,7 @@ function validateTitleFilenameMatch(filePath, content) {
|
|
|
3591
3627
|
}
|
|
3592
3628
|
|
|
3593
3629
|
// lib/lint/validators/goal-hierarchy.ts
|
|
3594
|
-
import { dirname as
|
|
3630
|
+
import { dirname as dirname4, resolve } from "node:path";
|
|
3595
3631
|
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
3596
3632
|
function validateGoalHierarchySections(filePath, content) {
|
|
3597
3633
|
const violations = [];
|
|
@@ -3608,7 +3644,7 @@ function validateGoalHierarchySections(filePath, content) {
|
|
|
3608
3644
|
function extractGoalRelationships(filePath, content) {
|
|
3609
3645
|
const lines = content.split(`
|
|
3610
3646
|
`);
|
|
3611
|
-
const fileDir =
|
|
3647
|
+
const fileDir = dirname4(filePath);
|
|
3612
3648
|
const parentGoals = [];
|
|
3613
3649
|
const subGoals = [];
|
|
3614
3650
|
let currentSection = null;
|
|
@@ -3706,17 +3742,15 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
3706
3742
|
const violations = [];
|
|
3707
3743
|
const lines = content.split(`
|
|
3708
3744
|
`);
|
|
3745
|
+
const topLevelStructureMessage = "Open Questions must use `### Question?` headings and `#### Option` headings at the top level. Put supporting markdown (including lists and code blocks) under an option heading. Run `dust new idea` to see the expected format.";
|
|
3709
3746
|
let inOpenQuestions = false;
|
|
3710
3747
|
let currentQuestionLine = null;
|
|
3748
|
+
let inOption = false;
|
|
3711
3749
|
let inCodeBlock = false;
|
|
3712
3750
|
for (let i = 0;i < lines.length; i++) {
|
|
3713
3751
|
const line = lines[i];
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
continue;
|
|
3717
|
-
}
|
|
3718
|
-
if (inCodeBlock)
|
|
3719
|
-
continue;
|
|
3752
|
+
const trimmedLine = line.trimEnd();
|
|
3753
|
+
const nonWhitespaceLine = line.trim();
|
|
3720
3754
|
if (line.startsWith("## ")) {
|
|
3721
3755
|
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
3722
3756
|
violations.push({
|
|
@@ -3735,19 +3769,27 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
3735
3769
|
}
|
|
3736
3770
|
inOpenQuestions = line === "## Open Questions";
|
|
3737
3771
|
currentQuestionLine = null;
|
|
3772
|
+
inOption = false;
|
|
3773
|
+
inCodeBlock = false;
|
|
3738
3774
|
continue;
|
|
3739
3775
|
}
|
|
3740
3776
|
if (!inOpenQuestions)
|
|
3741
3777
|
continue;
|
|
3742
|
-
if (
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3778
|
+
if (line.startsWith("```")) {
|
|
3779
|
+
if (!inOption && !inCodeBlock) {
|
|
3780
|
+
violations.push({
|
|
3781
|
+
file: filePath,
|
|
3782
|
+
message: topLevelStructureMessage,
|
|
3783
|
+
line: i + 1
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
inCodeBlock = !inCodeBlock;
|
|
3748
3787
|
continue;
|
|
3749
3788
|
}
|
|
3789
|
+
if (inCodeBlock)
|
|
3790
|
+
continue;
|
|
3750
3791
|
if (line.startsWith("### ")) {
|
|
3792
|
+
inOption = false;
|
|
3751
3793
|
if (currentQuestionLine !== null) {
|
|
3752
3794
|
violations.push({
|
|
3753
3795
|
file: filePath,
|
|
@@ -3755,7 +3797,7 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
3755
3797
|
line: currentQuestionLine
|
|
3756
3798
|
});
|
|
3757
3799
|
}
|
|
3758
|
-
if (!
|
|
3800
|
+
if (!trimmedLine.endsWith("?")) {
|
|
3759
3801
|
violations.push({
|
|
3760
3802
|
file: filePath,
|
|
3761
3803
|
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
@@ -3769,6 +3811,15 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
3769
3811
|
}
|
|
3770
3812
|
if (line.startsWith("#### ")) {
|
|
3771
3813
|
currentQuestionLine = null;
|
|
3814
|
+
inOption = true;
|
|
3815
|
+
continue;
|
|
3816
|
+
}
|
|
3817
|
+
if (nonWhitespaceLine && !inOption) {
|
|
3818
|
+
violations.push({
|
|
3819
|
+
file: filePath,
|
|
3820
|
+
message: topLevelStructureMessage,
|
|
3821
|
+
line: i + 1
|
|
3822
|
+
});
|
|
3772
3823
|
}
|
|
3773
3824
|
}
|
|
3774
3825
|
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
@@ -3802,7 +3853,7 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
|
3802
3853
|
}
|
|
3803
3854
|
|
|
3804
3855
|
// lib/lint/validators/link-validator.ts
|
|
3805
|
-
import { dirname as
|
|
3856
|
+
import { dirname as dirname5, resolve as resolve2 } from "node:path";
|
|
3806
3857
|
var SEMANTIC_RULES = [
|
|
3807
3858
|
{
|
|
3808
3859
|
section: "## Goals",
|
|
@@ -3819,7 +3870,7 @@ function validateLinks(filePath, content, fileSystem) {
|
|
|
3819
3870
|
const violations = [];
|
|
3820
3871
|
const lines = content.split(`
|
|
3821
3872
|
`);
|
|
3822
|
-
const fileDir =
|
|
3873
|
+
const fileDir = dirname5(filePath);
|
|
3823
3874
|
for (let i = 0;i < lines.length; i++) {
|
|
3824
3875
|
const line = lines[i];
|
|
3825
3876
|
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
@@ -3846,7 +3897,7 @@ function validateSemanticLinks(filePath, content) {
|
|
|
3846
3897
|
const violations = [];
|
|
3847
3898
|
const lines = content.split(`
|
|
3848
3899
|
`);
|
|
3849
|
-
const fileDir =
|
|
3900
|
+
const fileDir = dirname5(filePath);
|
|
3850
3901
|
let currentSection = null;
|
|
3851
3902
|
for (let i = 0;i < lines.length; i++) {
|
|
3852
3903
|
const line = lines[i];
|
|
@@ -3897,7 +3948,7 @@ function validateGoalHierarchyLinks(filePath, content) {
|
|
|
3897
3948
|
const violations = [];
|
|
3898
3949
|
const lines = content.split(`
|
|
3899
3950
|
`);
|
|
3900
|
-
const fileDir =
|
|
3951
|
+
const fileDir = dirname5(filePath);
|
|
3901
3952
|
let currentSection = null;
|
|
3902
3953
|
for (let i = 0;i < lines.length; i++) {
|
|
3903
3954
|
const line = lines[i];
|
|
@@ -4136,6 +4187,24 @@ async function lintMarkdown(dependencies) {
|
|
|
4136
4187
|
// lib/cli/commands/check.ts
|
|
4137
4188
|
var log5 = createLogger("dust.cli.commands.check");
|
|
4138
4189
|
var DEFAULT_CHECK_TIMEOUT_MS = 13000;
|
|
4190
|
+
var MAX_OUTPUT_LINES = 500;
|
|
4191
|
+
var KEEP_LINES = 250;
|
|
4192
|
+
function truncateOutput(output) {
|
|
4193
|
+
const lines = output.split(`
|
|
4194
|
+
`);
|
|
4195
|
+
if (lines.length <= MAX_OUTPUT_LINES) {
|
|
4196
|
+
return output;
|
|
4197
|
+
}
|
|
4198
|
+
const snippedCount = lines.length - KEEP_LINES * 2;
|
|
4199
|
+
const firstLines = lines.slice(0, KEEP_LINES);
|
|
4200
|
+
const lastLines = lines.slice(-KEEP_LINES);
|
|
4201
|
+
return [
|
|
4202
|
+
...firstLines,
|
|
4203
|
+
`[...snip ${snippedCount} lines...]`,
|
|
4204
|
+
...lastLines
|
|
4205
|
+
].join(`
|
|
4206
|
+
`);
|
|
4207
|
+
}
|
|
4139
4208
|
async function runSingleCheck(check, cwd, runner) {
|
|
4140
4209
|
const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
4141
4210
|
log5(`running check ${check.name}: ${check.command}`);
|
|
@@ -4215,7 +4284,7 @@ function displayResults(results, context) {
|
|
|
4215
4284
|
context.stdout(`Note: This check was killed after ${result.timeoutSeconds}s. To configure a different timeout, set "timeoutMilliseconds" in the check configuration in .dust/config/settings.json`);
|
|
4216
4285
|
}
|
|
4217
4286
|
if (result.output.trim()) {
|
|
4218
|
-
context.stdout(result.output.trimEnd());
|
|
4287
|
+
context.stdout(truncateOutput(result.output).trimEnd());
|
|
4219
4288
|
}
|
|
4220
4289
|
if (result.hints && result.hints.length > 0) {
|
|
4221
4290
|
context.stdout("");
|
|
@@ -4238,7 +4307,7 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
4238
4307
|
settings
|
|
4239
4308
|
} = dependencies;
|
|
4240
4309
|
const serial = commandArguments.includes("--serial");
|
|
4241
|
-
|
|
4310
|
+
enableFileLogs("check");
|
|
4242
4311
|
if (!settings.checks || settings.checks.length === 0) {
|
|
4243
4312
|
context.stderr("Error: No checks configured in .dust/config/settings.json");
|
|
4244
4313
|
context.stderr("");
|
|
@@ -5125,6 +5194,7 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
|
|
|
5125
5194
|
context: {
|
|
5126
5195
|
cwd: processPrimitives.cwd(),
|
|
5127
5196
|
stdout: consolePrimitives.log,
|
|
5197
|
+
stdoutInline: consolePrimitives.write,
|
|
5128
5198
|
stderr: consolePrimitives.error
|
|
5129
5199
|
},
|
|
5130
5200
|
fileSystem,
|
|
@@ -5140,4 +5210,10 @@ await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFil
|
|
|
5140
5210
|
exit: (code) => {
|
|
5141
5211
|
process.exitCode = code;
|
|
5142
5212
|
}
|
|
5143
|
-
}, {
|
|
5213
|
+
}, {
|
|
5214
|
+
log: console.log,
|
|
5215
|
+
write: (message) => {
|
|
5216
|
+
process.stdout.write(message);
|
|
5217
|
+
},
|
|
5218
|
+
error: console.error
|
|
5219
|
+
});
|
package/dist/logging/index.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal debug logging framework.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* timestamped lines to `<cwd>/log/dust/<scope>.log`.
|
|
4
|
+
* Two independent output channels:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* - **File logging** — activated by `enableFileLogs(scope)` at command startup.
|
|
7
|
+
* Writes all logs to `./log/<scope>.log` by default. Two env vars control routing:
|
|
8
|
+
*
|
|
9
|
+
* Routing rules for enableFileLogs(scope):
|
|
10
|
+
* 1. If DUST_LOG_FILE is already set (inherited from a parent process such as
|
|
11
|
+
* `dust check`), use that path — all scopes land in the same file.
|
|
12
|
+
* 2. Otherwise compute the path from DUST_LOG_DIR (if set) or `<cwd>/log`, set
|
|
13
|
+
* DUST_LOG_FILE so child processes inherit the same destination, then write there.
|
|
14
|
+
*
|
|
15
|
+
* - **Stdout logging** — activated by `DEBUG=pattern`. Writes matching logs to
|
|
16
|
+
* stdout. Works in any command, regardless of whether file logging is enabled.
|
|
10
17
|
*
|
|
11
18
|
* DEBUG is a comma-separated list of match expressions. Each expression
|
|
12
19
|
* can contain `*` as a wildcard (matches any sequence of characters).
|
|
@@ -23,22 +30,32 @@
|
|
|
23
30
|
*
|
|
24
31
|
* No external dependencies.
|
|
25
32
|
*/
|
|
26
|
-
import { type
|
|
27
|
-
export { setLogScope } from './sink';
|
|
33
|
+
import { type LogSink } from './sink';
|
|
28
34
|
export type LogFn = (...messages: unknown[]) => void;
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
36
|
+
* Activate file logging for this command. Determines the log path as follows:
|
|
37
|
+
* - If DUST_LOG_FILE is already set (inherited from a parent process such as
|
|
38
|
+
* `dust check`), use that path — all scopes land in the same file.
|
|
39
|
+
* - Otherwise compute the path using DUST_LOG_DIR (if set) or `<cwd>/log`, set
|
|
40
|
+
* DUST_LOG_FILE so that any child processes inherit the same destination, then write there.
|
|
41
|
+
*
|
|
42
|
+
* Pass a LogSink as the second argument to override for testing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function enableFileLogs(scope: string, _sinkForTesting?: LogSink): void;
|
|
45
|
+
/**
|
|
46
|
+
* Create a named logger function. The returned function writes to:
|
|
47
|
+
* - The active file sink (if `enableFileLogs` was called), always, no filtering.
|
|
48
|
+
* - `process.stdout` if DEBUG is set and `name` matches the pattern.
|
|
32
49
|
*
|
|
33
50
|
* @param name - Logger name, e.g. `dust.bucket.loop`
|
|
34
|
-
* @param write - Override the default file writer (for testing)
|
|
35
51
|
*/
|
|
36
|
-
export declare function createLogger(name: string
|
|
52
|
+
export declare function createLogger(name: string): LogFn;
|
|
37
53
|
/**
|
|
38
|
-
* Check whether a logger name would
|
|
54
|
+
* Check whether a logger name would produce stdout output under the current DEBUG value.
|
|
39
55
|
*/
|
|
40
56
|
export declare function isEnabled(name: string): boolean;
|
|
41
57
|
/**
|
|
42
58
|
* Reset internal state (for testing only).
|
|
59
|
+
* Clears DUST_LOG_FILE only if this module set it (not if it was inherited).
|
|
43
60
|
*/
|
|
44
61
|
export declare function _reset(): void;
|
package/dist/logging/sink.d.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File-based log sink — the imperative shell for debug logging.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Writes log lines to an arbitrary file path, creating the directory lazily
|
|
5
|
+
* on first write. The path is determined by the caller (enableFileLogs in
|
|
6
|
+
* index.ts) rather than this class.
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export declare
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
export interface LogSink {
|
|
9
|
+
write(line: string): void;
|
|
10
|
+
}
|
|
11
|
+
type AppendFileSyncFn = (path: string, data: string) => void;
|
|
12
|
+
type MkdirSyncFn = (path: string, options: {
|
|
13
|
+
recursive: boolean;
|
|
14
|
+
}) => void;
|
|
15
|
+
export declare class FileSink implements LogSink {
|
|
16
|
+
private readonly logPath;
|
|
17
|
+
private readonly _appendFileSync;
|
|
18
|
+
private readonly _mkdirSync;
|
|
19
|
+
private resolvedPath;
|
|
20
|
+
private ready;
|
|
21
|
+
constructor(logPath: string, _appendFileSync?: AppendFileSyncFn, _mkdirSync?: MkdirSyncFn);
|
|
22
|
+
private ensureLogFile;
|
|
23
|
+
/**
|
|
24
|
+
* Write a line to the log file.
|
|
25
|
+
* Silently no-ops if the file cannot be opened.
|
|
26
|
+
*/
|
|
27
|
+
write(line: string): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
package/dist/logging.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// lib/logging/index.ts
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
// lib/logging/match.ts
|
|
2
5
|
function parsePatterns(debug) {
|
|
3
6
|
if (!debug)
|
|
@@ -20,41 +23,47 @@ function formatLine(name, messages) {
|
|
|
20
23
|
|
|
21
24
|
// lib/logging/sink.ts
|
|
22
25
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ready = true;
|
|
31
|
-
const dir = join(process.cwd(), "log", "dust");
|
|
32
|
-
logPath = join(dir, `${scope}.log`);
|
|
33
|
-
try {
|
|
34
|
-
mkdirSync(dir, { recursive: true });
|
|
35
|
-
} catch {
|
|
36
|
-
logPath = undefined;
|
|
37
|
-
}
|
|
38
|
-
return logPath;
|
|
39
|
-
}
|
|
40
|
-
function setLogScope(name) {
|
|
41
|
-
scope = name;
|
|
42
|
-
process.env.DEBUG_LOG_SCOPE = name;
|
|
43
|
-
logPath = undefined;
|
|
26
|
+
import { dirname } from "node:path";
|
|
27
|
+
|
|
28
|
+
class FileSink {
|
|
29
|
+
logPath;
|
|
30
|
+
_appendFileSync;
|
|
31
|
+
_mkdirSync;
|
|
32
|
+
resolvedPath;
|
|
44
33
|
ready = false;
|
|
34
|
+
constructor(logPath, _appendFileSync = appendFileSync, _mkdirSync = mkdirSync) {
|
|
35
|
+
this.logPath = logPath;
|
|
36
|
+
this._appendFileSync = _appendFileSync;
|
|
37
|
+
this._mkdirSync = _mkdirSync;
|
|
38
|
+
}
|
|
39
|
+
ensureLogFile() {
|
|
40
|
+
if (this.ready)
|
|
41
|
+
return this.resolvedPath;
|
|
42
|
+
this.ready = true;
|
|
43
|
+
this.resolvedPath = this.logPath;
|
|
44
|
+
try {
|
|
45
|
+
this._mkdirSync(dirname(this.logPath), { recursive: true });
|
|
46
|
+
} catch {
|
|
47
|
+
this.resolvedPath = undefined;
|
|
48
|
+
}
|
|
49
|
+
return this.resolvedPath;
|
|
50
|
+
}
|
|
51
|
+
write(line) {
|
|
52
|
+
const path = this.ensureLogFile();
|
|
53
|
+
if (!path)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
this._appendFileSync(path, line);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
|
-
var writeToFile = (line) => {
|
|
47
|
-
const path = ensureLogFile();
|
|
48
|
-
if (!path)
|
|
49
|
-
return;
|
|
50
|
-
try {
|
|
51
|
-
appendFileSync(path, line);
|
|
52
|
-
} catch {}
|
|
53
|
-
};
|
|
54
60
|
|
|
55
61
|
// lib/logging/index.ts
|
|
62
|
+
var DUST_LOG_FILE = "DUST_LOG_FILE";
|
|
56
63
|
var patterns = null;
|
|
57
64
|
var initialized = false;
|
|
65
|
+
var activeFileSink = null;
|
|
66
|
+
var ownedDustLogFile = false;
|
|
58
67
|
function init() {
|
|
59
68
|
if (initialized)
|
|
60
69
|
return;
|
|
@@ -62,12 +71,26 @@ function init() {
|
|
|
62
71
|
const parsed = parsePatterns(process.env.DEBUG);
|
|
63
72
|
patterns = parsed.length > 0 ? parsed : null;
|
|
64
73
|
}
|
|
65
|
-
function
|
|
74
|
+
function enableFileLogs(scope, _sinkForTesting) {
|
|
75
|
+
const existing = process.env[DUST_LOG_FILE];
|
|
76
|
+
const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
|
|
77
|
+
const path = existing ?? join(logDir, `${scope}.log`);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
process.env[DUST_LOG_FILE] = path;
|
|
80
|
+
ownedDustLogFile = true;
|
|
81
|
+
}
|
|
82
|
+
activeFileSink = _sinkForTesting ?? new FileSink(path);
|
|
83
|
+
}
|
|
84
|
+
function createLogger(name) {
|
|
66
85
|
return (...messages) => {
|
|
67
86
|
init();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
const line = formatLine(name, messages);
|
|
88
|
+
if (activeFileSink) {
|
|
89
|
+
activeFileSink.write(line);
|
|
90
|
+
}
|
|
91
|
+
if (patterns && matchesAny(name, patterns)) {
|
|
92
|
+
process.stdout.write(line);
|
|
93
|
+
}
|
|
71
94
|
};
|
|
72
95
|
}
|
|
73
96
|
function isEnabled(name) {
|
|
@@ -77,10 +100,15 @@ function isEnabled(name) {
|
|
|
77
100
|
function _reset() {
|
|
78
101
|
initialized = false;
|
|
79
102
|
patterns = null;
|
|
103
|
+
activeFileSink = null;
|
|
104
|
+
if (ownedDustLogFile) {
|
|
105
|
+
delete process.env[DUST_LOG_FILE];
|
|
106
|
+
ownedDustLogFile = false;
|
|
107
|
+
}
|
|
80
108
|
}
|
|
81
109
|
export {
|
|
82
|
-
setLogScope,
|
|
83
110
|
isEnabled,
|
|
111
|
+
enableFileLogs,
|
|
84
112
|
createLogger,
|
|
85
113
|
_reset
|
|
86
114
|
};
|
package/dist/workflow-tasks.js
CHANGED
|
@@ -111,7 +111,7 @@ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSen
|
|
|
111
111
|
return { filePath };
|
|
112
112
|
}
|
|
113
113
|
async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
|
|
114
|
-
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/goals/\` 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 \`#### Option\` headings, and only add questions that are meaningful decisions worth asking.`, [
|
|
114
|
+
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/goals/\` 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.`, [
|
|
115
115
|
"Idea is thoroughly researched with relevant codebase context",
|
|
116
116
|
"Open questions are added for any ambiguous or underspecified aspects",
|
|
117
117
|
"Open questions follow the required heading format and focus on high-value decisions",
|
|
@@ -175,7 +175,7 @@ ${description}
|
|
|
175
175
|
const ideaPath = `.dust/ideas/${ideaFilename}`;
|
|
176
176
|
const content = `# ${taskTitle}
|
|
177
177
|
|
|
178
|
-
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 \`#### Option\` headings, and only add questions that are meaningful decisions worth asking. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context.
|
|
178
|
+
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/goals/\` and \`.dust/facts/\` for relevant context.
|
|
179
179
|
|
|
180
180
|
## Idea Description
|
|
181
181
|
|
|
@@ -21,25 +21,36 @@ function formatMetrics(metrics) {
|
|
|
21
21
|
return parts.join(', ')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function getGapLines(fileCoverage) {
|
|
25
|
+
const gapLines = new Set()
|
|
26
|
+
|
|
25
27
|
const lineCoverage = fileCoverage.getLineCoverage()
|
|
28
|
+
for (const [lineStr, hits] of Object.entries(lineCoverage)) {
|
|
29
|
+
if (hits === 0) gapLines.add(Number.parseInt(lineStr, 10))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { branchMap, b } = fileCoverage
|
|
33
|
+
for (const [id, branch] of Object.entries(branchMap)) {
|
|
34
|
+
if (b[id].some(hits => hits === 0)) {
|
|
35
|
+
gapLines.add(branch.loc.start.line)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sorted = [...gapLines].sort((a, b) => a - b)
|
|
26
40
|
const ranges = []
|
|
27
41
|
let rangeStart = null
|
|
28
42
|
let rangeEnd = null
|
|
29
43
|
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
rangeStart = line
|
|
41
|
-
rangeEnd = line
|
|
42
|
-
}
|
|
44
|
+
for (const line of sorted) {
|
|
45
|
+
if (rangeStart === null) {
|
|
46
|
+
rangeStart = line
|
|
47
|
+
rangeEnd = line
|
|
48
|
+
} else if (line === rangeEnd + 1) {
|
|
49
|
+
rangeEnd = line
|
|
50
|
+
} else {
|
|
51
|
+
ranges.push([rangeStart, rangeEnd])
|
|
52
|
+
rangeStart = line
|
|
53
|
+
rangeEnd = line
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
|
|
@@ -68,7 +79,12 @@ class IncompleteCoverageReporter extends ReportBase {
|
|
|
68
79
|
},
|
|
69
80
|
})
|
|
70
81
|
|
|
71
|
-
if (incompleteFiles.length === 0)
|
|
82
|
+
if (incompleteFiles.length === 0) {
|
|
83
|
+
const cw = context.writer.writeFile(null)
|
|
84
|
+
cw.println('✔ 100% coverage!')
|
|
85
|
+
cw.close()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
const cw = context.writer.writeFile(null)
|
|
74
90
|
const count = incompleteFiles.length
|
|
@@ -78,7 +94,7 @@ class IncompleteCoverageReporter extends ReportBase {
|
|
|
78
94
|
for (const file of incompleteFiles) {
|
|
79
95
|
cw.println('')
|
|
80
96
|
cw.println(`${file.name} (${formatMetrics(file.metrics)})`)
|
|
81
|
-
for (const line of
|
|
97
|
+
for (const line of getGapLines(file.fileCoverage)) {
|
|
82
98
|
cw.println(`- ${line}`)
|
|
83
99
|
}
|
|
84
100
|
}
|