English | 中文
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
- 🎯 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
Install dedust as a dependency in your project:
npm install dedustThis allows you to import and use dedust in your JavaScript/TypeScript code:
import dedust from "dedust";Install dedust globally to use it as a command-line tool:
npm install -g dedustAfter global installation, you can run dedust from anywhere:
# Preview what would be deleted (default behavior)
dedust
# Actually delete files
dedust --delete
# Preview specific directories
dedust /path/to/project1 /path/to/project2
# Delete with custom config file
dedust --delete --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 dedustimport dedust 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 dedustResult1 = await dedust(dsl, "/path/to/project");
console.log("Would delete:", dedustResult.targets);
// Or scan multiple directories at once
const dedustResultMultiple = await dedust(dsl, ["/path/to/project1", "/path/to/project2"]);
// Actually delete the files - single directory
const dedustResult2 = await dedust(dsl, "/path/to/project");
const executed = await dedustResult2.execute();
console.log("Deleted:", executed.deleted);
console.log("Errors:", executed.errors);<Action> <Target> [when <Condition>]
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 support glob patterns:
target- Simple directory/file name*.log- All files with .log extension**/*.tmp- All .tmp files recursivelynode_modules- Specific directory name
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/**/*.js
Key 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 *.log
Key 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)
here- Current directory (default, can be omitted)parent- Parent directoryparents- Any ancestor directorychild- Direct child directorychildren- Any descendant directorysibling- Sibling directory
exists <pattern>- Check if pattern existsnot exists <pattern>- Check if pattern doesn't exist
and- Combine multiple conditions
# 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 Makefile
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
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 .git
Then load and execute the rules:
import { readFileSync } from "fs";
import dedust from "dedust";
// Load rules from dedust.rules
const rules = readFileSync("./dedust.rules", "utf-8");
// Preview what would be deleted (dry run)
const result = await dedust(rules, "/path/to/project");
console.log("Would delete:", result.targets);
// Execute cleanup
const executed = await result.execute();
console.log("Deleted:", executed.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
If you've installed dedust globally (with npm install -g dedust), you can use it from the command line.
# Show help
dedust --help
# Show version
dedust --version
# Preview what would be deleted (default behavior)
dedust
# Actually delete files
dedust --delete
# Preview specific directories
dedust /path/to/project
# Delete in multiple directories
dedust --delete /path/to/project1 /path/to/project2 /path/to/project3
# Use a custom config file
dedust --config my-cleanup.rules
# Delete with custom config
dedust --delete --config my-cleanup.rules
# Skip safety validation (use with caution!)
dedust --delete --skip-validation| Option | Alias | Description |
|---|---|---|
--help |
-h |
Show help message |
--version |
-v |
Show version number |
--delete |
-D |
Actually delete files (requires explicit confirmation) |
--config <file> |
-c |
Specify config file (default: dedust.rules) |
--skip-validation |
Skip safety validation (use with caution) |
# 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 (default behavior)
dedust
# If the preview looks good, execute the cleanup
dedust --delete
# Preview with a different config file
dedust --config production.rules
# Delete with a different config file
dedust --delete --config production.rules
# Preview multiple workspaces at once
dedust ~/workspace/project1 ~/workspace/project2 ~/workspace/project3
# Delete in multiple workspaces
dedust --delete ~/workspace/project1 ~/workspace/project2 ~/workspace/project3You can use npx to run dedust without installing it globally:
# Preview what would be deleted (default behavior)
npx dedust
# Actually delete files
npx dedust --delete
# Specify a version
npx dedust@latest --versionThe main function for file cleanup operations. Can perform dry run or actual deletion.
Import:
// Default export
import dedust from "dedust";Parameters:
rulesOrDsl:string | Rule[]- DSL text or parsed rulesbaseDirs:string | string[]- Base directory or directories to processoptions:DedustOptions(optional) - Configuration options
Returns:
DedustResult- Result object with targets and execute method
Examples:
import dedust from "dedust";
const dsl = `
delete target when exists Cargo.toml
delete node_modules when exists package.json
`;
// Dry run (default) - returns array of file paths
const targets = await dedust(dsl, "/path/to/project");
console.log("Would delete:", targets);
// Execute deletion
const result = await dedust(dsl, "/path/to/project");
const stats = await result.execute();
console.log("Deleted:", stats.deleted);
console.log("Errors:", stats.errors);
// Multiple directories
const result2 = await dedust(dsl, ["/path/to/project1", "/path/to/project2"]);
// With options
const result3 = await dedust(dsl, "/path/to/project", {
ignore: [".git", "*.keep"],
skip: ["node_modules"],
onFileDeleted: (data) => console.log("Deleted:", data.path)
});Options:
execute?: boolean- Whether to actually delete files (default:false)ignore?: string[]- Glob patterns to ignore (files won't be matched or deleted)skip?: string[]- Glob patterns to skip during traversal (improves performance)skipValidation?: boolean- Skip safety validation (use with caution)- Event listeners:
onFileFound?: (data) => void- Called when a file is foundonFileDeleted?: (data) => void- Called when a file is deletedonError?: (data) => void- Called when an error occursonScanStart?: (data) => void- Called when scanning startsonScanDirectory?: (data) => void- Called when scanning a directoryonScanComplete?: (data) => void- Called when scanning completes
For advanced customization, you can use the underlying classes directly:
import { Tokenizer, Parser, Evaluator } from "dedust";
// Tokenize DSL
const tokenizer = new Tokenizer(dsl);
const tokens = tokenizer.tokenize();
// Parse tokens into rules
const parser = new Parser(tokens);
const rules = parser.parse();
// Evaluate rules
const evaluator = new Evaluator(rules, "/path/to/project");
// Attach event listeners
evaluator.on("file:found", (data) => {
console.log("Found:", data.path);
});
// Execute
const targets = await evaluator.evaluate(true);
const result = await evaluator.execute(targets);Classes:
Tokenizer- Tokenize DSL text into tokensParser- Parse tokens into rulesEvaluator- Evaluate rules and execute cleanup
const dsl = `
# Ignore version control
ignore .git
ignore .svn
skip node_modules
# 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 dedust(dsl, process.cwd());
const stats = await result.execute();
console.log(`Cleaned up ${stats.deleted.length} items`);// 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";// DSL defines project-level ignore rules
const dsl = `
ignore .git
ignore node_modules
delete *
`;
// API provides runtime-specific ignore rules
const result = await dedust(dsl, "/path/to/project", {
ignore: ["important/**", "*.keep"], // Runtime ignores
});
// Both sets of patterns are merged and applied
// Ignored: .git, node_modules, important/**, *.keep// 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 dedust(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 rule// 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 dedust(dsl, "/path/to/project", {
skip: ["node_modules", "build"], // Can be matched if explicitly targeted
ignore: [".git", "*.keep"], // Never matched under any circumstances
});// 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 dedust(dsl, "/large/workspace");
// Equivalent using API skip patterns
const targets2 = await dedust("delete **/*.tmp delete **/*.log", "/large/workspace", {
skip: ["node_modules", ".git", "build"],
});Full TypeScript definitions are included
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 dedust(dsl, baseDir, { skipValidation: true }); // CLI: Use --skip-validation flag with --delete dedust --delete --skip-validation
-
Clear 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
- Dry run by default -
dedust()always scans first, letting you preview what will be deleted before calling.execute() - 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
-
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 target -
Use ignore rules to protect important files:
# Protect version control and configuration ignore .git ignore .env ignore *.keep # Then use broader cleanup rules delete *.tmp -
Preview before deleting:
# Preview what would be deleted (default behavior) dedust # Then execute if results look correct dedust --delete
-
Use 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 **/*
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.
- 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)
Contributions are welcome! Please feel free to submit a Pull Request.
SEE LICENSE IN LICENSE
Created by Axetroy