npm package: @kitschpatrol/renami
Description: A CLI tool and TypeScript library for config-driven and content-aware automatic filename management.
Author: Eric Mika
Latest version: 0.1.1

# renami

<!-- badges -->

[![NPM Package @kitschpatrol/renami](https://img.shields.io/npm/v/@kitschpatrol/renami.svg)](https://npmjs.com/package/@kitschpatrol/renami)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![CI](https://github.com/kitschpatrol/renami/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/renami/actions/workflows/ci.yml)

<!-- /badges -->

<!-- short-description -->

**A CLI tool and TypeScript library for config-driven and content-aware automatic filename management.**

<!-- /short-description -->

> [!IMPORTANT]
>
> Renami is in early development and specifically targets the author's niche
> use-cases. It will remain zero-versioned until it's reasonably safe and
> intuitive to use. Caveat emptor.

## Overview

Your files already know their names.

Renami generates file names as a function of file content. It provides a configuration-driven and deterministic approach to automated filename management. Specify how you want certain folders of files to be named in the root of your project, and then run `renami` to keep all the filenames consistent and up-to-date.

The tool makes it easy to pull specific metadata from a file's content and pass it into simple string-based templates to rename files.

Think of it as a linter + fixer for file names.

I use it to maintain consistent, content-driven filenames for a large collection of Markdown notes.

Renami provides an application-agnostic foundation for the [Renami Obsidian](https://github.com/kitschpatrol/renami-obsidian) plugin.

Despite being configuration-driven, Renami aspires to use convention over configuration wherever possible. Basic filename hygiene like invalid character removal, Unicode normalization, deduplication, truncation, and (optionally) case transformation are all handled automatically and implicitly.

### Features

- Automatically set file names based on file content on a per-directory basis
  - Set Markdown file names from frontmatter fields via simple string-based templates
  - Set Markdown file name from content with [CSS-like query selectors](https://github.com/syntax-tree/unist-util-select)
  - Set file names from file metadata like creation dates
- Easily extensible / customizable
  - Simple cases can be managed with simple template strings
  - Complex cases are possible by implementing custom handlers right in the TypeScript configuration file
  - Built-in support for additional file types is planned but not promised
- File extension normalization (`.jpeg` → `.jpg`, etc.)
- Unicode normalization
- Max-length name truncation with configurable `...` and support for breaking on words
- Automatic name collision resolution with numeric increments
- All kinds of text case transformations
- Support for formatting numbers and dates
- Idempotent — stable, deterministic file name output across repeat invocations

### Non-Features

- Directory renaming
- File relocation

## Getting started

### Dependencies

The `renami` CLI tool requires Node 20+. The exported APIs are isomorphic, and should work in any relatively recent runtime environment — though in the browser you will need to implement your own file handling and glob matching logic to suit your environment.

Renami is implemented in TypeScript and bundles a complete set of type definitions.

### Installation

Install locally to access the CLI commands in a single project or to import the provided APIs:

```sh
npm install @kitschpatrol/renami
```

Or, install globally for access across your system:

```sh
npm install --global renami
```

See the sections below for details on configuration and available commands.

## Configuration

Renami depends on configuration to describe how it should rename files when it's run. Internally, it uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) to search for and find relevant configuration in the usual locations.

The library also exports a typed configuration factory function to provide type hinting when authoring configurations.

A trivial configuration might look like this:

```ts
// File: "renami.config.ts"
import { defineRenamiConfig } from '@kitschpatrol/renami'

// Typed config factory...
export default defineRenamiConfig({
  rules: [
    {
      // Make all Markdown files kebab case.
      options: {
        caseType: 'kebab',
      },
      pattern: './**/*.md',
    },
  ],
})
```

A more complex configuration file might look like this:

```ts
// File: "renami.config.ts"
import { defineRenamiConfig, transformHelper } from '@kitschpatrol/renami'

// Renami provides factory function helpers for common transform tasks.
const { fileCallback } = transformHelper

export default defineRenamiConfig({
  // Global options become the default for all "rules" below, but may be
  // overridden on a per-rule basis.
  options: {
    dryRun: true,
    maxLength: 50,
  },
  // Each rule targets a group of files with a glob pattern, and specifies how
  // matching filenames should be managed. If a file matches multiple rules, only
  // the LAST rule in the array is applied to the file.
  rules: [
    {
      // Patterns are relative to this config file's location
      pattern: './test/assets/test-basic/**/*',
      // Transform functions take info about a file and return a filename
      // `string`, or `undefined` if no valid transform is possible. Multiple
      // functions can be passed to `transform`, and are evaluated left to
      // right, with the output of one transform passed to the next, unless it
      // returns `undefined`, in which case it's skipped. Additional changes
      // might be made to the filename afterwards depending on 'options'.
      // This one sets the filename to ctime:
      transform: fileCallback(({ fileInfo }) => `I was born at ${fileInfo.ctimeMs}`),
    },
    {
      options: {
        caseType: 'kebab',
      },
      pattern: './test/assets/test-frontmatter/**/*',
      // Transforms can also be simple strings, which are passed into a
      // universal template transformer which provides different template
      // variables for different file types.
      //
      // This one sets filename from the `title` frontmatter field in a Markdown
      // file.
      transform: 'Note-{title}',
    },
    {
      options: {
        caseType: 'kebab',
        // The final file name will be truncated to 15 characters, including the
        // extension
        maxLength: 15,
        truncateOnWordBoundary: false,
      },
      pattern: './test/assets/test-increment/**/*',
      // Example of a simple custom transform function, which takes a context
      // object with info about the file and can do whatever it wants to return
      // a file name string. This function must always be async even if it
      // doesn't actually await anything.
      transform: async (context) =>
        `My file extension is ${context.filePath.ext} and wow what a long name this is!`,
    },
  ],
})
```

Once you have a configuration file, run `renami` from the command line or
import and invoke the `renami()` function in the API to automatically discover and execute
the renaming rules.

### Configuration Options

These may be applied globally at the top level of the configuration file, or on a per-rule basis.

#### `caseType`

Enforce a specific letter casing on the final filenames.

#### `collapseDuplicateWhitespace`

Replace duplicate whitespace with a single space.

#### `collapseSurplusDelimiters`

If a template is missing values and has sections like `bla - - bla - `, this will collapse extra delimiter strings to yield `bla - bla`.

#### `defaultName`

In rare cases where a path contains only unsafe characters, or when no transformations work in strict mode, this default name is used.

#### `delimiter`

The string used to join array values in templates and to collapse surplus delimiters in templates.

#### `dryRun`

When true, files aren't actually renamed; the operation is simulated only.

#### `ignoreFolderNotes`

Ignore notes matching the containing folder name, as may be the case when using the [obsidian-folder-notes](https://github.com/LostPaul/obsidian-folder-notes) plugin.

#### `maxLength`

Maximum number of characters in the file name, including file extension but excluding base path. Any automatic truncation strings or increments will count towards this maximum.

#### `strict`

If no user-provided transformations work (they all return undefined), then use the default name. Otherwise, the original name is preserved. Technically breaks idempotence.

#### `trim`

Trim leading and trailing white space from the file name.

#### `truncateOnWordBoundary`

Try to truncate the file name on a word boundary, which might result in file names shorter than the `maxLength` target.

#### `truncationString`

String (like `'...'`) to use when truncation is needed.

#### `validateInput`

Run checks to make sure the input file list is valid.

#### `validateOutput`

Make sure we're not overwriting a file that wasn't included in the input files.

## Templates

Renami implements a simple string-based filename templating system to cover most simple renaming scenarios. (And more complex behavior can always be implemented in JavaScript or TypeScript through a bespoke `transform` function.)

Generally, `{single brackets}` denote _metadata_ placeholders related to the file, while `{{double brackets}}` denote _content_ placeholders from within the file, which might change subtly depending on file types.

Either template style may contain `|` characters to delimit basic per-placeholder inline transformation commands. More details below.

If a key in a template string placeholder cannot be resolved, it will be replaced with an empty string.

### Supported file types

Renami provides convenient template string behavior based on detected file types.

Currently, only Markdown (`*.md`) support is implemented for string templates. For now, other file types may be renamed by defining a transform function in your Renami configuration.

#### Markdown

Single braces, `{` and `}`, surround accessors to the file's frontmatter object about the file, e.g. `File - {date.created}` or `Meeting about {tags[0]}`.

Double braces, `{{` and `}}`, surround selector queries to an AST associated with the file, e.g. `{{heading}}`.

If no object or selection path can be resolved, then an empty string `''` is returned.

### Inline formatting

Within either single or double brace template keywords, an optional `|` character may be followed with a string to perform keyword-specific formatting. The interpolator will make a best-effort attempt to process the resolved value based on the string provided after the `|`. Multiple formatters may be chained.

Given a string template like `'File {key|format}'`, the `format` string will be tested for a match against the following rules, in order:

#### Case changes

If the format string is a case type name, the content of the template key will be transformed accordingly. This happens before global "options" level case directives, and may be overwritten by global case transformation options.

`'Note - {title|uppercase}'` → `Note - TITLE FROM FRONTMATTER.md`

Supported strings are `'camel'`, `'kebab'`, `'lowercase'`, `'pascal'`, `'preserve'`, `'screaming-kebab'`, `'screaming-snake'`, `'sentence'`, `'slug'`, `'snake'`, `'title'`, `'uppercase'`.

#### Number formatting

Next, renami will attempt to parse the resolved value as a number and format it according to the [numerable](https://github.com/gastonmesseri/numerable) library's format syntax. (Similar to formats specified by [TR35](https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns) / [ICU 67](https://github.com/unicode-org/icu/blob/main/docs/userguide/format_parse/numbers/skeletons.md).)

`I have {count|0,0.00}` → `TK.md`

#### Date formatting

Next, it will attempt to parse the resolved value as a date and format it, using [patterns](https://date-fns.org/v4.1.0/docs/format) based on [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns). (See [here](https://toolboxpro.app/blog/unicode-cheatsheet) for a nice reference.)

`'My Note about {{heading}} - {date|yyyy-MM-dd}'` → `My Note about Stuff - 2025-03-15.md`

#### Truncation

Passing a positive integer will trim the value:

`'Note - {title|2}'` → `Note - Ti.md`

_If none of the above value / format string combinations are valid, then the format string is ignored and the resolved value is returned as-is._

## Usage

### Library

```ts
import { renami } from '@kitschpatrol/renami'

// Rename files based on locally discoverable
// configuration, e.g. `renami.config.ts`
const report = await renami()

console.log(report)

// Or specify configuration inline...
const anotherReport = await renami({
  config: {
    rules: [
      {
        // Make all Markdown files kebab case.
        options: {
          caseType: 'kebab',
        },
        pattern: './**/*.md',
      },
    ],
  },
})

console.log(anotherReport)
```

### CLI

<!-- cli-help -->

#### Command: `renami`

Rename files using config. Searches for a config file if not provided, failing if none is found.

Usage:

```txt
renami [options]
```

| Option              | Description                                                                             | Type      | Default |
| ------------------- | --------------------------------------------------------------------------------------- | --------- | ------- |
| `--config`<br>`-c`  | Path to config file. If not provided, a config file will be searched for automatically. | `string`  |         |
| `--verbose`         | Enable verbose logging.                                                                 | `boolean` | `false` |
| `--help`<br>`-h`    | Show help                                                                               | `boolean` |         |
| `--version`<br>`-v` | Show version number                                                                     | `boolean` |         |

<!-- /cli-help -->

## Background

### Implementation notes

Case sensitivity...

- Will change case as requested, but doesn't allow identically named but differently-cased files.
- [scule](https://unjs.io/packages/scule)

Template expansion...

- [lxghtless/subslate-gist](https://gist.github.com/lxghtless/262c2c1193e2a9055bc7ca4ae9ab5914)
- [josh-hemphill/subslate](https://github.com/josh-hemphill/subslate)
- [sindresorhus/pupa](https://github.com/sindresorhus/pupa)
- [sevensc/typescript-string-operations](https://github.com/sevensc/typescript-string-operations)

Inline formatting syntax...

- [ICU formatting standard (UTS #35)](https://unicode.org/reports/tr35/)
- [FormatJS](https://formatjs.github.io/)

Number formatting...

- [zuji-ts](https://zuji-ts.vercel.app/)
- [Numbro](https://numbrojs.com/)
- [gastonmesseri/numerable](https://github.com/gastonmesseri/numerable)

Renaming...

- [vfile/vfile-rename](https://github.com/vfile/vfile-rename)

Markdown body selection...

- [syntax-tree/unist-util-select](https://github.com/syntax-tree/unist-util-select)
- [yshavit/mdq](https://github.com/yshavit/mdq)

Style...

- Should we use "file name" or "filename"? Usually prefer "filename".
- [StackExchange: filename vs file name](https://english.stackexchange.com/questions/5366/which-is-correct-filename-file-name-or-filename)

### Similar projects

- [F2](https://f2.freshman.tech/)\
  Great! But tricky to integrate in Obsidian because it's implemented in Go.
- [75lb/renamer](https://github.com/75lb/renamer)\
  Close! Depends on Node. Written in JS instead of TS. No config file.
- [Name Mangler](https://manytricks.com/namemangler/)
  Notable in this context for metadata integration.
- [vidir](https://joeyh.name/code/moreutils/)\
  Edit a directory of filenames in your text editor. Part of moreutils. Not exactly similar, but very useful.

## Maintainers

@kitschpatrol

<!-- license -->

## License

[MIT](license.txt) © Eric Mika

<!-- /license -->