File fragment merger - batteries included, dlx-ready.
Merge files from templates, fragments, and overrides with intelligent strategies for YAML, JSON, GitLab CI, Docker Compose, TypeScript configs, Markdown, text files, and more.
# Using pnpm dlx (recommended)
pnpm dlx @feinarbyte/file-merge
# Using npx
npx @feinarbyte/file-merge
# Or install globally
pnpm add -g @feinarbyte/file-mergeCreate a config file in your project root to customize behavior:
# Create YAML config (recommended)
file-merge init
# Or create JSON config
file-merge init --json# .file-merge.config.yaml
templatesDir: config-templates
# Glob patterns with negation support
fragmentPatterns:
- "**/*.fragment.*" # Include all fragment files
- "!node_modules/**" # Exclude node_modules
- "!dist/**" # Exclude dist
# Advanced: Exclude folder but include specific patterns
# - "!folderA/folderB/**"
# - "folderA/folderB/catalog.*.fragment.*"
ignorePatterns:
- "**/node_modules/**"
- "**/dist/**"
- "**/.git/**"{
"templatesDir": "config-templates",
"fragmentPatterns": [
"**/*.fragment.*",
"!node_modules/**",
"!dist/**"
],
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/.git/**"
]
}noSymlink- Copy single-source outputs instead of creating symlinks (default:false)copyPatterns- Per-file glob patterns (relative to project root) for targets that should be copied instead of symlinked when they have a single source (default:[])replaceArrayPatterns- Per-file glob patterns (relative to project root) for targets whose arrays are replaced by later sources instead of merged as a union (default:[]). See Array mergingtemplatesDir- Directory containing template files with__prefix (default:"atom-framework/config-templates")fragmentPatterns- Glob patterns to discover fragment files. Supports negation with!prefix- Example:
["**/*.fragment.*", "!node_modules/**", "!folderA/**", "folderA/catalog.*.fragment.*"]
- Example:
ignorePatterns- Additional patterns to ignore (applied after fragmentPatterns)modules- Optional module activation filtering (atom-framework specific)activeDir- Directory where active modules are symlinkedsourceDir- Directory containing available modules
watchPatterns- Patterns to watch in watch mode (optional, auto-derived if not set)
By default, targets with a single source (typically a template) are symlinked to keep a single canonical file.
If you need a real file (for example .gitignore in some environments), configure per-target copy rules:
# .file-merge.config.yaml
copyPatterns:
- ".gitignore"File-merge automatically detects and loads config files in this order:
.file-merge.config.yaml(recommended).file-merge.config.yml.file-merge.config.json
If no config file exists, file-merge uses default patterns suitable for the atom-framework structure.
You can specify a custom config file location:
# Use a different config file
file-merge apply --config configs/prod.config.json
# Use a shared config from parent directory
file-merge apply --config ../shared-config.json
# Watch with custom config
file-merge watch --config my-config.jsonfile-merge applyOptions:
--dry-run- Show what would be generated without writing files--check- Check if apply would change files (no writes, exits with code 1 when changes are needed)--verbose- Detailed output--filter <patterns...>- Only process files matching patterns--config <path>- Path to config file (default: auto-detect.yaml,.yml, or.json)
file-merge apply --check--check runs the same merge logic as apply but does not write files.
- Exit code
0: all managed targets already match the expected state. - Exit code
1: one or more managed targets would change. - Output includes
CHANGED <path>lines and a final summary (N changed, M unchanged, T total).
Use --dry-run when you want informational output only without failing automation.
CI example:
file-merge validate
file-merge apply --checkPre-commit hook example (.git/hooks/pre-commit):
#!/bin/sh
set -e
file-merge apply --checkfile-merge watchAutomatically regenerates merged files when source files change.
Options:
--verbose- Detailed output--config <path>- Path to config file (default: auto-detect.yaml,.yml, or.json)
# Analyze existing files
file-merge migrate analyze
# Extract differences into override files
file-merge migrate extract --strategy smartfile-merge validatefile-merge status [file]- Configurable - Customize templates directory, fragment patterns, and module locations via
.file-merge.config.json - Template-based merging - Define templates in configurable templates directory (default:
config-templates/) - Fragment merging - Merge fragments from packages/modules using configurable patterns
- Override support - Override templates with project-specific changes
- Smart merge strategies - Auto-detects merge strategy based on file type
- Template variables - Use
{{VARIABLE}}syntax in filenames and paths (resolved from environment variables) - Module filtering - Only include fragments from active modules (configurable)
- Supported formats: YAML, JSON, TOML, GitLab CI, Docker Compose, TypeScript configs, VS Code tasks, Markdown (
.md,.markdown), text files (.gitignore, .dockerignore), and more
File-merge supports template variables using {{VARIABLE}} syntax:
- In template filenames:
atom-framework/config-templates/__{{ENV}}.yamlβ resolves to{{ENV}}.yamlin project root - In fragment
_targetPath:_targetPath: "config/{{ENV}}.json"β resolves to the target path
Variables are resolved from environment variables. If a required variable is missing, the tool will fail with a clear error message.
If you need a literal {{...}} token in generated output (for another tool to consume), escape it with a leading backslash:
\{{VARIABLE}}β preserved as literal{{VARIABLE}}(no env substitution)
Example:
# Fragment file: packages/my-package/config.fragment.yaml
_targetPath: "config/{{ENV}}/settings.json"
# ... rest of fragment contentIf ENV=production, this fragment will target config/production/settings.json.
Literal placeholder example (e.g. for mise):
[env]
_.path = ['\{{config_root}}/node_modules/.bin']deep-merge- Deep merge for JSON objectsyaml-merge- Deep merge for YAML filestoml-merge- Deep merge for TOML filesgitlab-ci- GitLab CI/CD configuration mergingdocker-compose- Docker Compose file mergingtsconfig- TypeScript config mergingvscode-tasks- VS Code tasks.json mergingproperties-merge- Deterministic Java.propertiesmerge with template/override precedenceappend-lines- Line-by-line appending (for .gitignore, etc.)markdown-concat- Default for.md/.markdown. Joins sources in priority order with one blank linemarkdown-sections- Likemarkdown-concat, but merges a later## Headinginto the earlier section with the same textreplace- Last source wins
deep-merge, yaml-merge and toml-merge merge arrays as a union by default. Each source's array is appended, and items that earlier sources already contributed are skipped. Repeated items within one source are kept, so ordered lists like ["--flag", "a", "--flag", "b"] stay intact.
For files whose arrays are ordered lists, such as command-line arguments, a union is usually wrong. List those targets in replaceArrayPatterns, and a later source's array replaces the earlier one completely:
# .file-merge.config.yaml
replaceArrayPatterns:
- "config/servers.json"A Markdown target such as AGENTS.md works like any other target:
- Template only (
atom-framework/config-templates/__AGENTS.md):AGENTS.mdis a symlink to the template. - Template plus override or fragments:
AGENTS.mdis generated. The template comes first, then fragments by_priority, thenAGENTS.overrides.md.
The generated file starts with an HTML comment, because # is a heading in Markdown:
<!-- π€ GENERATED FILE - DO NOT EDIT DIRECTLY
Edit source files instead:
β’ file://./atom-framework/config-templates/__AGENTS.md
β’ file://./AGENTS.overrides.md
To regenerate: pnpm config:apply -->markdown-concat strips one trailing newline from each source and joins them with a blank line.
Internal whitespace and duplicate lines are kept, and the result ends with exactly one newline.
Sources that are empty or whitespace-only are skipped.
markdown-sections is opt-in. With it, an override can add bullets to an existing template section without repeating the heading:
---
_mergeStrategy: markdown-sections
---
## Learned Workspace Facts
- This project deploys with Helm.Headings match on their trimmed text. Only ## headings are merged, and headings inside fenced code blocks are ignored.
When a list item follows a list item, no blank line is inserted. Everything that doesn't match is appended like markdown-concat.
Selecting the strategy. Overrides and fragments can set _mergeStrategy in front matter. The first source in priority order that sets it wins. An override's front matter is only treated as metadata if it contains _-prefixed keys, and it is removed from the output.
Fragments use front matter for their metadata, for example packages/foo/agents.fragment.md:
---
_targetPath: AGENTS.md
_priority: 50
---
## Foo package
Notes contributed by the foo package.The front matter and the blank lines right after it are not included in the output.
file-merge override AGENTS.md creates AGENTS.overrides.md starting with an explanatory <!-- file-merge override ... --> comment. That comment is not copied into the generated file.
file-merge migrate analyze / extract treat Markdown as text and do a line diff against the template, ignoring the generated header.
Lines added inside an existing ## section are extracted under that heading, and the override then gets _mergeStrategy: markdown-sections.
Lines removed from the template can't be expressed in an override. extract prints a warning for them.
When merging .gitlab-ci.yml using the gitlab-ci strategy, file-merge normally prefixes jobs based on the fragment fileβs folder hierarchy.
You can override this per fragment using __gitlabJobPrefix:
_targetPath: .gitlab-ci.yml
__gitlabJobPrefix: deploy # "" disables prefixing
build:
stage: build
script:
- echo "hello"This project uses mise for tool version management.
# Install tools (node, pnpm)
mise install
# Install dependencies
pnpm install
# Build
pnpm run build
# Watch mode
pnpm run dev
# Tests (builds, then runs node --test)
pnpm testReleases are automated via GitHub Actions (.github/workflows/publish.yml). When a version tag is pushed, the workflow runs the tests and stages the package on npm using npm trusted publishing and npm stage publish, so no npm token is stored in GitHub. The package's settings on npmjs.com list feinarbyte/file-merge with the workflow publish.yml as trusted publisher, allowed to stage but not to publish directly.
A staged version is not public until a maintainer approves it with 2FA (on npmjs.com or via the CLI):
npm stage list @feinarbyte/file-merge
npm stage approve <stage-id># Patch release (bug fixes): 2.0.1 β 2.0.2
pnpm run release:patch
# Minor release (new features): 2.0.1 β 2.1.0
pnpm run release:minor
# Major release (breaking changes): 2.0.1 β 3.0.0
pnpm run release:majorEach release command will:
- Bump the version in
package.json - Create a git commit with message
chore(release): vX.Y.Z - Create a git tag
vX.Y.Z - Push the commit and tag to origin
The GitHub Actions publish workflow then stages the version on npm. Approve it with npm stage approve <stage-id> to make it public.
If you need to publish manually:
# Make sure you're logged in to npm with access to @feinarbyte scope
pnpm login
# Publish (will automatically build via prepublishOnly)
pnpm publish --access publicIf you're using an older version with hardcoded atom-framework/ paths, file-merge will continue to work with the default configuration. To customize for your project:
- Run
file-merge initto create.file-merge.config.json - Edit the config to match your project structure
- Update
templatesDir,fragmentPatterns, andmodulespaths as needed
Example for a different structure:
{
"templatesDir": "config-templates",
"fragmentPatterns": [
"src/**/*.fragment.*",
"libs/**/*.fragment.*"
],
"modules": {
"activeDir": "enabled-modules",
"sourceDir": "src/modules"
}
}MIT