dedust
v1.1.0
Published
An elegant file cleanup tool.
Maintainers
Readme
dedust
An elegant file cleanup tool using a simple, human-readable DSL DRL.
Dedust Rule Language (DRL) - A human-readable DSL for defining cleanup rules. The default configuration file is dedust.rules.
See DSL design specifications at spec.md
Features
- 🎯 Simple DSL - Human-readable, line-based cleanup rules
- 🔍 Context-aware - Support for parent, child, sibling, and ancestor directory conditions
- 🌟 Glob patterns - Full support for wildcard patterns (
*.log,**/*.tmp, etc.) - 🚀 Fast & Safe - Dry-run mode by default, explicit deletion when needed
- 📦 Zero config - Works out of the box with sensible defaults
- 🔧 TypeScript - Full TypeScript type definitions included
- 📦 Dual module support - Works with both ESM and CommonJS
Installation
As a Library (for programmatic use)
Install dedust as a dependency in your project:
npm install dedustThis allows you to import and use dedust in your JavaScript/TypeScript code:
import { parseRules, findTargets, executeCleanup } from "dedust";As a Global CLI Tool
Install dedust globally to use it as a command-line tool:
npm install -g dedustAfter global installation, you can run dedust from anywhere:
# Clean current directory using dedust.rules
dedust
# Preview what would be deleted
dedust --dry-run
# Clean specific directories
dedust /path/to/project1 /path/to/project2
# Use custom config file
dedust --config my-rules.txtWhen to use global vs local installation:
- Global installation (
-g): Best for usingdedustas a command-line tool across multiple projects. Thededustcommand becomes available system-wide. - Local installation: Best for integrating
dedustinto your project's code or build scripts. The package is only available within that project.
You can also use npx to run dedust without installing it globally:
npx dedust --dry-runQuick Start
import { parseRules, findTargets, executeCleanup } from "dedust";
// Define cleanup rules
const dsl = `
# Rust projects
delete target when exists Cargo.toml
# Node projects
delete node_modules when exists package.json
# Python projects
delete .venv when exists pyproject.toml
# Log files everywhere
delete *.log
`;
// Find what would be deleted (dry run) - single directory
const targets = await findTargets(dsl, "/path/to/project");
console.log("Would delete:", targets);
// Or scan multiple directories at once
const targets = await findTargets(dsl, ["/path/to/project1", "/path/to/project2"]);
// Actually delete the files - single directory
const result = await executeCleanup(dsl, "/path/to/project");
console.log("Deleted:", result.deleted);
console.log("Errors:", result.errors);DSL Syntax
Basic Rule Structure
<Action> <Target> [when <Condition>]Actions
delete- Delete matching files or directoriesignore- Ignore matching files or directories (exclude from deletion and matching)skip- Skip directory traversal but allow matching (performance optimization)
Targets
Targets support glob patterns:
target- Simple directory/file name*.log- All files with .log extension**/*.tmp- All .tmp files recursivelynode_modules- Specific directory name
Skip vs Ignore Patterns
Skip Patterns - Exclude from traversal but allow matching:
# Skip node_modules traversal (improves performance)
skip node_modules
# But still allow explicit deletion
delete node_modules when exists package.json
# Files inside node_modules won't be found by glob patterns
delete **/*.js # Won't match node_modules/**/*.jsKey features:
- Skip rules prevent directory traversal (performance optimization)
- Skipped directories can still be matched by explicit delete rules
- Supports all glob patterns (e.g.,
node_modules,.cache/**,build*)
Ignore Patterns - Exclude from both traversal and matching:
# Ignore version control directories completely
ignore .git
ignore .svn
# Ignore with glob patterns
ignore node_modules/**
ignore *.keep
# Then define your cleanup rules
delete target when exists Cargo.toml
delete *.logKey features:
- Ignore rules prevent directory traversal (performance optimization)
- Ignored paths cannot be matched by any delete rules
- Supports all glob patterns (e.g.,
*.log,.git/**,important.*) - Can be combined with API-level ignore options
- Ignored directories and their contents are skipped entirely
When to use which:
- Use
skipwhen you want to avoid traversing large directories but still allow explicit deletion (e.g.,skip node_modules+delete node_modules when exists package.json) - Use
ignorewhen you never want to delete something under any circumstances (e.g.,ignore .git)
Conditions
Location Modifiers
here- Current directory (default, can be omitted)parent- Parent directoryparents- Any ancestor directorychild- Direct child directorychildren- Any descendant directorysibling- Sibling directory
Predicates
exists <pattern>- Check if pattern existsnot exists <pattern>- Check if pattern doesn't exist
Logical Operators
and- Combine multiple conditions
Examples
# Ignore version control and dependencies
ignore .git
ignore node_modules
ignore .svn
# Delete target directory when Cargo.toml exists in current directory
delete target when exists Cargo.toml
# Delete target in child crates
delete target when parent exists Cargo.toml
# Delete only if both conditions are met
delete target when exists Cargo.toml and exists src
# Delete unless keep file exists
delete target when exists Cargo.toml and not exists keep.txt
# Delete log files in git repositories (but not .git itself)
ignore .git
delete **/*.log when parents exists .git
# Delete without any condition
delete *.log
# Skip large directories for performance
skip node_modules
skip .git
delete node_modules when exists package.json
delete **/*.log # Won't traverse into node_modules
# Ignore important files completely
ignore *.keep
ignore important/**
delete *.tmp
# Patterns with whitespace (use quotes)
delete "My Documents" when exists "Desktop.ini"
delete "Program Files" when exists "*.dll"
delete 'build output' when exists MakefilePatterns with Whitespace
For file or directory names containing spaces, enclose the pattern in quotes:
// Use double quotes
const dsl = 'delete "My Documents"';
// Or single quotes
const dsl = "delete 'Program Files'";
// Works in conditions too
const dsl = 'delete cache when exists "package.json"';Supported features:
- Single quotes (
'...') or double quotes ("...") - Escape sequences:
\n,\t,\\,\',\" - Both targets and condition patterns can be quoted
Configuration Files
Using dedust.rules
Create a dedust.rules file in your project root to define reusable cleanup rules.
See dedust.rules for a complete example configuration file.
Example configuration:
# dedust.rules - Cleanup configuration for this project
# Skip large directories for performance
skip node_modules
skip .git
# Rust projects
delete target when exists Cargo.toml
delete target when parent exists Cargo.toml
# Node.js projects
delete node_modules when exists package.json
delete .next when exists next.config.js
delete dist when exists package.json
# Python projects
delete .venv when exists pyproject.toml
delete __pycache__
delete .pytest_cache
# Build artifacts and logs
delete *.log
delete **/*.tmp when parents exists .gitThen load and execute the rules:
import { readFileSync } from "fs";
import { executeCleanup, findTargets } from "dedust";
// Load rules from dedust.rules
const rules = readFileSync("./dedust.rules", "utf-8");
// Preview what would be deleted
const targets = await findTargets(rules, "/path/to/project");
console.log("Would delete:", targets);
// Execute cleanup
const result = await executeCleanup(rules, "/path/to/project");
console.log("Deleted:", result.deleted.length, "items");Benefits of using dedust.rules:
- Centralized cleanup configuration
- Version controlled rules
- Easy to share across team members
- Reusable across multiple projects
- Self-documenting cleanup strategy
CLI Usage
If you've installed dedust globally (with npm install -g dedust), you can use it from the command line.
Basic Commands
# Show help
dedust --help
# Show version
dedust --version
# Clean current directory (requires dedust.rules file)
dedust
# Preview what would be deleted (dry run)
dedust --dry-run
# Clean specific directories
dedust /path/to/project
# Clean multiple directories
dedust /path/to/project1 /path/to/project2 /path/to/project3
# Use a custom config file
dedust --config my-cleanup.rules
# Skip safety validation (use with caution!)
dedust --skip-validationCLI Options
| Option | Alias | Description |
| ------------------- | ----- | ------------------------------------------------------- |
| --help | -h | Show help message |
| --version | -v | Show version number |
| --dry-run | -d | Preview what would be deleted without actually deleting |
| --config <file> | -c | Specify config file (default: dedust.rules) |
| --skip-validation | | Skip safety validation (use with caution) |
Example Workflows
# First, create a dedust.rules file in your project
cat > dedust.rules << 'EOF'
# Skip version control
skip .git
# Rust projects
delete target when exists Cargo.toml
# Node.js projects
delete node_modules when exists package.json
delete dist when exists package.json
# Log files
delete **/*.log
EOF
# Preview what would be deleted
dedust --dry-run
# If the preview looks good, execute the cleanup
dedust
# Or use a different config file
dedust --config production.rules --dry-run
# Clean multiple workspaces at once
dedust ~/workspace/project1 ~/workspace/project2 ~/workspace/project3Using with npx (no global installation needed)
You can use npx to run dedust without installing it globally:
# Run dedust directly
npx dedust --dry-run
# Specify a version
npx dedust@latest --versionAPI Reference
Security Validation Functions
validateRules(rules: Rule[]): {valid: boolean, errors: Array<{rule: Rule, error: string}>}
Validate an array of rules for dangerous patterns.
import { parseRules, validateRules } from "dedust";
const rules = parseRules("delete *");
const validation = validateRules(rules);
if (!validation.valid) {
console.error("Validation failed:");
validation.errors.forEach((e) => console.error(e.error));
}validateRule(rule: Rule): {valid: boolean, error: string | null}
Validate a single rule.
import { validateRule } from "dedust";
const rule = { action: "delete", target: "*", condition: null };
const result = validateRule(rule);
if (!result.valid) {
console.error(result.error);
}isDangerousPattern(pattern: string): boolean
Check if a pattern is considered dangerous.
import { isDangerousPattern } from "dedust";
console.log(isDangerousPattern("*")); // true
console.log(isDangerousPattern("**")); // true
console.log(isDangerousPattern("*.log")); // false
console.log(isDangerousPattern("target")); // falseparseRules(input: string): Rule[]
Parse DSL text into an array of rules.
import { parseRules } from "dedust";
const rules = parseRules("delete target when exists Cargo.toml");
console.log(rules);findTargets(rulesOrDsl: string | Rule[], baseDirs: string | string[], options?: CleanupOptions): Promise<string[]>
Find all targets that match the rules (dry run - doesn't delete anything).
Supports both single directory and multiple directories.
import { findTargets } from "dedust";
// Single directory
const targets = await findTargets("delete *.log", "/path/to/project");
console.log("Would delete:", targets);
// Multiple directories
const targets = await findTargets("delete *.log", ["/path/to/project1", "/path/to/project2", "/path/to/project3"]);
console.log("Would delete:", targets);
// With ignore patterns (API-level)
const targets = await findTargets("delete *", "/path/to/project", {
ignore: [".git", "node_modules", "*.keep"],
skipValidation: true, // Required for dangerous patterns
});
console.log("Would delete:", targets);
// With skip patterns (API-level)
const targets = await findTargets("delete **/*.js", "/path/to/project", {
skip: ["node_modules", ".git", "build*"],
});
console.log("Would delete:", targets);
// With both ignore and skip patterns
const targets = await findTargets("delete **/*", "/path/to/project", {
ignore: [".git", "*.keep"],
skip: ["node_modules", "dist"],
skipValidation: true, // Required for dangerous patterns
});
console.log("Would delete:", targets);Options:
ignore?: string[]- Array of patterns to ignore during cleanup. Supports glob patterns like*.log,.git/**,important.*. Ignored paths cannot be matched or deleted.skip?: string[]- Array of patterns to skip during traversal but allow matching. Supports glob patterns likenode_modules,.git/**,build*. Skipped directories won't be traversed (improves performance) but can still be matched by explicit delete rules.skipValidation?: boolean- Skip safety validation. Use with caution! Allows dangerous patterns likedelete *without conditions.
executeCleanup(rulesOrDsl: string | Rule[], baseDirs: string | string[], options?: CleanupOptions): Promise<ExecutionResult>
Execute the rules and actually delete matching files/directories.
Supports both single directory and multiple directories.
import { executeCleanup } from "dedust";
// Single directory
const result = await executeCleanup("delete *.log", "/path/to/project");
console.log("Deleted:", result.deleted);
console.log("Errors:", result.errors);
// Multiple directories
const result = await executeCleanup("delete *.log", ["/path/to/workspace1", "/path/to/workspace2"]);
console.log("Deleted:", result.deleted);
console.log("Errors:", result.errors);
// With ignore patterns (API-level)
const result = await executeCleanup("delete *", "/path/to/project", {
ignore: [".git", "node_modules/**", "*.keep", "important/**"],
skipValidation: true, // Required for dangerous patterns
});
console.log("Deleted:", result.deleted);
// With skip patterns (API-level)
const result = await executeCleanup("delete **/*.tmp", "/path/to/project", {
skip: ["node_modules", ".git", "cache*"],
});
console.log("Deleted:", result.deleted);
// With both ignore and skip patterns
const result = await executeCleanup("delete **/*", "/path/to/project", {
ignore: [".git", "*.keep"],
skip: ["node_modules", "build"],
skipValidation: true, // Required for dangerous patterns
});
console.log("Deleted:", result.deleted);Options:
ignore?: string[]- Array of patterns to ignore during cleanup. Supports glob patterns like*.log,.git/**,important.*. Ignored paths cannot be matched or deleted.skip?: string[]- Array of patterns to skip during traversal but allow matching. Supports glob patterns likenode_modules,.git/**,build*. Skipped directories won't be traversed (improves performance) but can still be matched by explicit delete rules.skipValidation?: boolean- Skip safety validation. Use with caution! Allows dangerous patterns likedelete *without conditions.
Returns:
{
deleted: string[], // Successfully deleted paths
errors: Array<{ // Errors encountered
path: string,
error: Error
}>
}Event-Based API
For real-time feedback during cleanup operations, use the event-based functions:
findTargetsWithEvents(rulesOrDsl, baseDir, listeners): Promise<string[]>
Find targets with event callbacks for real-time feedback.
import { findTargetsWithEvents } from "dedust";
const targets = await findTargetsWithEvents("delete *.log", "/path/to/project", {
onFileFound: (data) => {
console.log("Found:", data.path);
},
onScanStart: (data) => {
console.log(`Scanning ${data.rulesCount} rules...`);
},
onScanComplete: (data) => {
console.log(`Scan complete. Found ${data.filesFound} files.`);
},
});executeCleanupWithEvents(rulesOrDsl, baseDir, listeners): Promise<ExecutionResult>
Execute cleanup with event callbacks.
import { executeCleanupWithEvents } from "dedust";
const result = await executeCleanupWithEvents("delete *.log", "/path/to/project", {
onFileFound: (data) => {
console.log("Found:", data.path);
},
onFileDeleted: (data) => {
console.log("Deleted:", data.path, data.isDirectory ? "(directory)" : "(file)");
},
onError: (data) => {
console.error("Error:", data.error.message, "at", data.path);
},
});Available Event Listeners
| Event Listener | Description | Data Type |
| ----------------- | ----------------------------------- | -------------------- |
| onFileFound | Called when a file is found | FileFoundEvent |
| onFileDeleted | Called when a file is deleted | FileDeletedEvent |
| onError | Called when an error occurs | ErrorEvent |
| onScanStart | Called when scanning starts | ScanStartEvent |
| onScanDirectory | Called when scanning each directory | ScanDirectoryEvent |
| onScanComplete | Called when scanning completes | ScanCompleteEvent |
Multiple Directories
All API functions support scanning multiple directories in a single call. Simply pass an array of directory paths instead of a single string:
import { findTargets, executeCleanup } from "dedust";
const dsl = `
delete target when exists Cargo.toml
delete node_modules when exists package.json
`;
// Scan multiple directories
const targets = await findTargets(dsl, ["/home/user/workspace/project1", "/home/user/workspace/project2", "/home/user/workspace/project3"]);
// Execute cleanup across multiple directories
const result = await executeCleanup(dsl, ["/var/www/app1", "/var/www/app2"]);
console.log(`Cleaned ${result.deleted.length} files across multiple directories`);Benefits:
- Single DSL execution across multiple projects
- Consolidated results
- More efficient than running separately
- Events are emitted for all directories
Advanced Usage
For advanced use cases, you can access the lower-level APIs:
import { Tokenizer, Parser, Evaluator } from "dedust";
// Tokenize DSL text
const tokenizer = new Tokenizer("delete target");
const tokens = tokenizer.tokenize();
// Parse tokens into rules
const parser = new Parser(tokens);
const rules = parser.parse();
// Evaluate rules with direct event handling
const evaluator = new Evaluator(rules, "/path/to/project");
// Attach event listeners
evaluator.on("file:found", (data) => {
console.log("Found:", data.path);
});
evaluator.on("file:deleted", (data) => {
console.log("Deleted:", data.path);
});
evaluator.on("error", (data) => {
console.error("Error:", data.error.message);
});
// Execute
const targets = await evaluator.evaluate();
const result = await evaluator.execute(targets);Real-World Examples
Clean Multiple Project Types
const dsl = `
# Ignore version control
ignore .git
ignore .svn
# Rust workspace cleanup
delete target when exists Cargo.toml
delete target when parent exists Cargo.toml
# Node.js projects
delete node_modules when exists package.json
delete .next when exists next.config.js
delete dist when exists package.json
# Python projects
delete .venv when exists pyproject.toml
delete __pycache__
delete .pytest_cache
# Build artifacts
delete *.log
delete **/*.tmp when parents exists .git
`;
const result = await executeCleanup(dsl, process.cwd());
console.log(`Cleaned up ${result.deleted.length} items`);Selective Cleanup
// Only clean Rust projects with source code
const dsl = "delete target when exists Cargo.toml and exists src";
// Don't clean if keep marker exists
const dsl2 = "delete target when exists Cargo.toml and not exists .keep";Combining DSL and API Ignore Patterns
// DSL defines project-level ignore rules
const dsl = `
ignore .git
ignore node_modules
delete *
`;
// API provides runtime-specific ignore rules
const result = await executeCleanup(dsl, "/path/to/project", {
ignore: ["important/**", "*.keep"], // Runtime ignores
});
// Both sets of patterns are merged and applied
// Ignored: .git, node_modules, important/**, *.keepCombining DSL and API Skip Patterns
// DSL defines project-level skip rules for traversal optimization
const dsl = `
skip node_modules
skip .git
delete node_modules when exists package.json
delete **/*.log
`;
// API provides runtime-specific skip rules
const result = await executeCleanup(dsl, "/path/to/project", {
skip: ["build*", "cache"], // Runtime skip patterns
});
// Both sets of patterns are merged and applied
// Skipped for traversal: node_modules, .git, build*, cache
// But node_modules can still be matched by the explicit delete ruleSkip vs Ignore Patterns
// Skip prevents traversal but allows matching (performance optimization)
const dsl = `
skip node_modules
delete node_modules when exists package.json
delete **/*.js // Won't find files inside node_modules
`;
// Ignore prevents both traversal and matching (complete exclusion)
const dsl2 = `
ignore .git
delete .git // This won't match anything
delete **/* // Won't find anything inside .git
`;
// Use skip for large directories you want to occasionally clean
// Use ignore for directories you never want to touch
const result = await executeCleanup(dsl, "/path/to/project", {
skip: ["node_modules", "build"], // Can be matched if explicitly targeted
ignore: [".git", "*.keep"], // Never matched under any circumstances
});Performance Optimization with Skip Patterns
// Skip large directories to improve performance
const dsl = `
skip node_modules
skip .git
skip build
delete **/*.tmp
delete **/*.log
`;
// Scanning is much faster because skipped directories are not traversed
const targets = await findTargets(dsl, "/large/workspace");
// Equivalent using API skip patterns
const targets2 = await findTargets("delete **/*.tmp delete **/*.log", "/large/workspace", {
skip: ["node_modules", ".git", "build"],
});TypeScript Support
Full TypeScript definitions are included:
import { parseRules, findTargets, ExecutionResult, Rule } from "dedust";
const dsl: string = "delete *.log";
const rules: Rule[] = parseRules(dsl);
const targets: string[] = await findTargets(rules, "/path");
const result: ExecutionResult = await executeCleanup(rules, "/path");Safety Features
Built-in Security Validations
dedust includes automatic safety validations to prevent accidental mass deletion:
Dangerous Pattern Detection - Automatically rejects patterns that could delete all files without conditions:
delete *- Would delete all files in directorydelete **- Would delete all files recursivelydelete *.*- Would delete all files with extensionsdelete **/*- Would delete all files in subdirectoriesdelete **/*.*- Would delete all files with extensions recursively
Safe Patterns - These patterns are always allowed:
- Specific patterns like
delete *.log,delete target,delete node_modules - Dangerous patterns with conditions:
delete * when exists Cargo.toml - All
ignorerules (not subject to validation)
- Specific patterns like
Validation Bypass - For advanced users who understand the risks:
// API: Use skipValidation option await executeCleanup(dsl, baseDir, { skipValidation: true }); // CLI: Use --skip-validation flag dedust --skip-validationClear Error Messages - When validation fails, you get helpful suggestions:
SECURITY VALIDATION FAILED Dangerous pattern detected: 'delete *' without any condition. Suggestions: • Add a condition (e.g., 'when exists Cargo.toml') • Use a more specific pattern (e.g., '*.log' instead of '*') • Use 'ignore' rules to protect important files
Other Safety Features
- Dry run by default -
findTargets()lets you preview what will be deleted - No upward traversal - Rules cannot delete outside the base directory
- Explicit paths - No implicit deletion of system directories
- Error handling - Gracefully handles permission errors and continues
Security Best Practices
Always use conditions for broad patterns:
# Good: Only delete in Rust projects delete target when exists Cargo.toml # Bad: Would delete all 'target' directories everywhere delete targetUse ignore rules to protect important files:
# Protect version control and configuration ignore .git ignore .env ignore *.keep # Then use broader cleanup rules delete *.tmpTest with --dry-run first:
# Always preview before deleting dedust --dry-run # Then execute if results look correct dedustUse specific patterns when possible:
# Good: Specific to what you want to clean delete *.log delete **/*.tmp delete node_modules when exists package.json # Avoid: Too broad without conditions delete * delete **/*
Dedust Rule Language (DRL) Design Principles
The Dedust Rule Language (DRL) follows these core design principles:
- Declarative - Rules describe what to clean, not how
- Human-readable - Close to natural language
- Context-aware - Understands directory relationships
- Safe by default - Requires explicit conditions for cleanup
- Simple & Clear - No complex nesting or hidden behavior
DRL is designed to be: More powerful than glob, simpler than YAML, safer than scripts.
For detailed specifications, see spec.md.
Limitations
- No OR operator (use multiple rules instead)
- No regex patterns (use glob patterns)
- No relative path operators (
../,./) in patterns - Actions are limited to
delete(may be expanded in future)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
SEE LICENSE IN LICENSE
Credits
Created by Axetroy
