Skip to content

axetroy/dedust

Repository files navigation

dedust

English | 中文

Badge LICENSE Node npm version

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 dedust

This allows you to import and use dedust in your JavaScript/TypeScript code:

import dedust from "dedust";

As a Global CLI Tool

Install dedust globally to use it as a command-line tool:

npm install -g dedust

After 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.txt

When to use global vs local installation:

  • Global installation (-g): Best for using dedust as a command-line tool across multiple projects. The dedust command becomes available system-wide.
  • Local installation: Best for integrating dedust into 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

Quick Start

import 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);

DSL Syntax

Basic Rule Structure

<Action> <Target> [when <Condition>]

Actions

  • delete - Delete matching files or directories
  • ignore - 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 recursively
  • node_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/**/*.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 skip when 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 ignore when 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 directory
  • parents - Any ancestor directory
  • child - Direct child directory
  • children - Any descendant directory
  • sibling - Sibling directory

Predicates

  • exists <pattern> - Check if pattern exists
  • not 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 Makefile

Patterns 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 .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

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

# 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

CLI Options

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)

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 (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/project3

Using with npx (no global installation needed)

You 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 --version

API Reference

dedust(rulesOrDsl, baseDirs, options?)

The 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 rules
  • baseDirs: string | string[] - Base directory or directories to process
  • options: 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 found
    • onFileDeleted?: (data) => void - Called when a file is deleted
    • onError?: (data) => void - Called when an error occurs
    • onScanStart?: (data) => void - Called when scanning starts
    • onScanDirectory?: (data) => void - Called when scanning a directory
    • onScanComplete?: (data) => void - Called when scanning completes

Advanced Classes

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 tokens
  • Parser - Parse tokens into rules
  • Evaluator - Evaluate rules and execute cleanup

Real-World Examples

Clean Multiple Project Types

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`);

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 dedust(dsl, "/path/to/project", {
	ignore: ["important/**", "*.keep"], // Runtime ignores
});

// Both sets of patterns are merged and applied
// Ignored: .git, node_modules, important/**, *.keep

Combining 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 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 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 dedust(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 dedust(dsl, "/large/workspace");

// Equivalent using API skip patterns
const targets2 = await dedust("delete **/*.tmp delete **/*.log", "/large/workspace", {
	skip: ["node_modules", ".git", "build"],
});

TypeScript Support

Full TypeScript definitions are included

Safety Features

Built-in Security Validations

dedust includes automatic safety validations to prevent accidental mass deletion:

  1. Dangerous Pattern Detection - Automatically rejects patterns that could delete all files without conditions:

    • delete * - Would delete all files in directory
    • delete ** - Would delete all files recursively
    • delete *.* - Would delete all files with extensions
    • delete **/* - Would delete all files in subdirectories
    • delete **/*.* - Would delete all files with extensions recursively
  2. 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 ignore rules (not subject to validation)
  3. 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
  4. 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
    

Other Safety Features

  1. Dry run by default - dedust() always scans first, letting you preview what will be deleted before calling .execute()
  2. No upward traversal - Rules cannot delete outside the base directory
  3. Explicit paths - No implicit deletion of system directories
  4. Error handling - Gracefully handles permission errors and continues

Security Best Practices

  1. 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
    
  2. 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
    
  3. Preview before deleting:

    # Preview what would be deleted (default behavior)
    dedust
    
    # Then execute if results look correct
    dedust --delete
  4. 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 **/*
    

Dedust Rule Language (DRL) Design Principles

The Dedust Rule Language (DRL) follows these core design principles:

  1. Declarative - Rules describe what to clean, not how
  2. Human-readable - Close to natural language
  3. Context-aware - Understands directory relationships
  4. Safe by default - Requires explicit conditions for cleanup
  5. 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

About

An elegant file cleanup tool using a simple, human-readable DSL.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Contributors 2

  •  
  •